Flutter Integration

Flutter Integration

This guide covers how to integrate Skycloak authentication into Flutter applications using flutter_appauth and modern authentication patterns.

Prerequisites

  • Flutter 3.19+ SDK (Flutter 3.24+ recommended)
  • Dart 3.3+
  • Skycloak cluster with configured realm and mobile client
  • Android Studio / Xcode for platform-specific configuration
  • Basic understanding of Flutter and Dart
⚠️

Flutter 3.22.0+ Breaking Change

If you’re using Flutter 3.22.0 or later, you may encounter authentication issues where the app opens a new browser window instead of returning to the app after login. This is due to a change in how Flutter handles task affinity.

Fix: Add the following to your android/app/src/main/AndroidManifest.xml inside the <activity> tag:

<activity
    android:name=".MainActivity"
    android:launchMode="singleTop"
    android:taskAffinity=""
    ...>

See the flutter_appauth issue #430 for more details.

Quick Start

1. Add Dependencies

Update your pubspec.yaml:

dependencies:
  flutter:
    sdk: flutter

  # Authentication
  flutter_appauth: ^7.0.1
  flutter_secure_storage: ^9.2.2

  # HTTP & JSON
  http: ^1.2.2
  dio: ^5.7.0
  json_annotation: ^4.9.0

  # State Management (choose one)
  provider: ^6.1.2
  riverpod: ^2.6.1
  bloc: ^8.1.4

  # Utilities
  jwt_decoder: ^2.0.1
  url_launcher: ^6.3.1
  connectivity_plus: ^6.1.0

dev_dependencies:
  flutter_test:
    sdk: flutter
  build_runner: ^2.4.13
  json_serializable: ^6.8.0
  flutter_lints: ^4.0.0

2. Platform Configuration

Android Configuration

Update android/app/build.gradle:

android {
    defaultConfig {
        applicationId "com.yourcompany.app"
        minSdkVersion 21
        targetSdkVersion 33
        
        manifestPlaceholders += [
            'appAuthRedirectScheme': 'com.yourcompany.app'
        ]
    }
}

Update android/app/src/main/AndroidManifest.xml:

<manifest>
    <application>
        <!-- Your existing configuration -->
        
        <!-- Add for AppAuth -->
        <activity
            android:name="net.openid.appauth.RedirectUriReceiverActivity"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.VIEW"/>
                <category android:name="android.intent.category.DEFAULT"/>
                <category android:name="android.intent.category.BROWSABLE"/>
                <data android:scheme="com.yourcompany.app"/>
            </intent-filter>
        </activity>
    </application>
</manifest>

iOS Configuration

Update ios/Runner/Info.plist:

<key>CFBundleURLTypes</key>
<array>
    <dict>
        <key>CFBundleURLSchemes</key>
        <array>
            <string>com.yourcompany.app</string>
        </array>
        <key>CFBundleURLName</key>
        <string>com.yourcompany.app</string>
    </dict>
</array>

3. Create Auth Configuration

// lib/config/auth_config.dart
class AuthConfig {
  static const String issuer = 'https://your-cluster-id.app.skycloak.io/realms/your-realm';
  static const String clientId = 'your-flutter-app';
  static const String redirectUrl = 'com.yourcompany.app://oauth/callback';
  static const String postLogoutRedirectUrl = 'com.yourcompany.app://oauth/logout';
  static const List<String> scopes = ['openid', 'profile', 'email', 'offline_access'];
  
  // Discovery document URL
  static const String discoveryUrl = '$issuer/.well-known/openid-configuration';
  
  // Additional endpoints if needed
  static const String authorizationEndpoint = '$issuer/protocol/openid-connect/auth';
  static const String tokenEndpoint = '$issuer/protocol/openid-connect/token';
  static const String userInfoEndpoint = '$issuer/protocol/openid-connect/userinfo';
  static const String endSessionEndpoint = '$issuer/protocol/openid-connect/logout';
  
  // PKCE is required for public clients
  static const bool usePKCE = true;
  
  // Prompt for login
  static const Map<String, String> additionalParameters = {
    'prompt': 'login',
  };
}

4. Create Auth Service

// lib/services/auth_service.dart
import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:flutter_appauth/flutter_appauth.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:jwt_decoder/jwt_decoder.dart';
import '../config/auth_config.dart';
import '../models/user_info.dart';

class AuthService extends ChangeNotifier {
  static final AuthService _instance = AuthService._internal();
  factory AuthService() => _instance;
  AuthService._internal();

  final FlutterAppAuth _appAuth = const FlutterAppAuth();
  final FlutterSecureStorage _secureStorage = const FlutterSecureStorage();
  
  // Storage keys
  static const String _accessTokenKey = 'access_token';
  static const String _refreshTokenKey = 'refresh_token';
  static const String _idTokenKey = 'id_token';
  
  // Auth state
  bool _isAuthenticated = false;
  UserInfo? _userInfo;
  String? _error;
  bool _isLoading = false;
  
  // Getters
  bool get isAuthenticated => _isAuthenticated;
  UserInfo? get userInfo => _userInfo;
  String? get error => _error;
  bool get isLoading => _isLoading;
  
  // Initialize auth state
  Future<void> init() async {
    _setLoading(true);
    try {
      final accessToken = await _secureStorage.read(key: _accessTokenKey);
      if (accessToken != null && !JwtDecoder.isExpired(accessToken)) {
        final idToken = await _secureStorage.read(key: _idTokenKey);
        if (idToken != null) {
          _userInfo = _parseUserInfo(idToken);
          _isAuthenticated = true;
        }
      } else if (accessToken != null) {
        // Token expired, try to refresh
        await _refreshToken();
      }
    } catch (e) {
      debugPrint('Failed to initialize auth: $e');
    } finally {
      _setLoading(false);
    }
  }
  
