π Authentication Basics (JWT) & FastAPI Security
Authentication is not just a login page. π ββοΈ
It is about understanding how a backend identifies a user, verifies their identity, issues a token, and protects private API endpoints.
In this guide we'll learn everything from absolute zero:
- β How JWT works internally
- β
What
OAuth2PasswordBeareractually does - β Why password hashing is necessary
- β
How
Depends()makes authentication reusable - β How all these pieces connect in a real React + FastAPI + PostgreSQL project (my Social Feed app)
Simple examples, real-world analogies, Python code and execution flows. No assumed knowledge. Let's go! π
π Table of Contents
- Authentication vs Authorization
- Why do we need authentication?
- Token-based authentication
- What is JWT?
- JWT issuing and verification
- PyJWT and python-jose
- What is OAuth2?
- What is
OAuth2PasswordBearer? - What is
Depends()? - Password hashing (and why plaintext is a crime)
- Passlib, bcrypt and Argon2id
- Building the complete FastAPI auth system
- Full execution flow
- Expiry, access tokens, refresh tokens and logout
- JWT vs session-based authentication
- HTTP status codes
- Common security mistakes
- Architecture of a real FastAPI project
- Interview questions
- Quick quiz and final revision
1οΈβ£ Authentication vs Authorization
These two words sound similar, but they mean different things.
Imagine Instagram πΈ. You open it and enter:
- Email:
deepika@gmail.com - Password:
mypassword123
Instagram checks whether these credentials belong to a registered user. That's Authentication.
Now you're logged in and you try to edit someone else's profile. Instagram checks whether you have permission to do that. That's Authorization.
| Concept | Meaning | Example |
|---|---|---|
| πͺͺ Authentication | Who are you? | Verify your login |
| π¦ Authorization | What can you do? | Can you delete this post? |
π§ Remember: Authentication = identity verification. Authorization = permission checking. Authentication usually happens first, authorization follows.
2οΈβ£ Why do we need authentication?
Suppose you build a social media app using:
- βοΈ React frontend
- β‘ FastAPI backend
- π PostgreSQL database
Your API has these endpoints:
GET /feed
POST /posts
DELETE /posts/15
GET /users/10
What if anyone can call DELETE /posts/15? π± Anyone could delete anyone's post!
The backend needs a way to answer:
- Who is making this request?
- Are they logged in?
- Is their identity valid?
- Are they allowed to do this operation?
That's exactly what authentication (and authorization) solves.
3οΈβ£ What is token-based authentication?
A token is a piece of data that represents a user's authenticated session or identity.
Think of a movie theatre π¬:
- You buy a ticket.
- The ticket is given to you.
- You show it at the entrance.
- Staff verifies it.
- You're allowed inside.
You don't buy a new ticket for every section of the theatre. Same idea here:
- User enters email and password.
- Backend verifies the credentials.
- Backend generates a token.
- Frontend stores the token.
- Frontend sends the token with every later request.
- Backend verifies the token before allowing access.
After a successful login the backend returns something like:
{
"access_token": "eyJhbGciOiJIUzI1NiJ9.example.signature",
"token_type": "bearer"
}
And the frontend sends it back on future requests:
GET /feed
Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.example.signature
3.1 π·οΈ What is the Authorization header?
HTTP requests can carry headers, extra information about the request.
GET /profile
Authorization: Bearer abc123
Content-Type: application/json
-
Authorizationβ the header name -
Bearerβ tells the server "I'm giving you a bearer token" -
abc123β the token itself
A bearer token basically means: whoever holds this valid token can use it. ποΈ
β οΈ That's why tokens must be protected. If someone steals your token, they may be able to act as you.
3.2 π The complete authentication flow
-
User logs in: React sends email and password to
POST /login. - Backend verifies credentials: FastAPI fetches the user from PostgreSQL and checks the password hash.
- Backend creates a JWT: it creates and signs a token containing claims.
-
Frontend receives the token: the response contains
access_tokenandtoken_type. -
Frontend makes another request: React sends
Authorization: Bearer <token>. - FastAPI verifies the token: it checks the signature, expiry and claims.
- Protected endpoint runs: if valid, the backend identifies the user and continues.
Now let's understand JWT itself. π
4οΈβ£ What is JWT (JSON Web Token)?
JWT stands for JSON Web Token. It's a compact format for transferring information between parties as a JSON-based, signed token.
A JWT looks like this:
xxxxx.yyyyy.zzzzz
Three parts separated by dots:
HEADER . PAYLOAD . SIGNATURE
| Part | Contains |
|---|---|
| π§Ύ Header | Algorithm and token type |
| π¦ Payload | User identity and token claims |
| βοΈ Signature | Cryptographic proof of integrity |
4.1 π§Ύ Header
{
"alg": "HS256",
"typ": "JWT"
}
-
algβ algorithm used to sign the token -
HS256β HMAC using SHA-256 -
typβ token type (JWT)
4.2 π¦ Payload (claims)
Claims are statements about the user or the token.
{
"sub": "101",
"role": "user",
"iat": 1794398200,
"exp": 1794400000
}
| Claim | Meaning |
|---|---|
sub |
Subject, usually the user identifier |
exp |
Expiration time |
iat |
Issued-at time |
iss |
Issuer of the token |
aud |
Intended audience |
jti |
Unique token identifier |
These are standard registered claims. You can also add custom claims like role.
π¨ Very important: a JWT payload is NOT encrypted by default. Anyone holding the token can decode the header and payload!
So NEVER put these inside a JWT:
- β Passwords
- β Password hashes
- β Secret API keys
- β Sensitive personal information
A signed JWT gives you integrity, not confidentiality.
4.3 βοΈ Signature (the security-critical part)
The backend signs the token using a secret key. Conceptually:
Signature = HMAC-SHA256(
encoded_header + "." + encoded_payload,
SECRET_KEY
)
Imagine an attacker changes the payload from "sub": "user_101" to "sub": "admin_999". Without the secret key they cannot produce a matching signature, so verification fails. π‘οΈ
Encoding vs signing vs encryption:
- π Encoding β makes data representable in a transport format
- βοΈ Signing β verifies integrity and authenticity
- π Encryption β hides data from unauthorized readers
JWTs using HS256 are signed, not encrypted.
5οΈβ£ JWT issuing and verification
π Issuing
Creating and signing a JWT, usually right after a successful login.
token = create_access_token(user_id=101)
The function receives the identity, builds the payload, adds expiry, signs it and returns the JWT string.
π Verification
Happens whenever a client sends the token to a protected API:
- Extract the token
- Verify the signature
- Check the expiration
- Read the user ID from the payload
- Identify the corresponding user
- Allow or reject the request
A token can be rejected because:
- β Invalid signature
- β Expired
- β Malformed structure
- β Required claims missing
- β The user or session is no longer valid
π‘ A valid signature does not automatically mean the account is still active. You may also need to check the user or session in your database.
6οΈβ£ PyJWT and python-jose
You never implement signing algorithms by hand. You use a library. π¦
| Library | Purpose |
|---|---|
PyJWT |
Create, encode, decode and verify JWTs |
python-jose |
Create, encode, decode and verify JWTs |
Both work with FastAPI. Older tutorials (and older FastAPI docs) use python-jose, while the more actively maintained PyJWT is what the current FastAPI documentation uses. I'll use PyJWT in this article:
pip install pyjwt
If you'd rather use python-jose:
pip install "python-jose[cryptography]"
(The function names encode / decode are almost identical, so everything below translates easily.)
6.1 π Creating a JWT
import jwt
from datetime import datetime, timedelta, timezone
SECRET_KEY = "my-super-secret-key" # β οΈ demo only!
ALGORITHM = "HS256"
def create_access_token(user_id: int) -> str:
expire = datetime.now(timezone.utc) + timedelta(minutes=30)
payload = {
"sub": str(user_id),
"exp": expire,
}
return jwt.encode(payload, SECRET_KEY, algorithm=ALGORITHM)
Line-by-line:
-
SECRET_KEY: the secret used to sign the JWT. In real apps, never hardcode it. Load it from an environment variable. Make it strong, random and private. π -
ALGORITHM = "HS256": HMAC SHA-256. The same secret signs and verifies. -
expire: the token dies 30 minutes from now. If a token is stolen but expires soon, the damage window is small. β³ -
"sub": str(user_id): by conventionsubis a string, so we convert the ID. -
jwt.encode(...): encodes the header and payload, creates the signature, joins the three parts and returns the JWT string. β¨
That's JWT issuing!
6.2 π Verifying a JWT
import jwt
from jwt import InvalidTokenError
def verify_access_token(token: str):
try:
payload = jwt.decode(
token,
SECRET_KEY,
algorithms=[ALGORITHM], # π explicit allow-list!
)
except InvalidTokenError:
return None
user_id = payload.get("sub")
if user_id is None:
return None
return user_id
What jwt.decode() does:
- Decodes the token
- Checks the signature
- Validates
exp(it raises an error if the token has expired) - Rejects invalid tokens
β οΈ Notice
algorithms=[ALGORITHM]. Always specify the algorithms you accept. Never blindly trust the algorithm declared by an incoming token.
7οΈβ£ What is OAuth2?
You might be thinking: "We learned JWT, why OAuth2? Aren't they the same?"
NOOO! π They're different things.
- OAuth2 β an authorization framework. It describes how authorization is granted and how tokens are obtained.
- JWT β a token format. It describes how a token can be represented.
OAuth2 doesn't require JWT. OAuth2 access tokens can also be opaque random strings.
7.1 π Real-world example
You see a "Continue with Google" button and click it. Google authenticates you and your app receives authorization information through an OAuth-based flow. You never give your Google password to the third-party app. π
One nuance:
- OAuth2 primarily handles authorization.
- OpenID Connect (OIDC), built on top of OAuth2, adds an identity layer for authentication. Google Sign-In generally uses OIDC.
7.2 β‘ OAuth2 in FastAPI
FastAPI ships security utilities for common OAuth2-compatible patterns. The most important one is OAuth2PasswordBearer. π
8οΈβ£ What is OAuth2PasswordBearer?
OAuth2PasswordBearer is a FastAPI security dependency. Its job:
- π Tell FastAPI where the token comes from
- π€ Extract the bearer token from the
Authorizationheader - π₯ Hand that token to your dependency or endpoint
- π Describe the OAuth2 security scheme in the OpenAPI docs (that green Authorize button in Swagger UI!)
from fastapi.security import OAuth2PasswordBearer
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="login")
What does tokenUrl="login" mean?
It tells FastAPI (and the docs UI) that the endpoint used to obtain a token is POST /login.
What does it actually do?
Given this request:
GET /profile
Authorization: Bearer my-secret-token
it extracts my-secret-token (it strips the Bearer prefix).
It does NOT:
- β Generate the JWT
- β Verify the JWT signature
- β Check the user's password
- β Fetch the user from PostgreSQL
- β Log the user in
π§
OAuth2PasswordBeareronly extracts the token. You still have to verify it yourself.
8.1 What if the token is missing?
If a request has no Authorization header, OAuth2PasswordBearer automatically responds with HTTP 401 Unauthorized. π«
9οΈβ£ What is Depends()?
Before building the full system we need dependency injection, another core FastAPI concept.
Imagine ten protected endpoints:
GET /profile
GET /feed
POST /posts
DELETE /posts/10
POST /comments
GET /notifications
...
Every one needs authentication. Would you copy-paste the same token-checking code ten times? π΅ No way. FastAPI gives us:
Depends()
It means: "FastAPI, before running this endpoint, run this dependency and give me its result."
from fastapi import Depends, FastAPI
app = FastAPI()
def verify_user():
return "Deepika"
@app.get("/profile")
def get_profile(user=Depends(verify_user)):
return {"user": user}
What happens:
- A request hits
/profile - FastAPI sees
Depends(verify_user) - FastAPI runs
verify_user() - It returns
"Deepika" - FastAPI passes that value into the
userparameter - The endpoint runs
Response:
{
"user": "Deepika"
}
That's Dependency Injection (DI). π
π Connect the dots
Depends()β the dependency injection mechanismOAuth2PasswordBearerβ a dependency that extracts a bearer token- Your custom dependency (
get_current_user) β verifies the token and identifies the user
Combining them
from fastapi import Depends
from fastapi.security import OAuth2PasswordBearer
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="login")
@app.get("/profile")
def get_profile(token: str = Depends(oauth2_scheme)):
return {"received_token": token}
Incoming request
Authorization: Bearer abc123
β
βΌ
Depends(oauth2_scheme)
β extracts "abc123"
βΌ
Endpoint receives token = "abc123"
But we haven't verified that abc123 is a valid JWT yet. That's coming up! π
π Password hashing: why we can't store passwords directly
This is a HUGE real-world security concept. π¨
A user registers with:
Email: deepika@gmail.com
Password: deepika123
A beginner might store this directly:
| id | password | |
|---|---|---|
| 1 | deepika@gmail.com | deepika123 |
If your database is ever compromised, the attacker reads everyone's passwords. π Applications must NEVER store plaintext passwords. They store password hashes.
10.1 What is hashing?
Hashing is a one-way transformation of input into a fixed-format output.
Input: deepika123
Hash: $argon2id$v=19$m=65536,t=3,p=4$...
(The exact output depends on the algorithm and a random salt.)
You cannot practically reverse a properly designed password hash back to the original password.
| Hashing | Encryption |
|---|---|
| One-way | Reversible with the key |
| No decryption operation | Can be decrypted |
| Used for password verification | Used for confidentiality |
| Example: Argon2id | Example: AES |
Password hashing is deliberately designed to make guessing expensive. πΈ
10.2 What is bcrypt?
bcrypt is a password-hashing algorithm built specifically for password storage. Its key property is a configurable computational cost: the higher the cost, the more resources each hash needs, making bulk guessing slower.
10.3 π§ What is a salt?
A salt is a random value added during password hashing.
Two users register with the same password:
User A: deepika123
User B: deepika123
Without salts, a deterministic scheme gives both the same hash. With unique random salts, their stored hashes differ.
Why it matters:
- β Identical passwords get different hashes
- β Precomputed rainbow-table attacks become much less useful
- β Bulk password guessing gets harder
Good libraries generate and manage salts automatically. Don't roll your own.
10.4 β How does verification work?
You do not "decrypt" the stored hash. Instead:
- The library reads the algorithm and salt from the stored hash
- Hashes the submitted password with those parameters
- Compares the results securely
- Returns
TrueorFalse
REGISTRATION LOGIN
plain password entered password
β β
βΌ βΌ
hashing verify against stored hash
β β
βΌ βΌ
store the hash True / False
1οΈβ£1οΈβ£ What is Passlib?
Passlib is a Python library for password hashing and verification. It supports bcrypt, Argon2, PBKDF2 and more, so you never implement hashing by hand.
pip install "passlib[argon2]"
π‘ I'm using Argon2id here, a modern, memory-hard algorithm (it needs both CPU and memory to compute, making guessing even more expensive). If you prefer bcrypt, use
pip install "passlib[bcrypt]"andschemes=["bcrypt"]. Just make sure yourpasslibandbcryptversions are compatible, since newerbcryptreleases have caused warnings or errors with olderpasslib.
from passlib.context import CryptContext
pwd_context = CryptContext(
schemes=["argon2"],
deprecated="auto",
)
-
CryptContextβ Passlib's hashing configuration object -
schemes=["argon2"]β our chosen algorithm -
deprecated="auto"β lets Passlib flag older schemes that should be upgraded when users log in
Hash a password
def hash_password(password: str) -> str:
return pwd_context.hash(password)
hashed = hash_password("deepika123")
print(hashed) # a long Argon2id hash, NOT the original password
Verify a password
def verify_password(plain_password: str, hashed_password: str) -> bool:
return pwd_context.verify(plain_password, hashed_password)
print(verify_password("deepika123", hashed)) # True β
print(verify_password("wrongpassword", hashed)) # False β
π§ Passlib is the library; Argon2id / bcrypt is the algorithm. Don't mix them up!
The important lesson isn't "always use bcrypt". It's use a modern, slow, salted password-hashing algorithm, never plaintext and never ordinary SHA-256.
1οΈβ£2οΈβ£ Building a complete FastAPI authentication system ποΈ
Time to combine everything. We'll build:
- β User registration
- β Password hashing
- β User login
- β Password verification
- β JWT generation
- β Token extraction
- β Token verification
- β Protected endpoints
- β A reusable authentication dependency
We'll use an in-memory dictionary so it's beginner-friendly. In a real app, PostgreSQL and a repository layer replace it.
12.1 π Folder structure
backend/
β
βββ app/
β βββ main.py
β β
β βββ api/
β β βββ auth.py
β β
β βββ core/
β β βββ security.py
β β
β βββ dependencies/
β β βββ auth.py
β β
β βββ models/
β β βββ user.py
β β
β βββ schemas/
β βββ auth.py
β
βββ requirements.txt
| File | Responsibility |
|---|---|
core/security.py |
Password hashing, JWT creation and verification |
api/auth.py |
Login and registration endpoints |
dependencies/auth.py |
Extract token, verify token, identify current user |
models/user.py |
User database model |
schemas/auth.py |
Request and response validation |
main.py |
Creates the FastAPI application |
This is separation of concerns: each file has one job. π―
Install what we need:
pip install fastapi uvicorn pyjwt "passlib[argon2]" python-multipart
(python-multipart is required because the login endpoint receives form data.)
12.2 π Step 1: core/security.py
import os
from datetime import datetime, timedelta, timezone
import jwt
from jwt import InvalidTokenError
from passlib.context import CryptContext
# π Load from environment, NEVER hardcode in real projects
SECRET_KEY = os.environ.get("JWT_SECRET_KEY", "dev-only-change-me")
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30
pwd_context = CryptContext(schemes=["argon2"], deprecated="auto")
# π Password helpers
def hash_password(password: str) -> str:
return pwd_context.hash(password)
def verify_password(plain_password: str, hashed_password: str) -> bool:
return pwd_context.verify(plain_password, hashed_password)
# ποΈ JWT helpers
def create_access_token(user_id: int) -> str:
now = datetime.now(timezone.utc)
payload = {
"sub": str(user_id),
"iat": now,
"exp": now + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES),
}
return jwt.encode(payload, SECRET_KEY, algorithm=ALGORITHM)
def decode_access_token(token: str) -> str | None:
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
except InvalidTokenError:
return None
return payload.get("sub")
Four reusable operations in one place:
hash_password()verify_password()create_access_token()decode_access_token()
12.3 πͺ Step 2: Registration and login routes (api/auth.py)
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordRequestForm
from pydantic import BaseModel
from app.core.security import (
create_access_token,
hash_password,
verify_password,
)
router = APIRouter()
# π§ͺ In-memory "database" (use PostgreSQL in real life!)
users: dict[str, dict] = {}
class RegisterRequest(BaseModel):
email: str
password: str
@router.post("/register", status_code=status.HTTP_201_CREATED)
def register(data: RegisterRequest):
if data.email in users:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="User already exists",
)
users[data.email] = {
"id": len(users) + 1,
"email": data.email,
"hashed_password": hash_password(data.password),
}
return {"message": "User registered successfully"}
@router.post("/login")
def login(form_data: OAuth2PasswordRequestForm = Depends()):
user = users.get(form_data.username) # username field = our email
# Same error for "no such user" and "wrong password" π
if not user or not verify_password(form_data.password, user["hashed_password"]):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect email or password",
headers={"WWW-Authenticate": "Bearer"},
)
token = create_access_token(user["id"])
return {"access_token": token, "token_type": "bearer"}
π§ What is OAuth2PasswordRequestForm?
form_data: OAuth2PasswordRequestForm = Depends()
It's another FastAPI-provided dependency that extracts login credentials sent as form data:
form_data.username
form_data.password
The client submits:
POST /login
Content-Type: application/x-www-form-urlencoded
username=deepika@gmail.com&password=deepika123
Notice it says username even if your app identifies users by email. Just treat the username field as the email.
β οΈ Heads up: the OAuth2 Resource Owner Password Credentials grant is discouraged by modern OAuth security guidance for new systems. This form-based pattern is perfect for learning and for a first-party login API, but for production with external identity providers prefer Authorization Code with PKCE. A first-party login API can also simply accept a JSON request body.
12.4 π‘οΈ Step 3: Reusable authentication dependency (dependencies/auth.py)
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
from app.api.auth import users
from app.core.security import decode_access_token
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="login")
def get_current_user(token: str = Depends(oauth2_scheme)):
credentials_error = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid or expired token",
headers={"WWW-Authenticate": "Bearer"},
)
user_id = decode_access_token(token)
if user_id is None:
raise credentials_error
# β
Check the user still exists (real apps: query the DB / session)
user = next((u for u in users.values() if str(u["id"]) == user_id), None)
if user is None:
raise credentials_error
return user
Trace it:
-
oauth2_schemeβ expects a bearer token in theAuthorizationheader -
Depends(oauth2_scheme)β FastAPI extracts the token and injects it -
decode_access_token(token)β verifies the token and returns the user ID -
if user_id is Noneβ invalid or expired token means reject - We look the user up, because a valid signature doesn't prove the account is still active
- Return the authenticated user
12.5 π Step 4: Protect an endpoint (main.py)
from fastapi import Depends, FastAPI
from app.api.auth import router as auth_router
from app.dependencies.auth import get_current_user
app = FastAPI()
app.include_router(auth_router)
@app.get("/profile")
def get_profile(current_user: dict = Depends(get_current_user)):
return {
"message": "Welcome!",
"user_id": current_user["id"],
"email": current_user["email"],
}
That one line:
current_user: dict = Depends(get_current_user)
does a LOT of work. Before /profile runs, FastAPI:
- Calls
get_current_user() - Extracts the token via
oauth2_scheme - Verifies the JWT
- Loads the user
- Injects the user into your endpoint
β
Success:
{
"message": "Welcome!",
"user_id": 1,
"email": "deepika@gmail.com"
}
β Failure (the endpoint body never runs):
{
"detail": "Invalid or expired token"
}
12.6 π§ͺ Try it!
Run the app:
uvicorn app.main:app --reload
Then open http://127.0.0.1:8000/docs, register a user, click Authorize (π) and log in. Swagger UI now automatically sends your bearer token. Or test with curl:
# 1. Register
curl -X POST http://127.0.0.1:8000/register \
-H "Content-Type: application/json" \
-d '{"email": "deepika@gmail.com", "password": "deepika123"}'
# 2. Login (form data!)
curl -X POST http://127.0.0.1:8000/login \
-d "username=deepika@gmail.com&password=deepika123"
# 3. Call the protected route
curl http://127.0.0.1:8000/profile \
-H "Authorization: Bearer <paste_access_token_here>"
β οΈ This is intentionally simplified. The in-memory
usersdict isn't shared between processes and disappears on restart. A real implementation uses database-backed users and sessions.
12.7 π¦ Bonus: Authorization (role checks)
Authentication tells you who. Now let's check what they can do, by building on get_current_user:
from fastapi import Depends, HTTPException, status
def require_admin(current_user: dict = Depends(get_current_user)):
if current_user.get("role") != "admin":
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="You don't have permission to do this",
)
return current_user
@app.delete("/posts/{post_id}")
def delete_post(post_id: int, admin: dict = Depends(require_admin)):
return {"message": f"Post {post_id} deleted"}
Dependencies can depend on other dependencies. π§© That's what makes Depends() so powerful.
1οΈβ£3οΈβ£ Complete execution flow π§
Phase A: Registration π
User enters email + password
β
βΌ
POST /register
β
βΌ
hash_password() β plaintext becomes a secure hash
β
βΌ
PostgreSQL stores: email + password hash
Phase B: Login π
POST /login (username + password)
β
βΌ
Backend finds the user, verify_password()
β
βΌ
create_access_token() β JWT created and signed
β
βΌ
Frontend receives: access_token + "bearer"
Phase C: Accessing a protected endpoint πͺ
React requests /profile
Authorization: Bearer <JWT>
β
βΌ
OAuth2PasswordBearer β extracts the JWT
β
βΌ
get_current_user() β verifies token, identifies user
β
βΌ
Depends(get_current_user) β injects the user into the endpoint
β
βΌ
β
Endpoint returns the user's profile
If you understand this flow, you understand the foundation of JWT authentication in FastAPI. π
1οΈβ£4οΈβ£ Token expiry, refresh tokens and logout β°
Our JWT contains:
{
"sub": "101",
"exp": 1794400000
}
Suppose the token is created at 10:00 AM and expires at 10:30 AM:
- 10:15 AM request β β valid, the request proceeds
- 10:45 AM request β β expired, the backend rejects it, and the user needs a new access token
14.1 π« Access token
A credential presented to an API to access protected resources.
GET /feed
Authorization: Bearer <access_token>
Access tokens are short-lived, so a stolen one is only useful for a limited time (plus any extra revocation checks).
14.2 π Refresh token
A credential used to get a new access token without asking for the password again.
- Access token lifetime: 30 minutes
- Refresh token lifetime: 7 days
When the access token expires:
- Frontend sends the refresh token to a refresh endpoint
- Backend validates it
- Backend issues a new access token
- The user keeps using the app seamlessly β¨
Refresh tokens are especially sensitive. Store them carefully, rotate them when used where appropriate, and make them revocable. For browser apps, a common approach is HttpOnly, Secure, properly configured SameSite cookies.
14.3 πͺ What about logout?
Traditional server-side sessions can be killed by deleting the session on the server. But a self-contained JWT stays cryptographically valid until it expires, even after the user clicks "Logout". The JWT doesn't magically know the user logged out. π€
Possible solutions:
- Keep access tokens short-lived
- Track or revoke sessions server-side
- Use a token denylist when appropriate
- Revoke refresh tokens
- Rotate session credentials
This is why real-world systems often combine tokens with database-backed sessions.
1οΈβ£5οΈβ£ JWT vs Session-based authentication βοΈ
Both are widely used in production.
ποΈ Session-based
The backend creates a session in the database:
| Session ID | User ID | Expires |
|---|---|---|
| abc123 | 101 | 7 days |
On every request: the frontend sends the session ID β the backend looks it up β checks validity β identifies the user.
ποΈ JWT-based
The backend issues a signed JWT containing identity and expiry. On every request: the frontend sends the JWT β the backend verifies the signature β checks the claims β identifies the user.
| JWT | Server-side session |
|---|---|
| Identity carried in token claims | Identity retrieved via session lookup |
| Can be verified without a DB lookup | Normally requires a session lookup |
| Revocation needs extra mechanisms | Revoked by invalidating the session |
| Self-contained token | Server maintains session state |
| Common in distributed APIs | Common in traditional web apps |
βοΈ Neither is automatically better. The right choice depends on your architecture and security requirements.
πΌ In my Social Feed project
My implementation actually uses database-backed sessions (7-day lifetime) with Argon2id for passwords, and SHA-256 hashing of a random session token for lookups. That's different from the JWT example above:
| My session implementation | JWT implementation |
|---|---|
| Random session token | Structured JWT |
| Hash token for DB lookup | Cryptographically sign token |
| Database session lookup | Signature verification |
| Easy server-side revocation | Needs extra revocation strategy |
π§ Why is SHA-256 okay here? Because it's hashing a high-entropy random token, not a human-chosen password. Passwords need slow, salted algorithms like Argon2id or bcrypt. Random tokens don't, since they can't be guessed from a dictionary.
1οΈβ£6οΈβ£ HTTP status codes in authentication π
| Code | Meaning | Example |
|---|---|---|
200 |
OK | Successful login |
201 |
Created | Successful registration |
400 |
Bad Request | Invalid request |
401 |
Unauthorized | Missing or invalid authentication |
403 |
Forbidden | Authenticated but lacks permission |
404 |
Not Found | User doesn't exist |
422 |
Validation Error | Invalid request schema |
500 |
Internal Server Error | Unexpected backend failure |
The most important distinction: 401 vs 403
- πͺͺ You try to enter an office without an ID card β
401 - π« You show a valid ID card but try to enter the server room β
403
Authentication asks: is your identity valid? Authorization asks: do you have permission?
1οΈβ£7οΈβ£ Common security mistakes β οΈ
| Mistake | Why it's dangerous |
|---|---|
| Storing plaintext passwords | A DB leak exposes passwords |
| Using plain SHA-256 for passwords | Too fast, easy to brute-force |
| Hardcoding JWT secrets | The secret leaks through source code |
| Never-expiring access tokens | Stolen tokens stay useful forever |
| Putting passwords in the JWT payload | The payload can be decoded |
| Accepting any JWT algorithm | Serious verification vulnerabilities |
| Not checking token expiration | Expired credentials may be accepted |
| Detailed login errors | Reveals which accounts exist |
| Storing tokens carelessly | XSS or other attacks can steal them |
| Forgetting HTTPS | Credentials and tokens can be intercepted |
π A few more terms worth knowing
- HTTPS β encrypts communication between browser and server
- CORS β controls which browser origins may access your backend (it is not authentication!)
- XSS β malicious scripts executing in a user's browser
- CSRF β tricks a browser into making an unwanted authenticated request
- Brute-force attack β repeatedly guessing passwords
- Rate limiting β restricts excessive requests, like repeated login attempts
πͺ Where should the frontend store tokens?
localStorage is readable by JavaScript, so it's exposed if an XSS vulnerability exists. HttpOnly cookies can't be read by JavaScript, but cookie-based auth needs proper CSRF protection. There's no free lunch. Design it carefully. π§
1οΈβ£8οΈβ£ How this connects to real FastAPI architecture ποΈ
ββββββββββββββββββββββββββββββββββββββββββββββββ
β API / Router layer β
β Receives login and protected requests β
β e.g. auth.py, users.py β
ββββββββββββββββββββββββββββββββββββββββββββββββ€
β Dependency layer β
β Extracts token, identifies the user β
β e.g. get_current_user() β
ββββββββββββββββββββββββββββββββββββββββββββββββ€
β Security layer β
β Hash/verify passwords, issue/verify JWTs β
β e.g. security.py β
ββββββββββββββββββββββββββββββββββββββββββββββββ€
β Service layer β
β Authentication business logic β
β e.g. auth_service.py β
ββββββββββββββββββββββββββββββββββββββββββββββββ€
β Repository / Database layer β
β Fetch users, create sessions, run queries β
β e.g. user_repository.py, session_repository.py β
ββββββββββββββββββββββββββββββββββββββββββββββββ
π§Ό Principle: keep the router thin. Delegate business logic to services and database work to repositories.
1οΈβ£9οΈβ£ Interview questions & answers π€
Q1. What is authentication?
Verifying the identity of a user before allowing access to protected resources.
Q2. What is JWT?
JSON Web Token, a compact, signed token format used to transfer claims between parties.
Q3. What are the three parts of a JWT?
Header, payload and signature.
Q4. Is JWT encrypted?
Not by default. A typical signed JWT has an encoded header and payload plus a signature. The payload can be decoded by anyone.
Q5. What is OAuth2PasswordBearer?
A FastAPI security dependency that extracts a bearer token from the Authorization header and declares an OAuth2 security scheme in the API docs.
Q6. Does OAuth2PasswordBearer verify JWT signatures?
No. It only extracts the token. You need separate verification logic.
Q7. What is Depends()?
FastAPI's dependency injection mechanism, which lets reusable functions and resources be provided to endpoints.
Q8. What is password hashing?
A one-way transformation of a password into a secure stored form that can be verified later without storing the original.
Q9. What is bcrypt?
A password-hashing algorithm designed to make guessing computationally expensive.
Q10. What is a salt?
A random value included in password hashing so identical passwords produce different stored hashes.
Q11. Access token vs refresh token?
An access token accesses protected APIs. A refresh token obtains a new access token.
Q12. What happens when a JWT expires?
The backend rejects it. The client may use a valid refresh mechanism to get a new one.
Q13. JWT vs session authentication?
JWT carries signed claims the server can verify. Sessions store state on the server and look it up via a session ID.
Q14. Why not store passwords directly in PostgreSQL?
A database breach would expose real passwords. We store salted, computationally expensive hashes instead.
Q15. 401 vs 403?
401 means valid authentication is missing. 403 means you're authenticated but lack permission.
2οΈβ£0οΈβ£ Quick knowledge test π§ͺ
Answer without scrolling up! (Answers are below.)
1. What does OAuth2PasswordBearer actually do?
- A) Generates a JWT
- B) Extracts the bearer token from the
Authorizationheader - C) Hashes the password
- D) Connects to PostgreSQL
2. Which three parts make up a JWT?
- A) Username, password, signature
- B) Header, payload, signature
- C) Request, response, token
- D) Issuer, database, session
3. Why do we hash passwords?
- A) To decrypt them later
- B) To make login faster
- C) To avoid storing plaintext passwords
- D) To create JWTs
4. What does Depends() do in FastAPI?
- A) Automatically injects dependency results
- B) Creates database tables
- C) Encrypts tokens
- D) Starts Uvicorn
5. Is JWT payload encryption automatic?
- A) Yes, always
- B) No, ordinary signed JWTs are not encrypted
- C) Only with HS256
- D) Only with FastAPI
6. What happens when a JWT expires?
- A) It becomes valid forever
- B) The backend should reject it
- C) It refreshes itself automatically
- D) The database deletes the user
π Click to reveal answers
- B
- B
- C
- A
- B
- B
π§Ύ Final revision: everything in one place
| Concept | What to remember |
|---|---|
| Authentication | Verifying who the user is |
| Authorization | Checking what the user can access |
| Token | Credential representing an authenticated session or identity |
| JWT | Signed token format containing claims |
| Header | Algorithm and token type |
| Payload | User identity and claims |
| Signature | Protects token integrity |
| OAuth2 | Authorization framework |
OAuth2PasswordBearer |
Extracts the bearer token |
Depends() |
Dependency injection |
| Password hashing | Secure password storage |
| bcrypt | Password-hashing algorithm |
| Argon2id | Modern memory-hard password-hashing algorithm |
| Passlib | Python password-hashing library |
| Access token | Used to access protected resources |
| Refresh token | Used to obtain new access tokens |
get_current_user() |
Reusable dependency to identify the authenticated user |
π― Final thoughts
Don't just memorize definitions. Be able to explain the complete journey:
A user clicks Login in React β FastAPI verifies the password hash β a token is issued β React sends it on every request β
OAuth2PasswordBearerextracts it βget_current_user()verifies it βDepends()injects the user β the protected/feedendpoint runs. π
If you can narrate that flow confidently, you truly understand authentication.
And remember: JWT-based and database-session-based authentication are different designs. Both are valid. Knowing which one your code uses, and why, is what separates memorizing from understanding. π‘
π Enjoyed this post?
- β€οΈ Drop a reaction
- π¦ Save it for revision
- π¬ Comment with questions, and I'll answer them!
Follow me so you don't miss it! π
Happy coding! π©βπ»β¨
Top comments (0)