Social Authentication in Flutter with Google/Gmail and GitHub - Backend Laravel Sociality/Django

Social Authentication (Google)

Video thumbnail

Social authentication is one of the most requested features in modern mobile applications. It allows users to register and log in with a single tap using their existing Google, GitHub, or other platform accounts. In this section, we will analyze how to implement the complete authentication workflow securely by combining Flutter and a web backend.

Offering this feature in mobile environments is essential for maintaining the consistency of the entire software ecosystem. If a platform already allows social authentication in its web version and also has a mobile app, the correct approach is to replicate this mechanism on the device. Otherwise, a user who originally created their account through an external provider will find it impossible to log in to the mobile application.

General Workflow

The recommended workflow for performing social authentication securely on mobile devices consists of the following steps:

  1. Native/Web Authentication on the Device: The mobile app initiates the corresponding flow (native for Google or via a controlled WebView for platforms like GitHub) for the user to log in.
  2. Obtaining the Token: Once authenticated with the social provider, the app obtains an Access Token or an Identity Token (ID Token). To mitigate issues with persistent cached states that force unwanted automatic logins, it is an excellent practice to execute an explicit logout on the client (signOut) immediately before triggering the new authentication flow.
  3. Sending to the Server (Backend): The app sends this token to the backend API via HTTPS along with the provider's name. It is indispensable to understand that social authentication should not be managed exclusively locally on the mobile device; session and user data must be persisted and interact directly with a backend (developed in Laravel, Django, FastAPI, or Node.js) for the authentication to have real and secure value within the system.
  4. Validation on the Server: The server contacts the provider to securely validate the token and obtain the user's data. If valid, it looks up or creates the user in the database and generates the definitive session token (for example, with Laravel Sanctum).
  5. Response and Persistence: The backend returns the token to the device, which stores it securely to authenticate future requests. The user information returned by the API must be identical to what would be delivered in a traditional username and password authentication flow.

1. Google Cloud Console Configuration

To implement Google Sign-In, it is essential to correctly configure the credentials in the Google Cloud Console. One of the most common mistakes is confusing the different types of credentials required:

  • Web Application Credential (Backend): You must create a credential of type "Web Application". The client ID generated here (referred to as serverClientId in Flutter) is used to tell the Google SDK that we need a compatible token so that the backend can validate it. If this parameter is omitted or configured with Android credentials, the web server will be unable to verify the validity of the received token.
  • Android Credential: You must create a credential of type "Android". In this credential, you must enter your app's package name (for example, com.your.app) and your application's SHA-1 signing certificate fingerprint. At the Flutter code level, it is not necessary to explicitly map this Android client ID, as the Google ecosystem performs the validation internally by cross-referencing the package name and the digital signature.

How to Obtain SHA-1 Fingerprints

Google requires the SHA-1 fingerprint to authorize native sign-in requests from Android.

SHA-1 for Testing (Debug):

On your development machine, the application is automatically signed with a default keystore. You can obtain this signature by running the following command in the terminal (the default keystore password is android):

keytool -list -v -keystore ~/.android/debug.keystore -alias androiddebugkey -storepass android
SHA-1 for Production:

If you are building a production APK on your machine via flutter build apk --release using your own keystore file (.jks or .keystore), you need to extract the SHA-1 directly from that private file. Run the following command in your terminal (you need to have Java/Keytool installed on your system):

keytool -list -v -keystore /path/to/your/key-file.jks -alias your-production-alias

Replace /path/to/your/key-file.jks with the actual location of your signature file. It will prompt you for the password you assigned to your key when creating it. In the output, you will see the Certificate fingerprints block with the production SHA-1.

Important note on Production and Google Play: When uploading the application to production, Google Play Console usually modifies the application's digital signature through the Play Store protection service. This means that the SHA-1 generated locally for release will stop working in production. To fix this, you must enter the Google Play Console, select your application, navigate to the Play App Signing section, copy the SHA-1 fingerprint provided directly by Google, and enter it as a new Android client within your project in the Google Cloud Console.

2. Implementation in Flutter (Dart Service)

To handle native Google logic, the google_sign_in package is used. Below is a clean structure for the service:

