はじめての WebAuthn

1. はじめに

WebAuthn / FIDO2 とは何ですか?

Web Authentication API (WebAuthn) を使うと、オリジンをスコープした公開鍵ベースのユーザー認証を実現することができます。API は、BLE、NFC、および USB の U2F もしくは FIDO2 の Roaming Authentictor(ローミング認証器 = セキュリティキー)のほか、ユーザーが指紋や画面ロックを使用して認証できる Platform Authenticator(プラットフォーム認証器)の使用をサポートしています。

これから作るもの

このコードラボでは、指紋センサーを使用した簡単な再認証機能を備えたウェブサイトを構築します。「再認証」とは、ユーザーがWebサイトに一度サインインしたあと、支払いやパスワード変更など、Web サイトの重要なセクションにアクセスしようとしたり、一定の間隔を置いて戻ってきたとき、アカウントデータを保護するため再度認証を行おうとすることです。

これから学ぶこと

さまざまな状況に対応する WebAuthn API とオプションの呼び出し方法、および、再認証特有のベストプラクティスとコツについても学びます。

必要なもの

ハードウェア (以下のいずれか)

  • (できれば) 生体認証センサーを備えた Android デバイス
  • Touch ID または Face ID を備えた iPhone または iPad (iOS 14 以降)
  • Touch ID を備えた MacBook Pro または Air (macOS Big Sur 以降)
  • Windows Hello セットアップを備えた Windows 10 (19H1 以降)

ブラウザ

  • Google Chrome 67 以降
  • Microsoft Edge 85 以降
  • Safari 14 以降

2. セットアップ

このコードラボで作業するために、 glitchというサービスを使用します。ここで、JavaScriptを使用してクライアント側とサーバー側の両方のコードを編集し、それらを即座にデプロイできます。次のURLにアクセスしてください。

https://glitch.com/edit/#!/webauthn-codelab-start

始める前の状態

最初にウェブサイトの初期状態を見てみましょう。上部にある "Show" をクリックし、"In a New Window" をクリックすれば、 動作中のWebサイトを新しいウィンドウで表示することができます。

  1. username (ユーザー名) を入力して送信します(登録は必要ありません。ユーザー名を指定すると新しいアカウントが作成されます)
  2. password (パスワード) を入力して送信します(パスワードは何を入力しても認証されます)
  3. ユーザーはホームページにアクセスします。"Sign out" をクリックすると、ログアウトします。"Try reauth" をクリックすると、2に戻ります。

再度サインインしようとするたびにパスワードを入力する必要があることに注意してください。これはユーザーがサイト内の大切なページにアクセスしようとして、再認証を求められている様子を再現しています。

何を実装するのか?

  1. ユーザーが "User Verifying Platform Authenticator (UVPA)"(ユーザー認証機能付きプラットフォーム認証器)を登録できるようにします。
  2. ユーザーがパスワードを入力せずに、例えば指紋を使った生体認証だけで再認証できるようにします。

完成品は ここからプレビューすることができます。

コードをリミックスする

まずは https://glitch.com/edit/#!/webauthn-codelab-start の左上隅にある "Remix Project" ボタンを押してプロジェクトをフォークし、新しいURLを使った自分のバージョンを作りましょう。

8d42bd24f0fd185c.png

次へ移りましょう!

3. 指紋を使用してクレデンシャルを登録する

最初に、UVPA を使って生成されたクレデンシャルを登録する必要があります。ユーザー認証機能付きプラットフォーム認証器 (UVPA) は、パソコンやスマートフォンといったプラットフォームに組み込まれ、生体認証や画面ロックを使用してユーザーを確認することができます。

260aab9f1a2587a7.png

この機能を /home ページに追加していきます。

registerCredential() 関数を作成する

registerCredential() というクレデンシャルを登録する関数を作ってみましょう。

public/client.js

export const registerCredential = async () => {

};

サーバーのエンドポイントからチャレンジとその他のオプションを取得する: /auth/registerRequest

ユーザーにクレデンシャルを求める前に、サーバーから WebAuthn を呼び出すために必要なチャレンジを含めたパラメーターを取得します。幸運にもサーバーにはすでに期待したパラメーターを返してくれるエンドポイントが用意してあります。

下記のコードを registerCredential() に追加して下さい。

public/client.js

