feat(mobile): Implement Keycloak WebView authentication with HTTP callback

- Replace flutter_appauth with custom WebView implementation to resolve deep link issues
- Add KeycloakWebViewAuthService with integrated WebView for seamless authentication
- Configure Android manifest for HTTP cleartext traffic support
- Add network security config for development environment (192.168.1.11)
- Update Keycloak client to use HTTP callback endpoint (http://192.168.1.11:8080/auth/callback)
- Remove obsolete keycloak_auth_service.dart and temporary scripts
- Clean up dependencies and regenerate injection configuration
- Tested successfully on multiple Android devices (Xiaomi 2201116TG, SM A725F)

BREAKING CHANGE: Authentication flow now uses WebView instead of external browser
- Users will see Keycloak login page within the app instead of browser redirect
- Resolves ERR_CLEARTEXT_NOT_PERMITTED and deep link state management issues
- Maintains full OIDC compliance with PKCE flow and secure token storage

Technical improvements:
- WebView with custom navigation delegate for callback handling
- Automatic token extraction and user info parsing from JWT
- Proper error handling and user feedback
- Consistent authentication state management across app lifecycle
This commit is contained in:
DahoudG
2025-09-15 01:44:16 +00:00
parent 73459b3092
commit f89f6167cc
290 changed files with 34563 additions and 3528 deletions

View File

@@ -1,69 +0,0 @@
import 'package:flutter/material.dart';
import '../../../../shared/theme/app_theme.dart';
import 'welcome_screen.dart';
class AuthWrapper extends StatefulWidget {
const AuthWrapper({super.key});
@override
State<AuthWrapper> createState() => _AuthWrapperState();
}
class _AuthWrapperState extends State<AuthWrapper> {
bool _isLoading = true;
bool _isAuthenticated = false;
@override
void initState() {
super.initState();
_checkAuthenticationStatus();
}
Future<void> _checkAuthenticationStatus() async {
// Simulation de vérification d'authentification
// En production : vérifier le token JWT, SharedPreferences, etc.
await Future.delayed(const Duration(milliseconds: 500));
setState(() {
_isLoading = false;
// Pour le moment, toujours false (pas d'utilisateur connecté)
_isAuthenticated = false;
});
}
@override
Widget build(BuildContext context) {
if (_isLoading) {
return _buildLoadingScreen();
}
if (_isAuthenticated) {
// TODO: Retourner vers la navigation principale
return _buildLoadingScreen(); // Temporaire
} else {
return const WelcomeScreen();
}
}
Widget _buildLoadingScreen() {
return Scaffold(
body: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
AppTheme.primaryColor,
AppTheme.primaryDark,
],
),
),
child: const Center(
child: CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
),
),
),
);
}
}

View File

