Google & Apple Sign-In in Flutter

Introduction

A production-grade walkthrough clean auth service, real code from a real app, and integration patterns. Every additional step between a user opening your app and being inside it is a drop-off point. Google and Apple Sign-In are the fastest path from “curious” to “engaged” two taps and you’re in, no password to forget.

01 / Prerequisites

  • A Firebase project with Authentication enabled Google and Apple providers turned on.
  • FlutterFire CLI installed and flutterfire configure already run.
  • For Apple Sign-In: an Apple Developer account with Sign In with Apple capability enabled.
  • For Android Google Sign-In: SHA-1 fingerprint registered in Firebase Console.

02 / Dependencies & Setup

Add to pubspec.yaml

dependencies:
  flutter_dotenv: ^5.2.1
  firebase_core: ^3.10.1
  firebase_auth: ^5.5.1
  google_sign_in: ^7.2.0
  sign_in_with_apple: ^7.0.1

flutter:
  assets:
    - .env

Then run:

flutter pub get

Create your .env file

Store sensitive config outside of source code. Create a .env file in your project root and add it to .gitignore:

SERVER_CLIENT_ID=123456789-abcdefg.apps.googleusercontent.com

Initialize Firebase in main.dart

You must call GoogleSignIn.instance.initialize() before runApp(), after Firebase is ready:

import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:your_app/service/auth_service.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();

  await dotenv.load(fileName: '.env');

  await Firebase.initializeApp(
    options: DefaultFirebaseOptions.currentPlatform,
  );

  await AuthService().initialize();

  runApp(const MyApp());
}

⚠️ If you call authenticate() or authorizationClient before initialize(), Flutter throws a StateError: instance not initialized. This crashes silently in release builds. Always initialize first.

03 / Getting the Server Client ID

The serverClientId is the Web Client ID from Google Cloud Console not the Android or iOS client. It is required for Google Sign-In on Android and enables server-side token verification.

  1. Go to console.cloud.google.com and select your Firebase project.
  2. Navigate to APIs & Services → Credentials.
  3. Under OAuth 2.0 Client IDs, find the entry with type “Web application” usually named “Web client (auto created by Google Service)”.
  4. Click it and copy the Client ID field it ends in .apps.googleusercontent.com.
  5. Paste it into your .env file as SERVER_CLIENT_ID=.

Can’t find the Web Client? Go to Firebase Console → Authentication → Sign-in method → Google → expand arrow. Firebase will show the Web SDK configuration containing the same client ID.

04 / The Clean AuthService

Here’s the full authentication service auth logic only, no database concerns. Use it as a foundation and add your own data layer on top.

lib/service/auth_service.dart

import 'dart:developer' as log;
import 'dart:math';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:google_sign_in/google_sign_in.dart';
import 'package:sign_in_with_apple/sign_in_with_apple.dart';

class AuthService {
  static final AuthService _instance = AuthService._internal();
  factory AuthService() => _instance;
  AuthService._internal();

  final FirebaseAuth _auth = FirebaseAuth.instance;
  final GoogleSignIn _googleSignIn = GoogleSignIn.instance;

  String get _serverClientId => dotenv.env['SERVER_CLIENT_ID'] ?? '';

  User? get currentUser => _auth.currentUser;
  bool get isSignedIn => currentUser != null;

  Future initialize() async {
    try {
      await _googleSignIn.initialize(serverClientId: _serverClientId);
      log.log('[AuthService] Google Sign-In initialized');
    } catch (e) {
      log.log('[AuthService] Google init error: $e');
      rethrow;
    }
  }

  Future signInWithGoogle() async {
    await _googleSignIn.signOut();
    final GoogleSignInAccount googleUser =
        await _googleSignIn.authenticate(scopeHint: ['email']);
    final GoogleSignInAuthentication googleAuth = googleUser.authentication;
    final authClient = _googleSignIn.authorizationClient;
    final authorization =
        await authClient.authorizationForScopes(['email', 'profile']);
    final credential = GoogleAuthProvider.credential(
      idToken: googleAuth.idToken,
      accessToken: authorization?.accessToken,
    );
    return _auth.signInWithCredential(credential);
  }

  Future signInWithApple() async {
    final rawNonce = _generateNonce();
    final appleCredential = await SignInWithApple.getAppleIDCredential(
      scopes: [
        AppleIDAuthorizationScopes.email,
        AppleIDAuthorizationScopes.fullName,
      ],
    );
    final oAuthCredential = OAuthProvider('apple.com').credential(
      idToken: appleCredential.identityToken,
      rawNonce: rawNonce,
      accessToken: appleCredential.authorizationCode,
    );
    return _auth.signInWithCredential(oAuthCredential);
  }

  Future signOut() async {
    await Future.wait([
      _auth.signOut(),
      _googleSignIn.signOut(),
    ]);
  }

  static String getFirebaseErrorMessage(FirebaseAuthException e) {
    switch (e.code) {
      case 'user-not-found': return 'No account found with this email.';
      case 'wrong-password': return 'Incorrect password.';
      case 'email-already-in-use': return 'This email is already registered.';
      case 'invalid-email': return 'Please enter a valid email.';
      case 'weak-password': return 'Password is too weak.';
      case 'too-many-requests': return 'Too many attempts. Try again later.';
      case 'network-request-failed': return 'No internet connection.';
      case 'operation-not-allowed': return 'This sign-in method is not enabled.';
      case 'account-exists-with-different-credential':
        return 'An account with this email exists with a different sign-in method.';
      default: return 'Authentication failed. Please try again.';
    }
  }

