Skip to content

Metadata Card

  • Prerequisites: Chapter 1 (hashing, signatures), Chapter 2 (certificates, PKI)
  • Estimated time: 60 minutes
  • Core difficulty: Advanced
  • Completion mark: Can explain the structure and verification flow of JWT, can draw the OAuth 2.0 Authorization Code flow, can distinguish Authentication from Authorization

Your Progress

Now you can communicate with the Grey Tower—you checked the other party's rune certificate and confirmed it was issued by a trusted rune certification center. The encrypted magical letters you send can only be decrypted by the Grey Tower.

But the Grey Tower has more than one guardian. Your letter needs to be signed by the "Archmage of the Tower Guard," not an apprentice on duty. How do you prove "I am who I am"? How do you ensure "my operation permission is Archmage level"?

This is the distinction between authentication and authorization. The former asks "who are you," the latter asks "what can you do"—two completely different things, often conflated.

Your Task

Understand the mainstream protocols and mechanisms for modern authentication and authorization: JWT as a structured token, OAuth 2.0 as an authorization framework, OpenID Connect as an identity layer, and Multi-Factor Authentication (MFA) as defense in depth.

Chapter Layers

  • Required: JWT structure & verification, Session vs Token comparison, OAuth 2.0 Authorization Code flow
  • Optional: OIDC's ID Token & UserInfo endpoint, PKCE
  • Advanced: OAuth 2.0's four authorization flow selection strategies, Token Binding

Breaking Ground · Tracing the Origin

Problem: You walk up to the fortress gate, and the sentry asks "password." You tell him the password, he checks it, and lets you in. But next time he comes to the fortress to find you, can he also use your password?

This is simple password authentication—you know a secret, and the server knows it too. The problems:

  1. The server stores your password (or its hash). If the server is breached, your password is leaked.
  2. You have to send the password with every request, increasing risk of interception.
  3. You can't prove "this request came from you" without giving up the password.

First Approach: Session (Session + Cookie)

The traditional web application solution:

Browser ←→ Server
1. POST /login (username + password)
2. Server verifies password, creates session, returns Set-Cookie: session_id=abc123
3. Browser includes Cookie: session_id=abc123 in all subsequent requests
4. Server looks up session storage, finds the corresponding user info

Disadvantages:

  • Sessions are stored server-side (memory, Redis, database), requiring sharing or synchronization (distributed scenarios)
  • Session ID is essentially an "opaque token"—the client doesn't know what it means
  • Cross-domain scenarios (mobile apps, third-party applications) aren't well-suited to cookies

Second Approach: JWT (JSON Web Token)

JWT encodes "who you are" in the token itself; the server doesn't need to look up storage to verify it.

A JWT consists of three parts, separated by .:

header.payload.signature

eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6Iui/meaYr+W9uSIsInJvbGUiOiLlrovnlJ_liY0ifQ.
dGhpcyBpcyB0aGUgc2lnbmF0dXJl...

After Base64 decoding:

Header:

json
{
  "alg": "RS256",
  "typ": "JWT"
}

Payload (Claims):

json
{
  "sub": "user_10086",
  "name": "Border Fortress Captain",
  "role": "captain",
  "iat": 1680000000,
  "exp": 1680086400,
  "iss": "auth.borderfortress.com"
}

Signature: Signs the first two parts to prevent tampering.

Standard registered claims (RFC 7519):

ClaimFull NameMeaning
issIssuerWho issued the token
subSubjectSubject (usually user ID)
audAudienceWhich service uses this token
expExpirationExpiration time (absolute timestamp)
nbfNot BeforeNot valid before
iatIssued AtWhen the token was issued
jtiJWT IDUnique identifier, prevents replay

Issue and verify JWT in Python. A JWT is like a signed pass—the fortress gatekeeper doesn't need to call headquarters to verify who you are, they just check if the pass's signature is valid:

python
# pip install pyjwt[crypto]
import jwt
from datetime import datetime, timedelta, timezone

# Use RSA signing (recommended, more secure than HS256)
private_key = open("private.pem").read()
public_key = open("public.pem").read()

# Issue
payload = {
    "sub": "user_10086",
    "name": "Border Fortress Captain",
    "role": "captain",
    "iat": datetime.now(timezone.utc),
    "exp": datetime.now(timezone.utc) + timedelta(hours=2),
    "iss": "auth.borderfortress.com",
}
token = jwt.encode(payload, private_key, algorithm="RS256")
print(f"JWT: {token}")

# Verify
try:
    decoded = jwt.decode(
        token,
        public_key,
        algorithms=["RS256"],
        audience=None,
        issuer="auth.borderfortress.com",
    )
    print(f"Verified, user: {decoded['name']}({decoded['role']})")
except jwt.ExpiredSignatureError:
    print("Token expired")
except jwt.InvalidTokenError as e:
    print(f"Invalid token: {e}")

