Skip to content

JWT Tokens Interview Questions & Answers

11 questions Updated 2026-06-20 Share:

FastAPI JWT interview questions — encoding, decoding, python-jose, expiry, claims, signature verification and common JWT pitfalls.

11 of 11

A JSON Web Token (JWT) is a compact, URL-safe string that encodes a signed JSON payload. It has three dot-separated Base64URL-encoded parts:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9   ← Header
.eyJzdWIiOiIxMjM0NTY3ODkwIiwiZXhwIjoxN}  ← Payload (claims)
.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c  ← Signature
  • Header: algorithm and token type ({"alg": "HS256", "typ": "JWT"}).
  • Payload: claims — standard (sub, exp, iat) and custom (role, scopes).
  • Signature: HMAC or RSA/EC signature over header + payload.

JWTs are not encrypted by default — the payload is only Base64-encoded. Anyone can decode it. The signature only proves it wasn't tampered with.

Rule of thumb: never put sensitive data (passwords, PII) in JWT payload — use JWE (encrypted JWT) if you need to protect the payload contents.

Install python-jose[cryptography] and use jose.jwt:

from datetime import datetime, timedelta, timezone
from jose import jwt, JWTError

SECRET_KEY = "your-256-bit-secret"
ALGORITHM = "HS256"

def create_access_token(user_id: int, expires_minutes: int = 30) -> str:
    payload = {
        "sub": str(user_id),
        "exp": datetime.now(timezone.utc) + timedelta(minutes=expires_minutes),
        "iat": datetime.now(timezone.utc),
    }
    return jwt.encode(payload, SECRET_KEY, algorithm=ALGORITHM)

def decode_access_token(token: str) -> dict:
    try:
        return jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
    except JWTError:
        raise HTTPException(401, "Invalid token")

Rule of thumb: always pass algorithms= as a list (not a string) to jwt.decode() — the list prevents algorithm-confusion attacks where an attacker switches from RS256 to HS256.

The exp claim is a Unix timestamp. python-jose automatically validates it during jwt.decode() — expired tokens raise ExpiredSignatureError (a subclass of JWTError).

from jose import ExpiredSignatureError, JWTError

async def get_current_user(token: str = Depends(oauth2_scheme)):
    try:
        payload = jwt.decode(token, SECRET_KEY, algorithms=["HS256"])
    except ExpiredSignatureError:
        raise HTTPException(401, "Token has expired")
    except JWTError:
        raise HTTPException(401, "Invalid token")
    return payload

Clock skew: python-jose allows a small tolerance via options={"leeway": 10} (10 seconds).

Rule of thumb: catch ExpiredSignatureError separately from other JWTError subtypes so you can give clients a specific "please refresh" error instead of a generic "invalid token".

Claim Name Meaning
sub Subject Who the token is about (user ID)
exp Expiration Unix timestamp when token expires
iat Issued At Unix timestamp when token was created
nbf Not Before Token not valid before this timestamp
jti JWT ID Unique ID for revocation
iss Issuer Who created the token
aud Audience Who the token is intended for
payload = {
    "sub": str(user.id),
    "exp": utcnow + timedelta(minutes=30),
    "iat": utcnow,
    "jti": str(uuid4()),     # for revocation
    "iss": "https://myapi.example.com",
}

Rule of thumb: always set sub, exp, and iat; add jti if you need token revocation; add iss/aud for multi-service architectures.

HS256 (HMAC) RS256 (RSA)
Key type Shared secret Public/private key pair
Who can sign Anyone with the secret Only the private key holder
Who can verify Anyone with the secret Anyone with the public key
Use case Single service Multiple services / external clients
# HS256 — single service
jwt.encode(payload, "shared-secret", algorithm="HS256")
jwt.decode(token,  "shared-secret", algorithms=["HS256"])

# RS256 — multi-service (sign with private, verify with public)
jwt.encode(payload, private_key, algorithm="RS256")
jwt.decode(token,  public_key,   algorithms=["RS256"])

With RS256, you can publish the public key (JWKS endpoint) so other services verify tokens without ever seeing the private key.

Rule of thumb: use HS256 for simple single-service auth; use RS256 when tokens need to be verified by external services or published via a JWKS endpoint.

