Files
gh-christianlouis-inboxconv…/docs/adr/007-jwt-authentication.md
T

151 lines
6.3 KiB
Markdown

# ADR 007: Use JWT for API Authentication
**Status:** Accepted
**Date:** 2026-02-08
**Deciders:** Security Team, Development Team
## Context
The multi-tenant SaaS API needs a stateless authentication mechanism that:
1. Works with both browser-based frontends and programmatic API clients
2. Supports short-lived access tokens to limit exposure on compromise
3. Allows session refresh without re-entering credentials
4. Integrates with Google OAuth2 (users who sign in via Google)
5. Requires no server-side session store
## Decision
We will use **JWT (JSON Web Tokens)** with **HMAC-SHA256 (HS256)** signing for both access tokens and refresh tokens, implemented via the `python-jose` library.
- **Access tokens**: 30-minute TTL, sent in `Authorization: Bearer <token>` header
- **Refresh tokens**: 7-day TTL, exchanged for a new access token
- **Token subject**: User ID (stored as `str` in `sub` claim, decoded to `int` in deps)
## Alternatives Considered
### 1. Session Cookies (server-side sessions)
- **Pros**: Easy revocation, browser-native, CSRF protectable with SameSite
- **Cons**: Requires server-side session store (Redis), doesn't work well for API-first architecture, harder to use from mobile/CLI clients
### 2. OAuth2 Opaque Tokens
- **Pros**: Easy revocation, no token content leakage
- **Cons**: Every request requires a database lookup to validate the token (not stateless)
### 3. JWT with RS256 (RSA asymmetric signing)
- **Pros**: Public key verification allows third-party validation without sharing secret
- **Cons**: Additional key management complexity, slower to sign/verify, not required at current scale
### 4. API Keys (long-lived static tokens)
- **Pros**: Simple for machine-to-machine, easy to understand
- **Cons**: Long-lived tokens increase risk on compromise, no user-session semantics
### 5. Paseto (Platform-Agnostic Security Tokens)
- **Pros**: Better defaults than JWT (no algorithm confusion attacks), cleaner spec
- **Cons**: Less widespread adoption, fewer library options in Python, migration cost from JWT
## Rationale
JWT with HS256 was chosen because:
1. **Stateless**: No database lookup required to validate a token — the signature is self-authenticating
2. **Short TTL**: 30-minute access tokens limit the window of opportunity if a token is stolen
3. **Refresh Token Pattern**: 7-day refresh tokens allow long sessions without exposing long-lived access tokens
4. **Standard**: JWT is the de-facto standard for REST API authentication
5. **OAuth2 Compatibility**: Google OAuth2 tokens can be exchanged for our own JWTs, giving unified auth handling
6. **`python-jose`**: Well-maintained library with HS256/RS256 support and JWKS endpoint capability
## Implementation
```python
# backend/app/core/security.py
from datetime import datetime, timedelta, timezone
from jose import jwt, JWTError
ACCESS_TOKEN_EXPIRE_MINUTES = 30
REFRESH_TOKEN_EXPIRE_DAYS = 7
def create_access_token(subject: str) -> str:
expire = datetime.now(timezone.utc) + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
payload = {"sub": subject, "exp": expire, "type": "access"}
return jwt.encode(payload, settings.SECRET_KEY, algorithm="HS256")
def create_refresh_token(subject: str) -> str:
expire = datetime.now(timezone.utc) + timedelta(days=REFRESH_TOKEN_EXPIRE_DAYS)
payload = {"sub": subject, "exp": expire, "type": "refresh"}
return jwt.encode(payload, settings.SECRET_KEY, algorithm="HS256")
def decode_token(token: str) -> dict:
return jwt.decode(token, settings.SECRET_KEY, algorithms=["HS256"])
```
```python
# backend/app/core/deps.py
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/v1/auth/login")
async def get_current_user(
token: str = Depends(oauth2_scheme),
db: AsyncSession = Depends(get_db),
) -> User:
try:
payload = decode_token(token)
user_id = int(payload["sub"]) # sub is str (jose requirement), decode to int
except (JWTError, KeyError, ValueError):
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED)
user = await db.get(User, user_id)
if user is None:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED)
return user
```
### Token Subject Encoding
The `sub` claim uses `str(user.id)` when creating tokens and `int(payload["sub"])` when decoding. This is required because `python-jose` mandates string subjects per the JWT spec.
## Consequences
### Positive
- No database query per request for authentication (stateless validation)
- Short access token TTL limits blast radius of token theft
- Works identically for browser, mobile, and CLI clients
- Google OAuth flow exchanges Google's token for our own JWT (unified handling)
### Negative
- No instant token revocation (must wait for TTL to expire)
- Refresh token theft allows extended session hijacking
- `SECRET_KEY` compromise invalidates all tokens and requires rotation
### Token Revocation Strategy
For logout and forced revocation, a **token blocklist** stored in Redis can be used:
```python
# Add jti (JWT ID) claim to tokens
# On logout: store jti in Redis with TTL = token TTL
# On each request: check if jti is blocklisted
```
This is planned but not yet implemented; current logout deletes the token client-side only.
## Security Considerations
1. **HTTPS Only**: JWTs must only be transmitted over TLS in production
2. **No Sensitive Data in Payload**: JWT payload is base64-encoded, not encrypted — never put passwords or PII in claims
3. **Algorithm Pinning**: Always specify `algorithms=["HS256"]` in `jwt.decode()` to prevent algorithm confusion attacks
4. **Secret Key Length**: `SECRET_KEY` must be ≥ 32 characters (validated at startup)
5. **Token Storage**: Frontend stores tokens in `localStorage` (XSS risk); consider `httpOnly` cookies for hardened deployments
## Related Decisions
- See ADR-006 for SECRET_KEY management
- See ADR-003 for FastAPI dependency injection (`Depends(get_current_user)`)
## References
- [RFC 7519: JSON Web Token](https://www.rfc-editor.org/rfc/rfc7519)
- [python-jose Documentation](https://python-jose.readthedocs.io/)
- [JWT Best Practices (RFC 8725)](https://www.rfc-editor.org/rfc/rfc8725)
- [OWASP Authentication Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html)