Adopt stateless PASETO authentication
Context
The authentication stack used PASETO tokens as transport for session IDs, with server-side sessions in Redis via tower-sessions and axum-login. ADR-0020 attempted to run token-based and session-based auth in parallel, but the session crates weren't built for it — custom middleware to bridge tokens and sessions created failures we couldn't control without forking the crates.
The session layer had become technical debt. Tower-sessions and axum-login are designed for cookie-based web apps. Native mobile apps use Authorization headers (Stripe, Firebase, AWS all work this way), store tokens in the Keychain, and GraphQL clients like Apollo expect bearer token auth.
Decision
Remove the Redis session layer entirely. PASETO tokens become the sole authentication mechanism.
Token structure
{
"user_id": "uuid",
"device_id": "uuid",
"iat": 1234567890,
"exp": 1234567890 // 90 days from iat
}
No roles or permissions in the token — the database remains authoritative for authorization. Every request fetches current user state from the database.
Authentication flow
On every authenticated request:
- Validate PASETO token (decrypt with any available key)
- Check token expiry (
exp > now()) - Validate device binding (
token.device_id == X-Device-IDheader) - Fetch user from database
- If token is >24h old or key is deprecated → issue new token via
X-New-Tokenresponse header
Device binding
iOS generates a stable UUID on first launch, stored in Keychain (survives reinstalls). Every request includes X-Device-ID header. Stolen tokens can't be used on a different device. This follows the sender-constrained token pattern from OAuth 2.0 DPoP (RFC 9449).
Session duration
90-day token expiry with sliding window — tokens refresh automatically during active usage (ADR-0027), so active users never experience unexpected logouts. Only users completely inactive for 90 days re-authenticate.
90 days aligns with the upper end of industry standard refresh token lifetimes. Mobile sessions are longer-lived than web sessions — interactions are sporadic and drawn out — but a stateless token without server-side revocation shouldn't extend beyond what's defensible without that infrastructure. Device binding (above) mitigates the primary risk of token theft.
Consequences
Eliminates the entire category of session/token race conditions. Authentication becomes a straightforward token parse, expiry check, and database lookup (~20 lines of middleware). No session cycling, no cookie handling, no Redis session management.
The trade-offs: no immediate token revocation without additional infrastructure (user-level revocation via database flags can be added later), and each request requires a database user lookup instead of a cached Redis session. Web client support would require reintroducing session management, but session-based code can be recovered from git history if needed.