You have spent seven posts learning how authentication and authorization work — passwords, JWTs, API keys, OAuth 2.0, OpenID Connect, RBAC, ABAC, and ReBAC. Now a product manager walks up and asks: “We’re building a B2B SaaS with a REST API. What should we use for auth?”
The answer is not one method. It is a combination — different methods for different actors, different contexts, and different security requirements. This post gives you the decision framework to answer that question for any system.
The Master Comparison
Authentication Methods
| Method | Identity Carrier | Stateful? | Lifetime | Revocation | Complexity | Best For |
|---|---|---|---|---|---|---|
| Password + Session | Session cookie | Yes (Redis/DB) | Hours–days | Instant (delete session) | Low | Server-rendered web apps |
| JWT | Bearer token | No (self-contained) | Minutes | Delayed (expiry or blocklist) | Medium | SPAs, mobile, microservices |
| API Key | Header/query param | Yes (DB lookup) | Weeks–months | Instant (deactivate key) | Low | Machine-to-machine, partner APIs |
| OAuth 2.0 | Access token | Depends | Minutes–hours | Depends on provider | High | Delegated access, “Login with X” |
| OpenID Connect | ID token (JWT) | No | Minutes | N/A (identity snapshot) | High | SSO, enterprise identity |
| mTLS | Client certificate | No | Months–years | CRL/OCSP | Very High | Service mesh, zero-trust |
Authorization Models
| Model | Decision Basis | Granularity | Query Cost | Scaling Challenge | Best For |
|---|---|---|---|---|---|
| RBAC | User → Role → Permission | Coarse | 1–2 queries | Role explosion | Admin panels, internal tools |
| ABAC | Attributes + Policies | Fine | Policy eval + attribute fetching | Policy complexity | Compliance, classification |
| ReBAC | User → Relationship → Resource | Per-resource | Graph traversal | Graph size | Collaborative platforms |
The Decision Tree
Authorization Decision
Production Combination Patterns
No production system uses a single auth method. Here are the four most common patterns:
Pattern 1: The Startup SaaS
Auth stack: Password + JWT + RBAC
- Users register with email/password
- Login returns JWT (access + refresh)
- RBAC with 3–5 roles: owner, admin, member, viewer
- Add Google/GitHub SSO when customers ask (OIDC)
When to evolve: When enterprise customers need their own IdP → add SAML/OIDC SSO.
Pattern 2: The API Platform
Auth stack: API Keys + OAuth 2.0 + Rate Limiting
- First-party integrations use scoped API keys
- Third-party apps use OAuth 2.0 (authorization code + PKCE)
- All access through an API gateway with per-key rate limiting
- Webhooks signed with HMAC for outbound events
When to evolve: When partners need user-level delegation → add OAuth scopes per user.
Pattern 3: The Enterprise B2B
Auth stack: OIDC SSO + RBAC + ABAC
- Each customer connects their IdP via OIDC (or SAML bridge)
- Roles provisioned via SCIM or mapped from IdP groups
- ABAC for compliance: data classification, regional access, time-based policies
- Tenant isolation enforced at the database level (row-level security)
When to evolve: When customers need per-document sharing → add ReBAC layer.
Pattern 4: The Microservice Platform
Auth stack: JWT + mTLS + Service Mesh
- User-facing JWT validated at the API gateway
- JWT forwarded to downstream services (RS256 — services verify with public key)
- Service-to-service: mTLS for transport security + JWT for user context
- Each service checks its own scope (
Depends(require_scope("billing:write")))
Migration Paths
Real systems evolve. Here are the common transitions:
| From | To | Trigger | Migration Strategy |
|---|---|---|---|
| Password only | + OAuth/OIDC SSO | Enterprise customers | Add OAuth alongside passwords — link accounts by email |
is_admin boolean | RBAC | Need >2 permission levels | Add roles table, backfill existing users, gate routes with dependencies |
| RBAC | + ABAC | Compliance requirements | Add policy engine, keep RBAC for coarse checks, ABAC for fine-grained |
| Session cookies | JWT | SPA or mobile client | Add JWT endpoints, keep sessions for web — both can coexist |
| API keys only | + OAuth 2.0 | Third-party marketplace | Add OAuth for new integrations, grandfather existing API keys |
| Self-managed auth | Auth-as-a-Service | Team capacity | Migrate to Auth0/Clerk/WorkOS, keep custom auth for edge cases |
The “Add, Don’t Replace” Principle
Auth migrations are safest when you add the new method alongside the old one:
# Support both session AND JWT auth during migrationasync def get_current_user( request: Request, token: str | None = Depends(oauth2_scheme_optional), session_id: str | None = Cookie(None), db: AsyncSession = Depends(get_db),) -> User: # Try JWT first if token: return await validate_jwt_user(token, db)
# Fall back to session if session_id: return await validate_session_user(session_id, db)
raise HTTPException(status_code=401, detail="Not authenticated")Old clients keep using sessions. New clients use JWT. Both work. Migrate gradually.
Pre-Launch Security Checklist
Before shipping any auth implementation to production, verify every item:
Transport Layer
- TLS/HTTPS enforced on all endpoints (HSTS header set)
- No credentials in URL query parameters (use headers or body)
- No credentials in server logs (redact
Authorizationheaders) - CORS configured —
Access-Control-Allow-Originis not*
Password Authentication
- Passwords hashed with Argon2id or bcrypt (never SHA/MD5)
- Minimum password length enforced (NIST: 8 characters)
- Maximum password length enforced (prevent DoS: 128 characters)
- Failed login attempts tracked — account lockout after 5 failures
- IP-based rate limiting on login endpoints
- Registration does not leak whether email exists (or it is an acceptable tradeoff)
Token Security
- JWTs signed with explicit algorithm list —
algorithms=["HS256"], never"none" - Access tokens expire in ≤15 minutes
- Refresh tokens stored in HttpOnly cookies with
SameSite=Strict - Refresh token rotation implemented — old tokens invalidated
- Token type checked — refresh tokens cannot be used as access tokens
- JWT secret key is ≥256 bits of entropy
API Keys
- Stored as SHA-256 hashes — plaintext never persisted
- Shown to user exactly once at creation
- Scoped with least-privilege permissions
- Prefixed for identification (
sk_,pk_,cdk_) - Expiry dates set (or reviewed quarterly)
- Per-key rate limiting enforced
OAuth / OIDC
-
stateparameter validated on callback (CSRF protection) -
noncevalidated in ID tokens (replay protection) - Client secret never exposed to frontend code
- PKCE used for SPAs and mobile (no implicit grant)
- Scopes are minimal — request only what you need
- ID token
audclaim matches your client ID
Authorization
- Every endpoint has an explicit authorization check (no “default allow”)
- BOLA prevention: resource queries filter by tenant/owner
- Admin endpoints are not accessible by guessing URLs
- Authorization failures return 403, not 404 (unless hiding resource existence is desired)
- Permissions checked in dependencies, not in route handler bodies
Audit
- All authentication events logged (login, logout, failure, token refresh)
- All authorization failures logged with user ID and resource ID
- Logs do not contain passwords, tokens, or API keys
- Logs are append-only (not editable by application code)
Quick Reference: Which Auth for What
| Scenario | Authentication | Authorization | Notes |
|---|---|---|---|
| Blog with admin panel | Password + session | is_admin boolean | Keep it simple |
| SPA with user accounts | Password + JWT | RBAC (3–5 roles) | Add SSO when customers ask |
| Mobile app | OAuth PKCE + JWT | RBAC | Never store client secrets in app |
| Public API | API keys (partners) + OAuth (third-party) | Scopes | Rate limit per key |
| Enterprise SaaS | OIDC SSO | RBAC + ABAC | Map IdP groups to roles |
| Collaborative platform | OIDC SSO + password fallback | ReBAC | Per-resource sharing like Google Docs |
| Microservices | JWT (user) + mTLS (service) | Scoped claims | RS256 for distributed validation |
| Internal tools | OIDC (corporate IdP) | RBAC | Single source of identity |
Series Recap
Over eight posts we have built:
- A mental model that separates authentication from authorization and maps the four-layer security stack
- Password authentication with Argon2id hashing, session cookies, CSRF protection, and brute-force mitigation
- JWT authentication with access/refresh tokens, rotation, and three revocation strategies
- API key authentication with hashed storage, scoping, zero-downtime rotation, and per-key rate limiting
- OAuth 2.0 with the authorization code flow, PKCE for SPAs, and client credentials for machines
- OpenID Connect with Google SSO, ID token validation, and an OIDC-vs-SAML comparison
- Authorization patterns — RBAC, ABAC, and ReBAC — implemented as FastAPI dependencies
- A decision framework with comparison matrices, production patterns, migration paths, and a security checklist
Auth is not a feature you bolt on. It is a design decision that shapes your architecture. Choose the simplest combination that meets your requirements today, and design your dependencies so you can add layers as your requirements grow.
The best auth system is the one your team understands well enough to operate correctly in production — at 2 AM, under pressure, when something goes wrong.