Sign in

Published

How OAuth Works: From Delegated Access to Secure Sign-In

OAuth solves a deceptively simple problem: how can you let an application use your account at another service without giving that application your password?

The answer is not “share a safer password.” It is delegate a specific kind of access, through a trusted authorization service, using credentials designed for that delegation.

That distinction explains nearly everything that matters in an OAuth implementation: why there are redirects, why an authorization code is not an access token, why tokens have scopes and audiences, and why “Sign in with…” usually requires OpenID Connect as well.

The Core Model: Delegate Access, Not Your Credentials

Suppose you build a photo-printing application. Your customers keep their photos at a separate photo service. Your application needs to retrieve photos to fulfill an order, but it should not receive customers’ passwords or gain unrestricted control of their accounts.

OAuth lets the photo service issue your application an access token representing authorized access. Your application presents that token to the photo API.

OAuth is an authorization framework: it lets a client obtain limited access to protected resources without receiving the resource owner’s credentials.

“Limited” can involve several dimensions:

  • Scope: Which operations or categories of data the application may access.
  • Audience: Which API or resource server should accept the token.
  • Lifetime: How long the token remains usable.
  • User and client context: On whose behalf, and for which application, access was granted.
  • Additional restrictions: Provider-specific permissions, organizational policy, or sender-constraining mechanisms.

OAuth does not automatically make access narrow. A service can issue overly broad permissions or long-lived tokens. The framework gives you mechanisms for delegation; your configuration determines how well you constrain it.

The Four Roles

OAuth defines four roles. A deployment may combine some of them, but keeping them conceptually separate helps you reason about trust.

RoleResponsibilityPhoto-printing example
Resource ownerCan authorize access to a protected resourceThe customer
ClientRequests access and calls the APIYour printing application
Authorization serverAuthenticates as needed, processes authorization, and issues tokensThe photo service’s authorization system
Resource serverHosts protected resources and enforces accessThe photo API

The authorization server and resource server may belong to the same organization, but they do different jobs.

The authorization server decides what authorization to issue. The resource server decides whether a particular request is permitted under that authorization.

Your application still needs its own permissions model. An OAuth token does not replace checks such as “does this customer own this order?” or “is this employee allowed to export this record?”

The Modern Default: Authorization Code with PKCE

For applications acting on behalf of a person, the usual starting point is the authorization code flow with PKCE.

PKCE, pronounced “pixy,” stands for Proof Key for Code Exchange. It binds an authorization request to the later token exchange, making a stolen authorization code insufficient on its own.

The flow has two distinct paths:

  • The front channel, through the browser, carries the authorization request and redirect response.
  • The token exchange, made directly to the authorization server over HTTPS, turns the code into tokens.

For a server-side application, the token exchange happens on your backend. For a browser-based application without a backend, browser code performs it. That architectural difference affects where tokens can safely live.

Step 1: Register the Client

Before the flow begins, you typically register your application with the authorization server. Registration establishes details such as:

  • A client ID identifying the application.
  • Allowed redirect URIs.
  • Supported client authentication methods, if any.
  • Application type and other provider-specific settings.

A client ID is not a secret. It appears in browser requests.

A confidential client, such as a backend service, can protect credentials and authenticate at the token endpoint. A public client, such as a distributed mobile application or browser application, cannot reliably keep a shared secret.

Shipping a client secret in JavaScript or a mobile binary does not make the client confidential.

Step 2: Prepare a Transaction and a PKCE Challenge

Your application generates a fresh, high-entropy code verifier for this authorization attempt. It derives a challenge using the S256 method:

The base64url encoding is unpadded.

For example, encoding 32 cryptographically random bytes as unpadded base64url produces a 43-character verifier with 256 bits of underlying randomness. PKCE permits verifiers from 43 to 128 characters using its specified character set.

Your application also normally generates a random state value and stores it with the pending authorization transaction.

These values serve different purposes:

  • PKCE proves that the party exchanging the code possesses the verifier associated with the original request.
  • State binds the redirect response to a browser transaction and helps prevent cross-site request forgery.

Use your OAuth library’s transaction handling rather than improvising these mechanisms. Their protection depends on correct generation, storage, comparison, and binding to the initiating session.

Step 3: Redirect the Browser to the Authorization Server

An authorization request might look like this:

