You open a FastAPI tutorial, search for “authentication,” and find a code block that hashes a password, issues a JWT, and checks a role — all in a single function. You copy it, it works, and three months later you are debugging a privilege escalation bug at 1 AM because the tutorial conflated three different security concepts into 40 lines.
Authentication and authorization are different problems with different threat models, different failure modes, and different solutions. Mixing them up does not just make your code messy — it makes your application insecure. This series separates them cleanly, builds each from first principles with production FastAPI code, and ends with a decision framework for choosing the right approach for your system.
Authentication vs. Authorization
The distinction is simple enough to fit on a napkin:
- Authentication (AuthN): “Who are you?” — Verifying identity.
- Authorization (AuthZ): “What can you do?” — Verifying permissions.
A 401 Unauthorized response means “I don’t know who you are” — despite the misleading HTTP status name, it is an authentication failure. A 403 Forbidden response means “I know who you are, and the answer is no.” If your application returns 403 to an unauthenticated request, you have leaked information — the attacker now knows the resource exists.
The Nightclub Analogy
Think of a building with two checkpoints:
-
The bouncer at the door (authentication) checks your ID. Are you who you claim to be? Valid ID → you enter. Fake ID → you are turned away. The bouncer does not care whether you are on the VIP list.
-
The host inside (authorization) checks the guest list. You have a valid ID, but are you allowed in the VIP section? On the list → access granted. Not on the list → you can stay in the general area but cannot pass the velvet rope.
Most security bugs happen when applications skip the bouncer and go straight to the guest list, or worse, when the bouncer and the host are the same person using the same checklist.
The Four-Layer Auth Stack
Every auth system — from a weekend project to a bank — has four layers. Some are explicit; some are implicit. Leaving any layer implicit is where vulnerabilities hide.
| Layer | Question | Failure Mode | Example Fix |
|---|---|---|---|
| Transport | Is the channel secure? | Eavesdropping, MITM | TLS everywhere, HSTS headers |
| Identity | Who is making this request? | Impersonation, credential theft | bcrypt hashing, JWT validation |
| Access Control | Are they allowed this action? | Privilege escalation, data leak | RBAC, ABAC, scope checks |
| Audit | What happened, and can we prove it? | Undetected breaches, compliance failure | Append-only logs, event sourcing |
When you read about a data breach, trace the root cause back to this stack. Almost every breach is a failure at one of these four layers — usually identity (stolen credentials) or access control (missing permission check).
The Threat Landscape for APIs
Web applications and APIs face overlapping but distinct threats. Since we are building FastAPI backends, here are the threats that should shape our auth decisions:
Credential-Based Attacks
- Brute force: Automated password guessing. Mitigated by rate limiting and account lockout.
- Credential stuffing: Using breached username/password pairs from other sites. Mitigated by multi-factor authentication and breach detection.
- Phishing: Tricking users into entering credentials on a fake site. Mitigated by WebAuthn/FIDO2 and user education.
Token-Based Attacks
- Token theft: Stealing JWTs or session tokens from storage, logs, or network traffic. Mitigated by short expiry, secure storage, and TLS.
- Token replay: Reusing a valid token after the user has logged out. Mitigated by token revocation lists or short-lived tokens.
- Token forgery: Creating fake tokens. Mitigated by proper signing (HMAC or RSA) and key rotation.
API-Specific Attacks
- Broken Object Level Authorization (BOLA): Accessing another user’s resources by changing an ID in the URL. The #1 API vulnerability in the OWASP API Security Top 10.
- Broken Function Level Authorization: Calling admin endpoints without admin privileges. A missing
Depends()in FastAPI. - Mass Assignment: Sending extra fields in a request body to modify protected attributes. Mitigated by strict Pydantic schemas.
FastAPI’s Security Toolkit
FastAPI provides a set of dependency-injection utilities in fastapi.security that handle the transport of credentials — extracting tokens from headers, cookies, or query parameters. They do not validate credentials or check permissions. That is your job.
from fastapi.security import ( OAuth2PasswordBearer, # Extracts Bearer token from Authorization header OAuth2PasswordRequestForm, # Parses username/password from form data HTTPBasic, # Extracts Basic auth credentials HTTPBearer, # Extracts Bearer token (simpler than OAuth2 scheme) APIKeyHeader, # Extracts API key from a custom header APIKeyQuery, # Extracts API key from query parameter APIKeyCookie, # Extracts API key from cookie)Each of these is a dependency — a callable that FastAPI injects into your route handler via Depends(). They return the raw credential (a string) or raise HTTPException(401) if the credential is missing.
Here is the simplest possible authenticated endpoint:
from fastapi import Depends, FastAPIfrom fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
app = FastAPI()security = HTTPBearer()
@app.get("/protected")async def protected_route( credentials: HTTPAuthorizationCredentials = Depends(security),): # credentials.scheme == "Bearer" # credentials.credentials == the actual token string # YOU must validate this token — FastAPI only extracted it return {"token_received": credentials.credentials[:10] + "..."}FastAPI extracts. You validate. This separation is intentional and important. The framework does not know whether your token is a JWT, a session ID, or an API key. It only knows where to find it in the request.
The Dependency Chain Pattern
The real power of FastAPI’s auth model is composing dependencies. You build small, focused dependencies and chain them:
from fastapi import Depends, HTTPException, status
# Layer 1: Extract tokenoauth2_scheme = OAuth2PasswordBearer(tokenUrl="/auth/token")
# Layer 2: Validate token → return userasync def get_current_user(token: str = Depends(oauth2_scheme)): user = await validate_token(token) # Your logic if not user: raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED) return user
# Layer 3: Check permissionsasync def require_admin(user: User = Depends(get_current_user)): if not user.is_admin: raise HTTPException(status_code=status.HTTP_403_FORBIDDEN) return user
# Route: fully protected@app.delete("/users/{user_id}")async def delete_user(user_id: int, admin: User = Depends(require_admin)): ...Each dependency has one job. Each can be tested independently. And if a dependency raises an exception, the entire chain stops — the route handler never executes.
Authentication Methods Overview
Before we dive into each method in subsequent posts, here is the landscape. Every authentication method answers the same question — “Who are you?” — but each makes different tradeoffs:
| Method | Credential | Stateful? | Best For | Weakness |
|---|---|---|---|---|
| Password + Session | Username/password → session cookie | Yes (server stores sessions) | Traditional web apps, server-rendered UIs | Session storage scaling, CSRF |
| JWT (Bearer Token) | Username/password → signed token | No (token is self-contained) | SPAs, mobile apps, microservices | Token revocation, size |
| API Key | Pre-shared key in header | Depends on implementation | Machine-to-machine, third-party integrations | Key rotation, no user context |
| OAuth 2.0 | Delegated authorization via tokens | Depends on grant type | ”Login with Google,” third-party API access | Complexity, many moving parts |
| OpenID Connect | OAuth 2.0 + ID token | No (ID token is JWT) | Single Sign-On, enterprise SSO | Requires OIDC provider |
| mTLS | Client certificate | No (certificate is self-contained) | Service mesh, zero-trust internal APIs | Certificate management |
We will build each of these in FastAPI across the next seven posts. By the end, you will understand not just how each works, but when to choose one over another — and how to combine them in a production system.
What We Are Building
Throughout this series, we will use a running example: CloudDocs, a document-sharing API. It is small enough to understand quickly but has natural auth requirements:
- Users sign up, log in, and manage their profiles (password auth, JWT, SSO)
- Documents are created, read, updated, and deleted (BOLA prevention, ownership checks)
- Organizations group users with different roles (RBAC, authorization)
- API clients integrate via programmatic access (API keys, OAuth scopes)
- Audit logs track every state change (compliance, debugging)
Each post adds one auth layer to CloudDocs with complete, runnable FastAPI code.
What’s Next
In Part 2, we start with the most familiar auth method: passwords. You will learn why bcrypt exists, how to build registration and login endpoints, and why session cookies are still the right choice for server-rendered applications.
But first, make sure your mental model is solid:
- Authentication verifies identity (who). Authorization verifies permissions (what).
- 401 means “unknown.” 403 means “known but denied.”
- FastAPI’s security utilities extract credentials. You validate them.
- Every auth system has four layers: transport, identity, access control, audit.
Get these right, and every auth implementation in this series will feel like a logical extension rather than a pile of magic code.