JavaScript でメールリンクを使用して Firebase 認証を行う

Firebase Authentication を使用すると、ログイン用のリンクを含むメールをユーザーに送信し、ログインしてもらうことができます。このプロセスでは、ユーザーのメールアドレスの検証も行います。

メールでのログインには、次のような利点があります。

  • 登録とログインが簡単になります。
  • アプリ間でパスワードが再利用されるリスクが低くなります。パスワードを再利用すると、適切なパスワードを選択していても、セキュリティが低下するおそれがあります。
  • ユーザー認証で、ユーザーがメールアドレスの正当な所有者であることも確認できます。
  • アクセス可能なメール アカウントがあればそれだけでログインできます。電話番号やソーシャル メディアのアカウントは必要ありません。
  • パスワードを入力(あるいは記憶)しなくても、安全にログインできます。モバイル デバイスでは、パスワードの入力や記憶が面倒な場合があります。
  • 前にメール ID(パスワードや認証連携)でログインしたユーザーをメールのみによるログインにアップグレードできます。たとえば、パスワードを忘れてしまった場合、パスワードを再設定しなくてもログインできます。

始める前に

初期化スニペットを Firebase コンソールからプロジェクトにコピーしていない場合は、Firebase を JavaScript プロジェクトに追加するの説明に沿って初期化スニペットをコピーします。

メールリンクでユーザーをログインさせるには、Firebase プロジェクトでメール プロバイダとメールリンクのログイン方法を有効にする必要があります。

  1. Firebase コンソールで [Auth] セクションを開きます。
  2. [Sign-in method] タブで [メール / パスワード] を有効にします。メールリンク ログインを使用するには、メール / パスワードによるログインを有効にする必要があります。
  3. 同じセクションで、ログイン方法として [メールリンク(パスワードなしでログイン)] を有効にします。
  4. [保存] をクリックします。

認証フローを開始するには、ユーザーにメールアドレスの入力を求めるインターフェースを表示します。次に、sendSignInLinkToEmail を呼び出し、ユーザーのメールアドレスに認証リンクを送信するように Firebase にリクエストします。

  1. メールリンクの作成方法を Firebase に伝える ActionCodeSettings オブジェクトを作成します。次のフィールドを設定します。

    • url: メールに埋め込むディープリンク。状態も一緒に渡します。リンクのドメインは、Firebase コンソールで認可済みドメインのリストに追加する必要があります。これは、[Sign-in method] タブ([Authentication] -> [Settings])で行えます。
    • androidios: Android または Apple デバイスでログインリンクを開くアプリ。モバイルアプリ経由でメールのアクション リンクを開く方法については、Firebase Dynamic Links の構成をご覧ください。
    • handleCodeInApp: true に設定します。他の帯域外メール アクション(パスワードのリセットやメールアドレスの確認など)と異なり、ログインはアプリ内で完結させる必要があります。これは、フローの最後でユーザーがログインに成功し、認証状態がアプリ内で維持される必要があるためです。
    • dynamicLinkDomain: 1 つのプロジェクトに複数のカスタム ダイナミック リンク ドメインが定義されている場合、指定のモバイルアプリ経由でリンクを開く際に使用するドメインを指定します(example.page.link など)。指定しない場合、最初のドメインが自動的に選択されます。

      Web

      const actionCodeSettings = {
        // URL you want to redirect back to. The domain (www.example.com) for this
        // URL must be in the authorized domains list in the Firebase Console.
        url: 'https://www.example.com/finishSignUp?cartId=1234',
        // This must be true.
        handleCodeInApp: true,
        iOS: {
          bundleId: 'com.example.ios'
        },
        android: {
          packageName: 'com.example.android',
          installApp: true,
          minimumVersion: '12'
        },
        dynamicLinkDomain: 'example.page.link'
      };

      Web

      var actionCodeSettings = {
        // URL you want to redirect back to. The domain (www.example.com) for this
        // URL must be in the authorized domains list in the Firebase Console.
        url: 'https://www.example.com/finishSignUp?cartId=1234',
        // This must be true.
        handleCodeInApp: true,
        iOS: {
          bundleId: 'com.example.ios'
        },
        android: {
          packageName: 'com.example.android',
          installApp: true,
          minimumVersion: '12'
        },
        dynamicLinkDomain: 'example.page.link'
      };

    ActionCodeSettings の詳細については、メール アクションで状態を渡すをご覧ください。

  2. ユーザーにメールアドレスを求めます。

  3. ユーザーのメールアドレスに認証リンクを送信し、ユーザーが同じデバイスでメールによるログインを完了する場合に備えてこのメールアドレスを保存します。

    Web

    import { getAuth, sendSignInLinkToEmail } from "firebase/auth";
    
    const auth = getAuth();
    sendSignInLinkToEmail(auth, email, actionCodeSettings)
      .then(() => {
        // The link was successfully sent. Inform the user.
        // Save the email locally so you don't need to ask the user for it again
        // if they open the link on the same device.
        window.localStorage.setItem('emailForSignIn', email);
        // ...
      })
      .catch((error) => {
        const errorCode = error.code;
        const errorMessage = error.message;
        // ...
      });

    Web

    firebase.auth().sendSignInLinkToEmail(email, actionCodeSettings)
      .then(() => {
        // The link was successfully sent. Inform the user.
        // Save the email locally so you don't need to ask the user for it again
        // if they open the link on the same device.
        window.localStorage.setItem('emailForSignIn', email);
        // ...
      })
      .catch((error) => {
        var errorCode = error.code;
        var errorMessage = error.message;
        // ...
      });

