thephpleague/oauth2-server · error · OAuthServerException
5
5
Error message
The requested scope is invalid, unknown, or malformed
What it means
OAuthServerException::invalidScope is thrown when a refresh-token grant request asks for a scope that was not present on the original access/refresh token. The OAuth spec allows a refreshed token to carry the original scopes or fewer, never new ones, so RefreshTokenGrant.php:68 rejects any requested scope identifier not found in the old refresh token's stored scope list. This is a client-side request problem, not a server fault.
Solutions
- Remove the scope parameter from the refresh-token request entirely so the new token inherits the original scopes.
- If a subset is needed, send only scope identifiers that exist in the original token's grant (intersect client-requested scopes with the previously issued ones).
- To genuinely add scopes, redirect the user through the full authorization-code flow again with the expanded scope list.
- Log the offending scope identifier (it is included in the exception) and compare it against the scopes stored when the refresh token was issued.
Example fix
// before: requesting a new scope during refresh POST /token grant_type=refresh_token&refresh_token=...&scope=read+write // after: omit scope (inherits original) or re-authorize for new scopes POST /token grant_type=refresh_token&refresh_token=...
Defensive patterns
Strategy: try-catch
Validate before calling
// client-side: filter requested scopes to those originally granted
const grantedScopes = ['read'];
const requested = new URLSearchParams({ scope: 'read write' });
const safeScopes = requested.get('scope').split(' ').filter(s => grantedScopes.includes(s));
if (safeScopes.length !== requested.get('scope').split(' ').length) {
// need full re-authorization to gain new scopes
} Type guard
function isScopeSubset(requested, granted) {
return Array.isArray(requested) && Array.isArray(granted) &&
requested.every(s => typeof s === 'string' && granted.includes(s));
} Try / catch
try {
$tokens = $server->respondToAccessTokenRequest($request, $response, $ttl);
} catch (OAuthServerException $e) {
if ($e->getCode() === 5) {
// drop the scope parameter or restart authorization-code flow
}
throw $e;
} Prevention
- Omit the scope parameter on refresh requests to inherit the original grant
- Persist the scopes issued at authorization time and intersect before re-requesting
- Route scope upgrades through the full authorization-code flow, never the refresh grant
When it happens
Trigger: Calling POST /token with grant_type=refresh_token and a scope parameter containing a scope identifier that was not granted in the original authorization. This happens when the client appends new scopes to the refresh request (e.g. upgrading 'read' to 'read write') instead of re-running the full authorization-code flow.
Common situations: A frontend adds a scope after a feature update; a mobile app hardcodes a broader scope list than the one issued at first login; a library upgrade starts forwarding scope parameters it previously omitted; admin changes the client's allowed scopes but the refresh request still asks for the old, now-removed scope.
Understand the failure class
- 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/dba1beb3856cd10c.
Report an issue: GitHub.
Appendix: source
Thrown at src/Grant/RefreshTokenGrant.php:68
DateInterval $accessTokenTTL
): ResponseTypeInterface {
// Validate request
$client = $this->validateClient($request);
$oldRefreshToken = $this->validateOldRefreshToken($request, $client->getIdentifier());
$scopes = $this->validateScopes(
$this->getRequestParameter(
'scope',
$request,
implode(self::SCOPE_DELIMITER_STRING, $oldRefreshToken['scopes'])
)
);
// The OAuth spec says that a refreshed access token can have the original scopes or fewer so ensure
// the request doesn't include any new scopes
foreach ($scopes as $scope) {
if (in_array($scope->getIdentifier(), $oldRefreshToken['scopes'], true) === false) {
throw OAuthServerException::invalidScope($scope->getIdentifier());
}
}
$userId = $oldRefreshToken['user_id'];
if (is_int($userId)) {
$userId = (string) $userId;
}
$scopes = $this->scopeRepository->finalizeScopes($scopes, $this->getIdentifier(), $client, $userId);
// Expire old tokens
$this->accessTokenRepository->revokeAccessToken($oldRefreshToken['access_token_id']);
if ($this->revokeRefreshTokens) {
$this->refreshTokenRepository->revokeRefreshToken($oldRefreshToken['refresh_token_id']);
}
// Issue and persist new access token
$accessToken = $this->issueAccessToken($accessTokenTTL, $client, $userId, $scopes);View on GitHub (pinned to 9d2f6fc0a0)