STORES Product Blog

こだわりを持ったお商売を支える「STORES」のテクノロジー部門のメンバーによるブログです。

Passkeyの作成・取得に関するWebAuthn APIの重要オプション解説

はじめに

こんにちは!@m11oです。

この記事はSTORES Advent Calendar 2024の17日目の記事です。今回はWebAuthn APIにおけるPasskeyの作成・取得に関する主要なオプションを網羅的に解説しようと思います。

というのも、自分がPasskeyを実装した際に、WebAuthn APIのドキュメント以外でまとまった記事が見つからず、試行錯誤したり、色々な人に教えてもらったりしながら、苦心して実装しました。

なので、この記事ではオプション一つひとつの役割や返ってくるデータを明示することで、実装時の参考にしていただければ嬉しいです。

想定読者

  • これからPasskey実装に取り組む・興味がある方
  • WebAuthn APIのドキュメントを読んでいるが、実際のオプション設定例やデータ構造がしっくりこない方
  • オプションによって認証器側がどのような動きをするかを知りたい方

この記事を読むとわかること

  • navigator.credentials.create() / navigator.credentials.get()の基本的な流れ
  • Passkey 実装時によく利用するオプションとそのデータ構造
  • ブラウザへの実装時における注意点

Passkey / WebAuthn / FIDO2の概要

Passkeyとは?

Passkey は、パスワードを使わずに本人確認を行うための新しい認証情報(クレデンシャル)で、FIDO2 標準の一部として策定されています。デバイス内部に秘密鍵を安全に保管し、サービス側(RP側)は公開鍵のみを扱う仕組みのため、盗み見やリスト型攻撃といったリスクを大幅に低減できると期待されています。さらに、パスワードを入力する手間や管理のストレスを軽減し、プライバシー保護とユーザビリティの向上を両立する点でも注目を集めています。

WebAuthn API、FIDO2 との関係

WebAuthn API は、FIDO2 という国際標準の枠組みにおいて「ブラウザからデバイスの認証機能を呼び出す」ためのフロントエンド向け API です。Passkey(FIDO2 Credential)を実際に作成・管理・使用する中心的な仕組みが WebAuthn であり、ユーザーデバイス側で行われる鍵の生成や署名なども、この API を通じて呼び出します。

たとえばユーザー登録時(Passkey の作成)や、ログイン時(Passkey の使用)に、navigator.credentials.create() や navigator.credentials.get() といったメソッドを呼び出して認証を進めます。本記事では、これらの呼び出し時に設定できるオプションの解説をメインに、実装上の注意点や裏側の仕組みを整理していきます。

Passkey実装の全体像

シーケンス図で見るフロー

会員登録

sequenceDiagram
    participant S as Server
    Actor U as ユーザー
    participant B as Browser
    participant A as Authenticator

    U->>B: 会員登録開始
    B->>S: Passkey登録開始

    Note over S: 1. Challenge を生成し、<br> セッションなどに保持
    S->>B: Challenge (ランダムバイト列など)

    Note over B: 2. フロントエンドで<br> create() を呼び出す
    B->>A: navigator.credentials.create({ publicKey { ... } })

    Note over A: 3. 認証器が秘密鍵生成<br>と署名データを作成
    A-->>B: credential (publicKeyCredential)

    Note over B: 4. 生成されたクレデンシャルを<br>サーバーに送信 (登録)
    B->>S: credential (id, response等)

    Note over S: 5. サーバー側で<br>クレデンシャルを検証し保存

上記シーケンス図では以下の6ステップがポイントとなります。

  1. サーバーでのチャレンジ生成
    • 登録処理の前に、サーバーは暗号的に安全な乱数(challenge)を生成します
    • この時、サーバー側は発行したchallengeをユーザーのセッションやメモリストアなどに保存しておきます
      • 目的 : 後のステップでブラウザから返ってきたチャレンジを照合し、本当にサーバーが発行したリクエストなのか(なりすましやリプレイ攻撃が行われていないか)を検証するために利用します
  2. ブラウザにチャレンジを送信
    • サーバーは生成したchallengeをフロントエンドへ送信します
  3. フロントエンドで navigator.credentials.create() を呼び出す(新規Passkey登録)
    • ブラウザ(JavaScript)はサーバーから取得したchallengeや RP(Relying Party)情報、ユーザー情報などを publicKey オブジェクトとして navigator.credentials.create() に渡します。(後述)
  4. 認証器(ブラウザ内蔵 or 外部デバイス)で鍵ペア生成・署名
    • デバイス内部にある Authenticator(生体認証や PIN 入力などが可能)で公開鍵・秘密鍵のペアを生成し、challengeに対して署名を作成します
  5. クレデンシャル情報がブラウザへ返却される
    • create() メソッドの実行結果として、id・rawId・response(attestationObject などを含む)が含まれた PublicKeyCredential オブジェクトが返ってきます(後述)
  6. ブラウザがサーバーへクレデンシャル情報を送信(登録処理)
    • サーバーは受け取ったデータを検証し、ユーザーと紐付けてデータベースへ保存します
    • 以降、このPasskeyがユーザーに紐付けられた状態となります