GET /authorize?
    response_type=code&
    client_id=photo-printing-app&
    redirect_uri=https%3A%2F%2Fprint.example%2Foauth%2Fcallback&
    scope=photos.read&
    state=<random-transaction-value>&
    code_challenge=<derived-challenge>&
    code_challenge_method=S256
Host: auth.photos.example

The important parameters are:

ParameterPurpose
response_type=codeRequests an authorization code
client_idIdentifies the requesting application
redirect_uriSpecifies where the authorization response should return
scopeRequests permissions
stateAssociates the response with the initiating transaction
code_challengeBinds the request to the later verifier
code_challenge_method=S256Specifies the challenge derivation method

The requested scope is not a guarantee. The authorization server may reject the request, issue narrower access, or apply policy constraints.

The browser now interacts with the authorization server—not with a password form controlled by your application.

Step 4: Authenticate and Authorize

The authorization server determines whether access should be granted. That may involve:

  • Signing the person in.
  • Reusing an existing authenticated session.
  • Requiring multifactor authentication.
  • Showing a consent screen.
  • Applying enterprise policy or a previously recorded grant.

A fresh password prompt and consent screen are not mandatory on every request. Existing sessions and prior authorization often make the interaction nearly invisible.

This is why it helps to separate authentication from authorization:

  • Authentication establishes who is interacting with the authorization server.
  • Authorization determines what the client may do.

The authorization server performs both as needed, but OAuth itself standardizes delegated authorization rather than a general-purpose login protocol for your application.

Step 5: Receive an Authorization Code

After approval, the authorization server redirects the browser to the registered callback:

HTTP/1.1 302 Found
Location: https://print.example/oauth/callback?
          code=<short-lived-code>&
          state=<original-state>

Your application verifies that the returned state belongs to the pending transaction.

The authorization code is an intermediate credential. It is short-lived, single-use, and bound to the client and authorization transaction. It is not what you send to the photo API.

Why use a code instead of returning the access token immediately? The code allows the authorization server to perform a separate exchange with client authentication where applicable and PKCE verification before issuing tokens.

The callback still carries sensitive material. Avoid logging codes, loading unnecessary third-party resources on callback pages, or letting callback URLs become open redirects.

Step 6: Exchange the Code for Tokens

The client sends a token request:

POST /token
Host: auth.photos.example
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code&
code=<received-code>&
redirect_uri=https%3A%2F%2Fprint.example%2Foauth%2Fcallback&
client_id=photo-printing-app&
code_verifier=<original-verifier>

A confidential client also authenticates using its configured method, such as a client secret or an asymmetric mechanism. A public client does not gain security by attaching a secret that anyone can extract.

The authorization server checks the code, client binding, redirect URI where applicable, and PKCE verifier. It then returns something like:

{
  "access_token": "<access-token>",
  "token_type": "Bearer",
  "expires_in": 900,
  "scope": "photos.read",
  "refresh_token": "<refresh-token>"
}

Here, expires_in: 900 means a 15-minute lifetime. That is an illustrative configuration, not an OAuth requirement. A refresh token is optional.

Step 7: Call the Resource Server

Your application presents the access token:

GET /photos
Host: api.photos.example
Authorization: Bearer <access-token>

The resource server validates the token and decides whether it authorizes this request.

That decision should include the relevant issuer, audience, expiry, permissions, and resource-specific policy—not merely “the token looks valid.”

What Tokens Mean—and What They Do Not

An implementation becomes much easier to reason about when you keep its credentials separate.

ArtifactIntended recipientWhat it does
Authorization codeToken endpointCan be exchanged once for tokens
Access tokenResource serverAuthorizes API access
Refresh tokenAuthorization serverRequests replacement access tokens
ID tokenOpenID Connect clientCommunicates an authentication result

These are not interchangeable.

Access Tokens Are API Credentials

A common access token is a bearer token:

Whoever possesses a bearer token can generally use it, subject to the token’s restrictions, without proving possession of a separate key.

That makes token theft consequential. TLS protects transport, but you also need to protect storage, logging, telemetry, and application execution environments.

Access tokens may be:

  • Opaque: The client cannot infer their meaning; the resource server may use token introspection or another server-side lookup.
  • Structured, often as a signed JWT: The resource server can validate specified claims locally.

OAuth does not require access tokens to be JWTs.

Even when the token is a JWT, a signature is not encryption. Its payload is usually readable by anyone holding it. Do not put sensitive data into a token under the assumption that signing hides it.

Scope Is Necessary, but Not Sufficient

