JustAppSec

Authentication Patterns

OAuth 2.1, passkeys, WebAuthn, and JWTs — modern identity for modern apps.

0:00

Authentication is the most critical part of most applications and the most frequently broken. This lesson covers the modern patterns — OAuth 2.1, passkeys, WebAuthn, and JWTs — with a focus on what to use, what to avoid, and the common mistakes that lead to account takeover.

The modern landscape

Authentication has moved significantly beyond username/password. The main patterns you will encounter:

  • OAuth 2.1 / OpenID Connect — Delegated authentication. The user proves their identity to an identity provider (Google, Microsoft, Okta), and your application receives an assertion.
  • Passkeys / WebAuthn — Passwordless, phishing-resistant authentication using public-key cryptography. The private key never leaves the user's device.
  • JWTs (JSON Web Tokens) — A token format for carrying claims between parties. Not an authentication protocol — a building block used within other protocols.
  • Session cookies — The traditional approach. Still widely used and perfectly valid when implemented correctly.

OAuth 2.1

OAuth 2.1 consolidates best practices from OAuth 2.0 and removes insecure flows. If you are implementing OAuth today, follow 2.1.

Key changes from 2.0

  • Authorization Code + PKCE is mandatory for all clients. The implicit flow is removed entirely.
  • Refresh token rotation — refresh tokens must be one-time use. The server issues a new refresh token with each use.
  • No resource owner password grant — passing username/password directly to the authorization server is removed.

Authorization Code flow with PKCE

  1. The client generates a random code_verifier and derives a code_challenge (SHA-256 hash).
  2. The client redirects the user to the authorization server with the code_challenge.
  3. The user authenticates and consents.
  4. The authorization server redirects back with an authorization code.
  5. The client exchanges the code for tokens by sending both the code and the original code_verifier.
  6. The authorization server verifies the code_verifier matches the code_challenge before issuing tokens.

PKCE prevents intercepted authorization codes from being used by a different client.

Common OAuth mistakes

  • Open redirect via redirect_uri. If the authorization server does not strictly validate the redirect URI, an attacker can steal authorization codes. Always register exact redirect URIs — no wildcards.
  • Missing state parameter. The state parameter prevents CSRF attacks against the OAuth flow. Generate a random value, store it in the session, and validate it when the callback arrives.
  • Token leakage via referrer. If the redirect page includes external resources, the authorization code can leak via the Referer header. Use Referrer-Policy: no-referrer on the callback page.
  • Storing tokens insecurely. Access tokens in localStorage are accessible to XSS. Prefer HttpOnly cookies for web applications, or use the Backend-for-Frontend (BFF) pattern where the server holds tokens and proxies API calls.

Passkeys and WebAuthn

Passkeys are the most significant shift in authentication in years. They replace passwords with public-key cryptography.

How it works

  1. Registration: The user's device generates a key pair. The public key is sent to the server. The private key stays on the device (protected by biometrics or a device PIN).
  2. Authentication: The server sends a challenge. The device signs it with the private key. The server verifies the signature with the stored public key.

No password is transmitted, stored, or phished. The private key never leaves the device.

Why passkeys matter

  • Phishing-resistant. The credential is bound to the origin. A fake login page at evil.com cannot trigger the credential for yourapp.com.
  • No password reuse. Each credential is unique per site.
  • No password database to breach. The server only stores public keys, which are useless to attackers.
  • Better UX. Users authenticate with a fingerprint or face scan instead of typing a password.

Practical considerations

  • Not all users have passkey-capable devices yet. Support passkeys alongside traditional methods during transition.
  • Account recovery needs a plan. If the user loses their device, you need a recovery path (backup codes, trusted contacts, or re-enrollment via email verification).
  • Cross-device flows (scanning a QR code on a phone to authenticate on a laptop) work but are not yet seamless on all platforms.

JWTs

JWTs are ubiquitous in modern authentication systems. They are also frequently misused.

What a JWT is

A JWT is a signed (and optionally encrypted) JSON object with three parts:

header.payload.signature

The header specifies the signing algorithm. The payload contains claims (user ID, roles, expiry). The signature proves the token has not been tampered with.

Common JWT mistakes

Algorithm confusion. If your verification code accepts the alg header from the token itself, an attacker can:

  • Set alg: none and strip the signature (the "none" algorithm attack)
  • Set alg: HS256 and sign with the public key (when the server expects RS256)

Fix: Always specify the expected algorithm in your verification code. Never read it from the token.

// jose library — specify algorithm explicitly
const { payload } = await jwtVerify(token, publicKey, {
  algorithms: ["RS256"],
});

No expiry. Tokens without an exp claim are valid forever. Always set short expiry times (minutes for access tokens, hours/days for refresh tokens).

Sensitive data in the payload. JWT payloads are base64-encoded, not encrypted. Anyone can decode them. Do not put passwords, API keys, or PII in the payload unless you are using JWE (encrypted JWTs).

Token revocation. JWTs are stateless — you cannot "invalidate" a specific token without additional infrastructure (blocklists, short expiry + refresh tokens, or session binding). If you need instant revocation, use short-lived tokens and server-side session state.

Session cookies

Despite the popularity of JWTs, session cookies remain a simple and secure choice for traditional web applications.

The pattern

  1. User authenticates.
  2. Server creates a session on the server side (in a database, Redis, or similar).
  3. Server sends a session ID back as a cookie.
  4. Browser sends the cookie automatically with every request.
  5. Server looks up the session to identify the user.

Cookie security flags

FlagPurpose
HttpOnlyCookie is not accessible via JavaScript — prevents theft via XSS
SecureCookie is only sent over HTTPS
SameSite=Lax or StrictPrevents the cookie from being sent in cross-site requests — mitigates CSRF
Path=/Scope the cookie to the relevant path
Max-Age or ExpiresSet an appropriate session lifetime

Session vs. JWT: when to use which

ScenarioRecommendation
Traditional web app with server-rendered pagesSession cookies
SPA with same-origin APISession cookies or BFF pattern with cookies
SPA calling third-party APIsJWT (issued by an identity provider)
Microservices authenticating each otherJWT (short-lived, signed by internal CA)
Need instant session revocationServer-side sessions

Multi-factor authentication

Passwords alone are insufficient. MFA adds a second factor:

  • TOTP (Time-based One-Time Password) — apps like Authy or Google Authenticator generate 6-digit codes. Widely supported, easy to implement.
  • WebAuthn/FIDO2 — hardware keys or passkeys. Strongest option, phishing-resistant.
  • SMS/email codes — better than nothing, but vulnerable to SIM swapping and email compromise. Use as a fallback, not a primary MFA method.

Implementation note: MFA should be enforced for sensitive operations (login, password change, admin actions), not just initial login. Step-up authentication (re-verifying identity before a high-risk action) is a strong pattern.

Summary

Use OAuth 2.1 with PKCE for delegated authentication. Support passkeys for phishing-resistant passwordless login. If you use JWTs, validate algorithms explicitly, set short expiry, and never put secrets in the payload. Session cookies remain a solid choice for server-rendered applications. Layer MFA on top of any method. No matter which pattern you choose, the principles are the same: protect credentials in transit, minimize token lifetimes, and never trust the client to enforce security.


This training content is AI-assisted and reviewed by our team, but issues may be missed and best practices evolve rapidly. Send corrections to [email protected].