API Security · intermediate · ~12 min

API authentication, tokens, and OAuth

**What you will learn** - Tell apart the common API authentication schemes — API keys, bearer tokens/JWT, and OAuth 2.0 — and where each one tends to fail. - Map the token *lifecycle* (issue -> transmit -> store -> validate -> expire -> revoke) and identify the leakage vectors at each stage. - Walk the OAuth 2.0 Authorization Code flow and name the three parameters that most often break: `redirect_uri`, `state`, and `scope`. - Build a strict, allowlist-based `redirect_uri` validator and prove it rejects attacker callbacks while accepting the legitimate one. - Correct the common myth that *decoding* a JWT is the same as *verifying* it, and validate issuer, audience, and expiry server-side. - Design detection and logging for token abuse without ever logging the tokens themselves.

Overview

Security objective. The asset you are protecting is the access token — the credential that lets a client act as a user or service on an API. The threat is an attacker who steals or forges that token (through a leaky URL, a loose OAuth redirect, or an unverified signature) and then calls the API as someone else. By the end of this lesson you will be able to detect weak token handling during an authorized API test and prevent it as a defender.

APIs usually authenticate with tokens, not browser session cookies. A token is a secret string the client attaches to every request to prove who it is. This changes where things go wrong: instead of one server-side session, you now have a portable secret that travels across networks, gets stored on clients, and can be copied.

The three token families you will meet are API keys (static per-client secrets), bearer tokens — frequently JWTs (signed, self-contained tokens) — and OAuth 2.0 tokens (short-lived access tokens plus longer-lived refresh tokens issued by a delegated-authorization flow).

This builds directly on your prerequisites. From Cookies, sessions, and browser storage you already know how the browser stores credentials and why localStorage is reachable by XSS while HttpOnly cookies are not — that same reasoning decides where a token is safe to keep. From Security headers, CORS, and JWT weaknesses you know that a JWT's trust depends entirely on its signature, that alg:none and weak secrets defeat it, and that CORS controls which origins may read a response — all of which apply the moment a token rides in an Authorization header.

The defenses are short to state and the rest of the lesson makes them concrete: use short-lived, narrowly-scoped tokens; enforce a strict redirect_uri allowlist; always send the state parameter; transmit tokens only in headers; and verify issuer, audience, signature, and expiry on the server.

Why it matters

Token mishandling is one of the leading causes of real API compromise, and it shows up in nearly every professional API assessment.

  • A leaked or over-scoped token is instant access. Unlike a password, a bearer token often needs no second factor — whoever holds it is the user until it expires. If it never expires, that is forever.
  • OAuth redirect and state flaws enable account takeover. A loose redirect_uri allowlist lets an attacker capture the authorization response; a missing state lets them stitch a victim's account to an attacker-controlled login (login CSRF).
  • Tokens leak in mundane places. URLs end up in server logs, browser history, and the Referer header; tokens get committed to public repositories; verbose error pages echo them back. Much of professional API work is knowing exactly these places to look.

For a defender, getting token handling right is high-leverage: short lifetimes, tight scopes, header-only transmission, and server-side validation eliminate whole categories of finding at once. For an authorized tester, knowing the schemes tells you precisely where to probe and — just as importantly — how to write a remediation that actually closes the gap.

Core concepts

Each concept below is taught on its own: what it is, how it works, when it applies, and the pitfall that bites people.

1. API keys

Definition. A static secret string assigned to a client (an app or service), sent on each request to identify it.

How it works. The server stores a hash of the key and compares it on each call, usually passed in a header like X-API-Key: <key> or Authorization: Bearer <key>.

When to use / not. Fine for server-to-server identification of a client. Poor for identifying a user, and poor when it cannot be rotated or scoped.

Pitfall. API keys are frequently long-lived, over-permissioned, and pasted into URLs or committed to code. A single leaked key can be a full breach because it often has no expiry and broad scope.

2. Bearer tokens and JWTs

Definition. A bearer token is any token where mere possession grants access ("the bearer"). A JWT (JSON Web Token) is one common format: three Base64URL parts — header, payload, signature — joined by dots.

How it works. The client sends Authorization: Bearer <token>. For a JWT the server recomputes the signature over header.payload using the expected algorithm and key, then checks the claims (iss, aud, exp).

When to use / not. Bearer tokens suit stateless APIs. They are risky when transmitted or stored carelessly, because possession alone is enough.

