Architecture Dual Flow Google OAuth 2.0 : Flutter & Laravel
Mettre en place une authentification Google robuste combinant sessions web et jetons Bearer Sanctum pour Flutter.

Technologies : Flutter (Dart), Laravel (PHP), Laravel Sanctum, Laravel Socialite, Google OAuth 2.0
Objectif : Unifier l'authentification Google entre une application web (cookies de session) et une application mobile Flutter (jeton API stateless).
Le Défi Architectural : Web vs Mobile #
Dans une application hybride moderne, l'authentification présente une divergence fondamentale :
- Sur le Web : L'authentification repose sur des cookies de session HTTP-only (gérés nativement par Laravel).
- Sur Mobile (Flutter) : L'application communique via des APIs REST stateless et nécessite un jeton d'accès sécurisé (Personal Access Token Sanctum) envoyé dans le header
Authorization: Bearer.
Pour éviter de gérer deux systèmes parallèles ou de multiplier les flux d'authentification, nous utilisons le pattern Dual Flow : la WebView mobile délègue l'échange OAuth à l'API Laravel, qui crée ou associe l'utilisateur puis renvoie le jeton via un Deep Link.
1. Configuration Google Cloud & Schéma DB #
Identifiant OAuth 2.0 #
Dans Google Cloud Console, créez un identifiant de type Application Web avec les URIs de redirection :
- Web :
https://mondomaine.com/auth/google/callback - Mobile :
https://api.mondomaine.com/api/auth/google/mobile/callback
Migration Base de Données #
Schema::table('users', function (Blueprint $table) {
$table->string('google_id')->nullable()->index();
$table->text('google_token')->nullable();
$table->boolean('has_custom_password')->default(true);
});
2. Implémentation Backend Laravel #
Le contrôleur expose deux endpoints : l'un génère l'URL d'autorisation Google, l'autre réceptionne le retour, réconcilie l'utilisateur et émet le jeton Sanctum.
namespace App\Http\Controllers\Api;
use App\Models\User;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\RedirectResponse;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;
use Laravel\Socialite\Facades\Socialite;
class MobileGoogleController extends Controller
{
class=class="text-emerald-400">"text-slate-500 italic">// 1. Génère l'URL de connexion Google
public function redirect(): JsonResponse
{
$url = Socialite::driver('google')
->redirectUrl(secure_url('/api/auth/google/mobile/callback'))
->stateless()
->with(['prompt' => 'select_account'])
->redirect()->getTargetUrl();
return response()->json(['success' => true, 'url' => $url]);
}
class=class="text-emerald-400">"text-slate-500 italic">// 2. Callback : réconciliation DB + émission du jeton Sanctum
public function callback(): RedirectResponse
{
$googleUser = Socialite::driver('google')
->redirectUrl(secure_url('/api/auth/google/mobile/callback'))
->stateless()->user();
class=class="text-emerald-400">"text-slate-500 italic">// Réconciliation : google_id -> email existant -> création
$user = User::where('google_id', $googleUser->getId())->first()
?? User::where('email', $googleUser->getEmail())->first()
?? User::create([
'name' => $googleUser->getName() ?? 'Utilisateur Google',
'email' => $googleUser->getEmail(),
'password' => Hash::make(Str::random(32)),
'google_id' => $googleUser->getId(),
'email_verified_at' => now(),
]);
$token = $user->createToken('mobile-app')->plainTextToken;
class=class="text-emerald-400">"text-slate-500 italic">// Redirection vers le Deep Link de l'app mobile
return redirect()->to("mondomaine:class=class="text-emerald-400">"text-slate-500 italic">//auth/callback?token={$token}");
}
}
3. Configuration Mobile & Flutter #
Deep Link Android (AndroidManifest.xml)
#
<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="mondomaine" android:host="auth"/>
</intent-filter>
Interception dans la WebView Flutter #
Dans l'écran de connexion Flutter, ouvrez l'URL Google via flutter_inappwebview et interceptez le callback contenant le jeton :
bool _checkCallbackUrl(String url) {
if (url.contains('token=')) {
final token = Uri.parse(url).queryParameters['token'];
if (token != null) {
_saveTokenAndContinue(token);
return true;
}
}
return false;
}
void _saveTokenAndContinue(String token) async {
await secureStorage.write(key: 'auth_token', value: token);
await userNotifier.loadProfile(); class=class="text-emerald-400">"text-slate-500 italic">// GET /api/user avec Bearer token
if (mounted) Navigator.pop(context, true);
}
Les 3 Pièges Majeurs à Éviter #
1. Erreur redirect_uri_mismatch : Derrière un reverse-proxy (Nginx, Traefik, FrankenPHP), forcez toujours secure_url() pour garantir que le protocole généré est bien en https://.
2. Interception prématurée : Filtrez strictement la présence de token= dans l'URL pour ne pas fermer la WebView avant la validation finale.
3. Mots de passe fantômes : Si votre application gère un cache local du mot de passe pour la biométrie, videz-le lors d'une connexion Google pour ne pas induire l'utilisateur en erreur.
En Résumé #
Ce pattern Dual Flow isole la complexité OAuth sur le backend Laravel tout en fournissant à l'application Flutter un jeton Sanctum standard. Vous obtenez ainsi une authentification robuste, rapide et uniforme sur l'ensemble de votre écosystème.