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

  1. Treat this as an expected outcome: catch OAuthServerException and check getCode() === 'access_denied' to show 'you denied access' in the client app
  2. In your authorization-confirm controller, only call completeAuthorizationRequest() after setAuthorizationApproved(true); return the redirect response directly for denials
  3. Inspect the exception's redirect payload to recover state and inform the user
  4. 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

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)