Auth0のHooksで検知できるイベントに「Post Change Password」が追加されたので、前々から実現したいと思っていたパスワードリセット時のリフレッシュトークンの無効化をやってみました。
設定手順
Applicationの追加
今回設定するHookはManagement APIを使うので、Hook用にManagement APIを叩くための権限を持たせたMachine to MachineのApplicationを用意します。

APIはAuth0 Management APIを選択し、scopeはread:device_credentials
とdelete:device_credentials
を設定します。

新しいHookを追加
Auth0のDashboardのHooksメニューから「Post Change Password」のHookを追加します。

ここから先は追加したHookに対して設定を行なっていきます。
Secretの追加
事前に登録しておいたHook用のApplicationのclient_id
とclient_secret
をsecretに登録します。(あとついでにAuth0テナントのドメインも)

このsecretに登録した値はHook内でcontext.webtask.secrets.{key}
の形式で参照することができます。こうすれば機密情報や環境によって変わる情報をHookにハードコーディングしなくて済みます。
こうしておいたほうがAuth0の設定をコマンドラインツールでexport/importするときも管理が楽だと思います。
NPMモジュールの追加
Hookで利用するNPMモジュールを追加します。ruleと違い、Hookは任意のNPMモジュールが使えるので便利です。
今回はManagement APIにhttpリクエストをするためのaxios
と、Management APIを叩くためのアクセストークンを取得するためのauth0
の2つのモジュールを追加します。

Hookの実装
とりあえず動けばいいやぐらいの気持ちで書いたコードなので、エラーハンドリングとかもしてない雑な実装ですが…。
module.exports = async function revokeRefreshTokens(user, context, cb) {
const { ManagementClient } = require('auth0');
const axios = require('axios');
const domain = context.webtask.secrets.domain;
const auth0 = new ManagementClient({
domain,
clientId: context.webtask.secrets.client_id,
clientSecret: context.webtask.secrets.client_secret,
});
async function getDeviceCredentials(accessToken) {
const response = await axios({
method: 'GET',
url: `https://${domain}/api/v2/device-credentials`,
data: {
fields: 'id',
include_fields: true,
user_id: user.id,
type: 'refresh_token',
},
headers: {
Authorization: `Bearer ${accessToken}`,
},
});
return response.data;
}
function deleteDeviceCredential(accessToken, id) {
axios({
method: 'DELETE',
url: `https://${domain}/api/v2/device-credentials/${id}`,
headers: {
Authorization: `Bearer ${accessToken}`,
},
});
}
const accessToken = await auth0.getAccessToken();
const deviceCredentials = await getDeviceCredentials(accessToken);
await Promise.all(deviceCredentials.map((device) => deleteDeviceCredential(accessToken, device.id)));
cb();
}
やっていることは単純で
- Management APIを叩くためのアクセストークンを取得
-
GET /api/v2/device-credentials
でリフレッシュトークンが発行されているデバイスの一覧を取得 -
DELETE /api/v2/device-credentials
でデバイスを一個ずつ削除
この3つだけです。最後の3でデバイスを削除することでリフレッシュトークンが無効化されます。
まとめ
Auth0のリフレッシュトークンは有効期限がないので一度発行されたらものをずっと利用可能ですが、このHookを実装することでパスワード変更時に発行済みの全てのリフレッシュトークンを無効化することができます。
また、このHookはUniversal Login画面の「パスワードをお忘れですか?」リンクからパスワードを変更した場合でも、Authentication APIのPOST /dbconnections/change_password
を使ってパスワードを変更した場合でも動作します。
このHookを利用すればパスワード変更時にユーザをAuth0の同一テナント上の全てのアプリから強制ログアウトさせる。みたいなアプリ実装も可能になるのではないでしょうか。
〜2020/5/9 追記〜
Auth0のリフレッシュトークンは有効期限がないので一度発行されたらものをずっと利用可能
現在はリフレッシュトークンのローテーション機能を有効にすることで、有効期限を設定することができるようになっています。