Mobile Security: JWT, Secure Storage, Biometrics & Encryption
JWT flow, flutter_secure_storage, biometric auth, certificate pinning, OWASP mobile top 10
Open interactive version (quiz + challenge)Real-world analogy
What is it?
Mobile security protects user data, session integrity, and app authenticity. For fintech, healthcare, and any app handling personal data, security vulnerabilities can result in regulatory fines, data breaches, and loss of user trust. Senior Flutter engineers must be able to implement and articulate the full security stack.
Real-world relevance
In a fintech claims/refunds app: JWT access tokens stored in flutter_secure_storage (never SharedPreferences). Dio interceptor auto-refreshes tokens on 401. Biometric gate prevents shoulder-surfing on the claims list. Certificate pinning protects BankID communication. R8 obfuscation and --obfuscate in release builds. Security audit uses OWASP Mobile Top 10 as the checklist.
Key points
- JWT Structure — JWT = base64(header).base64(payload).signature. Header: algorithm (RS256/HS256). Payload: claims (sub, exp, iat, custom). Signature: HMAC or RSA. Mobile apps should never validate JWT signatures client-side — the server validates. Client only reads the payload to check expiry.
- Token Refresh Flow — Access token (short-lived, 15 min–1 hr) + Refresh token (long-lived, days–weeks). When access token expires (401), use refresh token to get a new access token silently. Dio interceptors are the standard Flutter pattern for auto-refresh.
- flutter_secure_storage — Stores key-value pairs in Android Keystore (hardware-backed on modern devices) and iOS Keychain. Never use SharedPreferences or Hive for tokens. flutter_secure_storage encrypts data at rest and ties it to the device.
- local_auth (Biometrics) — local_auth package provides fingerprint/Face ID authentication. It gates access to already-authenticated sessions — it does NOT replace server authentication. On Android it uses BiometricPrompt API. Always provide a fallback PIN.
- Certificate Pinning — Embed the server's public key hash in the app. Reject TLS connections whose certificate chain does not match. Prevents man-in-the-middle attacks even if a rogue CA is trusted by the device. Implemented via http_certificate_pinning or custom HttpClient with SecurityContext.
- HTTPS/TLS — All network calls must use HTTPS. Android blocks plain HTTP by default (usesCleartextTraffic=false since Android 9). Use HSTS headers server-side. Never disable SSL verification (badCertificateCallback: (_,_,_) => true) in production — a critical vulnerability.
- Secrets Management — API keys must NEVER be hardcoded in Dart code or committed to git. Use --dart-define for build-time injection, or server-proxied endpoints. Obfuscation (ProGuard/R8) makes key extraction harder but does not make it impossible — the key can still be extracted from a decompiled APK.
- ProGuard/R8 Obfuscation — Android's R8 compiler shrinks, optimises, and obfuscates Kotlin/Java bytecode. Enable in build.gradle: minifyEnabled true, proguardFiles. Flutter's Dart code is obfuscated separately via --obfuscate --split-debug-info flags. Essential for production releases.
- OWASP Mobile Top 10 — M1: Improper Credential Usage. M2: Inadequate Supply Chain Security. M3: Insecure Authentication/Authorisation. M4: Insufficient Input/Output Validation. M5: Insecure Communication. M6: Inadequate Privacy Controls. M7: Insufficient Binary Protections. M8: Security Misconfiguration. M9: Insecure Data Storage. M10: Insufficient Cryptography.
- Insecure Data Storage (M9) — Common Flutter mistakes: storing tokens in SharedPreferences, writing sensitive data to the device filesystem without encryption, logging sensitive data with print() in production builds, caching PII in image caches without expiry.
Code example
// === JWT TOKEN REFRESH WITH DIO INTERCEPTOR ===
class AuthInterceptor extends Interceptor {
final FlutterSecureStorage _storage = const FlutterSecureStorage();
bool _isRefreshing = false;
final _pendingRequests = <ErrorInterceptorHandler>[];
@override
Future<void> onError(DioException err, ErrorInterceptorHandler handler) async {
if (err.response?.statusCode == 401 && !_isRefreshing) {
_isRefreshing = true;
try {
final refreshToken = await _storage.read(key: 'refresh_token');
if (refreshToken == null) { _logout(); return; }
final response = await _dio.post('/auth/refresh',
data: {'refresh_token': refreshToken});
final newAccessToken = response.data['access_token'];
await _storage.write(key: 'access_token', value: newAccessToken);
// Retry failed request with new token
err.requestOptions.headers['Authorization'] = 'Bearer $newAccessToken';
final retried = await _dio.fetch(err.requestOptions);
handler.resolve(retried);
} catch (e) {
_logout();
handler.reject(err);
} finally {
_isRefreshing = false;
}
} else {
handler.next(err);
}
}
}
// === SECURE TOKEN STORAGE ===
class TokenRepository {
static const _storage = FlutterSecureStorage(
aOptions: AndroidOptions(encryptedSharedPreferences: true),
iOptions: IOSOptions(accessibility: KeychainAccessibility.first_unlock),
);
Future<void> saveTokens({
required String access, required String refresh,
}) async {
await Future.wait([
_storage.write(key: 'access_token', value: access),
_storage.write(key: 'refresh_token', value: refresh),
]);
}
Future<bool> isTokenExpired() async {
final token = await _storage.read(key: 'access_token');
if (token == null) return true;
// Decode JWT payload (base64url) — no signature validation client-side
final parts = token.split('.');
final payload = json.decode(
utf8.decode(base64Url.decode(base64Url.normalize(parts[1])))
);
final exp = payload['exp'] as int;
return DateTime.now().millisecondsSinceEpoch / 1000 > exp;
}
}
// === BIOMETRIC AUTHENTICATION ===
class BiometricService {
final _localAuth = LocalAuthentication();
Future<bool> authenticate() async {
final canAuth = await _localAuth.canCheckBiometrics;
if (!canAuth) return false;
return _localAuth.authenticate(
localizedReason: 'Verify your identity to view claims',
options: const AuthenticationOptions(
biometricOnly: false, // Allow PIN fallback
stickyAuth: true, // Don't cancel on background
),
);
}
}
// === CERTIFICATE PINNING WITH CUSTOM HTTP CLIENT ===
HttpClient buildPinnedClient() {
final context = SecurityContext(withTrustedRoots: false);
// Only trust our server's certificate
context.setTrustedCertificatesBytes(
(await rootBundle.load('assets/certs/server.pem')).buffer.asUint8List(),
);
return HttpClient(context: context);
}
// === BUILD-TIME SECRET INJECTION ===
// flutter build apk --dart-define=API_KEY=prod_secret_here
const apiKey = String.fromEnvironment('API_KEY', defaultValue: '');Line-by-line walkthrough
- 1. AuthInterceptor.onError fires on every Dio error — checks for 401 before doing anything else
- 2. _isRefreshing flag prevents concurrent refresh calls when multiple requests fail simultaneously
- 3. storage.read('refresh_token') fetches from Android Keystore / iOS Keychain — hardware-backed encrypted storage
- 4. err.requestOptions.headers['Authorization'] = newToken — mutates the original request before retrying
- 5. FlutterSecureStorage AndroidOptions(encryptedSharedPreferences: true) uses EncryptedSharedPreferences — AES-256 backed by Keystore
- 6. parts[1] is the JWT payload segment — base64url decode it to read expiry without needing the signing key
- 7. local_auth biometricOnly: false allows PIN fallback — critical for accessibility and when biometrics fail
- 8. SecurityContext(withTrustedRoots: false) rejects ALL system CAs — only our pinned certificate is trusted
Spot the bug
// Token storage in onboarding flow
class OnboardingService {
Future<void> saveUserSession(String token, String userId) async {
final prefs = await SharedPreferences.getInstance();
await prefs.setString('access_token', token);
await prefs.setString('user_id', userId);
print('Session saved for user: $userId token: $token');
}
}Need a hint?
Show answer
Explain like I'm 5
Fun fact
Hands-on challenge
More resources
- flutter_secure_storage (pub.dev)
- local_auth package (pub.dev)
- OWASP Mobile Top 10 (2024) (OWASP)
- Android Keystore System (Android Docs)
- Flutter security best practices (Flutter Docs)