A scope such as photos.read expresses a permission category defined by the provider. Scope names are not universally standardized across APIs.

A resource server should combine scope with other controls:

  • Does the token target this API?
  • Does it represent the correct account or tenant?
  • Is the requested object accessible to that account?
  • Does the endpoint require an additional permission?
  • Has relevant policy changed?

An access token for a profile API should not become valid at a payments API simply because both APIs trust the same signing key.

This is the role of audience restriction: a token should be accepted only by its intended resource server or servers.

Token Format Changes Operational Trade-Offs

Opaque tokens and locally validated JWTs offer different advantages:

ConsiderationOpaque token with online validationLocally validated JWT
Validation pathLookup or introspection, possibly cachedSignature and claim checks
Network dependencyUsually presentUsually absent for each request
Revocation visibilityCan be immediate, depending on cachingOften delayed until expiry unless additional checks exist
Operational burdenAvailability and latency of validation serviceKey distribution, rotation, and claim validation

Neither format is inherently “the secure one.” The security properties depend on validation, lifetime, revocation design, and operational reliability.

Refresh Tokens: Continuity with a Larger Security Obligation

Access tokens expire so that exposure and stale authorization do not persist indefinitely. A refresh token lets the client obtain new access tokens without sending the person through another interactive flow.

A refresh request typically contains:

POST /token
Content-Type: application/x-www-form-urlencoded

grant_type=refresh_token&
refresh_token=<refresh-token>

Client authentication applies where required.

Refresh tokens can be long-lived and valuable. Keep them out of API requests: they belong at the authorization server’s token endpoint, not at resource servers.

Rotation and Replay Detection

With refresh token rotation, a successful refresh returns a new refresh token and invalidates the previous one.

If an invalidated token is later reused, the authorization server can detect potential theft and revoke the associated token family or grant.

Rotation adds operational details you need to handle:

  • Concurrent refresh attempts can race.
  • A response lost after successful rotation can complicate recovery.
  • Multiple application instances need coordinated token storage.
  • Replay handling and any grace behavior vary by provider.

For public clients, current OAuth security guidance calls for replay protection through refresh token rotation or sender-constrained refresh tokens.

Treat refresh-token handling as credential management, not background plumbing.

Revocation Is Not Necessarily Instant Everywhere

A revocation endpoint can invalidate supported tokens or their associated grant. But revoking a refresh token does not necessarily make every previously issued, locally validated access token stop working immediately.

Your effective revocation delay depends on:

  • Access-token lifetime.
  • Whether resource servers consult live status.
  • Introspection caching.
  • Any denylist or event-driven invalidation mechanism.

Short access-token lifetimes reduce the window, but they increase token issuance traffic and reliance on refresh handling.

Also distinguish disconnecting an integration from logging out. Ending an application session, ending an identity-provider session, and revoking API authorization are separate operations.

OAuth Versus OpenID Connect

“Sign in with…” looks like OAuth because it uses many of the same redirects and endpoints. But a standardized login implementation typically uses OpenID Connect, or OIDC.

OAuth answers “What may this application access?” OIDC adds a standardized answer to “Who authenticated?”

OIDC builds on OAuth 2.0. An authorization request signals OIDC by including the openid scope.

For example:

scope=openid profile photos.read

The same flow can support both login and API access:

  • An ID token communicates identity and authentication information to your application.
  • An access token authorizes calls to the photo API.

What the ID Token Adds

An ID token is a JWT containing claims such as:

  • iss: The issuer.
  • sub: The subject identifier.
  • aud: The intended client.
  • exp: Expiration time.
  • iat: Issuance time.
  • nonce: A request-bound value, when supplied.
  • Additional authentication or profile claims, where applicable.

Validate the token using the provider’s metadata and supported signing algorithms. Check its signature and required claims, including issuer, audience, expiry, and nonce when used, plus any flow-specific validation rules.

For account identity, use the combination of issuer and subject:

An email address is not generally a safe replacement. It can change, may not be verified, and may have different uniqueness semantics across providers.

Do Not Use an Access Token as an ID Token

An access token may contain a user identifier, but its format and semantics are meant for the resource server. Its audience may be an API rather than your application.

Accepting any provider-issued access token as proof of login can create token-substitution vulnerabilities: you may accept a credential issued for another purpose or another client.

Likewise, do not send an ID token to an API as if it were an access token. Its intended audience is normally the OIDC client.

