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

  1. Ensure the token refresh HTTP call includes refresh_token=<stored value> in the application/x-www-form-urlencoded body.
  2. 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).
  3. If using fetch/axios, verify the body is URL-encoded form data, not raw JSON, unless the server is configured for JSON.
  4. 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

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.

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)