OAuth 2.0

Backend Fundamentals · Backend Security · 9 min read

1. What problem does this solve?

ProblemWhy it mattersConsequence if ignoredReal‑world appearance
Credential leakage – users often reuse passwords or share them with third‑party apps.Exposes user accounts to compromise and data breaches.Unauthorized access, data loss, regulatory fines.Social login (Google, GitHub), SaaS integrations.
Fine‑grained access control – an app may need only read access to a user’s calendar, not full profile data.Improves security by limiting what an attacker can do if a token is stolen.Over‑privileged access, potential for mass data exfiltration.API gateways, microservice boundaries.
Stateless authentication – scaling horizontally without session stores.Enables load‑balanced, stateless services that can be deployed in containers or serverless.Need for shared session stores, increased latency.Cloud‑native APIs, serverless functions.
Delegated authorization – a user authorizes a third‑party app to act on their behalf without giving the app their password.Enables OAuth‑based “Login with X” flows and API access.Users must trust each app with their credentials.OAuth providers (Google, Microsoft), GitHub Apps.
Token revocation & expiration – ability to revoke access when a user logs out or changes a password.Keeps compromised tokens from being reused.Stale tokens remain valid, leading to silent data leaks.Revocation endpoints, introspection.

2. Mental model

Analogy – The Library Card

LibraryLibrary CardBooksBorrowing
Resource Owner (you)Access TokenResource Server (books)Authorization Server (librarian)
The card is valid for a limited time and can be returned (revoked).You don’t hand the card to each book vendor.You can read books without giving your password to each vendor.The librarian checks the card before lending a book.

Takeaway: An access token is a temporary, scoped credential that lets a client (app) act on behalf of a resource owner (user) at a resource server (API), without exposing the owner’s password.

Engineering model

+----------------+          +----------------+          +----------------+
| Resource Owner |          |  Authorization |          |  Resource      |
| (User)         |          |  Server (Auth) |          |  Server (API)  |
+----------------+          +----------------+          +----------------+
        |                            |                           |
        | 1. Request Authorization   |                           |
        |---------------------------->|                           |
        |                            | 2. Issue Authorization    |
        |                            |    Code (or Token)        |
        |                            |<---------------------------|
        | 3. Exchange Code for Token |                           |
        |<----------------------------|                           |
        |                            | 4. Issue Access Token     |
        |                            |<---------------------------|
        | 5. Call API with Token     |                           |
        |---------------------------->|                           |
        |                            | 6. Validate Token         |
        |                            |<---------------------------|
        |                            | 7. Return Resource        |

3. Deep explanation

Internals

  1. Authorization Server – issues tokens, authenticates users, stores client registrations.
  2. Resource Server – validates tokens and serves protected resources.
  3. Client – application that requests tokens and calls APIs.
  4. Resource Owner – end user who authorizes the client.

Key terminology

TermMeaning
ClientApp requesting access (confidential or public).
Client ID / SecretCredentials for confidential clients.
Authorization CodeShort‑lived code exchanged for tokens.
Access TokenGrants access to resources.
Refresh TokenLong‑lived token to obtain new access tokens.
ScopePermissions requested (e.g., read:user).
Redirect URIWhere the auth server sends the code.
PKCEProof Key for Code Exchange – mitigates code interception.
IntrospectionEndpoint to check token validity.
ID TokenOIDC extension for identity information.

Trade‑offs

AspectAuthorization Code (PKCE)ImplicitClient Credentials
SecurityHighest – code is short‑lived, PKCE protects mobile/SPAs.Lower – token in URL, risk of leakage.Good for server‑to‑server, no user context.
ComplexityRequires redirect flow, server endpoint.Simpler, but less secure.Simple token request.
Token typeJWT or opaque.JWT.JWT or opaque.
RevocationRefresh token revocation, introspection.Harder – token in browser.Revocation via introspection or blacklist.

Failure modes

  • Token leakage – stored in localStorage, intercepted over insecure channels.
  • Replay attacks – stolen code reused; mitigated by PKCE and one‑time codes.
  • CSRF – redirect URI not validated.
  • Refresh token abuse – stolen refresh token used to generate many access tokens.
  • Clock skew – token expiration mis‑validated.

