thephpleague/oauth2-server · error · OAuthServerException

access_denied

access_denied

Error message

access_denied

What it means

The user explicitly denied the authorization request on the verification page (getUserApproved() === false), so the grant throws OAuthServerException::accessDenied() per RFC 6749/8628. The device flow session is over; the client must stop polling and surface the denial to the user.

Solutions

  1. Treat access_denied as terminal on the client: stop polling and inform the user authorization was refused
  2. If the denial was accidental, restart the device flow with a fresh device code
  3. Verify the verification page sends an explicit approve=true when the user consents so false is only sent deliberately
  4. Do not retry polling after access_denied — it will keep failing

Example fix

// before
if (err.error) retryPoll(); // retries forever on access_denied

// after
if (err.error === 'access_denied') { ui.showError('You denied access on your device.'); stopPolling(); }
Defensive patterns

Strategy: try-catch

Validate before calling

$entity = $repo->getDeviceCodeEntityByDeviceCode($code); if ($entity !== null && $entity->getUserApproved() === false && $wasExplicitDenial) { showDenied(); exit; }

Try / catch

try { pollToken(); } catch (OAuthServerException $e) { if ($e->getErrorType() === 'access_denied') { stopPolling(); ui.notify('Authorization was denied'); return; } throw $e; }

Prevention

When it happens

Trigger: respondToAccessTokenRequest on a valid, non-expired device code after completeDeviceAuthorizationRequest was called with $userApproved = false — typically the user clicked 'Deny' (or your UI defaulted to false) on the verification URI page.

Common situations: Verification page 'Cancel'/'Deny' button wired to completeDeviceAuthorizationRequest(..., false); user approves the wrong device and denies; a default false value when the verification form posts without an approve flag.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


AI-assisted analysis of thephpleague/oauth2-server@9d2f6fc0a0 (2026-09-15). Data as JSON: /api/errors/7cc164b4596fae6f. Report an issue: GitHub.

Appendix: source

Thrown at src/Grant/DeviceCodeGrant.php:163

        $client = $this->validateClient($request);
        $deviceCodeEntity = $this->validateDeviceCode($request, $client);

        // If device code has no user associated, respond with pending or slow down
        if (is_null($deviceCodeEntity->getUserIdentifier())) {
            $shouldSlowDown = $this->deviceCodePolledTooSoon($deviceCodeEntity->getLastPolledAt());

            $deviceCodeEntity->setLastPolledAt(new DateTimeImmutable());
            $this->deviceCodeRepository->persistDeviceCode($deviceCodeEntity);

            if ($shouldSlowDown) {
                throw OAuthServerException::slowDown();
            }

            throw OAuthServerException::authorizationPending();
        }

        if ($deviceCodeEntity->getUserApproved() === false) {
            throw OAuthServerException::accessDenied();
        }

        // Finalize the requested scopes
        $finalizedScopes = $this->scopeRepository->finalizeScopes($deviceCodeEntity->getScopes(), $this->getIdentifier(), $client, $deviceCodeEntity->getUserIdentifier());

        // Issue and persist new access token
        $accessToken = $this->issueAccessToken($accessTokenTTL, $client, $deviceCodeEntity->getUserIdentifier(), $finalizedScopes);
        $this->getEmitter()->emit(new RequestAccessTokenEvent(RequestEvent::ACCESS_TOKEN_ISSUED, $request, $accessToken));
        $responseType->setAccessToken($accessToken);

        // Issue and persist new refresh token if given
        $refreshToken = $this->issueRefreshToken($accessToken);

        if ($refreshToken !== null) {
            $this->getEmitter()->emit(new RequestRefreshTokenEvent(RequestEvent::REFRESH_TOKEN_ISSUED, $request, $refreshToken));
            $responseType->setRefreshToken($refreshToken);
        }

View on GitHub (pinned to 9d2f6fc0a0)