1. The Mobile Dual-Token Strategy: Access vs Refresh Tokens
A frequent architectural mistake is issuing a single JWT token with a 30-day expiration. If that token is intercepted or the user's phone is compromised, the attacker retains unrestricted access for a month with no mechanism for the backend to revoke it.
The industry gold standard is the Dual-Token Rotation pattern. The backend issues two tokens upon login: a short-lived Access Token (15 minutes) signed with HMAC-SHA256, and a long-lived Refresh Token (30 days) stored as an encrypted hash in the PostgreSQL database alongside the device identifier.
When the Flutter mobile app makes API calls, it includes the short-lived access token in the Authorization: Bearer header. When the access token expires (HTTP 401), a Dio interceptor silently calls /auth/refresh with the refresh token to receive a fresh access token without logging the user out.
By pairing each refresh token with a unique device fingerprint, you can detect concurrent stolen token usage and revoke compromised sessions instantly across all devices from a central admin dashboard.
from datetime import datetime, timedelta, timezone
from jose import jwt
from app.core.config import settings
def create_access_token(data: dict, expires_delta: timedelta | None = None) -> str:
to_encode = data.copy()
expire = datetime.now(timezone.utc) + (expires_delta or timedelta(minutes=15))
to_encode.update({"exp": expire, "type": "access"})
return jwt.encode(to_encode, settings.JWT_SECRET_KEY, algorithm=settings.JWT_ALGORITHM)
def create_refresh_token(user_id: str) -> str:
expire = datetime.now(timezone.utc) + timedelta(days=30)
to_encode = {"sub": user_id, "exp": expire, "type": "refresh"}
return jwt.encode(to_encode, settings.REFRESH_SECRET_KEY, algorithm=settings.JWT_ALGORITHM)2. Enterprise Password Hashing with Argon2id
Legacy algorithms like MD5, SHA-1, and basic SHA-256 are completely unacceptable for password storage; modern GPUs can compute billions of SHA-256 hashes per second. Even traditional bcrypt, while decent, has limitations against ASIC hardware cracking.
I use Argon2id via the passlib or argon2-cffi library. Argon2 is the winner of the Password Hashing Competition. It provides memory-hard protection that neutralizes GPU and FPGA brute-force attacks by requiring substantial memory allocations during hash verification.
Configuring appropriate time cost, memory cost, and parallelism factors ensures that password verification takes approximately 200ms per attempt on the server—imperceptible to a legitimate user, but mathematically devastating to an attacker attempting dictionary brute force attacks.
3. Granular Role-Based Access Control (RBAC) with FastAPI Dependencies
Simple booleans like is_admin: bool fail as application features multiply. Real systems require granular permissions: 'users:read', 'orders:create', 'analytics:export'.
FastAPI's dependency injection system allows you to build clean, reusable permission guards that verify token scopes before executing endpoint handlers. If a standard customer attempts to access a vendor analytics route, FastAPI automatically rejects the request with HTTP 403 Forbidden.
Because dependencies can be nested hierarchically, you can combine authentication, rate limiting, and permission verification into modular chains that apply universally across API router groups.
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
from app.models.user import UserRole
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/v1/auth/login")
class RoleChecker:
def __init__(self, allowed_roles: list[UserRole]):
self.allowed_roles = allowed_roles
async def __call__(self, current_user = Depends(get_current_active_user)):
if current_user.role not in self.allowed_roles:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Insufficient permissions to access this resource",
)
return current_user4. Token Revocation and Mobile Security Hardening
Because JWT access tokens are stateless, revoking them immediately upon logout requires a lightweight token blacklist. When a user logs out or changes their password, we store the jti (JWT ID) in Redis with an expiration matching the token's remaining TTL.
On the mobile client, sensitive tokens must NEVER be stored in plain SharedPreferences or UserDefaults. Always use FlutterSecureStorage, which leverages hardware Keychain on iOS and Android KeyStore with EncryptedSharedPreferences.
Additionally, enforce SSL certificate pinning in mobile production builds to protect clients against man-in-the-middle interception over compromised public Wi-Fi networks.
5. Biometric Authentication & Cryptographic Challenge-Response
Modern mobile users expect biometric convenience: unlocking banking and sensitive personal data using FaceID, TouchID, or Android BiometricPrompt without typing a password repeatedly.
In my architecture, biometrics do not simply bypass password entry locally. When biometric verification succeeds on the mobile device, Flutter utilizes the hardware secure enclave to decrypt an asymmetric private key stored on device.
The client signs a cryptographic nonce generated by FastAPI, proving hardware possession without transmitting the master password over the network. This multi-layered defense guarantees enterprise-grade security even on rooted or jailbroken handsets.
Final Thoughts
Security is not a feature you bolt on after launch; it is the fundamental architecture that protects your users and business. By combining short-lived JWT access tokens, revocable refresh tokens, Argon2id hashing, and declarative FastAPI RBAC dependencies, you build a fortress for your mobile applications.
Key Takeaways
- Implement dual-token rotation with 15-minute access tokens and 30-day refresh tokens.
- Store passwords exclusively using memory-hard Argon2id hashing.
- Use callable FastAPI dependency classes for declarative Role-Based Access Control.
- Blacklist revoked tokens in Redis and store client tokens strictly in hardware secure enclaves.
- Enforce SSL certificate pinning on mobile apps to prevent man-in-the-middle attacks.
Frequently Asked Questions
Where should the Flutter mobile app store JWT access and refresh tokens?
Always store tokens using package:flutter_secure_storage. On iOS, it uses the hardware Keychain; on Android, it encrypts data via Android KeyStore. Never store tokens in shared_preferences or raw local SQLite files.
How does the mobile app handle token expiration automatically?
Configure a Dio interceptor (QueuedInterceptor). When an API response returns 401 Unauthorized, pause outgoing requests, call /auth/refresh with the stored refresh token, save the new access token, and retry the original request transparently.
Why is Argon2id preferred over bcrypt in 2026?
Argon2id combines resistance to side-channel cache attacks with memory-hard computation, rendering GPU and ASIC brute-force cracking mathematically infeasible compared to CPU-only bcrypt.
How do you invalidate all active sessions when a user resets their password?
Store a token_version integer or password_changed_at timestamp on the User database model and include it in the JWT payload. Incrementing token_version immediately invalidates all previously issued JWTs across all devices.
How do you defend against JWT secret key exposure?
Store your secret keys strictly in secure environment variables or cloud secret managers (AWS Secrets Manager, HashiCorp Vault). Use RS256 asymmetric signing (private key on backend, public key for verification) so that client verification services cannot forge signatures.



