フューチャー技術ブログ

Auth0で認証成功後に任意のWebページを表示させたい

TIG DX-Unit の先山です。

Auth0 を使ってアプリを構築しています。とある案件で、ユーザがログインした後に任意のページを表示させたいニーズがありました。こういった時はアプリ側でやってあげれなくもないのですが、複数のアプリが Auth0 で認証している場合には、アプリの数だけ改修が発生してしまいます。今回は context.redirect という Auth0 のユニークな機能を使って、改修を最小限に抑える方法を紹介します。

Auth0とは?

Auth0導入編をぜひ参照ください。他にもAuth0関連の記事があります。

context.redirect 機能紹介

概要

Auth0 はユーザが認証成功時に任意の JavaScript スクリプトを実行する Rules を提供しています。引数の context に redirect パラメータをセットすることで、任意のWebページに画面遷移することが可能です。本来であれば redirect_uri にリダイレクトしてアプリケーションへ戻るのですが、その前に1つユーザアクションを介入させることができます。

Auth0公式ドキュメント - Redirect Users From Within Rules

全然関係ない URL にリダイレクトさせちゃうと、認証できなくなっちゃうんじゃないの? と思われるかもしれませんが。リダイレクト先にはクエリパラメータで state={ハッシュ値} が渡されます。この値を Auth0 側で管理しています。認証を再開するには Auth0テナントのドメインに /continue?state={ハッシュ値} という形式で画面遷移します。

実装時の注意点

注意するべき点を紹介します。

  • 全ての Rules は2回実行されます。「context.redirect によってリダイレクトされる前」と「リダイレクトから戻ってきた後」の合計2回です。各 Rules が何度起動しても問題ないような実装をしましょう。
  • ある Rules で context.redirect にパラメータをセットしても、その Rules 終了後にリダイレクトが発生するわけではありません。全ての Rules の実行が終わってから context の中身が評価されリダイレクトが発生します。
  • context.redirect が有効なのは認証1回につき1度のみです。リダイレクトから戻ってきた後は、認証成功とともに callback_uri でアプリケーションへ戻ります。

ユースケース

Auth0 のドキュメントではユーザにパスワード更新を促す例が紹介されています。有効期限が短いトークンをクエリパラメータ経由で渡していますね。このやり方であればトークンが必要なAPIサーバとの通信もできますね。勉強になりました。これ以外にも、例えば利用規約の更新をユーザに表示させたい場合などにも使えそうです。

https://auth0.com/docs/rules/redirect-users#force-password-change-example

サンプル実装

ログイン画面で ID/Pass を入力成功した後、ある画面へ遷移させてユーザアクションを求め、ユーザがその画面で承諾しないと認証成功しないといったサンプルを実装します。Auth0 が提供する Vueのサンプル実装を改造します。

処理のフロー

  • アプリがログイン画面を開くいてログインを試行する
  • ログインが成功したら指定した localhost:3000/consent(以降、確認画面と呼びます)へリダイレクトして表示させる
  • 確認画面でユーザが “Yes” をクリックしたら再び Auth0 の認証処理を再び実行させ、認証成功してアプリへ戻る
  • “Yes” と回答したユーザは、次回以降のログインではその確認はせず、認証を成功させる
  • (もし確認画面で “No” をクリックした場合は認証エラーにする)

Rules 実装

まず1つ目の Rules です。ユーザメタデータに { agreed: "yes" } が含まれているか否かでステータス管理を行います。もし agreed が定義されてなかったり違う値だったりした場合は、context.redirect に遷移先のURLを代入して確認画面へリダイレクトさせます。

function (user, context, callback) {
const metadata = user.app_metadata || {};

// すでに規約同意している場合は何もせず終了
if (metadata.agreed) {
return callback(null, user, context);
}

context.redirect = {
url: "http://localhost:3000/consent"
};
return callback(null, user, context);
}

2つ目の Rules です。ここには確認画面から戻ってきた時の処理を書いています。確認画面から戻ってきた時のみ起動して欲しいので、 context.protocol の中身をチェックしてから実行するかの判定をしています。クエリパラメータからユーザの回答を受け取り、メタデータ更新を行ってから認証成功させてます。

function (user, context, callback) {
// context.redirect からの再開でない場合は本処理を終了する
if (context.protocol && context.protocol !== "redirect-callback") {
return callback(null, user, context);
}

// クエリパラメータを取得
const request = context.request || {};
const query = request.query || {};

// ユーザが同意すれば認証成功
if (query.answer && query.answer === "yes") {
user.app_metadata = user.app_metadata || {};
user.app_metadata.agreed = "yes";
auth0.users.updateAppMetadata(user.user_id, user.app_metadata)
.then(function(){
callback(null, user, context);
})
.catch(function(err){
callback(err);
});

return;
}

// ユーザが拒否すれば認証失敗
return callback(new UnauthorizedError("同意しないと使えません"), user, context);
}

