🔐 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"Source:https://dev.to/deepika_pusala/authentication-basics-jwt-fastapi-security-a-beginner-friendly-guide-2nlcRelated posts
