DEV Community

Deepika Pusala
Deepika Pusala

Posted on

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

  1. Authentication vs Authorization
  2. Why do we need authentication?
  3. Token-based authentication
  4. What is JWT?
  5. JWT issuing and verification
  6. PyJWT and python-jose
  7. What is OAuth2?
  8. What is OAuth2PasswordBearer?
  9. What is Depends()?
  10. Password hashing (and why plaintext is a crime)
  11. Passlib, bcrypt and Argon2id
  12. Building the complete FastAPI auth system
  13. Full execution flow
  14. Expiry, access tokens, refresh tokens and logout
  15. JWT vs session-based authentication
  16. HTTP status codes
  17. Common security mistakes
  18. Architecture of a real FastAPI project
  19. Interview questions
  20. 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
Enter fullscreen mode Exit fullscreen mode

What if anyone can call DELETE /posts/15? 😱 Anyone could delete anyone's post!

The backend needs a way to answer:

  1. Who is making this request?
  2. Are they logged in?
  3. Is their identity valid?
  4. 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 🎬:

  1. You buy a ticket.
  2. The ticket is given to you.
  3. You show it at the entrance.
  4. Staff verifies it.
  5. You're allowed inside.

You don't buy a new ticket for every section of the theatre. Same idea here:

  1. User enters email and password.
  2. Backend verifies the credentials.
  3. Backend generates a token.
  4. Frontend stores the token.
  5. Frontend sends the token with every later request.
  6. Backend verifies the token before allowing access.

After a successful login the backend returns something like:

{
  "access_token": "eyJhbGciOiJIUzI1NiJ9.example.signature",
  "token_type": "bearer"
}
Enter fullscreen mode Exit fullscreen mode

And the frontend sends it back on future requests:

GET /feed
Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.example.signature
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
  • 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

  1. User logs in: React sends email and password to POST /login.
  2. Backend verifies credentials: FastAPI fetches the user from PostgreSQL and checks the password hash.
  3. Backend creates a JWT: it creates and signs a token containing claims.
  4. Frontend receives the token: the response contains access_token and token_type.
  5. Frontend makes another request: React sends Authorization: Bearer <token>.
  6. FastAPI verifies the token: it checks the signature, expiry and claims.
  7. 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
Enter fullscreen mode Exit fullscreen mode

Three parts separated by dots:

HEADER . PAYLOAD . SIGNATURE
Enter fullscreen mode Exit fullscreen mode
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"
}
Enter fullscreen mode Exit fullscreen mode
  • 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
}
Enter fullscreen mode Exit fullscreen mode
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
)
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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:

  1. Extract the token
  2. Verify the signature
  3. Check the expiration
  4. Read the user ID from the payload
  5. Identify the corresponding user
  6. 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
Enter fullscreen mode Exit fullscreen mode

If you'd rather use python-jose:

pip install "python-jose[cryptography]"
Enter fullscreen mode Exit fullscreen mode

(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)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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:

  1. πŸ“ Tell FastAPI where the token comes from
  2. πŸ“€ Extract the bearer token from the Authorization header
  3. πŸ“₯ Hand that token to your dependency or endpoint
  4. πŸ“– 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")
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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    /profile
GET    /feed
POST   /posts
DELETE /posts/10
POST   /comments
GET    /notifications
...
Enter fullscreen mode Exit fullscreen mode

Every one needs authentication. Would you copy-paste the same token-checking code ten times? 😡 No way. FastAPI gives us:

Depends()
Enter fullscreen mode Exit fullscreen mode

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}
Enter fullscreen mode Exit fullscreen mode

What happens:

  1. A request hits /profile
  2. FastAPI sees Depends(verify_user)
  3. FastAPI runs verify_user()
  4. It returns "Deepika"
  5. FastAPI passes that value into the user parameter
  6. The endpoint runs

Response:

{
  "user": "Deepika"
}
Enter fullscreen mode Exit fullscreen mode

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}
Enter fullscreen mode Exit fullscreen mode
Incoming request
Authorization: Bearer abc123
        β”‚
        β–Ό
Depends(oauth2_scheme)
        β”‚  extracts "abc123"
        β–Ό
Endpoint receives token = "abc123"
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