  // Login
  Future<void> login() async {
    _setLoading(true);
    _clearError();
    
    try {
      final AuthorizationTokenResponse? result = await _appAuth.authorizeAndExchangeCode(
        AuthorizationTokenRequest(
          AuthConfig.clientId,
          AuthConfig.redirectUrl,
          serviceConfiguration: const AuthorizationServiceConfiguration(
            authorizationEndpoint: AuthConfig.authorizationEndpoint,
            tokenEndpoint: AuthConfig.tokenEndpoint,
            endSessionEndpoint: AuthConfig.endSessionEndpoint,
          ),
          scopes: AuthConfig.scopes,
          promptValues: ['login'],
          additionalParameters: AuthConfig.additionalParameters,
        ),
      );
      
      if (result != null) {
        await _handleAuthResult(result);
      }
    } catch (e) {
      _setError('Authentication failed: ${e.toString()}');
      rethrow;
    } finally {
      _setLoading(false);
    }
  }
  
  // Logout
  Future<void> logout() async {
    _setLoading(true);
    
    try {
      final idToken = await _secureStorage.read(key: _idTokenKey);
      
      if (idToken != null) {
        try {
          await _appAuth.endSession(
            EndSessionRequest(
              idTokenHint: idToken,
              postLogoutRedirectUrl: AuthConfig.postLogoutRedirectUrl,
              serviceConfiguration: const AuthorizationServiceConfiguration(
                authorizationEndpoint: AuthConfig.authorizationEndpoint,
                tokenEndpoint: AuthConfig.tokenEndpoint,
                endSessionEndpoint: AuthConfig.endSessionEndpoint,
              ),
            ),
          );
        } catch (e) {
          debugPrint('End session failed: $e');
        }
      }
    } finally {
      await _clearAuthState();
      _setLoading(false);
    }
  }
  
  // Get access token
  Future<String?> getAccessToken() async {
    final token = await _secureStorage.read(key: _accessTokenKey);
    
    if (token != null && JwtDecoder.isExpired(token)) {
      // Token expired, refresh it
      await _refreshToken();
      return await _secureStorage.read(key: _accessTokenKey);
    }
    
    return token;
  }
  
  // Refresh token
  Future<void> _refreshToken() async {
    final refreshToken = await _secureStorage.read(key: _refreshTokenKey);
    
    if (refreshToken == null) {
      await _clearAuthState();
      return;
    }
    
    try {
      final TokenResponse? result = await _appAuth.token(
        TokenRequest(
          AuthConfig.clientId,
          AuthConfig.redirectUrl,
          serviceConfiguration: const AuthorizationServiceConfiguration(
            authorizationEndpoint: AuthConfig.authorizationEndpoint,
            tokenEndpoint: AuthConfig.tokenEndpoint,
          ),
          refreshToken: refreshToken,
          grantType: 'refresh_token',
        ),
      );
      
      if (result != null) {
        await _handleTokenResponse(result);
      }
    } catch (e) {
      debugPrint('Token refresh failed: $e');
      await _clearAuthState();
    }
  }
  
  // Handle auth result
  Future<void> _handleAuthResult(AuthorizationTokenResponse result) async {
    await _secureStorage.write(key: _accessTokenKey, value: result.accessToken!);
    if (result.refreshToken != null) {
      await _secureStorage.write(key: _refreshTokenKey, value: result.refreshToken!);
    }
    if (result.idToken != null) {
      await _secureStorage.write(key: _idTokenKey, value: result.idToken!);
      _userInfo = _parseUserInfo(result.idToken!);
    }
    
    _isAuthenticated = true;
    notifyListeners();
  }
  
  // Handle token response
  Future<void> _handleTokenResponse(TokenResponse result) async {
    await _secureStorage.write(key: _accessTokenKey, value: result.accessToken!);
    if (result.refreshToken != null) {
      await _secureStorage.write(key: _refreshTokenKey, value: result.refreshToken!);
    }
    if (result.idToken != null) {
      await _secureStorage.write(key: _idTokenKey, value: result.idToken!);
      _userInfo = _parseUserInfo(result.idToken!);
    }
    
    _isAuthenticated = true;
    notifyListeners();
  }
  
  // Parse user info from ID token
  UserInfo _parseUserInfo(String idToken) {
    final Map<String, dynamic> decodedToken = JwtDecoder.decode(idToken);
    
    // Extract roles
    final List<String> realmRoles = 
        (decodedToken['realm_access']?['roles'] as List<dynamic>?)
            ?.cast<String>() ?? [];
    final Map<String, dynamic> resourceAccess = 
        decodedToken['resource_access'] as Map<String, dynamic>? ?? {};
    final List<String> clientRoles = 
        (resourceAccess[AuthConfig.clientId]?['roles'] as List<dynamic>?)
            ?.cast<String>() ?? [];
    
    return UserInfo(
      id: decodedToken['sub'] as String,
      username: decodedToken['preferred_username'] as String? ?? '',
      email: decodedToken['email'] as String? ?? '',
      name: decodedToken['name'] as String? ?? '',
      roles: [...realmRoles, ...clientRoles],
      groups: (decodedToken['groups'] as List<dynamic>?)?.cast<String>() ?? [],
    );
  }
  
  // Clear auth state
  Future<void> _clearAuthState() async {
    await _secureStorage.deleteAll();
    _isAuthenticated = false;
    _userInfo = null;
    notifyListeners();
  }
  
  // Role management
  bool hasRole(String role) {
    return _userInfo?.roles.contains(role) ?? false;
  }
  
  bool hasAnyRole(List<String> roles) {
    return roles.any((role) => hasRole(role));
  }
  
  bool hasAllRoles(List<String> roles) {
    return roles.every((role) => hasRole(role));
  }
  