If a service uses RS256 but jwt.decode() is called with algorithms=["RS256", "HS256"], an attacker can forge a token by taking the RS256 public key (which is public), using it as an HMAC secret, and signing a new token with HS256. The server accepts it because HS256 is allowed.

Prevention:

# SAFE — only allow the expected algorithm
jwt.decode(token, PUBLIC_KEY, algorithms=["RS256"])   # never include HS256

# DANGEROUS
jwt.decode(token, PUBLIC_KEY, algorithms=["RS256", "HS256"])

Also: never use algorithms=None or skip algorithm validation.

Rule of thumb: always pin the algorithm to exactly one value matching what you use to sign — allow-listing multiple algorithms invites confusion attacks.

Early JWT libraries accepted tokens with "alg": "none" — meaning no signature is needed. An attacker could forge any payload, set alg: none, and the library would accept it without any verification.

# Malicious token — unsigned
{"alg": "none", "typ": "JWT"}.{"sub": "admin"}.

Modern libraries (python-jose, PyJWT) reject none by default. Ensure "none" is not in your algorithms= list:

jwt.decode(token, SECRET, algorithms=["HS256"])   # "none" not listed → rejected

Rule of thumb: never include "none" in algorithms=; if your library has a verify_signature=False option, never use it in production.

Storage XSS risk CSRF risk Notes
localStorage High None JS-accessible — token stolen by XSS
sessionStorage High None Same as localStorage but tab-scoped
httpOnly cookie None Medium Inaccessible to JS; add SameSite=Lax

Best practice — httpOnly, Secure, SameSite=Lax cookie:

response.set_cookie(
    key="access_token",
    value=token,
    httponly=True,
    secure=True,
    samesite="lax",
    max_age=1800,
)

SameSite=Lax blocks cross-site POST requests (CSRF for state-changing operations) while allowing cross-site GET navigation.

Rule of thumb: store tokens in httpOnly cookies, not localStorage — XSS is more common than CSRF, and httpOnly provides a meaningful defence against it.

Refresh token rotation issues a new refresh token on every use and immediately invalidates the old one. If a stolen refresh token is used, the legitimate user's next request will fail (old token invalid) — alerting you to a compromise.

@app.post("/refresh")
async def refresh(old_token: str = Body(...)):
    payload = decode_refresh_token(old_token)
    jti = payload["jti"]

    # Check old token exists and isn't already used
    stored = await db.get_refresh_token(jti)
    if not stored or stored.used:
        # Token reuse detected — revoke all user tokens
        await db.revoke_all_tokens(payload["sub"])
        raise HTTPException(401, "Refresh token reuse detected")

    await db.mark_used(jti)
    new_refresh = create_refresh_token(payload["sub"])
    new_access  = create_access_token(payload["sub"])
    await db.store_refresh_token(new_refresh)
    return {"access_token": new_access, "refresh_token": new_refresh}

Rule of thumb: implement refresh token rotation for any app with sensitive data — a detected reuse should trigger automatic revocation of all user sessions.

Pass options={"verify_signature": False} to jwt.decode() in python-jose, or use PyJWT with algorithms=[]:

# python-jose
payload = jwt.decode(
    token,
    key="",
    algorithms=["HS256"],
    options={"verify_signature": False},
)

This is never safe for authenticating users. Valid uses:

  • Logging the sub from an already-verified token that expired (for debugging).
  • Inspecting headers to decide which key/algorithm to use before re-decoding with verification.

Rule of thumb: never use unverified JWT decoding in the auth path — it is only for tooling and debugging flows where the token has already been verified elsewhere.

JWTs have no spec-defined size limit, but practical limits exist:

  • HTTP headers: servers typically cap individual headers at 8 KB.
  • Cookies: limited to 4 KB per cookie.
  • Typical JWT: a token with 5-10 claims is 200-500 bytes after Base64URL encoding.

Problems arise when you embed large payloads (roles list, user profile) in the token:

# BAD — embedding permissions in token can push it over 4 KB
payload = {"sub": "1", "permissions": list_of_200_permissions}

# GOOD — store just the user ID; look up permissions from DB/cache
payload = {"sub": "1", "exp": ..., "role": "admin"}

Rule of thumb: keep JWT payloads under 1 KB — store only stable identity claims (sub, role, scopes); look up volatile permissions from cache at request time.

More ways to practice

The self-quiz is live. Get notified when mock interviews and new question packs drop.

or
Join our WhatsApp Channel