mcpField
A
axiom-passkeys

axiom-passkeys

github.com/charleswiltgen安全1.2k35 次安装今天更新
代码仓库

关于 axiom-passkeys

面向 iOS 开发者的 Passkeys 认证指南,涵盖注册、登录、AutoFill 和密码用户迁移。它也说明关联域配置及结合通行密钥、密码和 Sign in with Apple 的登录流程。

主要功能

  1. 01实现通行密钥注册与断言
  2. 02配置 AutoFill 辅助登录
  3. 03在 iOS 18+ 请求自动升级
  4. 04组合通行密钥、密码和 Apple 登录
  5. 05设置关联域与 AASA 文件
  6. 06处理跨设备通行密钥登录

适用场景

  1. 01为 iOS 应用添加通行密钥登录
  2. 02排查 QuickType 栏未显示通行密钥
  3. 03逐步将密码用户迁移到通行密钥
  4. 04同时支持多种凭据的登录流程

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/charleswiltgen/axiom --skill axiom-passkeys

安装后在 Claude Code 中输入/axiom-passkeys确认已加载。

详细介绍

密钥通行证

适用于 iOS 应用的密钥通行证身份验证——注册、断言、借助自动填充的请求、自动升级、组合凭据流程,以及从基于密码的身份验证迁移。

何时使用此技能

在以下情况下使用:

  • ☑ 为 iOS 应用添加密钥通行证登录
  • ☑ 将基于密码的身份验证替换为密钥通行证
  • ☑ 为密钥通行证注册或断言配置 ASAuthorizationController
  • ☑ 设置借助自动填充的密钥通行证请求(QuickType 栏)
  • ☑ 为现有密码用户添加自动密钥通行证升级(iOS 18+)
  • ☑ 支持组合凭据请求(密钥通行证 + 密码 + 使用 Apple 登录)
  • ☑ 为 webauthn/webcredentials 配置关联域
  • ☑ 调试密钥通行证断言失败或 QuickType 建议缺失

示例提示词

“如何为我的应用添加密钥通行证登录?” “我的密钥通行证没有显示在 QuickType 栏中” “如何将现有密码用户迁移到密钥通行证?” “ASAuthorizationError.canceled——出了什么问题?” “迁移期间如何同时支持密钥通行证和密码?” “如何为密钥通行证设置关联域?” “performRequests 和 performAutoFillAssistedRequests 有什么区别?” “如何在 iOS 18 上添加自动密钥通行证升级?”

风险信号

以下迹象表明你可能走错了方向:

  • ❌ 在密钥通行证可用时仍将密码用作主要身份验证方式——密钥通行证不是“额外的安全措施”,而是替代方案。每一次仅使用密码的登录,都是你留下的一个钓鱼机会。
  • ❌ 未将用户名字段的 textContentType 标注为 .username——没有此标注,系统就无法将该字段与密钥通行证凭据关联。对于未标注的字段,自动填充不会建议密钥通行证。
  • ❌ 在主要登录流程中使用 performRequests()——这会显示模态弹窗,而不是将密钥通行证放入 QuickType 栏。主要流程应使用 performAutoFillAssistedRequests()。将 performRequests() 留给注册和用户明确点按“登录”按钮的场景。
  • ❌ 在服务器上将 userVerification 设为 "required"——这会导致没有生物识别功能的设备无法登录。平台会根据设备情况妥善处理验证。请使用 "preferred"(默认值)。
  • ❌ 在扩展或非主应用环境中创建密钥通行证——创建密钥通行证需要主应用目标。扩展可以执行断言,但不能注册。
  • ❌ 不支持组合凭据请求——迁移期间,用户可能拥有密钥通行证、密码,或两者都没有。单个 ASAuthorizationController 可以处理这三种情况。只提供密钥通行证会让尚未升级的用户无法登录。

为什么选择密钥通行证

密钥通行证不是对密码的渐进式改进,而是替代密码的一种架构。

防钓鱼:每个密钥通行证都以加密方式绑定到特定域名。secure-myapp.com 上的虚假登录页面无法触发为 myapp.com 创建的密钥通行证。用户没有需要输入到错误网站的凭据。

无需担心凭据数据库泄露:服务器只存储公钥。发生泄露也不会暴露可利用的信息——没有可破解的密码哈希,也没有可重放的共享密钥。

轻点一次即可登录:Face ID 或 Touch ID 取代手动输入。注册和断言都只需轻点一次。