  String _generateNonce([int length = 32]) {
    const charset =
        '0123456789ABCDEFGHIJKLMNOPQRSTUVXYZabcdefghijklmnopqrstuvwxyz-._';
    final random = Random.secure();
    return List.generate(
        length, (_) => charset[random.nextInt(charset.length)]).join();
  }
}

05 / Platform Configuration

Android Get SHA-1 Fingerprint

keytool -list -v \
  -keystore ~/.android/debug.keystore \
  -alias androiddebugkey \
  -storepass android -keypass android

Add this SHA-1 in Firebase Console → Project Settings → Your Android app → Add fingerprint. Then re-download google-services.json and place it at android/app/google-services.json.

⚠️ Add both debug and release SHA-1. Missing the release SHA-1 causes silent failures after publishing to the Play Store.

Android Verify build.gradle files

android/build.gradle

buildscript {
  dependencies {
    classpath 'com.google.gms:google-services:4.4.2'
  }
}

android/app/build.gradle

apply plugin: 'com.google.gms.google-services'

iOS Add Reversed Client ID URL Scheme

Open GoogleService-Info.plist, find the REVERSED_CLIENT_ID value, then add it to ios/Runner/Info.plist:

<key>CFBundleURLTypes</key>
<array>
  <dict>
    <key>CFBundleTypeRole</key>
    <string>Editor</string>
    <key>CFBundleURLSchemes</key>
    <array>
      <string>com.googleusercontent.apps.YOUR_REVERSED_CLIENT_ID</string>
    </array>
  </dict>
</array>

iOS Apple Sign-In Setup

  1. Open ios/Runner.xcworkspace in Xcode → Runner target → Signing & Capabilities → + Capability → Sign In with Apple.
  2. Log into developer.apple.com → Identifiers → select your App ID → check Sign In with Apple → Save. Regenerate your provisioning profile.
  3. Firebase Console → Authentication → Sign-in method → Apple → Enable. Paste your App ID as the Service ID.
  4. Xcode handles the entitlement automatically. Verify Runner.entitlements contains com.apple.developer.applesignin with value Default.

06 / Common Errors & Fixes

  • StateError: instance not initialized — Call await AuthService().initialize() in main() before runApp().
  • sign_in_failed (Android) — SHA-1 missing or incorrect. Add both debug and release SHA-1 in Firebase Console, then re-download google-services.json.
  • PlatformException: sign_in_canceled — User closed the picker. Expected behavior — handle silently without showing an error toast.
  • operation-not-allowed — Go to Firebase Console → Authentication → Sign-in method → Enable Google.
  • idToken is null — Always check googleAuth.idToken != null before creating credentials.
  • Missing REVERSED_CLIENT_ID (iOS) — Add CFBundleURLSchemes using the REVERSED_CLIENT_ID from GoogleService-Info.plist.
  • AuthorizationError: canceled (Apple) — Catch SignInWithAppleAuthorizationException and ignore when e.code == AuthorizationErrorCode.canceled.
  • Apple name is null (returning user) — Apple only provides the name on first sign-in. Save it to your database when isNewUser == true.
  • account-exists-with-different-credential — Use fetchSignInMethodsForEmail() and guide the user to link accounts.
  • Apple Sign-In not working on Simulator — iOS Simulator does not support Apple Sign-In. Always test on a real device.

07 / UI Example

Future loginWithGoogle() async {
  setState(() => isLoading = true);
  final result = await AuthService().signInWithGoogle();
  setState(() => isLoading = false);
  if (result.isSuccess) {
    _showMessage("Google Login Success ✅");
  } else {
    _showMessage(result.error ?? "Error");
  }
}

Future loginWithApple() async {
  setState(() => isLoading = true);
  final result = await AuthService().signInWithApple();
  setState(() => isLoading = false);
  if (result.isSuccess) {
    _showMessage("Apple Login Success 🍎");
  } else {
    _showMessage(result.error ?? "Error");
  }
}

08 / Conclusion & Best Practices

  • Initialize once — call AuthService().initialize() in main() after Firebase and before runApp().
  • Server Client ID = Web Client — get the Web application client from Google Cloud Console, not Android or iOS.
  • Sign out Google on each sign-in — call _googleSignIn.signOut() before authenticating so the picker always appears.
  • Both tokens for Firebase — always pass both idToken and accessToken to GoogleAuthProvider.credential().
  • Use a secure nonce for Apple — generate with Random.secure(), always pass rawNonce to the Firebase credential.
  • Save Apple name immediately — it’s only available on the first sign-in; save to your database when isNewUser == true.
  • Detect new vs returning users — use userCredential.additionalUserInfo?.isNewUser to branch your post-auth flow.
  • Sign out from both — always call both _auth.signOut() and _googleSignIn.signOut() on logout.
  • SHA-1: debug + release — add both fingerprints to Firebase or Google Sign-In breaks in production builds.
  • Test Apple on a real device — iOS Simulator does not support Apple Sign-In.