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

  1. Re-issue the JWT signed with the tenant's current cfg.HMACSecret using HS256, aud="weknora", valid exp and non-empty sub.
  2. Confirm the client sends the token in the correct external user token header.
  3. Check server/client clock synchronization if tokens fail right after issuance.
  4. 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

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


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