thephpleague/oauth2-server · info · OAuthServerException
authorization_pending
authorization_pending
Error message
authorization_pending
What it means
The device code is valid but the user has not yet completed the verification step (or polling occurred before lastPolledAt handling marked this poll). RFC 8628 authorization_pending: the client should keep polling at the given interval until the user approves, denies, or the code expires.
Solutions
- Treat authorization_pending as transient: keep polling at the specified interval until approved/denied/expired
- Make sure the device authorization response's verification_uri (and verification_uri_complete) are surfaced to the user
- Ensure completeDeviceAuthorizationRequest is actually wired to your verification page so userApproved gets set
- Check expiry handling: switch to expired_token handling once the device code TTL passes
Example fix
// before
const token = await pollToken(deviceCode); // throws, request dies
// after
while (true) {
try { const token = await pollToken(deviceCode); break; }
catch (e) {
if (e.error === 'authorization_pending' || e.error === 'slow_down') { await sleep(interval); continue; }
throw e;
}
} Defensive patterns
Strategy: retry
Validate before calling
$entity = $repo->getDeviceCodeEntityByDeviceCode($code); if ($entity !== null && $entity->getUserApproved() === false) { keepPolling(); } Try / catch
try { pollToken(); } catch (OAuthServerException $e) { if ($e->getErrorType() === 'authorization_pending') { sleep($interval); retry(); } throw $e; } Prevention
- Treat authorization_pending as normal device-flow progress, not an error
- Always display verification_uri/verification_uri_complete to the user
- Cap total polling time to the device code TTL
- Wire completeDeviceAuthorizationRequest properly so approval eventually lands
When it happens
Trigger: respondToAccessTokenRequest finds a valid, non-expired device code whose lastPolledAt is null or old enough (not slow_down) but whose userApproved flag is still false/unset — i.e. normal polling before the user finishes approving at the verification URI.
Common situations: The expected state during device flow while the user is still typing the user_code; client treats it as a fatal error and aborts instead of continuing to poll; UI never shows the verification URI so the user never approves.
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/7709929a22ee79e5.
Report an issue: GitHub.
Appendix: source
Thrown at src/Grant/DeviceCodeGrant.php:159
ResponseTypeInterface $responseType,
DateInterval $accessTokenTTL
): ResponseTypeInterface {
// Validate request
$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) {View on GitHub (pinned to 9d2f6fc0a0)