  bool inGroup(String group) {
    return _userInfo?.groups.contains(group) ?? false;
  }
  
  // Helpers
  void _setLoading(bool loading) {
    _isLoading = loading;
    notifyListeners();
  }
  
  void _setError(String error) {
    _error = error;
    notifyListeners();
  }
  
  void _clearError() {
    _error = null;
    notifyListeners();
  }
}

// lib/models/user_info.dart
class UserInfo {
  final String id;
  final String username;
  final String email;
  final String name;
  final List<String> roles;
  final List<String> groups;
  
  UserInfo({
    required this.id,
    required this.username,
    required this.email,
    required this.name,
    required this.roles,
    required this.groups,
  });
  
  Map<String, dynamic> toJson() => {
    'id': id,
    'username': username,
    'email': email,
    'name': name,
    'roles': roles,
    'groups': groups,
  };
}

5. Create Auth Provider (Provider Package)

// lib/providers/auth_provider.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../services/auth_service.dart';

class AuthProvider extends StatefulWidget {
  final Widget child;
  
  const AuthProvider({Key? key, required this.child}) : super(key: key);
  
  @override
  State<AuthProvider> createState() => _AuthProviderState();
}

class _AuthProviderState extends State<AuthProvider> {
  final AuthService _authService = AuthService();
  
  @override
  void initState() {
    super.initState();
    _authService.init();
  }
  
  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider<AuthService>.value(
      value: _authService,
      child: widget.child,
    );
  }
}

// Extension for easy access
extension AuthExtension on BuildContext {
  AuthService get auth => Provider.of<AuthService>(this, listen: false);
  AuthService get authWatch => Provider.of<AuthService>(this);
}

UI Implementation

Login Screen

// lib/screens/login_screen.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../services/auth_service.dart';

class LoginScreen extends StatelessWidget {
  const LoginScreen({Key? key}) : super(key: key);
  
  @override
  Widget build(BuildContext context) {
    final authService = context.watch<AuthService>();
    
    return Scaffold(
      body: Container(
        decoration: const BoxDecoration(
          gradient: LinearGradient(
            begin: Alignment.topLeft,
            end: Alignment.bottomRight,
            colors: [Color(0xFF4c669f), Color(0xFF3b5998), Color(0xFF192f6a)],
          ),
        ),
        child: SafeArea(
          child: Center(
            child: Padding(
              padding: const EdgeInsets.all(40.0),
              child: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  const Icon(
                    Icons.lock_outline,
                    size: 80,
                    color: Colors.white,
                  ),
                  const SizedBox(height: 20),
                  const Text(
                    'Welcome to MyApp',
                    style: TextStyle(
                      fontSize: 32,
                      fontWeight: FontWeight.bold,
                      color: Colors.white,
                    ),
                  ),
                  const SizedBox(height: 10),
                  const Text(
                    'Secure Authentication with Skycloak',
                    style: TextStyle(
                      fontSize: 16,
                      color: Colors.white70,
                    ),
                  ),
                  const SizedBox(height: 60),
                  
                  if (authService.error != null) ...[
                    Container(
                      padding: const EdgeInsets.all(16),
                      decoration: BoxDecoration(
                        color: Colors.red.withOpacity(0.1),
                        borderRadius: BorderRadius.circular(8),
                        border: Border.all(color: Colors.red.withOpacity(0.3)),
                      ),
                      child: Text(
                        authService.error!,
                        style: const TextStyle(color: Colors.white),
                      ),
                    ),
                    const SizedBox(height: 20),
                  ],
                  
                  SizedBox(
                    width: double.infinity,
                    height: 56,
                    child: ElevatedButton(
                      onPressed: authService.isLoading
                          ? null
                          : () => _handleLogin(context),
                      style: ElevatedButton.styleFrom(
                        backgroundColor: Colors.white,
                        foregroundColor: const Color(0xFF3b5998),
                        shape: RoundedRectangleBorder(
                          borderRadius: BorderRadius.circular(28),
                        ),
                      ),
                      child: authService.isLoading
                          ? const CircularProgressIndicator()
                          : Row(
                              mainAxisAlignment: MainAxisAlignment.center,
                              children: const [
                                Icon(Icons.login),
                                SizedBox(width: 8),
                                Text(
                                  'Sign In with Skycloak',
                                  style: TextStyle(
                                    fontSize: 18,
                                    fontWeight: FontWeight.w600,
                                  ),
                                ),
                              ],
                            ),
                    ),
                  ),
                  const SizedBox(height: 20),
                  TextButton(
                    onPressed: authService.isLoading
                        ? null
                        : () => _handleRegister(context),
                    child: const Text(
                      'Create Account',
                      style: TextStyle(
                        color: Colors.white,
                        fontSize: 16,
                        decoration: TextDecoration.underline,
                      ),
                    ),
                  ),
                ],
              ),
            ),
          ),
        ),
      ),
    );
  }
  
  Future<void> _handleLogin(BuildContext context) async {
    try {
      await context.read<AuthService>().login();
    } catch (e) {
      // Error handling is done in AuthService
    }
  }
  
  Future<void> _handleRegister(BuildContext context) async {
    // Registration uses the same flow with different parameters
    _handleLogin(context);
  }
}

Main App with Navigation

// lib/main.dart
import 'package:flutter/material.dart';
import 'providers/auth_provider.dart';
import 'screens/login_screen.dart';
import 'screens/home_screen.dart';
import 'services/auth_service.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);
  
  @override
  Widget build(BuildContext context) {
    return AuthProvider(
      child: MaterialApp(
        title: 'Flutter Skycloak App',
        theme: ThemeData(
          primarySwatch: Colors.blue,
          useMaterial3: true,
        ),
        home: const AuthWrapper(),
      ),
    );
  }
}

