Skip to content

Bond Authentication

Bond Authentication provides complete authentication flows with secure token management, form integration, route guarding, and social authentication. Build secure authentication systems with minimal boilerplate.

Why Bond Authentication?

Traditional Flutter authentication is complex and error-prone:

// ❌ Traditional approach - scattered and insecure
class AuthService {
  Future<User?> login(String email, String password) async {
    try {
      final response = await dio.post('/auth/login', data: {
        'email': email,
        'password': password,
      });

      // Manual response parsing
      if (response.statusCode == 200) {
        final userData = response.data['user'];
        final token = response.data['token'];

        // Insecure token storage
        final prefs = await SharedPreferences.getInstance();
        await prefs.setString('token', token);

        return User.fromJson(userData);
      }
    } catch (e) {
      // Generic error handling
      throw Exception('Login failed');
    }
    return null;
  }

  // Manual token refresh logic
  // Manual route guarding
  // No form validation integration
  // No social auth support
}

Bond Authentication provides a complete, secure solution:

// ✅ Bond Authentication approach - secure and integrated
class AuthServiceProvider extends ServiceProvider with ResponseDecoding {
  @override
  Future<void> register(GetIt it) async {
    it.registerLazySingleton(() => AuthApi(it()));
    it.registerLazySingleton(() => TokenManager(it()));
    it.registerLazySingleton(() => AuthGuard(it()));
  }

  @override
  Map<Type, JsonFactory> get factories => {
    User: User.fromJson,
    AuthResponse: AuthResponse.fromJson,
  };
}

// Integrated with Bond Forms
class LoginFormController extends AutoDisposeFormStateNotifier<AuthResponse, ApiError> {
  LoginFormController() : super(loginFormState);

  Future<void> login() async {
    final result = await submit(AuthService.login);
    result.fold(
      (error) => handleAuthError(error),
      (response) => handleAuthSuccess(response),
    );
  }
}

Quick Start

1. Setup Authentication Service Provider

// lib/providers/auth_service_provider.dart
class AuthServiceProvider extends ServiceProvider with ResponseDecoding {
  @override
  Future<void> register(GetIt it) async {
    // Core auth services
    it.registerLazySingleton(() => AuthApi(it()));
    it.registerLazySingleton(() => TokenManager(it()));
    it.registerLazySingleton(() => AuthGuard(it()));
    it.registerLazySingleton(() => BiometricAuth(it()));

    // Social auth providers
    it.registerLazySingleton(() => GoogleAuthProvider());
    it.registerLazySingleton(() => AppleAuthProvider());
    it.registerLazySingleton(() => FacebookAuthProvider());
  }

  @override
  Map<Type, JsonFactory> get factories => {
    User: User.fromJson,
    AuthResponse: AuthResponse.fromJson,
    RefreshTokenResponse: RefreshTokenResponse.fromJson,
  };
}

2. Define Authentication Models

// lib/features/auth/models/user.dart
class User extends Jsonable {
  final String id;
  final String email;
  final String name;
  final String? avatar;
  final UserRole role;
  final DateTime createdAt;
  final bool emailVerified;
  final bool twoFactorEnabled;

  User({
    required this.id,
    required this.email,
    required this.name,
    this.avatar,
    required this.role,
    required this.createdAt,
    required this.emailVerified,
    required this.twoFactorEnabled,
  });

  factory User.fromJson(Map<String, dynamic> json) => User(
    id: json['id'],
    email: json['email'],
    name: json['name'],
    avatar: json['avatar'],
    role: UserRole.fromString(json['role']),
    createdAt: DateTime.parse(json['created_at']),
    emailVerified: json['email_verified'] ?? false,
    twoFactorEnabled: json['two_factor_enabled'] ?? false,
  );

  @override
  Map<String, dynamic> toJson() => {
    'id': id,
    'email': email,
    'name': name,
    'avatar': avatar,
    'role': role.name,
    'created_at': createdAt.toIso8601String(),
    'email_verified': emailVerified,
    'two_factor_enabled': twoFactorEnabled,
  };

  bool get isPremium => role == UserRole.premium;
  bool get isAdmin => role == UserRole.admin;
}

// lib/features/auth/models/auth_response.dart
class AuthResponse extends Jsonable {
  final User user;
  final String accessToken;
  final String refreshToken;
  final DateTime expiresAt;

  AuthResponse({
    required this.user,
    required this.accessToken,
    required this.refreshToken,
    required this.expiresAt,
  });

  factory AuthResponse.fromJson(Map<String, dynamic> json) => AuthResponse(
    user: User.fromJson(json['user']),
    accessToken: json['access_token'],
    refreshToken: json['refresh_token'],
    expiresAt: DateTime.parse(json['expires_at']),
  );

