Skip to content

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.

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

The SAP implementation supports two authentication modes:

  1. User Authentication: For logged-in users with full access
  2. Anonymous Authentication: For guest browsing with limited access
// User authentication flow
await sapAuthFeature.login(
username: "user@example.com",
password: "password123"
);
// Anonymous authentication (automatic)
// Happens automatically when no user token is available

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
}

Configure authentication in your main.dart:

await FlexCore.initialize([
SapAuthFeature(
tokenRefreshManager: RefreshManager(),
),
// Other features...
]);

For non-SAP implementations, use the base AuthFeature:

await FlexCore.initialize([
AuthFeature<FlexOAuth2Token>(
tokenStorage: SecureOauth2Storage(),
tokenRefreshManager: CustomRefreshManager(),
headerBuilder: oAuth2HeaderBuilder,
),
]);
// Uses encrypted device storage
final storage = SecureOauth2Storage();
// Implement your own storage
class 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
}
}

The AuthBloc manages authentication state throughout your app:

// In your app setup
BlocProvider(
create: (context) => AuthBloc()..add(AuthSubscribe()),
lazy: false,
),
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;
}
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(),
)
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()}');
}
}
}
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
}
}

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;
}

The system automatically handles anonymous authentication:

// This happens automatically in SapAuthFeature.initialize()
final anonymousToken = await _anonymousAuthManager.tokenRefreshManager
.tokenRefreshMethod(null, _anonymousAuthManager.dio);
await setAnonymousToken(anonymousToken);

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();
}
},
);
}

Authentication tokens are automatically added to HTTP requests:

// The auth feature automatically adds headers like:
// Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

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(),
)

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

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'],
);
}
}

Always use secure storage for tokens:

// ✅ Good - Uses encrypted storage
final storage = SecureOauth2Storage();
// ❌ Avoid - Insecure storage
final storage = SharedPreferencesTokenStorage(); // Don't do this

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;
}
}

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();
}
}
}

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);
}
}

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
}
}
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>()),
);
});
});

Token Not Persisting:

// Ensure secure storage is properly configured
await FlutterSecureStorage().write(
key: 'test_key',
value: 'test_value'
);

Auth State Not Updating:

// Ensure AuthBloc is subscribed to auth status
BlocProvider(
create: (context) => AuthBloc()..add(AuthSubscribe()),
lazy: false, // Important: don't lazy load
),

Network Requests Not Authenticated:

// Verify auth interceptor is enabled
final authFeature = FlexCore.container.get<SapAuthFeature>();
authFeature.enableAuthInterceptor();

Anonymous to User Authentication Issues:

// Ensure proper cleanup when switching auth modes
await clearAnonymousData();
await fetchUserData();

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.