class AuthWrapper extends StatelessWidget {
  const AuthWrapper({Key? key}) : super(key: key);
  
  @override
  Widget build(BuildContext context) {
    final authService = context.watch<AuthService>();
    
    if (authService.isLoading && !authService.isAuthenticated) {
      return const Scaffold(
        body: Center(
          child: CircularProgressIndicator(),
        ),
      );
    }
    
    return authService.isAuthenticated
        ? const HomeScreen()
        : const LoginScreen();
  }
}

Home Screen with Tab Navigation

// lib/screens/home_screen.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../services/auth_service.dart';
import '../widgets/protected_widget.dart';
import 'dashboard_tab.dart';
import 'profile_tab.dart';
import 'admin_tab.dart';

class HomeScreen extends StatefulWidget {
  const HomeScreen({Key? key}) : super(key: key);
  
  @override
  State<HomeScreen> createState() => _HomeScreenState();
}

class _HomeScreenState extends State<HomeScreen> {
  int _currentIndex = 0;
  
  @override
  Widget build(BuildContext context) {
    final authService = context.watch<AuthService>();
    
    final List<Widget> tabs = [
      const DashboardTab(),
      const ProfileTab(),
      if (authService.hasRole('admin')) const AdminTab(),
    ];
    
    return Scaffold(
      appBar: AppBar(
        title: const Text('MyApp'),
        actions: [
          IconButton(
            icon: const Icon(Icons.logout),
            onPressed: () => _handleLogout(context),
          ),
        ],
      ),
      body: IndexedStack(
        index: _currentIndex,
        children: tabs,
      ),
      bottomNavigationBar: BottomNavigationBar(
        currentIndex: _currentIndex,
        onTap: (index) => setState(() => _currentIndex = index),
        items: [
          const BottomNavigationBarItem(
            icon: Icon(Icons.dashboard),
            label: 'Dashboard',
          ),
          const BottomNavigationBarItem(
            icon: Icon(Icons.person),
            label: 'Profile',
          ),
          if (authService.hasRole('admin'))
            const BottomNavigationBarItem(
              icon: Icon(Icons.admin_panel_settings),
              label: 'Admin',
            ),
        ],
      ),
    );
  }
  
  Future<void> _handleLogout(BuildContext context) async {
    final confirmed = await showDialog<bool>(
      context: context,
      builder: (context) => AlertDialog(
        title: const Text('Logout'),
        content: const Text('Are you sure you want to logout?'),
        actions: [
          TextButton(
            onPressed: () => Navigator.of(context).pop(false),
            child: const Text('Cancel'),
          ),
          TextButton(
            onPressed: () => Navigator.of(context).pop(true),
            child: const Text('Logout'),
          ),
        ],
      ),
    );
    
    if (confirmed == true && mounted) {
      await context.read<AuthService>().logout();
    }
  }
}

// lib/screens/dashboard_tab.dart
class DashboardTab extends StatelessWidget {
  const DashboardTab({Key? key}) : super(key: key);
  
  @override
  Widget build(BuildContext context) {
    final userInfo = context.watch<AuthService>().userInfo;
    
    return RefreshIndicator(
      onRefresh: () async {
        // Refresh dashboard data
      },
      child: ListView(
        padding: const EdgeInsets.all(16),
        children: [
          Text(
            'Welcome, ${userInfo?.name ?? 'User'}!',
            style: Theme.of(context).textTheme.headlineMedium,
          ),
          const SizedBox(height: 20),
          _buildCard(
            title: 'Your Dashboard',
            description: 'View your personalized content and metrics',
            icon: Icons.dashboard,
          ),
          const SizedBox(height: 16),
          ProtectedWidget(
            roles: const ['admin', 'manager'],
            child: _buildCard(
              title: 'Management Tools',
              description: 'Access administrative features',
              icon: Icons.admin_panel_settings,
              color: Colors.orange,
            ),
          ),
          const SizedBox(height: 16),
          ProtectedWidget(
            roles: const ['editor', 'moderator'],
            child: _buildCard(
              title: 'Content Management',
              description: 'Create and manage content',
              icon: Icons.edit,
              color: Colors.green,
            ),
          ),
        ],
      ),
    );
  }
  
  Widget _buildCard({
    required String title,
    required String description,
    required IconData icon,
    Color color = Colors.blue,
  }) {
    return Card(
      elevation: 4,
      child: ListTile(
        leading: CircleAvatar(
          backgroundColor: color.withOpacity(0.1),
          child: Icon(icon, color: color),
        ),
        title: Text(
          title,
          style: const TextStyle(fontWeight: FontWeight.bold),
        ),
        subtitle: Text(description),
        trailing: const Icon(Icons.arrow_forward_ios),
        onTap: () {
          // Navigate to specific feature
        },
      ),
    );
  }
}

Protected Widget

// lib/widgets/protected_widget.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../services/auth_service.dart';

class ProtectedWidget extends StatelessWidget {
  final Widget child;
  final List<String> roles;
  final bool requireAll;
  final Widget? fallback;
  
  const ProtectedWidget({
    Key? key,
    required this.child,
    this.roles = const [],
    this.requireAll = false,
    this.fallback,
  }) : super(key: key);
  
  @override
  Widget build(BuildContext context) {
    final authService = context.watch<AuthService>();
    
    if (roles.isEmpty) {
      return child;
    }
    
    final hasAccess = requireAll
        ? authService.hasAllRoles(roles)
        : authService.hasAnyRole(roles);
    
    if (hasAccess) {
      return child;
    }
    
    return fallback ?? const SizedBox.shrink();
  }
}

API Integration

HTTP Client with Authentication

// lib/services/api_service.dart
import 'dart:convert';
import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart';
import 'auth_service.dart';

class ApiService {
  static final ApiService _instance = ApiService._internal();
  factory ApiService() => _instance;
  ApiService._internal();
  
