Authentication Tutorial¶
Learn to implement SSO authentication with OAuth2/OIDC in the example application.
Time: 30 minutes
What You Will Learn¶
- OAuth2/OIDC authentication flow
- Protecting API endpoints with authentication
- Accessing user information in handlers
- Testing authentication
Prerequisites¶
- Services running (
make up) - Authentik configured (
make tf-apply)
Part 1: Understanding the Login Flow¶
Step 1: Try Logging In¶
Visit http://localhost:8000/login
What happens:
1. Redirect to Authentik (http://localhost:9000/if/flow/...)
2. Enter credentials (alice@example.com / password)
3. Redirect back to app (http://localhost:8000/auth/callback?code=...&state=...)
4. Session cookie created
The password never touches the application. OAuth uses redirects for security.
Step 2: Examine the Session Cookie¶
In browser DevTools (Application > Cookies):
The session contains user identity, signed to prevent tampering.
Part 2: Creating an Authenticated Endpoint¶
Step 3: Create a Protected Route¶
File: webapp/app/routers/profile.py
from typing import Annotated
from fastapi import APIRouter, Depends
from app.auth import get_current_user
from app.models import User
router = APIRouter()
@router.get("/profile")
async def get_profile(
current_user: Annotated[User, Depends(get_current_user)]
):
"""Get current user's profile."""
return {
"id": current_user.id,
"email": current_user.email,
"name": current_user.name,
}
Depends(get_current_user) automatically:
- Checks if user is logged in
- Returns 401 if not
- Provides User object if authenticated
Step 4: Register the Router¶
File: webapp/app/main.py
Step 5: Test Your Endpoint¶
Rebuild and restart:
Test without authentication:
curl http://localhost:8000/profile
# Response: {"detail": "Not authenticated"}
# Status: 401 Unauthorized
Test with authentication:
curl -H "Cookie: session=$ALICE_SESSION_COOKIE" http://localhost:8000/profile
# Response: {"id": "...", "email": "alice@example.com", "name": "Alice Smith"}
Part 3: Working With User Data¶
Step 6: Create a Personal Resource¶
File: webapp/app/routers/profile.py
from sqlalchemy.orm import Session
from app.database import get_db
from app.models import Document
@router.get("/profile/documents")
async def get_my_documents(
current_user: Annotated[User, Depends(get_current_user)],
db: Annotated[Session, Depends(get_db)]
):
"""Get current user's documents."""
documents = db.query(Document).filter(
Document.owner_id == current_user.id
).all()
return {
"count": len(documents),
"documents": [
{"id": doc.id, "name": doc.name}
for doc in documents
]
}
Use current_user.id to filter data by authenticated user.
Step 7: Test User Isolation¶
Create documents as Alice:
curl -X POST -H "Cookie: session=$ALICE_SESSION_COOKIE" \
-H "Content-Type: application/json" \
-d '{"name": "Alice Doc", "content": "test"}' \
http://localhost:8000/api/documents
View as Alice:
curl -H "Cookie: session=$ALICE_SESSION_COOKIE" \
http://localhost:8000/profile/documents
# Response: {"count": 1, "documents": [...]}
View as Bob:
curl -H "Cookie: session=$BOB_SESSION_COOKIE" \
http://localhost:8000/profile/documents
# Response: {"count": 0, "documents": []}
Each user only sees their own data.
Part 4: Understanding the Code¶
Step 8: Examine get_current_user()¶
File: webapp/app/auth.py
async def get_current_user(request: Request, db: Session) -> User:
# 1. Get user data from session
user_data = request.session.get("user")
# 2. If no session, reject request
if not user_data:
raise HTTPException(status_code=401, detail="Not authenticated")
# 3. Look up user in database
user = db.query(User).filter(User.id == user_data["sub"]).first()
# 4. If user doesn't exist, create them (auto-registration)
if not user:
user = User(
id=user_data["sub"],
email=user_data["email"],
name=user_data["name"]
)
db.add(user)
db.commit()
return user
Step 9: Trace the Login Flow¶
File: webapp/app/main.py
Login endpoint:
@app.get("/login")
async def login(request: Request):
return await oauth.authentik.authorize_redirect(
request,
redirect_uri=settings.oidc_redirect_uri
)
Callback endpoint:
@app.get("/auth/callback")
async def auth_callback(request: Request):
token = await oauth.authentik.authorize_access_token(request)
userinfo = token.get('userinfo')
request.session['user'] = {
'sub': userinfo['sub'],
'email': userinfo['email'],
'name': userinfo['name']
}
return RedirectResponse(url="/")
Part 5: Testing¶
Step 10: Write a Test¶
File: integration-tests/tests/test_profile.py
from tests.conftest import AuthenticatedClient
def test_get_profile(alice_client: AuthenticatedClient):
"""Test profile endpoint returns user data."""
response = alice_client.get("/profile")
assert response.status_code == 200
data = response.json()
assert data["email"] == "alice@example.com"
def test_profile_requires_auth(client):
"""Test profile endpoint rejects unauthenticated requests."""
response = client.get("/profile")
assert response.status_code == 401
Run tests:
Summary¶
| Concept | Description |
|---|---|
| OAuth/OIDC Flow | Redirect to IdP, authenticate, return with code, exchange for tokens |
| Session Management | Signed cookies store user identity |
| Protected Endpoints | Depends(get_current_user) provides authentication |
| User Context | Access current_user.id, current_user.email in handlers |
Next Steps¶
- Authorization Tutorial - Add policy-based authorization
- SSO Concepts - Deep dive into OIDC
- OIDC Reference - Configuration options