Lesson 12 · FastAPI deep dive
FastAPI Tutorial #12: Authentication Basics with OAuth2
Video twelve of the eighteen-part series: proving who's calling, before JWT enters the picture. OAuth2PasswordBearer, form-based login, password hashing…
- CourseFastAPI deep dive
- Lesson12 of 12
- Video16 min
- FormatJupyter notebook · 10 code cells
What you'll learn
- The Problem - Storing Passwords in Plain Text
- passlib - Hashing Passwords Properly
- Verifying a Password Against Its Hash
- A Fake User Database With Hashed Passwords
- OAuth2PasswordBearer - Extracting the Token
- Missing Token - Automatically Rejected
- OAuth2PasswordRequestForm - the Login Endpoint
- getcurrentuser - Resolving a Token Back to a User
Data
No separate download needed — the notebook creates or downloads everything it uses.
📓 Full notebook
Download .ipynbFastAPI Deep-Dive, Video 12: Authentication Basics#
- Video twelve of the eighteen-part series: proving who's calling, before JWT enters the picture.
- OAuth2PasswordBearer, form-based login, password hashing with passlib, and protecting endpoints.
- Let's get into it.
Part 1: The Problem - Storing Passwords in Plain Text#
from fastapi import FastAPI
from fastapi.testclient import TestClient
app = FastAPI()
BAD_USERS = {'alex': 'hunter22'}
print(BAD_USERS)
Part 2: passlib - Hashing Passwords Properly#
from passlib.context import CryptContext
pwd_context = CryptContext(schemes=['bcrypt'], deprecated='auto')
hashed = pwd_context.hash('hunter22')
print(hashed[:20])
print(hashed == 'hunter22')
Part 3: Verifying a Password Against Its Hash#
print(pwd_context.verify('hunter22', hashed))
print(pwd_context.verify('wrong-password', hashed))
Part 4: A Fake User Database With Hashed Passwords#
USERS_DB = {
'alex': {'username': 'alex', 'hashed_password': pwd_context.hash('hunter22')},
}
def verify_login(username: str, password: str):
user = USERS_DB.get(username)
if not user:
return False
return pwd_context.verify(password, user['hashed_password'])
print(verify_login('alex', 'hunter22'))
print(verify_login('alex', 'wrong'))
print(verify_login('nobody', 'hunter22'))
Part 5: OAuth2PasswordBearer - Extracting the Token#
from fastapi import Depends, HTTPException
from fastapi.security import OAuth2PasswordBearer
oauth2_scheme = OAuth2PasswordBearer(tokenUrl='token')
@app.get('/whoami')
def whoami(token: str = Depends(oauth2_scheme)):
return {'token': token}
client = TestClient(app)
print(client.get('/whoami', headers={'Authorization': 'Bearer abc123'}).json())
Part 6: Missing Token - Automatically Rejected#
response = client.get('/whoami')
print(response.status_code)
print(response.headers.get('www-authenticate'))
Part 7: OAuth2PasswordRequestForm - the Login Endpoint#
from fastapi.security import OAuth2PasswordRequestForm
@app.post('/token')
def login(form_data: OAuth2PasswordRequestForm = Depends()):
if not verify_login(form_data.username, form_data.password):
raise HTTPException(status_code=401, detail='incorrect username or password')
return {'access_token': f'token-for-{form_data.username}', 'token_type': 'bearer'}
print(client.post('/token', data={'username': 'alex', 'password': 'hunter22'}).json())
print(client.post('/token', data={'username': 'alex', 'password': 'wrong'}).status_code)
Part 8: get_current_user - Resolving a Token Back to a User#
ISSUED_TOKENS = {}
def get_current_user(token: str = Depends(oauth2_scheme)):
username = ISSUED_TOKENS.get(token)
if not username:
raise HTTPException(status_code=401, detail='invalid or expired token')
return USERS_DB[username]
print(len(ISSUED_TOKENS))
Part 9: A Protected Endpoint Using get_current_user#
@app.post('/token-v2')
def login_v2(form_data: OAuth2PasswordRequestForm = Depends()):
if not verify_login(form_data.username, form_data.password):
raise HTTPException(status_code=401, detail='incorrect username or password')
token = f'token-for-{form_data.username}'
ISSUED_TOKENS[token] = form_data.username
return {'access_token': token, 'token_type': 'bearer'}
@app.get('/users/me')
def read_me(current_user: dict = Depends(get_current_user)):
return {'username': current_user['username']}
login_response = client.post('/token-v2', data={'username': 'alex', 'password': 'hunter22'})
issued_token = login_response.json()['access_token']
print(client.get('/users/me', headers={'Authorization': f'Bearer {issued_token}'}).json())
print(client.get('/users/me', headers={'Authorization': 'Bearer made-up-token'}).status_code)
Part 10: A Real Pattern - a Small, Working Login Flow#
auth_app = FastAPI()
auth_scheme = OAuth2PasswordBearer(tokenUrl='login')
auth_users = {'sam': {'username': 'sam', 'hashed_password': pwd_context.hash('correct-horse')}}
auth_tokens = {}
def auth_verify_login(username: str, password: str):
user = auth_users.get(username)
if not user:
return False
return pwd_context.verify(password, user['hashed_password'])
def auth_get_current_user(token: str = Depends(auth_scheme)):
username = auth_tokens.get(token)
if not username:
raise HTTPException(status_code=401, detail='invalid or expired token')
return auth_users[username]
@auth_app.post('/login')
def auth_login(form_data: OAuth2PasswordRequestForm = Depends()):
if not auth_verify_login(form_data.username, form_data.password):
raise HTTPException(status_code=401, detail='incorrect username or password')
token = f'token-for-{form_data.username}'
auth_tokens[token] = form_data.username
return {'access_token': token, 'token_type': 'bearer'}
@auth_app.get('/profile')
def auth_profile(current_user: dict = Depends(auth_get_current_user)):
return {'username': current_user['username']}
auth_client = TestClient(auth_app)
print(auth_client.get('/profile').status_code)
login = auth_client.post('/login', data={'username': 'sam', 'password': 'correct-horse'})
token = login.json()['access_token']
print(auth_client.get('/profile', headers={'Authorization': f'Bearer {token}'}).json())
Wrap-Up: What You Learned#
- A user store should hold a hashed password, never the raw original.
- CryptContext with bcrypt hashes a password one-way; the original can never be recovered from it.
- pwd_context.verify re-hashes a candidate password and compares, without ever reversing the hash.
- OAuth2PasswordBearer extracts the raw token from a Bearer Authorization header via Depends.
- A missing Authorization header is automatically rejected with 401, no extra endpoint code needed.
- OAuth2PasswordRequestForm reads username and password from a form-encoded login request.
- A get_current_user dependency resolves a token back to the specific user it belongs to.
- A protected endpoint just depends on get_current_user; the login endpoint must issue real tokens.
- This opaque, in-memory token pattern is the minimum flow that JWT later makes portable and stateless.
- That wraps up authentication basics. Next up: JWT Authentication.
Found this useful?
All lessons, notebooks and datasets here are free. If they helped you, a coffee keeps new lessons coming.