セキュリティに関する懸念

意図しないユーザーやデバイスがログインリンクでログインしないように、Firebase Auth はログインフローの完了時にユーザーにメールアドレスの入力を求めます。入力されたメールアドレスがログインリンクの送信アドレスと一致しないと、ログインに失敗します。

ログインメールの送信時に、localStorage や Cookie などを使用してメールアドレスをローカルに保存しておくことで、ユーザーがリンクをリクエストしたときと同じデバイスでログインリンクを開く場合の認証フローが簡単になります。メールアドレスが一致した場合、このアドレスを使用してフローを完了します。セッション インジェクションの危険性があるため、ユーザーのメールアドレスをリダイレクト URL パラメータに渡して再利用しないでください。

ログインが完了すると、未確認のログインはすべて破棄され、既存のセッションが無効になります。たとえば、未確認のアカウントが同じメールとパスワードですでに作成されている場合、所有者を装って未確認アカウントを作成した人物が未確認のメールアドレスとパスワードでログインできないように、ユーザーのパスワードが削除されます。

中間のサーバーでリンクがインターセプトされないように、本番環境では HTTPS URL を使用してください。

ウェブページでログインを完了する

メールリンクで使用するディープリンクの形式は、帯域外メール アクションで使用される形式(メールアドレスの確認、パスワードのリセット、メールアドレスの変更取り消しなどで使用される形式)と同じです。Firebase Auth は isSignInWithEmailLink API を提供することにより、リンクがメールリンクによるログインかどうかを判断する作業を簡易にします。

ランディング ページでログインを完了するには、signInWithEmailLink を呼び出し、ワンタイム コードを含む実際のメールリンクとユーザーのメールアドレスを渡します。

Web

import { getAuth, isSignInWithEmailLink, signInWithEmailLink } from "firebase/auth";

