This lesson on Authentication — JWT & OAuth2 is hands-on and example-driven. You will implement end-to-end user registration and token-based authentication in FastAPI using Bcrypt and JWTs. You will configure OAuth2 password flows with standard OpenAPI compatibility and issue digitally signed bearer tokens for verified users.
What You'll Be Able To Do
- Install and configure security dependencies including python-jose, passlib, and python-multipart
- Hash raw user passwords with Bcrypt before saving records to the database
- Define an OAuth2PasswordBearer dependency aligned with the token generation endpoint
- Implement an authenticate_user helper using passlib verification to validate credentials
- Construct and sign JWT access tokens containing payload claims and expiration timestamps
Detailed Concept Walkthrough
1. Security Dependencies and Project Configuration
FastAPI leverages specialized cryptographic and parsing libraries to implement the standard OAuth2 password bearer specification. These packages handle form decoding, password hashing, and token signature generation without rolling custom security logic.
- Package Roles:
passlib[bcrypt]manages one-way salt-and-hash operations,python-josehandles JWT serialization and cryptographic signing, andpython-multipartenables parsingapplication/x-www-form-urlencodedpayloads required by OAuth2. - Under the Hood: FastAPI's
OAuth2PasswordBearerclass acts as a dependency that extracts the bearer token from theAuthorization: Bearer <token>HTTP header during protected request handling. - Configuration Nuance: The
tokenUrlpassed toOAuth2PasswordBearermust match the relative or absolute path of your login endpoint (e.g.,auth/token), enabling the Swagger UI 'Authorize' modal to function properly.
# Dependencies installation and core security configuration
# pip install python-jose[cryptography] passlib[bcrypt] python-multipart
from fastapi import APIRouter
from fastapi.security import OAuth2PasswordBearer
from passlib.context import CryptContext
router = APIRouter(prefix="/auth", tags=["auth"])
SECRET_KEY = "your-super-secret-key-string"
ALGORITHM = "HS256"
bcrypt_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
oauth2_bearer = OAuth2PasswordBearer(tokenUrl="auth/token")
Key Takeaway: Always match the
tokenUrlparameter inOAuth2PasswordBearerto your router's actual token generation endpoint path.
2. Secure User Creation and Password Hashing
User passwords must never be stored as plain text in the database. Instead, passwords are transformed into irreversible cryptographic hashes using Bcrypt before the record is committed.
- Mechanism: When a registration request arrives, the plaintext password from the Pydantic schema is passed to
bcrypt_context.hash(), generating a salted hash string. - Under the Hood: Bcrypt automatically generates a unique salt for each password and incorporates a computational cost factor, protecting stored credentials against rainbow table and brute-force attacks.
- Best Practice: Separate the input schema (
UserVerificationorCreateUserRequest) from the internal ORM model so that raw password fields are never exposed in responses or logs.
from fastapi import status
from pydantic import BaseModel
class CreateUserRequest(BaseModel):
username: str
password: str
@router.post("/", status_code=status.HTTP_201_CREATED)
def create_user(create_user_request: CreateUserRequest, db: Session = Depends(get_db)):
create_user_model = Users(
username=create_user_request.username,
hashed_password=bcrypt_context.hash(create_user_request.password)
)
db.add(create_user_model)
db.commit()
Key Takeaway: Hash raw passwords with
bcrypt_context.hash()immediately before instantiating and committing the ORM user model.
3. User Verification and Authentication Endpoint
The login endpoint receives OAuth2 compliant form data, retrieves the corresponding user record, and verifies the plain text password against the stored Bcrypt hash.
- Execution Flow: The endpoint accepts
OAuth2PasswordRequestFormvia FastAPI dependency injection, which parsesusernameandpasswordfrom form data rather than standard JSON bodies. - Under the Hood: The helper function
bcrypt_context.verify(plain_password, hashed_password)computes the hash of the candidate password using the embedded salt and performs a constant-time comparison to prevent timing attacks. - Failure Handling: If the user does not exist or the password verification fails, return
Falseor raise anHTTPExceptionwith status code 401 Unauthorized.
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordRequestForm
from sqlalchemy.orm import Session
def authenticate_user(username: str, password: str, db: Session):
user = db.query(Users).filter(Users.username == username).first()
if not user:
return False
if not bcrypt_context.verify(password, user.hashed_password):
return False
return user
@router.post("/token")
def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends(), db: Session = Depends(get_db)):
user = authenticate_user(form_data.username, form_data.password, db)
if not user:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid credentials")
# Generate token next...
Key Takeaway: Use
bcrypt_context.verify()for constant-time hash comparison instead of querying database records directly by raw password.
4. JWT Creation and Expiration Management
A JSON Web Token (JWT) is a cryptographically signed credential containing claims (subject identifier and expiration time) that allows stateless identity verification by the server.
- Mechanism: The
create_access_tokenutility builds a claims payload dictionary, injects anexpclaim viadatetime.utcnow() + timedelta(), and encodes it usingjose.jwt.encode(). - Under the Hood: The resulting token contains three Base64URL-encoded segments separated by dots (header, payload, signature), where the signature is computed using
SECRET_KEYandHS256. - Best Practice: Keep access token lifespans short (e.g., 15–60 minutes) to minimize exposure windows if a client-side token is compromised.
from datetime import datetime, timedelta
from jose import jwt
def create_access_token(username: str, user_id: int, expires_delta: timedelta):
encode = {"sub": username, "id": user_id}
expires = datetime.utcnow() + expires_delta
encode.update({"exp": expires})
return jwt.encode(encode, SECRET_KEY, algorithm=ALGORITHM)
# Usage in token endpoint response:
# token = create_access_token(user.username, user.id, timedelta(minutes=20))
# return {"access_token": token, "token_type": "bearer"}
Key Takeaway: Always include an
expexpiration claim in your JWT payload to prevent stolen tokens from being valid indefinitely.
Topics Covered in Authentication — JWT & OAuth2
- Dependencies and Setup (0:00 - 1:31) — Review existing project architecture and install security packages.
- Authentication Configuration (1:31 - 4:28) — Configure CryptContext, OAuth2PasswordBearer, and cryptographic signing constants.
- User Registration (4:28 - 6:33) — Build the registration endpoint to hash passwords and persist new users.
- Login and Verification (6:33 - 8:14) — Implement the token login route and password verification logic.
- JWT Generation (8:14 - 10:00) — Construct access tokens with expiration claims and encode them with python-jose.
FastAPI Cheat Sheet
-
CryptContext(schemes=["bcrypt"], deprecated="auto")— Initializes Bcrypt password hashing contextbcrypt_context = CryptContext(schemes=["bcrypt"], deprecated="auto") -
bcrypt_context.hash(secret)— Generates salted one-way password hash stringhashed_pw = bcrypt_context.hash("mypassword123") -
bcrypt_context.verify(secret, hash)— Verifies plaintext string against stored hashis_valid = bcrypt_context.verify("mypassword123", stored_hash) -
OAuth2PasswordBearer(tokenUrl=...)— Declares token scheme dependency for swaggeroauth2_bearer = OAuth2PasswordBearer(tokenUrl="auth/token") -
OAuth2PasswordRequestForm— Extracts form-encoded login credentials from requestform: OAuth2PasswordRequestForm = Depends() -
jwt.encode(claims, key, algorithm=...)— Generates signed JWT access token stringtoken = jwt.encode({"sub": "alice"}, "secret", algorithm="HS256")
Comparison Table
| Feature | OAuth2PasswordRequestForm | Standard Pydantic Model |
|---|---|---|
| Payload Content-Type | application/x-www-form-urlencoded | application/json |
| Field Constraints | Fixed username/password fields | Fully customizable field names |
| Primary Use Case | OAuth2 standard token endpoint | User registration / general CRUD |
Common Pitfalls
- Mistake: Sending JSON body to the OAuth2 token endpoint. Avoid: Use form data since OAuth2PasswordRequestForm requires application/x-www-form-urlencoded payloads.
- Mistake: Misconfiguring the tokenUrl in OAuth2PasswordBearer. Avoid: Ensure the string path matches the exact router prefix and endpoint path.
- Mistake: Generating JWT tokens without an exp claim. Avoid: Always calculate and append an expiration timestamp using timedelta.
- Mistake: Comparing passwords using direct string equality. Avoid: Always use bcrypt_context.verify to evaluate candidate passwords against stored hashes.
FAQs
- Why does the login endpoint require python-multipart?
The OAuth2 specification requires credentials to be sent via form data rather than JSON. FastAPI uses
python-multipartunder the hood to parseapplication/x-www-form-urlencodedrequest bodies. - Can I store sensitive information like passwords inside the JWT payload? No. JWT payloads are only Base64URL-encoded and can be decoded by anyone; only use non-sensitive claims such as user IDs, usernames, and expiration dates.
- Why should I use passlib instead of hashing passwords manually?
Passlib's
CryptContextautomatically handles secure salt generation, algorithm versioning, and constant-time string comparisons to prevent timing attacks.