Autifyで2FAが絡むテストの自動化を実現させた話

この記事は [Autify アドベントカレンダー2021](https://qiita.com/advent-calendar/2021/autify) の13日目のエントリです。

こんにちは、Autifyの近澤です。最近はCEO業が忙しくコードが全く書けておらず、Ruby製のKPI集計スクリプトのメンテが、僕のエンジニアリングの最後の砦になっています。（おそらく来年の頭には僕の手から離れるでしょう:cry:）

しかしAdvent Calendarに参加した手前、何か技術的なことを書かなければいけません。というわけで、ちょっと前に2FAが絡むテストをAutifyで実現できるように実装した話を書きます。Autifyでどのように実現するかは [こちらのドキュメント](https://autify.zendesk.com/hc/ja/articles/4402453943321-Authenticator-app%E3%81%AB%E3%82%88%E3%82%8B%E4%BA%8C%E8%A6%81%E7%B4%A0%E8%AA%8D%E8%A8%BC-2FA-%E3%82%92Autify%E3%81%99%E3%82%8B) に書いてありますのでぜひご覧ください。

本エントリではどのように実装したか書きます。

# 背景
とあるアメリカのお客さんから、「ユーザーログインにAuth0を使っており、Authenticator Appを使わないとログインできない設定となっているため、これを突破できないとログイン後のテストができないので辛い」という話が来ました。

当時アメリカのセールスは僕一人でやっていたので、自分でどうにかしようと思い、Authenticator Appが生成するTOTP (Time-Based One-Time Password)であれば、生成してくれるライブラリがあるんじゃないかと思い調査しました。

# 実装

調査したところ、[totp-generatorという、まさしくなnpm](https://www.npmjs.com/package/totp-generator) を見つけました。

AutifyにはJavaScriptステップという、任意のJavaScriptコードが実行できる機能がありますが、これはブラウザ上で動くためnpmモジュールを動かすには難があります。というわけでTOTPを返してくれる簡単なREST APIを作ることにしました。

### REST APIの実装

expressを使って下記のように実装します。RequestにSecret Keyを渡すとTOTPが返ってきます。

```js
const express = require('express')
const totp = require('totp-generator')
const bodyParser = require('body-parser')
const cors = require('cors')
const app = express()
const port = process.env.PORT || 8080

app.use(bodyParser.urlencoded({
    extended: true
}))
app.use(bodyParser.json())
app.use(cors())

app.post('/totp', (req, res) => {
  try {
    let otp = totp(req.body.key)
    res.send(otp)
  } catch(e) {
    res.sendStatus(500).send(e)
  }
})

app.listen(port, () => {
  console.log(`Autify util api has started running`)
})
```

めちゃくちゃ簡単にできました。Herokuにデプロイして動作を確認します。

```sh
curl -X POST -H "Content-Type: application/json" -d '{"key":"YOUR SECRET"}' https://autify-jsstep-util-apis.herokuapp.com/totp
```

きちんとTOTPが返ってくることが確認できました。次にこれを呼び出すAutifyのJSステップを組みます。
上記のコードは[こちらのリポジトリ](https://github.com/autifyhq/autify-jssnipet-util-apis)にありますので、是非ご活用ください。

### JSステップの構築

まずユーザー登録のように、Secret Keyが固定されない場合はページから取得する必要があります。
AutifyにてSecret Keyが表示されているページにて下記のようなJSステップを実行し、Secret Keyを `return` します。`return` した値は別のステップで利用できるので、後続のAPI呼び出しを行うJSステップで利用します。

```js
return document.querySelector(".auth0-mfa-code-otp-input").value
```
![Screenshot for grabbing a secret key](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/18378/1664bb06-648f-221f-68c5-458294c3c5f9.png)

次に `return` したSecret Keyを使って先ほど実装したAPIを呼び出すJSステップを作ります。

```js
var url = "https://autify-jsstep-util-apis.herokuapp.com/totp";
var xhr = new XMLHttpRequest();
var otp = null;

xhr.onerror = function() {
  throw new Error("Network Error");
};

xhr.onload = function() {
  otp = xhr.response;
};

xhr.open("POST", url, false);
xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
xhr.send("key=" + secret);

return otp;
```

下記のスクリーンショットにあるように、JSステップの引数に先ほどのステップの返却値を設定します。JSステップにおける引数の使い方は、[こちらの「1.4. 「引数機能」を使う」](https://autify.zendesk.com/hc/ja/articles/900002403603)をご参考ください。

![Screenshot for calling REST API](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/18378/5443684e-38dc-69b4-ea67-260a7d5b9cfc.png)

# 最後に
MFAをE2Eでどう突破するかは多くの人が困っている問題ですが、Authenticator Appを使ったものであれば簡単に突破できることが分かりました。Autifyでは[メールを用いた２段階認証も可能](https://autify.zendesk.com/hc/ja/articles/4402605463449-%E3%83%A1%E3%83%BC%E3%83%AB%E3%81%AB%E3%82%88%E3%82%8B%E4%BA%8C%E6%AE%B5%E9%9A%8E%E8%AA%8D%E8%A8%BC%E3%82%92Autify%E3%81%99%E3%82%8B)ですので、そのうちSMSを使ったものを突破できるようにしたいと思います。

前述の通り、徐々にCEOがコードを書いている場合ではない規模になってきてしまい悲しい限りですが、技術力の研鑽は行っていきたい所存です。