Pitfall (and a myth to kill). Decoding a JWT is not verifying it. Base64URL is reversible text encoding, not encryption — anyone can read a JWT's payload with a decoder. Trust comes only from validating the signature with the correct key and algorithm. Accepting alg:none, hard-coding a weak HMAC secret, or confusing RS256 with HS256 all let an attacker forge tokens.

3. Token lifecycle and leakage vectors

Definition. The stages a token passes through: issue -> transmit -> store -> validate -> expire -> revoke. A weakness at any stage undermines the rest.

How it works. Good systems keep access tokens short-lived, transmit them only in headers over TLS, store them where XSS cannot reach, validate them fully on the server, and support revocation (a deny-list or short expiry) so a stolen token stops working quickly.

Pitfall. Common leak points: tokens in URLs (logged, kept in history, sent in Referer), tokens in client storage reachable by XSS, tokens echoed in error messages, and tokens committed to repositories. "No expiry" and "no revocation" turn a small leak into a permanent one.

4. OAuth 2.0 — delegated authorization

Definition. OAuth 2.0 lets a user grant an app limited access to their data on another service without sharing their password. Roles: the resource owner (user), the client (the app), the authorization server (issues tokens), and the resource server (the API).

How it works (Authorization Code flow). The client redirects the user to the authorization server with a client_id, requested scope, a redirect_uri, and a random state. The user approves; the authorization server redirects back to the registered redirect_uri with a one-time authorization code; the client exchanges that code (server-side, with its client secret / PKCE) for an access token and often a refresh token.

When to use / not. Use the Authorization Code flow (with PKCE for public clients). Avoid the legacy implicit flow, which returned tokens directly in the URL fragment and is now discouraged.

Pitfalls.

Pitfall What goes wrong Fix
Loose redirect_uri validation Attacker supplies their own callback and captures the code/token Exact-match allowlist of registered URIs
Missing state Callback is forgeable -> login CSRF / account stitching Generate random state, bind to session, verify on return
Implicit flow Token exposed in URL fragment and history Use Authorization Code + PKCE
Over-broad scope Token can do far more than needed Request least-privilege scopes; enforce them server-side

Threat model

TRUST BOUNDARIES (each | is a boundary tokens cross)

  [ Attacker ]                          [ Attacker-controlled site ]
       |  guesses/steals token                 ^  captures redirect
       v                                       |
  +----------+   Authorization: Bearer   +------------------+
  | Browser/ |-------- (header) -------->|   Resource API   |
  | Client   |                           |  (validates iss, |
  |  stores  |<-- code / access token ---|   aud, sig, exp) |
  |  token   |                           +------------------+
  +----------+                                   ^
       |  redirect w/ state + code               | token exchange
       v                                         |
  +------------------------+   register   +------------------+
  | Authorization Server   |<-- allowlist |  Client backend  |
  | (issues code/tokens,   |    of URIs    | (holds secret /  |
  |  checks redirect_uri)  |               |  does PKCE)      |
  +------------------------+               +------------------+

Assets:        access token, refresh token, authorization code, client secret
Entry points:  Authorization header, OAuth callback (redirect_uri + state),
               token-exchange endpoint, any URL that might carry a token
Trust boundary: client storage <-> network <-> resource server;
               authorization server <-> registered redirect_uri only

Knowledge check

  1. What asset is protected here, and which single stolen artifact lets an attacker call the API as the user? (The access token — and by extension the authorization code that can be exchanged for one.)
  2. Where is the trust boundary in the OAuth flow that a loose redirect_uri breaks? (Between the authorization server and the set of registered callback URIs — the server must only ever redirect to an allowlisted one.)
  3. Which insecure assumption causes redirect-based token theft? ("Any redirect_uri that starts with our domain is safe" — prefix/substring matching instead of exact match.)
  4. Which logs would reveal an attempted redirect attack, and why must this only be exercised in an authorized lab? (Auth-server logs of rejected redirect_uri values and mismatched state; you may only test systems you own or are explicitly authorized to test.)

Syntax notes

The key structures are the token header and the OAuth authorization request. Both are lab-safe illustrations with placeholder values.

Sending a bearer token (correct: header, not URL):

GET /api/v1/orders HTTP/1.1
Host: lab.localhost
Authorization: Bearer <access-token-placeholder>
Accept: application/json

An OAuth 2.0 Authorization Code request (the browser is redirected here). Note the four parameters that matter for security:

GET https://auth.lab.localhost/authorize
    ?response_type=code                     # ask for a code, NOT a token (avoids implicit flow)
    &client_id=lab-client                   # who is asking
    &redirect_uri=https://app.lab.localhost/callback  # MUST exactly match a registered URI
    &scope=orders:read                      # least privilege — only what is needed
    &state=9f2c...random...a17              # random, bound to the user's session (anti-CSRF)

Decode vs. verify — the distinction to remember:

base64url_decode(payload)   -> readable JSON      (anyone can do this; proves NOTHING)
verify(header.payload, key, alg) AND check iss/aud/exp  -> trust      (server only)

Decoding tells you what a token claims. Only verification tells you whether to believe it.

Lesson

APIs authenticate with tokens rather than session cookies. A token is a secret credential sent with each request. This shift changes the ways things can fail.

Schemes you'll meet

  • API keys — a static secret assigned to each client. They are often over-permissioned and end up leaked in code or URLs.
  • Bearer tokens / JWT — sent in the Authorization: Bearer ... header. (A JWT, or JSON Web Token, is a signed, self-contained token. JWT-specific flaws — alg:none, weak secrets, algorithm confusion — are covered in the Web App Security track and apply here directly.)
  • OAuth 2.0 — delegated authorization, explained below.

Token-handling pitfalls

  • Leakage. Tokens placed in URLs (which get logged and sent in the Referer header), stored in client-side storage exposed to XSS, printed in error messages, or committed to repositories.
  • No expiry. Tokens that are long-lived or never expire, with no way to revoke them.
  • Over-scoped tokens. A single token that is allowed to do everything.
  • Sent to the wrong host. A token meant for API A leaking to API B.

OAuth 2.0 in one paragraph

OAuth lets a user grant an app limited access to their data on another service without sharing their password. It does this through an authorization flow that returns an access token, and often a refresh token used to obtain new access tokens.

OAuth is authorization delegation, but it is frequently misused for authentication. Common issues are:

  • Weak, secretless flows such as the implicit flow.
  • Missing or loose redirect_uri validation, which allows token theft via an open redirect.
  • Missing state, which leaves the callback open to CSRF.
  • Over-broad scopes, granting more access than needed.

Fixes

  • Use short-lived, narrowly-scoped tokens.
  • Enforce a strict redirect_uri allowlist.
  • Include the state parameter on flows.
  • Transmit tokens only in headers, never in URLs.
  • Validate the issuer, audience, and expiry on the server.

Code examples

The example below shows the single most common OAuth token-theft primitive — a weak redirect_uri check — then the secure fix, then tests that prove the fix rejects bad input and accepts good input. It is written in Python (standard library only) so it runs anywhere without a framework, but the logic maps directly to any language.

(1) Insecure version

# WARNING: intentionally vulnerable - use only in a local, isolated, authorized lab. Do not deploy.
#
# The bug: the authorization server accepts any redirect_uri that merely
# STARTS WITH the registered prefix. An attacker registers a lookalike.

REGISTERED = "https://app.lab.localhost/callback"

def is_allowed_redirect_insecure(redirect_uri: str) -> bool:
    # BAD: prefix / substring matching
    return redirect_uri.startswith("https://app.lab.localhost")

# An attacker-controlled callback that still passes the weak check:
attacker = "https://app.lab.localhost.evil.example/callback"   # different host!
print(is_allowed_redirect_insecure(attacker))   # -> True  (token would be sent to the attacker)

The string https://app.lab.localhost.evil.example/callback starts with the trusted prefix, but its real host is app.lab.localhost.evil.example — an attacker domain. The authorization server would happily redirect the code/token there.

(2) Secure version

# Secure: exact-match against an allowlist of fully-registered redirect URIs,
# compared on parsed components (scheme + host + path), not raw string prefixes.

from urllib.parse import urlsplit

# Exact URIs registered for this client. No wildcards, no prefixes.
ALLOWLIST = {
    "https://app.lab.localhost/callback",
}

def _canonical(uri: str):
    parts = urlsplit(uri)
    # Normalize: lowercase scheme/host, keep path exact, reject anything odd.
    return (parts.scheme.lower(), parts.hostname or "", parts.port, parts.path)

def is_allowed_redirect(redirect_uri: str) -> bool:
    if redirect_uri not in ALLOWLIST:          # exact string match first
        return False
    # Defensive re-parse so a normalization trick cannot slip past the set check.
    want = {_canonical(u) for u in ALLOWLIST}
    return _canonical(redirect_uri) in want