  late final Dio _dio;
  final AuthService _authService = AuthService();
  
  static const String baseUrl = 'https://api.example.com';
  
  void init() {
    _dio = Dio(BaseOptions(
      baseUrl: baseUrl,
      connectTimeout: const Duration(seconds: 30),
      receiveTimeout: const Duration(seconds: 30),
      headers: {
        'Content-Type': 'application/json',
      },
    ));
    
    // Add interceptors
    _dio.interceptors.add(AuthInterceptor(_authService));
    
    if (kDebugMode) {
      _dio.interceptors.add(LogInterceptor(
        requestBody: true,
        responseBody: true,
      ));
    }
  }
  
  // Generic request method
  Future<T> request<T>({
    required String path,
    required String method,
    Map<String, dynamic>? data,
    Map<String, dynamic>? queryParameters,
    Options? options,
    T Function(dynamic)? parser,
  }) async {
    try {
      final response = await _dio.request(
        path,
        data: data,
        queryParameters: queryParameters,
        options: Options(
          method: method,
          headers: options?.headers,
        ),
      );
      
      if (parser != null) {
        return parser(response.data);
      }
      
      return response.data as T;
    } on DioException catch (e) {
      throw _handleError(e);
    }
  }
  
  // Convenience methods
  Future<T> get<T>(
    String path, {
    Map<String, dynamic>? queryParameters,
    T Function(dynamic)? parser,
  }) async {
    return request<T>(
      path: path,
      method: 'GET',
      queryParameters: queryParameters,
      parser: parser,
    );
  }
  
  Future<T> post<T>(
    String path, {
    Map<String, dynamic>? data,
    T Function(dynamic)? parser,
  }) async {
    return request<T>(
      path: path,
      method: 'POST',
      data: data,
      parser: parser,
    );
  }
  
  Future<T> put<T>(
    String path, {
    Map<String, dynamic>? data,
    T Function(dynamic)? parser,
  }) async {
    return request<T>(
      path: path,
      method: 'PUT',
      data: data,
      parser: parser,
    );
  }
  
  Future<T> delete<T>(
    String path, {
    T Function(dynamic)? parser,
  }) async {
    return request<T>(
      path: path,
      method: 'DELETE',
      parser: parser,
    );
  }
  
  // Error handling
  Exception _handleError(DioException error) {
    switch (error.type) {
      case DioExceptionType.connectionTimeout:
      case DioExceptionType.sendTimeout:
      case DioExceptionType.receiveTimeout:
        return TimeoutException('Connection timeout');
        
      case DioExceptionType.badResponse:
        final statusCode = error.response?.statusCode;
        final message = error.response?.data['message'] ?? error.message;
        
        switch (statusCode) {
          case 401:
            return UnauthorizedException(message);
          case 403:
            return ForbiddenException(message);
          case 404:
            return NotFoundException(message);
          case 500:
            return ServerException(message);
          default:
            return ApiException(message, statusCode: statusCode);
        }
        
      case DioExceptionType.cancel:
        return CancelledException('Request cancelled');
        
      default:
        return NetworkException('Network error: ${error.message}');
    }
  }
}

// Auth interceptor
class AuthInterceptor extends Interceptor {
  final AuthService _authService;
  
  AuthInterceptor(this._authService);
  
  @override
  Future<void> onRequest(
    RequestOptions options,
    RequestInterceptorHandler handler,
  ) async {
    // Add auth token if available
    final token = await _authService.getAccessToken();
    if (token != null) {
      options.headers['Authorization'] = 'Bearer $token';
    }
    
    handler.next(options);
  }
  
  @override
  Future<void> onError(
    DioException err,
    ErrorInterceptorHandler handler,
  ) async {
    // Handle 401 errors
    if (err.response?.statusCode == 401) {
      // Try to refresh token
      try {
        await _authService.refreshToken();
        
        // Retry the request
        final token = await _authService.getAccessToken();
        if (token != null) {
          err.requestOptions.headers['Authorization'] = 'Bearer $token';
          
          final response = await Dio().fetch(err.requestOptions);
          handler.resolve(response);
          return;
        }
      } catch (e) {
        // Refresh failed, logout user
        await _authService.logout();
      }
    }
    
    handler.next(err);
  }
}

// Custom exceptions
class ApiException implements Exception {
  final String message;
  final int? statusCode;
  
  ApiException(this.message, {this.statusCode});
  
  @override
  String toString() => 'ApiException: $message';
}

class NetworkException extends ApiException {
  NetworkException(String message) : super(message);
}

class TimeoutException extends ApiException {
  TimeoutException(String message) : super(message);
}

class UnauthorizedException extends ApiException {
  UnauthorizedException(String message) : super(message, statusCode: 401);
}

class ForbiddenException extends ApiException {
  ForbiddenException(String message) : super(message, statusCode: 403);
}

class NotFoundException extends ApiException {
  NotFoundException(String message) : super(message, statusCode: 404);
}

class ServerException extends ApiException {
  ServerException(String message) : super(message, statusCode: 500);
}

class CancelledException extends ApiException {
  CancelledException(String message) : super(message);
}

Repository Pattern

// lib/repositories/user_repository.dart
import '../services/api_service.dart';
import '../models/user_profile.dart';

class UserRepository {
  final ApiService _apiService = ApiService();
  
  Future<UserProfile> getProfile() async {
    final data = await _apiService.get<Map<String, dynamic>>(
      '/user/profile',
      parser: (json) => json as Map<String, dynamic>,
    );
    
    return UserProfile.fromJson(data);
  }
  
  Future<UserProfile> updateProfile(UserProfile profile) async {
    final data = await _apiService.put<Map<String, dynamic>>(
      '/user/profile',
      data: profile.toJson(),
      parser: (json) => json as Map<String, dynamic>,
    );
    
    return UserProfile.fromJson(data);
  }
  
