Google Login Flutter without Firebase - Laravel Backend Integration Tutorial
Google Sign-In is one of the most popular authentication methods for mobile apps. Most users are lazy and prefer the 1-click solution to sign in. I know this from myself as well. So why not offer Google Login? I implemented Google Login in my app Twork and as a result, daily registrations have significantly improved. How I did this - and especially without Firebase - I’ll show you in this blog post.
Why Google Sign-In without Firebase?
Firebase is great, but not always the best solution. And admittedly, I’m not a big fan of Firebase. While I use Laravel as my backend, the concept is applicable to all types of backends.
My Firebase-free approach:
- Flutter App gets the ID token from Google
- Flutter App sends the token to your Laravel Backend
- Laravel Backend validates the token with Google
- Laravel Backend creates/updates the user in the database
- Laravel Backend returns its own auth token
Benefits without Firebase:
- ✅ No additional costs: No Firebase account needed
- ✅ Full control: All user data in your database
- ✅ Simpler architecture: No additional service
- ✅ Flexibility: Laravel offers more customization options
- ✅ One backend: Everything in Laravel, no microservices
Google Cloud Console Setup
1. Create Project
Go to Google Cloud Console and create a new project or select an existing one.
2. Create OAuth 2.0 Credentials
- Navigate to APIs & Services → Credentials
- Click Create Credentials → OAuth client ID
- Create credentials for:
- Web application (for Laravel Backend)
- iOS (if you support iOS)
- Android (if you support Android)
Important: Note down the Client IDs and the Client Secret for the Web Application.
Tip: I created separate credentials for development and production. e.g: dev-android, prod-android, dev-ios, prod-ios, dev-web, prod-web.
3. Add Android SHA-1 Fingerprint
For Android, you need the SHA-1 fingerprint of your app. This is also divided into one for development and one for production. If you have already published the app in the Play Store, use the one from the Play Store. You can find this under Testing and Release → App Integrity → Play App Signing → Settings.
For development and if you don’t use Google Play App Signing, you can use this command:
# Development Keystore
keytool -list -v -keystore ~/.android/debug.keystore -alias androiddebugkey -storepass android -keypass android
# Release Keystore
keytool -list -v -keystore /path/to/your/keystore.jks -alias your-alias
Add the SHA-1 fingerprint to the respective Android OAuth client in the Google Cloud Console.
Flutter Implementation
1. Add Dependencies
Add the following packages to your pubspec.yaml:
dependencies:
flutter:
sdk: flutter
google_sign_in: ^7.2.0
Then run:
flutter pub get
2. Create Google Service Class
Create a service class for Google Sign-In. This class handles authentication and retrieves the ID token that will be sent to the backend. Here’s my implementation from the twork app:
import 'dart:io';
import 'package:google_sign_in/google_sign_in.dart';
class GoogleService {
GoogleSignIn get _googleInstance => GoogleSignIn.instance;
bool _isGoogleSignInInitialized = false;
GoogleService() {
_initializeGoogleSignIn();
}
Future<void> _initializeGoogleSignIn() async {
String? clientId;
if (Platform.isIOS) {
clientId = ''; // iOS Client ID here
} else if (Platform.isAndroid) {
clientId = ''; // Android Client ID here
}
await _googleInstance.initialize(
clientId: clientId,
serverClientId: const String.fromEnvironment('SERVER_CLIENT_ID'),
);
_isGoogleSignInInitialized = true;
}
/// Ensure Google Sign-In is initialized
Future<void> _ensureGoogleSignInInitialized() async {
if (!_isGoogleSignInInitialized) {
await _initializeGoogleSignIn();
}
}
/// Check if Google Sign-In is supported
bool checkGoogleSignInSupport() {
return _googleInstance.supportsAuthenticate();
}
/// Perform Google Sign-In and return ID token
Future<String?> signIn() async {
try {
await _ensureGoogleSignInInitialized();
final GoogleSignInAccount account = await _googleInstance.authenticate();
final GoogleSignInAuthentication authentication = account.authentication;
if (authentication.idToken == null) {
throw ApiException('Failed to get ID token from Google');
}
return authentication.idToken;
} on GoogleSignInException catch (e) {
throw ApiException(e.description ?? 'Google Sign-In failed');
} catch (e) {
throw ApiException('An unexpected error occurred during Google Sign-In');
}
}
}
Important Notes:
serverClientIdis the Web Client ID from Google Cloud Console- The
idTokenis what we send to the backend
3. Set Environment Variable During Build
For the Web Client ID, you need to set an environment variable. I always do this via a config.json file, which I then include with --dart-define-from-file=config.json. Alternatively, you can also specify it directly during run.
flutter run --dart-define=SERVER_CLIENT_ID=your-web-client-id.apps.googleusercontent.com
4. Android Configuration
For Android, you need to configure almost nothing except making sure that the minSdkVersion is set to at least 21.
5. iOS Configuration
For iOS, add the URL Scheme to ios/Runner/Info.plist:
<key>GIDClientID</key>
<string>YOUR-CLIENT-ID.apps.googleusercontent.com</string>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLSchemes</key>
<array>
<string>com.googleusercontent.apps.YOUR-CLIENT-ID</string>
</array>
</dict>
</array>
Laravel Backend Implementation
Laravel Socialite makes integrating social login extremely easy. Normally, Socialite is used for OAuth flows with redirects, so it’s more intended for web apps. But we can also use it to validate the ID token.
1. Install Laravel Socialite
Install Laravel Socialite:
composer require laravel/socialite
2. Environment Variables
Add your Web Client IDs to .env:
GOOGLE_CLIENT_ID=your-web-client-id.apps.googleusercontent.com
GOOGLE_CLIENT_SECRET=your-client-secret
3. Config for Google Services
Add your Google Client IDs to config/services.php:
'google' => [
'client_id' => env('GOOGLE_CLIENT_ID'),
'client_secret' => env('GOOGLE_CLIENT_SECRET'),
],
3. Create Auth Controller with Laravel Socialite
Now comes the actual controller. Here we use Laravel Socialite. This is similar to how I implemented it in the twork app:
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Laravel\Socialite\Facades\Socialite;
class GoogleAuthController extends Controller
{
/**
* Handle Google Sign-In with ID Token from Flutter
*/
public function googleSignIn(Request $request)
{
$request->validate([
'id_token' => 'required|string',
]);
try {
// Socialite automatically validates the token with Google
$googleUser = Socialite::driver('google')
->stateless()
->userFromToken($request->id_token);
// Normalize email (e.g., for Gmail aliases)
$email = $this->normalizeEmail($googleUser->getEmail());
// 1. Check if user with Google ID exists
$user = User::where('google_id', $googleUser->getId())->first();
if ($user) {
// If yes, return auth token and user
return response()->json([
'access_token' => $user->createToken('auth_token')->plainTextToken,
'user' => $user,
'message' => 'Successfully authenticated with Google'
], 200);
}
// 2. Check if user with Google email exists but doesn't have Google Sign-In yet
$user = User::where('email', $email)->first();
if ($user) {
// User with email found - add Google ID
$user->google_id = $googleUser->getId();
$user->save();
return response()->json([
'access_token' => $user->createToken('auth_token')->plainTextToken,
'user' => $user,
'message' => 'Successfully authenticated with Google'
], 200);
}
// 3. Create new user
$user = User::create([
'name' => $googleUser->getName(),
'email' => $email,
'google_id' => $googleUser->getId(),
'password' => Hash::make(str()->random(32)),
]);
return response()->json([
'access_token' => $user->createToken('auth_token')->plainTextToken,
'user' => $user,
'message' => 'Successfully authenticated with Google'
], 201);
} catch (\Exception $e) {
return response()->json([
'message' => 'Failed to authenticate with Google',
'error' => config('app.debug') ? $e->getMessage() : 'Authentication failed'
], 500);
}
}
/**
* Normalize email (handle Gmail aliases)
*/
private function normalizeEmail(string $email): string
{
$email = Str::lower(Str::trim($email));
// Replace googlemail.com with gmail.com
if (Str::endsWith($email, '@googlemail.com')) {
$email = Str::replace('@googlemail.com', '@gmail.com', $email);
}
return $email;
}
}
Ok, let’s go through the flow:
- First, the user is validated with Google using the
id_token. - If that worked, we check our database if a user with the Google ID exists.
- If yes, we return the user with our backend token.
- If not, we check if a user with the Google email exists.
- If yes, we add the Google ID to the user and then return the user.
- If no user was found, we create a new user with the Google data.
Also note the normalizeEmail method. This function is an email normalization for Gmail aliases, so that users with a googlemail.com address are not created twice.
Actually, googlemail.com and gmail.com are the same thing.
This function brings several advantages:
- ✅ Prevents duplicate accounts for users with Gmail aliases
- ✅ Links existing Google email addresses with the Google account
- ✅ Everything is stored in our database
- ✅ Full control
Flutter - Backend Integration
Now let’s connect Flutter with the Laravel backend:
class AuthRepository {
final String baseUrl = 'https://your-api.com/api';
final GoogleService _googleService;
AuthRepository(this._googleService);
Future<AuthResponse> signInWithGoogle() async {
try {
// 1. Get ID token from Google
final idToken = await _googleService.signIn();
if (idToken == null) {
throw ApiException('Failed to get Google ID token');
}
// 2. Send ID token to Laravel backend
final response = await http.post(
Uri.parse('$baseUrl/auth/google'),
headers: {'Content-Type': 'application/json'},
body: jsonEncode({'idToken': idToken}),
);
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
// 3. Store token for future requests
final authToken = data['token'];
final user = User.fromJson(data['user']);
// Store token (e.g., with flutter_secure_storage)
await _secureStorage.write(key: 'auth_token', value: authToken);
return AuthResponse(user: user, token: authToken);
} else {
throw 'Backend authentication failed';
}
} catch (error) {
throw ApiException(error.toString());
}
}
}
The code shown doesn’t necessarily correspond to the code I use in the Twork app, but the basic principle is the same. In Twork, a lot more work is done with Bloc and DataProviders.
UI Implementation
I’ll spare you the UI implementation here. Basically, you just need a button that calls the signInWithGoogle method. Whether you do this with Bloc, Provider, or Riverpod doesn’t matter.
I can show you my Bloc setup as an example.
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:twork_app/data/repositories/auth_repository.dart';
import 'package:flutter/material.dart';
part 'login_event.dart';
part 'login_state.dart';
class LoginBloc extends Bloc<LoginEvent, LoginState> {
final AuthRepository _authRepository;
LoginBloc({
required AuthRepository authRepository,
}) : _authRepository = authRepository,
super(LoginInitial()) {
on<LoginGoogleSubmitted>((event, emit) async {
try {
emit(LoginInProgress());
await _authRepository.googleLogin();
emit(LoginSuccess());
} catch (e) {
emit(LoginFailure(error: e.toString()));
}
});
}
}
In the UI, the Bloc method is called via the LoginGoogleSubmitted trigger and the login process is started.
Your implementation may look different, but the basic principle remains the same.
Troubleshooting
Problem: “Invalid ID token”
Solution:
- Check if Client IDs are configured correctly
- Make sure you’re using the Web Client ID as
serverClientId - Check if token hasn’t expired
Problem: “Email not verified”
Solution:
- User must verify email with Google
- You can temporarily disable this check during development
Problem: SHA-1 Error on Android
Solution:
- Add both debug and release SHA-1 in Google Console
- Make sure package name is correct
What about Google Firebase Authentication?
Many people wonder: “Don’t I need Firebase for Google Login?” No!
Firebase is just one of many options. If you already have a Laravel backend (or plan to build one), the direct route via Laravel Socialite is a pretty good alternative.
Firebase makes sense when:
- You don’t want your own backend
- You use all Google services (Firestore, Cloud Functions, etc.)
Laravel makes sense when:
- You already have a Laravel backend
- You want full control over your data
- You need more complex business logic
- You want to save costs
Conclusion
With this implementation, you have a secure Google Sign-In integration in your Flutter app WITHOUT Firebase with Laravel backend validation. Let me summarize the most important points:
- No Firebase: Saves costs and complexity
- Laravel Socialite: Easy integration with
userFromToken() - Client-Side: Flutter gets ID token from Google
- Server-Side: Laravel validates token with Google
- Database: User is stored with Google ID in your DB
- Security: Token is validated server-side
- UX: Simple one-tap login for users
- Gmail Normalization: Prevents duplicate accounts
I use this method in production in my Twork App and it has proven to be very reliable and secure! Already in the first few weeks, daily registrations have doubled. Ok, admittedly from 0-1 to 1-2, but hey, it’s a start! 🚀
Further Resources
Do you have questions or problems with the implementation? Contact me directly!
Subscribe to Newsletter
Get updates on new blog posts, tutorials and tips about Flutter, Laravel and more.