WebAuthn の動作デモをFirebaseで作ってみた

パスワードレス認証の旗手である WebAuthn、早くサービスに普及して欲しいですね。
認証の手間が省けるうえセキュリティ強度も上がるし、ユーザーにとってメリットが多くて良いことづくめです。
Yahoo Japan や GitHub、Backlog などサービスでの導入事例も増えてきていますし、いずれ自分も実装をする機会になるかもしれません。
ただ、WebAuthn の仕組みや解説の情報はあるものの、実際に動作するサンプルとその実装コードの情報とかがあまりないように思えたので、いつ案件が来ても慌てず対応できるように、動作デモを作りながら実装するための仕組みや手順をまとめてみました。

# WebAuthn 動作デモ

以下から実際に動いてるものを確認できます。

動作デモ： https://webauthn-demo.firebaseapp.com/
ソースコード： https://github.com/okamoai/webauthn-demo

**注：**

- このデモではビルドイン認証器のある環境のみで動作検証しています。USB や NFC のいわゆる cross-platform の動作検証はしていません。
- 動作検証は Surface Pro 4 の Windows Hello で ブラウザは Edge, Chrome, Firefox、Android は Pixel 3 の Chrome で行いました。
- WebAuthn の制限として、SSL 通信環境下、もしくは localhost ドメインでのみ動作します。

## クライアントサイド処理

WebAuthn の前提条件として、Web Authentication API に対応したブラウザが必要になります。

