Authentication Basics (JWT) & FastAPI Security: A Beginner-Friendly Guide π
π 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 OAuth2PasswordBearer actually 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:
d******@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 /feedPOST /postsDELETE /posts/15GET /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 convention sub is 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 Authorization header
- π₯ 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
π§ OAuth2PasswordBearer only 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 /profileGET /feedPOST /postsDELETE /posts/10POST /commentsGET /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 user parameter
- The endpoint runs
- Response:
{ "user" : "Deepika" }
That's Dependency Injection (DI). π
π Connect the dots Depends() β the dependency injection mechanism OAuth2PasswordBearer β 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:
d******@gmail.com - Password:
deepika123
A beginner might store this directly:
id email password 1 d******@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?
bcryp
Comments
No comments yet. Start the discussion.