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

  1. Verify every server instance uses the same, unchanged encryptionKey passed to the AuthorizationServer — restoring the previous key revives old tokens.
  2. Check client storage/transmission: the refresh token must be sent exactly as received (avoid HTML-escaping, cookie encoding, or DB truncation of '+' and '/' characters).
  3. Widen the storage column to TEXT/VARCHAR(255+) and stop logging or transforming the raw token.
  4. 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

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

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)