auth0-spa-jsを使って多要素認証(MFA) を実装する方法を調べました

2021.12.20

はじめに

私が担当している案件では Auth0 を使っており、フロントエンドではauth0-spa-jsというライブラリを使っています。多要素認証(MFA)を実装する予定があるため実装方法を調査しました。

ユーザーごとにMFAの有効、無効を選択できるようにしたかったのですが、Auth0側に必要なMFAの設定は以下の記事を見ながらやりました。今回試したのはTOTP(Time-based One-Time Password)です。MFA Proが使えるプランへの加入が必要になります。

Auth0のActionsを使って一部のユーザー向けにMFAをカスタマイズ

MFAの有効化

MFAを有効化するために行ったことは以下の2つです。

  • ログイン後に実行されるMFAをenableにするActionsを作成します。
  • [Security] - [Multi-factor Auth]のページでOne-time PasswordをEnabledにします。これだけでその後、ユニバーサルログイン画面からログインすると以下のような画面になります。ここでMFAの設定を行います。


※画像を一部マスキングしています。

WebアプリでMFAを実装するのであればMFA無効化も実装する必要があると思います。MFA無効化はMFA APIかManagement APIを使います。MFA APIを使う場合は追加で準備が必要になります。Auth0ダッシュボード > アプリケーションを選択し、Advanced Setting > Grantの種類に MFAを追加します。

MFA API

Auth0ではMFAを実装するためにMFA APIが用意されています。MFA APIを呼ぶためにはMFA Tokenというトークンが必要です。APIを見るとログインユーザーのパスワードが必要です。私の環境ではログインにはユニバーサルログインを使っていてフロントエンドではパスワードはもっていなかったので結構ハマりました。いろいろ試したところTypescriptでは以下の方法で取得できました。

audience に https://YOUR_DOMAIN/mfa/ scopeに enroll read:authenticators remove:authenticatorsを指定してログイン画面にリダイレクトさせます。YOUR_DOMAINの部分はお使いの環境によるので書き換えてください。

    const options = {
      scope: "enroll read:authenticators remove:authenticators",
      audience: "https://YOUR_DOMAIN/mfa/",
      redirect_uri: "リダイレクトしたいURI"
    } as RedirectLoginOptions;
    await $auth.loginWithRedirect(options);

再ログイン後、以下のような認証の画面になりますので許可します。


※画像を一部マスキングしています。

その後、以下のソースコードを実行するとトークンが取得できました。

    const options = {
      scope: "enroll read:authenticators remove:authenticators",
      audience: "https://YOUR_DOMAIN/mfa/"
    } as GetTokenSilentlyOptions;
    const token:IdToken = await $auth.getTokenSilently(options);

あとはドキュメントにソースコードのサンプルがあるのでそちらを参照すれば問題なく使えると思います。私はcurlで試しましたが問題ありませんでした。ログインしたユーザー以外のMFAを無効化したい場合はManagement APIを使います。

Enroll and Challenge OTP Authenticators

curl --request GET \
  --url 'https://YOUR_DOMAIN/mfa/authenticators' \
  --header 'authorization: Bearer MFA_TOKEN'

Management API

Management APIでもEnrollment(MFAの登録情報)を取得したり無効化できます。アクセストークンを取得するためにはauth0のクライアントの情報が必要になるためバックエンドで実装しました試したソースコードを載せておきます。メソッドの引数にはアクセストークンとユーザーIDを渡してください。

Enrollmentsの取得

import request = require("request");
・・・

  public async getEnrollments(token: string, userId: string): Promise<string> {
    const url = `https://YOUR_DOMAIN/api/v2/users/${encodeURIComponent(userId)}/enrollments`;
    const options = {
      method: "GET",
      url: url ,
      headers: {
        authorization: `Bearer ${token}`,
      },
    };

    return new Promise(function (resolve, reject) {
      request(options, function (error: any, res: any, body: any) {
        if (error) {
          reject(error);
        } else if (![200, 201, 204].includes(res.statusCode)) {
          reject(body);
        }
        resolve(body);
      });
    });
  }

Enrollmentsの削除

import request = require("request");
・・・
  public async deleteEnrollments(token: string, enrollmentId: string): Promise<void> {
    const url = `https://YOUR_DOMAIN/api/v2/guardian/enrollments/${encodeURIComponent(enrollmentId)}`;
    const options = {
      method: "DELETE",
      url: url ,
      headers: {
        authorization: `Bearer ${token}`,
      },
    };

    new Promise(function (resolve, reject) {
      request(options, function (error: any, res: any, body: any) {
        if (error) {
          reject(error);
        } else if (![200, 201, 204].includes(res.statusCode)) {
          reject(body);
        }
        resolve(body);
      });
    });
  }

調べたことは以上になります。もっといい方法や間違っている場合は教えていただけると幸いです。