Scenario — Inspecting Authentication

🔐 Lesson 8 — Scenario: Inspecting Authentication

A scenario lesson that applies the tools from lessons 1–7 to one real task: sit down in front of any web application and determine exactly how it authenticates users, read the tokens it sends, trace the full login-to-API flow, and diagnose why authentication is failing.

Step 1 Open the Right Panels
🚀 Setup Before You Start 1. Press F12 → click the Network tab.
2. Press Escape to open the Console drawer at the bottom — now you can watch requests and run JS at the same time.
3. Enable Persist Logs in the Network toolbar. Authentication involves redirects — without this you lose the login request the moment the redirect fires.
Step 2 Identify the Authentication Mechanism

Different sites use different approaches. Filter the Network panel by XHR/Fetch and look for these signals:

🎫
JWT Bearer Token
Login response body contains access_token
API requests carry Authorization: Bearer eyJ…
Token stored in localStorage or a cookie
Token is a three-part base64 string
🔗
OAuth 2.0 / OpenID Connect
"Sign in with Google / GitHub" button
Browser redirects to third-party URL
Returns with ?code=… in URL
Backend exchanges code for tokens
🔑
API Key
Static key in Authorization or
custom header X-Api-Key, or
query parameter ?api_key=…
No login flow — key used on every request
Step 3 Trace the Full Login Flow

With Persist Logs enabled, log out, then log back in and watch the Network panel. Here is what a complete JWT login sequence looks like:

Network panel — JWT login flow (XHR/Fetch filter active)
1
POST /auth/login 200 OK
Credentials sent in request body. Response body contains access_token and refresh_token.
2
// JavaScript stores token in localStorage
The page's JS saves the token. Check Storage → localStorage — it appears here after step 1 completes.
3
GET /api/user/profile 200 OK
First authenticated request. Request headers now contain Authorization: Bearer eyJ…
4
GET /api/dashboard 200 OK
Subsequent authenticated requests — same Bearer header on all of them.

Reading the Login Request

Click the login request → Request tab to see what was sent:

POST /auth/login 200 OK
Headers
Request
Response
Cookies
Request Body (application/json)
{
  "email": "alice@example.com",
  "password": "••••••••" never stored in plain text
}

Reading the Login Response

POST /auth/login 200 OK
Headers
Request
Response
Cookies
Response Body (application/json)
{
  "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI0MiIsInJvbGUiOiJhZG1pbiIsImV4cCI6MTczMDAwMDAwMH0.Xk3Q" ← JWT,
  "token_type": "Bearer",
  "expires_in": 3600
}
Step 4 Decode the JWT

A JWT is three base64url-encoded sections separated by dots. The header and payload are readable by anyone — they are signed, not encrypted.

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI0MiIsInJvbGUiOiJhZG1pbiIsImV4cCI6MTczMDAwMDAwMH0.Xk3Q_example_signature
Header — algorithm & type
Payload — your claims (sub, role, exp)
Signature — server verifies this; clients cannot forge it
Header (decoded)
{
  "alg": "HS256",
  "typ": "JWT"
}
Payload (decoded) ← readable!
{
  "sub": "42",
  "role": "admin",
  "exp": 1730000000
}

Decode any JWT in the Console — no third-party tool needed:

