OAuth2 Routing Guide¶
Use this guide when changing routes under /oauth2/*.
The architectural rule is recorded in the ADR Preserve typed FastAPI parameters on OAuth2 protocol routes.
Core Pattern¶
Keep typed FastAPI request extraction on protocol routes and translate
RequestValidationError at the OAuth2 route boundary.
Do not delete typed Query, Form, Header, Cookie, Security, or
dependency parameters just to suppress FastAPI 422 responses.
Route Boundary¶
Zero Auth Lite scopes protocol-validation translation through the custom route class
in app/oauth2/protocol_route.py.
OAuth2ProtocolRoute is applied to OAuth2 routers such as:
app/oauth2/authorization/router.pyapp/oauth2/devices/router.pyapp/oauth2/tokens/router.py
It catches FastAPI RequestValidationError and maps it to OAuth2 errors
without changing application APIs outside the protocol routers.
Authorization redirect safety remains a separate concern. The authorization endpoint must validate the client and redirect URI before redirecting protocol errors back to a client.
Typed Transport Extraction¶
Authorization Request¶
Authorization request parameters belong in Query fields or a typed query
dependency:
class AuthorizationRequestParams:
def __init__(
self,
response_type: Annotated[Literal["code"], Query()],
client_id: Annotated[str, Query(min_length=1)],
redirect_uri: Annotated[AnyUrl, Query()],
code_challenge: Annotated[str, Query(pattern=r"^[A-Za-z0-9_-]{43,128}$")],
code_challenge_method: Annotated[Literal["S256"], Query()],
scope: Annotated[str | None, Query()] = None,
state: Annotated[str | None, Query()] = None,
nonce: Annotated[str | None, Query()] = None,
) -> None: ...
Adapt exact PKCE constraints to the current policy, but keep them typed and visible in the route contract.
Token Form¶
Token request parameters belong in
application/x-www-form-urlencoded Form fields:
class TokenRequestForm:
def __init__(
self,
grant_type: Annotated[OAuth2GrantType, Form()],
client_id: Annotated[str | None, Form()] = None,
client_secret: Annotated[str | None, Form()] = None,
code: Annotated[str | None, Form()] = None,
redirect_uri: Annotated[str | None, Form()] = None,
code_verifier: Annotated[str | None, Form()] = None,
refresh_token: Annotated[str | None, Form()] = None,
device_code: Annotated[str | None, Form()] = None,
scope: Annotated[str | None, Form()] = None,
) -> None: ...
Grant-specific conditional rules still belong in domain parsing and services, not in a giant endpoint signature.
Revocation And Introspection¶
Revocation and introspection requests keep these fields in the form body:
tokentoken_type_hintclient_idclient_secret
Never move any of these into query parameters.
Explicitly prohibited:
client_secretin the query stringtokenin the query stringAuthorizationdata in the query string
Browser Decisions¶
Browser POST routes keep typed forms:
POST /oauth2/authorize: the same authorization request as GET, encoded as a formPOST /oauth2/authorize/decision:transaction_id,decision, and optionalcsrf_tokenwhen an HTML form is used instead of the configured header
State-changing browser forms must continue to use existing browser-session and CSRF dependencies. Device verification remains typed inside the built-in web adapter. It is not an OAuth2 protocol endpoint, but it remains visible in the application OpenAPI document when the built-in OAuth2 UI is mounted. Authorization transactions are server-side, user-bound before consent, organization-bound, expiring, and single-use. Never put editable authorization request fields in the decision form.
Client Authentication¶
HTTP Basic client authentication should use a typed Security or header
dependency so the OpenAPI security scheme remains defined and discoverable.
If body-based client authentication is also supported, its credentials remain in form data. Secrets must never move into query strings.
Raw Request Access¶
Raw Request access is allowed only when the route genuinely needs request URL
details, request state, response header control, or another documented
framework-level concern.
One such concern exists for optional OAuth2 client credentials. Typed optional
form extraction maps both a missing value and client_secret= or client_id=
to None, but authentication policy must treat an explicitly empty credential
as supplied. Client-authentication dependencies may reread only those fields
from the cached form body when the typed value is None. Typed Form
parameters remain authoritative for non-empty values, route signatures, and
OpenAPI.
It is not an acceptable substitute for typed protocol parameters.
OpenAPI Expectations¶
Typed FastAPI parameters are the primary source of protocol OpenAPI.
Apply the shared limits from OAuth2Specs to typed form/query parameters and
their domain DTOs. This keeps oversized requests from crossing a persistence
boundary whose columns have stricter limits. The OAuth2 route class translates
these transport validation failures into protocol 400 responses.
Use targeted OpenAPI adjustments only when FastAPI cannot accurately express a detail such as:
- removing unreachable protocol
422responses; - defining protocol-specific response metadata;
- documenting security schemes that must remain aligned with typed runtime extraction.
Do not replace typed runtime parameters with a handwritten schema.
OAuth2 Agent Checklist¶
Transport Contract¶
- Are query parameters still typed?
- Are form parameters still typed?
- Are headers represented as headers or security dependencies?
- Are cookies represented through cookie or session dependencies?
- Are credentials absent from query strings?
- Does every state-changing browser form remain CSRF-protected?
Protocol Errors¶
- Can any malformed protocol request return FastAPI
422? - Does validation become
OAuth2ErrorResponse? - Are invalid clients handled with the correct status and headers?
- Does authorization redirect only after validating the client and redirect URI?
OpenAPI¶
- Does every endpoint expose its real input?
- Is every referenced security scheme defined?
- Are form bodies represented as
application/x-www-form-urlencoded? - Is the password grant absent?
- Are protocol
422responses absent? - Are browser HTML responses documented as
text/html? - Are redirects documented with
Location?
Runtime And Generated Contract Alignment¶
- Do typed dependency fields match the generated OpenAPI schema?
- Are supported grant enums identical in code and OpenAPI?
- Are conditional grant fields validated after transport extraction?
- Do OpenAPI tests fail if a request body or parameter disappears?
Review Checklist¶
Any change touching /oauth2/*, OAuth2 router dependencies, OAuth2 request
models, OAuth2 exception handling, or OAuth2 OpenAPI generation must include:
- runtime protocol-error tests;
- OpenAPI contract tests;
- confirmation that typed parameters remain;
- confirmation that credentials remain outside query strings;
- confirmation that no OAuth2 protocol operation advertises
422.
Reviewers should reject changes that "simplify" endpoint signatures by moving typed fields into raw request parsing without a demonstrated and documented need.