Limitations

  • Opaque tokens require introspection, adding latency.
  • JWT revocation is non‑trivial; requires blacklist or short lifetimes.
  • PKCE not supported by all legacy clients.
  • Scope granularity limited by provider implementation.

Where it fits

  • API Gateways – validate tokens before routing.
  • Microservices – each service validates tokens locally (JWT) or via introspection.
  • Mobile & SPA – use Authorization Code with PKCE.
  • Server‑to‑Server – Client Credentials flow.
  • JWT – common token format; self‑contained, signed.
  • OAuth 2.0 vs OpenID Connect – OIDC adds identity layer (ID token).
  • Session management – tokens replace server‑side sessions.
  • Redis – token revocation lists, rate limiting.
  • Kafka – event‑driven revocation notifications.

4. How it works – Authorization Code with PKCE

sequenceDiagram
    participant U as User
    participant C as Client (SPA)
    participant A as Authorization Server
    participant R as Resource Server

    U->>C: Click "Login with X"
    C->>A: GET /authorize?response_type=code&client_id=...
    A->>U: Prompt login & consent
    U->>A: POST /login
    A->>U: Redirect to C with ?code=XYZ
    U->>C: Receive code
    C->>A: POST /token
      Note right of C: code=XYZ, code_verifier=...
    A->>C: 200 {access_token, refresh_token}
    C->>R: GET /api/resource
      Note right of C: Authorization: Bearer <access_token>
    R->>C: 200 {data}

Key points

  1. PKCE – client generates a code_verifier and code_challenge; server verifies challenge during token exchange.
  2. Redirect URI – must be registered and validated to prevent open redirects.
  3. Access token – short‑lived (e.g., 15 min) JWT signed by auth server.
  4. Refresh token – long‑lived (e.g., 30 days) stored securely; used to obtain new access tokens.

5. Practical implementation

5.1 Authorization Server (Node.js + Express + oauth2orize)

// auth-server.ts
import express from 'express';
import session from 'express-session';
import bodyParser from 'body-parser';
import { createServer, AuthorizationServer, GrantType } from 'oauth2orize';
import { generateToken, verifyUser, findClient } from './utils';
import jwt from 'jsonwebtoken';
import Redis from 'ioredis';

const redis = new Redis(); // token revocation store

const app = express();
app.use(bodyParser.urlencoded({ extended: false }));
app.use(session({ secret: 's3cr3t', resave: false, saveUninitialized: false }));

const server = new AuthorizationServer();

server.grant(GrantType.AuthorizationCode((client, redirectURI, user, ares, done) => {
  const code = generateToken(32);
  // Store code in Redis with short TTL
  redis.set(`auth_code:${code}`, JSON.stringify({ clientId: client.id, redirectURI, userId: user.id }), 'EX', 300);
  return done(null, code);
}));

server.exchange(GrantType.AuthorizationCode((client, code, redirectURI, done) => {
  redis.get(`auth_code:${code}`, (err, data) => {
    if (err || !data) return done(null, false);
    const { clientId, redirectURI: storedURI, userId } = JSON.parse(data);
    if (clientId !== client.id || storedURI !== redirectURI) return done(null, false);
    // Issue JWT access token
    const accessToken = jwt.sign({ sub: userId, aud: client.id }, 'jwt-secret', { expiresIn: '15m' });
    // Issue opaque refresh token
    const refreshToken = generateToken(64);
    redis.set(`refresh_token:${refreshToken}`, JSON.stringify({ clientId, userId }), 'EX', 60 * 60 * 24 * 30);
    return done(null, accessToken, refreshToken, { expires_in: 900 });
  });
}));

app.get('/authorize', server.authorize((req, clientID, redirectURI, done) => {
  const client = findClient(clientID);
  if (!client) return done(null, false);
  // In real app: render login page
  return done(null, client, redirectURI);
}));

app.post('/authorize', server.decision());

app.post('/token', server.token(), server.errorHandler());

app.listen(3000, () => console.log('Auth server listening on 3000'));