If you use the OIDC UserInfo endpoint to retrieve profile claims, verify that its returned sub matches the ID token’s subject.

Choose the Architecture Before You Choose Token Storage

The protocol flow is only part of your design. Where tokens live determines which attacks become most important.

Server-Side Web Applications and Backend-for-Frontend Designs

A server-side application can store access and refresh tokens on the backend and give the browser a separate application session cookie.

A backend for frontend, or BFF, applies this pattern to a frontend application: browser requests go through your backend, which handles OAuth tokens and upstream API calls.

Benefits include:

  • No OAuth tokens exposed to browser JavaScript.
  • Centralized refresh and revocation handling.
  • Protected confidential-client credentials.

The trade-offs include backend infrastructure, proxying costs, and cookie-session security. You still need CSRF defenses, secure cookie configuration, and XSS prevention. Malicious JavaScript may be able to act through a logged-in session even without reading tokens.

Browser-Only Applications

A browser-only application is a public client. Use authorization code with PKCE rather than assuming a bundled secret provides authentication.

Token storage involves trade-offs:

  • Persistent storage helps survive reloads but exposes credentials to scripts executing in the origin.
  • In-memory storage reduces persistence but is still accessible to malicious code running in the application.
  • Refresh-token support requires appropriate replay defenses and careful provider configuration.

No storage location turns a compromised JavaScript execution environment into a safe one.

Native Applications

Native applications are also generally public clients. Use authorization code with PKCE and perform authorization through an external user-agent, typically the system browser.

Choose redirect mechanisms supported by the platform and provider, such as claimed HTTPS redirects, app-specific URI schemes, or loopback redirects for suitable desktop applications.

Avoid collecting the provider’s password inside an embedded login form. The system-browser approach preserves the authorization server’s authentication controls and avoids training people to enter third-party credentials into your application.

Other OAuth Flows—and When They Fit

Not every authorization involves a browser or a person.

FlowTypical useImportant boundary
Authorization code with PKCEWeb, browser, and native applications acting for a personRecommended starting point for interactive authorization
Client credentialsService-to-service accessRepresents the client’s authorization, not a person’s delegated session
Device authorizationTVs, consoles, and input-constrained devicesA person completes authorization on another device
Refresh token grantContinuing previously authorized accessDepends on secure refresh-token handling

Two older patterns should not be your starting point:

  • Implicit grant: Returns an access token through the front channel. Modern security guidance favors authorization code flows instead.
  • Resource owner password credentials grant: Has the application collect the person’s password. Current OAuth security guidance says it must not be used.

If your design requires your application to receive a third-party account password, you have lost one of OAuth’s central benefits.

A Practical Security Checklist

Before shipping, verify the complete system—not just that the happy-path redirect works.

  1. Use authorization code with S256 PKCE. Use maintained libraries and fresh transaction-specific values.
  2. Register and validate redirect URIs strictly. Avoid wildcards and open redirects; understand narrowly defined native-app exceptions.
  3. Bind responses to the initiating transaction. Validate state where used, OIDC nonce where used, and issuer information in multi-provider designs.
  4. Keep credentials out of URLs and logs. Use the authorization header for API access tokens and redact sensitive telemetry.
  5. Validate tokens for their intended purpose. API validation and ID-token validation are not the same operation.
  6. Enforce audience and resource permissions. A valid signature alone does not authorize an endpoint.
  7. Request the smallest useful scopes. Account for partial grants, denied authorization, expiry, and revoked access.
  8. Protect refresh tokens. Use rotation or sender-constraining where appropriate and handle concurrency deliberately.
  9. Plan key rotation and outages. Cache signing keys appropriately, handle unknown key IDs safely, and define introspection failure behavior.
  10. Separate login, API authorization, and application permissions. Each has its own lifecycle and checks.

The Mental Model to Keep

OAuth is best understood as a controlled exchange of credentials with distinct purposes:

  1. Your application requests authorization.
  2. The authorization server authenticates and obtains or applies permission.
  3. A short-lived code returns through the browser.
  4. The client exchanges that code, with PKCE and client authentication where applicable, for tokens.
  5. The access token authorizes API requests.
  6. A refresh token may preserve access over time.
  7. OIDC adds an ID token when your application needs a standardized authentication result.

The practical rule is simple:

Use access tokens for APIs, ID tokens for login, and your own authorization checks for your application’s rules.

Keep those boundaries intact, and OAuth becomes far less mysterious—and much harder to misuse.

Further Reading