OAuth 2.0 and API Security: The Complete Guide
OAuth 2.0 is the standard for API authorization, but getting the implementation right requires understanding flows, token management, and common pitfalls.
Strategic Systems Architect & Enterprise Software Developer
OAuth 2.0 and API Security: The Complete Guide
OAuth 2.0 is the authorization framework that underpins nearly every modern API. When you log into a service with Google, when a mobile app accesses your calendar, when a third-party integration reads your Salesforce data — OAuth 2.0 is the protocol negotiating what access is granted, to whom, and for how long.
Despite its ubiquity, OAuth implementations are frequently wrong in ways that create serious security vulnerabilities. The protocol is flexible by design, which means there are many correct ways to implement it and even more incorrect ways.
Understanding OAuth 2.0 Flows
OAuth 2.0 defines several authorization flows, each designed for different client types and trust levels. Choosing the wrong flow for your use case is the most common OAuth mistake.
Authorization Code Flow is the standard for server-side web applications. The user is redirected to the authorization server, authenticates, and grants permission. The authorization server redirects back to your application with a short-lived authorization code. Your server exchanges that code for an access token by making a server-to-server request that includes your client secret. The access token never passes through the user's browser.
This flow is secure because the client secret and access token are handled server-side where they cannot be intercepted by browser extensions, network sniffers, or malicious JavaScript. Use this flow for any application with a server component.
Authorization Code Flow with PKCE extends the standard flow for public clients — mobile apps, single-page applications, and desktop apps that cannot securely store a client secret. PKCE (Proof Key for Code Exchange) replaces the client secret with a dynamically generated code verifier and code challenge. The client creates a random string, hashes it, and sends the hash with the authorization request. When exchanging the code for a token, it sends the original random string. The authorization server verifies the hash matches.
PKCE should be used for all public clients. It should also be used for confidential clients as a defense-in-depth measure. There is no reason not to use it.
Client Credentials Flow is for machine-to-machine communication where no user is involved. Your backend service authenticates directly with the authorization server using its client ID and secret, and receives an access token scoped to its service permissions. This is the appropriate flow for backend API integrations, scheduled jobs, and service-to-service communication.
The Implicit Flow was designed for browser-based apps before PKCE existed. It returns the access token directly in the URL fragment. It is deprecated. Do not use it for new implementations. The token is exposed in browser history, referrer headers, and server logs. Use Authorization Code with PKCE instead.
Token Management
Access tokens are the credentials your API uses to authorize requests. Managing them correctly is essential to API security.
Access tokens should be short-lived. Fifteen minutes to one hour is typical. A short lifetime limits the damage if a token is stolen — the attacker has a narrow window before the token expires. This also limits the scope of the CSRF protection surface area since short-lived tokens reduce the value of token theft attacks.
Refresh tokens extend sessions without re-authentication. When an access token expires, the client uses a refresh token to obtain a new access token without requiring the user to log in again. Refresh tokens are longer-lived — days to weeks — and must be stored securely. They should be rotated on each use: when a refresh token is exchanged for a new access token, a new refresh token is also issued and the old one is invalidated.
interface TokenResponse {
access_token: string;
token_type: "Bearer";
expires_in: number;
refresh_token: string;
scope: string;
}
Async function refreshAccessToken(refreshToken: string): Promise<TokenResponse> {
const response = await fetch(TOKEN_ENDPOINT, {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
grant_type: "refresh_token",
refresh_token: refreshToken,
client_id: CLIENT_ID,
client_secret: CLIENT_SECRET,
}),
});
if (!response.ok) {
throw new Error("Token refresh failed — re-authentication required");
}
return response.json();
}
Token storage matters. On the server, tokens should be stored encrypted in a database or secure session store. In the browser, avoid localStorage — it is accessible to any JavaScript running on the page, including XSS payloads. HttpOnly, Secure, SameSite cookies are the safest browser storage mechanism for tokens. For mobile apps, use the platform keychain (iOS Keychain, Android Keystore).
Scopes and Least Privilege
Scopes define what an access token is authorized to do. They are OAuth's mechanism for enforcing least privilege.
Define scopes granularly. Instead of a single "api" scope that grants access to everything, define scopes like "read:users," "write:orders," and "admin:settings." When a third-party application requests access, the user can see exactly what they are granting and can make an informed decision.
Your API must enforce scopes on every request. Possessing a valid access token is not sufficient — the token must contain the specific scope required for the requested operation. A token with "read:users" should be rejected when it attempts to delete a user, even if the token is otherwise valid.
Scope validation is an authorization check, not an authentication check. It happens after the token is verified as valid and the identity is established. This is where the principles of API security intersect with OAuth — your API needs both a valid token and appropriate scope for every operation.
Common OAuth Security Pitfalls
State parameter omission is a frequent vulnerability. The state parameter in the authorization request prevents CSRF attacks against the OAuth flow. Without it, an attacker can initiate an OAuth flow and trick a victim into completing it, linking the attacker's account to the victim's session. Always generate a random state value, store it in the session, and verify it matches when the callback is received.
Redirect URI validation must be exact. If your authorization server accepts any URL under your domain as a redirect URI, an attacker can register a redirect to a page they control (through an open redirect vulnerability) and intercept the authorization code. Validate redirect URIs against a strict allowlist of exact URLs.
Token leakage through logs and error messages happens more often than anyone admits. Access tokens appear in URL query parameters, HTTP headers, and error stack traces. Ensure your logging infrastructure sanitizes authorization headers and that error responses never include token values. A leaked access token in a log file aggregated to a third-party service is a breach waiting to happen.
OAuth 2.0 is not inherently complex, but it has enough flexibility that incorrect implementations are easy to create and difficult to detect without deliberate security review. Use the right flow for your client type, manage tokens with short lifetimes and secure storage, enforce scopes rigorously, and validate every parameter the specification requires.