const opts = {
  attestation: 'none',
  authenticatorSelection: {
    authenticatorAttachment: 'platform',
    userVerification: 'required',
    requireResidentKey: false
  }
};

const options = await _fetch('/auth/registerRequest', opts);

サーバーとクライアント間のプロトコルは WebAuthn の仕様に含まれていませんが、このコードラボでは PublicKeyCredentialCreationOptions によく似た JSON オブジェクトをサーバーに渡せるようデザインしてあります。下記はその中でも重要なパラメーターです。

attestation

アテステーション伝達の優先度(noneindirect または direct)。必要でない限り none を選択してください。

excludeCredentials

認証器の重複登録を防ぐための PublicKeyCredentialDescriptor の配列。

authenticatorSelection

authenticatorAttachment

利用可能な認証器をフィルタします。プラットフォーム認証器を使う場合は "platform" を、ローミング認証器を使う場合は"cross-platform" を使用します。

userVerification

認証器の端末上のユーザー検証が "required"(必須)、"preferred"(好ましい)、"discouraged"(回避)のいずれであるかを確認します。指紋認証や画面ロック認証が必要な場合は、"required" を使用します。

requireResidentKey

作成したクレデンシャルを将来のアカウント選択 UI で使用できるようにする場合は true を使用します。

これらのオプションについて詳しく調べるには、 WebAuthnの公式仕様をご覧ください。

サーバーから返ってくるオプションの例を下記に示します。

{
  "rp": {
    "name": "WebAuthn Codelab",
    "id": "webauthn-codelab.glitch.me"
  },
  "user": {
    "displayName": "User Name",
    "id": "...",
    "name": "test"
  },
  "challenge": "...",
  "pubKeyCredParams": [
    {
      "type": "public-key",
      "alg": -7
    }, {
      "type": "public-key",
      "alg": -257
    }
  ],
  "timeout": 1800000,
  "attestation": "none",
  "excludeCredentials": [
    {
      "id": "...",
      "type": "public-key",
      "transports": [
        "internal"
      ]
    }
  ],
  "authenticatorSelection": {
    "authenticatorAttachment": "platform",
    "userVerification": "required"
  }
}

クレデンシャルを生成する

これらのオプションは、HTTP プロトコルを通過する際、文字列にエンコードして送信されるため、一部のパラメーター、具体的には user.idchallenge および excludeCredentials という配列に含まれる id を、バイナリーに変換する必要があります:

public/client.js

options.user.id = base64url.decode(options.user.id);
options.challenge = base64url.decode(options.challenge);

if (options.excludeCredentials) {
  for (let cred of options.excludeCredentials) {
    cred.id = base64url.decode(cred.id);
  }
}

最後に新しいクレデンシャルを作るために navigator.credentials.create() を呼び出します。この呼び出しにより、ブラウザーは認証器と対話し、指紋センサーまたは画面ロックを使用してユーザーのアイデンティティを検証します。

public/client.js

const cred = await navigator.credentials.create({
  publicKey: options
});

ユーザーのアイデンティティを端末上で検証できたら、受け取ったクレデンシャルオブジェクトをサーバーに送信して認証器を登録します。

クレデンシャルをサーバーのエンドポイントに登録する: /auth/registerResponse

これは受け取ったクレデンシャルオブジェクトの例です。

{
  "id": "...",
  "rawId": "...",
  "type": "public-key",
  "response": {
    "clientDataJSON": "...",
    "attestationObject": "..."
  }
}

クレデンシャルを登録する際に受け取ったオプションオブジェクトと同様、クレデンシャルのバイナリパラメータをエンコードして、文字列としてサーバーに送信できるようにする必要があります。

public/client.js

const credential = {};
credential.id =     cred.id;
credential.rawId =  base64url.encode(cred.rawId);
credential.type =   cred.type;

if (cred.response) {
  const clientDataJSON =
    base64url.encode(cred.response.clientDataJSON);
  const attestationObject =
    base64url.encode(cred.response.attestationObject);
  credential.response = {
    clientDataJSON,
    attestationObject
  };
}

ユーザーが戻ってきた時に認証に使用できるよう、クレデンシャル ID をローカルに保存しておきます。

public/client.js

localStorage.setItem(`credId`, credential.id);