  Future<List<UserProfile>> getUsers({int page = 1}) async {
    final data = await _apiService.get<Map<String, dynamic>>(
      '/admin/users',
      queryParameters: {'page': page},
      parser: (json) => json as Map<String, dynamic>,
    );
    
    final users = (data['users'] as List<dynamic>)
        .map((json) => UserProfile.fromJson(json as Map<String, dynamic>))
        .toList();
    
    return users;
  }
  
  Future<void> deleteUser(String userId) async {
    await _apiService.delete('/admin/users/$userId');
  }
}

// lib/models/user_profile.dart
import 'package:json_annotation/json_annotation.dart';

part 'user_profile.g.dart';

@JsonSerializable()
class UserProfile {
  final String id;
  final String username;
  final String email;
  final String name;
  final String? avatar;
  final DateTime createdAt;
  final DateTime updatedAt;
  
  UserProfile({
    required this.id,
    required this.username,
    required this.email,
    required this.name,
    this.avatar,
    required this.createdAt,
    required this.updatedAt,
  });
  
  factory UserProfile.fromJson(Map<String, dynamic> json) =>
      _$UserProfileFromJson(json);
  
  Map<String, dynamic> toJson() => _$UserProfileToJson(this);
}

State Management with Riverpod

// lib/providers/providers.dart
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../services/auth_service.dart';
import '../services/api_service.dart';
import '../repositories/user_repository.dart';

// Auth service provider
final authServiceProvider = ChangeNotifierProvider<AuthService>((ref) {
  return AuthService()..init();
});

// API service provider
final apiServiceProvider = Provider<ApiService>((ref) {
  return ApiService()..init();
});

// User repository provider
final userRepositoryProvider = Provider<UserRepository>((ref) {
  return UserRepository();
});

// User profile provider
final userProfileProvider = FutureProvider.autoDispose<UserProfile?>((ref) async {
  final auth = ref.watch(authServiceProvider);
  
  if (!auth.isAuthenticated) {
    return null;
  }
  
  final repository = ref.read(userRepositoryProvider);
  return repository.getProfile();
});

// Users list provider (admin)
final usersListProvider = FutureProvider.family<List<UserProfile>, int>(
  (ref, page) async {
    final repository = ref.read(userRepositoryProvider);
    return repository.getUsers(page: page);
  },
);

Testing

Unit Tests

// test/services/auth_service_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';
import 'package:mockito/annotations.dart';
import 'package:flutter_appauth/flutter_appauth.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:your_app/services/auth_service.dart';

@GenerateMocks([FlutterAppAuth, FlutterSecureStorage])
import 'auth_service_test.mocks.dart';

void main() {
  group('AuthService', () {
    late AuthService authService;
    late MockFlutterAppAuth mockAppAuth;
    late MockFlutterSecureStorage mockSecureStorage;
    
    setUp(() {
      mockAppAuth = MockFlutterAppAuth();
      mockSecureStorage = MockFlutterSecureStorage();
      
      // Create auth service with mocked dependencies
      authService = AuthService();
      // Inject mocks (you'll need to modify AuthService to accept these)
    });
    
    test('login should store tokens on success', () async {
      // Arrange
      const mockResult = AuthorizationTokenResponse(
        'access_token',
        'refresh_token',
        DateTime.now(),
        'id_token',
        'token_type',
        {},
      );
      
      when(mockAppAuth.authorizeAndExchangeCode(any))
          .thenAnswer((_) async => mockResult);
      
      // Act
      await authService.login();
      
      // Assert
      expect(authService.isAuthenticated, true);
      verify(mockSecureStorage.write(
        key: 'access_token',
        value: 'access_token',
      )).called(1);
    });
    
    test('hasRole should check user roles correctly', () {
      // Arrange
      authService.userInfo = UserInfo(
        id: '123',
        username: 'testuser',
        email: '[email protected]',
        name: 'Test User',
        roles: ['user', 'admin'],
        groups: [],
      );
      
      // Act & Assert
      expect(authService.hasRole('admin'), true);
      expect(authService.hasRole('superadmin'), false);
    });
    
    test('hasAnyRole should return true if user has any role', () {
      // Arrange
      authService.userInfo = UserInfo(
        id: '123',
        username: 'testuser',
        email: '[email protected]',
        name: 'Test User',
        roles: ['editor'],
        groups: [],
      );
      
      // Act & Assert
      expect(authService.hasAnyRole(['admin', 'editor']), true);
      expect(authService.hasAnyRole(['admin', 'manager']), false);
    });
  });
}

Widget Tests

// test/widgets/protected_widget_test.dart
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:provider/provider.dart';
import 'package:your_app/widgets/protected_widget.dart';
import 'package:your_app/services/auth_service.dart';

