UGA Boxxx

つぶやきの延長のつもりで、知ったこと思ったこと書いてます

【Firebase】メールリンクと他プロバイダでログイン用のメールアドレスを紐づける

FirebaseでGoogleFacebookなど、2つ以上のログイン方法をサポートしたい場合、少し工夫がいるみたい

account-exists-with-different-credential エラーの処理より

Firebase コンソールで [1 つのメールアドレスにつき 1 つのアカウント] 設定を有効にしている場合、Firebase ユーザーが、あるプロバイダ(Google など)用にすでに存在しているメールアドレスを使って別のプロバイダ(Facebook など)にログインしようとすると、AuthCredential オブジェクト(Facebook アクセス トークン)とともにエラー auth/account-exists-with-different-credential がスローされます。目的のプロバイダにログインするには、まず既存のプロバイダ(Google)にログインしてから、目的のプロバイダの AuthCredential(Facebook アクセス トークン)にリンクする必要があります。

例えばGoogleアカウントで既に使用しているメアドでログインしたことがあり、さらにFacebookアカウントもそのメアドを使用していて、今度はFacebookアカウントでログインをしようとする場合、エラーが発生するためエラーキャッチ後に自力で紐付けをしなくてはならないというものだ

上に貼ったリンクでは他プロバイダとFacebookアカウントを紐付けるフローが書かれているのだがメールリンク認証の紐付けをする場合はさらにもう一工夫が必要だったので調査した

メールリンクとFacebookアカウントでログイン用のメールアドレスを紐づける

他プロバイダとFacebookアカウントを紐付けるフローの大まかな流れは(下のコードを参照)、Step1でエラーをキャッチし、Step2で失敗したプロバイダのクレデンシャルpendingCred を取得し、Step3で他プロバイダで認証したあと、認証済みのユーザに対してStep4でuser.linkWithCredential メソッドをつかってpendingCred と結びつける
user.linkAndRetrieveDataWithCredentialは現在非推奨になっているので、user.linkWithCredential を使う

https://firebase.google.com/docs/reference/js/firebase.User.html#link-with-credential

// Step 1.
// User tries to sign in to Facebook.
auth.signInWithPopup(new firebase.auth.FacebookAuthProvider()).catch(function(error) {
  // An error happened.
  if (error.code === 'auth/account-exists-with-different-credential') {
    // Step 2.
    // User's email already exists.
    // The pending Facebook credential.
    var pendingCred = error.credential;
    // The provider account's email address.
    var email = error.email;
    // Get sign-in methods for this email.
    auth.fetchSignInMethodsForEmail(email).then(function(methods) {
      // Step 3.
      // If the user has several sign-in methods,
      // the first method in the list will be the "recommended" method to use.
      if (methods[0] === 'password') {
        // Asks the user their password.
        // In real scenario, you should handle this asynchronously.
        var password = promptUserForPassword(); // TODO: implement promptUserForPassword.
        auth.signInWithEmailAndPassword(email, password).then(function(user) {
          // Step 4a.
          return user.linkWithCredential(pendingCred);
        }).then(function() {
          // Facebook account successfully linked to the existing Firebase user.
          goToApp();
        });
        return;
      }
      // All the other cases are external providers.
      // Construct provider object for that provider.
      // TODO: implement getProviderForProviderId.
      var provider = getProviderForProviderId(methods[0]);
      // At this point, you should let the user know that they already has an account
      // but with a different provider, and let them validate the fact they want to
      // sign in with this provider.
      // Sign in to provider. Note: browsers usually block popup triggered asynchronously,
      // so in real scenario you should ask the user to click on a "continue" button
      // that will trigger the signInWithPopup.
      auth.signInWithPopup(provider).then(function(result) {
        // Remember that the user may have signed in with an account that has a different email
        // address than the first one. This can happen as Firebase doesn't control the provider's
        // sign in flow and the user is free to login using whichever account they own.
        // Step 4b.
        // Link to Facebook credential.
        // As we have access to the pending credential, we can directly call the link method.
        result.user.linkAndRetrieveDataWithCredential(pendingCred).then(function(usercred) {
          // Facebook account successfully linked to the existing Firebase user.
          goToApp();
        });
      });
    });
  }
});

Googleなどの認証では画面遷移(リダイレクト)させずにポップアップで認証画面を出し、thenメソッドでその結果を受け取ることで処理を完結させることができるのだが、メールリンクの場合は一度ユーザにメールを送信し、そのメールのリンクから再度ページにアクセスすることになるので、Step2で取得したpendingCred をブラウザに保存しておかなくてはならない

これらを踏まえると、メールリンクの場合はStep3でpendingCredの保存とメール送信を行う必要があり、Step4の紐付けはメールリンクをクリックして着地した画面で行う必要がある

// Step 1.
// User tries to sign in to Facebook.
auth.signInWithPopup(new firebase.auth.FacebookAuthProvider()).catch(function(error) {
  // An error happened.
  if (error.code === 'auth/account-exists-with-different-credential') {
    // Step 2.
    // User's email already exists.
    // The pending Facebook credential.
    var pendingCred = error.credential;
    // The provider account's email address.
    var email = error.email;
    // Get sign-in methods for this email.
    auth.fetchSignInMethodsForEmail(email).then(function(methods) {
      // Step 3.
      if (methods[0] === 'emailLink') {
        // Preserve the pending Facebook credential.
        window.localStorage.setItem("pendingCred", pendingCred.toJSON());
        // Send email.
        firebase.auth().sendSignInLinkToEmail(email, actionCodeSettings)
      }
    });
  }
});

メールリンククリック後の着地画面

// Step4
var pendingCred = window.localStorage.getItem("pendingCred");
firebase
  .auth()
  .signInWithEmailLink(email, window.location.href)
  .then(function(result) {
    var user = result.user;
    return user.linkWithCredential(firebase.auth.AuthCredential.fromJSON(pendingCred))
  });

これで紐づけることができる

pendingCredの保存

保留された認証情報はブラウザにキャッシュする必要があるが、単純にlocalStorageに入れるだけではダメで、シリアライズとデシリアライズが必要だった

認証情報(AuthCredentail)にはシリアライズとデシリアライズのメソッドが用意されているのでそれを使う

AuthCredential | JavaScript SDK  |  Firebase