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

  1. Implement (or fix) ScopeRepository::getScopeEntityByIdentifier to return a ScopeEntityInterface instance for every scope identifier your server supports
  2. Check the exact scope string in the failing request against the identifiers your repository recognizes (watch for typos, extra whitespace, or unsupported scopes)
  3. If the scope is legitimate, register it in the repository before clients request it
  4. 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

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.

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)