thephpleague/oauth2-server · error · OAuthServerException
8
8
Error message
The refresh token is invalid.
What it means
OAuthServerException::invalidRefreshToken('Cannot decrypt the refresh token', e) is thrown when $this->decrypt() fails on the supplied refresh_token (error code 8). The refresh token is an encrypted JSON payload; any tampering, corruption, truncation, or key mismatch makes decryption impossible, so the server treats the token as invalid.
Solutions
- Verify every server instance uses the same, unchanged encryptionKey passed to the AuthorizationServer — restoring the previous key revives old tokens.
- Check client storage/transmission: the refresh token must be sent exactly as received (avoid HTML-escaping, cookie encoding, or DB truncation of '+' and '/' characters).
- Widen the storage column to TEXT/VARCHAR(255+) and stop logging or transforming the raw token.
- If the key truly rotated, force users through re-authorization and issue fresh token pairs.
Example fix
// before: key changed on some nodes
new AuthorizationServer($clients, $tokenRepo, $scopeRepo, 'private.key', $newKey);
// after: share one stable key across the fleet
$encryptionKey = file_get_contents('/etc/oauth/encryption.key');
new AuthorizationServer($clients, $tokenRepo, $scopeRepo, 'private.key', $encryptionKey); Defensive patterns
Strategy: try-catch
Try / catch
try {
$tokens = $server->respondToAccessTokenRequest($request, $response, $ttl);
} catch (OAuthServerException $e) {
if ($e->getCode() === 8) {
// invalid refresh token: discard it and re-authenticate
$this->tokenStore->forget('refresh_token');
return $this->redirectToLogin();
}
throw $e;
} Prevention
- Keep the encryptionKey stable and identical across all server instances (store in a shared secret file)
- Pass the token through untouched: avoid HTML entities, cookie mangling, and log redaction that alters characters
- Store refresh tokens in a TEXT/255+ column to prevent silent truncation
When it happens
Trigger: Sending a refresh token that was encrypted with a different encryption key than the one currently configured (encryptionKey changed), a token that was URL-decoded/encoded incorrectly and got mangled (e.g. '+' turned into a space), a truncated token stored in a column/cookie too small, or an outright forged token.
Common situations: Rotating the league/oauth2-server encryption key in production invalidating all outstanding refresh tokens; storing tokens in a VARCHAR column that silently truncates; copying tokens through logs and losing characters; multiple server instances with mismatched keys.
Understand the failure class
- Authentication and authorization failures — expired tokens, bad credentials, and missing scopes.
Related errors
AI-assisted analysis of thephpleague/oauth2-server@9d2f6fc0a0 (2026-09-15).
Data as JSON: /api/errors/25d2a7b4f64fa104.
Report an issue: GitHub.
Appendix: source
Thrown at src/Grant/RefreshTokenGrant.php:115
return $responseType;
}
/**
* @throws OAuthServerException
*
* @return array<string, mixed>
*/
protected function validateOldRefreshToken(ServerRequestInterface $request, string $clientId): array
{
$encryptedRefreshToken = $this->getRequestParameter('refresh_token', $request)
?? throw OAuthServerException::invalidRequest('refresh_token');
// Validate refresh token
try {
$refreshToken = $this->decrypt($encryptedRefreshToken);
} catch (Exception $e) {
throw OAuthServerException::invalidRefreshToken('Cannot decrypt the refresh token', $e);
}
$refreshTokenData = json_decode($refreshToken, true);
if ($refreshTokenData['client_id'] !== $clientId) {
$this->getEmitter()->emit(new RequestEvent(RequestEvent::REFRESH_TOKEN_CLIENT_FAILED, $request));
throw OAuthServerException::invalidRefreshToken('Token is not linked to client');
}
if ($refreshTokenData['expire_time'] < time()) {
throw OAuthServerException::invalidRefreshToken('Token has expired');
}
if ($this->refreshTokenRepository->isRefreshTokenRevoked($refreshTokenData['refresh_token_id']) === true) {
throw OAuthServerException::invalidRefreshToken('Token has been revoked');
}
return $refreshTokenData;
}View on GitHub (pinned to 9d2f6fc0a0)