Note: JWT is signed, not encrypted. The payload is only Base64-encoded; anyone can decode and read the content. Don't put sensitive information like passwords or credit card numbers in a JWT. If you need confidentiality, use JWE (JSON Web Encryption, rarely used in practice).

Session vs Token Comparison

DimensionSessionJWT
Storage locationServer-side (Redis/DB)Client-side (browser/localStorage)
Verification methodLook up storageVerify signature
DistributedNeeds shared storage or sticky sessionNaturally stateless
RevocationDelete session directlyNeeds blacklist (negates statelessness)
Data sizeSmall (session ID)Larger (contains claims)
Cross-domainHard (Cookie SameSite)Easy (Authorization header)
RefreshTransparentNeeds refresh token mechanism

Third Approach: OAuth 2.0

In the real world, you won't have just one system. You might want to use your fortress identity to access the "Quartermaster System," without giving the quartermaster your fortress password.

OAuth 2.0 solves the "third-party authorization" problem—letting an application access a user's resources on another service without obtaining the user's password.

OAuth 2.0 core roles:

+--------+                               +---------------+
|        |--(A) Request Authorization--->|               |
| Client |                               | Auth Server   |
| (App)  |<-(B) Authorization Code/Token-|               |
|        |                               +---------------+
|        |                               +---------------+
|        |--(C) Request Resource with    |               |
|        |      Access Token             | Resource Server|
|        |<-(D) Return Resource----------|               |
+--------+                               +---------------+

The most commonly used Authorization Code Grant flow:

1. User visits client application, clicks "Log in with Fortress Account"
2. Client redirects user to authorization server:
   GET /authorize?
     response_type=code&
     client_id=my_app&
     redirect_uri=https://myapp.com/callback&
     scope=read:supplies&
     state=random_csrf_token
3. User logs in at authorization server, confirms authorization
4. Authorization server redirects to redirect_uri:
   GET https://myapp.com/callback?code=AUTHORIZATION_CODE&state=random_csrf_token
5. Client exchanges authorization code for access token:
   POST /token
     grant_type=authorization_code&
     code=AUTHORIZATION_CODE&
     redirect_uri=https://myapp.com/callback&
     client_id=my_app&
     client_secret=my_secret
6. Authorization server returns: { "access_token": "....", "token_type": "Bearer", "expires_in": 3600, "refresh_token": "..." }
7. Client uses access_token to request the resource server's API

PKCE (Proof Key for Code Exchange)

For pure frontend apps (mobile or SPA), you can't securely store a client_secret. PKCE solves this:

1. Client generates a code_verifier (random string)
2. Compute code_challenge = base64url(sha256(code_verifier))
3. Send code_challenge with the request
4. Send code_verifier when exchanging the token
5. Authorization server verifies sha256(code_verifier) == code_challenge

Even if the authorization code is intercepted, an attacker without the code_verifier cannot exchange it for a token.

Implementing an OAuth 2.0 client in Python (simplified). The OAuth authorization code flow is like the fortress issuing a one-time pass to a third party—the pass holder goes to headquarters to exchange it for an official token, and headquarters checks that the pass was issued by them before issuing:

python
# This is a conceptual example; in practice use requests-oauthlib or authlib
import requests
import hashlib
import base64
import secrets

# PKCE
code_verifier = secrets.token_urlsafe(64)
code_challenge = base64.urlsafe_b64encode(
    hashlib.sha256(code_verifier.encode()).digest()
).rstrip(b"=").decode()

# Step 1-2: Construct authorization URL
auth_url = (
    "https://auth.borderfortress.com/authorize?"
    "response_type=code&"
    "client_id=supply_app&"
    "redirect_uri=https://supply.app/callback&"
    "scope=read:supplies&"
    f"code_challenge={code_challenge}&"
    "code_challenge_method=S256&"
    f"state={secrets.token_urlsafe(16)}"
)
print(f"Visit: {auth_url}")

# (User authorizes in browser, authorization server redirects to callback)

# Step 5: Exchange token
callback_url = "https://supply.app/callback?code=xxx&state=yyy"
# Extract code...
token_response = requests.post(
    "https://auth.borderfortress.com/token",
    data={
        "grant_type": "authorization_code",
        "code": extracted_code,
        "redirect_uri": "https://supply.app/callback",
        "client_id": "supply_app",
        "code_verifier": code_verifier,
    },
)
access_token = token_response.json()["access_token"]

# Step 7: Use token to access resources
supplies = requests.get(
    "https://api.borderfortress.com/v1/supplies",
    headers={"Authorization": f"Bearer {access_token}"},
)

OpenID Connect (OIDC): The Authentication Layer

OAuth 2.0 only handles authorization ("what can you do"), not authentication ("who are you"). OIDC adds an identity layer on top of OAuth 2.0.

OAuth 2.0 returns an access_token (for the resource server) OIDC additionally returns an id_token (JWT format, for the client)

