Comment Implémenter "Se Connecter avec Google" dans Flutter avec Laravel (Architecture Dual Flow)

Par Devinsto
Posté le 28 août 2026
Mis à jour le 3 novembre 2026
27 min de lecture
339 vues

Niveau : Intermédiaire / Avancé
Technologies : Flutter (Dart), Laravel 13 (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 (ex: nomdomaine App), 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 :

  • Application type : Application Web.

  • Nom : nomdomaine 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

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

Schema::table('users', function (Blueprint $table) {
    $table->string('google_id')->nullable()->index();
    $table->text('google_token')->nullable();
    $table->text('google_refresh_token')->nullable();
    $table->boolean('has_custom_password')->default(true);
});

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 :

use Laravel\Sanctum\HasApiTokens;

class User extends Authenticatable
{
    use HasApiTokens, HasFactory, Notifiable;
    
    // ...
}

É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.

composer require laravel/fortify
php artisan fortify:install
php 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.

composer require laravel/sanctum
php artisan vendor:publish --provider="Laravel\Sanctum\SanctumServiceProvider"
php artisan migrate

Assurez-vous que le middleware Sanctum est configuré dans bootstrap/app.php :

->withMiddleware(function (Middleware $middleware) {
    $middleware->statefulApi();
})

ÉTAPE 4 : Implémentation du Contrôleur Backend (MobileGoogleController.php)

Créons un contrôleur spécifiquement taillé pour les besoins de l'API mobile :

<?php

declare(strict_types=1);

namespace App\Http\Controllers\Api\Core;

use App\Http\Controllers\Controller;
use App\Models\User;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;
use Laravel\Socialite\Facades\Socialite;

class MobileGoogleController extends Controller
{
    /**
     * Génère l'URL d'autorisation Google OAuth pour l'application Mobile.
     */
    public function redirect(Request $request): JsonResponse
    {
        $ref = $request->query('ref');

        // Forcer le HTTPS sur la route de callback mobile pour éviter le bug redirect_uri_mismatch
        $callbackUrl = secure_url('/api/auth/google/mobile/callback');
        $driver = Socialite::driver('google')->redirectUrl($callbackUrl)->stateless();

        $stateItems = ['source=mobile'];
        if (! empty($ref)) {
            $stateItems[] = 'ref=' . $ref;
        }

        // Forcer Google à afficher l'écran de choix de compte Gmail (prompt=select_account)
        $driver->with([
            'prompt' => 'select_account',
            'state'  => implode('|', $stateItems),
        ]);

        return response()->json([
            'success' => true,
            'url'     => $driver->redirect()->getTargetUrl(),
        ]);
    }

    /**
     * Reçoit le callback de Google, réconcilie le compte DB et redirige vers le Deep Link Mobile.
     */
    public function callback(Request $request): RedirectResponse|JsonResponse
    {
        try {
            $callbackUrl = secure_url('/api/auth/google/mobile/callback');
            $googleUser = Socialite::driver('google')->redirectUrl($callbackUrl)->stateless()->user();
        } catch (\Exception $e) {
            return redirect()->to('nomdomaine://auth/callback?error=' . urlencode($e->getMessage()));
        }

        // Réconciliation DB intelligente (recherche par google_id -> email -> création)
        $user = User::where('google_id', $googleUser->getId())->first();

        if (! $user) {
            $user = User::where('email', $googleUser->getEmail())->first();

            if ($user instanceof User) {
                // Liaison de compte existant
                $user->update([
                    'google_id'            => $googleUser->getId(),
                    'google_token'         => $googleUser->token,
                    'google_refresh_token' => $googleUser->refreshToken ?? null,
                    'email_verified_at'    => $user->email_verified_at ?? now(),
                ]);
            } else {
                // Création d'un nouveau compte
                $user = DB::transaction(function () use ($googleUser, $request) {
                    $u = User::create([
                        'name'                => $googleUser->getName() ?? 'Utilisateur Google',
                        'email'               => $googleUser->getEmail(),
                        'password'            => Hash::make(Str::random(32)),
                        'has_custom_password' => false,
                        'google_id'           => $googleUser->getId(),
                        'google_token'        => $googleUser->token,
                        'email_verified_at'   => now(),
                        'registration_ip'     => $request->ip(),
                    ]);
                    $u->profile()->create(['avatar' => $googleUser->getAvatar()]);
                    return $u;
                });
            }
        }

        // Vérification de sécurité si l'utilisateur est banni
        if ($user->is_banned) {
            return redirect()->to('nomdomaine://auth/callback?error=' . urlencode("Votre compte est suspendu."));
        }

        // Génération du jeton Sanctum pour l'application Mobile
        $token = $user->createToken('mobile-app')->plainTextToken;

        $params = http_build_query([
            'token'  => $token,
            'name'   => $user->name,
            'email'  => $user->email,
            'avatar' => $user->profile?->avatar ?? '',
        ]);

        // Redirection HTTP 302 vers le Deep Link Mobile
        return redirect()->to("nomdomaine://auth/callback?{$params}");
    }
}

Déclaration des Routes (routes/api.php)

Route::get('/auth/google/mobile/redirect', [MobileGoogleController::class, 'redirect']);
Route::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 ouvre ou communique avec l'application mobile :

Android (android/app/src/main/AndroidManifest.xml)

Ajoutez un <intent-filter> sous l'activité principale <activity> :

<intent-filter android:autoVerify="false">
    <action android:name="android.intent.action.VIEW"/>
    <category android:name="android.intent.category.DEFAULT"/>
    <category android:name="android.intent.category.BROWSABLE"/>
    <data android:scheme="nomdomaine"
          android:host="auth"/>
</intent-filter>


ÉTAPE 6 : Implémentation Front-End (Flutter)

1. Dépendances Flutter (pubspec.yaml)

dependencies:
  flutter:
    sdk: flutter
  flutter_riverpod: ^x.x.x
  flutter_inappwebview: ^x.x.x # ou webview_flutter
  flutter_secure_storage: ^x.x.x
  dio: ^x.x.x

2. Écran WebView OAuth (google_oauth_webview_screen.dart)

L'écran WebView charge l'URL Google transmise par l'API et intercepte le callback contenant le jeton Sanctum :

import 'package:flutter/material.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

class GoogleOAuthWebViewScreen extends ConsumerStatefulWidget {
  final String initialUrl;

  const GoogleOAuthWebViewScreen({super.key, required this.initialUrl});

  @override
  ConsumerState<GoogleOAuthWebViewScreen> createState() => _GoogleOAuthWebViewScreenState();
}

class _GoogleOAuthWebViewScreenState extends ConsumerState<GoogleOAuthWebViewScreen> {
  bool _isProcessing = false;

  bool _checkCallbackUrl(String url) {
    if (_isProcessing) return false;

    // 1. Interception du jeton Sanctum dans l'URL (nomdomaine://auth/callback?token=...)
    if (url.contains('token=')) {
      _isProcessing = true;
      String? token;

      try {
        final uri = Uri.parse(url);
        token = uri.queryParameters['token'];
      } catch (_) {}

      if (token != null && token.isNotEmpty) {
        _handleTokenSuccess(token);
        return true;
      }
    }

    // 2. Détection d'une erreur
    if (url.contains('error=')) {
      _isProcessing = true;
      final uri = Uri.parse(url);
      final error = uri.queryParameters['error'];

      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(
          content: Text('Erreur: ${error ?? "Authentification annulée"}'),
          backgroundColor: Colors.red,
        ),
      );
      Navigator.of(context).pop(false);
      return true;
    }

    return false;
  }

  void _handleTokenSuccess(String token) async {
    // Enregistrer le token dans FlutterSecureStorage
    await ref.read(secureStorageServiceProvider).saveAuthToken(token);
    
    // Hydrater le profil utilisateur via GET /api/auth/me
    await ref.read(authNotifierProvider.notifier).fetchCurrentUser();

    if (mounted) {
      Navigator.of(context).pop(true); // Succès !
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text("Connexion avec Google")),
      body: InAppWebView(
        initialUrlRequest: URLRequest(url: WebUri(widget.initialUrl)),
        onLoadStart: (controller, url) {
          if (url != null) _checkCallbackUrl(url.toString());
        },
        onLoadStop: (controller, url) {
          if (url != null) _checkCallbackUrl(url.toString());
        },
      ),
    );
  }
}

É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 !

Ajouter un commentaire

Vous devez être connecté pour laisser un commentaire.

Avatar
Waffo lele rostand
il y a 1 mois

Laravel 13 et flutter

1 réponse
Avatar
Waffo lele rostand
il y a 1 mois

Sdk