All topics
General · Learning hub

Authentication & Authorization notes for developers

Master Authentication & Authorization with a curated set of 4 developer notes — core concepts, patterns, and interview prep. Maintained by the DevRecall team.

Save this stack to your DevRecallMore General notes
Authentication & Authorization

OAuth 2.0 & OpenID Connect

Authentication: OAuth 2.0 & OpenID Connect OAuth 2.0 is an authorization framework — it lets users grant third-party apps access to their resources without shar

Authentication: OAuth 2.0 & OpenID Connect

OAuth 2.0 is an authorization framework — it lets users grant third-party apps access to their resources without sharing credentials. OpenID Connect (OIDC) is a thin identity layer on top of OAuth 2.0 that adds authentication.

OAuth 2.0 Roles

  • Resource Owner — the user who owns the data

  • Client — the application requesting access (your app)

  • Authorization Server — issues tokens (Google, GitHub, Auth0, your own)

  • Resource Server — the API being accessed (Google APIs, GitHub API)

Grant Types (Flows)

Authorization Code + PKCE  — for user-facing web/mobile apps (RECOMMENDED)
  1. App redirects user to Authorization Server with code_challenge (PKCE)
  2. User authenticates and grants consent
  3. AS redirects back with authorization_code
  4. App exchanges code + code_verifier for access_token + refresh_token
  5. App uses access_token to call Resource Server APIs

  PKCE (Proof Key for Code Exchange): prevents auth code interception attacks.
  Required for public clients (SPAs, mobile apps). Use for all new flows.

Client Credentials — for machine-to-machine (no user involved)
  1. Service POSTs client_id + client_secret to AS
  2. AS returns access_token (no refresh token)
  3. Service uses access_token to call other services

Device Flow — for CLIs, smart TVs (no browser)
  1. App requests device_code and shows user_code to user
  2. User visits URL and enters user_code on another device
  3. App polls for token until user approves

Implicit flow: DEPRECATED — use Authorization Code + PKCE instead
Resource Owner Password: DEPRECATED — share credentials directly (legacy only)

Authorization Code + PKCE Flow

// 1. Generate PKCE code verifier and challenge
const codeVerifier = base64urlEncode(crypto.randomBytes(32))
const codeChallenge = base64urlEncode(sha256(codeVerifier))

// 2. Redirect to Authorization Server
const authUrl = new URL('https://auth.example.com/oauth/authorize')
authUrl.searchParams.set('response_type', 'code')
authUrl.searchParams.set('client_id', 'my-client-id')
authUrl.searchParams.set('redirect_uri', 'https://app.example.com/callback')
authUrl.searchParams.set('scope', 'openid email profile')
authUrl.searchParams.set('state', randomState)          // CSRF protection
authUrl.searchParams.set('code_challenge', codeChallenge)
authUrl.searchParams.set('code_challenge_method', 'S256')
window.location.href = authUrl.toString()

// 3. Handle callback (app receives code)
const code = new URLSearchParams(window.location.search).get('code')
// verify state matches

// 4. Exchange code for tokens
const response = await fetch('https://auth.example.com/oauth/token', {
  method: 'POST',
  headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
  body: new URLSearchParams({
    grant_type: 'authorization_code',
    code,
    redirect_uri: 'https://app.example.com/callback',
    client_id: 'my-client-id',
    code_verifier: codeVerifier,
  }),
})

OpenID Connect (OIDC)

OIDC adds authentication on top of OAuth 2.0:
  - ID Token (JWT) — contains user identity claims (sub, email, name, picture)
  - UserInfo endpoint — GET /userinfo with access_token to get user profile
  - Discovery document — /.well-known/openid-configuration (auto-discovery)

OIDC Scopes:
  openid    — required for OIDC; returns ID token with sub claim
  email     — adds email + email_verified claims
  profile   — adds name, picture, given_name, family_name, locale
  address   — postal address
  phone     — phone number