最後にオブジェクトをサーバーに送信し、HTTP コード 200 が返ってきたら、新しいクレデンシャルは正常に登録されています。

public/client.js

return await _fetch('/auth/registerResponse' , credential);

おめでとうございます。これで registerCredential() は完成です!

このページのコードまとめ

public/client.js

...
export const registerCredential = async () => {
  const opts = {
    attestation: 'none',
    authenticatorSelection: {
      authenticatorAttachment: 'platform',
      userVerification: 'required',
      requireResidentKey: false
    }
  };

  const options = await _fetch('/auth/registerRequest', opts);

  options.user.id = base64url.decode(options.user.id);
  options.challenge = base64url.decode(options.challenge);

  if (options.excludeCredentials) {
    for (let cred of options.excludeCredentials) {
      cred.id = base64url.decode(cred.id);
    }
  }
  
  const cred = await navigator.credentials.create({
    publicKey: options
  });

  const credential = {};
  credential.id =     cred.id;
  credential.rawId =  base64url.encode(cred.rawId);
  credential.type =   cred.type;

  if (cred.response) {
    const clientDataJSON =
      base64url.encode(cred.response.clientDataJSON);
    const attestationObject =
      base64url.encode(cred.response.attestationObject);
    credential.response = {
      clientDataJSON,
      attestationObject
    };
  }

  localStorage.setItem(`credId`, credential.id);
  
  return await _fetch('/auth/registerResponse' , credential);
};
...

4. クレデンシャルを登録、取得、削除する UI を作る

登録済みのクレデンシャルのリストと、それらを削除するためのボタンがあると便利です。

9b5b5ae4a7b316bd.png

UI のプレースホルダーを作る

クレデンシャルを一覧表示する UI と、新しいクレデンシャルを登録するボタンを追加しましょう。機能が利用できるかどうかによって、警告文もしくはボタンのいずれかを hidden クラスを取り除くことで表示します。ul#list が登録済みクレデンシャルのリストを追加するためのプレースホルダーになります。

views/home.html

<p id="uvpa_unavailable" class="hidden">
  This device does not support User Verifying Platform Authenticator. You can't register a credential.
</p>
<h3 class="mdc-typography mdc-typography--headline6">
  Your registered credentials:
</h3>
<section>
  <div id="list"></div>
</section>
<mwc-button id="register" class="hidden" icon="fingerprint" raised>Add a credential</mwc-button>

機能検知と UVPA の確認

UVPA が利用できるかをチェックするためには、2つのことをする必要があります。ひとつは window.PublicKeyCredential をチェックして WebAuthn が利用できるかを確認すること。もうひとつは PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable() を呼び出して UVPA が利用できるかを確認することです。 どちらも利用できる場合は、新しいクレデンシャルを登録するボタンを表示し、いずれかが利用できない場合は、警告文を表示します。

views/home.html

const register = document.querySelector('#register');

if (window.PublicKeyCredential) {
  PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable()
  .then(uvpaa => {
    if (uvpaa) {
      register.classList.remove('hidden');
    } else {
      document
        .querySelector('#uvpa_unavailable')
        .classList.remove('hidden');
    }
  });        
} else {
  document
    .querySelector('#uvpa_unavailable')
    .classList.remove('hidden');
}

クレデンシャルのリストを取得して表示する: getCredentials()

登録済みのクレデンシャルを取得してリストに表示できるよう、 getCredentials() を作成しましょう。幸いなことに、サーバーには、サインインしたユーザーの登録されたクレデンシャルを取得できる便利なエンドポイント /auth/getKeys が既にあります。

返される JSON には、id や publicKey などのクレデンシャルに関する情報が含まれています。これらは HTML を作成することで表示することができます。

views/home.html

const getCredentials = async () => {
  const res = await _fetch('/auth/getKeys');
  const list = document.querySelector('#list');
  const creds = html`${res.credentials.length > 0 ? res.credentials.map(cred => html`
    <div class="mdc-card credential">
      <span class="mdc-typography mdc-typography--body2">${cred.credId}</span>
      <pre class="public-key">${cred.publicKey}</pre>
      <div class="mdc-card__actions">
        <mwc-button id="${cred.credId}" @click="${removeCredential}" raised>Remove</mwc-button>
      </div>
    </div>`) : html`
    <div>No credentials found.</div>
    `}`;
  render(creds, list);
};

