Search code examples
androidflutterfirebaseflutter-dependenciesapple-sign-in

([firebase_auth/unknown] null) when Sign In with Apple on android Flutter


I've implemented Apple sign in for my app using sign_in_with_apple: ^4.3.0 plugin and on iOS it all works as expected, but on Android signInWithCredential throws [firebase_auth/unknown] null error.

I created a ServiceID and I added my firebase project url in Domains and Subdomains and the auth handler from Firebase console in Return URLs. This was working fine with iOS but on Android I was getting Unable to process request due to missing initial state. error. Searching for the error I found to that Firebase doesn't work well with it (https://github.com/firebase/firebase-js-sdk/issues/4256) and I saw that others set a Cloud Function for it(How to write Firebase Functions for Sign in with Apple on Flutter Android). So I set up an end point on my Node.js server run-in on AKS and added the server domain in Domains and Subdomains and end point url in Return URLs of the ServiceID. enter image description here

The Apple sign in flow and pop up are correct. I receive the authorised Apple credentials but one difference I see is that the userIdentifier is null while on iOS is not, and on Android authorizationCode parameter is missing completely.

Future<Map<String, dynamic>> authorizeAppleIdCredentials() async {
    final rawNonce = _generateNonce();
    final nonce = _sha256ofString(rawNonce);

    final Uri uri = Uri.parse(
        'https://xxx.xxxxx.cloudapp.azure.com/server/api/apple_callback'
        // 'https://xxxx-xxxx.firebaseapp.com/__/auth/handler' // still getting the Unable to process request due to missing initial state error.
        );
    WebAuthenticationOptions webOptions =
        WebAuthenticationOptions(clientId: 'xxxx', redirectUri: uri);

    /// Request credential for the currently signed in Apple account.
    final AuthorizationCredentialAppleID appleIdCredential =
        await SignInWithApple.getAppleIDCredential(
      webAuthenticationOptions: webOptions,
      scopes: [
        AppleIDAuthorizationScopes.email,
        AppleIDAuthorizationScopes.fullName,
      ],
      nonce: nonce,
    ).catchError((e) {
      print(
          'UserRepository.authorizeAppleIDCredentials() SignInWithApple.getAppleIDCredential error: $e');
    });

    Map<String, dynamic> authorizedAppleCredential = {
      'appleIdCredential': {
        'email': appleIdCredential.email,
        'familyName': appleIdCredential.familyName,
        'givenName': appleIdCredential.givenName,
        'userIdentifier': appleIdCredential.userIdentifier,
        'identityToken': appleIdCredential.identityToken,
        'authorizationCode': appleIdCredential.authorizationCode,
        'state': appleIdCredential.state
      },
      'rawNonce': rawNonce
    };
    dev.log(
        'UserRepository.authorizeAppleIDCredentials() SignInWithApple.getAppleIDCredential is $appleIdCredential\n'
        'authorizedAppleCredential is $authorizedAppleCredential');
    return authorizedAppleCredential;
  }

on Android

authorizedAppleCredential is {appleIdCredential: {email: myMAil@gmail.com, familyName: surname, givenName: vincenzo, userIdentifier: null, identityToken: some token, state: null}, rawNonce: jsH8DDAoURn1Vf8z.hvxM_sAqxu.PGGP}

on iOS

authorizeAppleIdCredentials is: {appleIdCredential: {email: myMaili@gmail.com, familyName: surname, givenName: vincenzo, userIdentifier: xxxxx.xxxxxxxxx.xxxxx, identityToken: some token, authorizationCode: some auth code, state: null}, rawNonce: BmhreGGUY.3E-Ni0_9yi3yJ_0v6bZ0Me}

I'm not sure if that is the problem, but for creating the OAuth credentials is not used, identityToken is used and that is present:

Future<OAuthCredential> createAppleOauthCredential(
      {required Map<String, dynamic> authorizedAppleCredential,
      required String rawNonce}) async {
    // Create an `OAuthCredential` from the credential returned by Apple.
    print(
        'UserRepository.createAppleOauthCredentials apple idToken is ${authorizedAppleCredential['appleIdCredential']['identityToken']}'); //correct
    print(
        'UserRepository.createAppleOauthCredentials rawNonce is ${authorizedAppleCredential['rawNonce']}'); // correct

    final oauthCredential = OAuthProvider("apple.com").credential(
      idToken: authorizedAppleCredential['appleIdCredential']['identityToken'],
      rawNonce: rawNonce,
    );
    return oauthCredential;
  }

and the prints I get from it are the same both on iOS and Android.

AuthCredential(providerId: apple.com, signInMethod: oauth, token: null, accessToken: null) 

Now using those OAuth credentials I can sign in into firebase auth without problems on iOS but not on Android.