[Web Authentication API - Can I use](https://caniuse.com/#feat=webauthn)  では

- IE： 非対応
- Edge：18 以上 
- Firefox: 60 以上
- Chrome： 67 以上
- Safari： 13 以上
- Android Browser：5 以上 
- Android Chrome：対応 
- iOS Safari： 非対応

となっています。
IE11 とかはまあどうでもいいんですが、WebAuthn は認証機器が搭載済みのスマートフォンにおいて真価を発揮できるので、iPhone/iPad 非対応は日本のスマホシェア的には痛いところ。いちおう PC版 Safari の最新バージョン（13）では対応されているので、iOS の方にもいずれやって来ることを期待したいですね。

対応ブラウザでは JavaScript の `navigator.credentials.create()` で新規登録処理を、`navigator.credentials.get()` で認証処理を行います。


## サーバサイド処理

WebAuthn を成立させるために 4つの API のエンドポイントが必要になり、実行される手順は以下のような形になります。

1. 新規登録時、ユーザ ID に紐付いたチャレンジレスポンスを発行する API
2. 新規登録時、ブラウザの認証器が発行した公開鍵を検証し、データベースに保存する API
3. 認証チェック時、ユーザ ID に紐付いたチャレンジレスポンスを発行する API
4. ブラウザの認証器が発行した署名をサーバに保存した公開鍵で検証してセッションを発行する API

クライアントサイドからやって来るデータはバッファデータも含まれているので、それらを処理する必要もあります。


# 新規登録時のフロー

新規登録時におけるクライアントサイドとサーバサイドの一連の流れをまとめてみました。
サーバサイド側の検証項目が多くて大変ですが、一つずつステップに分けて解説します。

![navigator.credentials.create.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99076/c20f4d7c-b3f3-4f8b-d1ad-d1d4931df226.png)


## Step 1. 非同期通信でチャレンジ発行APIにリクエスト

最初にクライアント側からチャレンジ発行のAPIに向けてリクエストを発行します。
チャレンジ発行はユーザー識別情報と紐づいている必要があります。（デモではメールアドレスとしています）
ここでは省略していますが、実際には登録したメールアドレス宛に届く新規登録用URLを経由するなどして、メールアドレスの所有者であることを事前に確認しておく必要があるかと思います。
非同期通信方法は問いませんが、デモでは [axios](https://github.com/axios/axios) を使ってます。


```js
axios.post('/api/attestation/challenge', { userId: 'example@example.com' })
```

## Step 2. チャレンジを発行し、ユーザ識別情報に紐づけてデータベースに保存

API 側では受け取ったリクエストに対して推測不可能で一意でランダムな値を発行します。
サーバ側の UUID を発行する仕組みを使って発行すると良いかと思います。
この値をキーにしてリクエストに含まれている userId を紐づけてデータベースに保存します。

デモでは FireStore の Document Id をそのまま使っています。

```js
const db = firebase.firestore()
const challenge = await db
  .collection('challenge')
  .add({
    userId: 'example@example.com',
    timestamp: new Date().getTime(),
  })
  .then(docRef => docRef.id)
```

また、デモでは省略していますがチャレンジ値の保存はAPIリクエストの度に追加されていくので、
ガベージコレクションをして定期的に古いデータ削除する必要があります。


## Step 3. 発行したチャレンジをレスポンスで返す

API はチャレンジ値をレスポンスデータに含めて返却をします。
認証器への処理を行う際にこのチャレンジ値が必須になるためです。
このチャレンジ発行の一連の流れはリプレイ攻撃に対する対策のため必要な手順になります。

## Step 4. `navigator.credentials.create()` でユーザーの認証処理を行う

`navigator.credentials.create()` を実行することでブラウザから OS の認証器に向けて新規認証登録処理を行うことができます。
実行に必要最低限のオプションを適用したコード例は以下のような形になります。

```js
const credentials = await navigator.credentials.create({
  publicKey: {
    rp: {
      id: 'localhost',
      name: 'WebAuthn Demo'
    },
    user: {
      id: base64url.toBuffer('user_id').buffer,
      name: 'example@example.com',
      displayName: 'User Name'
    },
    pubKeyCredParams: [
      {
        type: 'public-key',
        alg: -7
      },
      {
        type: 'public-key',
        alg: -257
      }
    ],
    challenge: base64url.toBuffer('Pj5VVSB2wIBmTU3NIiKg').buffer,
  }
})
```
publicKey オプションはほかにもいろいろな項目があるんですが、ここでは必要最低限の説明に留めておきます。

| プロパティ | 型| 説明 |
|---|---|---|
| rp.id | String | サービス事業者の識別値。ドメイン名など。後の検証で使われる |
| rp.name | String | サービス事業者名称 |
| user.id | ArrayBuffer | ユーザー識別値 |
| user.name | String | ユーザー名を示す値（メールアドレスなど） |
| pubKeyCredParams.type| String | `'public-key'` の固定文字列 |
| pubKeyCredParams.alg| Number | 鍵作成時のアルゴリズム（[COSE Algorithms準拠](https://www.iana.org/assignments/cose/cose.xhtml#algorithms)）を示す数値 |
| challenge| ArrayBuffer | 発行されたチャレンジ値 |

[publicKey オプションの構成](https://developer.mozilla.org/en-US/docs/Web/API/PublicKeyCredentialCreationOptions)

`user.id` `challenge` は ArrayBuffer 型に変換する必要があります。
デモでは [base64url](https://www.npmjs.com/package/base64url) を使って文字列を ArrayBuffer に変換しています。

## Step 5. 認証器が情報を返却する

認証器での認証が成功すると OS のセキュアな領域に秘密鍵を保存した後に、公開鍵や署名情報などを含むデータを返却します。

```js
{
  "id": "JjBd6aeQ2h7MmJMW7sVqTpGQ5q6KpzhYlYYjABNKDZk",
  "rawId": ArrayBuffer,
  "type": "public-key",
  "response": {
    "attestationObject": ArrayBuffer,
    "clientDataJSON": ArrayBuffer
  }
}
```

| プロパティ | 型| 説明 |
|---|---|---|
| id | String | rawId を base64url 化したもの |
| rawId | ArrayBuffer | 認証情報の一意な識別情報。 `navigator.credentials.get()` 時に認証情報の検索に使用されます |
| type | String | `'public-key'` の固定文字列 |
| response.attestationObject | ArrayBuffer | 認証用の公開鍵や署名情報などを含むデータ。[CBOR](http://cbor.io/) エンコードされた ArrayBuffer   |
| response.clientDataJSON| ArrayBuffer | 認証用のクライアントデータ。JSON String を ArrayBuffer 化したもの  |

[navigator.credentials.create() の返却値](https://developer.mozilla.org/en-US/docs/Web/API/PublicKeyCredential)
[navigator.credentials.create().response の返却値](https://developer.mozilla.org/en-US/docs/Web/API/AuthenticatorAttestationResponse)


## Step 6. 非同期通信で認証登録APIにリクエスト

認証器からの返却値を認証登録用のAPIに非同期通信でリクエストを送ります。
返却値の `clientDataJSON` と `attestationObject` は ArrayBuffer 型なので、そのままでは送信できません。
APIに送る前に base64 化してから送る必要があります。

デモでは以下のように base64url で ArrayBuffer を String に変換して送信しています。

```js
await axios.post('/api/attestation/register', {
  id: credentials.id,
  attestationObject: base64url(credentials.response.attestationObject),
  clientDataJSON: base64url(credentials.response.clientDataJSON),
})
```

## Step 7. `clientDataJSON`, `attestationObject` を base64 デコード

API にリクエストされたデータは base64 エンコードされているので、受け取ったデータを先ずは base64 デコードしてデータとして扱えるようにします。
デモではクライアントサイドで使っていた base64url を使ってデコード処理をしています。 `clientDataJSON` は JSON データを文字列化したものなので、 `JSON.pase()` でオブジェクトとして扱えるようにします。

```js
const clientDataJSON = JSON.parse(base64url.decode(req.body.clientDataJSON))
const attestationObjectBuffer = base64url.toBuffer(req.body.attestationObject)
```

clientDataJSON は以下のような内容で構成されています。

```js
const clientDataJSON = {
  challenge: 'lTbl5U06Hra0SEowP2SH',
  origin: 'http://localhost:8080',
  type: 'webauthn.create',
}
```

| プロパティ | 型| 説明 |
|---|---|---|
| challenge | String | `navigator.credentials.create()` の challenge 値 |
| origin | String | `navigator.credentials.create()` を実行したときのプロトコルを含めたFQDN |
| type| String | 発行形式。 `navigator.credentials.create()` の場合 `'webauthn.create'` 固定 |

[clientDataJSON の構成](https://developer.mozilla.org/en-US/docs/Web/API/AuthenticatorResponse/clientDataJSON) 

## Step 8. チャレンジをデータベースに照会してユーザーの正当性を検証

`clientDataJSON.challenge` の値をデータベースに照会して、正規のユーザーかどうかを確認します。
デモでは FireStore の Document ID を検索してユーザーID の値を見る形でチェックしています。

```js
const userId = await db
  .collection('challenge')
  .doc(clientDataJSON.challenge)
  .get()
  .then(doc => (doc.exists ? doc.get('userId') : null))
```

## Step 9. origin が認証処理を行った FQDN と一致しているか検証

`clientDataJSON.origin` の値がサービスを提供している FQDN と一致をしているか確認します。

```js
if (clientDataJSON.origin !== 'http://localhost:8080') {
  // エラー処理
}
```

## Step 10. `attestationObject` を CBOR デコード

`attestationObject` は [CBOR](http://cbor.io/) という形式のバッファデータです。
これをデータとして扱える形にデコードする必要があります。
デモでは [cbor-js](https://github.com/paroga/cbor-js) というパッケージを使ってデコード処理をしています。

```js
const attestationObject = cbor.decode(attestationObjectBuffer)
```
`attestationObject` は以下のような構成になっています。

```js
const attestationObject = {
  fmt: 'packed',
  authData: ArrayBuffer,
  attStmt: {
    alg: -257,
    sig: ArrayBuffer,
  }
}
```

| プロパティ | 型| 説明 |
|---|---|---|
| fmt | String | `attStmt` の形式を示す文字列。WebAuthn が扱える形式は現時点では `"packed"`, `"tpm"`, `"android-key"`, `"android-safetynet"`, `"fido-u2f"`, `"none"` です |
| authData | ArrayBuffer| 公開鍵などの認証用のデータ群が格納された ArrayBuffer  |
| attStmt | Object | 署名情報など。 `fmt` の種類よって返却値が変わります。 詳しい仕様は [Defined Attestation Statement Formats](https://www.w3.org/TR/webauthn/#defined-attestation-formats) を参照  |

[attestationObject の構成](https://developer.mozilla.org/en-US/docs/Web/API/AuthenticatorAttestationResponse/attestationObject) 


`attestationObject` の全体像は以下の図を見ると分かりやすいです。
![fido-attestation-structures.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99076/9d74716c-5093-b4f5-2167-e8cd7c713897.png)
 [Attestation object layout illustrating the included authenticator data - W3C](https://www.w3.org/TR/webauthn/#fig-attStructs) 


## Step 11. `attestationObject.authData` をパース

attestationObject の全体像を見ての通り、 `attestationObject.authData` は取り決めされたルールによってバイトごとに情報が区切られたバッファデータです。

| プロパティ | byte | 説明 |
|---|---|---|
| rpIdHash| 32 | `navigator.credentials.create()` で設定した `publicKey.rp.id` の値を SHA256ハッシュ化したもの |
| flags | 1 | 認証時の状態を示す真偽値。さらにbit単位に分割して検証する必要があります  |
| signCount | 4 | 認証器の検証回数。32ビットの符号なしビッグエンディアン整数で格納されてます |
| attestedCredentialData | 可変 | 公開鍵情報などが含まれたデータ。この中も[Attested Credential Data](https://www.w3.org/TR/webauthn/#sec-attested-credential-data) のルールに従ってデータを分割する必要があります |
| attestedCredentialData.aaguid | 16 | 認証器ごとの識別情報 |
| attestedCredentialData.credentialIdLength| 2 | `attestedCredentialData.credentialId` のバイト数。 16ビットの符号なしビッグエンディアン整数で格納されてます |
| attestedCredentialData.credentialId| `credentialIdLength` の値 | 公開鍵に割り振られたユニークID |
| attestedCredentialData.credentialPublicKey| 可変 | 公開鍵情報。[COSE](https://tools.ietf.org/html/rfc8152) 形式で格納されています  |

[Authenticator Data の構成](https://www.w3.org/TR/webauthn/#authenticator-data)

デモでは `ArrayBuffer.slice()` を使ってバイト毎にデータを分割して必要な情報を取り出しています。 

```js
const { authData } = attestationObject
const rpIdHash = authData.slice(0, 32)
const flags = authData[32]
const signCount = (authData[33] << 24) | (authData[34] << 16) | (authData[35] << 8) | authData[36]
const aaguid = authData.slice(37, 53)
const credentialIdLength = (authData[53] << 8) + authData[54]
const credentialId = authData.slice(55, 55 + credentialIdLength)
const credentialPublicKey = authData.slice(55 + credentialIdLength)
```


## Step 12. `attestationObject.authData.credentialPublicKey ` の公開鍵情報を JWK に変換

credentialPublicKey は[COSE](https://tools.ietf.org/html/rfc8152) という形式で格納されています。
これをテキストベースの情報である[JWK（JSON Web Key）](https://openid-foundation-japan.github.io/rfc7517.ja.html) 形式に変換しておくことでデータベースへの保存や署名の検証で取り扱いやすくなります。

デモでは `cbor-js` を使ってデータをデコードしつつ、JWK形式に変換しています。 

```js
const coseToJwk = cose => {
  try {
    let publicKeyJwk = {}
    const publicKeyCbor = cbor.decode(cose)
    if (publicKeyCbor[3] === -7) {
      publicKeyJwk = {
        kty: 'EC',
        crv: 'P-256',
        x: base64url(publicKeyCbor[-2]),
        y: base64url(publicKeyCbor[-3]),
      }
    } else if (publicKeyCbor[3] === -257) {
      publicKeyJwk = {
        kty: 'RSA',
        n: base64url(publicKeyCbor[-1]),
        e: base64url(publicKeyCbor[-2]),
      }
    } else {
      throw new Error('Unknown public key algorithm')
    }
    return publicKeyJwk
  } catch (e) {
    throw new Error('Could not decode COSE Key')
  }
}
const publicKeyJwk = coseToJwk(credentialPublicKey.buffer)
```

## Step 13. `attestationObject.attStmt` の署名を検証

`attestationObject.attStmt` に認証器の署名情報があり、認証器が信頼のできるものかを検証する必要があります。
`attestationObject.fmt` で指定された形式ごとに署名の形式が違うため、形式に合わせて方法で検証する必要があります。


| 形式 | 説明 |
|---|---|
| [packed](https://www.w3.org/TR/webauthn/#packed-attestation) | WebAuthnに最適化された認証形式  |
| [tpm](https://www.w3.org/TR/webauthn/#tpm-attestation) | 主にWindowsで使われる認証方式 |
| [android-key](https://www.w3.org/TR/webauthn/#android-key-attestation) | Android で使われる認証形式 |
| [android-safetynet](https://www.w3.org/TR/webauthn/#android-safetynet-attestation) | Android の SafetyNet API に対応した認証形式 |
| [fido-u2f](https://www.w3.org/TR/webauthn/#fido-u2f-attestation) | FIDO U2F に対応した認証形式 |
| [none](https://www.w3.org/TR/webauthn/#none-attestation) | 認証形式を提供しないことを明示  |

デモでは `packed` のみ、`x5c` の形式をカバーしてます（デモで全ての形式を網羅するの大変なので手を抜かせてもらいましたが、ホントは形式毎に検証処理を書くのが正しいです）
また、JWK 形式のデータを PEM に変換する際に [jwk-to-pem](https://www.npmjs.com/package/jwk-to-pem) というパッケージを使用しています。

```js
const sha256 = data => {
  const hash = crypto.createHash('sha256')
  hash.update(data)
  return hash.digest()
}

const invalidSignature = ({
  authData,
  clientDataJSON,
  signature,
  pem,
  cryptType = 'sha256',
}) => {
  const clientDataHash = sha256(clientDataJSON)
  const verify = crypto.createVerify(cryptType)
  verify.update(authData)
  verify.update(clientDataHash)
  return !verify.verify(pem, signature)
}

const { fmt, attStmt } = attestationObject
const clientDataJSON = base64url.decode(req.body.clientDataJSON)

switch (fmt) {
  case 'packed':
    if ('x5c' in attStmt) {
      const [attestnCert] = attStmt.x5c
      const pem = '-----BEGIN CERTIFICATE-----\n'
        + base64url(attestnCert)
        + '\n-----END CERTIFICATE-----' 
      const signature = attStmt.sig
      if (
        invalidSignature({
          authData,
          clientDataJSON,
          pem,
          signature,
        })
      ) {
        // エラー処理
      }
    } else {
      const cryptType = publicKeyJwk.kty === 'RSA' ? 'RSA-SHA256' : 'sha256'
      const pem = jwkToPem(publicKeyJwk)
      const signature = attStmt.sig
      if (
        invalidSignature({
          authData,
          clientDataJSON,
          pem,
          signature,
          cryptType,
        })
      ) {
        // エラー処理
      }
    }
    break
}
```

署名情報の形式は以下の図のように、 `attestationObject.authData` と SHA-256ハッシュ化した `clientDataJSON` を結合したものです。

![fido-signature-formats-figure2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99076/b531ec5c-b723-c608-9b81-4c86a55e3d3f.png)
[Generating an assertion signature - W3C](https://www.w3.org/TR/webauthn/#fig-signature)

注意点として、 `clientDataJSON` を SHA-256ハッシュ化する際には、リクエストデータを base64 デコードをして `JSON.parse()` を実行する前のバッファデータに対して行う必要があります。
`JSON.parse()` 実行後の `clientDataJSON` をハッシュ化すると、署名情報と差異がでてきてしまい検証に失敗してしまいます。


## Step 14. `attestationObject.authData.rpIdHash` を検証

`attestationObject.authData.rpIdHash` の値が `navigator.credentials.create()` のオプションで指定した `publicKey.rp.id` の値と一致しているか確認します。
`rpIdHash` を [arraybuffer-to-buffer](https://www.npmjs.com/package/arraybuffer-to-buffer) というパッケージを使って ArrayBuffer を Buffer に変換しつつハッシュ化された値と突き合わせます。


```js
if (sha256('localhost').equals(arrayBufferToBuffer(rpIdHash)) === false) {
  // エラー処理
}
```

## Step 15. `attestationObject.authData.flags` を検証

`attestationObject.authData.flags` は 1byte のバッファデータですが、
これを bit に分割して真偽値として検証します。
各 bit の仕様の詳細は [flags](https://www.w3.org/TR/webauthn/#flags) を参照のこと。

今回は
- Bit 0: User Present (UP) ※ユーザーの存在を示す
- Bit 2: User Verified (UV) ※ユーザーの検証が正常に完了したことを示す
の値が それぞれ true であることを検証します。

```js
// Flags UP の一致を確認
if (Boolean(flags & 0x01) === false) {
  // エラー処理
}

// Flags UV の一致を確認
if (Boolean(flags & 0x04) === false) {
  // エラー処理
}
```

## Step 16. 認証情報をデータベースに保存

Step 8～15 までの検証を全てクリアしたら、

- `credentials.id` （`navigator.credentials.create()` が発行したID）
- [Step 12](#step-12-attestationobjectauthdatacredentialpublickey-の公開鍵情報を-jwk-に変換) で作成した JWK
- `attestationObject.authData.signCount`

をユーザ識別情報に紐づけてデータベースに保存します。

```js
await db
  .collection('credential')
  .doc(userId)
  .set({
    id: req.body.id,
    jwk: credentialPublicKey,
    signCount,
    timestamp: new Date().getTime(),
  })
```


## Step 17. クライアントにレスポンスを返す

Step 16 で検証に成功してデータベース保存したなら成功ステータスを、
途中で検証に失敗したらエラーレスポンスを返して登録フローは終了です。

基本的に登録フローは最初の一回のみで良く、次回以降のアクセスは認証フローとなります。


# 認証時のフロー

認証時におけるクライアントサイドとサーバサイドの一連の流れをまとめてみました。
新規登録時と共通する部分は多いので、同じところは省略します。

![navigator.credentials.get.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99076/2e0afbda-796f-e599-e6e5-10adc6bb006a.png)


## Step 1. 非同期通信でチャレンジ発行APIにリクエスト

新規登録時と手順は一緒です。APIのリクエスト先のみが違います。

```js
axios.post('/api/assertion/challenge', { userId: 'example@example.com' })
```

## Step 2. チャレンジを発行し、ユーザ識別情報に紐づけてデータベースに保存し認証IDを取得

こちらも新規登録と手順は同じです。相違点としてはデータベース保存の後、ユーザー識別情報から保存されている認証IDを取得することです。
認証IDは後の `navigator.credentials.get()` 実行時のオプションで利用します。

```js
const credentialId = await db
  .collection('credential')
  .doc(userId)
  .get()
  .then(doc => (doc.exists ? doc.get('id') : null))
```

## Step 3. 発行したチャレンジと認証IDをレスポンスで返す

API はチャレンジ値と取得した認証IDをレスポンスデータに含めて返却をします。
認証器への処理を行う際にこのチャレンジ値と認証IDが必要になるためです。

## Step 4. `navigator.credentials.get()` でユーザーの認証処理を行う

`navigator.credentials.get()` を実行することでブラウザから OS の認証器に向けて認証処理を行うことができます。
実行に必要最低限のオプションを適用したコード例は以下のような形になります。

```js
const credentials = await navigator.credentials.get({
  publicKey: {
    allowCredentials: [
      {
        transports: ['internal'],
        type: 'public-key',
        id: base64url.toBuffer('iPe3vi-z090G1Cal-eBNSHjfL_oylWphYOB6JhWboaA').buffer,
      }
    ],
    challenge: base64url.toBuffer('Pj5VVSB2wIBmTU3NIiKg').buffer,
  },
})
```

`navigator.credentials.create()` 同様、publicKey オプションはほかにもいろいろな項目があるんですが、ここでは必要最低限の説明に留めておきます。

| プロパティ | 型| 説明 |
|---|---|---|
| allowCredentials[].transports[] | String | 認証方法の指定。`"usb"`, `"nfc"`, `"ble"`, `"internal"` を指定できる。 `internal は指定必須` |
| allowCredentials[].type| String | `'public-key'` の固定文字列 |
| allowCredentials[].id | ArrayBuffer | 新規登録時に保存した認証ID（navigator.credentials.create() が発行したID） |
| challenge| ArrayBuffer | 発行されたチャレンジ値 |

[publicKey オプションの構成](https://developer.mozilla.org/en-US/docs/Web/API/PublicKeyCredentialRequestOptions)

`challenge` `allowCredentials[].id`  は ArrayBuffer 型に変換する必要があります。
デモでは [base64url](https://www.npmjs.com/package/base64url) を使って文字列を ArrayBuffer に変換しています。

## Step 5. 認証器が情報を返却する


認証器での認証が成功すると 、認証時の状態や署名情報などを含むデータを返却します。  

```js
{
  "id": "iPe3vi-z090G1Cal-eBNSHjfL_oylWphYOB6JhWboaA",
  "rawId": ArrayBuffer,
  "type": "public-key",
  "response": {
    "authenticatorData": ArrayBuffer,
    "clientDataJSON": ArrayBuffer,
    "signature": ArrayBuffer,
    "userHandle": ArrayBuffer
  }
}
```
`id`, `rawId`, `type`, `response.clientDataJSON` は `navigator.credentials.create()` の返却値と同義のものになるので、説明は省略します。

| プロパティ | 型| 説明 |
|---|---|---|
| response.authenticatorData| ArrayBuffer | `rpIdHash` や `signCount` などの認証情報を含んだ ArrayBuffer |
| response.signature| ArrayBuffer | 認証の署名を ArrayBuffer にしたもの  |
| response.userHandle| ArrayBuffer | ユーザーの識別情報を ArrayBuffer にしたもの |


[navigator.credentials.get() の返却値](https://developer.mozilla.org/en-US/docs/Web/API/PublicKeyCredential/response)
[navigator.credentials.get().response の返却値](https://developer.mozilla.org/en-US/docs/Web/API/AuthenticatorAssertionResponse)

## Step 6. 非同期通信で認証APIにリクエスト

認証器からの返却値を認証検証用のAPIに非同期通信でリクエストを送ります。
検証には `clientDataJSON` と `authenticatorData` `signature` が必要になり、それぞれ ArrayBuffer 型なので、`navigator.credentials.create()` のとき同様、API に送る前に base64 化してから送ります。

デモでは以下のように base64url で ArrayBuffer を String に変換して送信しています。

```js
await axios.post('/api/assertion/verify', {
  "authenticatorData": base64url(credentials.response.authenticatorData),
  "clientDataJSON": base64url(credentials.response.clientDataJSON),
  "signature": base64url(credentials.response.signature),
})
```

## Step 7. `clientDataJSON`, `authenticatorData`, `signature` を base64 デコード

`navigator.credentials.create()` のときと同じ手順で、受け取ったデータを base64 デコードします。

```js
const clientDataJSON = JSON.parse(base64url.decode(req.body.clientDataJSON))
const authenticatorData= base64url.toBuffer(req.body.authenticatorData)
const signature = base64url.toBuffer(req.body.signature)
```

clientDataJSON は以下のような内容で構成されています。

```js
const clientDataJSON = {
  challenge: 'lTbl5U06Hra0SEowP2SH',
  origin: 'http://localhost:8080',
  type: 'webauthn.get',
}
```
`type` が `webauthn.get` となっている以外は `navigator.credentials.create()` と同じです。

## Step 8. チャレンジをデータベースに照会してユーザーの正当性を検証

[navigator.credentials.create() の Step 8](#step-8-チャレンジをデータベースに照会してユーザーの正当性を検証)と同じです。

## Step 9. origin が認証処理を行った FQDN と一致しているか検証

[navigator.credentials.create() の Step 9](#step-9-origin-が認証処理を行った-fqdn-と一致しているか検証)と同じです。

## Step 10. `authenticatorData` をパース

[navigator.credentials.create() の Step 11](#step-11-attestationobjectauthdata-をパース) と手順は一緒で、バッファデータを指定のバイトで分割して処理をします。
`navigator.credentials.create()` と違い、値が `rpIdHash`, `flags`, `signCount` の3つのみです。

## Step 11. `authenticatorData.rpIdHash` を検証

[navigator.credentials.create() の Step 14](#step-14-attestationobjectauthdatarpidhash-を検証)と同じです。

## Step 12. `authenticatorData.flags` を検証

[navigator.credentials.create() の Step 15](#step-15-attestationobjectauthdataflags-を検証)と同じです。

## Step 13. データベースに保存されている認証情報一式を取得する

[navigator.credentials.create() の Step 16](#step-16-認証情報をデータベースに保存)で保存した情報をロードします。

```js
const credential = await db
  .collection('credential')
  .doc(userId)
  .get()
  .then(doc => (doc.exists ? doc.data() : null))
```

## Step 14. `signature` を公開鍵で検証


[navigator.credentials.create() の Step 13](#step-13-attestationobjectattstmt-の署名を検証)で行った署名検証と同じ流れで、保存された公開鍵を使って送信されてきた署名情報を検証をします。

```js
const clientDataJSON = base64url.decode(req.body.clientDataJSON)
const cryptType = credential.jwk.kty === 'RSA' ? 'RSA-SHA256' : 'sha256'
const pem = jwkToPem(credential.jwk)
if (
  invalidSignature({
    authData,
    clientDataJSON,
    pem,
    signature,
    cryptType,
  })
) {
  // エラー処理
}
```

## Step 15. `authenticatorData.signCount` の値を比較検証

データベースに保存されていた `signCount` と APIに送られてきた `authenticatorData.signCount` の値を比較して、データベースの値よりもAPIから送信された値の方が大きいことを確認します。
正規ユーザーであればこの値が逆転することはなくユーザー本人の環境からの認証であることの一つの理由になるためです。

```js
  if (signCount !== 0 && signCount < credential.signCount) {
    // エラー処理
  }
```

## Step 16. `authenticatorData.signCount` の更新情報をデータベースに保存

`signCount` の比較が正しければ、データベースの `signCount` を更新しておきます。

```js
await db
  .collection('credential')
  .doc(userId)
  .update({
    signCount,
    timestamp: new Date().getTime(),
  })
```

## Step 17. 従来の認証処理を行う

Step 8～16までの処理でエラーが一度も発生せずに検証が完了したら、従来の認証と同じログインセッションを発行する処理を行います。

## Step 18. クライアントにレスポンスを返す

検証にすべて成功したなら成功のレスポンスを、
途中で検証に失敗したらエラーレスポンスを返して認証フローは終了です。
お疲れさまでした。

# まとめ

ユーザーの認証を簡単にする WebAuthn、実装には上記で紹介した手順を踏む必要がありました。

フロントエンド側の処理は Web Authentication API を使って生成したデータを非同期で API に投げるだけなのでとても簡単ですが、バンクエンド側の処理は各種デコードやバッファのパース、形式別の署名検証など、やることが多くてなかなかに大変でした。
検証処理自体は定型的なものなので、いったん型に嵌めてしまえば次回からはラクできそうな感じではあります。

また、WebAuthn は秘密鍵をデバイスに保存するものなので、アカウントの認証機構を WebAuthn のみとすると、デバイスが物理的に利用不能になった時点でアカウントへのアクセスができずに色々と詰んでしまいます。
ですので、WebAuthn は従来の認証に併用する形でユーザーの利便性を高めるためのオプション的な位置づけで提供される形にすると良いでしょう。