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.

Leave a Comment