Future<User?> signInWithCredential(
      {required AuthCredential oauthCredential}) async {
    // Future<User?> signInWithCredential({required OAuthCredential oauthCredential}) async {
    // Sign in the user with Firebase. If the nonce we generated earlier does
    // not match the nonce in `appleCredential.identityToken`, sign in will fail.
    UserCredential userCredential = await _firebaseAuth
        .signInWithCredential(oauthCredential)
        .catchError((e) {
      print('_firebaseAuth.signInWithCredential(oauthCredential) error: $e');
    });
    dev.log(
        'UserRepository.signInWithCredential() user is: ${userCredential.user}');

    return userCredential.user;
  }

and the print is _firebaseAuth.signInWithCredential(oauthCredential) error: [firebase_auth/unknown] null.

This is the endpoint

exports.callback = async (req, res) => {
  const package_name = 'xxxxx';
    const redirect = `intent://callback?${new URLSearchParams(
      req.body
    ).toString()}#Intent;package=${package_name};scheme=signinwithapple;end`;

    console.log('apple callback req is: ', req);
    console.log(`Redirecting to ${redirect}`);

    res.redirect(307, redirect);

};

And this is the AndroidManifest.xml

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    package="xxxxx"
    >

    <!-- io.flutter.app.FlutterApplication is an android.app.Application that
         calls FlutterMain.startInitialization(this); in its onCreate method.
         In most cases you can leave this as-is, but you if you want to provide
         additional functionality it is fine to subclass or reimplement
         FlutterApplication and put your custom class here. -->
    <uses-permission android:name="com.google.android.gms.permission.AD_ID" tools:node="remove"/>
    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
    <uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
    <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
    <uses-permission android:name="android.permission.VIBRATE" />
    <uses-permission android:name="android.permission.WAKE_LOCK" />
    <uses-permission android:name="android.permission.INTERNET" />
    <queries>
        <!--UrlLauncher If your app opens https URLs -->
        <intent>
            <action android:name="android.intent.action.VIEW" />
            <data android:scheme="https" />
        </intent>
    </queries>

    <application
        android:name="${applicationName}"
        android:allowBackup="false"
        android:label="xxx"
        android:icon="@mipmap/ic_launcher">
        <meta-data
            android:name="com.google.firebase.messaging.default_notification_icon"
            android:resource="@drawable/notification" />

<!--        <meta-data-->
<!--            android:name="com.google.firebase.messaging.default_notification_color"-->
<!--            android:resource="@color/colorAccent" />-->


        <activity
            android:name=".MainActivity"
            android:launchMode="singleTop"
            android:theme="@style/LaunchTheme"
            android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
            android:hardwareAccelerated="true"
            android:windowSoftInputMode="adjustResize"
            android:exported="true">

            <intent-filter>
                <action android:name="android.intent.action.MAIN"/>
                <category android:name="android.intent.category.LAUNCHER"/>


            </intent-filter>
            <intent-filter android:autoVerify="true">
                <action android:name="android.intent.action.VIEW" />
                <category android:name="android.intent.category.DEFAULT" />
                <category android:name="android.intent.category.BROWSABLE" />
                <data android:scheme="flutterstripe" android:host="safepay" />
            </intent-filter>
        </activity>

        <!-- Set up the Sign in with Apple activity, such that it's callable from the browser-redirect -->
        <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" />
                <data android:path="/callback" />
            </intent-filter>
        </activity>
<!--        <receiver android:name="cox-->
        <!-- Don't delete the meta-data below.
             This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
        <!-- Specify that the launch screen should continue being displayed -->
        <!-- until Flutter renders its first frame. -->

        <meta-data
            android:name="io.flutter.embedding.android.SplashScreenDrawable"
            android:resource="@drawable/launch_background" />

        <!-- Theme to apply as soon as Flutter begins rendering frames -->
        <meta-data
            android:name="io.flutter.embedding.android.NormalTheme"
            android:resource="@style/NormalTheme"
            />
        <meta-data
            android:name="flutterEmbedding"
            android:value="2"
            />
    </application>
</manifest>

Can you spot what I'm doing wrong?


Solution

  • I finally found out that I wasn't using the authorizationCode from the AuthorizationCredentialAppleID in OAuthProvider("apple.com").credential accessToken and once added it all works as expected, so the corrected method is:

    Future<OAuthCredential> createAppleOauthCredential(
          {required Map<String, dynamic> authorizedAppleCredential,
          required String rawNonce}) async {
        // Create an `OAuthCredential` from the credential returned by Apple.
        log('UserRepository.createAppleOauthCredentials apple idToken is ${authorizedAppleCredential['appleIdCredential']['identityToken']}');
        log('UserRepository.createAppleOauthCredentials rawNonce is ${authorizedAppleCredential['rawNonce']}');
        final oauthCredential = OAuthProvider("apple.com").credential(
          idToken: authorizedAppleCredential['appleIdCredential']['identityToken'],
          accessToken: authorizedAppleCredential['appleIdCredential']
              ['authorizationCode'], // new
          rawNonce: rawNonce,
        );
        return oauthCredential;
      }