バイセル Tech Blog

バイセル Tech Blogは株式会社BuySell Technologiesのエンジニア達が知見・発見を共有する技術ブログです。

バイセル Tech Blog

経験から学ぶAuth0のSAML連携

はじめに

テクノロジー戦略本部開発2部の山口です。

バイセルでは現在、新しい基幹システムの開発をマイクロサービスで進めています。 その基幹システムの認証・認可にはAuth0を採用しているのですが、 既に稼働している基幹システムの認証・認可には、Active Directory(以下、AD)を使ったユーザー管理とOneLoginによるSingle Sign On(以下、SSO)での統合アクセス管理を行っています。

その結果、Auth0とOneLoginのSAML連携が必要となり、認可を行えるようになるまでに様々な工夫が必要となりました。

この記事では、自分と同様にAuth0のSAML連携を実装している人や、Auth0のログイン時にちょっとした処理を実行できるAuth0 ActionsのPost Loginと呼ばれる機能について知りたい人の助けとなる情報を提供します。

バイセルでSAML連携が必要になった背景

冒頭でも軽く触れたのですが、バイセルでは開発中のシステムにAuth0を採用しています。 しかし、既存の基幹システムではADを使ったユーザー管理をしているため、 バイセルとしてはADとAuth0によるユーザーの二重管理が発生していることとなります。 これは、ユーザーを管理する側にとって負担となってしまうため、 SAMLを使ったADとAuth0間でのユーザー連携が必要だという判断になりました。

Post Loginのフロー

Auth0には、Auth0 Actionsと呼ばれる、ログイン時、パスワード変更時、Machine to Machine(M2M)などの各アクションのタイミングで、ちょっとした処理を実行できる仕組みが設けられています。 今回はその中から、実装するのに用いた「ログイン時に実行される」Post Loginについて触れていきます。

Post Loginはユーザーがログイン認証を完了した後から、認可に必要となるAccess Tokenを発行するまでの間に処理を挟むことができ、アプリケーション側で必要とするAuth0の持つユーザー情報などを追加でAccess Tokenに含めJWTとして引き渡すことができるようになります。

Auth0のログインフロー

このPost Loginは大きく2つの関数に分かれています。

  • onExecutePostLogin関数

これはPost Loginで最初に実行される関数であり、エントリポイントとも言える関数になります。 この関数の中でapi.access.denyなどのAuth0が引数で提供しているメソッドを使うことで、ユーザーのアクセスを拒否したり、Access Tokenに含まれる内容をカスタマイズできます。

  • onContinuePostLogin関数

Post Login実行中、一時的に処理をアプリケーション側へ返したい時、アプリケーション側からAuth0へ戻ってくる場所がこの関数となります。

onExecutePostLogin関数からアプリケーション側に返す時はapi.redirect.sendUserToメソッドを用います。

exports.onExecutePostLogin = async (event, api) => {
  api.redirect.sendUserTo("https://my-app.example.com");
};

exports.onContinuePostLogin = async (event, api) => {
}

反対に、アプリケーション側からPost Loginへ処理を戻す際はwindow.location.assign("https://my.auth0.com/continue?state=abc123")のように行います。 このとき、stateというクエリパラメータがありますが、これはapi.redirect.sendUserToメソッドでアプリケーションへリダイレクトした時に付与されているものであり、認証トランザクションを管理しているものになります。 Auth0側へ返す必要があることに注意してください。

const state = route.query.state
window.location.assign(`https://my.auth0.com/continue?state=${state}`)

以上がPost Loginのフローとなります。

バイセルでは、ここにSAML連携が加わることとなります。 SAML連携では、Post Login時にJust In Time Provisioningという仕組みを用いて、ADのユーザー情報をOneLogin経由でAuth0に提供します。 そのADのユーザー情報を元に、Auth0上に新しいユーザーが作成されることとなります。

そのため、全体としてのフローは以下のようになります。

SAML連携を含んだ全体の流れ

Post Loginの特徴

上記で、Post Loginについては漠然とどういったものなのか分かっていただけましたでしょうか。 ここでは、自分がPost Loginを実装する中で躓いたPost Loginの特徴とも言える部分について話します。

リダイレクト時にID Token、Access Tokenは発行されない

リダイレクト先のアプリケーション側でAPIを実行したい。でも、JWTによる認可制御が入っているからAccess Tokenがほしい

という場面がありました。

当初は、リダイレクトを行った時にPost Loginの処理から離れるので、ID TokenやAccess Tokenが発行されると考えていました。 しかし、実際に確認した結果、発行されることはありませんでした。

Auth0のマネジメントコンソール上にも、Auth0 Actionsの後にToken Issuedという文字が来ていることからも、ID TokenやAccess Tokenが発行されるのはPost Loginの完了した後だと分かります。

ドキュメントにも、リダイレクトはアクションパイプラインをサスペンドしていると書かれているため、Post Loginが完了していないことが分かります。

Auth0 docs - Start a redirect

最終的にリダイレクト時には、Auth0 ActionsからSessionToken(アプリケーション側に必要なペイロード)をJWT化して引き渡すことになるので、SessionTokenのJWT検証・署名をもって認可することとしました。

ID TokenやAccess Tokenに含まれるユーザー情報はPost Login開始時の情報となる

Post Login中にユーザー情報を更新したい

Post Login中に最新のユーザー情報がほしい

という場面がありました。

Auth0が公開しているnode-auth0をAuth0 Actionsの中で使うことができます。 これにより、Auth0 ActionsのランタイムでAuth0 Management APIを使ったユーザー情報の参照や更新ができます。

const auth0 = require('auth0')