  @override
  Map<String, dynamic> toJson() => {
    'user': user.toJson(),
    'access_token': accessToken,
    'refresh_token': refreshToken,
    'expires_at': expiresAt.toIso8601String(),
  };
}

3. Create Authentication Forms

// lib/features/auth/forms/login_form.dart
final loginFormState = BondFormState(fields: {
  'email': TextFieldState('', rules: [
    Rules.required(),
    Rules.email(),
  ]),
  'password': TextFieldState('', rules: [
    Rules.required(),
    Rules.minLength(8),
  ]),
  'remember_me': BooleanFieldState(false),
});

class LoginFormController extends AutoDisposeFormStateNotifier<AuthResponse, ApiError> {
  LoginFormController() : super(loginFormState);

  Future<void> login() async {
    final result = await submit((data) => 
      bondFire.post<AuthResponse>('/auth/login')
        .body(data.toJson())
        .factory(AuthResponse.fromJson)
        .errorFactory(ApiError.fromJson)
        .execute()
    );

    result.fold(
      (error) => _handleLoginError(error),
      (response) => _handleLoginSuccess(response),
    );
  }

  void _handleLoginError(ApiError error) {
    if (error.code == 'invalid_credentials') {
      setFieldError('password', 'Invalid email or password');
    } else if (error.code == 'account_locked') {
      showDialog('Account Locked', 'Your account has been temporarily locked due to multiple failed login attempts.');
    } else {
      showError(error.message);
    }
  }

  void _handleLoginSuccess(AuthResponse response) {
    TokenManager.saveTokens(response);
    AuthService.setCurrentUser(response.user);
    NavigationService.pushReplacementNamed('/home');

    // Track login event
    AppAnalytics.fire(LoginEvent(method: 'email'));
  }
}

// Registration form
final registerFormState = BondFormState(fields: {
  'name': TextFieldState('', rules: [
    Rules.required(),
    Rules.minLength(2),
  ]),
  'email': TextFieldState('', rules: [
    Rules.required(),
    Rules.email(),
  ]),
  'password': TextFieldState('', rules: [
    Rules.required(),
    Rules.minLength(8),
    Rules.containsUppercase(),
    Rules.containsNumber(),
    Rules.containsSpecialChar(),
  ]),
  'confirm_password': TextFieldState('', rules: [
    Rules.required(),
    Rules.same('password', message: 'Passwords must match'),
  ]),
  'terms_accepted': BooleanFieldState(false, rules: [
    Rules.mustBeTrue(message: 'You must accept the terms and conditions'),
  ]),
});

4. Build Authentication UI

// lib/features/auth/pages/login_page.dart
class LoginPage extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final formState = ref.watch(loginFormProvider);
    final controller = ref.read(loginFormProvider.notifier);

    return Scaffold(
      body: SafeArea(
        child: Padding(
          padding: EdgeInsets.all(24),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.stretch,
            children: [
              // Logo and title
              SizedBox(height: 60),
              Icon(Icons.lock_outline, size: 80, color: Theme.of(context).primaryColor),
              SizedBox(height: 24),
              Text(
                'Welcome Back',
                style: Theme.of(context).textTheme.headlineMedium,
                textAlign: TextAlign.center,
              ),
              SizedBox(height: 8),
              Text(
                'Sign in to your account',
                style: Theme.of(context).textTheme.bodyLarge?.copyWith(
                  color: Colors.grey[600],
                ),
                textAlign: TextAlign.center,
              ),
              SizedBox(height: 48),

              // Email field
              BondTextField(
                fieldName: 'email',
                state: formState,
                onChanged: controller.updateText,
                keyboardType: TextInputType.emailAddress,
                decoration: InputDecoration(
                  labelText: 'Email',
                  prefixIcon: Icon(Icons.email_outlined),
                  errorText: formState.getFieldError('email'),
                ),
              ),
              SizedBox(height: 16),

              // Password field
              BondTextField(
                fieldName: 'password',
                state: formState,
                onChanged: controller.updateText,
                obscureText: true,
                decoration: InputDecoration(
                  labelText: 'Password',
                  prefixIcon: Icon(Icons.lock_outlined),
                  errorText: formState.getFieldError('password'),
                ),
              ),
              SizedBox(height: 16),

              // Remember me and forgot password
              Row(
                children: [
                  BondCheckbox(
                    fieldName: 'remember_me',
                    state: formState,
                    onChanged: controller.updateBoolean,
                  ),
                  SizedBox(width: 8),
                  Text('Remember me'),
                  Spacer(),
                  TextButton(
                    onPressed: () => NavigationService.pushNamed('/forgot-password'),
                    child: Text('Forgot Password?'),
                  ),
                ],
              ),
              SizedBox(height: 32),

              // Login button
              ElevatedButton(
                onPressed: formState.isValid && !formState.isSubmitting 
                  ? controller.login 
                  : null,
                style: ElevatedButton.styleFrom(
                  padding: EdgeInsets.symmetric(vertical: 16),
                ),
                child: formState.isSubmitting
                  ? SizedBox(
                      height: 20,
                      width: 20,
                      child: CircularProgressIndicator(strokeWidth: 2),
                    )
                  : Text('Sign In'),
              ),
              SizedBox(height: 24),

              // Divider
              Row(
                children: [
                  Expanded(child: Divider()),
                  Padding(
                    padding: EdgeInsets.symmetric(horizontal: 16),
                    child: Text('or continue with'),
                  ),
                  Expanded(child: Divider()),
                ],
              ),
              SizedBox(height: 24),

              // Social login buttons
              Row(
                children: [
                  Expanded(
                    child: OutlinedButton.icon(
                      onPressed: () => controller.loginWithGoogle(),
                      icon: Icon(Icons.g_mobiledata),
                      label: Text('Google'),
                    ),
                  ),
                  SizedBox(width: 16),
                  Expanded(
                    child: OutlinedButton.icon(
                      onPressed: () => controller.loginWithApple(),
                      icon: Icon(Icons.apple),
                      label: Text('Apple'),
                    ),
                  ),
                ],
              ),
              SizedBox(height: 32),

              // Sign up link
              Row(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  Text("Don't have an account? "),
                  TextButton(
                    onPressed: () => NavigationService.pushNamed('/register'),
                    child: Text('Sign Up'),
                  ),
                ],
              ),
            ],
          ),
        ),
      ),
    );
  }
}

