Tencent/WeKnora · error · errInvalidExternalUserToken
%w: %w
Error message
%w: %w
What it means
In internal/middleware/auth.go:597, resolveAPIPrincipal (APIPrincipalModeSignedToken) wraps errInvalidExternalUserToken with the JWT verification failure from verifyExternalUserJWT. The signed external-user token is missing, malformed, fails signature/audience/expiration checks, or verifies to an empty subject, so the principal cannot be resolved.
Source
Thrown at internal/middleware/auth.go:597
externalUserID := strings.TrimSpace(header.Get(defaultExternalUserIDHeader))
if externalUserID == "" {
if cfg.RequireDirectHeader {
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")View on GitHub (pinned to 988cbb0330)
Solutions
- Re-issue the JWT signed with the tenant's current cfg.HMACSecret using HS256, aud="weknora", valid exp and non-empty sub.
- Confirm the client sends the token in the correct external user token header.
- Check server/client clock synchronization if tokens fail right after issuance.
- Use errors.Is(err, errInvalidExternalUserToken) to detect this family and log the wrapped cause.
Example fix
// before: token signed with old secret after rotation
tok := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{"aud": "weknora", "exp": ..., "sub": uid})
s, _ := tok.SignedString([]byte(oldSecret))
// after
s, _ := tok.SignedString([]byte(tenant.APIPrincipalConfig.HMACSecret)) Defensive patterns
Strategy: try-catch
Validate before calling
if strings.TrimSpace(tokenHeader) == "" { // ensure token present before calling
// obtain a fresh signed token first
} Try / catch
if err != nil {
if errors.Is(err, errInvalidExternalUserToken) {
// mint a new HS256 token (aud=weknora, exp<=TTL, sub set) and retry
}
} Prevention
- Keep token signing secret in sync with tenant APIPrincipalConfig.HMACSecret.
- Set aud="weknora", exp within TTL, and non-empty sub at issuance.
- Refresh tokens proactively before expiry; monitor server-side Warnf logs.
When it happens
Trigger: Tenant configured with APIPrincipalModeSignedToken; the external user token header (defaultExternalUserTokenHeader) is missing, the HS256 HMAC signature doesn't match cfg.HMACSecret, audience isn't "weknora", token expired/no exp claim, or sub claim is empty.
Common situations: Token signed with a different secret after rotating HMACSecret; wrong audience claim; client sending the raw API key instead of the JWT; clock skew making a just-issued token appear expired; token issued for a different workspace.
Related errors
- id_token missing sub claim
- invalid external user id: %v
- OIDC provider returned no user claims
- token not yet valid
- missing subject
AI-assisted analysis of Tencent/WeKnora@988cbb0330 (2026-09-02).
Data as JSON: /api/errors/01a67513ff014b48.
Report an issue: GitHub.