FIDO Alliance 标准:采用 WebAuthn/CTAP2 协议。适用于 Apple、Google 和 Microsoft 平台。在 iPhone 上创建的密钥通行证会通过 iCloud Keychain 同步,并可在 Mac、iPad 和 Web 上使用。

普及情况:Apple 将密钥通行证作为系统的一等功能提供。iCloud Keychain 会同步密钥通行证。Passwords 应用负责管理它们。自 iOS 17 起,第三方凭据管理器(1Password、Dashlane)也已原生支持密钥通行证。

关联域设置

密钥通行证需要关联域,以将你的应用与服务器关联起来。没有关联域,系统就不会为你的应用提供密钥通行证。

1. 添加权利

在 Xcode 中:Target > Signing & Capabilities > + Associated Domains。

添加:

webcredentials:example.com
2. 托管 AASA 文件

通过 HTTPS 从你的域名提供 /.well-known/apple-app-site-association,并设置 Content-Type: application/json:

{
  "webcredentials": {
    "apps": [
      "TEAMID.com.example.myapp"
    ]
  }
}

要求:

  • 使用有效证书通过 HTTPS 提供(不能是自签名证书)
  • 不得重定向——文件必须直接在该路径提供
  • TEAMID 是你的 Apple Developer Team ID,而不是 bundle ID 前缀
  • 文件必须位于根域名下,不能放在子目录中
3. 验证
curl -s "https://example.com/.well-known/apple-app-site-association" | python3 -m json.tool

Apple 的 CDN 会缓存 AASA 文件。更改最多可能需要 24 小时才能生效。开发期间,请在设备的 Developer Settings 中启用 Associated Domains Development,并使用 ?mode=developer 查询参数。

注册流程

注册会创建一个新的通行密钥,并将其存储在用户的凭证管理器中。

import AuthenticationServices

func registerPasskey(challenge: Data, userName: String, userID: Data) {
    let provider = ASAuthorizationPlatformPublicKeyCredentialProvider(
        relyingPartyIdentifier: "example.com"
    )

    let request = provider.createCredentialRegistrationRequest(
        challenge: challenge,
        name: userName,
        userID: userID
    )

    let controller = ASAuthorizationController(authorizationRequests: [request])
    controller.delegate = self
    controller.presentationContextProvider = self
    controller.performRequests()
}