Token Management

Secure Token Storage

// lib/core/auth/token_manager.dart
class TokenManager {
  static const _accessTokenKey = 'access_token';
  static const _refreshTokenKey = 'refresh_token';
  static const _expiresAtKey = 'expires_at';

  static Future<void> saveTokens(AuthResponse response) async {
    final secureStorage = FlutterSecureStorage(
      aOptions: AndroidOptions(
        encryptedSharedPreferences: true,
      ),
      iOptions: IOSOptions(
        accessibility: IOSAccessibility.first_unlock_this_device,
      ),
    );

    await Future.wait([
      secureStorage.write(key: _accessTokenKey, value: response.accessToken),
      secureStorage.write(key: _refreshTokenKey, value: response.refreshToken),
      secureStorage.write(key: _expiresAtKey, value: response.expiresAt.toIso8601String()),
    ]);

    // Update HTTP client with new token
    AuthInterceptor.setAccessToken(response.accessToken);
  }

  static Future<String?> getAccessToken() async {
    final secureStorage = FlutterSecureStorage();
    return await secureStorage.read(key: _accessTokenKey);
  }

  static Future<String?> getRefreshToken() async {
    final secureStorage = FlutterSecureStorage();
    return await secureStorage.read(key: _refreshTokenKey);
  }

  static Future<DateTime?> getExpiresAt() async {
    final secureStorage = FlutterSecureStorage();
    final expiresAtStr = await secureStorage.read(key: _expiresAtKey);
    return expiresAtStr != null ? DateTime.parse(expiresAtStr) : null;
  }

  static Future<bool> isTokenValid() async {
    final expiresAt = await getExpiresAt();
    if (expiresAt == null) return false;

    // Add 5 minute buffer for token refresh
    return DateTime.now().isBefore(expiresAt.subtract(Duration(minutes: 5)));
  }

  static Future<void> clearTokens() async {
    final secureStorage = FlutterSecureStorage();
    await Future.wait([
      secureStorage.delete(key: _accessTokenKey),
      secureStorage.delete(key: _refreshTokenKey),
      secureStorage.delete(key: _expiresAtKey),
    ]);

    AuthInterceptor.clearAccessToken();
  }
}

Automatic Token Refresh

// lib/core/auth/auth_interceptor.dart
class AuthInterceptor extends Interceptor {
  static String? _accessToken;

  static void setAccessToken(String token) {
    _accessToken = token;
  }

  static void clearAccessToken() {
    _accessToken = null;
  }

  @override
  void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
    if (_accessToken != null) {
      options.headers['Authorization'] = 'Bearer $_accessToken';
    }
    handler.next(options);
  }

  @override
  void onError(DioException err, ErrorInterceptorHandler handler) async {
    if (err.response?.statusCode == 401) {
      // Token expired, try to refresh
      final refreshed = await _refreshToken();

      if (refreshed) {
        // Retry the original request
        final options = err.requestOptions;
        options.headers['Authorization'] = 'Bearer $_accessToken';

        try {
          final response = await Dio().fetch(options);
          handler.resolve(response);
          return;
        } catch (e) {
          // Refresh worked but retry failed, continue with error
        }
      } else {
        // Refresh failed, logout user
        AuthService.logout();
        NavigationService.pushNamedAndClearStack('/login');
      }
    }

    handler.next(err);
  }

  Future<bool> _refreshToken() async {
    try {
      final refreshToken = await TokenManager.getRefreshToken();
      if (refreshToken == null) return false;

      final response = await bondFire
          .post<AuthResponse>('/auth/refresh')
          .body({'refresh_token': refreshToken})
          .factory(AuthResponse.fromJson)
          .execute();

      await TokenManager.saveTokens(response);
      return true;
    } catch (e) {
      print('Token refresh failed: $e');
      return false;
    }
  }
}

