API Security OAuth 2.0
OAuth 2.0 is the standard authorization framework used by almost every major platform on the internet. When you click "Login with Google" or "Connect your GitHub account," OAuth 2.0 is doing the work. Understanding how it works — and where it can go wrong — is essential for anyone building or securing modern APIs.
The Problem OAuth 2.0 Solves
Before OAuth existed, sharing access between apps was dangerous. Imagine a third-party app that wants to read your Google contacts. Without OAuth, the only option was to give that app your actual Google username and password. The app then had full access to your entire account — not just contacts, but email, drive, calendar, and everything else. And you had no way to revoke that access without changing your password.
OAuth 2.0 solves this with delegated access: you authorize a specific app to access a specific part of your account, for a limited time, without ever sharing your password.
Before OAuth: You → give your Google password → to ThirdPartyApp ThirdPartyApp has full account access forever To revoke: change your Google password With OAuth 2.0: You → authorize → ThirdPartyApp to read contacts only ThirdPartyApp gets a temporary token, not your password To revoke: click "Remove access" in Google settings Your password is never shared
The Four Key Roles in OAuth 2.0
Role 1: Resource Owner The user who owns the data and grants permission. Example: You — the person who owns the Google contacts. Role 2: Client The application requesting access to the data. Example: A contacts-import app that wants your Google contacts. Role 3: Authorization Server The server that authenticates the user and issues tokens. Example: accounts.google.com — Google's login and consent system. Role 4: Resource Server The API that holds the actual data. Example: contacts.googleapis.com — the API that stores your contacts. The Authorization Server and Resource Server can be the same system (common for smaller platforms) or separate systems (common at scale).
OAuth 2.0 Grant Types
OAuth 2.0 defines several "grant types" — different flows for obtaining access tokens depending on the type of application and use case. The four most important are Authorization Code, Client Credentials, Implicit (deprecated), and Device Code.
Grant Type 1: Authorization Code Flow
This is the most secure and widely used flow. It is designed for web and mobile applications where a user is present and can interact with a browser.
Authorization Code Flow – Step by Step:
1. User clicks "Login with Google" on ThirdPartyApp
2. ThirdPartyApp redirects browser to Authorization Server:
GET https://accounts.google.com/oauth/authorize
?client_id=app_client_id
&redirect_uri=https://thirdpartyapp.com/callback
&scope=contacts.read
&response_type=code
&state=randomstring123
3. Authorization Server shows login + consent screen:
"ThirdPartyApp wants to: Read your contacts. Allow or Deny?"
4. User clicks Allow.
Authorization Server redirects back:
GET https://thirdpartyapp.com/callback
?code=AUTH_CODE_XYZ
&state=randomstring123
5. ThirdPartyApp's backend exchanges the code for tokens:
POST https://accounts.google.com/oauth/token
client_id=app_client_id
client_secret=app_secret
code=AUTH_CODE_XYZ
grant_type=authorization_code
6. Authorization Server returns:
{
"access_token": "eyJhb...",
"expires_in": 3600,
"refresh_token": "1//xEod...",
"scope": "contacts.read"
}
7. ThirdPartyApp uses access_token to call Resource Server:
GET https://contacts.googleapis.com/v1/people
Authorization: Bearer eyJhb...
Why the Code Exchange Step Matters
The authorization code returned in step 4 travels through the browser URL — visible in browser history and server logs. But this code is short-lived and useless without the client secret. The actual tokens are obtained in step 5, which is a direct server-to-server call, never touching the browser. This separation keeps the valuable tokens off the browser entirely.
The PKCE Extension (Proof Key for Code Exchange)
Mobile apps and single-page web apps cannot safely store a client secret — the code is public. PKCE was designed to protect the Authorization Code flow for these environments without needing a client secret.
PKCE Flow Addition: Before Step 2: App generates a random "code_verifier" (43-128 random chars) App computes "code_challenge" = Base64URL(SHA256(code_verifier)) Step 2 includes: &code_challenge=BASE64URL_HASH &code_challenge_method=S256 Step 5 includes: code_verifier=ORIGINAL_RANDOM_VALUE Authorization Server verifies: SHA256(code_verifier) == code_challenge stored from step 2 Even if an attacker intercepts the auth code, they cannot exchange it without the original code_verifier which only the legitimate app knows.
Grant Type 2: Client Credentials Flow
This flow is for machine-to-machine communication where no user is involved. A background service authenticates using its own credentials to get a token.
Client Credentials Flow:
Service A needs to call Service B's API.
No user is present.
POST https://auth.company.com/oauth/token
grant_type=client_credentials
client_id=service_a_id
client_secret=service_a_secret
scope=reports.read
Authorization Server returns:
{ "access_token": "...", "expires_in": 3600 }
Service A calls Service B:
GET /api/reports
Authorization: Bearer access_token_here
Use case: Microservices, batch jobs, backend integrations.
Never involves a user login or consent screen.
Grant Type 3: Device Code Flow
Designed for devices without browsers — smart TVs, gaming consoles, CLI tools. The device shows a code, and the user enters it on another device with a browser.
Device Code Flow: TV App requests code from Authorization Server: POST /oauth/device/code client_id=tv_app_id scope=media.read Server returns: device_code: DEVICE_CODE_XYZ user_code: ABCD-1234 verification_uri: https://auth.service.com/activate TV displays: "Visit auth.service.com/activate and enter ABCD-1234" User visits URL on phone, logs in, enters ABCD-1234, clicks Allow. TV app polls the token endpoint until it receives the access token.
Access Tokens and Refresh Tokens
Access Token:
Short-lived credential used to call the Resource Server.
Typical lifetime: 15 minutes to 1 hour.
Should be treated like a password — kept secret.
Sent in every API call: Authorization: Bearer <token>
Refresh Token:
Long-lived credential used to get new access tokens.
Typical lifetime: days to months.
Stored securely by the client app.
NEVER sent to the Resource Server directly.
Used only with the Authorization Server to get fresh access tokens.
Token Refresh Flow:
POST /oauth/token
grant_type=refresh_token
refresh_token=1//xEod...
client_id=app_id
client_secret=app_secret
→ Returns new access_token (and optionally a new refresh_token)
OAuth 2.0 Scopes
Scopes define exactly what permissions an access token grants. They enforce least-privilege access.
Scope Examples from Real Platforms: Google: contacts.read → Read contacts only contacts.readwrite → Read and modify contacts gmail.readonly → Read emails only drive.file → Access only files created by this app GitHub: repo → Full repository access repo:status → Access commit status only read:user → Read user profile info write:packages → Upload packages to GitHub Packages Custom API Scopes: orders:read → View orders orders:write → Create and modify orders admin:users → Manage user accounts (admin only)
When a client requests a token with minimal scopes, even a stolen token causes limited damage. A stolen contacts.read token cannot send emails or delete files.
Common OAuth 2.0 Security Vulnerabilities
Vulnerability 1: Missing State Parameter (CSRF)
The state parameter prevents CSRF attacks during OAuth: Without state: Attacker initiates OAuth flow on victim's account. Tricks victim into clicking the callback URL. Victim's browser completes the flow, linking their account to the attacker's app. With state: App generates random state value before redirect. After callback, app verifies the returned state matches. Attacker cannot predict or replay state → attack fails. Always: generate cryptographically random state, store it in server session, verify it matches on callback return.
Vulnerability 2: Open Redirect in Redirect URI
If the Authorization Server does not strictly validate redirect_uri: Registered redirect URI: https://goodapp.com/callback Attacker's request: &redirect_uri=https://evil.com/capture If server allows partial matching or wildcards: Authorization code is delivered to evil.com Attacker exchanges code for access token Full account takeover Fix: Authorization Server must validate redirect_uri as an exact match against the pre-registered value. No wildcards. No partial matches.
Vulnerability 3: Token Leakage via Referrer Headers
If an access token appears in a URL (which the Implicit flow did — now deprecated), and the application loads any external resource (images, analytics scripts), the token may leak in the HTTP Referrer header sent to those external servers.
Vulnerability 4: Insufficient Scope Validation
The Resource Server must validate not just that the token is valid, but that it contains the correct scope for the requested operation. A token with contacts.read scope must be rejected when it tries to call contacts.write endpoints.
Vulnerability 5: Insecure Token Storage
Dangerous token storage locations: localStorage (JavaScript-accessible → XSS can steal it) URL parameters (visible in logs, browser history, referrer headers) Unencrypted database columns Plaintext configuration files Secure token storage: Server-side sessions (for web apps) Secure, HttpOnly cookies (cannot be read by JavaScript) OS keychain or secure storage (for mobile apps) Encrypted secrets management system (for backend services)
OAuth 2.0 vs OpenID Connect
OAuth 2.0 is an authorization framework — it grants access to resources. It does not define how to verify a user's identity. OpenID Connect (OIDC) is a thin identity layer built on top of OAuth 2.0 that adds authentication. OIDC is covered in the next topic.
Key Points
- OAuth 2.0 enables delegated access — users authorize apps to access specific data without sharing passwords.
- The Authorization Code flow is the standard for user-facing apps. PKCE extends it securely for mobile and single-page apps.
- Client Credentials flow is used for machine-to-machine authentication with no user present.
- Access tokens are short-lived. Refresh tokens are long-lived but used only to get new access tokens.
- Scopes restrict what a token can do. Always request the minimum scopes needed.
- The state parameter prevents CSRF attacks during the authorization flow.
- Redirect URI must be strictly validated as an exact match to prevent code interception attacks.