ユーザーが /home を表示したら、すぐに getCredentials()を呼び出して利用可能なクレデンシャルを表示しましょう。

views/home.html

getCredentials();

クレデンシャルを削除する: removeCredential()

クレデンシャルのリストに、各クレデンシャルを削除するボタンを追加しました。これはクエリパラメータ credId とともに /auth/removeKey にリクエストを送信することで削除することができます。

public/client.js

export const unregisterCredential = async (credId) => {
  localStorage.removeItem('credId');
  return _fetch(`/auth/removeKey?credId=${encodeURIComponent(credId)}`);
};

既存の import 文に unregisterCredential を追記します。

views/home.html

import { _fetch, unregisterCredential } from '/client.js';

"Remove" ボタンが押されたら呼び出す関数を追加します。

views/home.html

const removeCredential = async e => {
  try {
    await unregisterCredential(e.target.id);
    getCredentials();
  } catch (e) {
    alert(e);
  }
};

クレデンシャルを登録する

最後に、"Add a credential" ボタンがクリックされたら、新しいクレデンシャルを登録する registerCredential() を呼び出します。

既存の import 文に registerCredential を追記します。

views/home.html

import { _fetch, registerCredential, unregisterCredential } from '/client.js';

ボタンがクリックされたら、 navigator.credentials.create() に渡すオプションを指定して registerCredential() を呼び出します。登録後に getCredentials() でクレデンシャルリストを更新することも忘れないでください。

views/home.html

register.addEventListener('click', e => {
  registerCredential().then(user => {
    getCredentials();
  }).catch(e => alert(e));
});

これで、新しいクレデンシャルを登録し、登録されたクレデンシャルの情報を表示できるようになりました。うまくいくか実際に動かして試してみてください。

このページのコードまとめ

views/home.html

...
      <p id="uvpa_unavailable" class="hidden">
        This device does not support User Verifying Platform Authenticator. You can't register a credential.
      </p>
      <h3 class="mdc-typography mdc-typography--headline6">
        Your registered credentials:
      </h3>
      <section>
        <div id="list"></div>
        <mwc-fab id="register" class="hidden" icon="add"></mwc-fab>
      </section>
      <mwc-button raised><a href="/reauth">Try reauth</a></mwc-button>
      <mwc-button><a href="/auth/signout">Sign out</a></mwc-button>
    </main>
    <script type="module">
      import { _fetch, registerCredential, unregisterCredential } from '/client.js';
      import { html, render } from 'https://unpkg.com/lit-html@1.0.0/lit-html.js?module';

      const register = document.querySelector('#register');

      if (window.PublicKeyCredential) {
        PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable()
        .then(uvpaa => {
          if (uvpaa) {
            register.classList.remove('hidden');
          } else {
            document
              .querySelector('#uvpa_unavailable')
              .classList.remove('hidden');
          }
        });        
      } else {
        document
          .querySelector('#uvpa_unavailable')
          .classList.remove('hidden');
      }

      const getCredentials = async () => {
        const res = await _fetch('/auth/getKeys');
        const list = document.querySelector('#list');
        const creds = html`${res.credentials.length > 0 ? res.credentials.map(cred => html`
          <div class="mdc-card credential">
            <span class="mdc-typography mdc-typography--body2">${cred.credId}</span>
            <pre class="public-key">${cred.publicKey}</pre>
            <div class="mdc-card__actions">
              <mwc-button id="${cred.credId}" @click="${removeCredential}" raised>Remove</mwc-button>
            </div>
          </div>`) : html`
          <p>No credentials found.</p>
          `}`;
        render(creds, list);
      };

      getCredentials();

      const removeCredential = async e => {
        try {
          await unregisterCredential(e.target.id);
          getCredentials();
        } catch (e) {
          alert(e);
        }
      };

      register.addEventListener('click', e => {
        registerCredential({
          attestation: 'none',
          authenticatorSelection: {
            authenticatorAttachment: 'platform',
            userVerification: 'required',
            requireResidentKey: false
          }
        })
        .then(user => {
          getCredentials();
        })
        .catch(e => alert(e));
      });
    </script>
...

public/client.js

...
export const unregisterCredential = async (credId) => {
  localStorage.removeItem('credId');
  return _fetch(`/auth/removeKey?credId=${encodeURIComponent(credId)}`);
};
...