Route Guarding

Authentication Guard

// lib/core/auth/auth_guard.dart
class AuthGuard {
  static Future<bool> isAuthenticated() async {
    final token = await TokenManager.getAccessToken();
    if (token == null) return false;

    return await TokenManager.isTokenValid();
  }

  static Future<bool> hasRole(UserRole requiredRole) async {
    final user = AuthService.currentUser;
    if (user == null) return false;

    return user.role.hasPermission(requiredRole);
  }

  static Future<bool> canAccess(String route) async {
    if (!await isAuthenticated()) return false;

    // Define route permissions
    final routePermissions = {
      '/admin': UserRole.admin,
      '/premium': UserRole.premium,
      '/settings': UserRole.user,
    };

    final requiredRole = routePermissions[route];
    if (requiredRole == null) return true;

    return await hasRole(requiredRole);
  }
}

// lib/core/navigation/app_router.dart
class AppRouter {
  static Route<dynamic> generateRoute(RouteSettings settings) {
    return MaterialPageRoute(
      builder: (context) => FutureBuilder<bool>(
        future: AuthGuard.canAccess(settings.name ?? ''),
        builder: (context, snapshot) {
          if (snapshot.connectionState == ConnectionState.waiting) {
            return LoadingPage();
          }

          final canAccess = snapshot.data ?? false;
          if (!canAccess) {
            return UnauthorizedPage(requestedRoute: settings.name);
          }

          return _buildPageForRoute(settings);
        },
      ),
    );
  }

  static Widget _buildPageForRoute(RouteSettings settings) {
    switch (settings.name) {
      case '/home':
        return HomePage();
      case '/profile':
        return ProfilePage();
      case '/admin':
        return AdminPage();
      case '/premium':
        return PremiumPage();
      default:
        return NotFoundPage();
    }
  }
}

Protected Widgets

// lib/core/auth/protected_widget.dart
class ProtectedWidget extends StatelessWidget {
  final Widget child;
  final UserRole? requiredRole;
  final Widget? fallback;
  final VoidCallback? onUnauthorized;

  const ProtectedWidget({
    Key? key,
    required this.child,
    this.requiredRole,
    this.fallback,
    this.onUnauthorized,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return FutureBuilder<bool>(
      future: _checkAccess(),
      builder: (context, snapshot) {
        if (snapshot.connectionState == ConnectionState.waiting) {
          return SizedBox.shrink();
        }

        final hasAccess = snapshot.data ?? false;
        if (!hasAccess) {
          onUnauthorized?.call();
          return fallback ?? SizedBox.shrink();
        }

        return child;
      },
    );
  }

  Future<bool> _checkAccess() async {
    if (!await AuthGuard.isAuthenticated()) return false;

    if (requiredRole != null) {
      return await AuthGuard.hasRole(requiredRole!);
    }

    return true;
  }
}

// Usage
ProtectedWidget(
  requiredRole: UserRole.premium,
  fallback: PremiumUpgradeCard(),
  child: PremiumFeatureWidget(),
)

Social Authentication

Google Authentication

// lib/core/auth/providers/google_auth_provider.dart
class GoogleAuthProvider {
  final GoogleSignIn _googleSignIn = GoogleSignIn(
    scopes: ['email', 'profile'],
  );

  Future<AuthResponse> signIn() async {
    try {
      final GoogleSignInAccount? googleUser = await _googleSignIn.signIn();
      if (googleUser == null) {
        throw AuthException('Google sign-in was cancelled');
      }

      final GoogleSignInAuthentication googleAuth = await googleUser.authentication;

      // Send Google token to your backend
      final response = await bondFire
          .post<AuthResponse>('/auth/google')
          .body({
            'access_token': googleAuth.accessToken,
            'id_token': googleAuth.idToken,
          })
          .factory(AuthResponse.fromJson)
          .execute();

      await TokenManager.saveTokens(response);
      AuthService.setCurrentUser(response.user);

      // Track social login
      AppAnalytics.fire(LoginEvent(method: 'google'));

      return response;
    } catch (e) {
      throw AuthException('Google sign-in failed: ${e.toString()}');
    }
  }