exports.onExecutePostLogin = async (event, api) => {
  const ManagementClient = auth0.ManagementClient

  const management = new ManagementClient({
    domain: event.secrets.AUTH0_API_DOMAIN,
    clientId: event.secrets.AUTH0_API_CLIENT_ID,
    clientSecret: event.secrets.AUTH0_API_CLIENT_SECRET,
  })
}

しかしながら、この方法であってもID TokenやAccess Tokenにデフォルトで含まれるユーザー情報はPost Login開始時の引数eventを参照しているため、すぐに反映出来ませんでした。

また、ID TokenやAccess Tokenに新しい値をセットする方法がないため、 ユーザー情報を更新したとしても2回目以降のログイン時のID TokenやAccess Tokenに反映されることとなります。

SAML連携でAuth0 Management APIを使用する際の注意点

ここでは、先程のAuth0 Management APIをSAML連携で使うときの注意点について説明します。

SAML Enterpriseコネクションでは、Advanced設定「Sync user profile attributes at each login」を有効にしているとuser.name, user.family_name, user.given_nameが変更できない

リダイレクト先で入力した情報を元にAuth0のfamily_name、given_name、nameを更新したい

という場面がありました。

Auth0のfamily_nameなどをAuth0 Management APIを使って更新しようとするのですが、以下のエラーが出て更新できないという事態にも遭遇しました。

The following user attributes cannot be updated: family_name, given_name, name. The connection (onelogin) must either be a database connection (using the Auth0 store), a passwordless connection (email or sms) or has disabled

これは、EnterpriseコネクションのAdvanced設定「Sync user profile attributes at each login」が有効になっているため、ログインの度にADからAuth0へユーザー情報が共有されるようになっていました。 そのため、Auth0 Management API側からの操作を受け付けないようになっていました。

デフォルトでは、OneLoginなどのIdPのユーザー情報との同期をとるため設定がオンになっていますが、今回はあえてこの設定をオフにすることでリダイレクト先アプリケーションの情報でAuth0のユーザー情報を更新できるようにしました。

オフにしたことでIdPとのユーザー情報のログイン時の同期がとれなくなりましたが、バイセルではIdPにユーザー情報を依存したくない意図があったので許容できました。

パスワードログインとSAML連携を両立する

フォームデザインの変更

ここまでAuth0とOneLoginのSAML連携について話してきましたが、このシステムの一部はAuth0の認証にパスワードログインを行う形で先行利用されていました。 そのため、SAML連携の導入にあたって認証をSAML認証のみに揃えるかパスワードログインを併用するかを検討する必要がありました。

ところで実はこのシステム、将来的に外販する構想があります。 社内事情のみを勘案すればSAML連携に限定しても良かったのですが、外販時のことも考慮してパスワードログインを残すこととしました。

パスワードログインとSAML連携を両立するために、Auth0のログインフォームの設定を見直す必要もありました。 現在、Auth0ではログインフォームのデザインとして旧デザインの「Classic」と新デザインの「New」が存在します。

ClassicデザインとNewデザインの違いはauth0 docs - New Universal Login vs. Classic Universal Loginを確認してください。

Classicデザインでは、lockというライブラリを使ってフォームを実装できます。しかし、このlockには認証先のコネクションにデフォルトのパスワードログインとSAMLなどのEnterpriseコネクションを同時に扱うことができないという仕様があるため、ClassicではパスワードログインとSAMLの両立ができませんでした。

反対にNewデザインであれば、Enterpriseコネクションの設定画面から簡単にコネクションのリンクボタンを追加できたので、今回はNewデザインを使うようにしました。

上図がEnterpriseコネクションのログインフォームの設定になるのですが、 この時、設定画面のIdentity Provider domainsを設定してしまうと、パスワードログイン時のメールアドレスのドメイン部(user@example.comのexample.comの部分)からコネクションが判断されてしまいます。 そのため、パスワードログインをしたいつもりが、逆にSAML認証が必要だと判断する挙動をとるので意図的に空にする必要がありました。

ログイン経路による処理の分岐

バイセルでは、SAMLで初めてログインしたユーザーの情報を書き換えたかったので、パスワードログインとSAML連携でのログインをPost Loginの中で判定する必要がありました。 その判定には、event.user.identitiesプロパティを使用しました。 このプロパティは、パスワードログインであればoidc-basic-profile、SAMLログインであればsamlpという値が入ります。

SAMLによるSSOには、2種類あります。

  • IdP(OneLogin)へアクセスしてSAMLの処理を行うIdP-initiated Login
  • SP(Auth0)へアクセスしてSAMLの処理を行うSP-initiated Login
IdP-initiated Login SP-initiated Login

はじめは、一括にSAMLと考えていたためどちらも同じ値が取れるものとevent.transactionプロパティを使っていました。 このプロパティも上記と同じ様な値が入るのですが、IdP-initiated LoginとSP-initiated Loginで値が違うという問題があったため、event.user.identitiesプロパティを使用することにしました。

IdP-initiated Login SP-initiated Login

1つ目の値がevent.user.identitiesプロパティ、2つ目の値がevent.transactionプロパティを出力したものとなります。 上図のようにevent.transactionプロパティでは、SP-initiated Loginがパスワードログインと同じoidc-basic-profileとして扱われるため、判断するのに使うことができませんでした。

まとめ

今回、自分がAuth0とOneLoginとのSAML連携を実現するにあたり、躓いた点やドキュメントからは読み解くことが難しい部分について少しばかりではありますが紹介しました。SAML連携を実現するためにかなりの調査時間も要しましたので、この内容が少しでも同様のことを実現しようとしている人の技術的、時間的な助けになれば幸いです。

最後にBuySell Technologiesではエンジニアを募集しています。興味がある方はぜひご応募ください!

herp.careers