Introduction

OAuth 2.0 is an authorization framework (RFC 6749) that enables a third-party application to obtain limited access to a web service on behalf of a user, without the user sharing their credentials with the third party. Instead of giving an application your username and password, OAuth allows you to grant it a scoped, time-limited token that provides only the specific access it needs.

Before OAuth, the common pattern for third-party access was to give the application your actual password. This meant the application had full, unrestricted access to your account, could not be selectively revoked, and required you to change your password to remove access -- which would also break every other application using that password. OAuth solved these problems by introducing a delegation model mediated by the service provider.

OAuth 2.0 replaced the original OAuth 1.0 protocol (RFC 5849), which required complex cryptographic signatures on every request. OAuth 2.0 simplified the protocol by relying on HTTPS for transport security instead of requiring per-request signatures, making it significantly easier to implement.

"OAuth 2.0 is not an authentication protocol. It is an authorization delegation framework. Using OAuth for authentication without OpenID Connect is like using a valet key as proof of car ownership -- it grants access but proves nothing about identity." -- Justin Richer, co-author of "OAuth 2 in Action"

Core Concepts and Roles

OAuth 2.0 defines four distinct roles that interact during the authorization process:

RoleDescriptionExample
Resource OwnerThe user who owns the data and grants accessA GitHub user who authorizes a CI tool
ClientThe application requesting access on behalf of the userThe CI tool (e.g., Jenkins, CircleCI)
Authorization ServerThe server that authenticates the user and issues tokensGitHub's OAuth server
Resource ServerThe API that hosts the protected resourcesGitHub's API (api.github.com)

The separation between the authorization server and the resource server is a key architectural distinction. In small deployments, they may be the same server, but in large organizations they are typically separate services. The authorization server handles all the complexity of user authentication, consent, and token issuance, while the resource server simply validates tokens and serves resources.

OAuth also distinguishes between two types of clients:

  • Confidential clients can securely store a client secret (server-side applications). They authenticate to the authorization server using their client ID and secret.
  • Public clients cannot securely store secrets (single-page applications, mobile apps, desktop apps). They rely on other mechanisms like PKCE for security.

Authorization Code Flow

The authorization code flow is the most secure and most commonly recommended OAuth flow for server-side applications. It involves two exchanges: first, the client obtains an authorization code via the user's browser, then it exchanges that code for an access token via a direct server-to-server request.

# Authorization Code Flow - Step by Step# 1. Client redirects user to authorization serverGET https://auth.example.com/authorize? response_type=code& client_id=my_app& redirect_uri=https://myapp.com/callback& scope=read:profile%20read:email& state=random_csrf_token_abc123# 2. User authenticates and grants consent# (happens on the authorization server's login page)# 3. Authorization server redirects back to client with authorization codeHTTP/1.1 302 FoundLocation: https://myapp.com/callback?code=AUTH_CODE_XYZ&state=random_csrf_token_abc123# 4. Client exchanges authorization code for tokens (server-to-server)POST https://auth.example.com/tokenContent-Type: application/x-www-form-urlencodedgrant_type=authorization_code&code=AUTH_CODE_XYZ&redirect_uri=https://myapp.com/callback&client_id=my_app&client_secret=MY_SECRET# 5. Authorization server responds with tokens{ "access_token": "eyJhbGciOiJSUzI1NiIs...", "token_type": "Bearer", "expires_in": 3600, "refresh_token": "dGhpcyBpcyBhIHJlZnJlc2g...", "scope": "read:profile read:email"}

The two-step exchange is critical for security. The authorization code travels through the user's browser (front channel), which could potentially be observed. But the code alone is useless without the client secret, which is sent directly from the client's server to the authorization server (back channel), never exposed to the browser.

The state parameter serves as a CSRF protection mechanism. The client generates a random value, includes it in the authorization request, and verifies that the same value is returned in the callback. This prevents an attacker from tricking a victim into completing an OAuth flow with the attacker's authorization code.

Proof Key for Code Exchange (PKCE)

