Authentication
FLEX provides an authentication system that supports OAuth2 flows, secure token storage, and dual authentication modes. The SAP Starter Kit implements a sophisticated authentication pattern that supports both authenticated users and anonymous browsing.
Overview
Section titled “Overview”The FLEX authentication system consists of several key components:
- AuthFeature: Base authentication feature for token management
- SapAuthFeature: SAP-specific implementation with dual authentication
- AuthManager: Handles token lifecycle and HTTP interceptors
- Token Storage: Secure storage for authentication tokens
- AuthBloc: State management for authentication flows
Architecture
Section titled “Architecture”Dual Authentication System
Section titled “Dual Authentication System”The SAP implementation supports two authentication modes:
- User Authentication: For logged-in users with full access
- Anonymous Authentication: For guest browsing with limited access
// User authentication flowawait sapAuthFeature.login( username: "user@example.com", password: "password123");
// Anonymous authentication (automatic)// Happens automatically when no user token is available
Token Management
Section titled “Token Management”FLEX uses OAuth2 tokens with automatic refresh capabilities:
class FlexOAuth2Token { final String accessToken; // Main authentication token final String? tokenType; // Usually "Bearer" final int? expiresIn; // Token expiration time final String? refreshToken; // For automatic token refresh final String? scope; // OAuth2 scope permissions}
Configuration
Section titled “Configuration”Basic Setup
Section titled “Basic Setup”Configure authentication in your main.dart
:
await FlexCore.initialize([ SapAuthFeature( tokenRefreshManager: RefreshManager(), ), // Other features...]);
Custom Authentication Feature
Section titled “Custom Authentication Feature”For non-SAP implementations, use the base AuthFeature
:
await FlexCore.initialize([ AuthFeature<FlexOAuth2Token>( tokenStorage: SecureOauth2Storage(), tokenRefreshManager: CustomRefreshManager(), headerBuilder: oAuth2HeaderBuilder, ),]);
Token Storage Options
Section titled “Token Storage Options”Secure Storage (Recommended)
Section titled “Secure Storage (Recommended)”// Uses encrypted device storagefinal storage = SecureOauth2Storage();
Custom Storage
Section titled “Custom Storage”// Implement your own storageclass CustomTokenStorage extends ITokenStore<FlexOAuth2Token> { @override Future<FlexOAuth2Token?> read() async { // Your custom storage logic }
@override Future<void> write(FlexOAuth2Token token) async { // Your custom storage logic }
@override Future<void> delete() async { // Your custom storage logic }}
Authentication State Management
Section titled “Authentication State Management”AuthBloc Integration
Section titled “AuthBloc Integration”The AuthBloc
manages authentication state throughout your app:
// In your app setupBlocProvider( create: (context) => AuthBloc()..add(AuthSubscribe()), lazy: false,),
Authentication States
Section titled “Authentication States”sealed class AuthState extends Equatable { const AuthState();
// Initial state - checking for existing tokens const factory AuthState.initial() = AuthStateInitial;
// No authenticated user - anonymous mode const factory AuthState.unauthenticated() = AuthStateUnauthenticated;
// User is authenticated const factory AuthState.authenticated(UserInfo user) = AuthStateAuthenticated;}
Listening to Auth Changes
Section titled “Listening to Auth Changes”BlocListener<AuthBloc, AuthState>( listener: (context, state) { switch (state) { case AuthStateInitial(): // Show loading indicator break; case AuthStateUnauthenticated(): // Handle anonymous user break; case AuthStateAuthenticated(): // Handle authenticated user final user = state.user; break; } }, child: YourWidget(),)
User Authentication
Section titled “User Authentication”Login Process
Section titled “Login Process”class LoginService { final SapAuthFeature _authFeature;
Future<void> login(String email, String password) async { try { await _authFeature.login( username: email, password: password, ); // Login successful - AuthBloc will update automatically } catch (e) { // Handle login error throw AuthException('Login failed: ${e.toString()}'); } }}
Logout Process
Section titled “Logout Process”class AuthService { final AuthBloc _authBloc;
void logout() { _authBloc.add(AuthLogout()); // This will: // 1. Clear user tokens // 2. Clear user data from storage // 3. Switch to anonymous authentication // 4. Update UI state }}
User Information
Section titled “User Information”The system provides a UserInfo
model for authenticated users:
class UserInfo extends Equatable { const UserInfo({ required this.id, required this.email, required this.name, });
final String id; final String email; final String name;
// Convenience methods static const empty = UserInfo(id: 'anonymous', email: '', name: ''); bool get isEmpty => this == UserInfo.empty; bool get isNotEmpty => this != UserInfo.empty;}
Anonymous Authentication
Section titled “Anonymous Authentication”Automatic Anonymous Tokens
Section titled “Automatic Anonymous Tokens”The system automatically handles anonymous authentication:
// This happens automatically in SapAuthFeature.initialize()final anonymousToken = await _anonymousAuthManager.tokenRefreshManager .tokenRefreshMethod(null, _anonymousAuthManager.dio);
await setAnonymousToken(anonymousToken);
Anonymous vs Authenticated Features
Section titled “Anonymous vs Authenticated Features”Use authentication state to control feature availability:
Widget buildShoppingFeatures(BuildContext context) { return BlocBuilder<AuthBloc, AuthState>( builder: (context, state) { switch (state) { case AuthStateAuthenticated(): return AuthenticatedShoppingView(user: state.user); case AuthStateUnauthenticated(): return AnonymousShoppingView(); case AuthStateInitial(): return LoadingView(); } }, );}
HTTP Integration
Section titled “HTTP Integration”Automatic Header Injection
Section titled “Automatic Header Injection”Authentication tokens are automatically added to HTTP requests:
// The auth feature automatically adds headers like:// Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Custom Header Builder
Section titled “Custom Header Builder”Customize how authentication headers are built:
HeaderBuilder<FlexOAuth2Token> customHeaderBuilder = (token) => { 'authorization': '${token.tokenType} ${token.accessToken}', 'x-api-version': '2.0', 'x-client-id': 'your-client-id',};
SapAuthFeature( headerBuilder: customHeaderBuilder, tokenRefreshManager: RefreshManager(),)
Request Interceptors
Section titled “Request Interceptors”The auth system uses Dio interceptors for automatic token management:
// Automatically intercepts requests to add auth headers// Handles token refresh on 401 responses// Switches between user and anonymous tokens as needed
Token Refresh
Section titled “Token Refresh”Automatic Token Refresh
Section titled “Automatic Token Refresh”Tokens are automatically refreshed when they expire:
class RefreshManager extends ITokenRefreshManager<FlexOAuth2Token> { @override Future<FlexOAuth2Token> tokenRefreshMethod( FlexOAuth2Token? currentToken, Dio dio, ) async { // Implement your token refresh logic final response = await dio.post('/oauth/refresh', data: { 'refresh_token': currentToken?.refreshToken, 'grant_type': 'refresh_token', });
return FlexOAuth2Token( accessToken: response.data['access_token'], refreshToken: response.data['refresh_token'], ); }}
Security Best Practices
Section titled “Security Best Practices”Secure Token Storage
Section titled “Secure Token Storage”Always use secure storage for tokens:
// ✅ Good - Uses encrypted storagefinal storage = SecureOauth2Storage();
// ❌ Avoid - Insecure storagefinal storage = SharedPreferencesTokenStorage(); // Don't do this
Token Validation
Section titled “Token Validation”Validate tokens before use:
class TokenValidator { static bool isTokenValid(FlexOAuth2Token? token) { if (token == null) return false; if (token.accessToken.isEmpty) return false;
// Check expiration if available if (token.expiresIn != null) { // Implement expiration check logic }
return true; }}
Error Handling
Section titled “Error Handling”Handle authentication errors gracefully:
class AuthErrorHandler { static void handleAuthError(dynamic error) { if (error is RevokeAuthTokenException) { // Force logout and clear tokens FlexCore.container.get<AuthBloc>().add(AuthLogout()); } else if (error is NetworkException) { // Handle network errors showRetryDialog(); } else { // Handle other auth errors showGenericError(); } }}
Advanced Usage
Section titled “Advanced Usage”Custom Authentication Flows
Section titled “Custom Authentication Flows”Implement custom authentication methods:
extension CustomAuth on SapAuthFeature { Future<void> loginWithBiometrics() async { // 1. Verify biometric authentication final isAuthenticated = await BiometricAuth.authenticate(); if (!isAuthenticated) throw BiometricException();
// 2. Retrieve stored credentials final credentials = await SecureStorage.getCredentials();
// 3. Login with stored credentials await login( username: credentials.username, password: credentials.password, ); }
Future<void> loginWithSocialProvider(String provider) async { // Implement social login flow final socialToken = await SocialAuth.authenticate(provider);
// Exchange social token for app token final response = await dio.post('/oauth/social', data: { 'provider': provider, 'social_token': socialToken, });
final token = FlexOAuth2Token( accessToken: response.data['access_token'], refreshToken: response.data['refresh_token'], );
await setUserToken(token); }}
Testing
Section titled “Testing”Mocking Authentication
Section titled “Mocking Authentication”Create test doubles for authentication:
class MockAuthFeature extends Mock implements SapAuthFeature { @override Stream<AuthStatus> get authenticationStatus => Stream.value(AuthStatus.authenticated);
@override Future<void> login({required String username, required String password}) async { // Mock implementation }
@override Future<void> logout() async { // Mock implementation }}
Authentication Tests
Section titled “Authentication Tests”group('AuthBloc Tests', () { late AuthBloc authBloc; late MockSapAuthFeature mockAuthFeature;
setUp(() { mockAuthFeature = MockSapAuthFeature(); authBloc = AuthBloc(sapAuthFeature: mockAuthFeature); });
test('should emit authenticated state on successful login', () async { // Arrange when(() => mockAuthFeature.authenticationStatus) .thenAnswer((_) => Stream.value(AuthStatus.authenticated));
// Act authBloc.add(AuthSubscribe());
// Assert expectLater( authBloc.stream, emits(isA<AuthStateAuthenticated>()), ); });});
Troubleshooting
Section titled “Troubleshooting”Common Issues
Section titled “Common Issues”Token Not Persisting:
// Ensure secure storage is properly configuredawait FlutterSecureStorage().write( key: 'test_key', value: 'test_value');
Auth State Not Updating:
// Ensure AuthBloc is subscribed to auth statusBlocProvider( create: (context) => AuthBloc()..add(AuthSubscribe()), lazy: false, // Important: don't lazy load),
Network Requests Not Authenticated:
// Verify auth interceptor is enabledfinal authFeature = FlexCore.container.get<SapAuthFeature>();authFeature.enableAuthInterceptor();
Anonymous to User Authentication Issues:
// Ensure proper cleanup when switching auth modesawait clearAnonymousData();await fetchUserData();
Debug Logging
Section titled “Debug Logging”Enable debug logging for authentication:
final logger = FlexCore.getLogger('AuthDebug');logger.debug('Current auth status: ${authFeature.authenticationStatus}');logger.debug('Token valid: ${token != null}');
The FLEX authentication system provides a robust foundation for secure user management in commerce applications, with built-in support for both authenticated and anonymous user flows.