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:
| Role | Description | Example |
|---|---|---|
| Resource Owner | The user who owns the data and grants access | A GitHub user who authorizes a CI tool |
| Client | The application requesting access on behalf of the user | The CI tool (e.g., Jenkins, CircleCI) |
| Authorization Server | The server that authenticates the user and issues tokens | GitHub's OAuth server |
| Resource Server | The API that hosts the protected resources | GitHub'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.
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 issuedPKCE 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 expiresSince 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.
| Aspect | Implicit Flow | Authorization Code + PKCE |
|---|---|---|
| Token Delivery | URL fragment (front channel) | Server-to-server (back channel) |
| Token Exposure | Browser history, Referer header, logs | Not exposed to browser |
| Refresh Tokens | Not supported | Supported |
| Token Injection | Vulnerable | Protected by code_verifier |
| Status | Deprecated by OAuth 2.1 | Recommended 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 Type | Purpose | Lifetime | Format |
|---|---|---|---|
| Authorization Code | Exchanged for tokens (one-time use) | Seconds (typically 30-60s) | Opaque string |
| Access Token | Authorize API requests | Minutes to hours | JWT or opaque |
| Refresh Token | Obtain new access tokens | Days to months | Opaque string |
| ID Token (OIDC) | Prove user identity to the client | Minutes | JWT (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:usersSecurity 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.