thephpleague/oauth2-server · warning · OAuthServerException
access_denied
access_denied
Error message
The user denied the request
What it means
access_denied: the user (resource owner) denied the client's authorization request. completeAuthorizationRequest() detects the request was not approved and throws, redirecting back to the client with error=access_denied and the original state parameter.
Solutions
- Treat this as an expected outcome: catch OAuthServerException and check getCode() === 'access_denied' to show 'you denied access' in the client app
- In your authorization-confirm controller, only call completeAuthorizationRequest() after setAuthorizationApproved(true); return the redirect response directly for denials
- Inspect the exception's redirect payload to recover state and inform the user
- Retry the flow only if the user explicitly wants to try again
Example fix
// before
try {
$response = $server->completeAuthorizationRequest($authRequest, $response);
} catch (OAuthServerException $e) { throw $e; }
// after
try {
$response = $server->completeAuthorizationRequest($authRequest, $response);
} catch (OAuthServerException $e) {
if ($e->getCode() === 9 /* access_denied */) {
return new Response(['message' => 'Authorization was denied by the user']);
}
throw $e;
} Defensive patterns
Strategy: try-catch
Validate before calling
// before calling completeAuthorizationRequest, branch explicitly
if (!$authorizationRequest->isAuthorizationApproved()) {
return new Response(['message' => 'User denied access']); // do not call the server
} Type guard
function wasApproved($authorizationRequest): bool {
return $authorizationRequest !== null && $authorizationRequest->isAuthorizationApproved() === true;
} Try / catch
try {
$response = $server->completeAuthorizationRequest($authRequest, $response);
} catch (OAuthServerException $e) {
if ($e->getErrorType() === 'access_denied') {
return new Response(['message' => 'Authorization was denied by the user']);
}
throw $e;
} Prevention
- Only mark the request approved when the user explicitly consents
- Handle access_denied as a normal UX outcome, not a crash
- Preserve and echo the state parameter for CSRF continuity
- Show a clear deny path in the consent UI so denials are intentional
When it happens
Trigger: Authorization request completes with AuthorizationRequest::setAuthorizationApproved(false) — typically the user clicked 'Deny'/'Cancel' on your consent screen and you called completeAuthorizationRequest() anyway.
Common situations: Users declining consent — normal flow, not a bug; consent UI not distinguishing approve/deny before calling the server; automated tests exercising the deny path; users abandoning a consent dialog that still submits a denial.
Understand the failure class
Background: Permission denied / not authorized / 403 Forbidden: access-control rejections when the caller lacks the required role, grant, or ownership — this error's family across 18 libraries.
Related errors
AI-assisted analysis of thephpleague/oauth2-server@9d2f6fc0a0 (2026-09-15).
Data as JSON: /api/errors/c28179ed54fb04e8.
Report an issue: GitHub.
Appendix: source
Thrown at src/Grant/AuthCodeGrant.php:399
throw new LogicException('An error was encountered when JSON encoding the authorization request response');
}
$response = new RedirectResponse();
$response->setRedirectUri(
$this->makeRedirectUri(
$finalRedirectUri,
[
'code' => $this->encrypt($jsonPayload),
'state' => $authorizationRequest->getState(),
]
)
);
return $response;
}
// The user denied the client, redirect them back with an error
throw OAuthServerException::accessDenied(
'The user denied the request',
$this->makeRedirectUri(
$finalRedirectUri,
[
'state' => $authorizationRequest->getState(),
]
)
);
}
}
View on GitHub (pinned to 9d2f6fc0a0)