{"record":{"id":"dba1beb3856cd10c","repo":"thephpleague/oauth2-server","slug":"5-the-requested-scope-is-invalid-unknown-or-malformed","errorCode":"5","errorMessage":"The requested scope is invalid, unknown, or malformed","messagePattern":"The requested scope is invalid, unknown, or malformed","errorType":"http","errorClass":"OAuthServerException","httpStatus":400,"severity":"error","filePath":"src/Grant/RefreshTokenGrant.php","lineNumber":68,"sourceCode":"        DateInterval $accessTokenTTL\n    ): ResponseTypeInterface {\n        // Validate request\n        $client = $this->validateClient($request);\n        $oldRefreshToken = $this->validateOldRefreshToken($request, $client->getIdentifier());\n\n        $scopes = $this->validateScopes(\n            $this->getRequestParameter(\n                'scope',\n                $request,\n                implode(self::SCOPE_DELIMITER_STRING, $oldRefreshToken['scopes'])\n            )\n        );\n\n        // The OAuth spec says that a refreshed access token can have the original scopes or fewer so ensure\n        // the request doesn't include any new scopes\n        foreach ($scopes as $scope) {\n            if (in_array($scope->getIdentifier(), $oldRefreshToken['scopes'], true) === false) {\n                throw OAuthServerException::invalidScope($scope->getIdentifier());\n            }\n        }\n\n        $userId = $oldRefreshToken['user_id'];\n        if (is_int($userId)) {\n            $userId = (string) $userId;\n        }\n\n        $scopes = $this->scopeRepository->finalizeScopes($scopes, $this->getIdentifier(), $client, $userId);\n\n        // Expire old tokens\n        $this->accessTokenRepository->revokeAccessToken($oldRefreshToken['access_token_id']);\n        if ($this->revokeRefreshTokens) {\n            $this->refreshTokenRepository->revokeRefreshToken($oldRefreshToken['refresh_token_id']);\n        }\n\n        // Issue and persist new access token\n        $accessToken = $this->issueAccessToken($accessTokenTTL, $client, $userId, $scopes);","sourceCodeStart":50,"sourceCodeEnd":86,"githubUrl":"https://github.com/thephpleague/oauth2-server/blob/9d2f6fc0a0b5aa1bb02506971d3a4ecff2c6526c/src/Grant/RefreshTokenGrant.php#L50-L86","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before: requesting a new scope during refresh\nPOST /token  grant_type=refresh_token&refresh_token=...&scope=read+write\n\n// after: omit scope (inherits original) or re-authorize for new scopes\nPOST /token  grant_type=refresh_token&refresh_token=...","handlingStrategy":"try-catch","validationCode":"// client-side: filter requested scopes to those originally granted\nconst grantedScopes = ['read'];\nconst requested = new URLSearchParams({ scope: 'read write' });\nconst safeScopes = requested.get('scope').split(' ').filter(s => grantedScopes.includes(s));\nif (safeScopes.length !== requested.get('scope').split(' ').length) {\n  // need full re-authorization to gain new scopes\n}","typeGuard":"function isScopeSubset(requested, granted) {\n  return Array.isArray(requested) && Array.isArray(granted) &&\n    requested.every(s => typeof s === 'string' && granted.includes(s));\n}","tryCatchPattern":"try {\n  $tokens = $server->respondToAccessTokenRequest($request, $response, $ttl);\n} catch (OAuthServerException $e) {\n  if ($e->getCode() === 5) {\n    // drop the scope parameter or restart authorization-code flow\n  }\n  throw $e;\n}","preventionTips":["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"],"tags":["oauth2","scope","refresh-token-grant","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"}