1. Accueil
  2. Articles
7 min de lecture
1 vues

Comment Implémenter "Se Connecter avec Google" dans Flutter avec Laravel

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.email et .../auth/userinfo.profile.

#3. Créer un Identifiant Client OAuth 2.0 (Type Application Web)

Dans Credentials > Create Credentials > OAuth client ID :

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;
2 
3class User extends Authenticatable
4{
5 use HasApiTokens, HasFactory, Notifiable;
6 
7 // ...
8}
1use Laravel\Sanctum\HasApiTokens;
2 
3class User extends Authenticatable
4{
5 use HasApiTokens, HasFactory, Notifiable;
6 
7 // ...
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/fortify
2php artisan fortify:install
3php artisan migrate
1composer require laravel/fortify
2php artisan fortify:install
3php 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/sanctum
2php artisan vendor:publish --provider="Laravel\Sanctum\SanctumServiceProvider"
3php artisan migrate
1composer require laravel/sanctum
2php 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/socialite
1composer 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 Controller
2{
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();
10 
11 return response()->json(['success' => true, 'url' => $url]);
12 }
13 
14 // 2. Callback Google, Réconciliation DB & Génération Jeton Sanctum
15 public function callback(): RedirectResponse {
16 $googleUser = Socialite::driver('google')
17 ->redirectUrl(secure_url('/api/auth/google/mobile/callback'))
18 ->stateless()->user();
19 
20 // Réconciliation DB : google_id -> email -> création
21 $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 ]);
30 
31 $token = $user->createToken('mobile-app')->plainTextToken;
32 
33 // Redirection 302 vers le Deep Link Mobile
34 return redirect()->to("nomdomaine://auth/callback?token={$token}");
35 }
36}
1class MobileGoogleController extends Controller
2{
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();
10 
11 return response()->json(['success' => true, 'url' => $url]);
12 }
13 
14 // 2. Callback Google, Réconciliation DB & Génération Jeton Sanctum
15 public function callback(): RedirectResponse {
16 $googleUser = Socialite::driver('google')
17 ->redirectUrl(secure_url('/api/auth/google/mobile/callback'))
18 ->stateless()->user();
19 
20 // Réconciliation DB : google_id -> email -> création
21 $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 ]);
30 
31 $token = $user->createToken('mobile-app')->plainTextToken;
32 
33 // Redirection 302 vers le Deep Link Mobile
34 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.x
3 flutter_inappwebview: ^x.x.x
4 flutter_secure_storage: ^x.x.x
5 dio: ^x.x.x
1dependencies:
2 flutter_riverpod: ^x.x.x
3 flutter_inappwebview: ^x.x.x
4 flutter_secure_storage: ^x.x.x
5 dio: ^x.x.x

#2. Écran WebView OAuth (google_oauth_webview_screen.dart)

1class _GoogleOAuthWebViewScreenState extends State<GoogleOAuthWebViewScreen> {
2 bool _isProcessing = false;
3 
4 // Interception du callback dans la WebView
5 bool _checkCallbackUrl(String url) {
6 if (_isProcessing) return false;
7 
8 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 }
18 
19 void _handleSuccess(String token) async {
20 await secureStorage.saveAuthToken(token); // Stocke le token Sanctum
21 await authNotifier.fetchCurrentUser(); // Charge le profil (GET /api/auth/me)
22 if (mounted) Navigator.pop(context, true); // Ferme la WebView
23 }
24 
25 @override
26 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;
3 
4 // Interception du callback dans la WebView
5 bool _checkCallbackUrl(String url) {
6 if (_isProcessing) return false;
7 
8 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 }
18 
19 void _handleSuccess(String token) async {
20 await secureStorage.saveAuthToken(token); // Stocke le token Sanctum
21 await authNotifier.fetchCurrentUser(); // Charge le profil (GET /api/auth/me)
22 if (mounted) Navigator.pop(context, true); // Ferme la WebView
23 }
24 
25 @override
26 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 de https://.
  • Solution : Utilisez toujours secure_url('/api/auth/google/mobile/callback') pour forcer le protocole https://. 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= ou error=.

#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_password dans votre SecureStorage lors 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 !