Vue 実装

確認画面の実装はこんな感じです。Rules からこの画面にリダイレクトされた場合はクエリパラメータに state=ハッシュ値 が付いています。なので state がない場合にはエラー画面へ遷移させちゃってます。ボタンをクリックしたら再び Auth0 へ遷移するように、https://{Auth0ドメイン}/continue?state={ハッシュ値} という URL を作成しています。

vue
<template>
<div>
<h3>アプリケーションの利用を継続しますか?</h3>
<a @click.prevent="clickYes" class="button-a">Yes</a>
<a @click.prevent="clickNo" class="button-a">No</a>
</div>
</template>

<script>
import { domain } from "../../auth_config.json";

export default {
name: "consent",
computed: {
query: function() {
return this.$route.query;
},
state: function() {
if (this.query && this.query.state) {
return this.query.state
}
return null;
}
},
watch: {
state: {
immediate: true,
handler: function() {
if (!this.state) {
this.$router.replace("/error");
}
}
}
},
methods: {
clickYes: function() {
location.href = `https://${domain}/continue?state=${this.state}&answer=yes`;
},
clickNo: function() {
location.href = `https://${domain}/continue?state=${this.state}&answer=no`;
}
}
};
</script>

動かしてみる

アプリケーションを起動して Login をクリックします。Google アカウントを使ってログインする画面が表示されたので、ログインを行います。

Googleで続ける をクリックしたら想定通り localhost:3000/consent?status=... にリダイレクトされ確認画面が表示しました。Yes をクリックして再び Auth0 での認証を継続します。

同意ダイアログが表示しました。許可を押すとログインが完了します(localhost で起動したアプリケーションは初回ログインで表示される仕様です。実際の Auth0 設定では audience をちゃんと指定して、Consent Skip を有効にしておけばこの画面はスキップされます)

ログインが完了しました! プロフィール画面から IDトークンの中身を見ることができるので、無事に成功している様子です。

ちなみに確認画面で No をクリックした場合は Rules で認証エラーにしてます。クエリパラメータに error=unauthorized error_description=同意しないと使えません でエラー内容を通知してます。

state の有効期限

これはわかりませんでした。試しに 30 分待ってみたのですが、有効期限切れなどなく認証が継続できました。

実際にあったトラブル

すでに稼働しているアプリケーションに context.redirect を適用する場合は、ちょっと注意が必要です。

これは私が実際にテストで検知したものです。現在、私がメンテしているアプリケーションは Vue.js と auth0-spa-js で構築したものなのですが、context.redirect で画面遷移を追加したことにより、Auth0 の Silent Authentication でエラーが発生してしまいました(しっかりテストを行って正解でした!)

Silent Authentication の詳細な説明は割愛しますが、「Auth0で設定した認証の有効期限内であれば、ログイン画面をスキップしてユーザを自動ログインさせる機能」と思ってください。

導入前のアプリの振る舞いは、ユーザが認証有効期限内であれば自動ログインを行うものでした。この改修によって、リダイレクト先画面でユーザアクションが必要になってしまったため、Silent Authentication は interaction_required を出力しました。原因は auth0-spa-js のバージョンが 1.8.0 と前バージョンのものだったため、このエラーをキャッチする処理が実装されていませんでした。最新版はエラーを網羅的にハンドリングしている習性が入っていたので、ライブラリをバージョンアップすることで解決しました(ご参考までに、修正対応がされていたコードはこちらです)

これ以外の実現方法の紹介

私が思いつく限りをあげてみます。

Universal Login をカスタマイズする

独自のhtmlを実装して、ログイン画面に利用規約を一緒に表示させる方法です。
ただし、この html ソースの管理やメンテナンスが発生するので、おすすめ度は中くらいです。

アプリ側で制御する

IDトークンに規約同意済フラグのようなパラメータを設けて、アプリ側でそのパラメータを見て利用規約を表示する・しないを制御する方法です。この方法でも実現は可能ですが、アプリごとに対応が必要であるため、メンテナンスが大変になります。運用するアプリが1つなど少ない場合には、context.redirect でコントロールするよりシンプルかもしれません。

同意ダイアログに表示する

3rdパーティアプリでログインさせるときに同意ダイアログを表示させることが可能です。ちょっと無理矢理ですが、この画面に任意の文字を表示することは可能であるため、それを利用します。しかし、3rdパーティのみに限定されてしまう点と、ダイアログという小さいエリアに向いていない使い方のため、おすすめ度は低いです。