5. 指紋でユーザーを認証する

これで、クレデンシャルが登録され、ユーザーを認証する準備が整いました。ウェブサイトに再認証機能を追加していきましょう。ユーザー体験は以下のとおりです。

ユーザーが /reauth にアクセスすると、利用可能な場合は指紋を使用した再認証を行う "Authenticate" ボタンを表示します。この "Authenticate" ボタンを押すと指紋を使用した再認証を求め、認証に成功したら /home にリダイレクトします。指紋を使用した再認証が利用できない場合、もしくは認証に失敗した場合は、既存のパスワードフォームを代わりに表示します。

b8770c4e7475b075.png

authenticate() 関数を作る:

指紋を使用してユーザーの身元を確認する authenticate() という関数を作成してみましょう。ここに JavaScript のコードを追加していきます。

public/client.js

export const authenticate = async () => {

};

サーバーのエンドポイントからチャレンジとその他のオプションを取得する: /auth/signinRequest

認証する前に、ブラウザがクレデンシャル ID を保存しているかを調べ、ある場合はそれをクエリパラメータとして設定します。サーバーはクレデンシャル ID を他のオプションとともに提示することにより最適な allowCredentials を返し、ユーザー検証の信頼性を高めることができます。

public/client.js

const opts = {};

let url = '/auth/signinRequest';
const credId = localStorage.getItem(`credId`);
if (credId) {
  url += `?credId=${encodeURIComponent(credId)}`;
}

ユーザーに認証を求める前に、サーバーにチャレンジとその他のパラメーターをリクエストします。opts を引数として _fetch() を呼び出し、POST リクエストをサーバーに送ります。

public/client.js

const options = await _fetch(url, opts);

受け取るオプションの例を示します( PublicKeyCredentialRequestOptions に近い構成になっています)。

{
  "challenge": "...",
  "timeout": 1800000,
  "rpId": "webauthn-codelab.glitch.me",
  "userVerification": "required",
  "allowCredentials": [
    {
      "id": "...",
      "type": "public-key",
      "transports": [
        "internal"
      ]
    }
  ]
}

ここで最も重要なオプションは allowCredentials です。サーバーからオプションを受け取ったとき allowCredentials は、渡されたクレデンシャル ID と一致するものが見つかったかどうかによって、ひとつのオブジェクトを含む配列か、空の配列のはずです。

allowCredentials が空の配列の場合は null でリゾルブして WebAuthn をスキップしましょう。

if (options.allowCredentials.length === 0) {
  console.info('No registered credentials found.');
  return Promise.resolve(null);
}

ユーザーを端末上で認証し、クレデンシャルを取得する

これらのオプションは、HTTP プロトコルを通過する際、文字列にエンコードして送信されるため、一部のパラメーター、具体的には challenge および allowCredentials という配列に含まれる id を、バイナリーに変換する必要があります:

public/client.js

options.challenge = base64url.decode(options.challenge);

for (let cred of options.allowCredentials) {
  cred.id = base64url.decode(cred.id);
}

最後に、navigator.credentials.get() を呼び出して、指紋センサーまたは画面ロックを使用してユーザーのアイデンティティを検証します。

public/client.js

const cred = await navigator.credentials.get({
  publicKey: options
});

ユーザーのアイデンティティを端末上で検証できたら、受け取ったクレデンシャルオブジェクトをサーバーに送信してクレデンシャルを検証し、ユーザーを認証します。

クレデンシャルを検証する: /auth/signinResponse

これは受け取った PublicKeyCredential オブジェクト (responseAuthenticatorAssertionResponse) の例です。

{
  "id": "...",
  "type": "public-key",
  "rawId": "...",
  "response": {
    "clientDataJSON": "...",
    "authenticatorData": "...",
    "signature": "...",
    "userHandle": ""
  }
}

ここでも、クレデンシャルのバイナリパラメータをエンコードして、文字列としてサーバーに送信できるようにする必要があります。

public/client.js

const credential = {};
credential.id =     cred.id;
credential.type =   cred.type;
credential.rawId =  base64url.encode(cred.rawId);

if (cred.response) {
  const clientDataJSON =
    base64url.encode(cred.response.clientDataJSON);
  const authenticatorData =
    base64url.encode(cred.response.authenticatorData);
  const signature =
    base64url.encode(cred.response.signature);
  const userHandle =
    base64url.encode(cred.response.userHandle);
  credential.response = {
    clientDataJSON,
    authenticatorData,
    signature,
    userHandle
  };
}