ログイン

sequenceDiagram
    participant S as Server
    Actor U as ユーザー
    participant B as Browser
    participant A as Authenticator

    U->>B: ログイン開始
    B->>S: Passkey認証開始

    Note over S: 1. Challenge を生成
    S->>B: Challenge (ログイン用)

    Note over B: 2. フロントエンドで<br> get() を呼び出す
    B->>A: "navigator.credentials.get({ publicKey { ... } })"

    Note over A: 3. 秘密鍵で署名
    A-->>B: assertion (signature等)

    Note over B: 4. 署名付きデータを<br>サーバーへ送信
    B->>S: assertion

    Note over S: 5. 公開鍵で署名確認し認証完
  1. challengeを生成&セッションに保存
    • ログイン時もサーバーは同じ手順で再度チャレンジを生成し、セッションに保存します
  2. フロントエンドで navigator.credentials.get() を呼び出す(認証)
    • allowCredentials(該当ユーザーのクレデンシャル ID など)を含むオブジェクトを publicKey に指定して、navigator.credentials.get() を呼び出します
  3. 認証器が秘密鍵で署名し、Assertion を返却
    • ブラウザが受け取った Assertion(authenticatorData, signature, clientDataJSON など)をサーバーへ送信します
  4. サーバーが公開鍵で署名を検証し、認証完了

より詳細なPasskeyの概要とPasskeyがもたらす認証の安全性と利便性の向上について知りたい方は、以下のSTORES アドベントカレンダー2023 の記事がとてもわかりやすいので、参考にしてみてください。

product.st.inc

  • 新規Passkey登録時のメソッド。publicKey オブジェクトにサーバーから受け取ったチャレンジや RP 情報、ユーザー情報などを含めます
  • ブラウザ/認証器が内部で鍵ペアを生成し、登録用のクレデンシャル情報をサーバーに返すことで、ユーザーのPasskeyが完成します
  • ログイン時のメソッド。サーバーが生成したチャレンジや該当ユーザーのクレデンシャル ID を含む allowCredentials などを publicKey に設定し、認証器が秘密鍵で署名した結果を返します
  • サーバーはセッションに保持しているチャレンジと照合し、正しい秘密鍵による署名であるかどうかを検証してユーザーを認証します

Passkey 作成のオプション

Passkey 作成時にブラウザ側で実行される navigator.credentials.create() に渡すオプションについて整理します。

challenge

概要

  • サーバー側で生成されたランダムなバイト列

    the values of both PublicKeyCredentialCreationOptions.challenge and PublicKeyCredentialRequestOptions.challenge MUST be randomly generated by Relying Parties in an environment they trust

  • なりすまし防止やリプレイ攻撃対策のために必須
  • 登録(Passkey作成)時には必ず使用する

ポイント

  • サーバー側で毎回新しい challenge を生成し、セッションなどに保存する必要があります。
  • フロントエンドでは Base64URL 形式をデコードして Uint8Array 型として渡します。

変更した場合の挙動

  • challengeがサーバー側に保持されていない、または整合性が取れないと、認証器が署名して返すデータをサーバーが検証できず、登録に失敗します

    • the returned challenge value in the client’s response MUST match what was generated.

  • チャレンジを誤って使い回しているとリプレイ攻撃のリスクが生じるため、毎回新しいチャレンジを生成する必要があります

rp (Relying Party)

概要

  • サービスやアプリを提供する主体の情報。FIDO2 でのスコープ(ドメインの範囲)を定義する重要プロパティ
  • rp.name: 表示用のサービス名(例: "Example Inc.")
  • rp.id: 対象となるドメイン(例: "example.com")

"rp": {
  "name": "Example Inc.",
  "id": "example.com"
}