  Future<void> signOut() async {
    await _googleSignIn.signOut();
  }
}

Apple Authentication

// lib/core/auth/providers/apple_auth_provider.dart
class AppleAuthProvider {
  Future<AuthResponse> signIn() async {
    try {
      final credential = await SignInWithApple.getAppleIDCredential(
        scopes: [
          AppleIDAuthorizationScopes.email,
          AppleIDAuthorizationScopes.fullName,
        ],
      );

      // Send Apple credential to your backend
      final response = await bondFire
          .post<AuthResponse>('/auth/apple')
          .body({
            'identity_token': credential.identityToken,
            'authorization_code': credential.authorizationCode,
            'user_identifier': credential.userIdentifier,
            'email': credential.email,
            'given_name': credential.givenName,
            'family_name': credential.familyName,
          })
          .factory(AuthResponse.fromJson)
          .execute();

      await TokenManager.saveTokens(response);
      AuthService.setCurrentUser(response.user);

      // Track social login
      AppAnalytics.fire(LoginEvent(method: 'apple'));

      return response;
    } catch (e) {
      throw AuthException('Apple sign-in failed: ${e.toString()}');
    }
  }
}

Facebook Authentication

// lib/core/auth/providers/facebook_auth_provider.dart
class FacebookAuthProvider {
  Future<AuthResponse> signIn() async {
    try {
      final LoginResult result = await FacebookAuth.instance.login(
        permissions: ['email', 'public_profile'],
      );

      if (result.status != LoginStatus.success) {
        throw AuthException('Facebook login failed');
      }

      final AccessToken accessToken = result.accessToken!;

      // Send Facebook token to your backend
      final response = await bondFire
          .post<AuthResponse>('/auth/facebook')
          .body({
            'access_token': accessToken.token,
            'user_id': accessToken.userId,
          })
          .factory(AuthResponse.fromJson)
          .execute();

      await TokenManager.saveTokens(response);
      AuthService.setCurrentUser(response.user);

      // Track social login
      AppAnalytics.fire(LoginEvent(method: 'facebook'));

      return response;
    } catch (e) {
      throw AuthException('Facebook sign-in failed: ${e.toString()}');
    }
  }

  Future<void> signOut() async {
    await FacebookAuth.instance.logOut();
  }
}

Biometric Authentication

Setup Biometric Auth

// lib/core/auth/biometric_auth.dart
class BiometricAuth {
  final LocalAuthentication _localAuth = LocalAuthentication();

  Future<bool> isAvailable() async {
    final isAvailable = await _localAuth.canCheckBiometrics;
    final isDeviceSupported = await _localAuth.isDeviceSupported();
    return isAvailable && isDeviceSupported;
  }

  Future<List<BiometricType>> getAvailableBiometrics() async {
    return await _localAuth.getAvailableBiometrics();
  }

  Future<bool> authenticate({
    String reason = 'Please authenticate to access your account',
  }) async {
    try {
      final isAuthenticated = await _localAuth.authenticate(
        localizedReason: reason,
        options: AuthenticationOptions(
          biometricOnly: true,
          stickyAuth: true,
        ),
      );

      if (isAuthenticated) {
        // Track biometric auth success
        AppAnalytics.fire(BiometricAuthEvent(success: true));
      }

      return isAuthenticated;
    } catch (e) {
      print('Biometric authentication error: $e');
      AppAnalytics.fire(BiometricAuthEvent(
        success: false, 
        error: e.toString(),
      ));
      return false;
    }
  }

  Future<void> enableBiometricLogin() async {
    final user = AuthService.currentUser;
    if (user == null) return;

    // Authenticate first
    final authenticated = await authenticate(
      reason: 'Enable biometric login for your account',
    );

    if (authenticated) {
      // Store biometric preference
      await UserPreferences.setBiometricEnabled(true);

      // Update user profile
      await bondFire
          .patch<User>('/user/profile')
          .body({'biometric_enabled': true})
          .factory(User.fromJson)
          .execute();
    }
  }

  Future<void> disableBiometricLogin() async {
    await UserPreferences.setBiometricEnabled(false);

    await bondFire
        .patch<User>('/user/profile')
        .body({'biometric_enabled': false})
        .factory(User.fromJson)
        .execute();
  }
}

Biometric Login Flow

// lib/features/auth/controllers/biometric_login_controller.dart
class BiometricLoginController {
  static Future<bool> attemptBiometricLogin() async {
    final biometricAuth = sl<BiometricAuth>();

    // Check if biometric is available and enabled
    if (!await biometricAuth.isAvailable()) return false;
    if (!await UserPreferences.isBiometricEnabled()) return false;

    // Authenticate with biometrics
    final authenticated = await biometricAuth.authenticate(
      reason: 'Use your fingerprint to sign in',
    );

    if (authenticated) {
      // Get stored refresh token and refresh session
      final refreshToken = await TokenManager.getRefreshToken();
      if (refreshToken == null) return false;

      try {
        final response = await bondFire
            .post<AuthResponse>('/auth/refresh')
            .body({'refresh_token': refreshToken})
            .factory(AuthResponse.fromJson)
            .execute();

        await TokenManager.saveTokens(response);
        AuthService.setCurrentUser(response.user);

        // Track biometric login
        AppAnalytics.fire(LoginEvent(method: 'biometric'));

        return true;
      } catch (e) {
        print('Biometric login failed: $e');
        return false;
      }
    }

    return false;
  }
}

// Usage in login page
class LoginPage extends StatefulWidget {
  @override
  _LoginPageState createState() => _LoginPageState();
}

class _LoginPageState extends State<LoginPage> {
  @override
  void initState() {
    super.initState();
    _attemptBiometricLogin();
  }