// Confirm the link is a sign-in with email link.
const auth = getAuth();
if (isSignInWithEmailLink(auth, window.location.href)) {
  // Additional state parameters can also be passed via URL.
  // This can be used to continue the user's intended action before triggering
  // the sign-in operation.
  // Get the email if available. This should be available if the user completes
  // the flow on the same device where they started it.
  let email = window.localStorage.getItem('emailForSignIn');
  if (!email) {
    // User opened the link on a different device. To prevent session fixation
    // attacks, ask the user to provide the associated email again. For example:
    email = window.prompt('Please provide your email for confirmation');
  }
  // The client SDK will parse the code from the link for you.
  signInWithEmailLink(auth, email, window.location.href)
    .then((result) => {
      // Clear email from storage.
      window.localStorage.removeItem('emailForSignIn');
      // You can access the new user by importing getAdditionalUserInfo
      // and calling it with result:
      // getAdditionalUserInfo(result)
      // You can access the user's profile via:
      // getAdditionalUserInfo(result)?.profile
      // You can check if the user is new or existing:
      // getAdditionalUserInfo(result)?.isNewUser
    })
    .catch((error) => {
      // Some error occurred, you can inspect the code: error.code
      // Common errors could be invalid email and invalid or expired OTPs.
    });
}

Web

// Confirm the link is a sign-in with email link.
if (firebase.auth().isSignInWithEmailLink(window.location.href)) {
  // Additional state parameters can also be passed via URL.
  // This can be used to continue the user's intended action before triggering
  // the sign-in operation.
  // Get the email if available. This should be available if the user completes
  // the flow on the same device where they started it.
  var email = window.localStorage.getItem('emailForSignIn');
  if (!email) {
    // User opened the link on a different device. To prevent session fixation
    // attacks, ask the user to provide the associated email again. For example:
    email = window.prompt('Please provide your email for confirmation');
  }
  // The client SDK will parse the code from the link for you.
  firebase.auth().signInWithEmailLink(email, window.location.href)
    .then((result) => {
      // Clear email from storage.
      window.localStorage.removeItem('emailForSignIn');
      // You can access the new user via result.user
      // Additional user info profile not available via:
      // result.additionalUserInfo.profile == null
      // You can check if the user is new or existing:
      // result.additionalUserInfo.isNewUser
    })
    .catch((error) => {
      // Some error occurred, you can inspect the code: error.code
      // Common errors could be invalid email and invalid or expired OTPs.
    });
}

モバイルアプリでログインを完了する

Firebase Authentication は、Firebase Dynamic Links を使用してモバイル デバイスにメールリンクを送信します。ウェブフローと同様に、モバイルアプリ経由でログインを行う場合にも、リンクの受信を検出し、ディープリンクを解析してログインを完了するようにアプリを構成する必要があります。

Android アプリでメールリンクによるログインを処理する方法については、Android のガイドをご覧ください。

Apple アプリでメールリンクによるログインを処理する方法については、Apple プラットフォームのガイドをご覧ください。

この認証方法は既存ユーザーの認証でも利用できます。たとえば、ユーザーが別のプロバイダにおいて電話番号などで認証されている場合、既存のアカウントにメールリンクによる認証方法を追加できます。

オペレーションの後半部分が異なります。

Web

import { getAuth, linkWithCredential, EmailAuthProvider } from "firebase/auth";

// Construct the email link credential from the current URL.
const credential = EmailAuthProvider.credentialWithLink(
  email, window.location.href);

// Link the credential to the current user.
const auth = getAuth();
linkWithCredential(auth.currentUser, credential)
  .then((usercred) => {
    // The provider is now successfully linked.
    // The phone user can now sign in with their phone number or email.
  })
  .catch((error) => {
    // Some error occurred.
  });

Web

// Construct the email link credential from the current URL.
var credential = firebase.auth.EmailAuthProvider.credentialWithLink(
  email, window.location.href);

// Link the credential to the current user.
firebase.auth().currentUser.linkWithCredential(credential)
  .then((usercred) => {
    // The provider is now successfully linked.
    // The phone user can now sign in with their phone number or email.
  })
  .catch((error) => {
    // Some error occurred.
  });

これは、重要なオペレーションの前にメールリンク ユーザーを再認証する場合にも使用できます。

Web

import { getAuth, reauthenticateWithCredential, EmailAuthProvider } from "firebase/auth";