ポイント

  • 実行中のウェブサイトのドメインと rp.id を一致させることが必須
  • サービス名(rp.name)はユーザーが識別しやすいよう、わかりやすい名称を設定します。

💁 Tips

サブドメイン間で同じpasskeyを利用したい場合には、サブドメインがsuffixマッチするようにrp.idに設定できます。

rp.idに example.com と設定すると、以下のサブドメインにマッチします。

  • hoge.example.com
  • foo.example.com
  • www.example.com
  • etc…

変更した場合の挙動

  • rp.id が実際にアクセスしているドメインと一致しない場合、セキュリティ上の理由からブラウザがクレデンシャル生成を拒否する、あるいはエラーとなる
  • rp.name を変更すると、認証UIなどで表示されるサービス名が変わる(認証処理そのものには大きな影響はない)

user

概要

  • Passkeyを作成するユーザーを特定するための情報
  • user.id: サーバー側で一意に割り当てられるユーザーID(バイナリや Base64 など)
  • user.name / user.displayName: ユーザーを識別するための文字列、UI 表示用の名前

ポイント

  • user.id は一意性を保証する必要があります
  • displayName はわかりやすく設定し、複数アカウントを持つ場合でもユーザーが識別できるようにします

変更した場合の挙動

  • user.id を変更すると、サーバー側とのユーザー紐付けが変わってしまい、同じ人物として認証できなくなる
  • user.name / displayName は、ブラウザのPasskey選択画面などで表示され、ユーザーが複数アカウントを持つ場合の識別にも使われる
"user": {
  "id": Uint8Array.from("m11o@example.com"),
  "name": "m11o@example.com",
  "displayName": "m11o"
}

pubKeyCredParams

概要

  • クライアントから認証器への暗号アルゴリズム指定のためのオプション
  • 生成されるクレデンシャルに使用される暗号方式(アルゴリズム)の候補を指定
  • 例として [{ type: "public-key", alg: -7 }] は ECDSA (ES256) を利用し、-257 は RSA (RS256) を指すなど、複数設定可能
  • 上から優先度が高い順に並べる

"pubKeyCredParams": [
  { "type": "public-key", "alg": -7 },   // ES256
  { "type": "public-key", "alg": -257 }  // RS256
]

ポイント

  • セキュリティ要件に応じて高度な暗号方式を指定できますが、対応デバイスが限られる場合があるため注意が必要です。

💁 Tips

ES256(-7)が鍵サイズが比較的小さく、署名時の計算コストも低めであり、FIDO2ではMUST実装とされているので、ほぼ全てのプラットフォームやAuthenticatorで利用可能。最も使われているアルゴリズム。

以下利用可能なアルゴリズム