  Future<void> _attemptBiometricLogin() async {
    final success = await BiometricLoginController.attemptBiometricLogin();
    if (success) {
      NavigationService.pushReplacementNamed('/home');
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      // Regular login UI with biometric option
      body: Column(
        children: [
          // Email/password fields...

          // Biometric login button
          FutureBuilder<bool>(
            future: sl<BiometricAuth>().isAvailable(),
            builder: (context, snapshot) {
              if (snapshot.data == true) {
                return OutlinedButton.icon(
                  onPressed: _attemptBiometricLogin,
                  icon: Icon(Icons.fingerprint),
                  label: Text('Use Biometric'),
                );
              }
              return SizedBox.shrink();
            },
          ),
        ],
      ),
    );
  }
}

Two-Factor Authentication

TOTP Setup

// lib/core/auth/two_factor_auth.dart
class TwoFactorAuth {
  static Future<String> generateSecret() async {
    final response = await bondFire
        .post<Map<String, dynamic>>('/auth/2fa/generate')
        .factory((json) => json)
        .execute();

    return response['secret'];
  }

  static Future<String> getQRCodeUrl(String secret) async {
    final user = AuthService.currentUser!;
    final appName = 'YourApp';

    return 'otpauth://totp/$appName:${user.email}?secret=$secret&issuer=$appName';
  }

  static Future<bool> verifyCode(String secret, String code) async {
    try {
      await bondFire
          .post<void>('/auth/2fa/verify')
          .body({
            'secret': secret,
            'code': code,
          })
          .execute();

      return true;
    } catch (e) {
      return false;
    }
  }

  static Future<void> enable(String secret, String code) async {
    await bondFire
        .post<void>('/auth/2fa/enable')
        .body({
          'secret': secret,
          'code': code,
        })
        .execute();
  }

  static Future<void> disable(String code) async {
    await bondFire
        .post<void>('/auth/2fa/disable')
        .body({'code': code})
        .execute();
  }

  static Future<List<String>> generateBackupCodes() async {
    final response = await bondFire
        .post<Map<String, dynamic>>('/auth/2fa/backup-codes')
        .factory((json) => json)
        .execute();

    return List<String>.from(response['codes']);
  }
}

2FA Setup UI

// lib/features/auth/pages/two_factor_setup_page.dart
class TwoFactorSetupPage extends StatefulWidget {
  @override
  _TwoFactorSetupPageState createState() => _TwoFactorSetupPageState();
}

class _TwoFactorSetupPageState extends State<TwoFactorSetupPage> {
  String? secret;
  String? qrCodeUrl;
  final codeController = TextEditingController();
  bool isLoading = false;

  @override
  void initState() {
    super.initState();
    _generateSecret();
  }

  Future<void> _generateSecret() async {
    setState(() => isLoading = true);

    try {
      secret = await TwoFactorAuth.generateSecret();
      qrCodeUrl = await TwoFactorAuth.getQRCodeUrl(secret!);
    } catch (e) {
      showError('Failed to generate 2FA secret');
    } finally {
      setState(() => isLoading = false);
    }
  }

  Future<void> _verifyAndEnable() async {
    if (secret == null || codeController.text.isEmpty) return;

    setState(() => isLoading = true);

    try {
      final isValid = await TwoFactorAuth.verifyCode(secret!, codeController.text);

      if (isValid) {
        await TwoFactorAuth.enable(secret!, codeController.text);

        // Generate backup codes
        final backupCodes = await TwoFactorAuth.generateBackupCodes();

        // Show backup codes dialog
        _showBackupCodesDialog(backupCodes);
      } else {
        showError('Invalid verification code');
      }
    } catch (e) {
      showError('Failed to enable 2FA');
    } finally {
      setState(() => isLoading = false);
    }
  }

