coderqh

Sign in with Google and Apple in Flutter

Short answer: use google_sign_in for Google and sign_in_with_apple for Apple, or wrap both with google_apple_sign_in. On Android, Google needs a Web OAuth client id plus one Android client per signing key's SHA-1. On iOS, Apple needs the Sign in with Apple capability. Then send the id token to Firebase or your backend.

The code is the easy part. Most of the time goes into console setup, and most of the bugs only show up in release builds or in App Review. This guide walks through the four configurations in the order they usually break, then shows what to do with the token.

What you are building

A login screen with two buttons that work on both platforms:

AndroidiOS
GoogleNative sheet (Credential Manager)Native sheet
AppleWeb flow in a browser tabNative sheet

Both buttons return the same thing: an id token (a signed JWT) that proves who the user is. Your app never handles a password.

Step 0: one interface for both providers

If you call the two SDKs directly, you end up with two sets of exceptions, two result types and platform checks scattered across your UI. A thin interface keeps the login screen simple:

final signIn = PlatformSocialSignIn(const SocialSignInConfig(
  googleClientId: 'IOS_CLIENT_ID.apps.googleusercontent.com',
  googleServerClientId: 'WEB_CLIENT_ID.apps.googleusercontent.com',
  appleSignIn: true,
));

// Which buttons to show on this platform
for (final provider in signIn.providers) { /* build a button */ }

try {
  final c = await signIn.signIn(SocialProvider.google);
  // c.idToken, c.email, c.displayName (and c.rawNonce for Apple)
} on SocialSignInCancelled {
  // the user closed the sheet: not an error
} on SocialSignInFailure catch (e) {
  // misconfiguration, no network, platform error
}

That is the google_apple_sign_in package (MIT). Install it with flutter pub add google_apple_sign_in. Everything below applies whether you use it or call the SDKs yourself.

Step 1: Google on Android

Google Cloud console → APIs & Services → Credentials. Configure the OAuth consent screen first if it asks.

  1. Create a Web application client. Its id goes into serverClientId (googleServerClientId in the package). This is the step people skip: without a Web client id, Android signs the user in but returns no id token, so your backend has nothing to verify.
  2. Create an Android client for every key that signs your app. Each one needs your package name and that key's SHA-1:
    • your debug key: keytool -list -v -keystore ~/.android/debug.keystore -alias androiddebugkey -storepass android -keypass android
    • your upload key: the same command on your upload keystore, or cd android && ./gradlew signingReport
    • Google Play's app signing key: Play Console → your app → App integrity → App signing.

The third one causes the classic "works on my phone, fails for everyone on the Play Store" bug. Play re-signs your app with its own key, so the SHA-1 your users run is not the one on your machine.

You do not need google-services.json for Google sign-in itself. You need it only if you also use Firebase.

Step 2: Google on iOS

  1. Create an iOS client with your bundle id. Its id is clientId (googleClientId).
  2. Add the reversed client id as a URL scheme in ios/Runner/Info.plist:
<key>CFBundleURLTypes</key>
<array>
  <dict>
    <key>CFBundleURLSchemes</key>
    <array>
      <string>com.googleusercontent.apps.YOUR_IOS_CLIENT_ID</string>
    </array>
  </dict>
</array>
  1. Set the Web client id from step 1 as serverClientId here too. Then the token's audience is the same on both platforms, and your backend checks one value.

Step 3: Apple on iOS, and App Store guideline 4.8

  1. Apple Developer → Certificates, Identifiers & Profiles → Identifiers → your App ID → enable Sign in with Apple.
  2. Xcode → Runner target → Signing & Capabilities → + Capability → Sign in with Apple.

Do both before you show the button. On an App ID without the capability, every tap fails.

Guideline 4.8 is why Apple is hard to skip: an iOS app that offers a third-party login such as Google must also offer an equivalent login option that limits data to name and email, lets users hide their email, and does not track them. Sign in with Apple meets all three out of the box, so it is the usual answer, and a Google button alone on iOS is a common rejection. The package takes the safe default. providers hides the Google button on iOS until appleSignIn is true, and a debug build prints why in the console. Android shows whatever you configured.

Two Apple behaviors to plan for:

  • Apple sends the user's name and email only on the first sign-in for that Apple ID. Save them then. Later sign-ins return no name.
  • The email may be a private relay address (an address at privaterelay.appleid.com). Treat it as a real address that forwards; do not use it as the user's identity.

Step 4: Apple on Android