// Paste the token from the Authorization header or localStorage const token = localStorage.getItem('access_token'); const payload = JSON.parse(atob(token.split('.')[1])); console.table([payload]); // Check expiry console.log('Expires:', new Date(payload.exp * 1000).toLocaleString()); console.log('Expired?', Date.now() > payload.exp * 1000);
Step 5 Audit the Security Configuration
Cookie-based session — security checklist (Storage → Cookies)
🔴
HttpOnly flag present on session cookie
XSS theft if missing
🔴
Secure flag present — cookie only sent over HTTPS
Network interception if missing
🟡
SameSite=Lax or Strict — not None without Secure
CSRF risk if missing
🟡
Explicit Max-Age or Expires — session doesn't live forever
Indefinite sessions
JWT Bearer — security checklist (Network → Authorization header + localStorage)
🔴
Token is NOT in localStorage — use HttpOnly cookie instead
XSS can steal localStorage
🟡
exp claim is reasonable — hours, not years
Long-lived tokens stay valid after compromise
🟡
Payload does not contain PII (email, phone, address)
Payload is base64-readable by anyone
🟢
Refresh token NOT in localStorage (use HttpOnly cookie or not stored client-side)
Refresh tokens have long lifetimes
Step 6 Diagnose Authentication Failures
401 Unauthorized
Click the request → Headers. Is Authorization present in the request?
No Authorization → client isn't sending credentials. Check localStorage/cookies for the token.
Authorization present but 401 → token is invalid or expired. Decode exp in the Console.
Check the Response tab for a JSON error body: "token_expired", "invalid_signature"
403 Forbidden
Credentials are valid but access is denied — a permissions issue, not an auth issue.
Decode the JWT in the Console. Check the role or scope claim — does it include the required permission?
Check the Response tab — the server usually explains: "insufficient_scope"
CORS Error
Read the full error in the Console — it names the missing header.
Click the failed request → Response Headers. Is Access-Control-Allow-Origin present and matching?
Does Access-Control-Allow-Headers include Authorization? A wildcard origin doesn't help if the auth header is blocked.
Look for a preceding OPTIONS preflight request — what status did it return?
Redirect Loop
Enable Persist Logs. Reload and watch the Network panel for repeated 302 responses.
Check the Location header on each redirect — where is the server sending the browser?
Open Storage → Cookies. Is the session cookie being set before the redirect lands on the protected page?

Annotated 401 Response

GET /api/user/profile 401 Unauthorized
Headers
Response
Request Headers — what the browser sent
Authorization: (missing) ← client not sending token
Content-Type: application/json
Response Headers — what the server replied
WWW-Authenticate: Bearer realm="api" ← server expects Bearer token
Content-Type: application/json
Response Body
{ "error": "missing_token", "message": "Authorization header required" }

📋 Copy as cURL — Replay Any Request

💡 Right-Click → Copy as cURL Right-click any request in the Network panel → Copy Value → Copy as cURL. This copies a complete curl command with all headers — including the Authorization token — ready to paste into a terminal or import into Postman for testing outside the browser.
# What "Copy as cURL" produces — paste directly into a terminal curl 'https://api.example.com/api/user/profile' \ -H 'Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiI0MiJ9.Xk3Q' \ -H 'Accept: application/json' \ -H 'Content-Type: application/json'

✏️ Exercises

  • Identify the mechanism. Open the Network panel on a site you're logged into. Filter by XHR/Fetch. Click an API request → Headers. Does it use Cookie, Authorization: Bearer, or something else?
  • Decode a JWT. If the site uses Bearer auth, copy the token value and run this in the Console: const t = 'PASTE_TOKEN_HERE'; console.table(JSON.parse(atob(t.split('.')[1]))); What claims does the payload contain? When does it expire?
  • Find the login request. Enable Persist Logs. Log out and log back in. Find the login POST in the Network panel. Click Request tab — what format is the body? What does the Response tab contain?
  • Audit cookie flags. Open Storage → Cookies. Check the session cookie. Does it have HttpOnly, Secure, and SameSite set? Note anything missing.
  • Simulate a 401. Edit the session cookie or localStorage token to an invalid string. Trigger an API call. Does the server return 401? Does the app handle it gracefully (redirect to login, show an error)?
  • Test logout completeness. Log out. Open Storage → check Cookies and localStorage. Are all tokens gone? Navigate directly to a protected URL (/dashboard or similar). Are you redirected to login?
  • Copy as cURL. Right-click any authenticated API request → Copy Value → Copy as cURL. Paste it into a terminal or note it. Can you see the exact auth token that was used?

📌 Key Takeaways

  • Enable Persist Logs before any login flow — the login request is gone the moment the redirect fires without it.
  • Session cookies show a Set-Cookie on the login response; JWT shows a token in the response body and Authorization: Bearer on subsequent calls.
  • Decode any JWT in the Console with JSON.parse(atob(token.split('.')[1])) — no external tools needed.
  • 401 = credentials missing or invalid; 403 = credentials valid but wrong role/scope — always check the Response body for the server's error message.
  • CORS errors on authenticated requests usually mean Access-Control-Allow-Headers is missing Authorization — a server-side misconfiguration.
  • Copy as cURL (right-click any request) gives you an instantly replayable command with all headers — invaluable for testing APIs outside the browser.
  • After logout, open the Storage panel — all tokens and session cookies must be gone; any that remain after logout are a security bug.
Up next
Lesson 9 — Scenario: Troubleshooting Client Access — diagnosing CORS errors, mixed content, TLS failures, DNS issues, and why a user can't reach your app