最後にオブジェクトをサーバーに送信し、HTTP コード 200 が返ってきたら、ユーザーは正常にサインインできています。

public/client.js

return await _fetch(`/auth/signinResponse`, credential);

おめでとうございます。これで authencation() は完成です!

このページのコードまとめ

public/client.js

...
export const authenticate = async () => {
  const opts = {};

  let url = '/auth/signinRequest';
  const credId = localStorage.getItem(`credId`);
  if (credId) {
    url += `?credId=${encodeURIComponent(credId)}`;
  }
  
  const options = await _fetch(url, opts);
  
  if (options.allowCredentials.length === 0) {
    console.info('No registered credentials found.');
    return Promise.resolve(null);
  }

  options.challenge = base64url.decode(options.challenge);

  for (let cred of options.allowCredentials) {
    cred.id = base64url.decode(cred.id);
  }

  const cred = await navigator.credentials.get({
    publicKey: options
  });

  const credential = {};
  credential.id = cred.id;
  credential.type = cred.type;
  credential.rawId = base64url.encode(cred.rawId);

  if (cred.response) {
    const clientDataJSON =
      base64url.encode(cred.response.clientDataJSON);
    const authenticatorData =
      base64url.encode(cred.response.authenticatorData);
    const signature =
      base64url.encode(cred.response.signature);
    const userHandle =
      base64url.encode(cred.response.userHandle);
    credential.response = {
      clientDataJSON,
      authenticatorData,
      signature,
      userHandle,
    };
  }

  return await _fetch(`/auth/signinResponse`, credential);
};
...

6. 再認証を有効にする

UI を作る

ユーザーが戻って来た時、できるだけ簡単かつ安全に再認証をしてもらいたいでしょう。ここで役立つのが生体認証です。ただ、下記のようなケースも想定する必要があります:

  • UVPA が利用できない
  • このデバイスからクレデンシャルが登録されていない
  • ストレージが削除され、クレデンシャル ID が分からない
  • 何らかの理由でユーザーが端末上で認証できない (手が濡れている、マスクを付けているなど)

そのため、常にフォールバックとなる認証方法を提供することが重要であり、このコードラボではその方法としてパスワードフォームによる認証を提供します。

19da999b0145054.png

既存のパスワードフォームに加えて、生体認証を起動するボタンを UI に追加しましょう。hidden クラスを活用することで、機能が利用できるかどうかで表示する UI を切り替えます。

views/reauth.html

<div id="uvpa_available" class="hidden">
  <h2>
    Verify your identity
  </h2>
  <div>
    <mwc-button id="reauth" raised>Authenticate</mwc-button>
  </div>
  <div>
    <mwc-button id="cancel">Sign-in with password</mwc-button>
  </div>
</div>

既存のフォームに class="hidden" を追加します。

views/reauth.html

<form id="form" method="POST" action="/auth/password" class="hidden">

機能検知と UVPA の確認

下記のいずれかの条件に当てはまる場合、ユーザーはパスワードで認証しなければなりません:

  • WebAuthn が使えない
  • UVPA が使えない
  • クレデンシャル ID が保存されていない

条件によって、"Authenticate" ボタンを表示、もしくはパスワードフォームを表示します。

views/reauth.html

if (window.PublicKeyCredential) {
  PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable()
  .then(uvpaa => {
    if (uvpaa && localStorage.getItem(`credId`)) {
      document
        .querySelector('#uvpa_available')
        .classList.remove('hidden');
    } else {
      form.classList.remove('hidden');
    }
  });        
} else {
  form.classList.remove('hidden');
}

パスワードフォームにフォールバックする

ユーザーは任意でパスワードログインも選択できるべきです。"Sign-in with password" ボタンをクリックすることで、"Authenticate" ボタンを隠してパスワードフォームを表示します。

views/reauth.html

const cancel = document.querySelector('#cancel');
cancel.addEventListener('click', e => {
  form.classList.remove('hidden');
  document
    .querySelector('#uvpa_available')
    .classList.add('hidden');
});

c4a82800889f078c.png

生体認証を起動する

生体認証を有効にしましょう。

