Flutter Authentication: JWT vs. Session Cookies for Mobile & Web Backends
The Flutter news you actually need
No spam, ever. Unsubscribe in one click.
When building a Flutter app that shares a backend with a web application, you face a classic architectural decision: how should your app authenticate users? The web side likely uses traditional session cookies, but mobile introduces different constraints. Do you retrofit cookie handling into your Flutter app, or switch the entire system to use JSON Web Tokens (JWT)? Let’s break down the practical differences and how to implement each approach in Flutter.
The Core Difference: Stateful vs. Stateless
Session Cookies are a stateful mechanism. When a user logs in, the server creates a session record (often in a database or cache) and sends its ID back as a cookie. Every subsequent request from the client includes this cookie. The server must look up the session ID to validate the request. This is straightforward for web browsers, which automatically handle cookie storage and transmission.
JWT (JSON Web Tokens) are stateless. The server generates a signed token containing user claims (like user ID and permissions) and sends it to the client. The client stores this token (often in secure_store for Flutter) and sends it back, typically in an Authorization header. The server can validate the token’s signature without querying a database, making it highly scalable.
Why This Matters for Flutter
Flutter apps don’t behave like browsers. They don’t automatically send or receive cookies with HTTP requests unless you explicitly configure them to do so. This makes JWT’s header-based approach feel more native to mobile development. However, if your backend already uses sessions for the web, you might want to maintain a single auth system.
Implementing Cookie-Based Authentication in Flutter
To use your existing session backend, you need to manually handle cookies. The http package can manage a cookie jar, but the dio package makes it simpler.
First, add dio and dio_cookie_manager to your pubspec.yaml:
dependencies:
dio: ^5.4.0
dio_cookie_manager: ^2.0.0
cookie_jar: ^2.0.0
Here’s a basic setup for a login flow that preserves cookies:
import 'package:dio/dio.dart';
import 'package:dio_cookie_manager/dio_cookie_manager.dart';
import 'package:cookie_jar/cookie_jar.dart';
class SessionAuthClient {
late Dio dio;
late CookieJar cookieJar;
SessionAuthClient() {
cookieJar = CookieJar();
dio = Dio(BaseOptions(baseUrl: 'https://your-api.com'));
// Add the cookie manager to interceptors
dio.interceptors.add(CookieManager(cookieJar));
}
Future<bool> login(String email, String password) async {
try {
final response = await dio.post(
'/login',
data: {'email': email, 'password': password},
);
// Cookies are automatically saved by CookieManager
return response.statusCode == 200;
} on DioException catch (e) {
print('Login failed: ${e.message}');
return false;
}
}
Future<Response> getProtectedData() async {
// The saved cookies are automatically attached to this request
return await dio.get('/user/profile');
}
}
The key is the CookieManager interceptor, which automatically stores cookies from responses and attaches them to subsequent requests, mimicking a browser. You must persist the cookie jar across app restarts, which requires a persistent storage location like path_provider.
Implementing JWT Authentication in Flutter
With JWT, you explicitly manage the token. You’ll typically store it securely and add it to request headers.
Add dio and flutter_secure_storage:
dependencies:
dio: ^5.4.0
flutter_secure_storage: ^9.0.0
Here’s a typical JWT client implementation:
import 'package:dio/dio.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
class JWTAuthClient {
late Dio dio;
final FlutterSecureStorage _storage = const FlutterSecureStorage();
static const String _accessTokenKey = 'access_token';
static const String _refreshTokenKey = 'refresh_token';
JWTAuthClient() {
dio = Dio(BaseOptions(baseUrl: 'https://your-api.com'));
// Add an interceptor to attach the token
dio.interceptors.add(InterceptorsWrapper(
onRequest: (options, handler) async {
final token = await _storage.read(key: _accessTokenKey);
if (token != null) {
options.headers['Authorization'] = 'Bearer $token';
}
handler.next(options);
},
onError: (error, handler) async {
// Implement token refresh on 401 errors
if (error.response?.statusCode == 401) {
final refreshed = await _refreshToken();
if (refreshed) {
// Retry the original request
return handler.resolve(await dio.fetch(error.requestOptions));
}
}
handler.next(error);
},
));
}
Future<bool> login(String email, String password) async {
try {
final response = await dio.post(
'/auth/login',
data: {'email': email, 'password': password},
);
await _storage.write(
key: _accessTokenKey, value: response.data['access_token']);
await _storage.write(
key: _refreshTokenKey, value: response.data['refresh_token']);
return true;
} on DioException {
return false;
}
}
Future<bool> _refreshToken() async {
try {
final refreshToken = await _storage.read(key: _refreshTokenKey);
final response = await dio.post('/auth/refresh', data: {
'refresh_token': refreshToken,
});
await _storage.write(
key: _accessTokenKey, value: response.data['access_token']);
return true;
} catch (e) {
// Refresh failed, user must log in again
await logout();
return false;
}
}
Future<void> logout() async {
await _storage.delete(key: _accessTokenKey);
await _storage.delete(key: _refreshTokenKey);
}
}
Which Should You Choose? Practical Guidance
- Stick with Sessions if: Your backend is already built around sessions, and you want minimal backend changes. The Flutter implementation is manageable with
dio_cookie_manager. Be mindful of cross-origin issues if your API and Flutter app use different domains. - Choose JWT if: You’re building a new system or can modify the backend. JWTs are excellent for scalability (no session store to query) and work seamlessly with native mobile patterns. They also simplify scenarios where your Flutter app talks to multiple backend services, as each service can independently validate the token.
- The Hybrid Approach: Some backends support both. The web app uses sessions, while the mobile API accepts a JWT sent in an
Authorizationheader. This keeps both clients happy but adds complexity on the server.
Common Mistakes to Avoid
- Storing JWTs insecurely: Never store a JWT in
shared_preferencesor as a plain file. Always useflutter_secure_storagefor tokens. - Forgetting to persist cookies: The default
CookieJaris in-memory. Use aPersistCookieJarwithpath_providerto save cookies across app restarts. - Ignoring token expiration: Always implement a token refresh flow for JWTs. The interceptor pattern shown above is a robust way to handle 401 errors silently.
- Not securing your backend: Regardless of your choice, always use HTTPS. For sessions, ensure cookies are marked
SecureandHttpOnly. For JWTs, keep them short-lived and use strong signing algorithms.
Ultimately, both methods are secure when implemented correctly. Your decision should hinge on your backend’s existing architecture, your team’s expertise, and the long-term scalability requirements of your application. Flutter is flexible enough to handle either pattern effectively.
This blog is produced with the assistance of AI by a human editor. Learn more
Related Posts
Localizing Dynamic Content in Flutter: A Guide to Backend-Driven Translations
Many Flutter apps need to display content that changes based on user locale, but also comes from a backend (like Firebase). This post will explore best practices for fetching and integrating dynamic, localized content from a backend, ensuring a seamless user experience across different languages and regions without hardcoding translations.
Unraveling Type Mismatch Errors in Flutter: A Guide to 'X can't be assigned to Y' and '_InternalLinkedHashMap' Issues
Developers frequently encounter cryptic type mismatch errors like 'The argument type X can't be assigned to the parameter type Y' or '_InternalLinkedHashMap has no instance method 'cast''. This post will demystify these common Flutter/Dart type errors, explain their root causes (e.g., conflicting imports, dynamic typing pitfalls, JSON deserialization issues), and provide practical solutions to diagnose and fix them, improving code robustness and reducing debugging time.
Solving Flutter Web Memory Leaks: A Practical Guide to Identifying and Fixing Performance Issues
Flutter Web applications can suffer from increasing memory usage over time, leading to performance degradation. This post will delve into common causes of memory leaks in Flutter Web, provide practical debugging techniques using browser developer tools and Dart DevTools, and offer actionable strategies to identify and fix these issues for a smoother user experience.