フューチャー技術ブログ

Auth0 Rulesのユニットテストを書きたい

TIG DXユニット 1 アルバイトの小林です。

案件で認証プラットフォームであるAuth0を利用していますが、Auth0の機能の中でもRulesと呼ばれるユーザ認証時にJavaScriptの関数を走らせる事が出来る機能は非常に強力で様々なニーズに対応することが可能になります。

その中でJavaScriptの関数で書けるRulesに対して、ユニットテストを書く事が出来れば、Ruleの質も担保出来ます。

Auth0テナントへのRulesのexport、importにはauth0-deploy-cliを利用出来ますが、Ruleの記述方法が

function anyRule(user, context, callback) {
callback(null,user,context)
}

の様な名前付き関数の形式なのでユニットテストの実現には工夫が必要となります。

今回はその工夫の部分について書いていきたいと思います。

Auth0について

Auth0の概要についてはAuth0 導入編を、
Auth0 RulesについてはAuth0のRulesを使って認証認可を自在にカスタマイズするをそれぞれご参照ください。

前提

検証に利用したマシンのNode.jsのバージョンはv12.15.0です。

検証に使うRule

以下の2つのRuleをテストしたいことにします。

  • IDトークンのクレームに{"https://example.com/color": "blue"}を追加する、add-claims.js
add-claims.js
function addClaim(user, context, callback) {
const idTokenClaims = context.idToken || {};
idTokenClaims["https://example.com/color"] = "blue";
context.idToken = idTokenClaims;

callback(null, user, context);
}
  • http://example.com/some/apiにGETリクエストを送信して上手く行った場合はレスポンスをIDトークンの
    http://example.com/dataキーに入れる、request-example.js
  • このRuleはリクエストのレスポンスが200以外の場合にログインエラーとする。
request-example.js
async function requestExample(user, context, callback) {
const axios = require('axios@0.19.2');
const response = await axios({
url: `http://example.com/some/api`,
method: 'GET',
});

// リクエストのレスポンスが200以外の場合にログインエラーとする。
if (response instanceof Error) {
const e = 'Failed to fetch data';
return callback(new UnauthorizedError(`${e}: ${response.data}`));
}

const idTokenClaims = context.idToken || {};
idTokenClaims["http://example.com/data"] = response.data;

context.idToken = idTokenClaims;

return callback(null, user, context);
}

