Tencent/WeKnora · error
invalid external user id: %v
Error message
invalid external user id: %v
What it means
In internal/middleware/auth.go:600, resolveAPIPrincipal wraps errInvalidExternalUserID with the validateExternalUserID failure for the subject extracted from a verified signed token. Even though the JWT is cryptographically valid, its sub claim is empty, exceeds maxExternalUserIDLen, or contains control characters.
Source
Thrown at internal/middleware/auth.go:600
return types.Principal{}, errMissingDirectHeader
}
return fallback, nil
}
if err := validateExternalUserID(externalUserID); err != nil {
return types.Principal{}, fmt.Errorf("%w: %v", errInvalidExternalUserID, err)
}
return types.Principal{
Type: types.PrincipalAPIExternalUser,
ID: strconv.FormatUint(tenantID, 10) + ":" + externalUserID,
}, nil
case types.APIPrincipalModeSignedToken:
externalUserID, err := verifyExternalUserJWT(header.Get(defaultExternalUserTokenHeader), tenantID, cfg.HMACSecret)
if err != nil || externalUserID == "" {
logger.Warnf(ctx, "invalid external user token for tenant=%d: %v", tenantID, err)
return types.Principal{}, fmt.Errorf("%w: %w", errInvalidExternalUserToken, err)
}
if err := validateExternalUserID(externalUserID); err != nil {
return types.Principal{}, fmt.Errorf("%w: %v", errInvalidExternalUserID, err)
}
return types.Principal{
Type: types.PrincipalAPIExternalUser,
ID: strconv.FormatUint(tenantID, 10) + ":" + externalUserID,
}, nil
default:
return fallback, nil
}
}
func verifyExternalUserJWT(tokenString string, tenantID uint64, secret string) (string, error) {
tokenString = strings.TrimSpace(tokenString)
secret = strings.TrimSpace(secret)
if tokenString == "" {
return "", errors.New("missing external user token")
}
if secret == "" {
return "", errors.New("external user token secret is not configured")View on GitHub (pinned to 988cbb0330)
Solutions
- Ensure the token issuer always sets a non-empty, short, printable sub claim.
- Trim the sub on the issuing side and validate its length before signing.
- If the IDP cannot provide sub, switch the tenant to APIPrincipalModeTenant to skip external-user resolution.
Example fix
// before: token without sub
claims := jwt.MapClaims{"aud": "weknora", "exp": ...}
// after
claims := jwt.MapClaims{"aud": "weknora", "exp": ..., "sub": externalUserID} Defensive patterns
Strategy: validation
Validate before calling
sub, _ := claims["sub"].(string)
sub = strings.TrimSpace(sub)
if sub == "" || len(sub) > maxExternalUserIDLen { // fix at issuance
} Type guard
func hasValidSub(claims jwt.MapClaims) bool {
sub, ok := claims["sub"].(string)
sub = strings.TrimSpace(sub)
return ok && sub != "" && len(sub) <= maxExternalUserIDLen
} Prevention
- Always set sub when minting external-user tokens.
- Validate sub length/characters at your token issuer.
- Test tokens against validateExternalUserID rules in CI.
When it happens
Trigger: APIPrincipalModeSignedToken tenant; verifyExternalUserJWT succeeds but claims["sub"] trims to empty, is longer than maxExternalUserIDLen, or contains bytes <0x20 or 0x7f.
Common situations: Identity provider issues tokens without a sub claim; sub contains an embedded newline from a bad issuer integration; sub is a long composite string exceeding the length cap.
Related errors
- id_token missing sub claim
- missing subject
- principal context is required to authorize OAuth MCP service
- %w: %v
- %w: %w
AI-assisted analysis of Tencent/WeKnora@988cbb0330 (2026-09-02).
Data as JSON: /api/errors/89381c2ca102d47a.
Report an issue: GitHub.