アルゴリズム 設定値 特徴
ES256 (ECDSA w/ SHA-256) -7 鍵・署名サイズが小さく、認証時の計算コストが低い
FIDO2 では必須 (MUST) 実装
RS256 (RSA PKCS#1 v1.5 w/ SHA-256) -257 鍵・署名サイズが大きく、計算コストは高め
古くから利用されている署名方式
EdDSA (Ed25519 等) -8 鍵・署名サイズが小さく高速、実装が簡単
サポートはまだ限定的
ES384/ES512 (ECDSA w/ SHA-384/512) -35 / -36 ES256 と同じ楕円曲線 ECDSA
高セキュリティだが計算コストが増大

ref

https://www.w3.org/TR/webauthn-2/#sctn-alg-identifier

変更した場合の挙動

  • ブラウザやデバイスが未対応のアルゴリズムが指定されるとエラーになる
  • 複数指定した場合は、認証器側がサポートするアルゴリズムを選択して処理を進める
  • 高度な暗号方式を指定するとセキュリティが高まるが、対応デバイスが限られる可能性がある

authenticatorSelection

概要

  • どのような認証器を使うか、ユーザー検証をどう行うかなど、UX とセキュリティに直結するオプション
  • 主なプロパティ:
    • authenticatorAttachment
      • platform: PC やスマホ内蔵の認証器を利用(Touch ID、Face ID など)
      • cross-platform: USB セキュリティキーなど外部デバイスを利用
    • residentKey
      • Passkey をデバイス上でdiscoverableにするかどうかを指定。
      • default値は preferred
      • Discoverable(resident)にすると、ユーザーがログイン時に 「ユーザー名入力なし」 でアカウントを直接選択できる。
      • required, preferred, discouraged などの指定方法がある。
        • required: 必ずResidentKeyとしてPasskeyを登録する
        • preferred: ResidentKeyが使える認証器の場合は使う
        • discouraged: Non-ResidentKeyとしてPasskeyを登録する
          • アカウント情報(アカウント名、emailなど)
    • userVerification
      • required: 生体認証や PIN 入力を常に要求
      • preferred: デバイスが対応可能なら要求(ユーザーに選択肢が出る場合も)
      • discouraged: 可能であればスキップし、UX を優先

"authenticatorSelection": {
  "authenticatorAttachment": "platform",
  "residentKey": "preferred",
  "userVerification": "required"
}

ポイント

  • UX とセキュリティのバランスを考慮して設定
  • Discoverable Credential(residentKey=required) を利用する場合、認証器の対応状況を確認。

変更した場合の挙動

authenticatorAttachment

platform から cross-platform に変更すると、デバイス内蔵認証から外部デバイスへの切り替えが起こり、ユーザーに USB キー挿入などの物理操作を求められます。 ただし、cross-platformで対応する認証器がない場合は、選択画面が表示されます。

platform cross-platform
residentKey

required や preferred にすると discoverable credential(デバイス上にクレデンシャルを記憶する形)となり、ログイン時にユーザー名不要のフローが可能になります。 一部の認証器では、residentKey を要求されるとエラーを返す場合もあります。(未対応の場合など) discouraged だと、クレデンシャルをサーバーに依存する非 discoverable 形式となり、ログイン時は必ずユーザー名を入力した後に allowCredentials で指定されたクレデンシャルを使用します

userVerification

required にすると、必ず PIN や生体認証などを実施するため、セキュリティ強度は高まりますが、ユーザーに認証操作の追加負荷がかかります。 一方で、discouraged にすると、対応デバイスでは UX は向上しますが、不正利用リスクがやや高まります。

macOSで指紋認証を使えない状態にしてPasskey登録を行うと以下ような差分があるようです。

required preferred discouraged
指紋認証利用可能
(でない想定だったが出た。理由が分からないので誰か教えてください )
指紋認証利用不可 何も表示されずに登録できる 何も表示されずに登録できる

timeout

概要

  • ユーザー操作を待機する時間をミリ秒で指定

ポイント

  • 一般的には 30~60 秒程度を設定
    • 値が小さすぎると、認証器が生体認証の準備や PIN 入力を待つ間にタイムアウトとなる恐れがある

attestation

概要

  • 認証器の証明書チェーン(デバイスの製造元情報など)をどう扱うかを指定。
  • 主な設定値: none, indirect, direct, enterprise
    • none
      • デバイス情報をサーバー側に送らない。プライバシーを重視したい場合に適している。
    • direct
      • 認証器の証明書チェーンが返却されるため、サーバー側で詳細なデバイス検証が可能。ただし、ユーザープライバシーが多少侵害される可能性がある。
    • indirect
      • 中間機関を介してデバイス検証を行う想定のため、実装やデバイスによっては "direct" と似た挙動をとる場合もある。
    • enterprise
      • 企業向けの特殊なユースケース(Managed デバイス等)。対応していない認証器もあり、エラーとなる場合がある。

ポイント

  • プライバシーとセキュリティ要件に応じて設定。

変更した場合の挙動

認証器からの attStmt の値が変化する。

noneの場合
{
    "rawId": "OqXuL2IRcmyeMe2KBlHfby6mS/Mc+++s7cXs48TEbQE=",
    "response": {
        "attestationObject": {
            "authData": {...},
            "fmt": "none",
            "attStmt": {} // <- ココ
        },
    }
}
directの場合
{
    "rawId": "OqXuL2IRcmyeMe2KBlHfby6mS/Mc+++s7cXs48TEbQE=",
    "response": {
        "attestationObject": {
            "authData": {...},
            "fmt": "none",
            "attStmt": { // <- ココ
                "signature": "3045022100de9c3da20d7dc791364b76c51897f3df28d412d870aeb3fbc6bfd99ec0f4f16f022075c920c24507acda5f3620d2680a554e80dd1ce9adbbf4b0ade6b99b55a8eba7",
                "algorithm": -7
            }
        },
    }
}

excludeCredentials

概要

登録時にすでに登録されているクレデンシャル ID を指定し、それらを除外することで重複登録を防ぐオプション。
サーバー側で管理している既存のクレデンシャル ID をフロントエンドに渡し、新規登録を制限します。

excludeCredentials: [
  {
    id: new Uint8Array([ ... ]), // 既存のクレデンシャル ID
    type: "public-key",
    transports: ["usb", "nfc", "ble", "internal"] // 使用可能なトランスポート(任意)
  }
]

ポイント

  • ユーザーが同一デバイスに同じサービスのクレデンシャルを複数登録しようとするケースを防ぐために使用します。
  • サーバー側で該当ユーザーの登録済みクレデンシャル ID を管理し、それを excludeCredentials に含める必要があります。
  • transports を指定すると、特定の接続方法(USB, NFC, Bluetooth, 内蔵など)の認証器に限定することが可能です。
  • 一部の認証器やブラウザは、このオプションを無視する可能性があるため、登録済みの確認はサーバー側でも行うことが推奨されます。

設定した場合の挙動

  • 指定したクレデンシャル ID が認証器内に存在する場合
    • 登録がブロックされ、「すでに登録済み」というエラーが発生する可能性があります。
    • The user attempted to register an authenticator that contains one of the credentials already registered with the relying party.
  • 指定したクレデンシャル ID が認証器内に存在しない場合
    • 通常どおり新規登録が進行します。
  • excludeCredentials を空または未指定
    • 既存のクレデンシャルがある場合でも、新しいクレデンシャルを登録できる可能性があり、重複登録が発生することがあります。

extensions

extensions は、クライアントや認証デバイスに追加の機能を要求するために使用されるフィールドです。

以下に主な拡張機能と使用例を掲載しています。

credProps (Credential Properties)

概要

登録される資格情報(credential)が residentKeyであるかどうかを確認するために使用されます

この拡張機能の値は通常ブール値で、true に設定されることが多いです。

ref https://www.w3.org/TR/webauthn-2/#sctn-authenticator-credential-properties-extension

使用例

{
  "credProps": true
}

response

{
  "authenticatorExtensions": {
    "credProps": {
      "rk": true
    }
  }
}

uvm (User Verification Methods)

概要

認証時に使用されたユーザ認証方法(例: PIN、指紋、顔認証など)を確認します。trueを指定するとレスポンスにどの認証方式を利用したかが返ってきます。

ref https://www.w3.org/TR/webauthn-2/#sctn-uvm-extension

使用例

{
  "uvm": true
}

response

"uvm": [
  [<userVerificationMethod>, <keyProtectionType>, <matcherProtectionType>],
  ...
]

{
  "authenticatorExtensions": {
    "uvm": [
      [2, 1, 1] // 指紋認証
    ]
  }
}

他の認証方法については以下を参照してください。

https://fidoalliance.org/specs/fido-v2.0-id-20180227/fido-registry-v2.0-id-20180227.html#user-verification-methods

また、その他の利用可能なextensionsに関しては以下を参照してください

https://www.w3.org/TR/webauthn-2/#sctn-defined-extensions

まとめ

オプション名 目的 / 内容 ポイント
challenge サーバーが発行するランダムバイト列。なりすまし・リプレイ攻撃を防ぐ必須項目 サーバー側との整合性が取れないと認証失敗。毎回新規生成が必要。
rp Relying Party(サービス)の情報。ドメインスコープを定義 rp.id と実行ドメインが異なる場合、ブラウザが生成を拒否。rp.name は表示名。
user Passkeyを作成するユーザー情報。ID はサーバー側で一意となる必要。 user.id が別の値に変わると同じユーザーとして扱えなくなる。displayName はUI表示用。
pubKeyCredParams 対応暗号アルゴリズムの候補一覧。 未対応のアルゴリズムを指定するとエラー。複数指定で認証器が対応できるアルゴリズムを選択。
authenticatorSelection 認証器タイプ (内蔵 / 外部) やユーザー検証 (PIN / 生体認証) の要否などを指定。 UX とセキュリティに直結。residentKey を "required" などにすると、Discoverable(デバイス常駐)クレデンシャルとなる。
timeout ユーザー操作を待機する時間(ミリ秒)。 短すぎるとタイムアウトが頻発。長すぎるとUXが冗長になる。
attestation 認証器の証明書をどこまで取得・検証するか。 "none" はプライバシー重視で製造元情報を送らない。"direct" は詳細情報を取得しセキュリティは高いが、ユーザープライバシーに影響。
excludeCredentials 既存クレデンシャルの除外 重複登録の防止
extensions 拡張機能設定 ブラウザや認証器固有の機能を制御

以下、実装例

const publicKey = {
  challenge: Uint8Array.from(atob("<server_generated_challenge>"), c => c.charCodeAt(0)),
  rp: {
    name: "Example Inc.",
    id: "example.com"
  },
  user: {
    id: Uint8Array.from("<user_id_string>", c => c.charCodeAt(0)),
    name: "user@example.com",
    displayName: "John Doe"
  },
  pubKeyCredParams: [
    { type: "public-key", alg: -7 }, // ES256
    { type: "public-key", alg: -257 } // RS256
  ],
  authenticatorSelection: {
    authenticatorAttachment: "platform",
    userVerification: "required"
  },
  timeout: 60000,
  attestation: "none"
};

const credential = await navigator.credentials.create({ publicKey });
// response内のcredential.id, credential.rawId, credential.response などをサーバーに送る

Passkey 取得のオプション

ここからはPasskey 取得時にブラウザ側で実行される navigator.credentials.get() に渡すオプションについて整理します。

challenge

概要

  • サーバーが生成するランダムなバイト列。登録 (create()) 時と同様に、なりすましやリプレイ攻撃を防ぐため必須
  • 認証器がこの challenge に対して署名を行うことで、サーバーはリクエストが正規かどうかを確認する

ポイント

  • 毎回新しい challenge をMUSTで生成し、セッションなどで保持することが一般的
  • フロントエンドでは Base64URL デコード後に Uint8Array へ変換して指定する必要がある

変更した場合の挙動

  • サーバー側が保持している challenge と一致しない場合、署名検証が失敗し、認証も失敗する
  • チャレンジを再利用(同じ値を使い回し)してしまうと、リプレイ攻撃を受けるリスクが高まる

allowCredentials

概要

  • 認証時に使用可能なクレデンシャル(ID と type: "public-key" の配列)を指定する。
  • ユーザーが複数のクレデンシャルを所持している場合、どのクレデンシャル ID を使うかを明示的に絞り込める

ポイント

  • 大規模サービスで複数のクレデンシャルをユーザーが保有する可能性がある場合は、サーバー側で特定したクレデンシャル ID のみを渡すことで認証をスムーズにする。
  • 認証器が「指定のクレデンシャルは存在しない」と判断した場合、エラーになることを想定しておく必要がある

変更した場合の挙動

  • 指定されたクレデンシャル ID がデバイス内に存在しない場合
    • 認証が行われない(クレデンシャルが見つからない)。
  • 空配列や未指定の場合
    • 対応するクレデンシャルが複数あればブラウザが自動で選択するか、ユーザーが選択 UI から選ぶことになる
  • Discoverable(resident)クレデンシャル利用時には、allowCredentials を省略しても認証器側がクレデンシャルを探索・選択できる場合がある
未指定の場合 指定された場合 指定されたクレデンシャルが存在しない場合
作成された複数アカウント選択画面が表示される 選択画面は表示されず認証画面が表示される クレデンシャルが見つからない

timeout

概要

  • 認証が開始してからユーザー操作(生体認証や PIN 入力など)が行われるまでの待機時間をミリ秒単位で指定する

ポイント

  • 一般的には 30~60 秒程度が設定されることが多い。
  • 実際のタイムアウト挙動はデバイスやブラウザの実装にも依存するため、過度に短く設定しすぎない方が無難。

変更した場合の挙動

  • 短すぎる場合、ユーザーが認証器を操作する前にタイムアウトし、認証が失敗する。
  • 長すぎる場合は、ユーザーが操作を放置すると待ち時間が長くなり、UX に影響する。

userVerification

概要

  • PIN や生体認証など、ユーザー本人確認をどのレベルで行うかを指定。
  • "required", "preferred", "discouraged" のいずれかから選択する。
  • Passkey作成時のオプションと同じ

ポイント

  • サービスのセキュリティ要件や利用シーン(金融サービスかSNSか等)によって設定値を決める。
  • "preferred" はスマートな選択肢だが、運用上「必ず生体認証してほしい」場合には "required" が望ましい。

変更した場合の挙動

  • "required": 生体認証や PIN 入力が必須となり、セキュリティは高い反面、UX が多少煩雑。
  • "preferred": デバイスが対応可能であれば本人確認を行い、非対応の場合はスキップされるなど柔軟に動作。
  • "discouraged": 可能な限り本人確認を行わず、即時に認証を進める。セキュリティは下がるが操作はシンプル。

mediation

概要

  • ユーザーに対する認証 UI の表示タイミングや方法を制御する。
  • optional , silent , required , conditional などの値があるが、対応ブラウザは限定的。

ポイント

  • 現時点ではブラウザ・デバイスによって実装状況がまちまちのため、必須要件としては組み込みにくい。
  • 特定のプラットフォーム向けに UX を最適化したい場合のみ活用を検討する。

変更した場合の挙動

  • silent を指定しても、実際には多くのブラウザ・認証器で対話 UI が表示されるなど、必ずしも期待通りに動作しないことがある
  • required は確実にユーザーが UI を目にするフローとなるため、ユーザーが利用するクレデンシャルを明示的に選ばせたい場合に有用
  • conditionalを指定すると、 autocomplete=webauthn が設定されたフィールドにfocusが当たったタイミングでUIが表示される(Conditional UI)

ref https://w3c.github.io/webappsec-credential-management/#mediation-requirements

Conditional UIについては、以下の記事は詳しいので、気になる方は参考にしてみてください。

moneyforward-dev.jp

www.corbado.com

github.com

まとめ

オプション名** 目的/概要 **ポイント
challenge サーバーが生成するランダムなバイト列。認証器(デバイス)がこれに対して署名することで、なりすましやリプレイ攻撃を防ぐ。 - 毎回異なる challenge を生成する必要がある
- サーバー側で保持した値と一致しない場合、認証は失敗
- フロントエンドでは Base64URL デコード後に Uint8Array へ変換して渡すのが一般的
allowCredentials 認証時に利用可能なクレデンシャル(credentialId と type: "public-key" の配列)を指定する。ユーザーが複数のPasskeyを所有している場合に「どのクレデンシャルを使うか」を絞り込める。 - 指定がない場合、認証器がデバイス内のすべてのクレデンシャルを探索する。
- Discoverable Credential を使っている場合は、ここを空でも認証可能なケースがある。
- 未登録の credentialId を渡すとエラーになることがある。
timeout 認証操作を待機する時間(ミリ秒単位)を指定。
ユーザーが生体認証や PIN 入力を行うまでのタイムリミットを決める。
- 短すぎると操作前にタイムアウトが発生しやすい。
- 長すぎるとユーザーが離脱しても待ち時間が継続する可能性があり、UX に影響。
- ブラウザやデバイス実装にも依存するため、推奨値は 30~60 秒程度が多い。
userVerification ユーザー本人確認(生体認証や PIN 入力など)をどの程度求めるかを設定。required, preferred, discouraged のいずれかを指定。 - "required": 常に本人確認を実施。セキュリティは高いが、操作が増える。
- "preferred": 対応可能なら本人確認を行う。
- "discouraged": 極力スキップし、即時に認証できるがセキュリティは下がる。
mediation ユーザーがいつ・どのように認証 UI に誘導されるかを制御する。optional, silent, required , conditional の値が存在。 - 実装状況がブラウザによってまちまち。
- silent を設定しても実際に UI が表示されるケースが多い。
- required は必ず UI 表示となり、ユーザーがクレデンシャルを選択する明示的なフローに。
- conditionalはautocomple=webauthnが設定されたフィールドにfocusが当たったタイミングでUIが表示される

以下、実装例

const publicKey = {
  challenge: Uint8Array.from(atob("<server_generated_challenge>"), c => c.charCodeAt(0)),
  allowCredentials: [{
    id: Uint8Array.from("<user_credential_id>", c => c.charCodeAt(0)),
    type: "public-key"
  }],
  userVerification: "required",
  timeout: 60000
};

const assertion = await navigator.credentials.get({ publicKey });
// response内のassertion.response.authenticatorData, assertion.response.signature などをサーバーに送る

終わりに

今回は、自分がPasskeyを実装するにあたって色々調べて学んだoption周りの設定値をまとめてみました。 大変長くなってしまった&ドキュメントっぽくなってしまったので退屈なものになってしまいました。

ただ、実装する際に不明なオプションがあった場合や、やりたい事が実現できない場合にこの記事を思い出して、参考にしていただけると嬉しいです。

また、実際の実装方法はSTORES アドベントカレンダー2023でも明言されているので、参考にしてみてください。

product.st.inc

参考資料

www.w3.org

w3c.github.io

developer.mozilla.org

developer.mozilla.org