Important lines
redis.set(..., 'EX', 300) – short TTL for auth codes.
jwt.sign(..., { expiresIn: '15m' }) – short‑lived access token.
redis.set(..., 'EX', 60 * 60 * 24 * 30) – 30‑day refresh token.

5.2 Resource Server (Node.js + Express + express-jwt)

// resource-server.ts
import express from 'express';
import jwt from 'express-jwt';
import jwksRsa from 'jwks-rsa';
import Redis from 'ioredis';

const redis = new Redis();

const app = express();

const checkJwt = jwt({
  secret: jwksRsa.expressJwtSecret({
    cache: true,
    rateLimit: true,
    jwksRequestsPerMinute: 5,
    jwksUri: 'http://localhost:3000/.well-known/jwks.json'
  }),
  audience: 'client-id',
  issuer: 'http://localhost:3000',
  algorithms: ['RS256']
});

app.get('/api/user', checkJwt, async (req, res) => {
  // Token is valid; payload available in req.user
  res.json({ userId: req.user.sub, message: 'Protected data' });
});

app.listen(4000, () => console.log('Resource server listening on 4000'));

Important lines
jwksUri – fetches signing keys from auth server.
audience – ensures token is intended for this API.

5.3 Client (Next.js + next-auth)

// pages/api/auth/[...nextauth].ts
import NextAuth from 'next-auth';
import Providers from 'next-auth/providers';

export default NextAuth({
  providers: [
    Providers.OAuth({
      id: 'custom',
      name: 'Custom OAuth',
      type: 'oauth',
      version: '2.0',
      scope: 'read:user',
      params: { grant_type: 'authorization_code' },
      accessTokenUrl: 'http://localhost:3000/token',
      authorizationUrl: 'http://localhost:3000/authorize?response_type=code',
      profileUrl: 'http://localhost:3000/userinfo',
      clientId: 'client-id',
      clientSecret: 'client-secret',
      // PKCE is automatically handled by next-auth
    })
  ],
  callbacks: {
    async jwt(token, user) {
      // token contains access_token, refresh_token
      return token;
    }
  }
});

6. Production considerations

ConcernWhat to do
ScalabilityUse stateless JWTs; cache public keys; avoid DB lookups for each request.
ReliabilityDeploy auth server behind a load balancer; use sticky sessions only for auth code flow.
SecurityEnforce HTTPS everywhere; use PKCE for public clients; rotate signing keys; short access token lifetimes; store refresh tokens in secure HTTP‑Only cookies or encrypted storage.
ObservabilityLog token issuance, revocation events; expose metrics (oauth_tokens_issued, oauth_tokens_revoked).
PerformancePrefer JWT over introspection; if introspection needed, cache results.
Error handlingReturn RFC 6749 error codes (invalid_grant, invalid_client).
Resource usageKeep token payload minimal; avoid storing large objects in JWT.
OperationalAutomate key rotation; monitor Redis memory for revocation lists; set up alerts for high token revocation rates.
Failure recoveryImplement graceful fallback: if auth server down, serve cached tokens for a short grace period.
DeploymentContainerize with Docker; use environment variables for secrets; separate auth and resource servers for isolation.

7. Common mistakes

MistakeWhy it’s problematic
Using Implicit flow for SPAsTokens are exposed in URL fragment; vulnerable to XSS and leakage.
Storing tokens in localStorageXSS can read them; use HTTP‑Only cookies instead.
Not using PKCE for mobile/SPAsAuthorization code can be intercepted and exchanged.
Long‑lived access tokensIncreases window for abuse if stolen.
Ignoring scope validationClient may access resources beyond its intended permissions.
Not revoking refresh tokensStale tokens remain valid; attacker can keep generating access tokens.
Using opaque tokens without introspectionResource server cannot validate tokens without hitting auth server, causing latency.

8. Senior engineer perspective

Architecture decisions

  • JWT vs opaque: JWT gives stateless validation but hard to revoke; opaque + introspection allows revocation but adds network hop.
  • Key management: rotate signing keys, expose JWKS endpoint, use asymmetric keys (RS256) for easier key rotation.
  • Token revocation strategy: Redis blacklist, short access token TTL + refresh token revocation, or event‑driven revocation via Kafka.
  • Distributed auth: Deploy auth server in multiple zones; use sticky sessions only for the short auth code exchange.

