OAuth & Authentication Concepts

OAuth 2.0, OpenID Connect, JWT, JWKS, PKCE, and modern auth patterns

Security / Auth
Contents
πŸ”

OAuth 2.0 Overview

OAuth 2.0 is an authorization framework that lets third-party apps access a user's resources without exposing their credentials. It delegates authentication to the service that hosts the user account.

Key Roles

RoleDescriptionExample
Resource OwnerThe user who owns the dataYou (the end user)
ClientThe application requesting accessYour web/mobile app
Authorization ServerIssues tokens after authenticationGoogle, Auth0, Keycloak
Resource ServerHosts the protected resources (API)Google Drive API
OAuth 2.0 vs Authentication: OAuth 2.0 is an authorization protocol β€” it answers "what can this app do?" not "who is this user?" For authentication (identity), you need OpenID Connect (OIDC) built on top of OAuth 2.0.

OAuth 2.0 Grant Types

Client App Authorization Server Resource Server β”‚ β”‚ β”‚ │─── Authorization Req ──▢│ β”‚ │◀── Authorization Grant ──│ β”‚ β”‚ β”‚ β”‚ │─── Exchange Grant ──────▢│ β”‚ │◀── Access Token ─────────│ β”‚ β”‚ β”‚ β”‚ │─── API Request + Token ──────────────────────────────▢│ │◀── Protected Resource ────────────────────────────────│
πŸ”‘

Authorization Code Flow

The most secure and commonly used flow for server-side web apps. The client never sees the user's credentials.

Step-by-Step Flow

1. User clicks "Login with Google" 2. App redirects to Authorization Server: GET /authorize? response_type=code& client_id=abc123& redirect_uri=https://app.com/callback& scope=openid profile email& state=random_csrf_token 3. User logs in & consents 4. Auth server redirects back: GET /callback?code=AUTH_CODE&state=random_csrf_token 5. App exchanges code for tokens (server-to-server): POST /token grant_type=authorization_code& code=AUTH_CODE& client_id=abc123& client_secret=SECRET& redirect_uri=https://app.com/callback 6. Auth server returns: { access_token, refresh_token, id_token }
Why use a code? The authorization code is exchanged server-side, so the access token never passes through the browser. The state parameter prevents CSRF attacks.

Token Exchange Request

// Server-side token exchange
const response = await fetch('https://auth.example.com/token', {
  method: 'POST',
  headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
  body: new URLSearchParams({
    grant_type: 'authorization_code',
    code: authCode,
    client_id: 'abc123',
    client_secret: process.env.CLIENT_SECRET,
    redirect_uri: 'https://app.com/callback'
  })
});
const { access_token, refresh_token, id_token } = await response.json();
πŸ›‘οΈ

PKCE (Proof Key for Code Exchange)

PKCE (pronounced "pixy") protects the authorization code flow for public clients (SPAs, mobile apps) that cannot securely store a client secret.

How PKCE Works

StepActionDetail
1Generate code_verifierRandom string, 43–128 chars (A-Z, a-z, 0-9, -, ., _, ~)
2Create code_challengeBASE64URL(SHA256(code_verifier))
3Send challenge in /authorizecode_challenge=...&code_challenge_method=S256
4Send verifier in /tokencode_verifier=ORIGINAL_STRING
5Server verifiesServer hashes verifier and compares with stored challenge
// Generate PKCE code_verifier and code_challenge
function generateCodeVerifier() {
  const array = new Uint8Array(32);
  crypto.getRandomValues(array);
  return btoa(String.fromCharCode(...array))
    .replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
}

async function generateCodeChallenge(verifier) {
  const encoder = new TextEncoder();
  const data = encoder.encode(verifier);
  const digest = await crypto.subtle.digest('SHA-256', data);
  return btoa(String.fromCharCode(...new Uint8Array(digest)))
    .replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
}

// Usage in authorization URL
const verifier = generateCodeVerifier();
const challenge = await generateCodeChallenge(verifier);
// Store verifier in sessionStorage for later use

const authUrl = `https://auth.example.com/authorize?
  response_type=code&
  client_id=abc123&
  code_challenge=${challenge}&
  code_challenge_method=S256&
  redirect_uri=https://app.com/callback`;
Always use S256: The plain method sends the verifier as the challenge (no hashing) β€” only use S256 in production.
πŸ€–

Client Credentials Flow

Used for machine-to-machine (M2M) communication where no user is involved β€” the app authenticates as itself.

// Client Credentials β€” server-to-server auth
const response = await fetch('https://auth.example.com/token', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/x-www-form-urlencoded',
    'Authorization': `Basic ${btoa(`${CLIENT_ID}:${CLIENT_SECRET}`)}`
  },
  body: new URLSearchParams({
    grant_type: 'client_credentials',
    scope: 'api:read api:write'
  })
});
Use cases: Microservice communication, cron jobs, CI/CD pipelines, backend services calling other APIs.
πŸ“±

Implicit & Device Flow

Implicit Flow (Deprecated)

