Skip to content

All lessons

Area 08 // BackendLesson 130 / 14016 min read

Authentication with JWT and OAuth 2.0

Authentication proves who is calling your API; a JWT is a signed token that carries the user's identity and permissions so any server can verify it without a database lookup, and OAuth 2.0 is the standard way to let users sign in with another provider or grant an app limited access to their accounts.

Think of it like this

Analogy

A festival wristband: at the gate (login) staff check your ticket once and give you a wristband printed with your name, which areas you can enter, and an expiry date, sealed with a tamper-proof clip (the signature). Every stage's security just looks at the wristband; they don't phone the ticket office. Anyone can read what's printed on it, so you wouldn't print your bank PIN there.

OAuth is the hotel giving a cleaner a key card for certain rooms and hours, instead of handing over the master key.

The problem it solves

Your AI API costs money per call and touches private data, so every request must come from a known user, and you need to know what that user may do. Sessions stored on one server don't scale across replicas; passwords should never travel on every request; and users want to "Sign in with Google" or connect their calendar to your agent without giving you their password.

How it works: authentication with JWT

  1. The user logs in (password plus MFA, a magic link, or OAuth); the auth server issues an access token, often a JWT.
  2. A JWT has three base64 parts: a header (algorithm), a payload of claims (sub user ID, exp expiry, iat issued at, iss issuer, aud audience, scope permissions) and a signature.
  3. Signing: HS256 uses a shared secret; RS256 or ES256 use a private key to sign and a public key to verify (better when many services verify tokens).
  4. The client sends it on each request: Authorization: Bearer <token>.
  5. The API verifies the signature with a pinned algorithm, then exp, iss and aud, and reads the user and scopes, with no database lookup (see the code).
  6. Short lifetimes: access tokens last minutes; a refresh token (stored securely, revocable) gets new ones.
  7. In FastAPI: a dependency (Depends(current_user)) that every protected route uses; then authorization checks per action (agent authorization).

How it works: OAuth 2.0

  1. OAuth 2.0 is a standard for delegated access: a user lets an app act on their behalf at another service, with specific scopes, without sharing their password.
  2. Authorization code flow with PKCE (for web and mobile apps): redirect the user to the provider's login and consent page → the provider redirects back with a short-lived code → your backend exchanges the code (plus the PKCE verifier) for tokens.
  3. OpenID Connect adds an ID token on top for "Sign in with...": who the user is.
  4. Client credentials flow for service-to-service calls (no user).
  5. For AI agents: agents call Gmail, Slack or GitHub with the user's OAuth token and narrow scopes; MCP uses OAuth 2.1 for remote servers (MCP server trust).
  6. Use a provider or library (identity platforms, auth services, well-known libraries) rather than writing auth yourself.
Log in once, then every request carries a signed token the API can verify on its own.

See it in code

Install
pip install fastapi httpx pyjwt
Needs
Python 3.10+. The signing secret is a placeholder; load real ones from a secrets manager.

Issuing and verifying JWTs in a FastAPI dependency: missing, valid, expired and tampered tokens.

jwt_auth.py
import time
import jwt                                    # pip install pyjwt
from fastapi import Depends, FastAPI, HTTPException
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
from fastapi.testclient import TestClient
 
SECRET = "change-me: load from a secrets manager"   # HS256 shared secret (RS256/ES256 use key pairs)
 
def issue_token(user_id: str, scopes: list[str], ttl_s: int = 900) -> str:
    now = int(time.time())
    return jwt.encode({"sub": user_id, "scope": " ".join(scopes), "iat": now, "exp": now + ttl_s},
                      SECRET, algorithm="HS256")
 
def current_user(creds: HTTPAuthorizationCredentials = Depends(HTTPBearer(auto_error=False))) -> dict:
    if creds is None:
        raise HTTPException(401, "missing token")
    try:
        return jwt.decode(creds.credentials, SECRET, algorithms=["HS256"])   # checks signature AND expiry
    except jwt.ExpiredSignatureError:
        raise HTTPException(401, "token expired")
    except jwt.InvalidTokenError:
        raise HTTPException(401, "invalid token")
 
app = FastAPI()
@app.get("/me")
def me(user: dict = Depends(current_user)):
    return {"user": user["sub"], "scope": user["scope"]}
 
c = TestClient(app)
good = issue_token("alice", ["chat:read", "chat:write"])
expired = issue_token("alice", ["chat:read"], ttl_s=-10)
tampered = good[:-4] + ("AAAA" if not good.endswith("AAAA") else "BBBB")
for name, headers in [("no token", {}), ("valid", {"Authorization": f"Bearer {good}"}),
                      ("expired", {"Authorization": f"Bearer {expired}"}),
                      ("tampered", {"Authorization": f"Bearer {tampered}"})]:
    r = c.get("/me", headers=headers)
    print(f"{name:9} -> {r.status_code} {r.json()}")
