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

  1. Ensure the token issuer always sets a non-empty, short, printable sub claim.
  2. Trim the sub on the issuing side and validate its length before signing.
  3. 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

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


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