thephpleague/oauth2-server · error · OAuthServerException
3
3
Error message
The request is missing a required parameter, includes an invalid parameter value, includes a parameter more than once, or is otherwise malformed.
What it means
OAuthServerException::invalidRequest('refresh_token') is thrown when the refresh-token grant request has no refresh_token parameter at all (error code 3, 'invalid_request'). validateOldRefreshToken reads the refresh_token request parameter and, if absent, immediately throws this exception because a refresh grant cannot proceed without the token to decrypt.
Solutions
- Ensure the token refresh HTTP call includes refresh_token=<stored value> in the application/x-www-form-urlencoded body.
- Check client-side storage for the refresh token before calling the endpoint; do not call the refresh endpoint if it is missing (redirect to login instead).
- If using fetch/axios, verify the body is URL-encoded form data, not raw JSON, unless the server is configured for JSON.
- Log the outgoing request body (minus secrets) to confirm the parameter name is exactly 'refresh_token'.
Example fix
// before
const res = await fetch('/token', { method: 'POST', body: { grantType: 'refresh_token' } });
// after
const body = new URLSearchParams({ grant_type: 'refresh_token', refresh_token: storedRefreshToken });
const res = await fetch('/token', { method: 'POST', body }); Defensive patterns
Strategy: validation
Validate before calling
// before calling the token endpoint
if (!storedRefreshToken || typeof storedRefreshToken !== 'string' || storedRefreshToken.length < 16) {
// skip refresh; go straight to login flow
} Type guard
function hasRefreshToken(v) {
return typeof v === 'string' && v.trim().length > 0;
} Try / catch
try {
$tokens = $server->respondToAccessTokenRequest($request, $response, $ttl);
} catch (OAuthServerException $e) {
if ($e->getCode() === 3) {
// refresh_token param missing: force re-authentication
}
throw $e;
} Prevention
- Always build the refresh request body with URLSearchParams / http_build_query so the parameter cannot be dropped
- Never invoke the refresh endpoint without first verifying the token is present in storage
- Send application/x-www-form-urlencoded bodies, matching PSR-7 parsed-body expectations
When it happens
Trigger: POST /token with grant_type=refresh_token but the body omits the refresh_token parameter — e.g. an empty form body, the token was never persisted client-side, or the parameter is sent under a different name.
Common situations: Client stores the refresh token but a refactor drops it from the token-refresh call; a proxy or middleware strips the body; storage layer returns null/undefined for the stored refresh token and it is serialized away; sending JSON body while the server expects application/x-www-form-urlencoded.
Understand the failure class
Background: "missing required argument" and "the following required arguments were not provided": what required-argument errors mean and how to fix them — this error's family across 20 libraries.
- Parsing and encoding errors: unexpected token, malformed input — why parsers reject input and how to find the real culprit.
Related errors
AI-assisted analysis of thephpleague/oauth2-server@9d2f6fc0a0 (2026-09-15).
Data as JSON: /api/errors/408e9293251b6a67.
Report an issue: GitHub.
Appendix: source
Thrown at src/Grant/RefreshTokenGrant.php:109
$refreshToken = $this->issueRefreshToken($accessToken);
if ($refreshToken !== null) {
$this->getEmitter()->emit(new RequestRefreshTokenEvent(RequestEvent::REFRESH_TOKEN_ISSUED, $request, $refreshToken));
$responseType->setRefreshToken($refreshToken);
}
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');
}
View on GitHub (pinned to 9d2f6fc0a0)