print("payload is only base64, not encrypted:", jwt.decode(good, options={"verify_signature": False})["sub"])
Output
no token  -> 401 {'detail': 'missing token'}
valid     -> 200 {'user': 'alice', 'scope': 'chat:read chat:write'}
expired   -> 401 {'detail': 'token expired'}
tampered  -> 401 {'detail': 'invalid token'}
payload is only base64, not encrypted: alice

algorithms=["HS256"] pins the algorithm, which blocks classic attacks that switch it. In production also pass audience= and issuer= to jwt.decode.

A real example

Example

An assistant that reads your calendar

A scheduling assistant needed users' calendars. Instead of asking for passwords, it used OAuth with the calendar provider: users clicked "Connect calendar", saw a consent screen listing read-only calendar access, and the app received tokens limited to that scope. The agent's calendar tool used the user's own token, so it could never see anyone else's calendar (agent authorization), and users could revoke access in their account settings at any time. Logins to the assistant itself used short-lived JWTs verified by every API replica with the auth service's public key.

The easy way

Start here

Use a hosted identity provider or a well-maintained auth library for login and OAuth; in your API, verify the bearer token in one FastAPI dependency and use it on every route. Never build password storage or OAuth flows from scratch for a first version.

When you grow: the production setup

Teams use an identity provider with MFA and SSO, asymmetric signing with key rotation (JWKS endpoints), short-lived access tokens with refresh-token rotation, audience and issuer checks, scope-based authorization, token revocation lists for emergencies, OAuth with PKCE and minimal scopes for integrations, and audit logs of logins and consent.

Trade-offs

What you gain

  • Stateless verification scales across many servers.
  • OAuth avoids handling other services' passwords.
  • Scopes give fine-grained, revocable access.

What it costs you

  • JWTs are hard to revoke before expiry.
  • Easy to get subtly wrong (algorithms, audiences, storage).
  • OAuth flows have many moving parts.

When not to use it

  • A simple server-rendered app with one server can use plain server-side sessions; JWTs aren't required for everything.

Common mistakes

  • Putting sensitive data in JWT payloads.
  • Not pinning the algorithm or skipping audience checks.
  • Long-lived access tokens with no refresh or revocation plan.
  • Storing tokens where page scripts can steal them without considering XSS.
  • Requesting broad OAuth scopes "just in case".

Interview questions

Try answering out loud first, then open the model answer.

How does JWT authentication work and what are its pitfalls?

Model answer

After login, the server issues a signed token with claims (subject, expiry, issuer, audience, scopes). Clients send it as a bearer token; any service verifies the signature and claims without a database lookup. Pitfalls: payloads aren't encrypted, tokens are hard to revoke (keep them short-lived and use refresh tokens), algorithm confusion if not pinned, missing audience or issuer checks, and insecure client-side storage.

Explain the OAuth 2.0 authorization code flow with PKCE.

Model answer

The client creates a random code verifier and its hash (challenge), redirects the user to the authorization server with the challenge and requested scopes; the user logs in and consents; the server redirects back with a short-lived code; the client exchanges the code plus the original verifier for access (and refresh) tokens. PKCE stops an intercepted code from being used by someone else.

Authentication vs authorization?

Model answer

Authentication establishes identity (who is making the request), e.g. by verifying a token. Authorization decides whether that identity may perform a specific action on a specific resource, using roles, scopes, ownership and policies.

Quiz

Pick an answer to see whether it’s right and why.

  1. 01Is the payload of a JWT secret?
  2. 02What must the server check when it receives a JWT?
  3. 03Why keep access tokens short-lived?
  4. 04What is OAuth 2.0 used for?
  5. 05Which OAuth flow should a browser or mobile app use?

0 of 5 answered

Key terms

Authentication
Proving who a user or service is, for example by verifying a password, a token or a signed key.
JWT (JSON Web Token)
A compact signed token carrying claims such as user ID, scopes and expiry, which servers can verify without a database lookup. Signed, but not encrypted.
OAuth
A standard for granting an app limited, revocable access to an account or API through tokens with scopes, without sharing passwords. OAuth 2.1 is used for remote MCP servers.
Access token
A short-lived credential sent with each request (usually as a Bearer token) that proves the caller is authenticated and what they may access.
Scope
A named permission attached to a token, such as chat:read or calendar.readonly, limiting what the token can be used for.

Before and after

Progress is saved in this browser only.