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 nilView on GitHub (pinned to 988cbb0330)
Solutions
- Fix the token issuer so every external user token carries a non-empty sub claim with the external user ID.
- Inspect the failing token's payload (decode without verification) to confirm sub is missing or empty.
- If migrating issuers, map the identifier (e.g. user ID or UUID) into sub before forwarding the token.
- 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
- Add issuer-side validation rejecting tokens without sub at mint time.
- Include sub in token contract tests for external IdP integrations.
- Reject empty-claim tokens during CI against the IdP's staging issuer.
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
- token not yet valid
- empty external user id
- external user id contains invalid characters
- invalid external user id: %v
- invite code has expired
AI-assisted analysis of Tencent/WeKnora@988cbb0330 (2026-09-02).
Data as JSON: /api/errors/3c1ac805a0b58571.
Report an issue: GitHub.