既存の import 文に authenticate を追記します。

views/reauth.html

import { _fetch, authenticate } from '/client.js';

"Authenticate" ボタンが押されたら authenticate() を実行して生体認証をスタートします。失敗した場合はパスワードフォームにフォールバックします。

views/reauth.html

const button = document.querySelector('#reauth');
button.addEventListener('click', e => {
  authenticate().then(user => {
    if (user) {
      location.href = '/home';
    } else {
      throw 'User not found.';
    }
  }).catch(e => {
    console.error(e.message || e);
    alert('Authentication failed. Use password to sign-in.');
    form.classList.remove('hidden');
    document.querySelector('#uvpa_available').classList.add('hidden');
  });        
});

このページのコードまとめ

views/reauth.html

...
    <main class="content">
      <div id="uvpa_available" class="hidden">
        <h2>
          Verify your identity
        </h2>
        <div>
          <mwc-button id="reauth" raised>Authenticate</mwc-button>
        </div>
        <div>
          <mwc-button id="cancel">Sign-in with password</mwc-button>
        </div>
      </div>
      <form id="form" method="POST" action="/auth/password" class="hidden">
        <h2>
          Enter a password
        </h2>
        <input type="hidden" name="username" value="{{username}}" />
        <div class="mdc-text-field mdc-text-field--filled">
          <span class="mdc-text-field__ripple"></span>
          <label class="mdc-floating-label" id="password-label">password</label>
          <input type="password" class="mdc-text-field__input" aria-labelledby="password-label" name="password" />
          <span class="mdc-line-ripple"></span>
        </div>
        <input type="submit" class="mdc-button mdc-button--raised" value="Sign-In" />
        <p class="instructions">password will be ignored in this demo.</p>
      </form>
    </main>
    <script src="https://unpkg.com/material-components-web@7.0.0/dist/material-components-web.min.js"></script>
    <script type="module">
      new mdc.textField.MDCTextField(document.querySelector('.mdc-text-field'));
      import { _fetch, authenticate } from '/client.js';
      const form = document.querySelector('#form');
      form.addEventListener('submit', e => {
        e.preventDefault();
        const form = new FormData(e.target);
        const cred = {};
        form.forEach((v, k) => cred[k] = v);
        _fetch(e.target.action, cred)
        .then(user => {
          location.href = '/home';
        })
        .catch(e => alert(e));
      });

      if (window.PublicKeyCredential) {
        PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable()
        .then(uvpaa => {
          if (uvpaa && localStorage.getItem(`credId`)) {
            document
              .querySelector('#uvpa_available')
              .classList.remove('hidden');
          } else {
            form.classList.remove('hidden');
          }
        });        
      } else {
        form.classList.remove('hidden');
      }

      const cancel = document.querySelector('#cancel');
      cancel.addEventListener('click', e => {
        form.classList.remove('hidden');
        document
          .querySelector('#uvpa_available')
          .classList.add('hidden');
      });

      const button = document.querySelector('#reauth');
      button.addEventListener('click', e => {
        authenticate().then(user => {
          if (user) {
            location.href = '/home';
          } else {
            throw 'User not found.';
          }
        }).catch(e => {
          console.error(e.message || e);
          alert('Authentication failed. Use password to sign-in.');
          form.classList.remove('hidden');
          document.querySelector('#uvpa_available').classList.add('hidden');
        });        
      });
    </script>
...

7. おめでとうございます!

あなたは無事 "はじめての WebAuthn" のコードラボを修了しました。

あなたが学んだこと

  • ユーザー認証機能付きプラットフォーム認証器を使用してクレデンシャルを登録する方法。
  • 登録済みの認証器を使用してユーザーを認証する方法。
  • 新しい認証器を登録するための利用可能なオプション。
  • 生体認証センサーを使用して再認証するための UX ベストプラクティス。

次のステップ

  • FIDO2 API を使用して Android ネイティブアプリで同様のエクスペリエンスを構築する方法を学ぶ。
  • Ditigal Asset Links を使用して、ウェブサイトと Android アプリを関連付け、それらの間でクレデンシャルを共有する方法を学ぶ。

Your first Android FIDO2 API コードラボを試すことで、これら両方を学ぶことができます。

リソース

FIDO Alliance の Yuriy Ackermann 氏に感謝します。