import 'dart:convert';
import 'package:google_sign_in/google_sign_in.dart';
import 'package:http/http.dart' as http;

class SocialAuthService {
  // It is essential to configure the client ID corresponding to the WEB APPLICATION
  final GoogleSignIn _googleSignIn = GoogleSignIn(
    serverClientId: "YOUR_WEB_APPLICATION_CLIENT_ID.apps.googleusercontent.com",
    scopes: ['email', 'profile'],
  );

  Future<String?> getGoogleAccessToken() async {
    try {
      // Force previous logout to clear cached states and avoid automatic logins
      if (await _googleSignIn.isSignedIn()) {
        await _googleSignIn.signOut();
      }
      
      final GoogleSignInAccount? account = await _googleSignIn.signIn();
      if (account == null) return null; // Flow canceled by the user

      final GoogleSignInAuthentication auth = await account.authentication;
      
      // Priority is given to accessToken or idToken according to the backend validation requirements
      return auth.accessToken ?? auth.idToken;
    } catch (e) {
      print("Error in Google Sign-In: \$e");
      return null;
    }
  }
}

The example login page:

lib\pages\user\login_page.dart

class _LoginPageState extends State<LoginPage> {
  final TextEditingController _emailController = TextEditingController();
  final TextEditingController _passwordController = TextEditingController();
  final SocialAuthService _socialAuthService = SocialAuthService();
  ***
  ElevatedButton(
    text: "Google",
      colors: [
      const Color(0xFFEA4335),
      const Color(0xFFDB4437)
    ],
    onPressed: _loginWithGoogle,
  ),
  ***
   void _loginWithGoogle() async {
    setStateIfMounted(() {
      socialLoading = true;
    });

    try {
      final token = await _socialAuthService.getGoogleAccessToken();

      if (token == null) {
        setStateIfMounted(() {
          socialLoading = false;
        });
        return;
      }

      final response = await _socialAuthService.authenticateOnBackend(
        provider: 'google',
        accessToken: token,
      );

      _handleSocialBackendResponse(response, 'google');
    } catch (e) {
      debugPrint("Error Google Auth flow: $e");
      showToastMessage(context, LocaleKeys.errorGoogleLogin.tr());
      setStateIfMounted(() {
        socialLoading = false;
      });
    }
  }

As you can see, there are two steps:

  1. getGoogleAccessToken, is 100% Google, to obtain the Token.
  2. authenticateOnBackend, is a method that manages the response from your Rest API which, given the Google authorization token from step one, validates it and returns YOUR authenticated user from YOUR app.
Future<Map<String, dynamic>> authenticateOnBackend({
    required String provider,
    required String accessToken,
  }) async {
    var url = Uri.https(baseUrlAPI, "/api/v1/auth/social");
    if (!appApiUseHttps) {
      url = Uri.http(baseUrlAPI, "/api/v1/auth/social");
    }

    try {
      final response = await Api.post(
        url,
        body: {
          'provider': provider,
          'access_token': accessToken,
        },
      );

      debugPrint("Response Status: ${response.statusCode}");
      debugPrint("Response Body: ${response.body}");

      if (response.statusCode == 200) {
        return json.decode(response.body) as Map<String, dynamic>;
      } else {
        final Map<String, dynamic> errorData =
            json.decode(response.body) as Map<String, dynamic>;
        return {
          'error': true,
          'message':
              errorData['message'] ?? 'Server authentication error.'
        };
      }
    } catch (e) {
      debugPrint("Error connecting to backend: $e");
      return {
        'error': true,
        'message': 'Could not connect to the Desarrollolibre server.'
      };
    }
  }

3. Integration with the Backend

Option A: Laravel Server with Laravel Socialite

Laravel Socialite simplifies social token validation using the userFromToken() method. Below is the Laravel controller that processes the request from the mobile app:

<?php

namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use App\Models\User;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Laravel\Socialite\Facades\Socialite;

class SocialController extends Controller
{
    public function handleSocialAuth(Request $request): JsonResponse
    {
        $request->validate([
            'provider' => ['required', 'string', 'in:google,github'],
            'access_token' => ['required', 'string'],
        ]);

        $provider = $request->input('provider');
        $token = $request->input('access_token');

        // For GitHub: if a temporary code arrives (OAuth2 Web) instead of the final token, we exchange it
        if ($provider === 'github' && !str_starts_with($token, 'gho_') && !str_starts_with($token, 'ghu_')) {
            $response = \Illuminate\Support\Facades\Http::asJson()->post('https://github.com/login/oauth/access_token', [
                'client_id' => config('services.github.client_id'),
                'client_secret' => config('services.github.client_secret'),
                'code' => $token,
            ]);

            if ($response->successful()) {
                $data = [];
                parse_str($response->body(), $data);
                $token = $data['access_token'] ?? $token;
            }
        }

        try {
            $socialUser = Socialite::driver($provider)->userFromToken($token);
        } catch (\Exception $e) {
            return response()->json(['message' => 'Invalid token', 'error' => $e->getMessage()], 401);
        }

        $user = User::where($provider . '_id', $socialUser->getId())->first();

        if (!$user) {
            $email = $socialUser->getEmail();
            if (!$email) {
                return response()->json([
                    'requires_email' => true,
                    'social_data' => [
                        'id' => $socialUser->getId(),
                        'name' => $socialUser->getName() ?? 'User',
                        'avatar' => $socialUser->getAvatar(),
                        'provider' => $provider,
                    ]
                ], 200);
            }

            $user = User::create([
                'name' => $socialUser->getName() ?? 'User',
                'email' => $email,
                $provider . '_id' => $socialUser->getId(),
                'avatar' => $socialUser->getAvatar(),
                'password' => bcrypt(str_random(24)),
            ]);
        }

        return response()->json([
            'access_token' => $user->createToken('mobile-app')->plainTextToken,
            'token_type' => 'Bearer',
            'user' => $user
        ], 200);
    }
}

This method is the one used from Flutter in authenticateOnBackend().

Option B: Manual Process in Backend (Python / Django)

If you do not use Laravel, you can validate the token manually by making a direct HTTPS request to the provider's validation APIs. Below is a conceptual example in Python (Django / Flask) to verify Google and GitHub tokens:

import requests
from django.http import JsonResponse
from django.views.decorators.csrf import csrf_exempt
import json

@csrf_exempt
def handle_social_auth(request):
    if request.method == 'POST':
        data = json.loads(request.body)
        provider = data.get('provider')
        token = data.get('access_token')

        if provider == 'google':
            // Validation of ID Token with the Google API
            response = requests.get(f'https://oauth2.googleapis.com/tokeninfo?id_token={token}')
            if response.status_code != 200:
                return JsonResponse({'message': 'Invalid Google token'}, status=401)
            
            user_info = response.json()
            social_id = user_info.get('sub')
            email = user_info.get('email')
            name = user_info.get('name')

        elif provider == 'github':
            // Code exchange for access_token if applicable
            if not token.startswith('gho_'):
                res = requests.post(
                    'https://github.com/login/oauth/access_token',
                    headers={'Accept': 'application/json'},
                    data={
                        'client_id': 'YOUR_GITHUB_CLIENT_ID',
                        'client_secret': 'YOUR_GITHUB_CLIENT_SECRET',
                        'code': token
                    }
                )
                token = res.json().get('access_token')

            // Direct query to the GitHub API
            headers = {'Authorization': f'token {token}'}
            response = requests.get('https://api.github.com/user', headers=headers)
            if response.status_code != 200:
                return JsonResponse({'message': 'Invalid GitHub token'}, status=401)
                
            user_info = response.json()
            social_id = str(user_info.get('id'))
            name = user_info.get('name') or user_info.get('login')
            email = user_info.get('email') // May require an additional call if it is private

        // Here you proceed to register or log in the user in your DB and issue your JWT token
        return JsonResponse({'message': 'Successful authentication', 'user': {'name': name, 'email': email}})

Learn step by step how you can socially authenticate a user in Flutter with Gmail/Google or GitHub and a backend in Laravel Sociality or Django.


Únete a la comunidad de desarrolladores que han decidido dejar de picar código y empezar a construir productos reales. Recibe mis mejores trucos de arquitectura cada semana:

I agree to receive announcements of interest about this Blog.

Andrés Cruz

ES En español