@@ -0,0 +1,296 @@
import 'package:flutter/material.dart';
import '../../../../core/auth/services/keycloak_webview_auth_service.dart';
import '../../../../core/auth/models/auth_state.dart';
import '../../../../core/di/injection.dart';
import '../../../../shared/theme/app_theme.dart';
/// Page de connexion utilisant Keycloak OIDC
class KeycloakLoginPage extends StatefulWidget {
const KeycloakLoginPage({super.key});
@override
State<KeycloakLoginPage> createState() => _KeycloakLoginPageState();
}
class _KeycloakLoginPageState extends State<KeycloakLoginPage> {
late KeycloakWebViewAuthService _authService;
bool _isLoading = false;
@override
void initState() {
super.initState();
_authService = getIt<KeycloakWebViewAuthService>();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: AppTheme.backgroundLight,
body: StreamBuilder<AuthState>(
stream: _authService.authStateStream,
builder: (context, snapshot) {
final authState = snapshot.data ?? const AuthState.unknown();
if (authState.isAuthenticated) {
// Rediriger vers la page principale si déjà connecté
WidgetsBinding.instance.addPostFrameCallback((_) {
Navigator.of(context).pushReplacementNamed('/main');
});
}
return SafeArea(
child: SingleChildScrollView(
padding: const EdgeInsets.all(24.0),
child: ConstrainedBox(
constraints: BoxConstraints(
minHeight: MediaQuery.of(context).size.height -
MediaQuery.of(context).padding.top -
MediaQuery.of(context).padding.bottom - 48,
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Logo et titre
_buildHeader(),
const SizedBox(height: 48),
// Message d'accueil
_buildWelcomeMessage(),
const SizedBox(height: 32),
// Bouton de connexion
_buildLoginButton(authState),
const SizedBox(height: 16),
// Message d'erreur si présent
if (authState.errorMessage != null)
_buildErrorMessage(authState.errorMessage!),
const SizedBox(height: 32),
// Informations sur la sécurité
_buildSecurityInfo(),
],
),
),
),
);
},
),
);
}
Widget _buildHeader() {
return Column(
children: [
// Logo UnionFlow
Container(
width: 120,
height: 120,
decoration: BoxDecoration(
color: AppTheme.primaryColor,
borderRadius: BorderRadius.circular(60),
boxShadow: [
BoxShadow(
color: AppTheme.primaryColor.withOpacity(0.3),
blurRadius: 20,
offset: const Offset(0, 10),
),
],
),
child: const Icon(
Icons.groups,
size: 60,
color: Colors.white,
),
),
const SizedBox(height: 24),
// Titre
Text(
'UnionFlow',
style: Theme.of(context).textTheme.headlineMedium?.copyWith(
fontWeight: FontWeight.bold,
color: AppTheme.primaryColor,
),
),
const SizedBox(height: 8),
// Sous-titre
Text(
'Gestion d\'organisations',
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
color: Colors.grey[600],
),
),
],
);
}
Widget _buildWelcomeMessage() {
return Card(
elevation: 2,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
children: [
const Icon(
Icons.security,
size: 48,
color: AppTheme.primaryColor,
),
const SizedBox(height: 16),
Text(
'Connexion sécurisée',
style: Theme.of(context).textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
Text(
'Connectez-vous avec votre compte UnionFlow pour accéder à toutes les fonctionnalités de l\'application.',
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Colors.grey[600],
),
),
],
),
),
);
}
Widget _buildLoginButton(AuthState authState) {
return ElevatedButton(
onPressed: authState.isLoading || _isLoading ? null : _handleLogin,
style: ElevatedButton.styleFrom(
backgroundColor: AppTheme.primaryColor,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
elevation: 3,
),
child: authState.isLoading || _isLoading
? const SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
),
)
: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.login, size: 24),
const SizedBox(width: 12),
Text(
'Se connecter avec Keycloak',
style: Theme.of(context).textTheme.titleMedium?.copyWith(
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
],
),
);
}
Widget _buildErrorMessage(String errorMessage) {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: AppTheme.errorColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: AppTheme.errorColor.withOpacity(0.3)),
),
child: Row(
children: [
const Icon(
Icons.error_outline,
color: AppTheme.errorColor,
size: 24,
),
const SizedBox(width: 12),
Expanded(
child: Text(
errorMessage,
style: const TextStyle(
color: AppTheme.errorColor,
fontWeight: FontWeight.w500,
),
),
),
],
),
);
}
Widget _buildSecurityInfo() {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.blue.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.blue.withOpacity(0.3)),
),
child: Column(
children: [
Row(
children: [
const Icon(
Icons.info_outline,
color: Colors.blue,
size: 20,
),
const SizedBox(width: 8),
Text(
'Authentification sécurisée',
style: Theme.of(context).textTheme.titleSmall?.copyWith(
color: Colors.blue,
fontWeight: FontWeight.bold,
),
),
],
),
const SizedBox(height: 8),
Text(
'Vos données sont protégées par Keycloak, une solution d\'authentification enterprise. '
'Votre mot de passe n\'est jamais stocké sur cet appareil.',
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Colors.blue[700],
),
),
],
),
);
}
Future<void> _handleLogin() async {
setState(() {
_isLoading = true;
});
try {
await _authService.loginWithWebView(context);
} catch (e) {
// L'erreur sera gérée par le stream AuthState
print('Erreur de connexion: $e');
} finally {
if (mounted) {
setState(() {
_isLoading = false;
});
}
}
}
}