capacitor-community / apple-sign-in

Sign in with Apple Support
MIT License
135 stars 58 forks source link

Answer: using apple-sign-in with Firebase-auth #65

Closed arielhasidim closed 2 years ago

arielhasidim commented 2 years ago

Describe the problem apple-sign-in plugin v1.0.1 worked great for me with: "@capacitor/cli": "3.3.2", "@capacitor/ios": "^3.3.2", "@capacitor/angular": "^2.0.0", "@angular/cli": "^13.1.1", Angular CLI: 13.1.1

BUT when I tried to pass the credentials to Firebase SDK I encountered: ERROR {"code":"auth/missing-or-invalid-nonce","message":"Nonce is missing in the request."} (as mentioned also in this https://github.com/capacitor-community/apple-sign-in/issues/60#issuecomment-947929185)

Solution According to this documentation, when using Firebase's firebase.auth().signInWithCredential, you also required to pass "raw nonce" that, after being hashed, suppose to match the nonce that is contained in the identityToken we get back from this plugin (not sure if this is a new requirement in Firebase's side).

THE CATCH IS that you need to hash the nonce before you pass it in the SignInWithAppleOptions. I used this snippet for hashing nonce:

import {Injectable} from '@angular/core';
import firebase from 'firebase/app';
import { SignInWithApple, SignInWithAppleResponse, SignInWithAppleOptions } from '@capacitor-community/apple-sign-in';

@Injectable({
  providedIn: 'root'
})
export class AuthService {

  ...

  async appleLogin() {
    const nonce = 'nonce';
    const hashedNonceHex = await this.sha256(nonce); // see next function
    const options: SignInWithAppleOptions = {
      clientId: 'com.your.app',
      redirectURI: '',
      scopes: 'email, name',
      state: '1256',
      nonce: hashedNonceHex
    };

    SignInWithApple.authorize(options)
      .then(async (res: SignInWithAppleResponse) => {
        if (res.response && res.response.identityToken) {
          const provider = new firebase.auth.OAuthProvider('apple.com');
          const appleCredential = provider.credential({
              idToken: res.response.identityToken,
              rawNonce: nonce,
            });
          firebase.auth().signInWithCredential(appleCredential)
              .then((credential: any) => {
                console.log('credential:' + credential);
              }).catch(err => {
                console.log('signInWithCredential in apple-sign-in caught error');
                console.log(err);
              });
        }
      });
  }

  async sha256(message) {
    // encode as UTF-8
    const msgBuffer = new TextEncoder().encode(message);

    // hash the message
    const hashBuffer = await crypto.subtle.digest('SHA-256', msgBuffer);

    // convert ArrayBuffer to Array
    const hashArray = Array.from(new Uint8Array(hashBuffer));

    // convert bytes to hex string
    const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
    return hashHex;
  }
}
viking2917 commented 2 years ago

When I try this on iOS 15, the code crashes when it gets to: const hashBuffer = await crypto.subtle.digest('SHA-256', msgBuffer);

There is no 'subtle' in the window.crypto, it is undefined. Apparently crypto is only supported under secure contexts (e.g. https) (via: https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto), and some web comments have suggested crypto.subtle was present in mobile Safari until recently, but was removed, which could explain why it's not working for me. Ionic is serving the internal pages on http (or other capacitor/app/file schemes), which aren't 'secure'.

Anyone else have this issue? I hate to have to send the nonce off-device to get a SHA hash, but not finding a good alternative.

viking2917 commented 2 years ago

AWS has an Apache 2.0-licensed implementation of SHA256 that uses crypto.subtle if it exists, and implements directly if not: https://www.npmjs.com/package/@aws-crypto/sha256-browser

so, npm i @aws-crypto/sha256-browser and then

import { Sha256 } from '@aws-crypto/sha256-browser';
.....
 async sha256(message) {

    const hash = new Sha256();
    hash.update(message);
    const digest = await hash.digest();
    // convert ArrayBuffer to Array
    const hashArray = Array.from(new Uint8Array(digest));
    // convert bytes to hex string
    const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
    return hashHex;
}

worked for me.

lssilveira11 commented 1 year ago

Hi everyone!

I'm having a similar problem.

In my context, I am already authenticated anonymously in web context, so I have to link this anonymous user using appleCredential and linkWithCredential method of firebase auth module. The link works, but not e-mail was set (in Firebase Console, it shows a single "-" instead of user e-mail) and the workaround for this was to update user e-mail manually later in this process.

My problem is with signInWithCredential, that has been executed after the account has been linked. It always throws this error: Domain: Code: -1001 NSLocalizedDescription: Duplicate credential received. Please try again with a new credential.

If anyone have thoughts about this, I'll apreciate :)

lssilveira11 commented 1 year ago

After a while trying and reading about this problem, I could figure it out. To solve this, I had to retrieve the appleCredential back from the error generated from linkWithCredential.

If the user is signing-up, the auth.currentUser contains an anonymous firebase auth user and it will be converted into an non-anonymous user, linking with the apple credential correctly. Later, when user signs-out and signs-in again, the link will failed with auth/credential-already-in-use error. Following the firebase auth docs and other topics, this error object contains the firebase credential at error.credential or error.userInfo.authCredential (I am not sure in which one, it didn't becomes very clear to me), then I used this credential it in signInWithCredential, which worked gracefully then.

Follows the implementation for further references.

try {
    userCred = await auth.currentUser?.linkWithCredential(appleCredential);
  } catch (error: any) {
    const errorCode = error.code || "";
    if (errorCode === "auth/credential-already-in-use") {
      /*
        auth/credential-already-in-use
          Thrown if the account corresponding to the credential already exists among your users,
          or is already linked to a Firebase User. For example, this error could be thrown
          if you are upgrading an anonymous user to a Google user by linking a Google credential to it
          and the Google credential used is already associated with an existing Firebase Google user.
          The fields error.email, error.phoneNumber, and error.credential (firebase.auth.AuthCredential) may be provided,
          depending on the type of credential.
          You can recover from this error by signing in with error.credential directly via firebase.auth.Auth.signInWithCredential.
        */
      console.warn("Credential already in use");
      const errorCred = error.userInfo?.authCredential || error.credential;
      if (errorCred) {
        userCred = await auth.signInWithCredential(errorCred);
      }
    } else {
      throw error;
    }
  }