  void _showBackupCodesDialog(List<String> codes) {
    showDialog(
      context: context,
      barrierDismissible: false,
      builder: (context) => AlertDialog(
        title: Text('Backup Codes'),
        content: Column(
          mainAxisSize: MainAxisSize.min,
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text('Save these backup codes in a secure place:'),
            SizedBox(height: 16),
            ...codes.map((code) => SelectableText(code)),
          ],
        ),
        actions: [
          TextButton(
            onPressed: () {
              Navigator.of(context).pop();
              Navigator.of(context).pop(true);
            },
            child: Text('I\'ve Saved Them'),
          ),
        ],
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Setup Two-Factor Authentication')),
      body: Padding(
        padding: EdgeInsets.all(24),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: [
            Text(
              'Scan the QR code below with your authenticator app:',
              style: Theme.of(context).textTheme.titleMedium,
            ),
            SizedBox(height: 24),

            // QR Code
            if (qrCodeUrl != null)
              Center(
                child: QrImageView(
                  data: qrCodeUrl!,
                  version: QrVersions.auto,
                  size: 200.0,
                ),
              ),

            SizedBox(height: 24),

            // Manual entry option
            Text('Or enter this code manually:'),
            SizedBox(height: 8),
            SelectableText(
              secret ?? '',
              style: TextStyle(
                fontFamily: 'monospace',
                fontSize: 16,
              ),
            ),
            SizedBox(height: 32),

            // Verification code input
            TextField(
              controller: codeController,
              decoration: InputDecoration(
                labelText: 'Verification Code',
                hintText: 'Enter 6-digit code from your app',
              ),
              keyboardType: TextInputType.number,
              maxLength: 6,
            ),
            SizedBox(height: 24),

            // Enable button
            ElevatedButton(
              onPressed: isLoading ? null : _verifyAndEnable,
              child: isLoading
                ? CircularProgressIndicator()
                : Text('Enable Two-Factor Authentication'),
            ),
          ],
        ),
      ),
    );
  }
}

Session Management

Session Monitoring

// lib/core/auth/session_manager.dart
class SessionManager {
  static Timer? _sessionTimer;
  static DateTime? _lastActivity;

  static void startSession() {
    _lastActivity = DateTime.now();
    _startSessionTimer();
  }

  static void updateActivity() {
    _lastActivity = DateTime.now();
  }

  static void _startSessionTimer() {
    _sessionTimer?.cancel();
    _sessionTimer = Timer.periodic(Duration(minutes: 1), (timer) {
      _checkSessionTimeout();
    });
  }

  static void _checkSessionTimeout() {
    if (_lastActivity == null) return;

    final sessionTimeout = Duration(minutes: 30); // Configurable
    final timeSinceLastActivity = DateTime.now().difference(_lastActivity!);

    if (timeSinceLastActivity > sessionTimeout) {
      _handleSessionTimeout();
    }
  }

  static void _handleSessionTimeout() {
    _sessionTimer?.cancel();
    AuthService.logout();

    showDialog(
      context: NavigationService.currentContext!,
      barrierDismissible: false,
      builder: (context) => AlertDialog(
        title: Text('Session Expired'),
        content: Text('Your session has expired. Please sign in again.'),
        actions: [
          TextButton(
            onPressed: () {
              Navigator.of(context).pop();
              NavigationService.pushNamedAndClearStack('/login');
            },
            child: Text('Sign In'),
          ),
        ],
      ),
    );
  }

  static void endSession() {
    _sessionTimer?.cancel();
    _lastActivity = null;
  }
}

// Usage in main app
class MyApp extends StatefulWidget {
  @override
  _MyAppState createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addObserver(this);
  }

  @override
  void dispose() {
    WidgetsBinding.instance.removeObserver(this);
    super.dispose();
  }

  @override
  void didChangeAppLifecycleState(AppLifecycleState state) {
    switch (state) {
      case AppLifecycleState.resumed:
        SessionManager.updateActivity();
        break;
      case AppLifecycleState.paused:
      case AppLifecycleState.inactive:
        // App backgrounded
        break;
      case AppLifecycleState.detached:
        SessionManager.endSession();
        break;
      default:
        break;
    }
  }

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: () => SessionManager.updateActivity(),
      onPanDown: (_) => SessionManager.updateActivity(),
      child: MaterialApp(
        // App configuration...
      ),
    );
  }
}

Testing

Authentication Testing

