Learn/software dev/Authentication
Advanced~20 min read

Authentication

AuthN vs authZ, password hashing with bcrypt/argon2, sessions and cookies, JWT structure and access/refresh tokens, OAuth 2.0/OIDC with PKCE, RBAC, MFA/TOTP, and common attacks with mitigations.

JWTOAuthHashingSessions

Authentication vs Authorization

These two are constantly confused but solve different problems. Authentication (authN) answers "who are you?" — verifying identity via credentials. Authorization (authZ) answers "what are you allowed to do?" — deciding whether an already-identified principal may perform an action. AuthN always precedes authZ.

AspectAuthenticationAuthorization
QuestionWho are you?What can you do?
MechanismPassword, MFA, OAuth loginRoles, permissions, policies
ResultA verified identity/sessionAllow or deny a specific action
HTTP status401 Unauthorized403 Forbidden

Password Hashing

Never store passwords in plaintext, and never "encrypt" them (encryption is reversible). Store a slow, salted one-way hash. When a user logs in, hash their input and compare. A per-user random salt defeats rainbow tables and ensures two identical passwords produce different hashes. Modern algorithms are deliberately slow and memory-hard to resist GPU/ASIC cracking.

AlgorithmNotes
Argon2id2026 recommended default — memory-hard, tunable memory/time/parallelism
bcryptBattle-tested, widely available; cap input at 72 bytes
scryptMemory-hard alternative to Argon2
MD5 / SHA-1 / SHA-256NEVER for passwords — too fast, no built-in salt
javascript
import argon2 from 'argon2';

// Register — salt is generated and embedded in the hash string automatically
const hash = await argon2.hash(plainPassword, { type: argon2.argon2id });
// store `hash` in the DB (it also encodes params + salt)

// Login — constant-time verification, no manual salt handling
const ok = await argon2.verify(storedHash, submittedPassword);
if (!ok) throw new Error('invalid credentials');

Peppering & timing