关键参数:

  • relyingPartyIdentifier — 必须与你的关联域名匹配(不带 https:// 前缀)
  • challenge — 由服务器生成的加密质询(至少使用 16 个随机字节)。切勿重复使用质询。
  • name — 在通行密钥提示词和 Passwords app 中向用户显示的名称
  • userID — 用户账户的不透明标识符。不要使用电子邮件或用户名——请使用随机 UUID 或服务器端用户 ID
处理注册响应
func authorizationController(controller: ASAuthorizationController,
                             didCompleteWithAuthorization authorization: ASAuthorization) {
    guard let credential = authorization.credential
        as? ASAuthorizationPlatformPublicKeyCredentialRegistration else { return }

    let attestationObject = credential.rawAttestationObject
    let clientDataJSON = credential.rawClientDataJSON
    let credentialID = credential.credentialID

    // Send attestationObject, clientDataJSON, credentialID to your server
    // Server validates and stores the public key
}

注册使用 performRequests()(模态),因为用户明确选择创建通行密钥。这是适合使用模态展示的唯一场景。

断言流程(登录)

登录有两种路径,分别适用于不同的 UX 场景。

AutoFill 辅助(首选路径)

推荐的登录流程。当用户轻点内容类型为 .username 的文本字段时,通行密钥会显示在 QuickType 栏中。轻点一次即可登录,不会被模态界面打断。

func signInWithAutoFill() {
    let provider = ASAuthorizationPlatformPublicKeyCredentialProvider(
        relyingPartyIdentifier: "example.com"
    )

    let request = provider.createCredentialAssertionRequest(
        challenge: serverChallenge
    )

    let controller = ASAuthorizationController(authorizationRequests: [request])
    controller.delegate = self
    controller.presentationContextProvider = self
    controller.performAutoFillAssistedRequests()
}

关键细节:

  • 尽早调用 performAutoFillAssistedRequests()——在用户聚焦用户名字段之前。请在 viewDidAppear 中或登录视图出现时调用。
  • 用户名 UITextField 必须设置 .textContentType = .username。否则,QuickType 栏不会显示通行密钥建议。
  • 不要在请求上设置 allowedCredentials。AutoFill 需要显示该域名下的所有可用通行密钥。
  • 请求会一直保持有效,直到用户选择凭证、离开当前页面或取消请求。
模态(回退路径)

当用户明确轻点“登录”按钮,或你已知用户名并希望请求特定凭证时使用。

func signInWithModal(allowedCredentials: [ASAuthorizationPlatformPublicKeyCredentialDescriptor]? = nil) {
    let provider = ASAuthorizationPlatformPublicKeyCredentialProvider(
        relyingPartyIdentifier: "example.com"
    )

    let request = provider.createCredentialAssertionRequest(
        challenge: serverChallenge
    )

    if let allowedCredentials {
        request.allowedCredentials = allowedCredentials
    }

    let controller = ASAuthorizationController(authorizationRequests: [request])
    controller.delegate = self
    controller.presentationContextProvider = self
    controller.performRequests()
}

当你知道用户的凭证 ID 时使用 allowedCredentials(例如,用户输入了用户名,且你的服务器返回了其已注册的凭证 ID)。这会将通行密钥选择范围缩小到该账户。

处理断言响应
func authorizationController(controller: ASAuthorizationController,
                             didCompleteWithAuthorization authorization: ASAuthorization) {
    guard let credential = authorization.credential
        as? ASAuthorizationPlatformPublicKeyCredentialAssertion else { return }

    let signature = credential.signature
    let clientDataJSON = credential.rawClientDataJSON
    let authenticatorData = credential.rawAuthenticatorData
    let credentialID = credential.credentialID
    let userID = credential.userID

    // Send to server for verification
}

自动升级通行密钥(iOS 18+)

在不中断用户流程的情况下,静默将使用密码的用户升级为通行密钥。系统会显示简短通知,确认升级已完成——无需模态界面,也无需额外轻点。

工作原理

当用户使用密码登录时,系统可以为同一账户自动创建通行密钥。以下条件必须同时满足:

  1. 凭证管理器支持自动升级
  2. 用户刚刚使用密码成功验证了同一账户
  3. 你的应用请求条件式注册
实现
func requestAutomaticUpgrade(challenge: Data, userName: String, userID: Data) {
    let provider = ASAuthorizationPlatformPublicKeyCredentialProvider(
        relyingPartyIdentifier: "example.com"
    )

    let request = provider.createCredentialRegistrationRequest(
        challenge: challenge,
        name: userName,
        userID: userID
    )
    request.requestStyle = .conditional

    let controller = ASAuthorizationController(authorizationRequests: [request])
    controller.delegate = self
    controller.presentationContextProvider = self
    controller.performAutoFillAssistedRequests()
}

关键细节:.requestStyle = .conditional 会使注册以机会式方式进行。条件满足时会静默成功,不满足时会静默失败。不要将失败回调视为错误——这表示此次未满足条件。

调用时机:用户使用密码成功验证身份后。先检查用户是否已为此账户设置通行密钥——如果已设置,就不要请求升级。

服务器要求

你的服务器必须能够处理在密码登录后不久到达的异步注册。userID 和 challenge 必须有效,并且与该会话相关联。

组合凭证请求

迁移期间,用户可能使用通行密钥、密码或“通过 Apple 登录”凭证。一个 ASAuthorizationController 即可处理这三种凭证。

func signInWithCombinedRequest() {
    var requests: [ASAuthorizationRequest] = []

    // Passkey assertion
    let passkeyProvider = ASAuthorizationPlatformPublicKeyCredentialProvider(
        relyingPartyIdentifier: "example.com"
    )
    requests.append(
        passkeyProvider.createCredentialAssertionRequest(challenge: serverChallenge)
    )

    // Password
    let passwordProvider = ASAuthorizationPasswordProvider()
    requests.append(passwordProvider.createRequest())

    // Sign in with Apple
    let appleIDProvider = ASAuthorizationAppleIDProvider()
    requests.append(appleIDProvider.createRequest())

    let controller = ASAuthorizationController(authorizationRequests: requests)
    controller.delegate = self
    controller.presentationContextProvider = self
    controller.performAutoFillAssistedRequests()
}
处理多种凭证类型
func authorizationController(controller: ASAuthorizationController,
                             didCompleteWithAuthorization authorization: ASAuthorization) {
    switch authorization.credential {
    case let credential as ASAuthorizationPlatformPublicKeyCredentialAssertion:
        // Passkey sign-in — verify with server
        handlePasskeyAssertion(credential)

    case let credential as ASPasswordCredential:
        // Password sign-in — verify, then offer passkey upgrade
        handlePasswordSignIn(credential)

    case let credential as ASAuthorizationAppleIDCredential:
        // Apple ID sign-in
        handleAppleIDSignIn(credential)

    default:
        break
    }
}

密码登录成功后,调用自动升级流程,逐步将用户迁移到通行密钥。

跨设备登录

用户可以使用手机作为身份验证器,在没有通行密钥的设备上登录。

工作原理:

  1. App 发起通行密钥断言请求
  2. 系统在发起登录请求的设备上显示二维码
  3. 用户用手机扫描二维码(手机上有通行密钥)
  4. 通过蓝牙近距离验证,确认手机就在附近
  5. 用户在手机上通过 Face ID/Touch ID 完成身份验证
  6. 断言在原设备上完成

无需更改 App。这是系统级功能。任何支持通行密钥的设备都可以作为跨设备身份验证器。通信通过 Apple 中继服务进行端到端加密。

需要蓝牙:两台设备都必须启用蓝牙。近距离检查可防止远程网络钓鱼——身份验证设备必须在发起请求的设备附近。

迁移策略

阶段 1 — 在保留密码的同时添加通行密钥支持

保留现有的密码身份验证。添加通行密钥注册和断言。使用组合凭证请求,让两种方式都能正常使用。

服务端更改:添加用于注册和断言的 WebAuthn 端点。将公钥与密码哈希一起存储。两种身份验证方式都验证到同一个用户会话。

App 更改:实现注册流程(在密码登录后提供)、断言流程(借助自动填充)和组合请求。

阶段 2 — 自动升级(iOS 18+)

在密码登录后添加条件注册请求。用户会逐步、无感地迁移到通行密钥。跟踪升级指标以衡量采用情况。

无需用户操作。系统会透明地处理升级。

阶段 3 — 减少易遭网络钓鱼的验证因素

对于使用通行密钥的账户,可考虑:

  • 移除密码重置流程(通行密钥不需要重置)
  • 停用短信双重验证(通行密钥本身就是双重验证:设备持有权 + 生物识别)
  • 提供通过另一台设备上的通行密钥恢复账户的方式,而不是通过电子邮件/短信恢复

不要强制移除密码。让用户自行选择是否改用无密码登录。有些用户需要在不支持通行密钥的设备上使用密码登录。

密码 App 集成(iOS 18+)

密码 App 会使用关联域名中的 OpenGraph 元数据来显示你 App 的名称和图标。在网站的 <head> 中添加:

<meta property="og:title" content="MyApp" />
<meta property="og:image" content="https://example.com/icon.png" />

用户的凭证管理器会以这种方式显示你的 App。如果没有这些元数据,密码 App 只会显示域名。

反驳借口

借口事实时间成本
“目前用密码就够了”每次密码登录都存在网络钓鱼风险。撞库攻击会造成实际经济损失——数据泄露的平均成本为 450 万美元。通行密钥可以彻底消除整个攻击面。持续承担风险,而实施只需 2-3 天
“以后再添加通行密钥”借助自动填充功能发起通行密钥请求,所需的集成工作量与自定义密码文本字段加自动填充相同。推迟实施并不能节省时间。无论何时实施,工作量都相同
“用户不会理解通行密钥”用户无需理解公钥加密。他们看到的是“使用 Face ID 登录”——点一下就行。Apple、Google 和 Microsoft 正在所有平台上将通行密钥设为默认方式。无需额外进行用户教育
“我们的服务器不支持 WebAuthn”所有主流后端(Python、Node、Go、Ruby、Java、.NET)都有服务端 WebAuthn 库。多数库都经过充分测试,并且在积极维护。服务端集成需 1-2 天
“没有生物识别功能的用户怎么办?”设备密码也是有效的用户验证方式。所有受支持的设备至少都有设备密码。将 userVerification 设为 "preferred",平台就会妥善处理。无需额外工作——平台会处理
“我们必须永久保留密码作为备用方式”组合凭据请求可以同时支持密码和通行密钥。使用自动升级逐步迁移。你可以无限期保留密码,同时让通行密钥成为主要方式。无需强制二选一——两种方式并行运行

应对质疑的场景

场景 1:“我们的用户还没准备好使用通行密钥”

背景:产品经理以用户可能感到困惑为由,反对采用通行密钥。

质疑:“我们的用户不懂技术。他们不会理解什么是通行密钥。我们先继续用密码,明年再添加通行密钥。”

事实:Apple 将通行密钥作为内置系统功能,提供给所有平台——iPhone、iPad、Mac、Apple Watch,以及通过跨设备认证使用的 Windows。用户会在 QuickType 栏中看到“使用 Face ID 登录”。他们不会看到“WebAuthn CTAP2 公钥凭据”。“密码”应用会透明地将通行密钥与密码一并管理。Apple 自家的账户系统、Google 账户和 Microsoft 账户都使用通行密钥。你的用户已经在其他地方使用它们了。

正确做法:实现组合凭据请求。现有密码用户继续使用密码登录。对于凭据管理器支持通行密钥的用户,系统会自动显示通行密钥。添加自动升级(iOS 18+),无需用户操作即可逐步迁移。

回应模板:“用户无需理解通行密钥。他们看到的是‘使用 Face ID 登录’——点一下就完成。Apple、Google 和 Amazon 已经在自家的登录流程中使用通行密钥。我们会在密码之外添加通行密钥,因此不会改变任何人的使用流程。自动获得通行密钥的用户会享受到更好的体验;其他用户则可以照常使用。”

场景 2:“先上线密码认证,之后再添加通行密钥”

背景:新应用面临截止日期压力。开发者想把通行密钥支持推迟到上线后的更新中。

压力:“我们得在周五前上线。密码认证已经能用了。下个迭代再添加通行密钥。”

实际情况:实现由 AutoFill 辅助的通行密钥请求,其工作量与构建一个完善的密码文本字段(支持 AutoFill、安全存储和“忘记密码”流程)相当。无论如何,你都要构建 ASAuthorizationController 集成——问题在于你要接入一个提供方(密码),还是三个(通行密钥 + 密码 + Apple ID)。组合请求只需在代理中增加约 30 行代码。

正确做法:从一开始就实现组合凭据请求。服务器需要 WebAuthn 端点,但客户端的工作几乎完全相同。从第一天起就支持通行密钥上线,意味着你永远不必事后补上它,也可以避免“下个迭代”变成“下个季度”。

反驳模板:“由 AutoFill 辅助的通行密钥使用的 ASAuthorizationController,与我们用于密码 AutoFill 的相同。添加通行密钥支持只需在代理中增加约 30 行代码,不需要一个迭代的工作量。不支持它就意味着我们现在构建密码流程,以后还要重建认证流程来添加通行密钥。我们一次性做好吧。”

检查清单

上线通行密钥认证前:

关联域:

  • 已将 webcredentials:yourdomain.com 添加到 Associated Domains 功能中
  • AASA 文件通过 HTTPS 在 /.well-known/apple-app-site-association 提供
  • AASA 文件包含正确的 Team ID 和 bundle identifier
  • AASA 文件路径没有重定向
  • AASA 更改已传播(或已为测试启用开发者模式)

注册:

  • 服务器为每次注册尝试生成唯一的质询
  • userID 不包含可识别信息(不是电子邮件或用户名)
  • relyingPartyIdentifier 与关联域完全匹配
  • 注册使用 performRequests()(模态方式——适用于显式创建)
  • 注册成功后,服务器会存储 credentialID 和公钥

断言(登录):

  • 用户名文本字段设置了 .textContentType = .username
  • 由 AutoFill 辅助的请求尽早调用(在字段获得焦点前)
  • AutoFill 路径使用 performAutoFillAssistedRequests()
  • 可通过 performRequests() 提供模态备用方案,用于显式登录按钮
  • 由 AutoFill 辅助的请求未设置 allowedCredentials
  • 服务器会验证签名、authenticatorData 和 clientDataJSON

组合请求(如果支持多种认证方式):

  • 包含通行密钥、密码和 Apple ID 提供方
  • 代理通过 switch 语句处理所有凭据类型
  • 密码登录会触发自动通行密钥升级流程

自动升级(iOS 18+):

  • 注册请求使用 .requestStyle = .conditional
  • 升级请求使用 performAutoFillAssistedRequests()
  • 将失败回调视为“未满足条件”,而不是错误
  • 请求升级前检查是否已有通行密钥

错误处理:

  • 妥善处理 ASAuthorizationError.canceled(用户已关闭——不是错误)
  • 记录带有上下文的 ASAuthorizationError.failed,以便调试
  • 服务器验证期间发生网络故障时,不会导致认证状态不一致

资源

WWDC:2022-10092、2024-10125

文档:/authenticationservices、/authenticationservices/public-private-key-authentication/supporting-passkeys

技能:axiom-keychain

评论

支持 Markdown