void main() {
  group('Authentication', () {
    late MockAuthApi mockAuthApi;
    late MockTokenManager mockTokenManager;

    setUp(() {
      mockAuthApi = MockAuthApi();
      mockTokenManager = MockTokenManager();

      GetIt.instance.registerSingleton<AuthApi>(mockAuthApi);
      GetIt.instance.registerSingleton<TokenManager>(mockTokenManager);
    });

    tearDown(() {
      GetIt.instance.reset();
    });

    test('should login successfully with valid credentials', () async {
      // Arrange
      final authResponse = AuthResponse(
        user: User(id: '123', email: 'test@example.com', name: 'Test User'),
        accessToken: 'access_token',
        refreshToken: 'refresh_token',
        expiresAt: DateTime.now().add(Duration(hours: 1)),
      );

      when(mockAuthApi.login(any, any)).thenAnswer((_) async => authResponse);

      // Act
      final result = await AuthService.login('test@example.com', 'password');

      // Assert
      expect(result.isRight(), true);
      result.fold(
        (error) => fail('Should not return error'),
        (response) => expect(response.user.email, 'test@example.com'),
      );

      verify(mockTokenManager.saveTokens(authResponse)).called(1);
    });

    test('should handle login failure correctly', () async {
      // Arrange
      when(mockAuthApi.login(any, any))
          .thenThrow(ApiError(message: 'Invalid credentials', code: 'invalid_credentials'));

      // Act
      final result = await AuthService.login('test@example.com', 'wrong_password');

      // Assert
      expect(result.isLeft(), true);
      result.fold(
        (error) => expect(error.code, 'invalid_credentials'),
        (response) => fail('Should not return success'),
      );
    });

    test('should refresh token automatically', () async {
      // Arrange
      when(mockTokenManager.getRefreshToken()).thenAnswer((_) async => 'refresh_token');
      when(mockTokenManager.isTokenValid()).thenAnswer((_) async => false);

      final newAuthResponse = AuthResponse(
        user: User(id: '123', email: 'test@example.com', name: 'Test User'),
        accessToken: 'new_access_token',
        refreshToken: 'new_refresh_token',
        expiresAt: DateTime.now().add(Duration(hours: 1)),
      );

      when(mockAuthApi.refreshToken('refresh_token'))
          .thenAnswer((_) async => newAuthResponse);

      // Act
      final result = await AuthService.refreshTokenIfNeeded();

      // Assert
      expect(result, true);
      verify(mockTokenManager.saveTokens(newAuthResponse)).called(1);
    });
  });
}

Best Practices

✅ Do's

// Use secure token storage
await TokenManager.saveTokens(authResponse);  // Uses FlutterSecureStorage

// Implement proper error handling
try {
  final result = await AuthService.login(email, password);
  result.fold(
    (error) => _handleSpecificError(error),
    (response) => _handleSuccess(response),
  );
} catch (e) {
  _handleUnexpectedError(e);
}

// Use form validation
final loginForm = BondFormState(fields: {
  'email': TextFieldState('', rules: [Rules.required(), Rules.email()]),
  'password': TextFieldState('', rules: [Rules.required(), Rules.minLength(8)]),
});

// Implement route guarding
@override
Widget build(BuildContext context) {
  return ProtectedWidget(
    requiredRole: UserRole.admin,
    fallback: UnauthorizedPage(),
    child: AdminDashboard(),
  );
}

// Track authentication events
AppAnalytics.fire(LoginEvent(method: 'email'));
AppAnalytics.fire(LogoutEvent());

❌ Don'ts

// Don't store tokens insecurely
SharedPreferences.getInstance().then((prefs) {
  prefs.setString('token', token);  // ❌ Insecure storage
});

// Don't ignore token expiration
final token = await getToken();
// Use token without checking expiration  // ❌ Token might be expired

// Don't handle all errors the same way
catch (e) {
  showError('Login failed');  // ❌ Generic error message
}

// Don't skip form validation
final email = emailController.text;  // ❌ No validation
final password = passwordController.text;
AuthService.login(email, password);

// Don't hardcode user roles
if (user.role == 'admin') {  // ❌ String comparison
  showAdminPanel();
}

Troubleshooting

Common Issues

Issue: Token refresh not working

// ❌ Problem: Not handling 401 responses properly
// ✅ Solution: Implement proper auth interceptor
class AuthInterceptor extends Interceptor {
  @override
  void onError(DioException err, ErrorInterceptorHandler handler) async {
    if (err.response?.statusCode == 401) {
      final refreshed = await _refreshToken();
      if (refreshed) {
        // Retry original request
        final response = await _retry(err.requestOptions);
        handler.resolve(response);
        return;
      }
    }
    handler.next(err);
  }
}

Issue: Biometric authentication not working

// ❌ Problem: Not checking availability
final authenticated = await localAuth.authenticate(...);

// ✅ Solution: Check availability first
if (await localAuth.canCheckBiometrics) {
  final authenticated = await localAuth.authenticate(...);
}

Issue: Social login failing

// ❌ Problem: Not handling cancellation
final googleUser = await GoogleSignIn().signIn();

// ✅ Solution: Handle null result
final googleUser = await GoogleSignIn().signIn();
if (googleUser == null) {
  throw AuthException('Sign-in was cancelled');
}

Next Steps

Bond Authentication provides a complete, secure authentication system with minimal setup. Start with basic email/password authentication and gradually add social login, biometrics, and 2FA as needed! 🚀