Compare hashes with a constant-time function (the library's verify does this) to avoid timing attacks. Return the same generic "invalid credentials" error whether the email or the password was wrong, so attackers cannot enumerate accounts. An optional application-wide secret "pepper" (kept outside the DB) adds defence in depth.

Sessions & Cookies

In stateful session auth, the server creates a session record on login and hands the client an opaque session ID stored in a cookie. Every subsequent request sends the cookie; the server looks up the session. Because the ID is meaningless on its own, revoking a session is trivial — just delete the server-side record.

The security of cookie-based auth lives in the cookie flags:

FlagPurpose
HttpOnlyJS cannot read the cookie — blocks XSS token theft
SecureSent only over HTTPS
SameSiteLax/Strict limit cross-site sending — mitigates CSRF
__Host- prefixForces Secure, path=/, no Domain — hardens scoping
Max-AgeExpiry; short-lived sessions reduce hijack window
javascript
res.cookie('sid', sessionId, {
  httpOnly: true,
  secure:   true,
  sameSite: 'lax',        // 'strict' for high-value apps
  maxAge:   1000 * 60 * 60 * 8,   // 8 hours
  path:     '/',
});
// Regenerate the session ID on privilege change (login) to prevent fixation
req.session.regenerate(() => { req.session.userId = user.id; });

JSON Web Tokens (JWT)

A JWT is a self-contained, signed token in the form header.payload.signature — three base64url segments joined by dots. Because the claims travel inside the token, the server can verify it statelessly without a database lookup, which is why JWTs suit distributed systems and APIs.

javascript
// Header  { "alg": "HS256", "typ": "JWT" }
// Payload { "sub": "user_42", "role": "admin", "iat": 1717200000, "exp": 1717200900 }
// Signature = HMACSHA256(base64url(header) + "." + base64url(payload), secret)

// eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyXzQyIn0.SflKx...

import jwt from 'jsonwebtoken';

const access = jwt.sign(
  { sub: user.id, role: user.role },
  process.env.JWT_SECRET,
  { expiresIn: '15m', algorithm: 'HS256' }
);

// Verify — throws on tampering or expiry
const claims = jwt.verify(token, process.env.JWT_SECRET, {
  algorithms: ['HS256'],   // pin the algorithm — never trust the header's alg
});

The payload is not secret

Base64 is encoding, not encryption — anyone can decode a JWT and read its claims. The signature only guarantees integrity (it hasn't been altered), not confidentiality. Never put secrets in the payload. Always pin the accepted algorithms on verify to defeat the classic alg: none and RS256→HS256 confusion attacks.

Access vs Refresh Tokens

A stateless JWT cannot be revoked before it expires, so you keep access tokens short-lived (5–15 min). A longer-lived refresh token is exchanged for new access tokens. Refresh tokens should be opaque, stored server-side (so they can be revoked), rotated on each use, and delivered in an HttpOnly cookie.

Where to Store Tokens

StorageTradeoff
HttpOnly cookieSafe from XSS reads; needs CSRF protection (SameSite + token)
localStorageConvenient but fully exposed to any XSS — avoid for tokens
In-memory (JS)Not persisted across reloads; smallest XSS window

Sessions vs JWT

AspectServer SessionsJWT (stateless)
StateStored server-side (DB/Redis)Self-contained in the token
RevocationInstant — delete the recordHard — must wait for expiry or keep a blocklist
ScalingNeeds shared session storeNo lookup — scales horizontally
Payload sizeTiny opaque IDLarger; sent on every request
Best forClassic web apps, need instant logoutAPIs, microservices, mobile

OAuth 2.0 & OpenID Connect

OAuth 2.0 is a delegated authorization framework — it lets an app act on a user's behalf against a resource server without seeing their password ("Sign in with Google"). OpenID Connect (OIDC) is a thin identity layer on top that adds an id_token (a JWT describing who the user is). Use OAuth for access, OIDC for authentication.

Authorization Code Flow + PKCE

The Authorization Code flow with PKCE (Proof Key for Code Exchange) is the recommended flow for all clients in 2026 — SPAs, mobile, and server apps alike. PKCE binds the authorization request to the token exchange so an intercepted code is useless without the original secret.

javascript
// 1. Client makes a random verifier and its SHA-256 challenge
const verifier  = base64url(randomBytes(32));
const challenge = base64url(sha256(verifier));

// 2. Redirect user to the authorization server
//    /authorize?response_type=code&client_id=APP&redirect_uri=...
//      &scope=openid%20profile&state=xyz
//      &code_challenge=CHALLENGE&code_challenge_method=S256

// 3. User authenticates & consents -> server redirects back with ?code=...&state=xyz
//    (verify `state` matches to block CSRF on the callback)

// 4. Exchange the code for tokens, proving possession of the verifier
POST /token
  grant_type=authorization_code&code=AUTH_CODE
  &redirect_uri=...&client_id=APP&code_verifier=VERIFIER

// 5. Response: { access_token, refresh_token, id_token (OIDC), expires_in }

Other grant types exist — Client Credentials for machine-to-machine (no user), and Refresh Token for renewal. The legacy Implicit and Password grants are deprecated and should not be used.

Authorization: RBAC

Role-Based Access Control assigns permissions to roles and roles to users, so authorization checks reduce to "does this user's role grant this permission?" It is simpler than per-user ACLs and scales well. For finer-grained needs, ABAC (attribute-based) evaluates policies over user, resource, and context attributes.

javascript
const PERMISSIONS = {
  admin:  ['post:read', 'post:write', 'post:delete', 'user:manage'],
  editor: ['post:read', 'post:write'],
  viewer: ['post:read'],
};

function requirePermission(perm) {
  return (req, res, next) => {
    const role = req.user?.role;
    if (!role) return res.status(401).end();          // not authenticated
    if (!PERMISSIONS[role]?.includes(perm))
      return res.status(403).end();                   // authenticated, not allowed
    next();
  };
}

app.delete('/posts/:id', requirePermission('post:delete'), handler);

Enforce authZ server-side

Never rely on hiding a button in the UI. Every protected action must be checked on the server, and checks must include ownership — a user with post:write should only edit their own posts unless they are an admin. Skipping this is the root of IDOR and broken-access-control vulnerabilities.

Multi-Factor Authentication (MFA/TOTP)

MFA combines factors from different categories: something you know (password), something you have (phone, hardware key), and something you are (biometric). The most common software second factor is TOTP (RFC 6238) — an authenticator app derives a 6-digit code from a shared secret and the current 30-second time window.

javascript
import { authenticator } from 'otplib';

// Enrollment: generate a secret, show it as a QR (otpauth:// URI)
const secret = authenticator.generateSecret();
const uri = authenticator.keyuri(user.email, 'MyApp', secret);
// store `secret` encrypted, plus one-time recovery codes

// Login step 2: verify the code (allows +/- 1 window for clock drift)
const isValid = authenticator.verify({ token: submittedCode, secret });

Prefer phishing-resistant factors where possible — WebAuthn/passkeys (FIDO2) bind the credential to the origin and cannot be replayed on a phishing site, unlike SMS or even TOTP.

Common Attacks & Mitigations

AttackWhat happensMitigation
CSRFAttacker site triggers a state-changing request using the victim's cookieSameSite cookies + anti-CSRF token; check Origin
XSSInjected JS steals tokens or acts as the userOutput encoding, CSP, HttpOnly cookies
Session fixationAttacker sets a known session ID before loginRegenerate the session ID on login
Credential stuffingReused leaked passwords sprayed at loginRate limiting, MFA, breached-password checks
Brute forceAutomated password guessingSlow hashing, throttling, account lockout/backoff
Token theftStolen JWT/refresh token replayedShort expiry, refresh rotation, TLS, revocation list

Practice Exercises

  1. Implement register/login with Argon2id, returning identical errors for unknown-email and wrong-password cases.
  2. Build a session-cookie login with correct HttpOnly/Secure/SameSite flags and session-ID regeneration on login.
  3. Issue a 15-minute access JWT plus a rotating refresh token stored server-side; add a refresh endpoint.
  4. Add algorithm pinning to your JWT verification and write a test proving an alg: none token is rejected.
  5. Implement the OAuth Authorization Code + PKCE flow against a provider, verifying the state parameter on the callback.
  6. Add TOTP-based MFA with QR enrollment and one-time recovery codes.
  7. Write an RBAC middleware and add an ownership check so users can only edit their own resources.

Section navigation