PKCE (RFC 7636, pronounced "pixy") is an extension to the authorization code flow that protects against authorization code interception attacks. Originally designed for public clients (mobile and native apps) that cannot securely store a client secret, PKCE is now recommended for all OAuth clients, including confidential ones, as a defense-in-depth measure.

# PKCE Flow# 1. Client generates a random code_verifier (43-128 characters)code_verifier = "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"# 2. Client creates code_challenge = BASE64URL(SHA256(code_verifier))code_challenge = "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM"# 3. Client sends code_challenge with the authorization requestGET https://auth.example.com/authorize? response_type=code& client_id=my_app& redirect_uri=https://myapp.com/callback& scope=read:profile& code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM& code_challenge_method=S256& state=random_state_value# 4. Authorization server stores the code_challenge with the auth code# 5. Client sends code_verifier when exchanging the code for tokensPOST https://auth.example.com/tokenContent-Type: application/x-www-form-urlencodedgrant_type=authorization_code&code=AUTH_CODE_XYZ&redirect_uri=https://myapp.com/callback&client_id=my_app&code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk# 6. Authorization server verifies:# SHA256(code_verifier) == stored code_challenge# If they match, the token is issued

PKCE works because even if an attacker intercepts the authorization code (step 3), they cannot exchange it for a token without the original code_verifier, which was never transmitted through the browser. The authorization server only received the SHA-256 hash of the verifier, so it cannot be reverse-engineered.

Client Credentials Flow

The client credentials flow is used for machine-to-machine (M2M) communication where no user is involved. The client authenticates directly with the authorization server using its own credentials (client ID and secret) to obtain an access token. This flow is appropriate for backend services, daemons, and automated processes that need to access APIs.

# Client Credentials FlowPOST https://auth.example.com/tokenContent-Type: application/x-www-form-urlencodedAuthorization: Basic base64(client_id:client_secret)grant_type=client_credentials&scope=read:analytics%20write:reports# Response{ "access_token": "eyJhbGciOiJSUzI1NiIs...", "token_type": "Bearer", "expires_in": 3600, "scope": "read:analytics write:reports"}# No refresh token is issued -- the client can simply request a new# access token using its credentials when the current one expires

Since there is no user context in the client credentials flow, the access token represents the client application itself, not a specific user. The scopes granted should be carefully limited to only what the application needs, following the principle of least privilege.

Implicit Flow (Deprecated)

The implicit flow was originally designed for browser-based applications (SPAs) that could not make server-to-server requests. In this flow, the authorization server returns the access token directly in the URL fragment of the redirect, skipping the authorization code exchange entirely.

AspectImplicit FlowAuthorization Code + PKCE
Token DeliveryURL fragment (front channel)Server-to-server (back channel)
Token ExposureBrowser history, Referer header, logsNot exposed to browser
Refresh TokensNot supportedSupported
Token InjectionVulnerableProtected by code_verifier
StatusDeprecated by OAuth 2.1Recommended for all clients

The OAuth Security Best Current Practice document (RFC 9700) and the draft OAuth 2.1 specification formally deprecate the implicit flow. The access token is exposed in the URL fragment, making it vulnerable to token leakage through browser history, server logs, and Referer headers. All browser-based applications should use the authorization code flow with PKCE instead.

OpenID Connect

OpenID Connect (OIDC) is an identity layer built on top of OAuth 2.0 that adds authentication capabilities. While OAuth 2.0 is purely an authorization framework (it tells you what a user can access, not who they are), OIDC extends it with standardized mechanisms for verifying user identity.

The key addition in OIDC is the ID token -- a JWT that contains claims about the authenticated user. Unlike an access token (which is meant for the resource server), the ID token is intended for the client application and contains information such as the user's unique identifier, name, email, and when they authenticated.