(3) Verify — prove it rejects bad and accepts good

def test_redirect_validation():
    good = "https://app.lab.localhost/callback"

    bad_inputs = [
        "https://app.lab.localhost.evil.example/callback",  # lookalike host
        "https://app.lab.localhost/callback/../evil",       # path trickery
        "https://app.lab.localhost:8443/callback",          # unexpected port
        "http://app.lab.localhost/callback",                # downgraded scheme
        "https://app.lab.localhost/callback?next=//evil",   # extra query
        "https://attacker.example/callback",                # wrong host
    ]

    assert is_allowed_redirect(good) is True, "legit callback must be accepted"
    for b in bad_inputs:
        assert is_allowed_redirect(b) is False, f"must reject: {b}"
    print("PASS: rejects all attacker callbacks, accepts the registered one")

if __name__ == "__main__":
    test_redirect_validation()

Expected output: the insecure snippet prints True (demonstrating the flaw), and the test prints PASS: rejects all attacker callbacks, accepts the registered one. The lesson of the VERIFY step is that a fix is only trustworthy once you have shown it both blocks the malicious inputs and still allows the legitimate one — a validator that rejects everything is not secure, just broken.

Line by line

Walking the secure validator and its test:

  1. ALLOWLIST = {"https://app.lab.localhost/callback"} — the only URIs this client may receive a code at. A set of exact strings; no wildcards, no prefixes. This is the entire security boundary.
  2. _canonical(uri) uses urlsplit to break the URI into (scheme, host, port, path). Comparing parsed components, not raw text, defeats tricks like a trailing path or an unexpected port that look similar as strings.
  3. is_allowed_redirect first does an exact set membership test (redirect_uri not in ALLOWLIST). This alone rejects the lookalike host, the wrong host, the downgraded scheme, and the extra-query variants, because none of them equal the registered string.
  4. The second check re-parses and compares canonical tuples as defense in depth — if you ever loosen the set check, this still forces scheme+host+port+path to match.
  5. In the test, good is the registered callback and must return True. Each entry in bad_inputs is a real-world bypass attempt.

Trace of the key inputs through the validator:

Input In ALLOWLIST? Result Why
https://app.lab.localhost/callback yes accept exact registered URI
https://app.lab.localhost.evil.example/callback no reject host is a different domain
http://app.lab.localhost/callback no reject scheme downgraded to http
https://app.lab.localhost:8443/callback no reject unexpected port
https://app.lab.localhost/callback?next=//evil no reject extra query changes the string

Contrast with the insecure startswith check: the lookalike host passes because the string begins with the trusted prefix even though the host is attacker-controlled. That single difference — comparing hosts vs. comparing string prefixes — is the whole vulnerability.

Common mistakes

Real mistakes seen in production and in assessments:

1. Prefix/substring redirect_uri matching.

  • WRONG: redirect_uri.startswith("https://app.example").
  • WHY: https://app.example.attacker.com and https://app.example/../evil both pass; the code/token is sent to the attacker.
  • CORRECTED: exact-match against a registered allowlist, compared on parsed scheme+host+port+path.
  • RECOGNISE/PREVENT: grep the auth server for startswith, contains, RegExp, or wildcard redirect config; write a test with a lookalike host.

2. Treating a decoded JWT as verified.

  • WRONG: base64-decode the payload, read "role":"admin", trust it.
  • WHY: anyone can craft that payload; decoding is not authentication. Encoding is reversible; only the signature is trust.
  • CORRECTED: verify the signature with the expected key and algorithm, then check iss, aud, exp.
  • RECOGNISE/PREVENT: ensure the library call is verify, not decode; pin the allowed algorithm; reject alg:none.

3. Putting tokens in the URL.

  • WRONG: GET /api/orders?access_token=....
  • WHY: URLs land in server logs, browser history, and the Referer header sent to third parties.
  • CORRECTED: send Authorization: Bearer <token> in a header, over TLS only.
  • RECOGNISE/PREVENT: scan access logs and code for token= in query strings.

4. Omitting or not verifying state.

  • WRONG: start the OAuth flow with no state, or ignore it on return.
  • WHY: the callback becomes forgeable, enabling login CSRF / account stitching.
  • CORRECTED: generate a random state, store it bound to the session, and require an exact match on the callback.
  • RECOGNISE/PREVENT: confirm state is present and checked; a state you generate but never compare is no protection.

