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

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

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

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

始める前に

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

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

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

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

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

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

      ウェブ向けのモジュラー API

      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'
      };

      ウェブ向けの名前空間付き API

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

    ウェブ向けのモジュラー API

    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;
        // ...
      });

    ウェブ向けの名前空間付き API

    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 を呼び出し、ワンタイム コードを含む実際のメールリンクとユーザーのメールアドレスを渡します。

ウェブ向けのモジュラー API

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 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.
    });
}

ウェブ向けの名前空間付き API

// 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 プラットフォームのガイドをご覧ください。

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

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

ウェブ向けのモジュラー API

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.
  });

ウェブ向けの名前空間付き API

// 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.
  });

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

ウェブ向けのモジュラー API

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.
  });

ウェブ向けの名前空間付き API

// 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 の情報を提供するため、リンクと一緒に状態を渡すこともできます。

メールのパスワードとリンクベースの両方のログイン方法に対応する場合、fetchSignInMethodsForEmail を使用して、それぞれのログイン方法を区別します。この方法は、ユーザーにメールアドレスの入力を求め、入力後にログイン方法を提示する ID 優先のフローで役立ちます。

ウェブ向けのモジュラー API

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

// After asking the user for their email.
const email = window.prompt('Please provide your email');

const auth = getAuth();
fetchSignInMethodsForEmail(auth, email)
  .then((signInMethods) => {
    // This returns the same array as fetchProvidersForEmail but for email
    // provider identified by 'password' string, signInMethods would contain 2
    // different strings:
    // 'emailLink' if the user previously signed in with an email/link
    // 'password' if the user has a password.
    // A user could have both.
    if (signInMethods.indexOf(EmailAuthProvider.EMAIL_PASSWORD_SIGN_IN_METHOD) != -1) {
      // User can sign in with email/password.
    }
    if (signInMethods.indexOf(EmailAuthProvider.EMAIL_LINK_SIGN_IN_METHOD) != -1) {
      // User can sign in with email/link.
    }
  })
  .catch((error) => {
    // Some error occurred, you can inspect the code: error.code
  });

ウェブ向けの名前空間付き API

// After asking the user for their email.
var email = window.prompt('Please provide your email');
firebase.auth().fetchSignInMethodsForEmail(email)
  .then((signInMethods) => {
    // This returns the same array as fetchProvidersForEmail but for email
    // provider identified by 'password' string, signInMethods would contain 2
    // different strings:
    // 'emailLink' if the user previously signed in with an email/link
    // 'password' if the user has a password.
    // A user could have both.
    if (signInMethods.indexOf(
            firebase.auth.EmailAuthProvider.EMAIL_PASSWORD_SIGN_IN_METHOD) != -1) {
      // User can sign in with email/password.
    }
    if (signInMethods.indexOf(
            firebase.auth.EmailAuthProvider.EMAIL_LINK_SIGN_IN_METHOD) != -1) {
      // User can sign in with email/link.
    }
  })
  .catch((error) => {
    // Some error occurred, you can inspect the code: error.code
  });

上述のように、メール / パスワードとメールリンクは、異なるログイン方法を使用する同じ firebase.auth.EmailAuthProvider(同じ PROVIDER_ID)と考えることができます。

次のステップ

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

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

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

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

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

ウェブ モジュラー API

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

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

名前空間が指定されたウェブ API

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