Trade‑offs

DecisionProsCons
JWTStateless, fast validationHard to revoke, larger payload
Opaque + introspectionFine‑grained revocationExtra latency, single point of failure
Short access tokens + refreshLimits damageRequires refresh token handling
PKCEMitigates interceptionAdds complexity to client

Bottlenecks

  • Auth server DB: client lookup, user authentication, token storage.
  • Redis revocation list: memory usage grows with number of revoked tokens.
  • JWT signing: CPU load for RSA signing; consider hardware acceleration.

Failure scenarios

  • Token theft: Mitigate with short lifetimes, token binding, device fingerprinting.
  • Revocation lag: Use event streams to propagate revocation quickly.
  • Clock skew: Synchronize NTP; validate iat and exp carefully.

Monitoring

  • Token metrics: issuance rate, revocation rate, average token age.
  • Auth server health: response times, error rates.
  • Redis health: memory usage, eviction count.

Cost/performance

  • JWT: minimal DB hits → lower cost, but larger payload → higher bandwidth.
  • Redis: memory cost; use LRU eviction for short‑lived tokens.
  • Kafka: high throughput for revocation events; cost of running brokers.

When NOT to use OAuth 2.0

  • Internal services with mutual TLS: simpler to use mTLS and service principals.
  • Single‑tenant, low‑traffic apps: a simple session cookie may suffice.
  • Highly regulated environments: consider custom identity solutions with stricter controls.

9. Interview questions

1BeginnerWhat is the difference between authentication and authorization?
Answer
Authentication verifies who you are; authorization decides what you can do.
2BeginnerExplain the Authorization Code flow in OAuth 2.0.
Answer
User authenticates at auth server → receives code → client exchanges code for access token → uses token to call APIs.
3SeniorHow would you design a token revocation strategy for a distributed system with millions of users?
Answer
Use short‑lived JWTs + refresh tokens stored in Redis; revoke by adding token ID to blacklist; propagate revocation via Kafka; cache introspection results.
4SeniorCompare JWT access tokens and opaque tokens in terms of scalability and revocation.
Answer
JWTs are stateless → scale horizontally; revocation hard → rely on short lifetimes or blacklist. Opaque tokens require introspection → extra latency but allow immediate revocation.
5ScenarioA client receives a 401 after refreshing the token. What could be wrong?
Answer
Refresh token expired or revoked; client sent wrong client credentials; token audience mismatch; clock skew causing exp check to fail.

10. Hands‑on task

Build a minimal OAuth 2.0 Authorization Server

  1. Create a new Node.js project with express, oauth2orize, jsonwebtoken, and ioredis.
  2. Register a client with a client ID and secret.
  3. Implement the Authorization Code flow with PKCE.
  4. Issue JWT access tokens and opaque refresh tokens.
  5. Store refresh tokens in Redis with a 30‑day TTL.
  6. Add a protected /api/profile endpoint that validates the access token.
  7. Test the flow using curl or Postman:
    • Request /authorize → login → get code → exchange for tokens → call /api/profile.

Deliverables

  • auth-server.ts (authorization server)
  • resource-server.ts (resource server)
  • README.md with setup and test instructions

11. Quick revision

  • OAuth 2.0 separates authorization (granting access) from authentication (verifying identity).
  • Authorization Server issues tokens; Resource Server validates them.
  • Access Token – short‑lived, scoped credential.
  • Refresh Token – long‑lived, used to obtain new access tokens.
  • PKCE protects public clients from code interception.
  • JWT tokens are stateless; opaque tokens require introspection.
  • Revocation: blacklist in Redis, short lifetimes, or event streams.
  • Scopes limit permissions; validate on each request.
  • HTTPS mandatory; never expose tokens in URLs or localStorage.
  • Error codes follow RFC 6749 (invalid_grant, invalid_client, etc.).
  • Monitoring: track token issuance, revocation, and auth server health.
  • Key rotation: expose JWKS endpoint; rotate keys without downtime.

12. References