passbolt/passbolt_api · error · BadRequestException
Unable to decode JWT token.
Error message
Unable to decode JWT token.
What it means
After extracting the id_token string, BaseIdToken decodes and verifies it with firebase/jwt (JWT::decode using the provider's verification keys). Any failure — bad signature, expired token, malformed JWT — is caught and rethrown as BadRequestException('Unable to decode JWT token.') with the original exception chained. When `passbolt.plugins.sso.debugEnabled` is set, the raw token is logged before throwing.
Solutions
- Enable `passbolt.plugins.sso.debugEnabled` to log the failing token and inspect its payload at jwt.io (check exp, alg, iss).
- Synchronize server clock via NTP — expired/nbf claims are the most frequent decode failure.
- Clear any cached JWKS/verification keys so the provider's current signing keys are fetched.
- Confirm the token is passed unmodified (no truncation, URL-decoding, or quoting issues) from client to server.
Defensive patterns
Strategy: try-catch
Validate before calling
$parts = explode('.', $idToken);
if (count($parts) !== 3) {
throw new RuntimeException('Malformed JWT: expected 3 dot-separated segments');
} Type guard
function looksLikeJwt(mixed $token): bool {
return is_string($token) && preg_match('/^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]*$/', $token) === 1;
} Try / catch
try {
$idTokenObj = new AzureIdToken($provider, ['id_token' => $jwt]);
} catch (BadRequestException $e) {
// getPrevious() holds JWT::decode failure: signature/exp/alg — inspect it
} Prevention
- Keep server clocks NTP-synchronized; exp/nbf failures dominate decode errors
- Fetch JWKS keys fresh on failure; don't cache provider signing keys indefinitely
- Pass the JWT string through unmodified (no URL-encoding or truncation)
When it happens
Trigger: JWT::decode throwing during __construct: signature verification failure (wrong JWKS/keys), token expired (exp in the past), malformed token string, or unsupported algorithm (e.g. RS256 token validated with HS256 key material).
Common situations: Clock skew making freshly issued tokens appear expired; provider rotated signing keys and cached JWKS is stale; token truncated/URL-encoded in transit; testing with a token from a different provider/environment.
Understand the failure class
Background: "Invalid JSON response" and "Failed to parse response" errors: when an API answers 200 but the body isn't the JSON your library expected — this error's family across 28 libraries.
- Parsing and encoding errors: unexpected token, malformed input — why parsers reject input and how to find the real culprit.
Related errors
- JWT token is missing.
- No claims
- The aud (client id) parameter is invalid.
- The email claim is not found or invalid.
- The iss (issuer) parameter does not match.
AI-assisted analysis of passbolt/passbolt_api@31c1bbc10f (2026-09-17).
Data as JSON: /api/errors/06b932ba6e3d1ef6.
Report an issue: GitHub.
Appendix: source
Thrown at plugins/PassboltEe/Sso/src/Utility/OpenId/BaseIdToken.php:83
unset($this->values['id_token']);
$keys = $provider->getJwtVerificationKeys();
try {
/**
* To fix "Firebase\JWT\BeforeValidException: Cannot handle token prior" error.
*
* @link https://github.com/googleapis/google-api-php-client/issues/1630
* @link https://stackoverflow.com/questions/53658600/uncaught-exception-firebase-jwt-beforevalidexception-with-message-cannot-hand
*/
JWT::$leeway = Configure::read('passbolt.plugins.sso.security.jwtLeeway');
$tokenClaims = (array)JWT::decode($this->idToken, $keys);
} catch (Exception $exception) {
if (Configure::read('passbolt.plugins.sso.debugEnabled')) {
Log::error('idToken => ' . json_encode($this->idToken));
}
throw new BadRequestException(__('Unable to decode JWT token.'), 400, $exception);
}
try {
$this->assertTokenClaims($tokenClaims);
} catch (BadRequestException $exception) {
if (Configure::read('passbolt.plugins.sso.debugEnabled')) {
Log::error('tokenClaims => ' . json_encode($tokenClaims));
}
throw $exception;
}
$this->idTokenClaims = $tokenClaims;
}
/**
* Validate the access token claims from an access token you received in your application.
* Note: nbf and exp claims are validated in JWT::decodeView on GitHub (pinned to 31c1bbc10f)