thephpleague/oauth2-server · error · OAuthServerException

Access token could not be verified

Error message

Access token could not be verified

What it means

The JWT was parsed but failed Lcobucci JWT validation constraints (signature verification, expiry, audience, etc.). Thrown as access_denied('Access token could not be verified') wrapping RequiredConstraintsViolated.

Solutions

  1. Ensure the same signing/public key pair used to issue tokens is configured in the resource server's AuthorizationServer/BearerTokenValidator.
  2. Re-issue a fresh access token and retry (expiry or signature mismatch).
  3. Check clock skew; add leeway if servers' clocks drift.
  4. Verify environment (keys, issuer/audience) matches where the token was issued.

Example fix

// before
// resource server uses stale public key
$server = new AuthorizationServer(..., new CryptKey('file://old-public.pem'));
// after
$server = new AuthorizationServer(..., new CryptKey('file://current-public.pem', null, false));
Defensive patterns

Strategy: try-catch

Validate before calling

// no pre-call validation possible for signature/expiry; ensure fresh token issued with matching keys
if (time() >= $claimsExpiryFromLocalDecode) { $accessToken = $this->refresh(); }

Try / catch

try { $request = $validator->validateAuthorization($request); } catch (OAuthServerException $e) {
  // 401 + WWW-Authenticate: Bearer so client refreshes the token
  return $e->generateHttpResponse(new Response(), 401);
}

Prevention

When it happens

Trigger: Token signed with a key different from the server's cryptographic key; expired token (outside leeway); audience/issuer mismatch; tampered token.

Common situations: Rotated private key on server while clients hold old tokens; clock skew between issuer and resource server; copying tokens across environments (staging token used in production).

Related errors


AI-assisted analysis of thephpleague/oauth2-server@9d2f6fc0a0 (2026-09-15). Data as JSON: /api/errors/00b44de45b20ebfd. Report an issue: GitHub.

Appendix: source

Thrown at src/AuthorizationValidators/BearerTokenValidator.php:121

        $jwt = trim((string) preg_replace('/^\s*Bearer\s/i', '', $header[0]));

        if ($jwt === '') {
            throw OAuthServerException::accessDenied('Missing "Bearer" token');
        }

        try {
            // Attempt to parse the JWT
            $token = $this->jwtConfiguration->parser()->parse($jwt);
        } catch (Exception $exception) {
            throw OAuthServerException::accessDenied($exception->getMessage(), null, $exception);
        }

        try {
            // Attempt to validate the JWT
            $constraints = $this->jwtConfiguration->validationConstraints();
            $this->jwtConfiguration->validator()->assert($token, ...$constraints);
        } catch (RequiredConstraintsViolated $exception) {
            throw OAuthServerException::accessDenied('Access token could not be verified', null, $exception);
        }

        if (!$token instanceof UnencryptedToken) {
            throw OAuthServerException::accessDenied('Access token is not an instance of UnencryptedToken');
        }

        $claims = $token->claims();

        // Check if token has been revoked
        if ($this->accessTokenRepository->isAccessTokenRevoked($claims->get('jti'))) {
            throw OAuthServerException::accessDenied('Access token has been revoked');
        }

        // Return the request with additional attributes
        return $request
            ->withAttribute('oauth_access_token_id', $claims->get('jti'))
            ->withAttribute('oauth_client_id', $claims->get('aud')[0])
            ->withAttribute('oauth_user_id', $claims->get('sub'))

View on GitHub (pinned to 9d2f6fc0a0)