{"record":{"id":"3c1ac805a0b58571","repo":"Tencent/WeKnora","slug":"missing-subject","errorCode":null,"errorMessage":"missing subject","messagePattern":"missing subject","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/middleware/auth.go","lineNumber":654,"sourceCode":"\t\treturn \"\", errors.New(\"invalid external user token\")\n\t}\n\texp, err := claims.GetExpirationTime()\n\tif err != nil || exp == nil {\n\t\treturn \"\", errors.New(\"missing expiration\")\n\t}\n\tif time.Until(exp.Time) > maxExternalUserTokenTTL {\n\t\treturn \"\", fmt.Errorf(\"token lifetime exceeds %s\", maxExternalUserTokenTTL)\n\t}\n\tif nbf, nbfErr := claims.GetNotBefore(); nbfErr == nil && nbf != nil && time.Now().Before(nbf.Time) {\n\t\treturn \"\", errors.New(\"token not yet valid\")\n\t}\n\tif got := principalTenantIDFromClaims(claims); got != tenantID {\n\t\treturn \"\", fmt.Errorf(\"workspace mismatch: got %d want %d\", got, tenantID)\n\t}\n\tsub, _ := claims[\"sub\"].(string)\n\tsub = strings.TrimSpace(sub)\n\tif sub == \"\" {\n\t\treturn \"\", errors.New(\"missing subject\")\n\t}\n\treturn sub, nil\n}\n\nfunc validateExternalUserID(id string) error {\n\tid = strings.TrimSpace(id)\n\tif id == \"\" {\n\t\treturn errors.New(\"empty external user id\")\n\t}\n\tif len(id) > maxExternalUserIDLen {\n\t\treturn fmt.Errorf(\"external user id too long (max %d)\", maxExternalUserIDLen)\n\t}\n\tfor _, r := range id {\n\t\tif r < 0x20 || r == 0x7f {\n\t\t\treturn errors.New(\"external user id contains invalid characters\")\n\t\t}\n\t}\n\treturn nil","sourceCodeStart":636,"sourceCodeEnd":672,"githubUrl":"https://github.com/Tencent/WeKnora/blob/988cbb03305e055d8ebb7d46d9ac6cc0803cd074/internal/middleware/auth.go#L636-L672","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before: issuer omits subject\nclaims := jwt.MapClaims{\"iss\": \"partner-idp\", \"tenant\": tenantID, \"exp\": time.Now().Add(time.Hour).Unix()}\n// after: include the external user id as subject\nclaims := jwt.MapClaims{\"iss\": \"partner-idp\", \"sub\": externalUserID, \"tenant\": tenantID, \"exp\": time.Now().Add(time.Hour).Unix()}","handlingStrategy":"validation","validationCode":"// verify sub before using the token\nparts := strings.Split(token, \".\")\npayload, _ := base64.RawURLEncoding.DecodeString(parts[1])\nvar c map[string]any\njson.Unmarshal(payload, &c)\nsub, _ := c[\"sub\"].(string)\nif strings.TrimSpace(sub) == \"\" {\n    return errors.New(\"token has empty sub; fix issuer\")\n}","typeGuard":"func hasSubject(claims jwt.MapClaims) bool {\n    sub, _ := claims[\"sub\"].(string)\n    return strings.TrimSpace(sub) != \"\"\n}","tryCatchPattern":"if err != nil && strings.Contains(err.Error(), \"missing subject\") {\n    log.Error(\"external token lacks sub claim; check issuer config\")\n    return 401\n}","preventionTips":["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."],"tags":["auth","jwt","validation"],"backgroundTag":"jwt-missing-subject-claim","analyzedSha":"988cbb03305e055d8ebb7d46d9ac6cc0803cd074","analyzedAt":"2026-09-02T14:41:08.344Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}