apache/pulsar · error · AuthenticationException

Failed to authentication token: ${e.getMessage()}

Error message

Failed to authentication token: ${e.getMessage()}

What it means

authenticateToken wraps any JwtException from the jjwt parser into an AuthenticationException with this message. The underlying JwtException covers expired tokens, bad signatures, malformed JWTs, missing required claims, and null audience. If the exception is an ExpiredJwtException the provider additionally records a token-expired metric before rethrowing.

Source

Thrown at pulsar-broker-common/src/main/java/org/apache/pulsar/broker/authentication/AuthenticationProviderToken.java:273

                                "Audiences in token: [" + object + "] not contains this broker: " + audience);
                    }
                } else {
                    // should not reach here.
                    incrementFailureMetric(ErrorCode.INVALID_AUDIENCES);
                    throw new AuthenticationException("Audiences in token is not in expected format: " + object);
                }
            }

            var expiration = jwt.getBody().getExpiration();
            var tokenRemainingDurationMs = expiration != null ? expiration.getTime() - new Date().getTime() : null;
            authenticationMetricsToken.recordTokenDuration(tokenRemainingDurationMs);
            return jwt;
        } catch (JwtException e) {
            if (e instanceof ExpiredJwtException) {
                authenticationMetricsToken.recordTokenExpired();
            }
            incrementFailureMetric(ErrorCode.INVALID_TOKEN);
            throw new AuthenticationException("Failed to authentication token: " + e.getMessage());
        }
    }

    private String getPrincipal(Jws<Claims> jwt) {
        try {
            return jwt.getBody().get(roleClaim, String.class);
        } catch (RequiredTypeException requiredTypeException) {
            Collection list = jwt.getBody().get(roleClaim, Collection.class);
            Optional<String> firstEntry = list.stream().findFirst().map(Object::toString);
            return firstEntry.orElse(null);
        }
    }

    /**
     * Try to get the validation key for tokens from several possible config options.
     */
    private Key getValidationKey(ServiceConfiguration conf) throws IOException {
        String tokenSecretKey = (String) conf.getProperty(confTokenSecretKeySettingName);

View on GitHub (pinned to 820761864e)

Solutions

  1. Read the wrapped cause message (e.getMessage()) to identify whether it is expiration, signature, or malformed-token, then address specifically.
  2. If expired, issue a fresh token and ensure the client refreshes tokens before exp (set a TTL with margin).
  3. If a signature error, confirm the broker's tokenSecretKey/tokenPublicKey matches the key used to sign client tokens.
  4. If malformed, re-copy the token carefully (it must be the raw base64url JWT, usually supplied via the token parameter in client conf).

Example fix

// before: expired cached token reused by client
AuthenticationException: Failed to authentication token: JWT expired at 2026-09-05T00:00:00Z
// after: refresh token before expiry
if (Instant.now().isAfter(tokenExpiry.minus(5, MINUTES))) { token = issueNewToken(); }
Defensive patterns

Strategy: try-catch

Validate before calling

// Pre-check expiry before sending the token:
String[] parts = token.split("\\.");
org.json.JSONObject claims = new org.json.JSONObject(
    new String(java.util.Base64.getUrlDecoder().decode(parts[1])));
long exp = claims.optLong("exp", Long.MAX_VALUE);
if (System.currentTimeMillis() / 1000 >= exp) { refreshToken(); }

Type guard

static boolean tokenLooksUsable(String token) {
    if (token == null) return false;
    String[] parts = token.split("\\.");
    return parts.length == 3 && parts[0].matches("[A-Za-z0-9_-]+")
        && parts[1].matches("[A-Za-z0-9_-]+") && parts[2].matches("[A-Za-z0-9_-]+");
}

Try / catch

try {
    String role = authProvider.authenticate(authData);
} catch (AuthenticationException e) {
    String msg = e.getMessage();
    if (msg != null && msg.contains("expired")) {
        refreshTokenAndRetry();
    } else if (msg != null && (msg.contains("signature") || msg.contains("JWT signature"))) {
        throw new IllegalStateException("Token signed with wrong key; check tokenSecretKey/tokenPublicKey", e);
    } else {
        throw new IllegalStateException("Malformed or invalid token", e);
    }
}

Prevention

When it happens

Trigger: jwt parser.parseClaimsJws(token) throws: an ExpiredJwtException (token past its exp), SignatureException (signed with the wrong key), MalformedJwtException (corrupted/invalid token string), or MissingClaimException / null audience as constructed in this method.

Common situations: Client presents a token signed with a key that no longer matches the broker's tokenSecretKey/tokenPublicKey; token TTL expired and the client hasn't refreshed it; token string truncated or with extra characters copied into configuration; broker switched between symmetric and asymmetric keys without updating clients.

Understand the failure class

Related errors


AI-assisted analysis of apache/pulsar@820761864e (2026-09-06). Data as JSON: /api/errors/31c9314004afd3c7. Report an issue: GitHub.