OAuth guides

Understanding the OAuth 2.0 authorization code flow

How browsers, your app, and an authorization server cooperate to let a user sign in without handing your app their password.

8 min read

OAuth 2.0 is a framework for delegated access: a user can grant your application limited rights to act on their behalf, usually without sharing their password with you. The authorization code grant is the default choice for server-side web apps and many mobile or desktop apps that can open a system browser.

Roles in the system

Four parties show up in almost every integration:

  • Resource owner — the person signing in.
  • Client — your application (web app, API, mobile app).
  • Authorization server — authenticates the user and issues tokens (often the same company as the identity provider).
  • Resource server — APIs that accept access tokens (Google Calendar, your own backend, etc.).

Your client never sees the user's password for the authorization server. It only receives tokens after the user has approved access on a page the authorization server controls.

Step-by-step: authorization code

A typical login with redirect looks like this:

  1. Your app sends the user's browser to the authorization endpoint with parameters such as client_id, redirect_uri, response_type=code, scope, and often state (and code_challenge when using PKCE).
  2. The user signs in and approves the requested scopes on the authorization server's UI.
  3. The server redirects back to your redirect_uri with a short-lived authorization code in the query string.
  4. Your backend (or a confidential client) exchanges that code at the token endpoint, sending client_id, client_secret (if applicable), redirect_uri, and the code.
  5. The token endpoint returns an access token and often a refresh token. If you use OpenID Connect, you also receive an id_token.
  6. Your app calls APIs with the access token until it expires, then refreshes if you stored a refresh token.

The code is useless on its own to JavaScript on a public page: exchanging it requires your client secret (for confidential clients) or PKCE verification (for public clients). That split is intentional.

Access tokens vs refresh tokens

An access token is presented to resource servers, usually in an Authorization: Bearer header. It should be treated as a secret in logs and error pages. Lifetimes are often short (minutes to an hour).

A refresh token is only sent to the token endpoint to obtain new access tokens. Store it securely (encrypted at rest, never in localStorage for browser-only SPAs unless you accept the risk). Many providers rotate refresh tokens on each use.

Why redirect URI registration matters

You must pre-register every redirect URI with the authorization server. At redirect time, the server only sends the code to URIs on that allow list. That blocks an attacker who tricks a user into authorizing a code meant for the attacker's redirect endpoint.

Use HTTPS in production, avoid wildcards when the provider allows exact URIs, and give separate OAuth clients for local development and production rather than one client with dozens of redirect URIs.

Practical integration notes

Framework middleware (NextAuth, Auth.js, Passport, ASP.NET OpenIdConnect, etc.) implements this flow for you. When learning or debugging, it still helps to read the raw authorize and token requests in network tools.

Environment variables usually mirror production names: issuer or authority URL, client ID, client secret, and callback URL. Keeping those names stable while you point the issuer at a mock IdP during development (HTTPS issuer, localhost redirect) reduces surprise when you switch credentials for staging or production.