A beginner might store this directly:

id email 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$...
Enter fullscreen mode Exit fullscreen mode

(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
Enter fullscreen mode Exit fullscreen mode

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:

  1. The library reads the algorithm and salt from the stored hash
  2. Hashes the submitted password with those parameters
  3. Compares the results securely
  4. Returns True or False
REGISTRATION                      LOGIN
plain password                    entered password
     β”‚                                 β”‚
     β–Ό                                 β–Ό
  hashing                       verify against stored hash
     β”‚                                 β”‚
     β–Ό                                 β–Ό
 store the hash                    True / False
Enter fullscreen mode Exit fullscreen mode

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]"
Enter fullscreen mode Exit fullscreen mode

πŸ’‘ 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]" and schemes=["bcrypt"]. Just make sure your passlib and bcrypt versions are compatible, since newer bcrypt releases have caused warnings or errors with older passlib.

from passlib.context import CryptContext

pwd_context = CryptContext(
    schemes=["argon2"],
    deprecated="auto",
)
Enter fullscreen mode Exit fullscreen mode
  • 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
Enter fullscreen mode Exit fullscreen mode

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 ❌
Enter fullscreen mode Exit fullscreen mode

🧠 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:

  1. βœ… User registration
  2. βœ… Password hashing
  3. βœ… User login
  4. βœ… Password verification
  5. βœ… JWT generation
  6. βœ… Token extraction
  7. βœ… Token verification
  8. βœ… Protected endpoints
  9. βœ… 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
Enter fullscreen mode Exit fullscreen mode
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
Enter fullscreen mode Exit fullscreen mode

(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")
Enter fullscreen mode Exit fullscreen mode

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"}
Enter fullscreen mode Exit fullscreen mode

🧐 What is OAuth2PasswordRequestForm?

form_data: OAuth2PasswordRequestForm = Depends()
Enter fullscreen mode Exit fullscreen mode

It's another FastAPI-provided dependency that extracts login credentials sent as form data:

form_data.username
form_data.password
Enter fullscreen mode Exit fullscreen mode

The client submits:

POST /login
Content-Type: application/x-www-form-urlencoded

username=deepika@gmail.com&password=deepika123
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Trace it:

  1. oauth2_scheme β†’ expects a bearer token in the Authorization header
  2. Depends(oauth2_scheme) β†’ FastAPI extracts the token and injects it
  3. decode_access_token(token) β†’ verifies the token and returns the user ID
  4. if user_id is None β†’ invalid or expired token means reject
  5. We look the user up, because a valid signature doesn't prove the account is still active
  6. 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"],
    }
Enter fullscreen mode Exit fullscreen mode

That one line:

current_user: dict = Depends(get_current_user)
Enter fullscreen mode Exit fullscreen mode

does a LOT of work. Before /profile runs, FastAPI:

  1. Calls get_current_user()
  2. Extracts the token via oauth2_scheme
  3. Verifies the JWT
  4. Loads the user
  5. Injects the user into your endpoint

βœ… Success:

{
  "message": "Welcome!",
  "user_id": 1,
  "email": "deepika@gmail.com"
}
Enter fullscreen mode Exit fullscreen mode

❌ Failure (the endpoint body never runs):

{
  "detail": "Invalid or expired token"
}
Enter fullscreen mode Exit fullscreen mode

12.6 πŸ§ͺ Try it!

Run the app:

uvicorn app.main:app --reload
Enter fullscreen mode Exit fullscreen mode

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>"
Enter fullscreen mode Exit fullscreen mode

⚠️ This is intentionally simplified. The in-memory users dict 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"}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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:

  1. Frontend sends the refresh token to a refresh endpoint
  2. Backend validates it
  3. Backend issues a new access token
  4. 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 β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
Enter fullscreen mode Exit fullscreen mode

🧼 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 Authorization header
  • 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

  1. B
  2. B
  3. C
  4. A
  5. B
  6. 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 β†’ OAuth2PasswordBearer extracts it β†’ get_current_user() verifies it β†’ Depends() injects the user β†’ the protected /feed endpoint 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! πŸ‘©β€πŸ’»βœ¨

fastapi #python #jwt #security #webdev

Top comments (0)