ID Token validation (must do on client/server):
  1. Verify signature using AS public keys (from JWKS endpoint)
  2. Verify iss = expected issuer
  3. Verify aud = your client_id
  4. Verify exp > now (not expired)
  5. Verify iat is reasonable (not too old)
  6. Verify nonce if included in auth request

Popular OIDC providers:
  Auth0, Okta, AWS Cognito, Keycloak (self-hosted), Google, GitHub (partial OIDC)
Authentication & Authorization

Authentication Fundamentals

Authentication & Authorization: Fundamentals Authentication (AuthN) verifies identity — who are you? Authorization (AuthZ) determines permissions — what are you

Authentication & Authorization: Fundamentals

Authentication (AuthN) verifies identity — who are you? Authorization (AuthZ) determines permissions — what are you allowed to do? They are distinct concerns and should be implemented separately.

Sessions vs Tokens

Session-based (stateful):
  1. User logs in → server creates session in DB/Redis → sets cookie with session ID
  2. Subsequent requests → server looks up session ID → gets user data
  3. Logout → delete session from storage

  Pros: easy revocation, smaller cookie, server controls session lifetime
  Cons: server must store sessions (scales horizontally with shared Redis/DB)

Token-based (stateless JWT):
  1. User logs in → server generates signed JWT → client stores token
  2. Subsequent requests → client sends JWT → server validates signature (no DB lookup)
  3. Logout → client deletes token (server can't revoke without a blocklist)

  Pros: stateless — scales without shared storage, works across microservices
  Cons: can't revoke individual tokens without blocklist; sensitive data in payload (not encrypted)

Recommendation:
  Public APIs → JWT (stateless, good for third-party consumers)
  Web apps   → HTTP-only cookies with session OR JWT in HttpOnly cookie
  Microservices → JWT passed in Authorization header

Password Storage

Never store passwords in plaintext or with reversible encryption.
Use adaptive one-way hashing with a salt:

bcrypt:  industry standard. Cost factor makes brute-force expensive.
         bcrypt.hash("password", 12)  — cost factor 12 (~250ms)

Argon2id: winner of Password Hashing Competition (2015). Preferred for new systems.
          Memory-hard — resistant to GPU/ASIC cracking.
          argon2id({ memory: 65536, iterations: 3, parallelism: 4 })

scrypt:  similar to Argon2, memory-hard, PBKDF2 upgrade.

Avoid: MD5, SHA-1, SHA-256 without salt/iterations — too fast for passwords.
       PBKDF2 is acceptable but less resistant than Argon2 or bcrypt.

Salt: generated per-password (stored with hash). Prevents rainbow tables.
      bcrypt/Argon2 handle salting automatically.

Cookie Security

Set-Cookie: session=abc123;
  HttpOnly;           // JS cannot read — XSS protection
  Secure;             // HTTPS only
  SameSite=Strict;    // never sent cross-site — CSRF protection
                      // SameSite=Lax: sent on top-level navigations (GET)
                      // SameSite=None: cross-site (requires Secure)
  Path=/;
  Max-Age=3600;       // 1 hour (seconds)
  Domain=example.com

Multi-Factor Authentication (MFA)

  • TOTP (Time-based OTP): Google Authenticator, Authy — generates 6-digit code every 30s from shared secret (RFC 6238)

  • SMS OTP: one-time code via SMS — convenient but vulnerable to SIM swap attacks

  • FIDO2 / WebAuthn: hardware security keys (YubiKey) or biometrics — phishing-resistant, strongest option

  • Push notifications: Duo, Okta — approve/deny on mobile app

  • Recovery codes: backup one-time codes — store hashed, never plaintext

  • TOTP implementation: use speakeasy or otplib (Node.js), pyotp (Python)

Authentication & Authorization

JWT & Token Security

Authentication: JWT & Token Security JWT Structure JWT = base64url(header) . base64url(payload) . signature Header: { "alg": "RS256", "typ": "JWT" } Payload: {

Authentication: JWT & Token Security

JWT Structure

JWT = base64url(header) . base64url(payload) . signature

Header:  { "alg": "RS256", "typ": "JWT" }
Payload: { "sub": "user123", "email": "alice@example.com",
           "roles": ["admin"], "iat": 1716768000, "exp": 1716854400 }
Signature: RSA_SHA256(secret, header + "." + payload)

Standard claims (IANA registered):
  sub  — subject (user ID)
  iss  — issuer (who created the token, e.g. "https://auth.example.com")
  aud  — audience (intended recipient, e.g. "api.example.com")
  exp  — expiration timestamp (Unix seconds)
  iat  — issued at timestamp
  jti  — JWT ID (unique identifier — use for revocation blocklist)
  nbf  — not before (token not valid until this time)

WARNING: JWT payload is base64-encoded, NOT encrypted.
         Anyone can decode it. Never put passwords, card numbers, or
         secrets in the payload. Use JWE (JSON Web Encryption) if needed.

Signing Algorithms

HS256 (HMAC-SHA256) — symmetric: same secret signs and verifies
  Pros: simple, fast
  Cons: every service that verifies must have the secret → secret sprawl

RS256 (RSA-SHA256) — asymmetric: private key signs, public key verifies
  Pros: publish public key → any service can verify without knowing the private key
  Cons: slower than HS256

ES256 (ECDSA P-256) — asymmetric like RS256 but much smaller keys and faster
  Recommended for new systems

EdDSA (Ed25519) — most modern, fastest, smallest signatures

Recommendation:
  Internal (single service) → HS256
  Distributed / microservices → RS256 or ES256
  Never use "none" algorithm — it disables signature verification!

Access + Refresh Token Pattern

Access token:  Short-lived (15-60 min). Sent with every API request.
               Stateless — validated by signature. No DB lookup per request.

Refresh token: Long-lived (7-30 days). Stored in HttpOnly cookie.
               Used ONLY to get new access tokens. Stored as hash in DB.

Flow:
  POST /auth/login
    → {accessToken, refreshToken (in HttpOnly cookie)}

  API calls: Authorization: Bearer <accessToken>

  When access token expires (401):
    POST /auth/refresh  (refresh token sent automatically via cookie)
    → new accessToken + rotated refreshToken (old one invalidated)

  POST /auth/logout
    → delete refresh token from DB; client clears access token

Refresh token rotation: issue a new refresh token with every refresh.
  If an old refresh token is ever used → revoke the entire family (detect theft).

Revocation: maintain a blocklist in Redis with jti + exp.
  Check blocklist on critical operations (password reset, account deletion).

Token Storage in Browsers

localStorage / sessionStorage:
  ❌ XSS vulnerable — any injected script can read window.localStorage
  ❌ Do not store long-lived tokens here

In-memory (JS variable):
  ✅ Not accessible to other tabs or XSS from different origins
  ❌ Lost on page refresh
  ✅ Good for short-lived access tokens

HttpOnly Cookie (recommended for refresh tokens):
  ✅ Inaccessible to JavaScript
  ✅ Sent automatically
  ❌ CSRF risk → mitigate with SameSite=Strict or CSRF token

Recommended setup:
  Access token  → in-memory JS variable (short-lived, refreshed silently)
  Refresh token → HttpOnly Secure SameSite=Strict cookie
Authentication & Authorization

Authorization: RBAC, ABAC & Best Practices

Authorization: RBAC, ABAC & Best Practices Role-Based Access Control (RBAC) RBAC assigns permissions to roles, then roles to users. Simple to manage for most ap

Authorization: RBAC, ABAC & Best Practices

Role-Based Access Control (RBAC)

RBAC assigns permissions to roles, then roles to users. Simple to manage for most applications.

// Roles and permissions
const ROLES = {
  admin:     ['read', 'write', 'delete', 'manage_users'],
  editor:    ['read', 'write'],
  viewer:    ['read'],
  moderator: ['read', 'write', 'delete_comments'],
} as const

// Simple RBAC check
function can(user: User, action: string): boolean {
  const permissions = ROLES[user.role] ?? []
  return permissions.includes(action)
}

// Hierarchical RBAC — roles inherit from parent roles
const ROLE_HIERARCHY = {
  admin: ['editor', 'viewer'],
  editor: ['viewer'],
  viewer: [],
}

// Resource-level RBAC — can user X do action Y on resource Z?
async function authorize(user: User, action: string, resource: Resource): Promise<boolean> {
  if (user.role === 'admin') return true
  if (action === 'read' && resource.isPublic) return true
  if (action === 'write' && resource.ownerId === user.id) return true
  return false
}

// Middleware example (Express)
function requireRole(...roles: string[]) {
  return (req: Request, res: Response, next: NextFunction) => {
    if (!roles.includes(req.user.role))
      return res.status(403).json({ error: 'Forbidden' })
    next()
  }
}

app.delete('/api/users/:id', requireRole('admin'), deleteUser)

Attribute-Based Access Control (ABAC)

ABAC evaluates policies based on attributes of the user, resource, and environment. More flexible than RBAC for complex rules.

// Policy-based authorization
interface AuthContext {
  user: { id: string; role: string; department: string; clearanceLevel: number }
  resource: { ownerId: string; classification: string; department: string }
  environment: { time: Date; ipAddress: string }
}

// Policies are pure functions — easy to test
const policies = {
  'document:read': (ctx: AuthContext) =>
    ctx.resource.classification === 'public' ||
    ctx.user.department === ctx.resource.department ||
    ctx.user.clearanceLevel >= 3,

  'document:delete': (ctx: AuthContext) =>
    ctx.user.role === 'admin' ||
    (ctx.user.id === ctx.resource.ownerId && ctx.user.department === ctx.resource.department),

  'system:access': (ctx: AuthContext) =>
    ctx.environment.time.getHours() >= 9 &&
    ctx.environment.time.getHours() < 18,  // business hours only
}

function authorize(action: string, ctx: AuthContext): boolean {
  return policies[action]?.(ctx) ?? false
}

// Open Policy Agent (OPA) — external policy engine
// Rego policy language; decouples policy from application code

Row-Level Security

// Always filter data by ownership — never return all rows to non-admins
async function getPosts(userId: string, isAdmin: boolean) {
  if (isAdmin) return db.posts.findMany()                  // admin sees all
  return db.posts.findMany({ where: { authorId: userId }}) // user sees own
}

// PostgreSQL Row Level Security (RLS) — enforce at DB level
// CREATE POLICY user_isolation ON posts
//   USING (author_id = current_user_id());
// ALTER TABLE posts ENABLE ROW LEVEL SECURITY;

// Supabase uses RLS extensively
// CREATE POLICY "Users can view own posts" ON posts
//   FOR SELECT USING (auth.uid() = user_id);

Best Practices

  • Principle of least privilege: grant minimum permissions needed. Default deny — explicitly grant access.

  • Check authorization on every request: don't rely on UI hiding. Always enforce on the server.

  • BOLA / IDOR: verify object ownership on every resource endpoint. GET /orders/123 must verify order 123 belongs to the requesting user.

  • Centralize authorization logic: don't scatter if (user.role === "admin") checks — use a policy/guard layer.

  • Audit log: log all authorization failures and sensitive actions (who, what, when, from where).

  • Separate AuthN and AuthZ: authentication verifies identity; authorization grants permissions. Keep the code separate.

  • Re-verify on sensitive operations: even with a valid session, require re-authentication for password change, payment, account deletion.

  • Test authorization: explicitly test that non-owners, non-admins, and unauthenticated users are denied. Happy path tests alone are not enough.

Keep your Authentication & Authorization knowledge sharp.

Save this stack to your personal DevRecall — add your own notes, track what you're learning, and share what you know with the community.

Get started — free forever