Upgrade to Pro — share decks privately, control downloads, hide ads and more …

サーバーサイド開発者のためのパスキー入門

Yoshikazu Nojima
February 13, 2025
720

 サーバーサイド開発者のためのパスキー入門

Yoshikazu Nojima

February 13, 2025
Tweet

Transcript

  1. Copyright © Yoshikazu Nojima 2024 自己紹介 • 能島良和 • Red

    Hatでミドルウェア製品のテクニカルサポートを担当 • WebAuthn4Jというライブラリの開発者 https://github.com/webauthn4j/webauthn4j Keycloak、Spring、Quarkus等で採用 • Twitter:@shiroica • GitHub:ynojima 1
  2. Copyright © Yoshikazu Nojima 2024 目次 • パスキーの処理の概要 • サーバーサイドで必要な処理

    • ライブラリ・フレームワークが提供する処理 • アプリケーション側で必要な実装 • Javaでの実装例 • Spring • Quarkus 2
  3. Copyright © Yoshikazu Nojima 2024 クレデンシャル の保存 パスキーの登録フロー(概要) パスキープロバイダ ブラウザ

    サーバー • Credential Id • Authenticator data • 署名 を送信 クレデンシャル(Credential Id, Authenticator data, Client Data, 署名、etc.)を送信 Client dataのハッシュを送信 Authenticator data(RpIdのハッシュ, フラグ,カウンタ,RP固有公開鍵,拡張)を生成 challengeを提供 Storage RP固有秘密鍵 を保存 Client data (challenge, origin, etc.)を生成 Client dataのハッシュと Authenticator dataに対する署名を生成 challenge を生成・保存 challenge の読込 RP固有鍵ペアを生成 クレデンシャルに対し、 ドメイン(RP Id)、署名、 challenge、その他の検証 DB Session
  4. Copyright © Yoshikazu Nojima 2024 クレデンシャル の読込 パスキーの認証フロー(概要) パスキープロバイダ ブラウザ

    サーバー • Credential Id • Authenticator data • 署名 を送信 アサーション(Credential Id, Authenticator data, Client Data, 署名、etc.)を送信 Client dataのハッシュを送信 Authenticator data(RpIdのハッシュ, フラグ,カウンタ,拡張)を生成 Storage RP固有秘密鍵 を読込 Client data (challenge, origin, etc.)を生成 Client dataのハッシュと Authenticator dataに対する署名を生成 challenge の読込 クレデンシャルに対し、 ドメイン(RP Id)、署名、 challenge、その他の検証 DB Session challengeを提供 challenge を生成・保存
  5. Copyright © Yoshikazu Nojima 2024 クレデンシャル (公開鍵)の読込 プロトコルレベルで組み込まれたセキュリティ(1): クレデンシャル流出対策としての公開鍵認証 パスキープロバイダ

    ブラウザ サーバー • Credential Id • Authenticator data • 署名 を送信 アサーション(Credential Id, Authenticator data, Client Data, 署名、etc.)を送信 Client dataのハッシュを送信 Authenticator data(RpIdのハッシュ, フラグ,カウンタ,拡張)を生成 Storage RP固有秘密鍵 を読込 Client data (challenge, origin, etc.)を生成 Client dataのハッシュと Authenticator dataに対する署名を生成 challenge の読込 クレデンシャルに対し、 ドメイン(RP Id)、署名、 challenge、その他の検証 DB Session challengeを提供 challenge を生成・保存 サーバーに保存されているのは公開鍵、 通信経路でやり取りされるのは署名なので流出しても影響は軽減
  6. Copyright © Yoshikazu Nojima 2024 クレデンシャル の読込 プロトコルレベルで組み込まれたセキュリティ(2): リプレイ攻撃対策としてのチャレンジデータ パスキープロバイダ

    ブラウザ サーバー • Credential Id • Authenticator data • 署名 を送信 アサーション(Credential Id, Authenticator data, Client Data, 署名、etc.)を送信 Client dataのハッシュを送信 Authenticator data(RpIdのハッシュ, フラグ,カウンタ,拡張)を生成 Storage RP固有秘密鍵 を読込 Client data (challenge, origin, etc.)を生成 Client dataのハッシュと Authenticator dataに対する署名を生成 challenge の読込 クレデンシャルに対し、 ドメイン(RP Id)、署名、 challenge、その他の検証 DB Session challengeを提供 challenge を生成・保存 署名対象にチャレンジ(ランダムデータ)を含めることで、 詐取した正規のリクエストを繰り返すリプレイ攻撃を防止
  7. Copyright © Yoshikazu Nojima 2024 クレデンシャル の保存 WebAuthn APIが提供する範囲(登録処理) パスキープロバイダ

    ブラウザ サーバー • Credential Id • Authenticator data • 署名 を送信 クレデンシャル(Credential Id, Authenticator data, Client Data, 署名、etc.)を送信 Client dataのハッシュを送信 Authenticator data(RpIdのハッシュ, フラグ,カウンタ,RP固有公開鍵,拡張)を生成 challengeを提供 Storage RP固有秘密鍵 を保存 Client data (challenge, origin, etc.)を生成 Client dataのハッシュと Authenticator dataに対する署名を生成 challenge の読込 RP固有鍵ペアを生成 クレデンシャルに対し、 ドメイン(RP Id)、署名、 challenge、その他の検証 DB Session navigator.credentials.create() challenge を生成・保存
  8. Copyright © Yoshikazu Nojima 2024 クレデンシャル の読込 WebAuthn APIが提供する範囲(認証処理) パスキープロバイダ

    ブラウザ サーバー • Credential Id • Authenticator data • 署名 を送信 アサーション(Credential Id, Authenticator data, Client Data, 署名、etc.)を送信 Client dataのハッシュを送信 Authenticator data(RpIdのハッシュ, フラグ,カウンタ,拡張)を生成 Storage RP固有秘密鍵 を読込 Client data (challenge, origin, etc.)を生成 Client dataのハッシュと Authenticator dataに対する署名を生成 challenge の読込 クレデンシャルに対し、 ドメイン(RP Id)、署名、 challenge、その他の検証 DB Session challengeを提供 navigator.credentials.get() challenge を生成・保存
  9. Copyright © Yoshikazu Nojima 2024 クレデンシャル の保存 サーバーサイドで必要な処理(登録フロー) パスキープロバイダ ブラウザ

    サーバー • Credential Id • Authenticator data • 署名 を送信 クレデンシャル(Credential Id, Authenticator data, Client Data, 署名、etc.)を送信 Client dataのハッシュを送信 Authenticator data(RpIdのハッシュ, フラグ,カウンタ,RP固有公開鍵,拡張)を生成 challengeを提供 Storage RP固有秘密鍵 を保存 Client data (challenge, origin, etc.)を生成 Client dataのハッシュと Authenticator dataに対する署名を生成 challenge の読込 RP固有鍵ペアを生成 クレデンシャルに対し、 ドメイン(RP Id)、署名、 challenge、その他の検証 DB Session チャレンジ提供用エンドポイント クレデンシャル登録用エンドポイント challenge を生成・保存
  10. Copyright © Yoshikazu Nojima 2024 クレデンシャル の読込 サーバーサイドで必要な処理(認証フロー) パスキープロバイダ ブラウザ

    サーバー • Credential Id • Authenticator data • 署名 を送信 アサーション(Credential Id, Authenticator data, Client Data, 署名、etc.)を送信 Client dataのハッシュを送信 Authenticator data(RpIdのハッシュ, フラグ,カウンタ,拡張)を生成 Storage RP固有秘密鍵 を読込 Client data (challenge, origin, etc.)を生成 Client dataのハッシュと Authenticator dataに対する署名を生成 challenge の読込 クレデンシャルに対し、 ドメイン(RP Id)、署名、 challenge、その他の検証 DB Session challengeを提供 チャレンジ提供用エンドポイント アサーション認証用エンドポイント challenge を生成・保存
  11. Copyright © Yoshikazu Nojima 2024 クレデンシャル の読込 エンドポイント含めて提供するフレームワークの場合 パスキープロバイダ ブラウザ

    サーバー • Credential Id • Authenticator data • 署名 を送信 アサーション(Credential Id, Authenticator data, Client Data, 署名、etc.)を送信 Client dataのハッシュを送信 Authenticator data(RpIdのハッシュ, フラグ,カウンタ,拡張)を生成 Storage RP固有秘密鍵 を読込 Client data (challenge, origin, etc.)を生成 Client dataのハッシュと Authenticator dataに対する署名を生成 challenge の読込 クレデンシャルに対し、 ドメイン(RP Id)、署名、 challenge、その他の検証 DB Session challengeを提供 challenge を生成・保存 エンドポイント含め提供 エンドポイント含め提供
  12. Copyright © Yoshikazu Nojima 2024 クレデンシャル の読込 クレデンシャル検証のみを提供するライブラリの場合 パスキープロバイダ ブラウザ

    サーバー • Credential Id • Authenticator data • 署名 を送信 アサーション(Credential Id, Authenticator data, Client Data, 署名、etc.)を送信 Client dataのハッシュを送信 Authenticator data(RpIdのハッシュ, フラグ,カウンタ,拡張)を生成 Storage RP固有秘密鍵 を読込 Client data (challenge, origin, etc.)を生成 Client dataのハッシュと Authenticator dataに対する署名を生成 challenge の読込 クレデンシャルに対し、 ドメイン(RP Id)、署名、 challenge、その他の検証 DB Session challengeを提供 challenge を生成・保存 検証処理のみを提供
  13. Copyright © Yoshikazu Nojima 2024 クレデンシャル の読込 検証処理のみを提供するライブラリの場合の アプリケーション側で必要な実装 パスキープロバイダ

    ブラウザ サーバー • Credential Id • Authenticator data • 署名 を送信 アサーション(Credential Id, Authenticator data, Client Data, 署名、etc.)を送信 Client dataのハッシュを送信 Authenticator data(RpIdのハッシュ, フラグ,カウンタ,拡張)を生成 Storage RP固有秘密鍵 を読込 Client data (challenge, origin, etc.)を生成 Client dataのハッシュと Authenticator dataに対する署名を生成 challenge の読込 クレデンシャルに対し、 ドメイン(RP Id)、署名、 challenge、その他の検証 DB Session challengeを提供 challenge を生成・保存 アプリとして実装が必要 アプリとして実装が必要 検証処理のみを提供
  14. Copyright © Yoshikazu Nojima 2024 クレデンシャル の読込 フレームワークに特化したラッパーライブラリが 提供さている場合に必要な実装 パスキープロバイダ

    ブラウザ サーバー • Credential Id • Authenticator data • 署名 を送信 アサーション(Credential Id, Authenticator data, Client Data, 署名、etc.)を送信 Client dataのハッシュを送信 Authenticator data(RpIdのハッシュ, フラグ,カウンタ,拡張)を生成 Storage RP固有秘密鍵 を読込 Client data (challenge, origin, etc.)を生成 Client dataのハッシュと Authenticator dataに対する署名を生成 challenge の読込 クレデンシャルに対し、 ドメイン(RP Id)、署名、 challenge、その他の検証 DB Session challengeを提供 challenge を生成・保存 ラッパーライブラリが提供 ラッパーライブラリが提供 検証処理のみを提供 アプリとして実装が必要
  15. Copyright © Yoshikazu Nojima 2024 クレデンシャル の読込 例:Spring/Quarkus パスキープロバイダ ブラウザ

    サーバー • Credential Id • Authenticator data • 署名 を送信 アサーション(Credential Id, Authenticator data, Client Data, 署名、etc.)を送信 Client dataのハッシュを送信 Authenticator data(RpIdのハッシュ, フラグ,カウンタ,拡張)を生成 Storage RP固有秘密鍵 を読込 Client data (challenge, origin, etc.)を生成 Client dataのハッシュと Authenticator dataに対する署名を生成 challenge の読込 クレデンシャルに対し、 ドメイン(RP Id)、署名、 challenge、その他の検証 DB Session challengeを提供 challenge を生成・保存 Spring Security/Quarkus Securityが提供 Spring Security/Quarkus Securityが提供 WebAuthn4Jが提供 アプリとして実装が必要
  16. Copyright © Yoshikazu Nojima 2024 クレデンシャル の読込 おさらい: Quarkus Security

    WebAuthnで必要な実装箇所 パスキープロバイダ ブラウザ サーバー • Credential Id • Authenticator data • 署名 を送信 アサーション(Credential Id, Authenticator data, Client Data, 署名、etc.)を送信 Client dataのハッシュを送信 Authenticator data(RpIdのハッシュ, フラグ,カウンタ,拡張)を生成 Storage RP固有秘密鍵 を読込 Client data (challenge, origin, etc.)を生成 Client dataのハッシュと Authenticator dataに対する署名を生成 challenge の読込 クレデンシャルに対し、 ドメイン(RP Id)、署名、 challenge、その他の検証 DB Session challengeを提供 challenge を生成・保存 Quarkus Securityが提供 Quarkus Securityが提供 WebAuthn4Jが提供 アプリとして実装が必要
  17. Copyright © Yoshikazu Nojima 2024 クレデンシャルの永続化処理のために WebAuthnUserProviderインタフェースを実装 26 @Blocking @ApplicationScoped

    public class DemoWebAuthnUserProvider implements WebAuthnUserProvider { private final List<WebAuthnCredentialRecord> credentialRecords = new ArrayList<>(); @Override public Uni<List<WebAuthnCredentialRecord>> findByUsername(String username) { return Uni.createFrom().item(credentialRecords.stream() .filter(record -> record.getUsername().equals(username)).collect(Collectors.toList())); } @Override public Uni<WebAuthnCredentialRecord> findByCredentialId(String credentialId) { return Uni.createFrom().item(credentialRecords.stream() .filter(record -> record.getCredentialID().equals(credentialId)) .findFirst().orElseThrow(()-> new RuntimeException("credential not found"))); } @Override public Uni<Void> update(String credentialId, long counter) { findByCredentialId(credentialId).subscribe().with(credentialRecord -> credentialRecord.setCounter(counter)); return Uni.createFrom().voidItem(); } @Override public Uni<Void> store(WebAuthnCredentialRecord credentialRecord) { credentialRecords.add(credentialRecord); return Uni.createFrom().voidItem(); } }
  18. Copyright © Yoshikazu Nojima 2024 エンドポイントの動作 $ curl http://localhost:8080/q/webauthn/register-options-challenge?username=ynojima {

    "rp": { "id": "localhost", "name": "Quarkus server" }, "user": { "id": "Baeobf2jRmqbFa1nLz_rDg", "name": "ynojima", "displayName": "ynojima" }, "challenge": "nLZXvZrAz2rTU0erERIGrRsqY3zl8bW57-IUmzPzEX2FSR50yrGKWy-QPsMtDy6y6y9TWXOlf- _AcQZNSg6wWQ", "pubKeyCredParams": [ { "type": "public-key", "alg": -7 }, { "type": "public-key", "alg": -257} ], "timeout": 300000, "excludeCredentials": [], "authenticatorSelection": { "requireResidentKey": true, "residentKey": "required", "userVerification": "required" }, "attestation": "none", "extensions": {} } 27
  19. Copyright © Yoshikazu Nojima 2024 クレデンシャル の保存 登録処理の全体像 パスキープロバイダ ブラウザ

    サーバー • Credential Id • Authenticator data • 署名 を送信 クレデンシャル(Credential Id, Authenticator data, Client Data, 署名、etc.)を送信 Client dataのハッシュを送信 Authenticator data(RpIdのハッシュ, フラグ,カウンタ,RP固有公開鍵,拡張)を生成 challengeを提供 Storage RP固有秘密鍵 を保存 Client data (challenge, origin, etc.)を生成 Client dataのハッシュと Authenticator dataに対する署名を生成 challenge の読込 RP固有鍵ペアを生成 クレデンシャルに対し、 ドメイン(RP Id)、署名、 challenge、その他の検証 DB Session チャレンジ提供用エンドポイント クレデンシャル登録用エンドポイント challenge を生成・保存 navigator.credentials.create() フロントエンド 呼出 呼出 呼出
  20. Copyright © Yoshikazu Nojima 2024 フロントエンドの実装例 29 async function createCredential(username){

    // オプション(含チャレンジ)提供用エンドポイントの呼出 const registerOptionsChallengeEndpoint = username ? `/q/webauthn/register-options-challenge?username=${encodeURIComponent(username)}` : “/q/webauthn/register-options-challenge” const response = await fetch(registerOptionsChallengeEndpoint) const publicKeyCredentialCreationOptionsJSON = await response.json() const credentialCreationOptions = PublicKeyCredential.parseCreationOptionsFromJSON(publicKeyCredentialCreationOptionsJSON); // パスキークレデンシャルの作成 const publicKeyCredential = await navigator.credentials.create({ publicKey: credentialCreationOptions}); const registrationResponseJSON = publicKeyCredential.toJSON(); console.log(registrationResponseJSON); // クレデンシャル登録用エンドポイントの呼出 const registerEndpoint = username ? `/q/webauthn/register?username=${encodeURIComponent(username)}` : “/q/webauthn/register” await fetch(registerEndpoint, { method : 'POST', body: JSON.stringify(registrationResponseJSON) }); } ※PublicKeyCredential.parseCreationOptionsFromJSON, PublicKeyCredential#toJSON メソッドはSafariで2025/01現在未実装なので注意。github/webauthn-jsonのようなPolyfillが必須
  21. Copyright © Yoshikazu Nojima 2024 クレデンシャル の読込 認証処理の全体像 パスキープロバイダ ブラウザ

    サーバー • Credential Id • Authenticator data • 署名 を送信 アサーション(Credential Id, Authenticator data, Client Data, 署名、etc.)を送信 Client dataのハッシュを送信 Authenticator data(RpIdのハッシュ, フラグ,カウンタ,拡張)を生成 Storage RP固有秘密鍵 を読込 Client data (challenge, origin, etc.)を生成 Client dataのハッシュと Authenticator dataに対する署名を生成 challenge の読込 クレデンシャルに対し、 ドメイン(RP Id)、署名、 challenge、その他の検証 DB Session challengeを提供 チャレンジ提供用エンドポイント アサーション認証用エンドポイント challenge を生成・保存 navigator.credentials.get() フロントエンド 呼出 呼出 呼出
  22. Copyright © Yoshikazu Nojima 2024 ※登録エンドポイントの独自実装が必要なケース • QuarkusやSpringといった、フレームワークが提供する 標準のパスキー登録エンドポイントは、パスキーの登録のみが可能 •

    1回のフォームPOSTで、ユーザーの氏名や住所等、ユーザーアカウントの 他の属性も登録したい場合、標準の登録エンドポイントでは対応不可能 • 独自のエンドポイントを実装し、受け取ったパスキーのクレデンシャルの 検証処理を呼び出す必要有 32
  23. Copyright © Yoshikazu Nojima 2024 リファレンス • Quarkus公式のquickstartプロジェクト • https://github.com/quarkusio/quarkus-quickstarts/tree/main/security-webauthn-quickstart

    • Quarkus公式のSecurity WebAuthnのガイド • https://quarkus.io/guides/security-webauthn • Spring公式のPasskeysのガイド • https://docs.spring.io/spring-security/reference/servlet/authentication/passkeys.html 33
  24. Copyright © Yoshikazu Nojima 2024 まとめ • パスキー認証の登録・認証処理のシーケンスの全体像を説明しました • パスキー認証の実装には、サーバーサイドはチャレンジの提供や、

    クレデンシャルの登録、認証等のエンドポイントの提供が必要 • ライブラリによって提供する機能は、 検証処理だけ、エンドポイントも提供、DBへの永続化処理も提供、と様々 • ライブラリを選定する際は、利用しているフレームワークにおいて どこまでの機能を提供してくれるのか、自らのユースケースにあうのか、 がポイントの一つです 35
  25. Copyright © Yoshikazu Nojima 2024 E2Eテストで扱う典型的な認証フロー(パスワード認証) 1. 登録画面に遷移する 2. 登録画面でユーザー名と

    パスワードを入力する 3. 登録ボタンを押す 4. 認証画面に遷移する 5. 認証画面でユーザー名と パスワードを入力する 6. 認証画面で認証ボタンを押す 38 自動化は容易
  26. Copyright © Yoshikazu Nojima 2024 E2Eテストで扱う典型的な認証フロー(パスキー認証) 1. 登録画面に遷移する 2. 登録画面でユーザー名を入力する

    3. パスキーの登録ボタンを押す 4. パスキープロバイダー側で承認操作を行う 5. 登録ボタンを押す 6. 認証画面に遷移する 7. 認証画面で認証ボタンを押す 8. パスキープロバイダーで承認操作を行う 39
  27. Copyright © Yoshikazu Nojima 2024 E2Eテストで扱う典型的な認証フロー(パスキー認証) 1. 登録画面に遷移する 2. 登録画面でユーザー名を入力する

    3. パスキーの登録ボタンを押す 4. パスキープロバイダー側で承認操作を行う 5. 登録ボタンを押す 6. 認証画面に遷移する 7. 認証画面で認証ボタンを押す 8. パスキープロバイダーで認証操作を行う 40 手動操作が必要
  28. Copyright © Yoshikazu Nojima 2024 selenium-javaでのテストコード例 44 public class RegistrationAndAuthenticationE2ETest

    extends E2ETestBase{ @Test public void test() { VirtualAuthenticatorOptions options = new VirtualAuthenticatorOptions(); ((HasVirtualAuthenticator) driver).addVirtualAuthenticator(options); // Registration SignupComponent signupComponent = new SignupComponent(driver); signupComponent.navigate(); signupComponent.setFirstname(“John”); signupComponent.setLastname(“Doe”); signupComponent.setUsername(“[email protected]”); signupComponent.setPassword(“password”); signupComponent.clickAddAuthenticator(); signupComponent.getResidentKeyRequirementDialog().clickNo(); signupComponent.waitRegisterClickable(); signupComponent.clickRegister(); // Password authentication wait.until(ExpectedConditions.urlToBe(“http://localhost:8080/angular/login”)); PasswordLoginComponent passwordLoginComponent = new PasswordLoginComponent(driver); passwordLoginComponent.setUsername(“[email protected]”); passwordLoginComponent.setPassword(“password”); passwordLoginComponent.clickLogin(); // 2nd-factor authentication AuthenticatorLoginComponent authenticatorLoginComponent = new AuthenticatorLoginComponent(driver); // nop wait.until(ExpectedConditions.urlToBe(“http://localhost:8080/angular/profile”)); } テストコード自体は ページオブジェクト パターンに則った Seleniumの普通の テストコード 仮想Authenticator追加
  29. Copyright © Yoshikazu Nojima 2024 Chromiumの仮想Authenticatorで十分か? • 仮想Authenticatorの設定項目は多くない (residentKey, uv,

    transport等) • 残念ながらパスキーは環境依存の問題が発生しがち • ChromeでのE2Eテストは、よく使われる環境をカバー出来、 テスト効率を向上させるが、依然として手動テストも重要 45
  30. Copyright © Yoshikazu Nojima 2024 まとめ • パスキーはパスキープロバイダーでユーザーの承認操作が必要 • E2Eテストを自動化する上で承認操作が障害

    • ChromeはWebAuthn WebDriver Extension APIを通じて 仮想Authenticatorに実装を切替、承認ジェスチャーをスキップ可能 • Chrome以外をカバーするために手動テストも依然として必要 • パスキーでも諦めずにE2Eテストを書いていきましょう! 46