🌊

Auth0でSingleSignOnに対応してみる

2022/12/06に公開

はじめに

ZENKIGEN(ゼンキゲン)revii(リービー)の開発・運用をしているmikan(みかん)です。
今回はユーザー認証基盤「Auth0」について紹介したいと思います。

ユーザー登録やログインといったユーザー認証機能は toB 向けのサービスには必須の要件ですが、これまではサービスごとに自前で実装することが多かったのではないでしょうか?
しかし、それだと同じような機能なのに使用するプログラミング言語に合わせてゼロから実装する必要があるだけでなく、多要素認証などに対応するのも一苦労です。

そのため、近年ではFirebase AuthenticationAmazon Cognitoなどユーザー認証基盤を切り出した IDaaS(Identity as a Service)が現れてきましたが、その中でも Auth0 はユーザー認証基盤に特化しておりカスタマイズも豊富です。
一方で日本語のドキュメントは用意されておらず、日本語の技術記事もまだまだ少ない中で設計や開発をするのに自分自身も苦労したため、その経験をもとに特に Single Sign On をメインに紹介したいと思います。

Auth0 での Single Sign On について

Single Sign On (SSO)とは

外部の IdP(Identity Provider・認証情報の提供者)を使ってログインを行える機能のことです。
Twitter ログインや Facebook ログインのようなソーシャルログインも SSO の一種です。
例えば Twitter のアカウントでログインできるサービスが世の中にはたくさんあると思いますが、そのイメージをしてもらえると分かりやすいと思います。
toB 向けで言うと、会社用ドメインのメールアドレスとパスワードを Google Workspace や Microsoft Azure AD などの IdP で管理している会社が多いと思いますが、認証部分だけそういった IdP で行わせる機能を Single Sign On と呼びます。
また、ログインに必要なパスワードは IdP 側で管理するため、自社のアプリケーションでパスワードを管理する必要がないのもメリットです。

要件と実装方針

例えば、会社というグループにユーザーが所属するような構成で、ユーザーはメールアドレスとパスワードを設定してサービス専用のアカウントを発行する必要があるとします。
それとは別に、特定の会社に所属するユーザーについては外部の IdP で Single Sign On できるようにする、という要件を想定して考えてみましょう。

Auth0 には Organizations という組織を管理する機能が用意されており、アプリケーション上の会社と Auth0 の Organization を 1 対 1 で紐付けることで、会社ごとにログインフォームや認証方法を切り替えたりすることができます。
また、Enterprise Connections という企業向けに SSO の認証を設定できる機能も用意されています。
よって、会社ごとに Organization を作成し、任意の認証方式の Enterprise Connection を用意した上で、 それぞれ Organization と Enterprise Connection を紐付ければ、今回の要件が達成できると考えました。

Auth0 には様々な用語がありますが、Sansan 様の「B2B マルチテナント SaaS の認証に Auth0 を使うときに知っておきたかったこと」の記事で分かりやすくまとめてくださっていました。

Auth0 と Azure AD それぞれで行う設定

例えば、Azure AD のアカウントで Single Sign On を行う場合の設定方法についてはクラスメソッド様の「Enterprise Connection を設定して Auth0 での認証を Azure Active Directory と統合してみた」の記事に分かりやすくまとめていただいていたので割愛します。

実際には Azure AD 上の設定についてはユーザー側の情報システム部などに行ってもらい、クライアント ID やクライアントシークレットなどを共有していただく必要があります。

ユーザーの目線でログインフローを追ってくと、次のようになります

  1. 自前のサービスの URL にアクセスする
  2. 未ログインの場合、Auth0 にリダイレクトされて の組織名(code)入力フォームが表示される
  3. 会社ごとに設定された組織名(code)を入力する
  4. SSO の設定がされている会社であれば、外部の IdP のログイン画面に遷移する
  5. 外部の IdP のメールアドレス・パスワードを入力してログインを行う
  6. 正常に認証が完了すれば自前のサービスにリダイレクトされる

実装時に考慮が必要だったこと

料金プランによる Organization 数の上限

作成できる Organization の数は料金プランによって上限が設定されていました。
B2B Essentials プランだと 50 件まで、B2B Professional プランだと 100 件まで、B2B Enterprise プランで無制限となっています。

全ての会社が必ず Organization を作成するようにするとすぐに上限に達してしまうため、必要な会社だけ Organization を作成するように設計する必要がありました。

また、Auth0 には Applications という概念があり、クライアント側で使用する認証方法を Application として設定することができますが、1 つの Application で Organization があるログインとないログインを同時に扱うことができなかったため、URL の query parameter で Organization の有無を判断して Application を切り替えるように実装する必要がありました。

Connection(認証方式)の切り替えに伴うアカウントの移行

Auth0 では connections という概念で認証方式を管理しています。
通常のメールアドレス・パスワードの認証は Database Connections、Twitter や Facebook などのソーシャルログインは Social Connections、Google Workspace や Microsoft Azure AD などの企業向け認証は Enterprise Connections と呼ばれています。
今回のように Azure AD の SSO に対応するためには Enterprise Connection を設定して指定の企業の Organization に紐づける必要があります。

ただし、同じメールアドレスで複数の connection に登録されている場合、Auth0 上では別々のユーザーとして作成されてしまいます。
そのため、もともとメールアドレス・パスワードの認証を行っていたユーザーを SSO として登録し直したい場合などでは、古い認証方式のユーザーを削除して新しい認証方式でユーザーを作り直す必要があります。
Auth0 では料金プランによって月間に登録できるユーザー数が制限されているため、このようにユーザーの移行が発生する場合は注意が必要です。

