Niveau : Intermédiaire / Avancé
Technologies : Flutter (Dart), Laravel (PHP), Laravel Fortify, Laravel Sanctum, Laravel Socialite, Google Cloud OAuth 2.0
#Introduction
L'intégration d'un bouton "Continuer avec Google" dans une application mobile est devenue un standard indispensable d'expérience utilisateur (UX). Cependant, lorsqu'une application possède à la fois un front-end Web (ex: Nuxt/Vue) et une application mobile (Flutter), la gestion de l'authentification Google pose souvent des maux de tête architecturaux :
- Le Web utilise des sessions stateful par cookies de domaine (gérées par Fortify).
- Le Mobile utilise des jetons d'accès API stateless (Sanctum Bearer Tokens).
Dans cet article complet, nous allons vous expliquer pas à pas comment mettre en place une Architecture Dual Flow robuste, sécurisée et parfaitement synchronisée entre une application mobile Flutter et un backend **Laravel **.
#ÉTAPE 1 : Pré-requis Google Cloud Console (La Base)
Avant d'écrire la moindre ligne de code, vous devez configurer vos identifiants sur la Google Cloud Console.
#1. Créer ou Sélectionner un Projet
Rendez-vous sur Google Cloud Console et créez un projet.
#2. Configurer l'Écran de Consentement OAuth
Dans APIs & Services > OAuth consent screen :
- Choisissez le type d'utilisateur (External).
- Renseignez le nom de votre application, votre e-mail de support et les domaines autorisés (ex:
nomdomaine.com). - Ajoutez les scopes de base :
.../auth/userinfo.emailet.../auth/userinfo.profile.
#3. Créer un Identifiant Client OAuth 2.0 (Type Application Web)
Dans Credentials > Create Credentials > OAuth client ID :
- Application type : Application Web.
- Nom :
Backend API. - URIs de redirection autorisés (CRUCIAL) :
- Pour le Web :
https://nomdomaine.com/auth/google/callback - Pour le Mobile :
https://api.nomdomaine.com/api/auth/google/mobile/callback
- Pour le Web :
Remarque importante : Pour le flux mobile WebView + Deep Link, nous réutilisons les identifiants OAuth de type Web Client ID sur le serveur backend. Cela évite d'avoir à manipuler des empreintes SHA-1 complexes d'Android Keystore sur Google Cloud !
#ÉTAPE 2 : Pré-requis Base de Données (Laravel Schema)
Votre table users doit pouvoir accueillir les comptes créés ou liés via Google OAuth.
#Migration Laravel
1Schema::table('users', function (Blueprint $table) {2 $table->string('google_id')->nullable()->index();3 $table->text('google_token')->nullable();4 $table->text('google_refresh_token')->nullable();5 $table->boolean('has_custom_password')->default(true);6});1Schema::table('users', function (Blueprint $table) {2 $table->string('google_id')->nullable()->index();3 $table->text('google_token')->nullable();4 $table->text('google_refresh_token')->nullable();5 $table->boolean('has_custom_password')->default(true);6});
#Attributs du Modèle User.php
Assurez-vous d'ajouter google_id, google_token et google_refresh_token dans l'attribut fillable de votre modèle User, et d'inclure le trait HasApiTokens de Sanctum :
1use Laravel\Sanctum\HasApiTokens;23class User extends Authenticatable4{5 use HasApiTokens, HasFactory, Notifiable;67 // ...8}1use Laravel\Sanctum\HasApiTokens;23class User extends Authenticatable4{5 use HasApiTokens, HasFactory, Notifiable;67 // ...8}
#ÉTAPE 3 : Installation des Packages Backend (Fortify, Sanctum & Socialite)
Pour gérer à la fois l'authentification web/API, la gestion des sessions/mots de passe et l'OAuth Socialite, nous avons besoin de trois briques fondamentales dans Laravel :
#1. Installation de Laravel Fortify (Gestion de l'Auth de Base & Profil)
Fortify est le moteur headless d'authentification de Laravel. Il gère l'inscription, la réinitialisation de mot de passe, la vérification d'e-mail et le 2FA sans imposer de vues Blade.
1composer require laravel/fortify2php artisan fortify:install3php artisan migrate1composer require laravel/fortify2php artisan fortify:install3php artisan migrate
Dans config/fortify.php, activez les fonctionnalités souhaitées (ex: Features::emailVerification(), Features::updatePasswords()).
#2. Installation de Laravel Sanctum (Jetons d'Accès Bearer pour Mobile)
Sanctum est indispensable pour la partie mobile Flutter. Il permet de délivrer des Personal Access Tokens (plainTextToken) envoyés dans le header HTTP Authorization: Bearer <token> de chaque requête mobile.
1composer require laravel/sanctum2php artisan vendor:publish --provider="Laravel\Sanctum\SanctumServiceProvider"3php artisan migrate1composer require laravel/sanctum2php artisan vendor:publish --provider="Laravel\Sanctum\SanctumServiceProvider"3php artisan migrate
Assurez-vous que le middleware Sanctum est configuré dans bootstrap/app.php :
1->withMiddleware(function (Middleware $middleware) {2 $middleware->statefulApi();3})1->withMiddleware(function (Middleware $middleware) {2 $middleware->statefulApi();3})
#3. Installation de Laravel Socialite (OAuth Google)
Socialite simplifie la communication OAuth 2.0 avec Google.
1composer require laravel/socialite1composer require laravel/socialite
Configuration dans config/services.php :
1'google' => [2 'client_id' => env('GOOGLE_CLIENT_ID'),3 'client_secret' => env('GOOGLE_CLIENT_SECRET'),4 'redirect' => env('GOOGLE_REDIRECT_URI'),5],1'google' => [2 'client_id' => env('GOOGLE_CLIENT_ID'),3 'client_secret' => env('GOOGLE_CLIENT_SECRET'),4 'redirect' => env('GOOGLE_REDIRECT_URI'),5],
#ÉTAPE 4 : Implémentation du Contrôleur Backend (MobileGoogleController.php)
1class MobileGoogleController extends Controller2{3 // 1. Demande d'URL OAuth Google (stateless + HTTPS + choix de compte)4 public function redirect(): JsonResponse {5 $url = Socialite::driver('google')6 ->redirectUrl(secure_url('/api/auth/google/mobile/callback'))7 ->stateless()8 ->with(['prompt' => 'select_account'])9 ->redirect()->getTargetUrl();1011 return response()->json(['success' => true, 'url' => $url]);12 }1314 // 2. Callback Google, Réconciliation DB & Génération Jeton Sanctum15 public function callback(): RedirectResponse {16 $googleUser = Socialite::driver('google')17 ->redirectUrl(secure_url('/api/auth/google/mobile/callback'))18 ->stateless()->user();1920 // Réconciliation DB : google_id -> email -> création21 $user = User::where('google_id', $googleUser->getId())->first()22 ?? User::where('email', $googleUser->getEmail())->first()23 ?? User::create([24 'name' => $googleUser->getName() ?? 'Utilisateur Google',25 'email' => $googleUser->getEmail(),26 'password' => Hash::make(Str::random(32)),27 'google_id' => $googleUser->getId(),28 'email_verified_at' => now(),29 ]);3031 $token = $user->createToken('mobile-app')->plainTextToken;3233 // Redirection 302 vers le Deep Link Mobile34 return redirect()->to("nomdomaine://auth/callback?token={$token}");35 }36}1class MobileGoogleController extends Controller2{3 // 1. Demande d'URL OAuth Google (stateless + HTTPS + choix de compte)4 public function redirect(): JsonResponse {5 $url = Socialite::driver('google')6 ->redirectUrl(secure_url('/api/auth/google/mobile/callback'))7 ->stateless()8 ->with(['prompt' => 'select_account'])9 ->redirect()->getTargetUrl();1011 return response()->json(['success' => true, 'url' => $url]);12 }1314 // 2. Callback Google, Réconciliation DB & Génération Jeton Sanctum15 public function callback(): RedirectResponse {16 $googleUser = Socialite::driver('google')17 ->redirectUrl(secure_url('/api/auth/google/mobile/callback'))18 ->stateless()->user();1920 // Réconciliation DB : google_id -> email -> création21 $user = User::where('google_id', $googleUser->getId())->first()22 ?? User::where('email', $googleUser->getEmail())->first()23 ?? User::create([24 'name' => $googleUser->getName() ?? 'Utilisateur Google',25 'email' => $googleUser->getEmail(),26 'password' => Hash::make(Str::random(32)),27 'google_id' => $googleUser->getId(),28 'email_verified_at' => now(),29 ]);3031 $token = $user->createToken('mobile-app')->plainTextToken;3233 // Redirection 302 vers le Deep Link Mobile34 return redirect()->to("nomdomaine://auth/callback?token={$token}");35 }36}
#Déclaration des Routes (routes/api.php)
1Route::get('/auth/google/mobile/redirect', [MobileGoogleController::class, 'redirect']);2Route::match(['get', 'post'], '/auth/google/mobile/callback', [MobileGoogleController::class, 'callback']);1Route::get('/auth/google/mobile/redirect', [MobileGoogleController::class, 'redirect']);2Route::match(['get', 'post'], '/auth/google/mobile/callback', [MobileGoogleController::class, 'callback']);
#ÉTAPE 5 : Configuration Mobile Android & iOS (Deep Links)
Pour que la redirection nomdomaine://auth/callback communique avec l'application mobile :
#Android (android/app/src/main/AndroidManifest.xml)
Ajoutez un <intent-filter> sous <activity> :
1<intent-filter android:autoVerify="false">2 <action android:name="android.intent.action.VIEW"/>3 <category android:name="android.intent.category.DEFAULT"/>4 <category android:name="android.intent.category.BROWSABLE"/>5 <data android:scheme="nomdomaine" android:host="auth"/>6</intent-filter>1<intent-filter android:autoVerify="false">2 <action android:name="android.intent.action.VIEW"/>3 <category android:name="android.intent.category.DEFAULT"/>4 <category android:name="android.intent.category.BROWSABLE"/>5 <data android:scheme="nomdomaine" android:host="auth"/>6</intent-filter>
#ÉTAPE 6 : Implémentation Front-End (Flutter)
#1. Dépendances Flutter (pubspec.yaml)
1dependencies:2 flutter_riverpod: ^x.x.x3 flutter_inappwebview: ^x.x.x4 flutter_secure_storage: ^x.x.x5 dio: ^x.x.x1dependencies:2 flutter_riverpod: ^x.x.x3 flutter_inappwebview: ^x.x.x4 flutter_secure_storage: ^x.x.x5 dio: ^x.x.x
#2. Écran WebView OAuth (google_oauth_webview_screen.dart)
1class _GoogleOAuthWebViewScreenState extends State<GoogleOAuthWebViewScreen> {2 bool _isProcessing = false;34 // Interception du callback dans la WebView5 bool _checkCallbackUrl(String url) {6 if (_isProcessing) return false;78 if (url.contains('token=')) {9 _isProcessing = true;10 final token = Uri.parse(url).queryParameters['token'];11 if (token != null) {12 _handleSuccess(token);13 return true;14 }15 }16 return false;17 }1819 void _handleSuccess(String token) async {20 await secureStorage.saveAuthToken(token); // Stocke le token Sanctum21 await authNotifier.fetchCurrentUser(); // Charge le profil (GET /api/auth/me)22 if (mounted) Navigator.pop(context, true); // Ferme la WebView23 }2425 @override26 Widget build(BuildContext context) {27 return Scaffold(28 body: InAppWebView(29 initialUrlRequest: URLRequest(url: WebUri(widget.initialUrl)),30 onLoadStart: (_, url) => _checkCallbackUrl(url.toString()),31 ),32 );33 }34}1class _GoogleOAuthWebViewScreenState extends State<GoogleOAuthWebViewScreen> {2 bool _isProcessing = false;34 // Interception du callback dans la WebView5 bool _checkCallbackUrl(String url) {6 if (_isProcessing) return false;78 if (url.contains('token=')) {9 _isProcessing = true;10 final token = Uri.parse(url).queryParameters['token'];11 if (token != null) {12 _handleSuccess(token);13 return true;14 }15 }16 return false;17 }1819 void _handleSuccess(String token) async {20 await secureStorage.saveAuthToken(token); // Stocke le token Sanctum21 await authNotifier.fetchCurrentUser(); // Charge le profil (GET /api/auth/me)22 if (mounted) Navigator.pop(context, true); // Ferme la WebView23 }2425 @override26 Widget build(BuildContext context) {27 return Scaffold(28 body: InAppWebView(29 initialUrlRequest: URLRequest(url: WebUri(widget.initialUrl)),30 onLoadStart: (_, url) => _checkCallbackUrl(url.toString()),31 ),32 );33 }34}
#ÉTAPE 7 : Résolution des Pièges & Bugs Fréquents (Leçons Apprises)
#1. Erreur Google redirect_uri_mismatch (Code 400)
- Cause : Si votre serveur Laravel fonctionne derrière un Reverse Proxy (Traefik, Nginx, FrankenPHP), Socialite peut générer un schéma HTTP
http://api.nomdomaine.com/...au lieu dehttps://. - Solution : Utilisez toujours
secure_url('/api/auth/google/mobile/callback')pour forcer le protocolehttps://. Déclarez précisément cet exact URL dans Google Cloud Console.
#2. Interception prématurée dans la WebView
- Cause : Si votre fonction de vérification d'URL écoute de façon trop large (ex:
url.contains('nomdomaine.com')), la WebView peut s'arrêter dès le chargement du callback backend avant même que Google ne redirige ! - Solution : Ne fermez la WebView qu'en cas de présence explicite de
token=ouerror=.
#3. Gestion propre du champ mot de passe local
- Problème : Lors d'une connexion via Google, aucun mot de passe n'existe localement. Si votre service de stockage garde un ancien mot de passe en cache, l'écran de profil risque d'afficher de faux masques
••••••••. - Solution : Effacez systématiquement la clé
saved_passworddans votreSecureStoragelors d'une connexion réussie par Google OAuth.
#Conclusion
En combinant Fortify pour le socle d'authentification, Sanctum pour la génération de jetons d'accès API mobile, et Socialite pour la réconciliation des comptes Google OAuth, vous obtenez une architecture propre, moderne et extrêmement sécurisée aussi bien sur Web que sur Flutter !
Happy Coding !