Auth0に登録されているSMS認証用の電話番号を更新する

Auth0の多要素認証としてSMS認証を設定している場合、ユーザー自身が電話番号を更新できたほうが良いと思ったので、[前回のログインID（メールアドレス）の変更](https://qiita.com/morikavva/items/88b0c5db2972b0ddc117)に続いてSMS認証用の電話番号の更新方法を考えてみました。

※この記事で言うSMS認証用の電話番号はOIDCで定義されている標準クレームの`phone_number`とは別物です。

## 電話番号変更の基本的な流れ
シーケンス図にするとこんな感じです。

![change_sms_authenticator.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/490013/b430fe3f-aa73-c2f0-d2a5-3126166f5ce2.png)


クライアントアプリにSMS認証用の電話番号の一覧を表示する画面を用意して、そこから電話番号の追加/削除ができるイメージで考えました。

## 操作手順
今回はアプリ実装ではなく、コマンラインベースで動作を確認しています。

### 事前準備
クライアントとなるアプリのAuth0設定を変更します。
`DashBoard > Applications > 対象のクライアントアプリ`を選択し、設定画面の下の方にある
Advanced SettingsのGrant Typesメニューの中にある`MFA`のチェックボックスにチェックを入れます。

### MFA情報更新用アクセストークンを取得
ユーザーのMFAに関する情報を参照したり更新するためには、`https://{Auth0のドメイン}/mfa/`へのアクセス権が必要なので、Auth0を使ってログインをするときの認可リクエストに`audience`と`scope`パラメーターを追加します。

* audience
    * `https://{Auth0のドメイン}/mfa/`
* scope
    * `enroll`
    * `read:authenticators`
    * `remove:authenticators`

```javascript:Node.js(Express.js)での認可リクエスト例
const querystring = require('querystring');

const params = {
  audience: 'https://{Auth0のドメイン}/mfa/',
  scope: 'openid enroll read:authenticators remove:authenticators',
  response_type: 'code',
  client_id: '{クライアントID}',
  state: '{state}',
  redirect_uri: '{リダイレクト先}',
  nonce: '{nonce}',
};

res.redirect(
  `${`https://{Auth0のドメイン}/authorize/`
    + '?'}${
    querystring.stringify(params)}`,
);

```
普段Auth0にログインするときと同じ流れなので手順は省略しますが、最終的に以下のようなトークンが取れればOKです。

#### IDトークンのペイロード

```json
{
  "iss": "https://.../",
  "sub": "auth0|5e4b...",
  "aud": [
    "https://.../mfa/",
    "https://.../userinfo"
  ],
  "iat": 1581248735,
  "exp": 1581249335,
  "azp": "K3S6Uk...",
  "scope": "openid enroll read:authenticators remove:authenticators"
}
```

#### アクセストークン（JWT）のペイロード

```json
{
  "iss": "https://.../",
  "sub": "auth0|5e4b...",
  "aud": [
    "https://.../mfa/",
    "https://.../userinfo"
  ],
  "iat": 1581248735,
  "exp": 1581249335,
  "azp": "...",
  "scope": "openid enroll read:authenticators remove:authenticators"
}
```

#### トークンの有効期限
IDトークンとアクセストークンの`iat`と`exp`を見るとわかりますが
audienceに`https://{Auth0のドメイン}/mfa/`が含まれているとトークンの有効期限が10分になります。
以降の処理は10分以内に完了させないと、トークンの有効期限切れで途中で失敗するので、その場合は最初からやり直す必要があります。

### 登録済み電話番号の一覧を取得

アクセストークンを使って登録済み電話番号を取得します。

```http:Request
GET https://{Auth0のドメイン}/mfa/authenticators HTTP/1.1
Content-Type: application/json
Authorization: Bearer {アクセストークン}
```

登録済みの電話番号に加えて、最初に電話番号を登録したときに表示されるリカバリーコードの情報が取得できます。

```json:Response
[
  {
    "id": "recovery-code|dev_iBNU...",
    "authenticator_type": "recovery-code",
    "active": true
  },
  {
    "id": "sms|dev_M38ers...",
    "authenticator_type": "oob",
    "active": true,
    "oob_channel": "sms",
    "name": "+81 XXXXXXXXXXX"
  }
]
```

### 電話番号の追加登録
ユーザーに入力してもらった電話番号を登録します。

```http:Request
POST https://{Auth0のドメイン}/mfa/associate HTTP/1.1
Content-Type: application/json
Authorization: Bearer {アクセストークン}

{
  "client_id": "{クライアントID}",
  "client_secret": "{クライアントシークレット}",
  "authenticator_types": ["oob"],
  "oob_channels": ["sms"],
  "phone_number": "+81 XXXXXXXXXXX"
}
```

```json:Response
{
  "authenticator_type": "oob",
  "binding_method": "prompt",
  "oob_channel": "sms",
  "oob_code": "Fe26.2*7fzi84..."
}
```

`oob_code`はこの後の電話番号のアクティベートで使用するので控えておきます。

ちなみに、ここまでの状態で再度`GET /mfa/authenticators`を叩いて電話番号を取得すると以下のようなレスポンスが返ってきます。

```json
[
  {
    "id": "recovery-code|dev_iBNU...",
    "authenticator_type": "recovery-code",
    "active": true
  },
  {
    "id": "sms|dev_M38ers...",
    "authenticator_type": "oob",
    "active": true,
    "oob_channel": "sms",
    "name": "+81 XXXXXXXXXXX"
  },
  // ↓追加した電話番号
  {
    "id": "sms|dev_qFCdZ...",
    "authenticator_type": "oob",
    "active": false,
    "oob_channel": "sms",
    "name": "+81 XXXXXXXXXXX"
  }
]
```

今追加した電話番号はSMS送信の疎通確認ができていないのでactiveが`false`になっています。

#### 補足：初めて電話番号を登録する場合
もしもユーザーが電話番号をひとつも登録していない状態で`POST /mfa/associate`を叩くと、リカバリーコードを含んだレスポンスが返ってきます。この場合はユーザーにリカバリーコードを提示しましょう。

```json
{
  "authenticator_type": "oob",
  "binding_method": "prompt",
  "recovery_codes": [
    "E54JQ..."
  ],
  "oob_channel": "sms",
  "oob_code": "Fe26.2*2dcSxXwo..."
}
```

### 電話番号のアクティベート

ここまでのAPIリクエストでユーザーが入力した電話番号に対してSMSが送信されているはずなので、SMSに記載されているワンタイムパスワードを入力してもらいます。
これでSMSの疎通確認が完了するので、SMS認証でこの電話番号が使えるようになります。

```http:Request
POST https://{Auth0のドメイン}/oauth/token HTTP/1.1
Content-Type: application/x-www-form-urlencoded

client_id={クライアントID}&client_secret={クライアントシークレット}&mfa_token={アクセストークン}&grant_type=http%3A%2F%2Fauth0.com%2Foauth%2Fgrant-type%2Fmfa-oob&oob_code={oob_code}&binding_code={OTP}
```

見辛いので使用しているパラメータを列挙します。

* client_id
* client_secret
* mfa_token -> MFA情報更新用のアクセストークン
* grant_type ->  `http://auth0.com/oauth/grant-type/mfa-oob`
* oob_code -> `POST /mfa/associate`のレスポンスに含まれていたoob_code
* binding_code -> SMSで送信されたワンタイムパスワード


```json:Response
{
  "access_token": "eyJhNc0l...",
  "scope": "enroll read:authenticators remove:authenticators",
  "expires_in": 600,
  "token_type": "Bearer"
}
```

これで電話番号の登録は完了です。

### 登録済みの電話番号を削除
`GET /mfa/authenticators`で取得したID（sms|〜で始まるやつ）を指定して電話番号を削除します。

```http:Request
DELETE https://{Auth0のドメイン}/mfa/authenticators/{id} HTTP/1.1
Authorization: Bearer {アクセストークン}
```

電話番号の削除処理はこれだけです。

#### 補足：電話番号を全て削除した場合
activeな電話番号を全て削除してしまい、ユーザーが保持するauthenticatorがリカバリーコードだけになると、次ログインするときにID/パスワード入力後のSMS認証でリカバリーコードの入力を求められます。
<img width="267" alt="スクリーンショット 2020-02-07 18.40.45.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/490013/87c73558-29eb-2933-9e3a-2109fb95aa37.png">

こんなふうに

## 電話番号が複数登録されているときのSMS認証

電話番号が複数登録されているとSMS認証のときにSMS送信先の電話番号を選択できるようになります。

<img width="267" alt="スクリーンショット 2020-02-07 18.50.17.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/490013/0bd16b89-bcff-3354-e5e0-ccc7cebd8131.png">

電話番号の左に表示される`>`をクリックすると

<img width="267" alt="スクリーンショット 2020-02-07 18.41.24.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/490013/c28bbdc5-dd17-505f-5bd7-05ed5d3d9c56.png">

SMSの送信先を選択できるようになりました！

このやり方でSMS認証用の電話番号のセルフメンテ機能が実現できそうです。