Building an OAuth 2.0 and OpenID Connect Server
Build your authorization server to the OAuth 2.0 Security Best Current Practice (RFC 9700, January 2025) from day one: authorization code with PKCE for every client, exact redirect URI matching, no implicit grant, and no password grant. (RFC 9700) That is effectively OAuth 2.1, which is still an IETF draft: revision 17 was published on October 31, 2026. (IETF Datatracker)
Use a certified OpenID Connect server library and pass the OpenID Foundation conformance suite before launch. Do not hand-roll the protocol.
The endpoints you will build
| Endpoint | Standard | Purpose |
|---|---|---|
/.well-known/openid-configuration |
OpenID Connect Discovery, RFC 8414 | Publishes every endpoint, supported algorithm, and feature |
/authorize |
RFC 6749, OIDC Core | Authenticates the user, gets consent, returns a code |
/token |
RFC 6749 | Exchanges code, refresh token, or client credentials for tokens |
/userinfo |
OIDC Core | Returns claims for the access token's user |
/jwks.json |
RFC 7517 | Public keys for verifying your tokens |
/revoke |
RFC 7009 | Lets clients revoke tokens on sign-out |
/introspect |
RFC 7662 | Lets resource servers check opaque tokens |
/par |
RFC 9126 | Pushed authorization requests, so parameters are not exposed in the browser URL |
/device_authorization |
RFC 8628 | Sign-in for TVs and CLIs |
Authorization code with PKCE, step by step
- Client creates a random
code_verifierand sendscode_challenge = BASE64URL(SHA256(code_verifier))withcode_challenge_method=S256, plusclient_id,redirect_uri,scope,state, andnonce. - Server validates
client_id, then comparesredirect_urito the registered value by exact string match. On mismatch, show an error page; never redirect. - Server authenticates the user (your login flow, with step-up if
acr_valuesormax_agerequire it) and records consent for third-party clients. - Server issues a single-use authorization code valid for about a minute, bound to client, redirect URI, PKCE challenge, user, and nonce. It returns
code,state, andiss(RFC 9207, which defends against mix-up attacks). - Client posts the code and
code_verifierto/token, authenticating itself if confidential. - Server checks the verifier against the stored challenge, burns the code, and issues tokens. If a code is used twice, revoke every token issued from it.
RFC 9700 requires servers to reject a code_verifier when no challenge was sent, which blocks PKCE downgrade. Accept only S256.
Client authentication
| Method | Use for |
|---|---|
| None (public client) + PKCE | Mobile apps, SPAs through a backend-for-frontend |
private_key_jwt |
Preferred for confidential clients: no shared secret to leak |
| mTLS (RFC 8705) | High-assurance and financial-grade APIs |
client_secret_basic |
Acceptable for simple server apps; rotate secrets, store them hashed |
ID tokens
An ID token tells the client who signed in. Include iss, sub, aud, exp, iat, auth_time, nonce, acr, and amr. Make sub a stable, opaque identifier per user, and consider pairwise subjects (different sub per client) for consumer privacy.
Scopes and consent
- Keep scopes coarse enough to understand, fine enough to limit damage:
orders:read, notapi. - Record consent per user, client, and scope, with a way to review and revoke it.
- First-party clients can skip the consent screen; third-party clients never should.
Machine and agent access
- Client credentials for service-to-service calls, with short-lived tokens and narrow scopes.
- Token exchange (RFC 8693) when one service or AI agent acts on behalf of a user, so tokens show both the user and the actor.
- Protected resource metadata (RFC 9728) and dynamic client registration if you expect AI agents and MCP clients to discover and connect to your APIs.
Common mistakes
- Wildcard or prefix redirect URI matching.
- Supporting the implicit or password grant "for legacy clients."
- Authorization codes that can be used twice, or live for minutes.
- Access tokens without audience restriction that every API accepts.
Key takeaways
- Build to RFC 9700 from day one: PKCE with S256 for every client, exact redirect matching, no implicit or password grant.
- Make authorization codes single use and short lived, and return iss to stop mix-up attacks.
- Prefer private_key_jwt or mTLS for confidential clients.
- Plan for agents now: token exchange, protected resource metadata, and dynamic client registration.