void main() {
  group('ProtectedWidget', () {
    late AuthService mockAuthService;
    
    setUp(() {
      mockAuthService = AuthService();
    });
    
    Widget createTestWidget(Widget child) {
      return MaterialApp(
        home: ChangeNotifierProvider<AuthService>.value(
          value: mockAuthService,
          child: Scaffold(body: child),
        ),
      );
    }
    
    testWidgets('shows child when user has required role', (tester) async {
      // Arrange
      mockAuthService.userInfo = UserInfo(
        id: '123',
        username: 'testuser',
        email: '[email protected]',
        name: 'Test User',
        roles: ['admin'],
        groups: [],
      );
      
      // Act
      await tester.pumpWidget(
        createTestWidget(
          const ProtectedWidget(
            roles: ['admin'],
            child: Text('Protected Content'),
          ),
        ),
      );
      
      // Assert
      expect(find.text('Protected Content'), findsOneWidget);
    });
    
    testWidgets('hides child when user lacks required role', (tester) async {
      // Arrange
      mockAuthService.userInfo = UserInfo(
        id: '123',
        username: 'testuser',
        email: '[email protected]',
        name: 'Test User',
        roles: ['user'],
        groups: [],
      );
      
      // Act
      await tester.pumpWidget(
        createTestWidget(
          const ProtectedWidget(
            roles: ['admin'],
            child: Text('Protected Content'),
          ),
        ),
      );
      
      // Assert
      expect(find.text('Protected Content'), findsNothing);
    });
    
    testWidgets('shows fallback when user lacks required role', (tester) async {
      // Arrange
      mockAuthService.userInfo = UserInfo(
        id: '123',
        username: 'testuser',
        email: '[email protected]',
        name: 'Test User',
        roles: ['user'],
        groups: [],
      );
      
      // Act
      await tester.pumpWidget(
        createTestWidget(
          const ProtectedWidget(
            roles: ['admin'],
            fallback: Text('Access Denied'),
            child: Text('Protected Content'),
          ),
        ),
      );
      
      // Assert
      expect(find.text('Protected Content'), findsNothing);
      expect(find.text('Access Denied'), findsOneWidget);
    });
  });
}

Integration Tests

// integration_test/auth_flow_test.dart
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:your_app/main.dart' as app;

void main() {
  IntegrationTestWidgetsFlutterBinding.ensureInitialized();
  
  group('Authentication Flow', () {
    testWidgets('shows login screen when not authenticated', (tester) async {
      // Start app
      app.main();
      await tester.pumpAndSettle();
      
      // Verify login screen is shown
      expect(find.text('Sign In with Skycloak'), findsOneWidget);
      expect(find.text('Welcome to MyApp'), findsOneWidget);
    });
    
    testWidgets('can tap login button', (tester) async {
      // Start app
      app.main();
      await tester.pumpAndSettle();
      
      // Tap login button
      await tester.tap(find.text('Sign In with Skycloak'));
      await tester.pumpAndSettle();
      
      // Note: Actual OAuth flow will open external browser
      // For integration tests, you might want to mock the auth service
    });
  });
}

Production Considerations

Security Configuration

// lib/utils/security_utils.dart
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter_jailbreak_detection/flutter_jailbreak_detection.dart';

class SecurityUtils {
  // Check if device is compromised
  static Future<bool> isDeviceSecure() async {
    if (kDebugMode) {
      // Skip security checks in debug mode
      return true;
    }
    
    try {
      // Check for jailbreak/root
      final isJailbroken = await FlutterJailbreakDetection.jailbroken;
      if (isJailbroken) {
        debugPrint('Device is jailbroken/rooted');
        return false;
      }
      
      // Check for development mode (Android)
      if (Platform.isAndroid) {
        final isDevelopmentMode = await FlutterJailbreakDetection.developerMode;
        if (isDevelopmentMode) {
          debugPrint('Development mode is enabled');
          return false;
        }
      }
      
      return true;
    } catch (e) {
      debugPrint('Security check failed: $e');
      return false;
    }
  }
  
  // Certificate pinning configuration
  static SecurityContext createSecurityContext() {
    final context = SecurityContext(withTrustedRoots: false);
    
    // Add your certificate
    context.setTrustedCertificatesBytes(yourCertificateBytes);
    
    return context;
  }
  
  // Obfuscate sensitive strings
  static String obfuscate(String input) {
    // Simple obfuscation - use more sophisticated methods in production
    return base64Encode(utf8.encode(input));
  }
  
  static String deobfuscate(String input) {
    return utf8.decode(base64Decode(input));
  }
}

// lib/utils/app_config.dart
class AppConfig {
  static const bool enableCertificatePinning = bool.fromEnvironment(
    'ENABLE_CERT_PINNING',
    defaultValue: true,
  );
  
  static const bool enableRootDetection = bool.fromEnvironment(
    'ENABLE_ROOT_DETECTION',
    defaultValue: true,
  );
  
  static const String apiBaseUrl = String.fromEnvironment(
    'API_BASE_URL',
    defaultValue: 'https://api.example.com',
  );
  
  static const int sessionTimeout = int.fromEnvironment(
    'SESSION_TIMEOUT',
    defaultValue: 1800, // 30 minutes
  );
}

Performance Optimization

// lib/utils/performance_utils.dart
import 'package:flutter/foundation.dart';
import 'package:cached_network_image/cached_network_image.dart';

class PerformanceUtils {
  // Debounce function calls
  static Function debounce(Function func, Duration delay) {
    Timer? timer;
    
    return () {
      timer?.cancel();
      timer = Timer(delay, () => func());
    };
  }
  
  // Throttle function calls
  static Function throttle(Function func, Duration delay) {
    bool isThrottled = false;
    
    return () {
      if (!isThrottled) {
        func();
        isThrottled = true;
        Timer(delay, () => isThrottled = false);
      }
    };
  }
  
  // Cached image widget
  static Widget cachedImage(String imageUrl, {
    double? width,
    double? height,
    BoxFit? fit,
  }) {
    return CachedNetworkImage(
      imageUrl: imageUrl,
      width: width,
      height: height,
      fit: fit ?? BoxFit.cover,
      placeholder: (context, url) => const CircularProgressIndicator(),
      errorWidget: (context, url, error) => const Icon(Icons.error),
      cacheKey: imageUrl,
      memCacheWidth: width?.toInt(),
      memCacheHeight: height?.toInt(),
    );
  }
  
  // Performance monitoring
  static T measurePerformance<T>(String name, T Function() operation) {
    final stopwatch = Stopwatch()..start();
    
    try {
      final result = operation();
      stopwatch.stop();
      
      if (kDebugMode) {
        debugPrint('[Performance] $name: ${stopwatch.elapsedMilliseconds}ms');
      }
      
      return result;
    } catch (e) {
      stopwatch.stop();
      debugPrint('[Performance] $name failed after ${stopwatch.elapsedMilliseconds}ms');
      rethrow;
    }
  }
}