ちなみに、逆に複数の connection に登録されたユーザーを同一アカウントとして扱いたい場合はUser Account Linkingという機能が用意されています。

Enterprise Connections(企業向け認証)のユーザー登録

to B 向けのサービスではユーザー自身がサインアップを行うのではなく、管理者ユーザーがあらかじめ登録したユーザーのみ招待メールが届いてログインできるようにすることが多いと思います。
例えば、管理者ユーザーが招待したいユーザーの登録を行った時、アプリケーション側のデータベースと Auth0 上の両方にユーザーを作成する必要があります。
Auth0 へのユーザー登録はManagement APIを使用して行うことができます。

しかし、Enterprise Connection に対して Management API でユーザー登録を行おうとすると次のようなエラーになってしまいました。

The connection does not support user creation through the API. It must either be a database or sms connection.

ドキュメント上で見つけることはできませんでしたが、Auth0 のコミュニティの「Creating users in Enterprise connection using Management API」の投稿を見ると同様の事象が報告されていました。
Auth0 の Solution Architect の方の回答を見ると、Enterprise Connection については事前に Management API でユーザーを作成することはできず、ユーザーがログインした際に作成されるようにしかできないようです。

そのため、Enterprise Connection を利用してユーザー登録を行う場合、アプリケーション側のデータベースにだけユーザーを作成しておき、初回ログイン時に作成された Auth0 のユーザーとの紐付けを行うように対応する必要がありました。

クライアントシークレットの有効期限

Azure AD の場合、ユーザー側に発行してもらうクライアントシークレットの有効期限は最大 2 年です。
そのため、クライアントシークレットの期限を管理したり、有効期限が切れる前にクライアントシークレットの更新を通知したりするなど、ユーザー側と連携して更新作業を行う必要があります。

おまけ

表題の SSO の件とは関係ないですが、Auth0 に関する実装で自分がつまづいた点についても残しておきます

MFA(多要素認証)の設定で気をつけること

Auth0 は MFA にも対応しおり簡単に有効にすることができます。
詳しくはドキュメントのMulti-Factor Authenticationをご確認ください。

Auth0 Actions を使った MFA の有効・無効の切り替え

SSO の要件と同じく、特定の会社でのみ MFA を有効にしたいという要件があり得ると思います。
その場合、Organizations の metadata にenable_mfaのようなフラグを持たせるようにすれば、Auth0 Actions で次のような Custom Actions を作成して MFA の有効・無効を切り替えることができます。

exports.onExecutePostLogin = async (event, api) => {
  if (event.organization?.metadata?.enable_mfa === "true") {
    api.multifactor.enable("any", { allowRememberBrowser: false });
  }
};

料金プランによる多要素認証の方法の制限

Pro MFA と Enterprise MFA で利用可能な認証要素が分かれています。
Pro MFA では Google Authenticator などでのワンタイムパスワードのみ利用可能となっています。
Enterprise MFA では SMS やメールなど様々な要素での認証が可能ですが、文字通り Enterprise プランのみとなっています。

クライアント側で使用する Auth0 のライブラリの初期化

開発時に MFA 完了後に認証にログインページに戻されてしまう現象が発生し、原因がなかなか掴めずにいました。
最終的にはクライアント側で使用している Auth0 のライブラリの初期化のオプションにcacheLocation: 'localstorage'useRefreshTokens: true を指定する必要があることが分かりました。
フロントエンドは React アプリケーションのため auth0-react を利用していましたが、このオプションの指定については auth0-spa-js の FAQ にしか記載されていなかったため気づきににくかったです。
https://github.com/auth0/auth0-spa-js/blob/master/FAQ.md

If silent auth is being used and Auth0 needs interaction from the user to complete the MFA step, then authentication will fail with an mfa_required error and the user must log in interactively.

To resolve this:

Auth0 単体ではできなかったこと

組織ごとに IP アドレスによるアクセス制限を設定する

Auth0 Actions を使って Post Login のトリガーで IP アドレスを取得してチェックすることはできますが、組織ごとに IP アドレスのホワイトリストやブラックリストを管理するところまでは対応していませんでした
Organization に metadata を持たせられるのでそこに保存することも検討しましたが、上限があるため数多くの IP アドレスを設定する可能性を考慮して断念しました

パスワードの有効期限を設定して期限切れの場合はパスワードを変更させる

Auth0 Actions を使って Post Login のトリガーで最後にパスワードが更新された日時を取得してチェックすることはできますが、組織ごとにパスワードの有効期限を設定したり、期限切れの場合にパスワードを変更するページに遷移させるところまでは対応していませんでした
拒否するだけで良ければ Organization の metadata に有効期限を保存して参照す流ようにすれば良いと思いますが、それだと UX が悪いため自前で実装することにしました

初回ログイン日時を取得する

ユーザーごとに最新ログイン日時やログイン回数を取得することはできますが、初回ログイン日時はありませんでした

おわりに

Auth0 はニーズに合わせせてカスタマイズできることが多い反面、いろんな機能を組み合わせて混乱したり、簡単にできると思っていたことがことが実は難しそうということもありました。
事前にしっかり調査した上で設計を行い、さらにプロトタイプの実装なども含めて慎重に進めていくことをおすすめします。
何か困った際にはコミュニティ以外にも公式に質問できるサポートフォームもあるので、そちらに問い合わせてみるのも良いと思います。
また、今回の紹介した対応がベストプラクティスだとは限らないため、よりよい方法があれば是非教えてください!

お知らせ

少しでも弊社や harutaka 、revii に興味を持っていただいたという方は、お気軽にご連絡頂けると幸いです!カジュアルにお話という形でも、副業したいという形でも大歓迎です。

https://recruit.zenkigen.co.jp/career

Discussion