5. Over-scoped, long-lived tokens.

  • WRONG: one token with scope=* and no expiry "to keep it simple."
  • WHY: any leak is total and permanent.
  • CORRECTED: request least-privilege scopes, short access-token lifetimes, and a refresh/revocation path.
  • RECOGNISE/PREVENT: inspect exp and scope claims; ask "what can this token do, and for how long?"

Debugging tips

When token or OAuth handling misbehaves, work the lifecycle stage by stage.

  • 401 Unauthorized on every call. Check the header spelling exactly: Authorization: Bearer <token> (capital B, one space). Confirm the token has not expired (exp) and that you are hitting the right aud/host.
  • Token "works" but shouldn't (forged tokens accepted). You are decoding, not verifying. Log the algorithm actually used and the key id; confirm the library rejects alg:none and mismatched algorithms.
  • OAuth callback fails with redirect_uri_mismatch. The URI sent must exactly equal a registered one — down to scheme, trailing slash, and port. Print both the sent and registered values and diff them character by character.
  • state mismatch on callback. Confirm the same state you generated is stored server-side (or in a signed cookie) and compared on return; a fresh page load or a second tab can desync it.
  • Intermittent auth failures across instances. A stateless JWT verified against a rotated or per-instance key will fail on some nodes; check key distribution and clock skew (exp/nbf need synced clocks).

Questions to ask when it fails: Which lifecycle stage broke — issue, transmit, store, validate, expire, or revoke? Is the token being verified or merely decoded? Does the redirect_uri match exactly? Is state both generated and checked? Is the token where it should never be — a URL, a log line, a repo?

Memory safety

Security & safety: detection and logging for token abuse.

Good logging is how you catch a stolen or forged token in production. The goal is to record enough to investigate an incident without ever turning your logs into the next leak.

Log (per auth-relevant event):

  • Timestamp (UTC), source IP / client id, and the resource or endpoint requested.
  • The security decision and result: allow/deny, and why (e.g. redirect_uri_rejected, signature_invalid, token_expired, scope_insufficient).
  • A correlation / request id so you can stitch a flow together.
  • Token metadata only: a token id (jti) or a truncated hash prefix, the scope, and exp — never the token itself.
  • OAuth specifics: client_id, requested vs. granted scope, and rejected redirect_uri/state values.

Never log: the raw access/refresh token, the authorization code, passwords, session cookies, client secrets, private keys, or full JWTs. Redact PII you do not need. If you must correlate on a token, store a salted hash, not the value.

Events that signal abuse: a spike in signature_invalid or alg mismatches (forgery attempts); repeated redirect_uri_rejected with lookalike hosts (redirect attack); state mismatches (callback tampering); one token used from many IPs or geographies at once (token replay/theft); use of a scope the client never requested.

False positives arise from legitimate causes: users behind rotating mobile IPs or corporate proxies (looks like multi-IP use), clock skew making valid tokens look expired (exp/nbf), an app that legitimately refreshes tokens rapidly, and load balancers that hide the real client IP. Tune thresholds and enrich with the correlation id before alerting so a real operational quirk is not mistaken for an attack — and so a real attack is not lost in the noise.

Real-world uses

Authorized use case. A team runs a scheduled API assessment of their own mobile-app backend in a staging environment. They enumerate every endpoint, confirm tokens travel only in headers over TLS, decode sample JWTs to read the claims, then verify signatures and check that expired or wrong-aud tokens are rejected. They test the OAuth callback with lookalike redirect_uri values and a missing state, all against systems they own — then file remediations with verification steps.

Professional best-practice habits

  • Validation: exact-match redirect_uri allowlists; verify JWT signature, iss, aud, and exp; reject alg:none and unexpected algorithms.
  • Least privilege: request the narrowest scope and enforce it on the resource server, not just at issuance.
  • Secure defaults: short access-token lifetimes, refresh-token rotation, Authorization Code + PKCE (never implicit), TLS everywhere.
  • Logging & error handling: log security decisions with correlation ids; return generic auth errors to clients while logging detail internally; never echo tokens in errors.
Beginner Advanced
Tokens Read/decode a JWT, send it in the correct header Verify signature, iss/aud/exp; catch alg confusion and weak secrets
OAuth Recognize the Authorization Code flow parameters Test redirect_uri/state/scope handling and PKCE; write remediations
Detection Know which events to log Build abuse-detection rules and tune false positives

