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
- Treat access_denied as terminal on the client: stop polling and inform the user authorization was refused
- If the denial was accidental, restart the device flow with a fresh device code
- Verify the verification page sends an explicit approve=true when the user consents so false is only sent deliberately
- 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
- Stop polling immediately when access_denied is returned
- Only post userApproved=false when the user explicitly denies
- Provide an explicit approve action that sets userApproved=true
- Restart the flow with a new device code if the user changes their mind
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)