thephpleague/oauth2-server · error · OAuthServerException
invalid_scope
invalid_scope
Error message
The requested scope is invalid, unknown, or malformed
What it means
Thrown when a requested OAuth scope identifier does not resolve to a registered scope entity. The grant calls ScopeRepository::getScopeEntityByIdentifier() for every scope in the request; if the repository returns null (or a non-ScopeEntityInterface value), the scope is rejected as invalid, unknown, or malformed. This enforces that only scopes the server explicitly defines can be granted.
Solutions
- Implement (or fix) ScopeRepository::getScopeEntityByIdentifier to return a ScopeEntityInterface instance for every scope identifier your server supports
- Check the exact scope string in the failing request against the identifiers your repository recognizes (watch for typos, extra whitespace, or unsupported scopes)
- If the scope is legitimate, register it in the repository before clients request it
- If the scope is not needed, remove it from the client's configured/requested scopes
Example fix
// before
public function getScopeEntityByIdentifier($identifier)
{
return null; // always null -> every scope throws invalid_scope
}
// after
public function getScopeEntityByIdentifier($identifier)
{
$scopes = ['basic', 'email', 'profile'];
if (in_array($identifier, $scopes, true)) {
$scope = new ScopeEntity();
$scope->setIdentifier($identifier);
return $scope;
}
return null;
} Defensive patterns
Strategy: validation
Validate before calling
const requested = new URLSearchParams(body).get('scope')?.split(' ') ?? [];
const supported = new Set(['basic', 'email', 'profile']);
const unsupported = requested.filter(s => !supported.has(s));
if (unsupported.length) throw new Error(`Unsupported scopes: ${unsupported.join(', ')}`); Type guard
function isRegisteredScope(scope: string, registry: Map<string, ScopeEntity>): boolean { return registry.has(scope.trim()); } Try / catch
try {
await authorize({ scope: requestedScopes.join(' ') });
} catch (e) {
if (e.code === 'invalid_scope') {
console.error(`Scope rejected: ${e.hint ?? e.message}`);
}
throw e;
} Prevention
- Register every scope your clients may request in the scope repository
- Log the exact scope string from failing requests to spot typos/whitespace
- Keep client-configured default scopes in sync with server-side scope definitions
- Trim and normalize scope strings before sending
When it happens
Trigger: Any grant flow (authorization code, access token, refresh token, device code) where the request's scope parameter contains an identifier that the application's scope repository does not recognize, e.g. 'scope=email profile' where 'profile' has no scope entity registered.
Common situations: Developers forget to implement/return entities in getScopeEntityByIdentifier, change default scopes in a client without registering them server-side, copy scope names from another project, or rename scopes during a refactor so stored client scopes no longer match the repository.
Understand the failure class
Background: Invalid enum value errors: "Unknown type", "Invalid scope", "must be one of" — when a string is not on the library's allowed list — this error's family across 23 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/38ccac20c7e0b27a.
Report an issue: GitHub.
Appendix: source
Thrown at src/Grant/AbstractGrant.php:273
* @throws OAuthServerException
*
* @return ScopeEntityInterface[]
*/
public function validateScopes(string|array|null $scopes, ?string $redirectUri = null): array
{
if ($scopes === null) {
$scopes = [];
} elseif (is_string($scopes)) {
$scopes = $this->convertScopesQueryStringToArray($scopes);
}
$validScopes = [];
foreach ($scopes as $scopeItem) {
$scope = $this->scopeRepository->getScopeEntityByIdentifier($scopeItem);
if ($scope instanceof ScopeEntityInterface === false) {
throw OAuthServerException::invalidScope($scopeItem, $redirectUri);
}
$validScopes[] = $scope;
}
return $validScopes;
}
/**
* Converts a scopes query string to an array to easily iterate for validation.
*
* @return string[]
*/
private function convertScopesQueryStringToArray(string $scopes): array
{
return array_filter(explode(self::SCOPE_DELIMITER_STRING, trim($scopes)), static fn ($scope) => $scope !== '');
}
View on GitHub (pinned to 9d2f6fc0a0)