Always remember: passing an automated scanner does not prove an API is secure, and no system is ever "completely secure" — the goal is to raise cost and shrink the attack surface with verified controls.

Practice tasks

All tasks are lab-only: run them against services you own or an intentionally-vulnerable app on localhost / a container / a CTF. Authorization checklist before any test: (1) you own the target or have explicit written permission; (2) it is an isolated lab (localhost/VM/container), not production or a third party; (3) you know the reset/cleanup steps; (4) you will stop if you hit anything out of scope. Every task ends by remediating and verifying, not just finding.

Beginner 1 — Decode vs. verify.

  • Objective: prove to yourself that decoding is not verifying.
  • Requirements: take a sample JWT from your lab, Base64URL-decode header and payload, then attempt verification with the correct key and again with a wrong key.
  • Expected: decoding always yields readable JSON; verification succeeds only with the correct key/algorithm.
  • Hints: watch the alg header; try flipping RS256/HS256 in your notes. Concepts: encoding vs. signature, iss/aud/exp.
  • Defensive conclusion: write one sentence on why your API must call verify, not decode.

Beginner 2 — Find tokens in the wrong place.

  • Objective: locate token leakage vectors in a lab app.
  • Requirements: search access logs, browser history, and source for token= in URLs and any token committed to the repo.
  • Constraints: your own repo/logs only.
  • Hints: check the Referer header behavior. Concepts: leakage vectors, header-only transmission.
  • Defensive conclusion: move any URL token to the Authorization header and confirm the log no longer contains it.

Intermediate 1 — Harden redirect_uri validation.

  • Objective: replace a weak redirect check with an exact-match allowlist.
  • Requirements: given a validator using startswith/substring/regex, rewrite it to exact-match on parsed scheme+host+port+path; keep a registered URI working.
  • Input/output: legitimate callback -> accept; lookalike host, downgraded scheme, extra port/query -> reject.
  • Hints: reuse the trace table from this lesson. Concepts: allowlist validation, mitigation verification.
  • Defensive conclusion: add a test that proves both rejection and acceptance; note which log event fires on rejection.

Intermediate 2 — Prove state stops callback CSRF.

  • Objective: add and verify the state parameter in a lab OAuth flow.
  • Requirements: generate random state, bind it to the session, and reject callbacks whose state does not match.
  • Expected: a replayed/forged callback with a missing or wrong state is rejected; the legitimate flow succeeds.
  • Hints: a state you generate but never compare is no defense. Concepts: anti-CSRF, session binding.
  • Defensive conclusion: log state_mismatch and confirm it appears only on the forged attempt.

Challenge — Build a token-abuse detection rule.

  • Objective: detect likely token theft from logs without logging the token.
  • Requirements: design a rule that flags one token id (jti) used from many IPs/geos in a short window, plus spikes in signature_invalid and redirect_uri_rejected; write down at least two false-positive sources and how you would tune them.
  • Constraints: logs must contain metadata only (no raw tokens/PII).
  • Hints: use the correlation id to group events. Concepts: detection & logging, false positives, least privilege.
  • Defensive conclusion: state the remediation each alert would trigger (revoke token, tighten allowlist) and how you would retest that it worked. Cleanup/reset: revoke any lab tokens you issued, clear test log entries, and restore the app to its baseline state.

Summary

  • API authentication centers on tokens, so the main risk areas become leakage, lifetime, and scope across the lifecycle: issue -> transmit -> store -> validate -> expire -> revoke.
  • The three schemes — API keys, bearer tokens/JWT, and OAuth 2.0 — each fail differently; know where to look in each.
  • Decoding a JWT is not verifying it. Trust comes only from validating the signature with the right key and algorithm, plus iss/aud/exp. Reject alg:none.
  • OAuth's recurring flaws are loose redirect_uri (fix: exact-match allowlist), missing/unchecked state (fix: random, session-bound, verified), the implicit flow (fix: Authorization Code + PKCE), and over-broad scope (fix: least privilege).
  • Key commands/structures: send Authorization: Bearer <token> in a header (never a URL); build allowlists that compare parsed scheme+host+port+path, not string prefixes.
  • Common mistakes: prefix redirect matching, trusting decoded JWTs, tokens in URLs, unverified state, and over-scoped/immortal tokens.
  • Verify every fix by proving it rejects bad input and accepts good input, and log security decisions (with metadata, never the token itself) to detect abuse. Passing a scanner does not prove security, and nothing is ever "completely secure."

Practice with these exercises