# OpenID Connect extends OAuth 2.0 with:# 1. The "openid" scope (required to trigger OIDC)GET https://auth.example.com/authorize? response_type=code& client_id=my_app& scope=openid%20profile%20email& redirect_uri=https://myapp.com/callback& state=random_state& nonce=random_nonce# 2. An ID token in the token response{ "access_token": "eyJhbGciOiJSUzI1NiIs...", "id_token": "eyJhbGciOiJSUzI1NiIs...", "token_type": "Bearer", "expires_in": 3600}# 3. Decoded ID token payload:{ "iss": "https://auth.example.com", "sub": "user123", "aud": "my_app", "exp": 1716239022, "iat": 1716235422, "nonce": "random_nonce", "name": "Jane Doe", "email": "jane@example.com", "email_verified": true}# 4. A standardized UserInfo endpointGET https://auth.example.com/userinfoAuthorization: Bearer access_token_here# Returns additional user claims beyond what is in the ID token

"If you are using OAuth 2.0 for login, you are almost certainly doing it wrong -- unless you are also using OpenID Connect. OAuth tells you nothing about who the user is. OIDC was specifically designed to fill that gap." -- Nat Sakimura, chairman of the OpenID Foundation

Token Types and Scopes

OAuth 2.0 uses different token types for different purposes, and scopes to limit what each token can access.

Token TypePurposeLifetimeFormat
Authorization CodeExchanged for tokens (one-time use)Seconds (typically 30-60s)Opaque string
Access TokenAuthorize API requestsMinutes to hoursJWT or opaque
Refresh TokenObtain new access tokensDays to monthsOpaque string
ID Token (OIDC)Prove user identity to the clientMinutesJWT (always)

Scopes define the specific permissions that the client is requesting. The user must consent to the requested scopes, and the authorization server may grant a subset of them. Scopes should follow the principle of least privilege -- request only the minimum access needed.

# Common scope patterns:# GitHubscope=read:user repo:status# Googlescope=openid profile email https://www.googleapis.com/auth/calendar.readonly# Microsoftscope=openid profile email User.Read Mail.Read# Custom APIscope=read:products write:orders admin:users

Security Considerations

OAuth implementations are only as secure as their configuration. The following are critical security requirements:

  • Always use HTTPS. OAuth 2.0 relies entirely on transport-layer security. Tokens sent over HTTP can be intercepted.
  • Validate the state parameter. Without state validation, the flow is vulnerable to CSRF attacks where an attacker forces the victim to complete an OAuth flow with the attacker's account.
  • Use PKCE for all clients. Even confidential clients benefit from PKCE as a defense-in-depth measure against code interception.
  • Validate redirect URIs exactly. Use exact string matching, not pattern matching. Open redirectors in redirect URIs allow attackers to steal authorization codes.
  • Store tokens securely. Access tokens in memory, refresh tokens in HttpOnly cookies or secure server-side storage.
  • Rotate refresh tokens. Issue a new refresh token with each use and detect token reuse as a sign of compromise.
  • Use short-lived access tokens. Access tokens should expire in minutes, not hours or days.
  • Validate ID tokens thoroughly (OIDC): check iss, aud, exp, nonce, and signature.
  • Never use the implicit flow. Use authorization code with PKCE for all browser-based applications.
  • Restrict client registration. Only allow trusted applications to register as OAuth clients.

References

  • Hardt, D. (2012). RFC 6749: The OAuth 2.0 Authorization Framework. IETF.
  • Jones, M. and Hardt, D. (2012). RFC 6750: The OAuth 2.0 Authorization Framework: Bearer Token Usage. IETF.
  • Sakimura, N. et al. (2015). RFC 7636: Proof Key for Code Exchange by OAuth Public Clients. IETF.
  • Sakimura, N. et al. (2014). OpenID Connect Core 1.0. OpenID Foundation.
  • Lodderstedt, T. et al. (2023). RFC 9700: OAuth 2.0 Security Best Current Practice. IETF.
  • Richer, J. and Sanso, A. (2017). OAuth 2 in Action. Manning Publications.
  • Parecki, A. (2020). OAuth 2.0 Simplified. Lulu.com.
  • OWASP Foundation. (2023). OWASP OAuth Security Cheat Sheet. OWASP.