Android has no native Apple sign-in, so the flow runs in a browser tab and comes back through your server:

  1. Apple Developer → Identifiers → Services IDs → create one, for example com.example.app.signin. That is appleServiceId.
  2. Turn on Sign in with Apple for it: choose your primary App ID, add your domain, and add a Return URL on your server, for example https://api.example.com/auth/apple/callback. That is appleRedirectUri.
  3. That endpoint receives Apple's form POST and redirects into the app: intent://callback?<the posted fields>#Intent;package=YOUR.PACKAGE.NAME;scheme=signinwithapple;end
  4. Register the callback activity in AndroidManifest.xml:
<activity
    android:name="com.aboutyou.dart_packages.sign_in_with_apple.SignInWithAppleCallback"
    android:exported="true">
  <intent-filter>
    <action android:name="android.intent.action.VIEW" />
    <category android:name="android.intent.category.DEFAULT" />
    <category android:name="android.intent.category.BROWSABLE" />
    <data android:scheme="signinwithapple" android:path="/callback" />
  </intent-filter>
</activity>

If you do not have a server yet, skip Apple on Android. Google covers Android users, and guideline 4.8 only applies to iOS. The package offers the Apple button on Android only when appleServiceId is set.

Step 5: use the token

With Firebase Auth

final c = await signIn.signIn(provider);
final AuthCredential cred = switch (c.provider) {
  SocialProvider.google => GoogleAuthProvider.credential(idToken: c.idToken),
  SocialProvider.apple => OAuthProvider('apple.com').credential(
      idToken: c.idToken,
      rawNonce: c.rawNonce,
      accessToken: c.authorizationCode,
    ),
};
await FirebaseAuth.instance.signInWithCredential(cred);

Apple with Firebase needs a nonce: you send Apple the SHA-256 of a random string and send Firebase the raw string, so Firebase can check that the token was issued for this request. The package generates the pair and returns the raw value as rawNonce.

With your own backend

Post the id token (and rawNonce for Apple) and verify it on the server. Never trust the email or name the app sends alongside it.

  1. Check the signature against the provider's public keys: https://www.googleapis.com/oauth2/v3/certs for Google, https://appleid.apple.com/auth/keys for Apple.
  2. Check iss: https://accounts.google.com or accounts.google.com for Google; https://appleid.apple.com for Apple.
  3. Check aud: your Web client id for Google; your bundle id (iOS) or Services ID (Android) for Apple.
  4. Check exp, and for Apple check that the nonce claim equals SHA-256 of rawNonce.
  5. Create or find the user by the token's sub. Do not use email as the key: Apple's can be a relay, and either provider's can change.

Most server frameworks have a JWT library that does steps 1 to 4 when you give it the keys URL and expected values.

Testing without real accounts

Make your login screen depend on the SocialSignIn interface and inject a fake in tests:

final fake = FakeSocialSignIn();
await loginController(fake).tapGoogle();
expect(fake.calls, 1);

fake.cancel = true; // the next tap behaves like the user closing the sheet

And you can check which buttons iOS would show without a device:

const c = SocialSignInConfig(googleServerClientId: 'x');
expect(c.offered(applePlatform: true), isEmpty); // guideline 4.8

Checklist before release

  • Web client id set as serverClientId on both platforms
  • Android clients for debug, upload and Play app signing SHA-1
  • iOS reversed client id in Info.plist
  • Sign in with Apple enabled on the App ID and in Xcode
  • Google not offered alone on iOS (guideline 4.8)
  • Name and email saved on the first Apple sign-in
  • Backend verifies signature, iss, aud, exp and keys users on sub

FAQ

Do I need Firebase to add Google and Apple sign-in to Flutter? No. Both SDKs return an id token you can verify on any backend. Firebase is one convenient place to send it.

Why does Google sign-in return a null id token on Android? serverClientId is missing or set to the Android client id. It must be the Web application client id.

Why does Google sign-in work in debug but fail from the Play Store? The SHA-1 of Google Play's app signing key is not registered. Add another Android OAuth client with that SHA-1.

Do I have to add Sign in with Apple if my app has Google sign-in? On iOS, App Store guideline 4.8 requires an equivalent privacy-focused login next to any third-party login, and Sign in with Apple is the simplest way to meet it. On Android it is optional.

Why does Apple not return the user's name the second time? Apple only shares the name and email on the first authorization. To test the first-time flow again, remove the app under Settings → your name → Sign-In & Security → Sign in with Apple.

Can I use Sign in with Apple on Android? Yes, through a web flow. It needs a Services ID and a server endpoint as the Return URL.


The google_apple_sign_in package is open source on GitHub. It comes from the login screens in our Flutter app templates.