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

iOSDC2026登壇資料.pdf

Sponsored · Ship Features Fearlessly Turn features on and off without deploys. Used by thousands of Ruby developers.
Avatar for RioFujimon RioFujimon
September 12, 2026

 iOSDC2026登壇資料.pdf

iOSDC2026 day1(2026/09/12)で発表したスライド

https://fortee.jp/iosdc-japan-2026/proposal/47e004d3-6da4-4936-9221-b0c37ec442ef

Avatar for RioFujimon

RioFujimon

September 12, 2026

More Decks by RioFujimon

Other Decks in Programming

Transcript

  1. ⾃⼰紹介 藤⾨ 莉⽣ Rio Fujimon iOSアプリエンジニア 名刺アプリ Eight のiOS版を開発 X

    @RioFujimon GitHub @rfjmn 発表資料 Foundation Model × VisionKit で 実現するローカル OCR Foundation Models を活⽤するための Tips 2
  2. Passkeyの概要 公開鍵暗号⽅式を使う、パスワード不要の認証⽅法 利⽤者の端末側 秘密鍵 サービスのServer 対になる鍵ペア 公開鍵(登録済み) 認証器が管理 アカウントと紐づけて保存 秘密鍵で署名を⽣成

    公開鍵で署名を検証 署名:対象のデータに対して、秘密鍵を使って⽣成する値。 対応する公開鍵を使い、そのデータに対する署名が正しいか検証できる。 秘密鍵と⽣体情報は、サービスのServerへ送らない 3
  3. iOSアプリにおけるPasskeyの役割分担 iOSアプリ Credential Manager サービスのServer OSへ処理を要求 認証情報を管理する機能 サービス側で検証 • 登録:Passkeyを作成

    • 認証:登録済みの Passkeyで署名を⽣成 例:Appleの「パスワード」 • Passkeyの保存‧選択 • Passkeyの端末間同期 (同期対応の場合) • Challengeを発⾏ Serverが発⾏する⼀度限りの ランダム値 • アプリから届いた登録‧認 証データを検証 Serverへデータを送信 Authenticator(認証器) 検証後の処理 登録:公開鍵など 認証:署名など 鍵ペアを⽣成 秘密鍵で署名を⽣成 登録:公開鍵を保存 認証:ログインを許可 4
  4. Passkey登録の仕組み 公開鍵とCredential ID(認証情報のID)を、アカウントに紐づけて保存する iOSアプリ Authenticator(認証器) サービスのServer ① 登録に使う情報を渡す Challenge(Serverが発⾏する⼀度限りのランダム値) RP

    ID(サービスを識別するドメイン名)‧User ID(アカウントID)など ② Passkeyの作成を要求 鍵ペアを⽣成 登録データ 公開鍵‧Credential IDなど 秘密鍵は認証器が管理 公開鍵を登録データに含める ③ 登録データをServerへ送信 公開鍵‧Credential ID‧Challenge‧Origin(要求元)など 秘密鍵は、サービスのServerへ 送らない ④ 登録データを検証 Challenge‧Origin‧RP IDなど 公開鍵‧Credential IDを アカウントに紐づけて保存 5
  5. Passkey認証の仕組み 登録済みの公開鍵で署名を検証し、ログインを許可する iOSアプリ Authenticator(認証器) サービスのServer ① 認証に使う情報を渡す Challenge(Serverが発⾏する⼀度限りのランダム値) ② 利⽤者の確認‧署名を要求

    署名付き認証データ(Assertion) Credential ID(認証情報のID) 署名など 登録済みの秘密鍵を使⽤ Face ID‧PINで本⼈確認 署名を⽣成 ③ 署名付き認証データをServerへ送信 Credential ID‧署名‧Challenge‧Origin(要求元)など 新しい鍵は作らず、 登録済みの鍵を利⽤する ⑤ アクセストークンを返す (ログイン状態を⽰す値) ④ 署名付き認証データを検証 登録済みの公開鍵を使⽤ 認証セッション(ログイン状態)を発⾏ 6
  6. 利⽤したフレームワーク‧ライブラリ iOSアプリ Server SwiftUI‧UIKit‧Observation Vapor 画⾯の描画‧構成‧表⽰状態の監視 HTTPリクエスト処理‧HTTPレスポンス⽣成 AuthenticationServices swift-webauthn Passkeyの登録‧認証、

    システムシートでの新規登録 登録‧認証オプションの⽣成とデータ検証 Foundation(URLSession) FluentKit‧FluentSQL ServerとのHTTP通信 DBの読み書き‧トランザクション Security(Keychain) FluentPostgresDriver アクセストークンを端末へ保存 FluentとPostgreSQLを接続 保存先:PostgreSQL(データベース) 10
  7. iOSアプリ側のアーキテクチャ 処理の呼び出し 処理結果∕表⽰状態の反映 操作 View SwiftUI画⾯ Observationで状態を監視 Presenter 処理 @Observable

    ⼊⼒と表⽰状態を管理 反映 Interactor actor APIとPasskey処理を調整 結果 APIClient Passkey操作 認証情報Store URLSession ServerのAPIを実⾏ AuthenticationServices 登録‧認証のUIを要求 Keychain アクセストークンを保存 値‧完了‧エラー Router:UIHostingControllerと依存先を組み⽴てる Entity:⼊⼒‧結果‧表⽰状態の型を定義する 11
  8. Server側のアーキテクチャ 型への依存 protocolへの適合(実装 → 抽象) PasskeyServer PasskeyApplication Controller UseCase⽤protocol PasskeyPersistence

    FluentでDBを読み書き Repositoryの実装型 UseCaseの実装型 依存の組み⽴て 具体型を⽣成し、 必要な依存先を注⼊ Repository⽤protocol Verifier⽤protocol PasskeyWebAuthn WebAuthnの変換‧検証 Verifierの実装型 PasskeyDomain:各Targetが利⽤する共通の型 Account‧Credential‧Challengeなど 12
  9. アカウントと認証情報のDB構成 accounts にメール‧認証情報‧セッションを紐づける PK=主キー FK=外部キー 親ID=accounts.id accounts アカウント本体(親テーブル) id(PK):Account ID

    display_name:表⽰名 1:1 1:0..1 1:0..N 1:0..N account_ emails password_ credentials passkey_ credentials authentication_ sessions メールアドレス パスワード認証 Passkey認証 ログイン状態 id:メールID account_id:親ID normalized_email:照合⽤ account_id:親ID password_hash:認証⽤ credential_id:Passkey ID account_id:親ID public_key:公開鍵 account_id:親ID token_digest:トークン expires_at:期限 Passkey移⾏ パスワード認証情報を残したまま、同じAccountへPasskey認証情報を追加 13
  10. 登録‧認証途中に使う⼀時テーブル Passkeyの登録‧認証開始時にChallengeを有効期限付きで保存し、 完了時に照合‧使⽤済みにして、期限切れの要求や同じ要求の再利⽤を拒否する passkey_registration_ challenges passkey_authentication_ challenges passkey_account_ registrations 既存Accountへ

    Passkeyを追加 Passkeyでログイン Passkeyで 新規Accountを作成 主なカラム id:登録処理ID account_id:対象Account challenge:要求照合値 expires_at:有効期限 consumed_at:使⽤済み⽇時 DB外部キーあり account_id:accounts.id(FK) 主なカラム id:認証処理ID challenge:要求照合値 expires_at:有効期限 consumed_at:使⽤済み⽇時 認証完了APIの⼊⼒(FKなし) Challenge ID:本テーブルの id Credential ID:passkey_credentials 主なカラム id:登録処理ID prospective_account_id:予定ID email / display_name:作成情報 challenge:照合値 completed_at:完了⽇時 DB外部キーなし prospective_account_id を accounts.id として新規作成 14
  11. 既存ユーザーのPasskey移⾏と新規登録 ① 既存ユーザーのPasskey移⾏ ② Passkeyの新規登録 従来の⽅式:⼊⼒フォーム ⼊⼒フォーム パスワード認証後、 同じアカウントにPasskeyを追加 メールアドレス‧

    表⽰名を⼊⼒ 作成確認シート Passkeyを作成 新しい⽅式:システムシート Automatic Passkey Upgrade 同じシートで情報選択と作成 ⽒名‧メールアドレスを選択し、 Passkeyを作成 登録済みPasskeyでログイン 移⾏‧新規登録で共通の認証処理 15
  12. 既存ユーザーのPasskeyへの段階的移⾏ パスワード認証を残してPasskeyを導⼊し、最終的にパスワードレスを⽬指す ① 導⼊前 ② 移⾏期間 ③ ⽬指す状態 パスワードのみ パスワード+Passkey

    Passkeyのみ パスワードで ログイン Passkeyを追加し、 利⽤を広げる パスワード認証を 廃⽌する 今回の実装範囲(①から②) 今後の移⾏(②から③) ③ パスワード認証を廃⽌する前に • Passkeyでログインできることを確認する • Passkeyを利⽤できなくなった場合の復旧⽅法やサポート体制を⽤意する 16
  13. パスワード認証後にPasskeyを追加する流れ iOSアプリ Authenticator(認証器) Server ① パスワード認証 リクエストボディ:メールアドレス‧パスワード アクセストークン(後続APIの認証に使⽤) ② Passkey登録の開始を要求

    認証ヘッダー:アクセストークン Challenge ID(登録ID)‧RP ID(サービスを識別するドメイン名)‧User ID(アカウントID) ユーザー名(表⽰名)‧Challenge(Serverが発⾏する⼀度限りのランダム値) ③ Passkey作成を要求 RP ID‧User ID‧ユーザー名‧Challenge 認証器が鍵ペアを⽣成 Passkey登録データ 公開鍵などを含む登録結果 秘密鍵は認証器が管理し、 Serverへ送信しない ④ 登録完了を要求(認証ヘッダー:アクセストークン) リクエストボディ:Passkey登録データ‧Challenge ID Passkey登録完了 HTTP 204(ボディなし) ⑤ 登録データを検証して保存 公開鍵などを既存アカウントに追加 パスワード認証情報は保持 17
  14. パスワード認証後にPasskeyを追加する流れ iOSアプリ Authenticator(認証器) Server ① パスワード認証 リクエストボディ:メールアドレス‧パスワード アクセストークン(後続APIの認証に使⽤) ② Passkey登録の開始を要求

    認証ヘッダー:アクセストークン Challenge ID(登録ID)‧RP ID(サービスを識別するドメイン名)‧User ID(アカウントID) ユーザー名(表⽰名)‧Challenge(Serverが発⾏する⼀度限りのランダム値) ③ Passkey作成を要求 RP ID‧User ID‧ユーザー名‧Challenge 認証器が鍵ペアを⽣成 Passkey登録データ 公開鍵などを含む登録結果 秘密鍵は認証器が管理し、 Serverへ送信しない ④ 登録完了を要求(認証ヘッダー:アクセストークン) リクエストボディ:Passkey登録データ‧Challenge ID Passkey登録完了 HTTP 204(ボディなし) ⑤ 登録データを検証して保存 公開鍵などを既存アカウントに追加 パスワード認証情報は保持 18
  15. パスワード認証後にPasskeyを追加する流れ iOSアプリ Authenticator(認証器) Server ① パスワード認証 リクエストボディ:メールアドレス‧パスワード アクセストークン(後続APIの認証に使⽤) ② Passkey登録の開始を要求

    認証ヘッダー:アクセストークン Challenge ID(登録ID)‧RP ID(サービスを識別するドメイン名)‧User ID(アカウントID) ユーザー名(表⽰名)‧Challenge(Serverが発⾏する⼀度限りのランダム値) ③ Passkey作成を要求 RP ID‧User ID‧ユーザー名‧Challenge 認証器が鍵ペアを⽣成 Passkey登録データ 公開鍵などを含む登録結果 秘密鍵は認証器が管理し、 Serverへ送信しない ④ 登録完了を要求(認証ヘッダー:アクセストークン) リクエストボディ:Passkey登録データ‧Challenge ID Passkey登録完了 HTTP 204(ボディなし) ⑤ 登録データを検証して保存 公開鍵などを既存アカウントに追加 パスワード認証情報は保持 23
  16. パスワード認証後にPasskeyを追加する流れ iOSアプリ Authenticator(認証器) Server ① パスワード認証 リクエストボディ:メールアドレス‧パスワード アクセストークン(後続APIの認証に使⽤) ② Passkey登録の開始を要求

    認証ヘッダー:アクセストークン Challenge ID(登録ID)‧RP ID(サービスを識別するドメイン名)‧User ID(アカウントID) ユーザー名(表⽰名)‧Challenge(Serverが発⾏する⼀度限りのランダム値) ③ Passkey作成を要求 RP ID‧User ID‧ユーザー名‧Challenge 認証器が鍵ペアを⽣成 Passkey登録データ 公開鍵などを含む登録結果 秘密鍵は認証器が管理し、 Serverへ送信しない ④ 登録完了を要求(認証ヘッダー:アクセストークン) リクエストボディ:Passkey登録データ‧Challenge ID Passkey登録完了 HTTP 204(ボディなし) ⑤ 登録データを検証して保存 公開鍵などを既存アカウントに追加 パスワード認証情報は保持 28
  17. パスワード認証後にPasskeyを追加する流れ iOSアプリ Authenticator(認証器) Server ① パスワード認証 リクエストボディ:メールアドレス‧パスワード アクセストークン(後続APIの認証に使⽤) ② Passkey登録の開始を要求

    認証ヘッダー:アクセストークン Challenge ID(登録ID)‧RP ID(サービスを識別するドメイン名)‧User ID(アカウントID) ユーザー名(表⽰名)‧Challenge(Serverが発⾏する⼀度限りのランダム値) ③ Passkey作成を要求 RP ID‧User ID‧ユーザー名‧Challenge 認証器が鍵ペアを⽣成 Passkey登録データ 公開鍵などを含む登録結果 秘密鍵は認証器が管理し、 Serverへ送信しない ④ 登録完了を要求(認証ヘッダー:アクセストークン) リクエストボディ:Passkey登録データ‧Challenge ID Passkey登録完了 HTTP 204(ボディなし) ⑤ 登録データを検証して保存 公開鍵などを既存アカウントに追加 パスワード認証情報は保持 32
  18. パスワード認証後にPasskeyを追加する流れ iOSアプリ Authenticator(認証器) Server ① パスワード認証 リクエストボディ:メールアドレス‧パスワード アクセストークン(後続APIの認証に使⽤) ② Passkey登録の開始を要求

    認証ヘッダー:アクセストークン Challenge ID(登録ID)‧RP ID(サービスを識別するドメイン名)‧User ID(アカウントID) ユーザー名(表⽰名)‧Challenge(Serverが発⾏する⼀度限りのランダム値) ③ Passkey作成を要求 RP ID‧User ID‧ユーザー名‧Challenge 認証器が鍵ペアを⽣成 Passkey登録データ 公開鍵などを含む登録結果 秘密鍵は認証器が管理し、 Serverへ送信しない ④ 登録完了を要求(認証ヘッダー:アクセストークン) リクエストボディ:Passkey登録データ‧Challenge ID Passkey登録完了 HTTP 204(ボディなし) ⑤ 登録データを検証して保存 公開鍵などを既存アカウントに追加 パスワード認証情報は保持 36
  19. Automatic Passkey Upgradeの成功例 ①「パスワード」 をタップ ②「パスワード」 アプリを選ぶ ③ ログインする アカウントを選ぶ

    保存済みパスワードを選び、⾃動⼊⼒でログインする 対応する認証情報管理アプリを使う(例:Appleの「パスワード」) ④ パスワードを 1回だけ使⽤する ⑤ Passkeyの 作成通知が届く 41
  20. Automatic Passkey Upgradeの失敗例 ① メールアドレス‧ パスワードを⼿⼊⼒ ②「認証してPasskey登録へ」 をタップ 認証情報管理アプリが確認する主な条件(Apple WWDC24)

    直前に、同じアカウントの保存済みパスワードを⾃動⼊⼒したか ⼿⼊⼒だけではこの条件を満たせない(リクエスト⾃体は実⾏する) ⾃動⼊⼒を促す導線を⽤意し、作成されなくてもログインを維持する ③ パスワード認証は成功 Passkey登録は未完了 42
  21. 実装中に遭遇したエラー 端末ではPasskeyを作成できているのに、 Serverで登録データの検証に失敗する Serverの実⾏ログ(抜粋‧改⾏調整) [PasskeyRegistration] step=registration.verifyCredential event=failed reason=userPresentFlagNotSet errorType=WebAuthn.WebAuthnError [

    WARNING ] Abort.400: Passkey登録データを検証できませんでした。 エラーの意味:登録データ内のUPフラグが0(未セット) UP(User Presence)は、登録‧認証時に認証器が利⽤者の操作を確認したかを表す。 利⽤しているswift-webauthnがUP=1を必須としていたため、検証に失敗していた。 43
  22. Passkey登録時のUP検証 WebAuthn Level 3 §7.1 ⼿順15(要約) WebAuthnの登録要求で mediation が "conditional"

    ではない場合、 Serverは登録データのUP=1を確認する。 通常のPasskey登録 Automatic Passkey Upgrade iOS:requestStyle: .standard iOS:requestStyle: .conditional UP=1を確認する UP=1は必須ではない Automatic Passkey Upgradeでは、 UP=0だけを理由に登録データを拒否しない。 WWDC2024 パスキーによるアップグレードと認証情報マネージャによるサインインの効率化 44
  23. UP検証エラーへの対応 swift-webauthnをローカルにクローンして修正 swift-webauthnのPR #118と同様に requireUserPresence を追加し、 Challenge‧Origin‧RP IDなどの検証を維持しながら、 Passkey登録時にUP=1を必須にするかを切り替える。 Passkey登録の経路

    requireUserPresence とUP検証 通常のPasskey登録 true:UP=1を必須にする Automatic Passkey Upgrade⽤API (パスワード認証済み) false:UP=1を必須にしない SPMの依存先にローカル修正版を指定する(Package.swift) .package(path: "Vendor/swift-webauthn"), 45
  24. Passkey新規登録でのiOSアプリとServerの役割 従来の⽅式:⼊⼒フォーム ASAuthorizationPlatformPublicKeyCredentialProvider Passkey対応:iOS 16以降 ⼊⼒フォーム 作成確認シート メールアドレス‧ 表⽰名を⼊⼒ Passkeyを作成

    Server 登録データを検証して アカウントを作成 新しい⽅式:システムシート ASAuthorizationAccountCreationProvider iOS 26以降 システムシート ⽒名‧メールアドレスを選択し、Passkeyを作成 Server 登録データを検証して アカウントを作成 どちらの⽅式でも、Serverが登録データを検証し、アカウントを作成する iOSアプリがPasskeyの登録データをServerへ送信する 47
  25. メールアドレス‧表⽰名の取得タイミング ⽅式 iOSアプリが 情報を取得するタイミング Serverへ 情報を送信するタイミング 従来の⽅式 ⼊⼒フォーム Passkeyの作成前に フォームから取得

    開始時‧完了時の どちらでも送信できる 新しい⽅式 システムシート Passkeyの作成完了時に シートの選択結果から取得 選択結果を取得した後に送信 本サンプルでは、フォームのメールアドレス‧表⽰名を開始APIで送信 システムシートの選択結果は、Passkey作成完了時にiOSアプリが受け取る 48
  26. システムシートを使ったPasskey新規登録の流れ iOSアプリ システムシート Server ① Passkey新規登録を開始(アカウント情報はまだ送らない) 登録ID(新規登録の管理ID)‧RP ID(サービスを識別するドメイン名) User ID(アカウントID)‧有効期限(登録を完了できる期限)

    Challenge(Serverが発⾏する⼀度限りのランダム値) ② システムシートを要求 RP ID‧Challenge‧User ID メールアドレス‧⽒名 Passkey登録データ (公開鍵などを含む登録結果) メールアドレス‧⽒名の選択 Passkeyを作成 ③ Passkey新規登録を完了 リクエストボディ:登録ID‧メールアドレス‧表⽰名‧Passkey登録データ ④ 登録データを検証してDBに保存 HTTP 201(ボディなし) アカウント‧メールアドレス‧Passkey 49
  27. Passkey登録の仕組み 公開鍵とCredential ID(認証情報のID)を、アカウントに紐づけて保存する iOSアプリ Authenticator(認証器) サービスのServer ① 登録に使う情報を渡す Challenge(Serverが発⾏する⼀度限りのランダム値) RP

    ID(サービスを識別するドメイン名)‧User ID(アカウントID)など ② Passkeyの作成を要求 鍵ペアを⽣成 登録データ 公開鍵‧Credential IDなど 秘密鍵は認証器が管理 公開鍵を登録データに含める ③ 登録データをServerへ送信 公開鍵‧Credential ID‧Challenge‧Origin(要求元)など 秘密鍵は、サービスのServerへ 送らない ④ 登録データを検証 Challenge‧Origin‧RP IDなど 公開鍵‧Credential IDを アカウントに紐づけて保存 50
  28. ① Passkey新規登録を開始する iOSアプリ側 registration = try await apiClient.execute( PasskeyAccountCreationStartOperation() )

    let options = try PasskeyAccountCreationOptions( relyingPartyID: registration.relyingPartyID, challenge: Base64URLCodec.decode(registration.challenge), userID: Base64URLCodec.decode(registration.userID) ) メールアドレスを選ぶ前に、ServerからPasskey作成⽤の情報を取得する。 Challenge‧User IDは、システムAPIが要求するData型へ変換する。 PasskeyAccountRegistrationInteractor.swift / 抜粋 51
  29. ① 新規登録の⼀時情報を保存する Server側 let accountID = AccountID() let options =

    try accountCreationOptionsGenerator .generate(for: accountID) let registrationID = PasskeyAccountRegistrationID() let expiresAt = Date.now.addingTimeInterval(registrationLifetime) try await registrationRepository.create( PasskeyAccountRegistration( id: registrationID, accountID: accountID, email: nil, displayName: nil, challenge: options.challenge, expiresAt: expiresAt, completedAt: nil ) ) 登録ID‧アカウントID‧Challenge‧新規登録の有効期限を⼀時保存する。 メールアドレス‧表⽰名は未選択。アカウントはまだDBに保存しない。 PasskeyAccountRegistrationStartUseCase.swift / 抜粋 52
  30. Passkey登録の仕組み 公開鍵とCredential ID(認証情報のID)を、アカウントに紐づけて保存する iOSアプリ Authenticator(認証器) サービスのServer ① 登録に使う情報を渡す Challenge(Serverが発⾏する⼀度限りのランダム値) RP

    ID(サービスを識別するドメイン名)‧User ID(アカウントID)など ② Passkeyの作成を要求 鍵ペアを⽣成 登録データ 公開鍵‧Credential IDなど 秘密鍵は認証器が管理 公開鍵を登録データに含める ③ 登録データをServerへ送信 公開鍵‧Credential ID‧Challenge‧Origin(要求元)など 秘密鍵は、サービスのServerへ 送らない ④ 登録データを検証 Challenge‧Origin‧RP IDなど 公開鍵‧Credential IDを アカウントに紐づけて保存 53
  31. ② システムシートでの新規登録を要求する iOSアプリ側 let provider = ASAuthorizationAccountCreationProvider() let request =

    provider .createPlatformPublicKeyCredentialRegistrationRequest( acceptedContactIdentifiers: [.email], shouldRequestName: true, relyingPartyIdentifier: options.relyingPartyID, challenge: options.challenge, userID: options.userID ) let authorizationController = ASAuthorizationController( authorizationRequests: [request] ) authorizationController.delegate = self // Presentation AnchorとContinuationの設定は省略 authorizationController.performRequests() メールアドレスと⽒名を要求し、同じシートでPasskeyを作成する。 Serverが返したRP ID‧Challenge‧User IDを渡す。 AuthenticationServicesAccountCreationRequester.swift / 抜粋 54
  32. ② シートの選択内容とPasskey登録データを受け取る iOSアプリ側 func authorizationController( controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization

    ) { // Controllerの一致確認は省略 guard let accountCreation = authorization.credential as? ASAuthorizationAccountCreationPlatformPublicKeyCredential else { /* エラー処理は省略 */ return } guard case let .email(email) = accountCreation.contactIdentifier, !email.value.isEmpty else { /* エラー処理は省略 */ return } let credential = accountCreation.credentialRegistration // accountCreation.nameから表示名を生成する処理は省略 // 選択内容と登録データをまとめ、待機中のInteractorへ返す } Delegateの成功通知から、メールアドレス‧⽒名‧Passkey登録データを取得する。 contactIdentifierが連絡先、nameが⽒名、credentialRegistrationが登録データ。 AuthenticationServicesAccountCreationRequester.swift / 抜粋 55
  33. Passkey登録の仕組み 公開鍵とCredential ID(認証情報のID)を、アカウントに紐づけて保存する iOSアプリ Authenticator(認証器) サービスのServer ① 登録に使う情報を渡す Challenge(Serverが発⾏する⼀度限りのランダム値) RP

    ID(サービスを識別するドメイン名)‧User ID(アカウントID)など ② Passkeyの作成を要求 鍵ペアを⽣成 登録データ 公開鍵‧Credential IDなど 秘密鍵は認証器が管理 公開鍵を登録データに含める ③ 登録データをServerへ送信 公開鍵‧Credential ID‧Challenge‧Origin(要求元)など 秘密鍵は、サービスのServerへ 送らない ④ 登録データを検証 Challenge‧Origin‧RP IDなど 公開鍵‧Credential IDを アカウントに紐づけて保存 56
  34. ③ Passkey新規登録の完了APIを呼ぶ iOSアプリ側 let credential = result.credential let operation =

    PasskeyAccountCreationFinishOperation( registrationID: registration.registrationID, emailAddress: result.emailAddress, displayName: result.displayName, credentialID: Base64URLCodec.encode(credential.credentialID), clientDataJSON: Base64URLCodec.encode(credential.clientDataJSON), attestationObject: Base64URLCodec .encode(credential.attestationObject) ) // キャンセル確認とエラー処理は省略 try await apiClient.execute(operation) シートで受け取ったメールアドレス‧表⽰名とPasskey登録データを、 開始APIで取得した登録IDとともにServerへ送信する。 PasskeyAccountRegistrationInteractor.swift / 抜粋‧改⾏調整 57
  35. Passkey登録の仕組み 公開鍵とCredential ID(認証情報のID)を、アカウントに紐づけて保存する iOSアプリ Authenticator(認証器) サービスのServer ① 登録に使う情報を渡す Challenge(Serverが発⾏する⼀度限りのランダム値) RP

    ID(サービスを識別するドメイン名)‧User ID(アカウントID)など ② Passkeyの作成を要求 鍵ペアを⽣成 登録データ 公開鍵‧Credential IDなど 秘密鍵は認証器が管理 公開鍵を登録データに含める ③ 登録データをServerへ送信 公開鍵‧Credential ID‧Challenge‧Origin(要求元)など 秘密鍵は、サービスのServerへ 送らない ④ 登録データを検証 Challenge‧Origin‧RP IDなど 公開鍵‧Credential IDを アカウントに紐づけて保存 58
  36. ④ Passkey登録データを検証する Server側 guard let registration = try await registrationRepository.findUsable(

    by: registrationID, at: Date.now ) else { throw PasskeyAccountRegistrationError.registrationUnavailable } // メールアドレス・表示名の確定、重複チェックは省略 credential = try await registrationVerifier.verify( response, against: registration.challenge, for: registration.accountID ) 有効な登録IDに対応するChallengeとアカウントIDで、登録データを検証する。 ⼊⼒フォーム⽅式と共通のVerifierを使い、新規登録ではUP=1も確認する。 PasskeyAccountRegistrationFinishUseCase.swift / 抜粋 59
  37. ④ アカウントとPasskeyをまとめて保存する Server側 return try await database.transaction { database in

    // 登録ID・アカウントID・未完了・有効期限を確認し、 // 新規登録の一時情報を完了済みに更新する処理は省略 try await AccountRecord(account: account) .create(on: database) try await AccountEmailRecord(accountEmail: accountEmail) .create(on: database) try await PasskeyCredentialRecord(credential: credential) .create(on: database) } return true アカウント‧メールアドレス‧Passkeyを同じトランザクションで保存する。 成功後、ControllerはHTTP 201を返す。パスワード認証情報は作成しない。 FluentPasskeyAccountRegistrationRepository.swift / 抜粋 60
  38. Passkeyでログインする流れ iOSアプリ Authenticator(認証器) Server ① Passkey認証を開始(アカウントはまだ指定しない) Challenge ID(今回の認証を識別するID)‧RP ID(サービスを識別するドメイン名) Challenge(Serverが発⾏する⼀度限りのランダム値)

    有効期限(Challengeを使⽤できる期限) ② Passkey認証を要求 RP ID‧Challenge 署名付き認証データ(Assertion) Credential ID(認証情報のID) 署名など Passkeyを選択‧本⼈確認 登録済みの秘密鍵で署名 ③ 署名付き認証データを送信 Challenge ID‧Credential ID‧署名など ⑤ アクセストークン(API認証⽤) 有効期限とともに受け取り、端末に保存 ④ 署名付き認証データを検証 ⑤ 認証セッション(ログイン状態)を発⾏ 62
  39. Passkey認証の仕組み 登録済みの公開鍵で署名を検証し、ログインを許可する iOSアプリ Authenticator(認証器) サービスのServer ① 認証に使う情報を渡す Challenge(Serverが発⾏する⼀度限りのランダム値) ② 利⽤者の確認‧署名を要求

    署名付き認証データ(Assertion) Credential ID(認証情報のID) 署名など 登録済みの秘密鍵を使⽤ Face ID‧PINで本⼈確認 署名を⽣成 ③ 署名付き認証データをServerへ送信 Credential ID‧署名‧Challenge‧Origin(要求元)など 新しい鍵は作らず、 登録済みの鍵を利⽤する ⑤ アクセストークンを返す (ログイン状態を⽰す値) ④ 署名付き認証データを検証 登録済みの公開鍵を使⽤ 認証セッション(ログイン状態)を発⾏ 63
  40. ① Passkey認証を開始する iOSアプリ側 let startResponse = try await apiClient.execute( PasskeyAuthenticationStartOperation()

    ) let options = try PasskeyAuthenticationOptions( relyingPartyID: startResponse.relyingPartyID, challenge: Base64URLCodec.decode(startResponse.challenge) ) let assertion = try await assertionRequestor.requestAssertion( using: options ) アカウントを指定せずに認証開始APIを呼び、RP IDとChallengeを取得する。 ChallengeをDataへ戻し、システムへの認証要求に渡す。 PasskeyAuthenticationInteractor.swift / 抜粋‧改⾏調整 64
  41. ① 認証⽤のChallengeを発⾏する Server側 let options = try optionsGenerator.generate() let challengeID

    = PasskeyChallengeID(value: UUID()) let expiresAt = Date.now.addingTimeInterval(challengeLifetime) try await challengeRepository.create( id: challengeID, challenge: options.challenge, expiresAt: expiresAt ) return PasskeyAuthenticationStartResult( challengeID: challengeID, options: options, expiresAt: expiresAt ) 今回の認証⽤のChallengeを、Challenge ID‧有効期限とともに保存する。 保存に成功してから、同じChallengeをiOSアプリへ返す。 PasskeyAuthenticationStartUseCase.swift / 抜粋‧改⾏調整 65
  42. Passkey認証の仕組み 登録済みの公開鍵で署名を検証し、ログインを許可する iOSアプリ Authenticator(認証器) サービスのServer ① 認証に使う情報を渡す Challenge(Serverが発⾏する⼀度限りのランダム値) ② 利⽤者の確認‧署名を要求

    署名付き認証データ(Assertion) Credential ID(認証情報のID) 署名など 登録済みの秘密鍵を使⽤ Face ID‧PINで本⼈確認 署名を⽣成 ③ 署名付き認証データをServerへ送信 Credential ID‧署名‧Challenge‧Origin(要求元)など 新しい鍵は作らず、 登録済みの鍵を利⽤する ⑤ アクセストークンを返す (ログイン状態を⽰す値) ④ 署名付き認証データを検証 登録済みの公開鍵を使⽤ 認証セッション(ログイン状態)を発⾏ 66
  43. ② システムへPasskey認証を要求する iOSアプリ側 let provider = ASAuthorizationPlatformPublicKeyCredentialProvider( relyingPartyIdentifier: options.relyingPartyID )

    let request = provider.createCredentialAssertionRequest( challenge: options.challenge ) request.userVerificationPreference = .required request.allowedCredentials = [] let controller = ASAuthorizationController( authorizationRequests: [request] ) controller.delegate = self // 表示先・Continuation・キャンセル処理の設定は省略 controller.performRequests() Credential IDを事前に限定せず、利⽤者が登録済みPasskeyを選択する。 本⼈確認を要求し、署名付き認証データをDelegateで受け取る。 AuthenticationServicesPasskeyAssertionRequester.swift / 抜粋‧改⾏調整 67
  44. ② Delegateで署名付き認証データを受け取る iOSアプリ側 guard let credential = authorization.credential as? ASAuthorizationPlatformPublicKeyCredentialAssertion

    else { /* エラー処理は省略 */ return } // Controllerの一致確認・userIDの必須チェックは省略 let result = PasskeyAssertionResult( credentialID: credential.credentialID, clientDataJSON: credential.rawClientDataJSON, authenticatorData: credential.rawAuthenticatorData, signature: credential.signature, userHandle: credential.userID ) finish(returning: result) Credential ID‧Client Data JSON‧Authenticator Data‧署名‧User Handleを取得する。 端末で認証データを取得後、Serverによる検証へ進む。 AuthenticationServicesPasskeyAssertionRequester.swift / 抜粋‧改⾏調整 68
  45. Passkey認証の仕組み 登録済みの公開鍵で署名を検証し、ログインを許可する iOSアプリ Authenticator(認証器) サービスのServer ① 認証に使う情報を渡す Challenge(Serverが発⾏する⼀度限りのランダム値) ② 利⽤者の確認‧署名を要求

    署名付き認証データ(Assertion) Credential ID(認証情報のID) 署名など 登録済みの秘密鍵を使⽤ Face ID‧PINで本⼈確認 署名を⽣成 ③ 署名付き認証データをServerへ送信 Credential ID‧署名‧Challenge‧Origin(要求元)など 新しい鍵は作らず、 登録済みの鍵を利⽤する ⑤ アクセストークンを返す (ログイン状態を⽰す値) ④ 署名付き認証データを検証 登録済みの公開鍵を使⽤ 認証セッション(ログイン状態)を発⾏ 69
  46. ③ 署名付き認証データをServerへ送信する iOSアプリ側 response = try await apiClient.execute( PasskeyAuthenticationFinishOperation( challengeID:

    startResponse.challengeID, credentialID: Base64URLCodec .encode(assertion.credentialID), clientDataJSON: Base64URLCodec .encode(assertion.clientDataJSON), authenticatorData: Base64URLCodec .encode(assertion.authenticatorData), signature: Base64URLCodec .encode(assertion.signature), userHandle: Base64URLCodec .encode(assertion.userHandle) ) ) 署名付き認証データをBase64URLへ変換し、Challenge IDとともに認証完了APIへ送る。 署名検証に使うClient Data JSONは、再構築せず元のバイト列を渡す。 PasskeyAuthenticationInteractor.swift / 抜粋‧改⾏調整 70
  47. Passkey認証の仕組み 登録済みの公開鍵で署名を検証し、ログインを許可する iOSアプリ Authenticator(認証器) サービスのServer ① 認証に使う情報を渡す Challenge(Serverが発⾏する⼀度限りのランダム値) ② 利⽤者の確認‧署名を要求

    署名付き認証データ(Assertion) Credential ID(認証情報のID) 署名など 登録済みの秘密鍵を使⽤ Face ID‧PINで本⼈確認 署名を⽣成 ③ 署名付き認証データをServerへ送信 Credential ID‧署名‧Challenge‧Origin(要求元)など 新しい鍵は作らず、 登録済みの鍵を利⽤する ⑤ アクセストークンを返す (ログイン状態を⽰す値) ④ 署名付き認証データを検証 登録済みの公開鍵を使⽤ 認証セッション(ログイン状態)を発⾏ 71
  48. ④ Challengeと登録済み認証情報を取得する Server側 guard let challenge = try await challengeRepository.consume(

    id: challengeID, at: Date.now ) else { throw PasskeyAuthenticationFinishError.challengeUnavailable } guard let credential = try await credentialRepository .find(by: response.credentialID), let account = try await accountRepository .find(by: credential.accountID) else { throw PasskeyAuthenticationFinishError.invalidCredentials } 未使⽤かつ期限内のChallengeを、⼀度だけ消費する。 Credential IDから登録済みの公開鍵と所有アカウントを取得する。 PasskeyAuthenticationFinishUseCase.swift / 抜粋‧改⾏調整 72
  49. ④ 署名付き認証データを検証し、認証状態を更新する Server側 verifiedCredential = try await authenticationVerifier.verify( response, against:

    challenge, using: credential ) // 検証エラーの処理・キャンセル確認は省略 guard try await credentialRepository .update(verifiedCredential) else { throw PasskeyAuthenticationFinishError.invalidCredentials } 保存済みの公開鍵で署名を検証し、Challenge‧Origin‧RP IDなどを確認する。 検証後の署名回数とバックアップ状態を保存する。 PasskeyAuthenticationFinishUseCase.swift / 抜粋‧改⾏調整 73
  50. Passkey認証の仕組み 登録済みの公開鍵で署名を検証し、ログインを許可する iOSアプリ Authenticator(認証器) サービスのServer ① 認証に使う情報を渡す Challenge(Serverが発⾏する⼀度限りのランダム値) ② 利⽤者の確認‧署名を要求

    署名付き認証データ(Assertion) Credential ID(認証情報のID) 署名など 登録済みの秘密鍵を使⽤ Face ID‧PINで本⼈確認 署名を⽣成 ③ 署名付き認証データをServerへ送信 Credential ID‧署名‧Challenge‧Origin(要求元)など 新しい鍵は作らず、 登録済みの鍵を利⽤する ⑤ アクセストークンを返す (ログイン状態を⽰す値) ④ 署名付き認証データを検証 登録済みの公開鍵を使⽤ 認証セッション(ログイン状態)を発⾏ 74
  51. ⑤ 認証セッションを発⾏する Server側 let token = try sessionTokenGenerator.generate() let tokenDigest

    = sessionTokenHasher.hash(token) let expiresAt = Date.now.addingTimeInterval(sessionLifetime) let session = AuthenticationSession( id: AuthenticationSessionID(), accountID: account.id, tokenDigest: tokenDigest, expiresAt: expiresAt, revokedAt: nil ) try await sessionRepository.create(session) return PasskeyAuthenticationFinishResult( account: account, token: token, expiresAt: expiresAt ) 認証セッションを作成し、アクセストークンのハッシュをDBへ保存する。 保存後、アクセストークンと有効期限をiOSアプリへ返す。 PasskeyAuthenticationFinishUseCase.swift / 抜粋‧改⾏調整 75
  52. ⑤ 認証情報を保存し、ログインを完了する iOSアプリ側 let credential = AuthorizationCredential( accessToken: response.accessToken, expiresAt:

    response.expiresAt ) guard credential.isUsable(at: Date.now) else { throw PasskeyAuthenticationError .invalidAuthorizationCredential } try await credentialStore.save(credential) authenticatedCredential = credential return response.displayName アクセストークンと有効期限を、Keychainを使うStoreへ保存する。 保存に成功してから、画⾯へログイン成功を返す。 PasskeyAuthenticationInteractor.swift / 抜粋‧改⾏調整 76
  53. まとめ • パスワード認証を維持しながら、Passkeyへ段階的に移⾏する 認証済みのアカウントに追加し、追加に失敗してもログイン状態を維持する。 • Automatic Passkey Upgradeでは、⾃動⼊⼒の導線とUP検証を整える 対応する認証情報管理アプリで、保存済みパスワードの⾃動⼊⼒を促す。 ⾃動移⾏の登録データでは、ServerでUP=1を必須にしない。

    • 新規登録では、iOSアプリが情報を取得するタイミングが異なる フォームの⼊⼒値は、開始時にも完了時にもServerへ送信できる。 システムシートの選択結果は、Passkey作成完了時に受け取る。 • 登録経路が異なっても、ログインの認証処理は共通にできる Serverが登録済みの公開鍵で署名付き認証データを検証し、認証セッションを発⾏する。 77
  54. 参考資料 基礎 • Meet passkeys — WWDC22∕仕組み‧基本概念 iOS実装 • Supporting

    passkeys • Streamline sign-in with passkey upgrades and credential managers — 登録‧ログインの実装 WWDC24∕Automatic Passkey Upgrade • What’s new in passkeys • Performing fast account creation with passkeys — WWDC25∕iOS 26の新規登録 — 新規登録の公式サンプル Server実装‧仕様 • Web Authentication: Level 3 • Going passwordless with Passkeys • swift-webauthn PR #118 — 登録‧認証‧UPの検証条件 — Swift.org∕Server実装 — requireUserPresenceの追加 78
  55. Challengeが必要な理由 Challenge:Serverが認証ごとに発⾏する、予測困難なランダム値 過去の署名付き認証データを再送してログインする 「リプレイ攻撃」を防ぐ。 毎回同じChallengeの場合 毎回異なるChallengeの場合 過去のデータ:Challenge = A 今回の要求:Challenge

    = A 過去のデータ:Challenge = A 今回の要求:Challenge = B 署名検証だけでは 過去の再送と区別できない 今回の値と⼀致しないため、 Serverが再送を拒否する 認証時はChallengeを含むデータも署名の対象。 AをBに書き換えても、秘密鍵なしでは有効な署名を作り直せない。 出典:W3C WebAuthn Level 3 Challenge∕認証データの検証 81
  56. Passkey登録で受け渡すデータ 利⽤する場⾯ データ名 ⽣成元 内容‧Serverでの⽤途 Passkey登録開始 Passkey登録完了 Challenge ID (登録ID)

    Server Passkey登録開始時に発⾏するID。 Passkey登録完了要求でServerへ返し、 保存した登録⼿続きを特定する。 Passkey作成 Passkey登録完了 Credential ID (認証情報のID) 認証器 作成した認証情報を識別するID。 公開鍵とともにアカウントへ保存。 Passkey作成 Passkey登録完了 Client Data JSON (登録要求の情報) iOSのシステム処理 AuthenticationServices Challenge‧Origin(要求元)を 含むJSON。Serverが値を照合する。 Passkey作成 Passkey登録完了 Attestation Object (認証器が⽣成) 認証器 主な内容:公開鍵‧Credential ID、 RP IDのハッシュ‧本⼈確認の実施状況。 Serverが検証し、認証情報を取り出す。 補⾜ Challenge ID:Passkey登録開始と登録完了を対応づける、本サンプルAPI独⾃の管理ID。 Challenge:Serverが要求ごとに発⾏する、⼀度限りのランダム値。 82
  57. Passkey登録の主な検証項⽬ iOSアプリが送信する登録データ ① 登録条件 を確認 Passkey登録⽤データ ② 発⾏した値 などと照合 Challenge

    確認 利⽤者の操作‧本⼈確認の結果 ⼀致確認 ⼀致確認 https://example.com 許可するOrigin https://example.com RP(Relying Party) Passkeyで認証するサービス 認証器が利⽤したRP IDを SHA-256でハッシュ化 ⼀致確認 公開鍵‧Credential ID 確認 認証器が新しく⽣成 今回発⾏したChallenge Serverに保存した値 RP IDのハッシュ ③ 保存する 認証情報を確認 登録開始時の要求条件 データ形式と本⼈確認を検証 Serverが発⾏した値を返す Origin(クライアントが設定) 値の照合 Serverが検証に使う情報 Serverが設定したRP ID SHA-256("example.com") 公開鍵の形式‧暗号⽅式 Credential IDの重複も確認 83
  58. Passkey認証の主な検証項⽬ iOSアプリが送信する認証データ ① 認証情報 を特定 Credential ID: key-A ② 認証データ

    を検証 Challenge IDで検索 認証情報を識別するID ⼀致確認 ⼀致確認 署名と検証に必要なデータ Challenge‧Origin‧ RP IDのハッシュなどを含む 許可するOrigin https://example.com RP(Relying Party) RP IDのハッシュ 署名の検証 今回発⾏したChallenge Serverに保存した値 https://example.com 認証器が利⽤したRP IDを SHA-256でハッシュ化 ID: key-A Account A / 公開鍵 A Serverが発⾏した値を返す Origin(クライアントが設定) 値の照合 Serverが検証に使う情報 Passkeyで認証するサービス ⼀致確認 Serverが設定したRP ID SHA-256("example.com") 検証 ①で特定した公開鍵 A 署名の正しさを検証 84