id_token contains:
{
  "iss": "https://accounts.borderfortress.com",
  "sub": "user_10086",
  "aud": "supply_app",
  "exp": 1680086400,
  "iat": 1680000000,
  "nonce": "xxx",  // Anti-replay
  "name": "Border Fortress Captain",
  "email": "captain@borderfortress.com"
}

The client verifies the id_token's signature (using the authorization server's public key) to obtain the user's identity—no extra API call needed.

Fourth Approach: MFA (Multi-Factor Authentication)

Passwords can be stolen. Sessions can be hijacked. So you need an additional layer of defense.

Three types of MFA factors:

  • Knowledge factor: Something you know (password, PIN)
  • Possession factor: Something you have (phone, hardware key)
  • Inherence factor: Something you are (fingerprint, face)

Common MFA implementation—TOTP (Time-based One-Time Password, RFC 6238):

Principle:
1. Server generates a shared secret (usually displayed as a QR code)
2. User scans the QR code with Authenticator App (Google Authenticator / Authy)
3. Each OTP is generated as:
   TOTP = HMAC-SHA1(shared_secret, current_time_window)
   current_time_window = floor(unix_timestamp / 30)
   # Changes every 30 seconds, outputs 6 digits
4. User enters the currently displayed 6-digit number
5. Server verifies with the same algorithm
python
# Using pyotp library (pip install pyotp)
import pyotp
import qrcode

# Generate secret (server stores)
secret = pyotp.random_base32()

# Generate URI for display (can be converted to QR code for user scanning)
uri = pyotp.totp.TOTP(secret).provisioning_uri(
    name="captain@borderfortress.com",
    issuer_name="BorderFortress",
)

# After user scans QR, the phone app generates a 6-digit number every 30 seconds
# Verify user-entered OTP
totp = pyotp.TOTP(secret)
user_input = input("Enter the 6-digit number from Authenticator: ")
if totp.verify(user_input):
    print("Verification passed")
else:
    print("Verification failed or expired")

More Secure MFA: WebAuthn

TOTP is still vulnerable to man-in-the-middle attacks (phishing sites can steal both password and TOTP). WebAuthn (FIDO2 standard) uses asymmetric encryption:

1. During registration, the user's device (YubiKey / phone / TPM) generates a key pair
2. The private key never leaves the device; the public key is registered with the server
3. During login, the server sends a challenge
4. The device signs the challenge with the private key; the server verifies with the public key
5. No password to steal, no OTP to phish

WebAuthn is one of the strongest authentication schemes available, supported by major platforms (Windows Hello, Apple Touch ID, Android fingerprint).


Common Pitfalls

  • JWT is not encrypted, only signed. Don't put sensitive data in the payload. For confidentiality, use JWE or fetch data through a separate API.
  • JWT expiry too long. Access tokens should be short (15 minutes to 1 hour), used with refresh tokens.
  • OAuth 2.0 Implicit Grant is no longer recommended. For security reasons, switch to Authorization Code + PKCE.
  • Ignoring the state parameter. Not validating state in OAuth flow leads to CSRF attacks—an attacker can trick a user into using the attacker's authorization code.
  • Storing access tokens in pure frontend. localStorage can be read by XSS. Use httpOnly cookie + BFF (Backend for Frontend) pattern.
  • Not using refresh token rotation. Return a new refresh token with each refresh; invalidate the old one. Reduces the risk of refresh token leakage.
  • Password complexity vs password length. Forcing mixed-case special characters is less effective than requiring at least 12 characters plus multi-factor authentication.

Pass Challenges

  • Warm-up: Decode a real JWT (using jwt.io or command line jq -R 'split(".") | .[0], .[1] | @base64d'), inspect its claims, observe the alg field.
  • Challenge: Implement a complete OAuth 2.0 Authorization Code flow in Python (using Flask or FastAPI), including authorization endpoint, token endpoint, and resource endpoint. Support PKCE at minimum.
  • Troubleshoot: Your JWT verification code works fine locally but always reports InvalidSignature in production. Analyze possible causes (public key format? algorithm mismatch? timezone issues?).
  • Observe: Open browser DevTools > Network, find a site using OAuth 2.0 (like GitHub), observe the HTTP request sequence during the authorization flow. Track the redirect_uri changes.

Traveler's Notes

  • Authentication (who you are) and authorization (what you can do) are two different things, handled by different mechanisms
  • JWT is a self-contained token using signatures for integrity; the payload is readable but not encrypted
  • Session suits traditional web apps; JWT suits distributed and mobile environments
  • OAuth 2.0 solves third-party authorization; Authorization Code + PKCE is the recommended flow
  • OIDC adds an identity layer on top of OAuth 2.0, using id_token to carry user info
  • MFA (especially WebAuthn) dramatically improves security; passwords alone are insufficient for critical systems

Next Stop Preview

The "who am I" and "what can I do" questions are solved. But your application is exposed on the public internet, potentially receiving malicious requests at any time. Next chapter, we look at how attackers exploit web application vulnerabilities and how you can defend at the code level.

Built with VitePress | Software Systems Atlas