10 KiB
Authentication Feature
Complete JWT-based authentication system for the Retail POS app.
Overview
This feature implements a production-ready authentication system with:
- User login with email/password
- User registration
- JWT token management with secure storage
- Automatic token injection in API requests
- Profile management
- Token refresh capability
- Proper error handling
Architecture
The authentication feature follows Clean Architecture principles:
auth/
├── domain/ # Business logic layer
│ ├── entities/ # Core business objects
│ │ ├── user.dart
│ │ └── auth_response.dart
│ └── repositories/ # Repository interfaces
│ └── auth_repository.dart
│
├── data/ # Data layer
│ ├── models/ # Data transfer objects and models
│ │ ├── login_dto.dart
│ │ ├── register_dto.dart
│ │ ├── user_model.dart
│ │ └── auth_response_model.dart
│ ├── datasources/ # Remote data sources
│ │ └── auth_remote_datasource.dart
│ └── repositories/ # Repository implementations
│ └── auth_repository_impl.dart
│
└── presentation/ # UI layer
├── providers/ # Riverpod state management
│ └── auth_provider.dart
├── pages/ # UI screens
│ ├── login_page.dart
│ └── register_page.dart
└── widgets/ # Reusable UI components
API Endpoints
Base URL: http://localhost:3000/api
1. Login
POST /auth/login
Content-Type: application/json
Request:
{
"email": "user@example.com",
"password": "Password123!"
}
Response (200):
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"user": {
"id": "uuid",
"name": "John Doe",
"email": "user@example.com",
"roles": ["user"],
"isActive": true,
"createdAt": "2025-01-01T00:00:00.000Z",
"updatedAt": "2025-01-01T00:00:00.000Z"
}
}
2. Register
POST /auth/register
Content-Type: application/json
Request:
{
"name": "John Doe",
"email": "user@example.com",
"password": "Password123!",
"roles": ["user"]
}
Response (201):
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"user": { ... }
}
3. Get Profile
GET /auth/profile
Authorization: Bearer {token}
Response (200):
{
"id": "uuid",
"name": "John Doe",
"email": "user@example.com",
"roles": ["user"],
"isActive": true,
"createdAt": "2025-01-01T00:00:00.000Z",
"updatedAt": "2025-01-01T00:00:00.000Z"
}
4. Refresh Token
POST /auth/refresh
Authorization: Bearer {token}
Response (200):
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"user": { ... }
}
Usage
1. Setup (Already configured in injection_container.dart)
The authentication system is registered in the dependency injection container:
// In lib/core/di/injection_container.dart
Future<void> initDependencies() async {
// Secure Storage
sl.registerLazySingleton<SecureStorage>(() => SecureStorage());
// Auth Remote Data Source
sl.registerLazySingleton<AuthRemoteDataSource>(
() => AuthRemoteDataSourceImpl(dioClient: sl()),
);
// Auth Repository
sl.registerLazySingleton<AuthRepository>(
() => AuthRepositoryImpl(
remoteDataSource: sl(),
secureStorage: sl(),
dioClient: sl(),
),
);
}
2. Login Flow
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:retail/features/auth/presentation/providers/auth_provider.dart';
// In a widget
class MyWidget extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
return ElevatedButton(
onPressed: () async {
final success = await ref.read(authProvider.notifier).login(
email: 'user@example.com',
password: 'Password123!',
);
if (success) {
// Navigate to home
} else {
// Show error
final error = ref.read(authProvider).errorMessage;
print('Login failed: $error');
}
},
child: Text('Login'),
);
}
}
3. Check Authentication Status
// Check if user is authenticated
final isAuthenticated = ref.watch(isAuthenticatedProvider);
// Get current user
final user = ref.watch(currentUserProvider);
if (user != null) {
print('Welcome ${user.name}!');
print('Roles: ${user.roles}');
print('Is Admin: ${user.isAdmin}');
}
4. Logout
await ref.read(authProvider.notifier).logout();
5. Register New User
final success = await ref.read(authProvider.notifier).register(
name: 'John Doe',
email: 'john@example.com',
password: 'Password123!',
roles: ['user'], // Optional, defaults to ['user']
);
6. Refresh Token
final success = await ref.read(authProvider.notifier).refreshToken();
if (!success) {
// Token refresh failed, user logged out automatically
}
Bearer Token Injection
The authentication system automatically injects the JWT bearer token into all API requests:
How it Works
- On Login/Register: Token is saved to secure storage and set in DioClient
- Automatic Injection: DioClient interceptor adds
Authorization: Bearer {token}header to all requests - On Logout: Token is cleared from secure storage and DioClient
- On App Start: Token is loaded from secure storage and set in DioClient if valid
Implementation Details
// In lib/core/network/dio_client.dart
class DioClient {
String? _authToken;
DioClient() {
_dio.interceptors.add(
InterceptorsWrapper(
onRequest: (options, handler) {
if (_authToken != null) {
options.headers['Authorization'] = 'Bearer $_authToken';
}
return handler.next(options);
},
),
);
}
void setAuthToken(String token) {
_authToken = token;
}
void clearAuthToken() {
_authToken = null;
}
}
Manual Token Management (Advanced)
If you need to manually manage tokens:
// Get token from storage
final token = await sl<SecureStorage>().getAccessToken();
// Set token in DioClient
sl<DioClient>().setAuthToken(token!);
// Clear token
sl<DioClient>().clearAuthToken();
await sl<SecureStorage>().deleteAllTokens();
Error Handling
The authentication system handles various error scenarios:
Error Types
- InvalidCredentialsFailure: Wrong email or password
- ConflictFailure: Email already exists (registration)
- UnauthorizedFailure: Invalid or expired token
- NetworkFailure: No internet connection
- ServerFailure: Server errors (500, 503, etc.)
- ValidationFailure: Invalid input data
Error Handling Example
final success = await ref.read(authProvider.notifier).login(
email: email,
password: password,
);
if (!success) {
final error = ref.read(authProvider).errorMessage;
// Show user-friendly error message
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(error ?? 'Login failed'),
backgroundColor: Colors.red,
),
);
}
Secure Storage
JWT tokens are stored securely using flutter_secure_storage:
- iOS: Keychain
- Android: EncryptedSharedPreferences
- Web: Web Crypto API
- Desktop: Platform-specific secure storage
// Access secure storage
final secureStorage = sl<SecureStorage>();
// Save token
await secureStorage.saveAccessToken(token);
// Get token
final token = await secureStorage.getAccessToken();
// Check if token exists
final hasToken = await secureStorage.hasAccessToken();
// Delete token
await secureStorage.deleteAllTokens();
State Management
The authentication state is managed using Riverpod 3.0:
AuthState
class AuthState {
final User? user; // Current authenticated user
final bool isAuthenticated; // Authentication status
final bool isLoading; // Loading state
final String? errorMessage; // Error message (if any)
}
Watching Auth State
// Watch entire auth state
final authState = ref.watch(authProvider);
// Access specific properties
final user = authState.user;
final isLoading = authState.isLoading;
final error = authState.errorMessage;
// Or use convenience providers
final isAuthenticated = ref.watch(isAuthenticatedProvider);
final currentUser = ref.watch(currentUserProvider);
Protected Routes
Implement route guards to protect authenticated routes:
class AuthGuard extends ConsumerWidget {
final Widget child;
const AuthGuard({required this.child});
@override
Widget build(BuildContext context, WidgetRef ref) {
final isAuthenticated = ref.watch(isAuthenticatedProvider);
final isLoading = ref.watch(authProvider.select((s) => s.isLoading));
if (isLoading) {
return const Scaffold(
body: Center(child: CircularProgressIndicator()),
);
}
if (!isAuthenticated) {
return const LoginPage();
}
return child;
}
}
// Usage
MaterialApp(
home: AuthGuard(
child: HomePage(),
),
);
Testing
Unit Tests
test('login should return success with valid credentials', () async {
// Arrange
final repository = MockAuthRepository();
when(repository.login(email: any, password: any))
.thenAnswer((_) async => Right(mockAuthResponse));
// Act
final result = await repository.login(
email: 'test@example.com',
password: 'Password123!',
);
// Assert
expect(result.isRight(), true);
});
Integration Tests
Test the complete authentication flow from UI to API.
Code Generation
Run code generation for Riverpod providers:
# Generate auth_provider.g.dart
flutter pub run build_runner build --delete-conflicting-outputs
# Watch for changes
flutter pub run build_runner watch
Production Considerations
- HTTPS: Always use HTTPS in production
- Token Expiry: Implement automatic token refresh on 401 errors
- Biometric Auth: Add fingerprint/face ID support
- Password Strength: Enforce strong password requirements
- Rate Limiting: Handle rate limiting (429 errors)
- Secure Storage: Tokens are already stored securely
- Session Management: Clear tokens on app uninstall
Future Enhancements
- Biometric authentication (fingerprint, face ID)
- Remember me functionality
- Password reset flow
- Email verification
- Social login (Google, Apple, Facebook)
- Multi-factor authentication (MFA)
- Session timeout warning
- Device management