Tencent/WeKnora · error

missing subject

Error message

missing subject

What it means

verifyExternalUserJWT requires a non-empty 'sub' (subject) claim identifying the external user. After validating expiry, lifetime, nbf, and tenant/workspace match, it trims the sub string and rejects the token if nothing remains. Without a subject the middleware cannot attribute the request to a user.

Source

Thrown at internal/middleware/auth.go:654

		return "", errors.New("invalid external user token")
	}
	exp, err := claims.GetExpirationTime()
	if err != nil || exp == nil {
		return "", errors.New("missing expiration")
	}
	if time.Until(exp.Time) > maxExternalUserTokenTTL {
		return "", fmt.Errorf("token lifetime exceeds %s", maxExternalUserTokenTTL)
	}
	if nbf, nbfErr := claims.GetNotBefore(); nbfErr == nil && nbf != nil && time.Now().Before(nbf.Time) {
		return "", errors.New("token not yet valid")
	}
	if got := principalTenantIDFromClaims(claims); got != tenantID {
		return "", fmt.Errorf("workspace mismatch: got %d want %d", got, tenantID)
	}
	sub, _ := claims["sub"].(string)
	sub = strings.TrimSpace(sub)
	if sub == "" {
		return "", errors.New("missing subject")
	}
	return sub, nil
}

func validateExternalUserID(id string) error {
	id = strings.TrimSpace(id)
	if id == "" {
		return errors.New("empty external user id")
	}
	if len(id) > maxExternalUserIDLen {
		return fmt.Errorf("external user id too long (max %d)", maxExternalUserIDLen)
	}
	for _, r := range id {
		if r < 0x20 || r == 0x7f {
			return errors.New("external user id contains invalid characters")
		}
	}
	return nil

View on GitHub (pinned to 988cbb0330)

Solutions

  1. Fix the token issuer so every external user token carries a non-empty sub claim with the external user ID.
  2. Inspect the failing token's payload (decode without verification) to confirm sub is missing or empty.
  3. If migrating issuers, map the identifier (e.g. user ID or UUID) into sub before forwarding the token.
  4. Reject such tokens at the issuer with a validation step so invalid tokens never reach the API.

Example fix

// before: issuer omits subject
claims := jwt.MapClaims{"iss": "partner-idp", "tenant": tenantID, "exp": time.Now().Add(time.Hour).Unix()}
// after: include the external user id as subject
claims := jwt.MapClaims{"iss": "partner-idp", "sub": externalUserID, "tenant": tenantID, "exp": time.Now().Add(time.Hour).Unix()}
Defensive patterns

Strategy: validation

Validate before calling

// verify sub before using the token
parts := strings.Split(token, ".")
payload, _ := base64.RawURLEncoding.DecodeString(parts[1])
var c map[string]any
json.Unmarshal(payload, &c)
sub, _ := c["sub"].(string)
if strings.TrimSpace(sub) == "" {
    return errors.New("token has empty sub; fix issuer")
}

Type guard

func hasSubject(claims jwt.MapClaims) bool {
    sub, _ := claims["sub"].(string)
    return strings.TrimSpace(sub) != ""
}

Try / catch

if err != nil && strings.Contains(err.Error(), "missing subject") {
    log.Error("external token lacks sub claim; check issuer config")
    return 401
}

Prevention

When it happens

Trigger: Calling resolveAPIPrincipal with an external user JWT that omits the 'sub' claim entirely, sets it to "", or sets it to whitespace only. Common with tokens minted by external IdPs or custom issuers that forget the subject claim.

Common situations: Misconfigured external token issuer that only sets tenant/email claims; tokens minted for service accounts without a user subject; JWT libraries dropping empty claims during serialization.

Related errors


AI-assisted analysis of Tencent/WeKnora@988cbb0330 (2026-09-02). Data as JSON: /api/errors/3c1ac805a0b58571. Report an issue: GitHub.