// Construct the email link credential from the current URL.
const credential = EmailAuthProvider.credentialWithLink(
  email, window.location.href);

// Re-authenticate the user with this credential.
const auth = getAuth();
reauthenticateWithCredential(auth.currentUser, credential)
  .then((usercred) => {
    // The user is now successfully re-authenticated and can execute sensitive
    // operations.
  })
  .catch((error) => {
    // Some error occurred.
  });

Web

// Construct the email link credential from the current URL.
var credential = firebase.auth.EmailAuthProvider.credentialWithLink(
  email, window.location.href);

// Re-authenticate the user with this credential.
firebase.auth().currentUser.reauthenticateWithCredential(credential)
  .then((usercred) => {
    // The user is now successfully re-authenticated and can execute sensitive
    // operations.
  })
  .catch((error) => {
    // Some error occurred.
  });

ただし、元のユーザーがログインしていない別のデバイスでフローを完了しようとすると、フローが完了できなくなります。この場合、ユーザーにエラーを表示し、強制的に同じデバイスでリンクを開かせます。オペレーションの種類やユーザーの UID の情報を提供するため、リンクと一緒に状態を渡すこともできます。

2023 年 9 月 15 日以降にプロジェクトを作成した場合、メール列挙保護はデフォルトで有効になっています。この機能により、プロジェクトのユーザー アカウントのセキュリティが強化されますが、以前は ID 優先フローを実装する際に推奨されていた fetchSignInMethodsForEmail() メソッドが無効になります。

プロジェクトでメール列挙保護を無効にすることはできますが、無効にしないことをおすすめします。

詳細については、メール列挙保護に関するドキュメントをご覧ください。

リンクによるログインのデフォルトのメール テンプレート

デフォルト メール テンプレートの件名とメール本文にはタイムスタンプが含まれるため、後続のメールが 1 つのスレッドにまとめられ、リンクが非表示になることはありません

このテンプレートは、次の言語に適用されます。

コード 言語
ar アラビア語
zh-CN 中国語 (簡体)
zh-TW 中国語 (繁体)
nl オランダ語
en 英語
en-GB 英語 (イギリス)
fr フランス語
de ドイツ語
id インドネシア語
it イタリア語
ja 日本語
ko 韓国語
pl ポーランド語
pt-BR ポルトガル語 (ブラジル)
pt-PT ポルトガル語 (ポルトガル)
ru ロシア語
es スペイン語
es-419 スペイン語(ラテンアメリカ)
th タイ語

次のステップ

ユーザーが初めてログインすると、新しいユーザー アカウントが作成され、ユーザーがログイン時に使用した認証情報(ユーザー名とパスワード、電話番号、または認証プロバイダ情報)にアカウントがリンクされます。この新しいアカウントは Firebase プロジェクトの一部として保存され、ユーザーのログイン方法にかかわらず、プロジェクトのすべてのアプリでユーザーを識別するために使用できます。

  • アプリでユーザーの認証ステータスを把握するには、Auth オブジェクトにオブザーバーを設定することをおすすめします。これによって、ユーザーの基本的なプロフィール情報を User オブジェクトから取得できます。ユーザーを管理するをご覧ください。

  • Firebase Realtime DatabaseCloud Storageセキュリティ ルールでは、ログイン済みユーザーの一意のユーザー ID を auth 変数から取得し、それを使用して、ユーザーがアクセスできるデータを制御できます。

既存のユーザー アカウントに認証プロバイダの認証情報をリンクすることで、ユーザーは複数の認証プロバイダを使用してアプリにログインできるようになります。

ユーザーのログアウトを行うには、signOut を呼び出します。

Web

import { getAuth, signOut } from "firebase/auth";

const auth = getAuth();
signOut(auth).then(() => {
  // Sign-out successful.
}).catch((error) => {
  // An error happened.
});

Web

firebase.auth().signOut().then(() => {
  // Sign-out successful.
}).catch((error) => {
  // An error happened.
});