// Widget performance optimization
mixin AutomaticKeepAliveClientMixin<T extends StatefulWidget> on State<T> {
  @override
  bool get wantKeepAlive => true;
  
  @override
  Widget build(BuildContext context) {
    super.build(context);
    return buildWidget(context);
  }
  
  Widget buildWidget(BuildContext context);
}

Error Handling and Logging

// lib/utils/error_handler.dart
import 'package:flutter/material.dart';
import 'package:sentry_flutter/sentry_flutter.dart';

class ErrorHandler {
  static Future<void> init() async {
    // Initialize Sentry for production error tracking
    await SentryFlutter.init(
      (options) {
        options.dsn = 'YOUR_SENTRY_DSN';
        options.environment = kReleaseMode ? 'production' : 'development';
        options.tracesSampleRate = 1.0;
      },
    );
    
    // Set up Flutter error handling
    FlutterError.onError = (FlutterErrorDetails details) {
      if (kReleaseMode) {
        // Send to Sentry in production
        Sentry.captureException(
          details.exception,
          stackTrace: details.stack,
        );
      } else {
        // Log to console in development
        FlutterError.dumpErrorToConsole(details);
      }
    };
    
    // Catch async errors
    PlatformDispatcher.instance.onError = (error, stack) {
      if (kReleaseMode) {
        Sentry.captureException(error, stackTrace: stack);
      } else {
        debugPrint('Async error: $error\n$stack');
      }
      return true;
    };
  }
  
  // Log custom events
  static void logEvent(String message, {
    Map<String, dynamic>? extra,
    SentryLevel? level,
  }) {
    if (kReleaseMode) {
      Sentry.captureMessage(
        message,
        level: level ?? SentryLevel.info,
        withScope: (scope) {
          extra?.forEach((key, value) {
            scope.setExtra(key, value);
          });
        },
      );
    } else {
      debugPrint('[${level ?? 'INFO'}] $message ${extra ?? ''}');
    }
  }
  
  // Show user-friendly error dialog
  static void showErrorDialog(BuildContext context, String message) {
    showDialog(
      context: context,
      builder: (context) => AlertDialog(
        title: const Text('Error'),
        content: Text(message),
        actions: [
          TextButton(
            onPressed: () => Navigator.of(context).pop(),
            child: const Text('OK'),
          ),
        ],
      ),
    );
  }
}

Troubleshooting

Common Issues

  1. Redirect URL Mismatch

    • Verify scheme in AndroidManifest.xml and Info.plist
    • Ensure no trailing slashes in redirect URLs
    • Check for case sensitivity
  2. Token Storage Issues

    • iOS: Enable Keychain sharing in capabilities
    • Android: Ensure proper ProGuard rules for flutter_secure_storage
    • Test on real devices, not just emulators
  3. Deep Link Not Working

    • iOS: Check Associated Domains configuration
    • Android: Verify intent-filter configuration
    • Use flutter doctor to check for issues

Debug Utilities

// lib/utils/debug_utils.dart
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';

class DebugUtils {
  // Enable debug mode features
  static void enableDebugMode() {
    if (kDebugMode) {
      // Show performance overlay
      debugPrintRebuildDirtyWidgets = true;
      
      // Log all HTTP requests
      HttpClient.enableTimelineLogging = true;
      
      // Show material grid
      debugPaintSizeEnabled = false;
      
      // Show semantic labels
      debugShowCheckedModeBanner = true;
    }
  }
  
  // Test deep links
  static Future<void> testDeepLink(String url) async {
    try {
      final uri = Uri.parse(url);
      debugPrint('Testing deep link: $url');
      
      // Check if URL can be launched
      if (await canLaunchUrl(uri)) {
        await launchUrl(uri);
        debugPrint('Deep link launched successfully');
      } else {
        debugPrint('Cannot launch deep link');
      }
    } catch (e) {
      debugPrint('Deep link test failed: $e');
    }
  }
  
  // Log auth state
  static void logAuthState(AuthService authService) {
    debugPrint('=== Auth State ===');
    debugPrint('Authenticated: ${authService.isAuthenticated}');
    debugPrint('User: ${authService.userInfo?.toJson()}');
    debugPrint('Error: ${authService.error}');
  }
  
  // Copy text to clipboard (useful for tokens)
  static Future<void> copyToClipboard(String text, BuildContext context) async {
    await Clipboard.setData(ClipboardData(text: text));
    
    if (context.mounted) {
      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(content: Text('Copied to clipboard')),
      );
    }
  }
}

// Debug console widget
class DebugConsole extends StatelessWidget {
  final AuthService authService;
  
  const DebugConsole({Key? key, required this.authService}) : super(key: key);
  
  @override
  Widget build(BuildContext context) {
    if (!kDebugMode) return const SizedBox.shrink();
    
    return Container(
      padding: const EdgeInsets.all(8),
      color: Colors.black87,
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        mainAxisSize: MainAxisSize.min,
        children: [
          Text(
            'Debug Console',
            style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold),
          ),
          Text(
            'Auth: ${authService.isAuthenticated}',
            style: TextStyle(color: Colors.white, fontSize: 12),
          ),
          Text(
            'User: ${authService.userInfo?.username ?? 'N/A'}',
            style: TextStyle(color: Colors.white, fontSize: 12),
          ),
          Text(
            'Roles: ${authService.userInfo?.roles.join(', ') ?? 'N/A'}',
            style: TextStyle(color: Colors.white, fontSize: 12),
          ),
        ],
      ),
    );
  }
}

Next Steps