コードについての補足
async function requestExample(user, context, callback) {

Auth0 Rulesではトップレベルのasyncは許可されています。

const axios = require('axios@0.19.2');

Auth0 Rulesではいくつかのライブラリが利用可能です。

利用可能なライブラリのリストはこちらに記載されています。

テスト環境

以下の様なNode.jsを用いたテスト環境を前提とします。

C:.
│ add-claim.js
│ package.json
│ request-example.js

└─tests
add-claim.test.js
request-example.test.js

それではこれらの前提を元に様々な手段について書いていきます。

記事中では引数のcallbackやスタブ化したrequireに適当なコードを書いていますが、実際にテストコードを書く際は引数のcallbackやスタブ化したaxiosをモック関数(jestでのjest.fn())にすることで動作の保証範囲をより広くすることが出来ます。

fsでスクリプトを文字列として取ってvmを利用して取り出す。

Auth0 docsに書かれている 2方法です。

add-claim.jsのRuleを呼び出すコードを書くとこのようになります。

add-claim.test.js
const vm = require('vm');
const fs = require('fs');

const user = {};
const context = {};

const script = fs.readFileSync('./add-claim.js') // => func addClaim...

const runCode = `
f = () => {
return ${script};
}
f() // => addClaimが関数として得られる。
`;

const rule = vm.runInThisContext(
runCode, {
// filename for stack traces
filename: 'add-claim.js',
displayErrors: true
}
)

rule(
user,
context,
function callback() {
console.log("Complete");
}
);

console.log(context); // => { idToken: { 'https://example.com/color': 'blue' } }

テストしたいRuleのJavaScriptのソースコードを文字列として取得して、vm.runInThisContextを使ってサンドボックス上にテスト対象のメソッドが返るコードを実行します。先述の通りvm.runInThisContextがテスト対象であるaddClaimメソッドを返し、そのメソッドに(user,context,callback)を入れて呼び出しをしている形となります。

今回はコードを少しでも読みやすくするため、少し冗長な書き方をしていますが、コードが少し分かりにくいことを除けば比較的短いコードで記述することが可能になります。

requireモックについて

axiosモジュールを利用しているrequest-example.jsテストについて考えます。

HTTPリクエストが必要なユニットテストではクライアントライブラリをスタブにして様々なレスポンスが来た場合についてテストすることが品質向上に対して有効です。

そこで、requireモジュールをスタブにすることでaxiosでは無く、独自のメソッドを利用出来る様にします。

嬉しいことに、vm モジュールの仕様は外部のライブラリを利用するメソッドにおいても都合が良く、contextは同一であってもscopeは同一で無い仕様があります。これにより、runInThisContext内ではrequireは未定義になります。

requireに何か代入してaxiosモジュールのモック化を試みましょう。

・contextを新たに作成する。

runInThisContextで目的のメソッドを取り出していましたが、requireが別の機能を果たす様なcontextを別途作成して、それを利用します。

まとめると以下のコードで実現可能になります。

request-example.js
const vm = require('vm');
const fs = require('fs');

const user = {};
const context = {};

const script = fs.readFileSync('./request-example.js')

const runCode = `
f = () => {
return ${script};
}
f()
`;

const runContext = vm.createContext(
{
require: () => {
// require()が返すメソッド
return (ctx) => {
// axios()が返すレスポンス
console.log("axios called:", ctx);
return {
data: "mock response"
}
}
}
}
)

const rule = vm.runInContext(
runCode, runContext,{
// filename for stack traces
filename: './request-example.js',
displayErrors: true
}
)

console.log(rule);

rule(
user,
context,
function callback() {
console.log("Complete");
}
).then(() => {
console.log(context); // => { idToken: { 'http://example.com/data': 'mock response' } }
})

ここまでがAuth0 Docsに記載されているユニットテストの実現方法です。

rewire を利用する方法

他にはrewireを利用すると、vm+fsよりは裏ワザ感少な目で
テストコードを実行出来ます。

それでは早速add-claim.jsのメソッドを呼び出すコードを書いていきます。

add-claim.test.js
const rewire = require('rewire');

// テスト対象のスクリプトを取得
const script = rewire('./../add-claim.js');

// テスト対象のメソッド名を指定してメソッドを取得
const rule = script.__get__('addClaim');

const user = {};
const context = {};
const callback = () => {
console.log("Complete");
};

rule(user,context,callback);

console.log(context); // => { idToken: { 'https://example.com/color': 'blue' } }

何をやっているかは圧倒的に分かりやすいと思います。

続いてrequest-example.test.jsの呼び出しを進めていくのですが、rewireはモック機能を持つので、
先程のvm+fsの組み合わせの時と同様にrequireをモックすることで、axiosをモックします。

request-example.test.js
const rewire = require('rewire');

// テスト対象のスクリプトを取得
const script = rewire('./../request-example.js');

script.__set__('require', () => {
return (ctx) => {
console.log('axios called',ctx);
return {
data: "mock response"
}
}
});


// テスト対象のメソッド名を指定してメソッドを取得
const rule = script.__get__('requestExample');

const user = {};
const context = {};
const callback = () => {
console.log("Complete");
};

await rule(user,context,callback).then(
() => {
console.log(context); // => { idToken: { 'http://example.com/data': 'mock response' } }
}
)

相変わらずrequireのモックは泥臭いですが、ある程度は見やすくなったかと思います。

その他調査したもの

auth0-test-harness

auth0-rules-testharnessを用いてwebtask上でRuleを実行させることが出来てた様です。

webtaskとは
Auth0 Inc.が持つ`Node.jsをweb上で実行できるサービス`です。HTTPエンドポイントが作成されるため、コードを書くだけでサーバを動かす事が可能でした。

webtaskはAuth0 Rulesの実行環境としても使われており、webtaskの作成するサンドボックス上でRuleが実行されます。

私も早速試してみようと思ったのですが、publicなwebtaskのサービスが終了している様子 3 4のため、検証を断念したいと思います。

auth0-local-test-harness

auth0-rules-local-testharnessauth0-rules-testharnessのwebtaskを使う所をlocalにした物です。

コードをよく見ると、fs+vmでサンドボックス上で実行している物にauth0-authz-rules-apiが定義しているcontextを流し込んでいる様に見えます。

手元のマシンがNode.js v12.15で、Auth0 Rulesで使われるNode.jsのバージョンも執筆当時12.20.1 5 6ですが、手元でのnpm installが失敗するのと、npmパッケージが2年前から更新されていないことを考慮して断念します。

まとめ

今回は以下の4つの方法についてテスト方法の調査を行いました。

  • Auth0 Docsに載っている方法
  • rewireを利用した方法
  • auth0-test-harnessを利用した方法
  • auth0-local-test-harnessを利用

上2つが現実的な実装案になると考えていますが、
Auth0 Docsに載っている方法はfsvmのシンプルな構成で利用可能な代わりに、コードが少し類雑、
rewireを利用した方法はシンプルに書けるが別途パッケージのインストールが必要と一長一短の様に見えます。

また、今回の調査においてはrequireのスタブ化が出来ても、ほぼ無理やり感は否めません。

1つのRuleが利用するモジュールが2つ以上の場合に置いて、与えられた引数から何を返すか場合分けで記述する必要があり、少し複雑です。

この辺りは今後の課題として、引き続き調査出来ればと思います。

Auth0の新機能 Actionsについて

執筆当時(2021/03/03)はBETA版機能ですが、認証認可を自由自在にカスタマイズする手段として、Rules,Hook に加えてActionsが存在します。

ActionsはRulesと同様ログイン時に何らかのスクリプトを走らせることが出来る機能です。

沢山の追加機能があるのですが、一部抜粋すると、

  • バージョン管理の実装
  • コードエディタの進化(コード補完、クイックヒント機能の搭載)
  • 任意のnpmパッケージが利用可能
  • スクリプトの記述方法がRulesと異なる

です。

詳しくはAuth0の公式ブログ: Introducing Auth0 Actions をご参照ください。

この記事で特筆すべき点はスクリプトの記述方法がRulesと異なる点です。

Actionは以下の形式で表記されています。

/** @type {PostLoginAction} */
module.exports = async (event, context) => {
return {};
};

Rulesとは異なり、Actionはmodule.exportsが記載されています

つまり、rewireやfsを使わずともテスト対象のメソッドのインポートが出来ます

インポート先でのrequireは、proxyquireなどを利用することでスタブ化が出来るため、これらを利用することでActionsの単体テストが実装可能になると考えられます。

まだBeta版であり、Auth0 Deploy CLIのSupported Featuresには記載されていませんが、Rulesよりも幅広い機能を持ち、改善されている点も多々あるため、今後はRulesの代わりにActionsの利用を視野に入れると良いと思います。


  1. 1.TIG: Technology Innovation Groupの略で、フューチャーの中でも特にIT技術に特化した部隊です。DXユニット: TIGの中にありデジタルトランスフォーメーションに関わる仕事を推進していくチームです。
  2. 2.Rules Testing Best Practices
  3. 3.Future of rules without webtask.io - Auth0 Community
  4. 4.Webtask
  5. 5.Migrate to Node.js 12
  6. 6.Can I require? - Search which node modules you can use in webtask.ioに記載