Deprecated in OAuth 2.1: Tokens are returned directly in the URL fragment β€” vulnerable to token leakage. Use Authorization Code + PKCE instead.
AspectImplicitAuth Code + PKCE
Token deliveryURL fragment (#access_token=...)Server-side exchange
Refresh tokensNot supportedSupported
SecurityVulnerable to interceptionProtected by PKCE challenge
StatusDeprecatedRecommended

Device Authorization Flow

For devices with limited input (TVs, CLI tools, IoT). User authorizes on a separate device.

1. Device requests device code: POST /device/code β†’ { device_code, user_code: "ABCD-1234", verification_uri } 2. Device displays: "Go to https://auth.example.com/device and enter ABCD-1234" 3. User opens URL on phone/laptop, enters code, logs in, consents 4. Device polls for token: POST /token { grant_type=urn:ietf:params:oauth:grant-type:device_code, device_code=DEVICE_CODE } 5. Once user approves β†’ returns access_token
🎟️

Access & Refresh Tokens

PropertyAccess TokenRefresh Token
PurposeAuthorize API requestsGet new access tokens
LifetimeShort (5–60 min)Long (days–months)
Sent toResource Server (API)Authorization Server only
FormatJWT or opaque stringUsually opaque
StorageMemory (SPA) or cookieSecure httpOnly cookie
RevocableNot easily (until expiry)Yes, via revocation endpoint

Token Refresh Flow

// Refresh an expired access token
const response = await fetch('https://auth.example.com/token', {
  method: 'POST',
  headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
  body: new URLSearchParams({
    grant_type: 'refresh_token',
    refresh_token: storedRefreshToken,
    client_id: 'abc123'
  })
});
// Returns new { access_token, refresh_token (rotated) }
Refresh Token Rotation: Each use of a refresh token returns a new one and invalidates the old. If a stolen token is used, the server detects reuse and revokes the entire family.
πŸ“

JWT (JSON Web Token)

A compact, URL-safe token format for securely transmitting claims between parties. Consists of three Base64URL-encoded parts separated by dots.

JWT Structure

eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.signature β”œβ”€β”€ Header β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”œβ”€β”€ Payload β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”œβ”€β”€ Signature ─── HEADER: PAYLOAD: SIGNATURE: { { RSASHA256( "alg": "RS256", "sub": "user123", base64UrlEncode(header) + "." + "typ": "JWT", "name": "John", base64UrlEncode(payload), "kid": "key-1" "iat": 1716239022, publicKey } "exp": 1716242622 )

Common Signing Algorithms

AlgorithmTypeKeyUse Case
HS256Symmetric (HMAC)Shared secretSingle service, internal APIs
RS256Asymmetric (RSA)Public/private key pairDistributed systems, OIDC providers
ES256Asymmetric (ECDSA)Elliptic curve keysPerformance-critical, mobile
EdDSAAsymmetric (Ed25519)Edwards curve keysModern high-performance signing
Never use alg: "none": Disabling signature verification is a critical vulnerability. Always validate the alg header against an allowlist.
πŸ“‹

JWT Claims

Claims are key-value pairs in the JWT payload. They carry information about the user, token, and authorization.

Registered (Standard) Claims

ClaimFull NameDescriptionExample
issIssuerWho issued this token"https://auth.example.com"
subSubjectUnique identifier for the user"user_abc123"
audAudienceIntended recipient(s) of the token"https://api.example.com"
expExpirationEpoch time when token expires1716242622
nbfNot BeforeToken not valid before this time1716239022
iatIssued AtWhen the token was created1716239022
jtiJWT IDUnique token identifier (prevents replay)"a1b2c3d4"

Common Custom Claims

{
  "iss": "https://auth.example.com",
  "sub": "user_abc123",
  "aud": "https://api.example.com",
  "exp": 1716242622,
  "iat": 1716239022,
  "scope": "openid profile email",
  "roles": ["admin", "editor"],
  "org_id": "org_xyz",
  "email": "john@example.com",
  "email_verified": true
}

Validation Checklist

πŸ”‘

JWKS (JSON Web Key Set)

A JWKS is a JSON document containing the public keys used to verify JWT signatures. Published at a well-known URL by the authorization server.

JWKS Endpoint

// Typically at: https://auth.example.com/.well-known/jwks.json
{
  "keys": [
    {
      "kty": "RSA",            // Key type
      "kid": "key-2024-01",     // Key ID β€” matches JWT header 'kid'
      "use": "sig",             // Usage: signing
      "alg": "RS256",           // Algorithm
      "n":   "0vx7agoebGc...",  // RSA modulus (Base64URL)
      "e":   "AQAB"             // RSA exponent
    }
  ]
}

How JWT + JWKS Verification Works

1. Client sends JWT to Resource Server 2. Server reads JWT header β†’ extracts "kid" (key ID) 3. Server fetches JWKS from auth server (cached) 4. Server finds the matching key by "kid" 5. Server verifies JWT signature using the public key 6. If valid β†’ process the claims

Key Rotation

πŸͺͺ

OpenID Connect (OIDC)

OIDC is an identity layer on top of OAuth 2.0. While OAuth handles authorization, OIDC adds standardized authentication β€” telling your app who the user is.

What OIDC Adds to OAuth 2.0

FeatureOAuth 2.0OIDC (OAuth 2.0 + Identity)
PurposeAuthorization (access to resources)Authentication (user identity)
ID TokenNot definedJWT with user claims
UserInfo endpointNot standardized/userinfo
DiscoveryNot standardized/.well-known/openid-configuration
Standard scopesApp-definedopenid, profile, email, address, phone

ID Token Claims

// ID token payload (returned when scope includes "openid")
{
  "iss": "https://auth.example.com",
  "sub": "user_abc123",
  "aud": "client_abc123",      // Your app's client_id
  "exp": 1716242622,
  "iat": 1716239022,
  "nonce": "n-0S6_WzA2Mj",    // Prevents replay attacks
  "auth_time": 1716239020,     // When user actually authenticated
  "name": "John Doe",
  "email": "john@example.com",
  "email_verified": true,
  "picture": "https://example.com/john.jpg"
}

Discovery Document

Every OIDC provider publishes a discovery document at /.well-known/openid-configuration:

{
  "issuer": "https://auth.example.com",
  "authorization_endpoint": "https://auth.example.com/authorize",
  "token_endpoint": "https://auth.example.com/token",
  "userinfo_endpoint": "https://auth.example.com/userinfo",
  "jwks_uri": "https://auth.example.com/.well-known/jwks.json",
  "scopes_supported": ["openid", "profile", "email"],
  "response_types_supported": ["code"],
  "id_token_signing_alg_values_supported": ["RS256"]
}
🎯

Scopes & Consent

Scopes define what access the client is requesting. The user sees a consent screen listing these permissions.

Standard OIDC Scopes

ScopeClaims Returned
openidsub (required for OIDC β€” triggers ID token)
profilename, family_name, given_name, picture, locale
emailemail, email_verified
addressaddress (structured JSON)
phonephone_number, phone_number_verified
offline_accessGrants a refresh token

Custom API Scopes

// Common patterns for API scopes
"scope": "read:users write:users delete:users"    // Resource-based
"scope": "api.read api.write api.admin"            // Namespace-based
"scope": "repos:read gists:write orgs:admin"       // GitHub-style
πŸ”

Token Introspection & Revocation

Token Introspection (RFC 7662)

Allows resource servers to check if an opaque token is valid and get its metadata.

// POST to introspection endpoint
POST /introspect
Authorization: Basic base64(client_id:client_secret)
Content-Type: application/x-www-form-urlencoded

token=ACCESS_TOKEN&token_type_hint=access_token

// Response
{
  "active": true,
  "sub": "user_abc123",
  "client_id": "my_app",
  "scope": "read write",
  "exp": 1716242622,
  "token_type": "Bearer"
}

Token Revocation (RFC 7009)

// Revoke a token (usually refresh token on logout)
POST /revoke
Authorization: Basic base64(client_id:client_secret)
Content-Type: application/x-www-form-urlencoded

token=REFRESH_TOKEN&token_type_hint=refresh_token

// Returns 200 OK (always, even if token was already invalid)
JWT vs Opaque: JWTs can be verified locally (no network call). Opaque tokens require introspection. JWTs are harder to revoke β€” you need a token blocklist or short expiry + refresh tokens.
πŸ›‘οΈ

Security Best Practices

Token Storage (Frontend)

StorageXSS RiskCSRF RiskRecommendation
localStorageVulnerableSafeAvoid for sensitive tokens
sessionStorageVulnerableSafeOK for short-lived tokens
httpOnly cookieSafeVulnerableBest β€” add CSRF protection
In-memory variableSafeSafeBest for SPAs (lost on refresh)

Checklist

Common Vulnerabilities

AttackDescriptionPrevention
CSRFAttacker tricks user into authorizing a malicious requeststate parameter
Token LeakageToken exposed in URL, logs, or referrer headerUse auth code flow, not implicit
Code InterceptionAuth code stolen in transit (mobile deep links)PKCE
Replay AttackStolen token reusednonce, jti, short expiry
alg:noneForged JWT with disabled signatureValidate alg against allowlist
SSRF via redirect_uriRedirect to attacker-controlled URLExact URI matching, pre-registered URIs
βš–οΈ

Protocol & Flow Comparison

When to Use Which Flow

ScenarioRecommended FlowWhy
Server-side web appAuthorization CodeClient secret stored securely on server
SPA (React, Vue, etc.)Auth Code + PKCENo client secret; PKCE protects the flow
Mobile appAuth Code + PKCESame as SPA; use custom URL scheme for redirect
CLI / deviceDevice AuthorizationNo browser; user authorizes on separate device
Server-to-server (M2M)Client CredentialsNo user involved; app authenticates as itself
Need user identityOIDC (any flow + openid scope)Returns ID token with user claims

OAuth 2.0 vs OAuth 2.1

ChangeOAuth 2.0OAuth 2.1 (Draft)
PKCEOptionalRequired for all clients
Implicit flowAllowedRemoved
Password grantAllowedRemoved
Redirect URILoose matchingExact string matching required
Refresh tokensNo rotation requiredRotation recommended
Back to top