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

  1. Enable `passbolt.plugins.sso.debugEnabled` to log the failing token and inspect its payload at jwt.io (check exp, alg, iss).
  2. Synchronize server clock via NTP — expired/nbf claims are the most frequent decode failure.
  3. Clear any cached JWKS/verification keys so the provider's current signing keys are fetched.
  4. 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

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.

Related errors


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::decode

View on GitHub (pinned to 31c1bbc10f)