[{"data":1,"prerenderedAt":88},["ShallowReactive",2],{"qa-\u002Ffastapi\u002Fsecurity\u002Fjwt":3},{"page":4,"siblings":78,"blog":69},{"id":5,"title":6,"body":7,"description":11,"difficulty":14,"extension":15,"framework":16,"frameworkSlug":17,"meta":18,"navigation":19,"order":12,"path":20,"questions":21,"questionsCount":68,"related":69,"seo":70,"seoDescription":71,"stem":72,"subtopic":73,"topic":74,"topicSlug":75,"updated":76,"__hash__":77},"qa\u002Ffastapi\u002Fsecurity\u002Fjwt.md","Jwt",{"type":8,"value":9,"toc":10},"minimark",[],{"title":11,"searchDepth":12,"depth":12,"links":13},"",2,[],"hard","md","FastAPI","fastapi",{},true,"\u002Ffastapi\u002Fsecurity\u002Fjwt",[22,27,32,36,40,44,48,52,56,60,64],{"id":23,"difficulty":24,"q":25,"a":26},"what-is-jwt","easy","What is a JWT and what are its three parts?","A **JSON Web Token** (JWT) is a compact, URL-safe string that encodes a signed\nJSON payload. It has three dot-separated Base64URL-encoded parts:\n\n```\neyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9   ← Header\n.eyJzdWIiOiIxMjM0NTY3ODkwIiwiZXhwIjoxN}  ← Payload (claims)\n.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c  ← Signature\n```\n\n- **Header**: algorithm and token type (`{\"alg\": \"HS256\", \"typ\": \"JWT\"}`).\n- **Payload**: claims — standard (`sub`, `exp`, `iat`) and custom (`role`, `scopes`).\n- **Signature**: HMAC or RSA\u002FEC signature over header + payload.\n\nJWTs are **not encrypted by default** — the payload is only Base64-encoded.\nAnyone can decode it. The signature only proves it wasn't tampered with.\n\nRule of thumb: never put sensitive data (passwords, PII) in JWT payload — use\nJWE (encrypted JWT) if you need to protect the payload contents.\n",{"id":28,"difficulty":29,"q":30,"a":31},"encode-decode-jwt","medium","How do you encode and decode a JWT in a FastAPI app?","Install `python-jose[cryptography]` and use `jose.jwt`:\n\n```python\nfrom datetime import datetime, timedelta, timezone\nfrom jose import jwt, JWTError\n\nSECRET_KEY = \"your-256-bit-secret\"\nALGORITHM = \"HS256\"\n\ndef create_access_token(user_id: int, expires_minutes: int = 30) -> str:\n    payload = {\n        \"sub\": str(user_id),\n        \"exp\": datetime.now(timezone.utc) + timedelta(minutes=expires_minutes),\n        \"iat\": datetime.now(timezone.utc),\n    }\n    return jwt.encode(payload, SECRET_KEY, algorithm=ALGORITHM)\n\ndef decode_access_token(token: str) -> dict:\n    try:\n        return jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])\n    except JWTError:\n        raise HTTPException(401, \"Invalid token\")\n```\n\nRule of thumb: always pass `algorithms=` as a list (not a string) to `jwt.decode()` —\nthe list prevents algorithm-confusion attacks where an attacker switches from RS256 to HS256.\n",{"id":33,"difficulty":29,"q":34,"a":35},"jwt-expiry","How does JWT expiry work and how does FastAPI check it?","The `exp` claim is a Unix timestamp. `python-jose` automatically validates it\nduring `jwt.decode()` — expired tokens raise `ExpiredSignatureError` (a subclass\nof `JWTError`).\n\n```python\nfrom jose import ExpiredSignatureError, JWTError\n\nasync def get_current_user(token: str = Depends(oauth2_scheme)):\n    try:\n        payload = jwt.decode(token, SECRET_KEY, algorithms=[\"HS256\"])\n    except ExpiredSignatureError:\n        raise HTTPException(401, \"Token has expired\")\n    except JWTError:\n        raise HTTPException(401, \"Invalid token\")\n    return payload\n```\n\nClock skew: `python-jose` allows a small tolerance via `options={\"leeway\": 10}` (10 seconds).\n\nRule of thumb: catch `ExpiredSignatureError` separately from other `JWTError` subtypes\nso you can give clients a specific \"please refresh\" error instead of a generic \"invalid token\".\n",{"id":37,"difficulty":29,"q":38,"a":39},"jwt-claims","What standard JWT claims should you include and what do they mean?","| Claim | Name | Meaning |\n|-------|------|---------|\n| `sub` | Subject | Who the token is about (user ID) |\n| `exp` | Expiration | Unix timestamp when token expires |\n| `iat` | Issued At | Unix timestamp when token was created |\n| `nbf` | Not Before | Token not valid before this timestamp |\n| `jti` | JWT ID | Unique ID for revocation |\n| `iss` | Issuer | Who created the token |\n| `aud` | Audience | Who the token is intended for |\n\n```python\npayload = {\n    \"sub\": str(user.id),\n    \"exp\": utcnow + timedelta(minutes=30),\n    \"iat\": utcnow,\n    \"jti\": str(uuid4()),     # for revocation\n    \"iss\": \"https:\u002F\u002Fmyapi.example.com\",\n}\n```\n\nRule of thumb: always set `sub`, `exp`, and `iat`; add `jti` if you need\ntoken revocation; add `iss`\u002F`aud` for multi-service architectures.\n",{"id":41,"difficulty":14,"q":42,"a":43},"hs256-vs-rs256","What is the difference between HS256 and RS256 JWT signing and when do you use each?","| | HS256 (HMAC) | RS256 (RSA) |\n|---|---|---|\n| Key type | Shared secret | Public\u002Fprivate key pair |\n| Who can sign | Anyone with the secret | Only the private key holder |\n| Who can verify | Anyone with the secret | Anyone with the public key |\n| Use case | Single service | Multiple services \u002F external clients |\n\n```python\n# HS256 — single service\njwt.encode(payload, \"shared-secret\", algorithm=\"HS256\")\njwt.decode(token,  \"shared-secret\", algorithms=[\"HS256\"])\n\n# RS256 — multi-service (sign with private, verify with public)\njwt.encode(payload, private_key, algorithm=\"RS256\")\njwt.decode(token,  public_key,   algorithms=[\"RS256\"])\n```\n\nWith RS256, you can publish the public key (JWKS endpoint) so other services\nverify tokens without ever seeing the private key.\n\nRule of thumb: use HS256 for simple single-service auth; use RS256 when tokens\nneed to be verified by external services or published via a JWKS endpoint.\n",{"id":45,"difficulty":14,"q":46,"a":47},"algorithm-confusion","What is the JWT algorithm confusion attack and how does FastAPI prevent it?","If a service uses RS256 but `jwt.decode()` is called with `algorithms=[\"RS256\", \"HS256\"]`,\nan attacker can forge a token by taking the RS256 public key (which is public),\nusing it as an HMAC secret, and signing a new token with HS256. The server\naccepts it because HS256 is allowed.\n\nPrevention:\n```python\n# SAFE — only allow the expected algorithm\njwt.decode(token, PUBLIC_KEY, algorithms=[\"RS256\"])   # never include HS256\n\n# DANGEROUS\njwt.decode(token, PUBLIC_KEY, algorithms=[\"RS256\", \"HS256\"])\n```\n\nAlso: never use `algorithms=None` or skip algorithm validation.\n\nRule of thumb: always pin the algorithm to exactly one value matching what you\nuse to sign — allow-listing multiple algorithms invites confusion attacks.\n",{"id":49,"difficulty":14,"q":50,"a":51},"jwt-none-attack","What is the JWT \"none\" algorithm attack?","Early JWT libraries accepted tokens with `\"alg\": \"none\"` — meaning no signature\nis needed. An attacker could forge any payload, set `alg: none`, and the library\nwould accept it without any verification.\n\n```\n# Malicious token — unsigned\n{\"alg\": \"none\", \"typ\": \"JWT\"}.{\"sub\": \"admin\"}.\n```\n\nModern libraries (`python-jose`, `PyJWT`) reject `none` by default. Ensure\n`\"none\"` is not in your `algorithms=` list:\n\n```python\njwt.decode(token, SECRET, algorithms=[\"HS256\"])   # \"none\" not listed → rejected\n```\n\nRule of thumb: never include `\"none\"` in `algorithms=`; if your library has a\n`verify_signature=False` option, never use it in production.\n",{"id":53,"difficulty":29,"q":54,"a":55},"jwt-storage","Where should JWTs be stored on the client side, and what are the security trade-offs?","| Storage | XSS risk | CSRF risk | Notes |\n|---------|----------|-----------|-------|\n| `localStorage` | High | None | JS-accessible — token stolen by XSS |\n| `sessionStorage` | High | None | Same as localStorage but tab-scoped |\n| `httpOnly` cookie | None | Medium | Inaccessible to JS; add SameSite=Lax |\n\nBest practice — **`httpOnly`, `Secure`, `SameSite=Lax` cookie**:\n```python\nresponse.set_cookie(\n    key=\"access_token\",\n    value=token,\n    httponly=True,\n    secure=True,\n    samesite=\"lax\",\n    max_age=1800,\n)\n```\n\n`SameSite=Lax` blocks cross-site POST requests (CSRF for state-changing operations)\nwhile allowing cross-site GET navigation.\n\nRule of thumb: store tokens in `httpOnly` cookies, not `localStorage` — XSS is\nmore common than CSRF, and `httpOnly` provides a meaningful defence against it.\n",{"id":57,"difficulty":14,"q":58,"a":59},"token-rotation","What is refresh token rotation and why is it more secure than long-lived access tokens?","**Refresh token rotation** issues a new refresh token on every use and immediately\ninvalidates the old one. If a stolen refresh token is used, the legitimate user's\nnext request will fail (old token invalid) — alerting you to a compromise.\n\n```python\n@app.post(\"\u002Frefresh\")\nasync def refresh(old_token: str = Body(...)):\n    payload = decode_refresh_token(old_token)\n    jti = payload[\"jti\"]\n\n    # Check old token exists and isn't already used\n    stored = await db.get_refresh_token(jti)\n    if not stored or stored.used:\n        # Token reuse detected — revoke all user tokens\n        await db.revoke_all_tokens(payload[\"sub\"])\n        raise HTTPException(401, \"Refresh token reuse detected\")\n\n    await db.mark_used(jti)\n    new_refresh = create_refresh_token(payload[\"sub\"])\n    new_access  = create_access_token(payload[\"sub\"])\n    await db.store_refresh_token(new_refresh)\n    return {\"access_token\": new_access, \"refresh_token\": new_refresh}\n```\n\nRule of thumb: implement refresh token rotation for any app with sensitive data —\na detected reuse should trigger automatic revocation of all user sessions.\n",{"id":61,"difficulty":14,"q":62,"a":63},"jwt-decode-without-verify","How do you decode a JWT payload without verifying the signature, and when is this safe?","Pass `options={\"verify_signature\": False}` to `jwt.decode()` in `python-jose`,\nor use `PyJWT` with `algorithms=[]`:\n\n```python\n# python-jose\npayload = jwt.decode(\n    token,\n    key=\"\",\n    algorithms=[\"HS256\"],\n    options={\"verify_signature\": False},\n)\n```\n\nThis is **never safe** for authenticating users. Valid uses:\n- Logging the `sub` from an **already-verified** token that expired (for debugging).\n- Inspecting headers to decide which key\u002Falgorithm to use before re-decoding with verification.\n\nRule of thumb: never use unverified JWT decoding in the auth path — it is only\nfor tooling and debugging flows where the token has already been verified elsewhere.\n",{"id":65,"difficulty":24,"q":66,"a":67},"jwt-size","What are the practical size limits for JWT tokens and how do they affect HTTP?","JWTs have no spec-defined size limit, but practical limits exist:\n- **HTTP headers**: servers typically cap individual headers at 8 KB.\n- **Cookies**: limited to 4 KB per cookie.\n- **Typical JWT**: a token with 5-10 claims is 200-500 bytes after Base64URL encoding.\n\nProblems arise when you embed large payloads (roles list, user profile) in the token:\n\n```python\n# BAD — embedding permissions in token can push it over 4 KB\npayload = {\"sub\": \"1\", \"permissions\": list_of_200_permissions}\n\n# GOOD — store just the user ID; look up permissions from DB\u002Fcache\npayload = {\"sub\": \"1\", \"exp\": ..., \"role\": \"admin\"}\n```\n\nRule of thumb: keep JWT payloads under 1 KB — store only stable identity claims\n(`sub`, `role`, `scopes`); look up volatile permissions from cache at request time.\n",11,null,{"description":11},"FastAPI JWT interview questions — encoding, decoding, python-jose, expiry, claims, signature verification and common JWT pitfalls.","fastapi\u002Fsecurity\u002Fjwt","JWT Tokens","Security & Auth","security","2026-06-20","a8L1LKxnRwm7dhkGZHtjql_sWeh8ZBuxGKpnQdkRifU",[79,83,84],{"subtopic":80,"path":81,"order":82},"OAuth2","\u002Ffastapi\u002Fsecurity\u002Foauth2",1,{"subtopic":73,"path":20,"order":12},{"subtopic":85,"path":86,"order":87},"API Keys","\u002Ffastapi\u002Fsecurity\u002Fapi-keys",3,1782244113053]