{"record":{"id":"25d2a7b4f64fa104","repo":"thephpleague/oauth2-server","slug":"8-the-refresh-token-is-invalid","errorCode":"8","errorMessage":"The refresh token is invalid.","messagePattern":"The refresh token is invalid\\.","errorType":"http","errorClass":"OAuthServerException","httpStatus":400,"severity":"error","filePath":"src/Grant/RefreshTokenGrant.php","lineNumber":115,"sourceCode":"\n        return $responseType;\n    }\n\n    /**\n     * @throws OAuthServerException\n     *\n     * @return array<string, mixed>\n     */\n    protected function validateOldRefreshToken(ServerRequestInterface $request, string $clientId): array\n    {\n        $encryptedRefreshToken = $this->getRequestParameter('refresh_token', $request)\n            ?? throw OAuthServerException::invalidRequest('refresh_token');\n\n        // Validate refresh token\n        try {\n            $refreshToken = $this->decrypt($encryptedRefreshToken);\n        } catch (Exception $e) {\n            throw OAuthServerException::invalidRefreshToken('Cannot decrypt the refresh token', $e);\n        }\n\n        $refreshTokenData = json_decode($refreshToken, true);\n        if ($refreshTokenData['client_id'] !== $clientId) {\n            $this->getEmitter()->emit(new RequestEvent(RequestEvent::REFRESH_TOKEN_CLIENT_FAILED, $request));\n            throw OAuthServerException::invalidRefreshToken('Token is not linked to client');\n        }\n\n        if ($refreshTokenData['expire_time'] < time()) {\n            throw OAuthServerException::invalidRefreshToken('Token has expired');\n        }\n\n        if ($this->refreshTokenRepository->isRefreshTokenRevoked($refreshTokenData['refresh_token_id']) === true) {\n            throw OAuthServerException::invalidRefreshToken('Token has been revoked');\n        }\n\n        return $refreshTokenData;\n    }","sourceCodeStart":97,"sourceCodeEnd":133,"githubUrl":"https://github.com/thephpleague/oauth2-server/blob/9d2f6fc0a0b5aa1bb02506971d3a4ecff2c6526c/src/Grant/RefreshTokenGrant.php#L97-L133","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before: key changed on some nodes\nnew AuthorizationServer($clients, $tokenRepo, $scopeRepo, 'private.key', $newKey);\n\n// after: share one stable key across the fleet\n$encryptionKey = file_get_contents('/etc/oauth/encryption.key');\nnew AuthorizationServer($clients, $tokenRepo, $scopeRepo, 'private.key', $encryptionKey);","handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"try {\n  $tokens = $server->respondToAccessTokenRequest($request, $response, $ttl);\n} catch (OAuthServerException $e) {\n  if ($e->getCode() === 8) {\n    // invalid refresh token: discard it and re-authenticate\n    $this->tokenStore->forget('refresh_token');\n    return $this->redirectToLogin();\n  }\n  throw $e;\n}","preventionTips":["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"],"tags":["oauth2","refresh-token","decryption","php","league-oauth2-server"],"backgroundTag":"oauth-token-exchange-failed","analyzedSha":"9d2f6fc0a0b5aa1bb02506971d3a4ecff2c6526c","analyzedAt":"2026-09-15T22:33:30.452Z","contentChangedAt":"2026-09-15T22:33:30.452Z","schemaVersion":2},"datasetVersion":"2026-09-16T04:17:20.429Z"}