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 // 365 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
365-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 365+ days re-authenticate.
Per OWASP Mobile Application Security Cheat Sheet:
There is an increasing trend towards allowing long timeout values on mobile app sessions due to the nature of user interaction with mobile apps. Interactions tend to be sporadic, unpredictable, and drawn out over a longer timeframe than with traditional web apps.
OWASP identifies the key risk as session theft — which device binding addresses.
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.