The Problem OAuth Solves
Before OAuth, third-party apps asked for your password — the actual password — and stored it to make requests on your behalf. There was no way to revoke access without changing your password, and no way to limit what the app could do. OAuth exists because that approach is obviously catastrophic, and the industry needed a standard way to grant limited, revocable access without credential sharing.
OAuth 2.0 (defined in RFC 6749) solves this by introducing delegated authorization: you grant an application limited, scoped access to a resource without sharing your credentials. The app gets a temporary access token for specific operations, which you can revoke independently of your password.
Core Concepts and Roles
OAuth 2.0 defines four roles:
- Resource Owner: The user who owns the data (you).
- Client: The application requesting access (the third-party app).
- Authorization Server: Issues access tokens after authenticating the user (Google, GitHub, your auth service).
- Resource Server: The API holding the protected data (Gmail API, GitHub API).
The authorization server and resource server can be the same service or separate — for example, Google's authorization server issues tokens used to access Gmail's resource server.
The Authorization Code Flow
The Authorization Code flow is the most secure and recommended flow for web applications and mobile apps. Here's the full sequence:
- Authorization Request: The client redirects the user to the authorization server with the requested scopes.
- User Consent: The authorization server authenticates the user and asks them to grant or deny the requested permissions.
- Authorization Code: On approval, the server redirects back to the client with a short-lived authorization code.
- Token Exchange: The client sends the authorization code plus its client secret to the token endpoint, receiving an access token (and optionally a refresh token).
- API Access: The client uses the access token to call the resource server.
# Step 1: Authorization URL https://auth.example.com/authorize? response_type=code& client_id=YOUR_CLIENT_ID& redirect_uri=https://yourapp.com/callback& scope=email+profile& state=RANDOM_STATE_VALUE # Step 4: Token exchange (server-to-server) POST https://auth.example.com/token Content-Type: application/x-www-form-urlencoded grant_type=authorization_code& code=AUTH_CODE_HERE& redirect_uri=https://yourapp.com/callback& client_id=YOUR_CLIENT_ID& client_secret=YOUR_CLIENT_SECRET
PKCE: Authorization Code for Public Clients
Mobile apps and single-page apps can't safely store a client secret — the code is visible to users. PKCE (Proof Key for Code Exchange, pronounced "pixie") solves this by replacing the client secret with a cryptographic challenge:
// 1. Generate a code verifier (random string) const verifier = generateRandomString(64); // 2. Hash it to create the code challenge const challenge = base64url(sha256(verifier)); // 3. Send challenge in authorization request code_challenge=CHALLENGE& code_challenge_method=S256 // 4. Send verifier in token exchange (instead of client secret) code_verifier=VERIFIER
The authorization server hashes the verifier and checks it matches the challenge. Since only the original app has the verifier, intercepted authorization codes are useless.
Other Grant Types
Client Credentials Flow
Used for machine-to-machine (M2M) authentication where no user is involved — microservices, background jobs, scheduled tasks. The client authenticates directly with its credentials to get a token:
POST /token grant_type=client_credentials& client_id=SERVICE_ID& client_secret=SERVICE_SECRET& scope=read:data
Device Authorization Flow
For input-constrained devices like smart TVs or CLI tools. The device displays a short code and URL. The user visits the URL on another device, enters the code, and the original device polls for the token. This is how "activate on google.com/device" works.
Access Tokens and Refresh Tokens
After a successful token exchange, the authorization server returns:
- Access Token: Short-lived (typically 1 hour). Sent in the Authorization header with every API request. Can be a JWT or opaque token.
- Refresh Token: Long-lived. Used only to obtain new access tokens when they expire. Stored securely, never sent to the resource server.
- ID Token: Present when using OpenID Connect. A JWT containing user identity claims.
# Using the access token GET /api/userinfo Authorization: Bearer eyJhbGciOiJSUzI1NiJ9... # Refreshing the access token POST /token grant_type=refresh_token& refresh_token=REFRESH_TOKEN& client_id=YOUR_CLIENT_ID
OAuth 2.0 vs OpenID Connect
This is a crucial distinction that confuses many developers:
- OAuth 2.0 is an authorization framework. It answers: "Can this app access this resource on behalf of this user?" It says nothing about who the user is.
- OpenID Connect (OIDC) is an authentication layer built on top of OAuth 2.0. It adds an ID Token (a JWT) containing claims about the user: sub (user ID), name, email, picture. It answers: "Who is this user?"
When you use "Sign in with Google," you're using OpenID Connect: Google authenticates you and returns an ID token with your identity, plus an access token to call Google APIs. The openid scope triggers OIDC behavior.
# OIDC scope triggers ID token issuance scope=openid+email+profile # ID Token (JWT) contains identity claims { "sub": "1234567890", // user ID (stable, unique) "email": "user@example.com", "name": "Jane Doe", "iss": "https://accounts.google.com", "aud": "YOUR_CLIENT_ID", "exp": 1738000000, "iat": 1737996400 }
Scopes: Limiting Access
Scopes define what access the client is requesting. Common patterns include:
# GitHub scopes repo # Full control of repositories read:user # Read user profile data user:email # Access user email addresses # Google scopes openid email profile https://www.googleapis.com/auth/gmail.readonly # Custom API scopes read:orders write:orders admin
Always request the minimum scopes your application needs. Users are more likely to approve limited, clearly explained permissions, and a compromised access token can only access scoped resources.
OAuth 2.0 has many security subtleties: validating state parameters to prevent CSRF, verifying ID token signatures and claims, handling token revocation, and securing token storage. Use a battle-tested library (Auth0, Passport.js, Spring Security, python-social-auth) rather than implementing the protocol yourself. Even experienced developers make implementation mistakes that create security vulnerabilities.
Conclusion
OAuth 2.0 and OpenID Connect are the foundation of modern authorization and authentication on the web. OAuth 2.0 lets users grant apps limited access without sharing credentials. OpenID Connect adds identity on top of OAuth. The Authorization Code flow with PKCE is the right choice for most applications. Understanding these protocols helps you build more secure applications, integrate third-party services correctly, and debug authentication issues when they arise.