如何使用 WebAuthn 将 iOS 应用程序中的 Passkey 与自定义 Python 后端集成

问题描述 投票:0回答:1

简介
我有自己的后端服务 API,可使用 Flask 框架和 py_webauthn 库(https://github.com/duo-labs/py_webauthn)编写 Python 编写的 Android 和 iOS 应用程序。

我成功构建了一个相互通信的 Android 应用程序和 API,允许用户在登录时注册新的密钥并验证密钥。

现在我正在针对 iOS 16.4 或更高版本的 Xcode 15.4 中使用 SwiftUI 对 iOS 重复此过程。为此,我查看了 Apple Food Truck 应用程序示例(请参阅:https://developer.apple.com/documentation/swiftui/food_truck_building_a_swiftui_multiplatform_app)。

Android 的密钥注册流程
以下步骤在 Android 上运行良好:

  1. 用户想要注册新的密钥
  2. 设备向服务器请求挑战
  3. 服务器接收请求并使用收到的用户电子邮件作为名称,使用全名作为显示名称,并生成一个唯一的 ID 作为用户 ID,以使用 WebAuthngenerate_registration_options 生成质询
  4. WebAuth 生成的选项以 json 形式发送回设备
  5. 设备根据收到的包含质询的选项创建公钥凭证请求
  6. 用户看到设备要求确认生成基于生物识别(或其他)的密钥,并在设备上完成密钥创建过程
  7. 设备向服务器发送注册响应json
  8. 服务器使用 WebAuth verify_registration_response 函数验证收到的注册响应。它使用收到的注册响应作为输入,即使从步骤 3 生成的质询作为预期挑战,预期来源格式为“android:apk-key-hash:8363ghjjhg_blablabla_87234765^^%”
  9. 服务器会回复设备验证是否成功。

问题
当我尝试在 iOS 应用程序中实现相同的步骤时,我陷入了步骤 7 和 8。WebAuthn validate_registration_response 总是返回 “客户端数据挑战不是预期的挑战”

在步骤 7 和 8 中,我发现 Google 和 Apple 实施这些步骤的方式存在差异。首先,Android 接受 WebAuthn 的generate_registration_options 函数生成的完整选项 json。对于 iOS,我似乎必须从这个响应中删除挑战并将其传递给:

    @Environment(\.authorizationController) private var authorizationController

let authorizationResult = try await authorizationController.performRequests(
                [passkeyRegistrationRequest(passkeyChallenge: passkeyChallenge, username: username, name: name)],
                    options: options)

private func passkeyRegistrationRequest(passkeyChallenge: Data, username: String, name: String) async -> ASAuthorizationRequest {
        ASAuthorizationPlatformPublicKeyCredentialProvider(relyingPartyIdentifier: Self.relyingPartyIdentifier)
           .createCredentialRegistrationRequest(challenge: passkeyChallenge, name: name, userID: Data(username.utf8))
    }

描述的步骤 5 和 6 在此代码中处理,但是当我在代码中结束于步骤 7 时:

private func handleAuthorizationResult(_ authorizationResult: ASAuthorizationResult, username: String? = nil) async throws -> String {
        switch authorizationResult {
        case let .password(passwordCredential):
            // Unused
            Logger.authorization.log("Password authorization succeeded: \(passwordCredential)")
        case let .passkeyAssertion(passkeyAssertion):
            // Unused
            // The login was successful.
            Logger.authorization.log("Passkey authorization succeeded: \(passkeyAssertion)")
        case let .passkeyRegistration(passkeyRegistration):
            // The registration was successful.
            Logger.authorization.log("Passkey registration succeeded: \(passkeyRegistration")
                        
            guard let attestationObject = passkeyRegistration.rawAttestationObject else { return "" }
            let clientDataJSON = passkeyRegistration.rawClientDataJSON
            let credentialID = passkeyRegistration.credentialID
            
            // Build the attestaion object
            let payload = ["rawId": credentialID.base64URLEncode(), // .base64EncodedString(), // Base64
                           "id": passkeyRegistration.credentialID.base64URLEncode(),
                           "authenticatorAttachment": "platform", // Optional parameter
                           "clientExtensionResults": [String: Any](), // Optional parameter
                           "type": "public-key",
                           "response": [
                            "attestationObject": attestationObject.base64EncodedString(),
                            "clientDataJSON": clientDataJSON.base64EncodedString()
                           ]
            ] as [String: Any]
            
            var payloadJSONText = ""
            if let payloadJSONData = try? JSONSerialization.data(withJSONObject: payload, options: .fragmentsAllowed) {
                payloadJSONText = String(data: payloadJSONData, encoding: .utf8) ?? ""
            }

            Logger.authorization.log("Passkey registration succeeded 2: \(payloadJSONText)")
            return payloadJSONText
        default:
            Logger.authorization.error("Received an unknown authorization result.")
            // Throw an error and return to the caller.
            throw AuthorizationHandlingError.unknownAuthorizationResult(authorizationResult)
        }
        return ""
    }

case let .passkeyRegistration(passkeyRegistration):
中,我将返回的有效负载发送回服务器,并应使用 WebAuthn verify_registration_response 函数对其进行验证。但我已经能够让它发挥作用了。

这里的区别是,我可以看到Android的expected_origin为“android:apk-key-hash:8363ghjjhg_blablabla_87234765^^%”,而对于iOS来说,origin似乎是我使用的服务器的URL格式来创造挑战,但我不确定。

到目前为止我尝试过的事情
我还认为可能存在以下问题:WebAuthn 应该期望采用 base64url 格式而不是 base64 格式的内容,尝试了该字段的几种格式组合,但没有成功。

到目前为止,我在互联网上找不到任何可以帮助我解决此问题的内容。我能找到的所有内容都是使用 iOS 库或 WebAuthn 互联网服务,但这不是我想要的。

服务器上的验证似乎不是其他人在注册新密钥时所做的事情。我在这里尝试做不可能的事吗?

因此,我什至还没有开始登录用户,所以目前我无法判断是否存在任何问题。

谁能帮助我走向正确的方向?

ios swiftui webauthn passkey
1个回答
0
投票

好吧,我想通了,现在 iOS 也可以使用了。我不是 100% 相信我的解决方案应该是这样,但它似乎有效。

我必须在这里指出,我似乎被 Google 的实现方式宠坏了。在 iOS 上,密钥实现似乎自动化程度较低,您必须自己做更多事情。

所以我犯的第一个错误是我没有对从服务器收到的质询进行 Base64URL 解码。我创建的有效负载方向正确,最终得到:

let payload = ["rawId": passkeyRegistration.credentialID.base64URLEncode(),
               "id": passkeyRegistration.credentialID.base64URLEncode(),
               "authenticatorAttachment": "platform",
               "clientExtensionResults": [String: Any](),
               "type": "public-key",
               "response": [
                   "transports": ["internal"],
                   "attestationObject": passkeyRegistration.rawAttestationObject!.base64URLEncode(),
                   "clientDataJSON": passkeyRegistration.rawClientDataJSON.base64URLEncode()
                   ]
               ] as [String: Any]

在服务器端,我的预期来源应设置为

https://www.example.com
(或任何您的站点地址)。

使用此功能,WebAuth verify_registration_response 函数现在可以正确验证响应,并且一切正常。

我还实现了执行登录的步骤,通过从上述步骤中吸取的经验教训,我也能够完成登录。以下是使用 WebAuth verify_authentication_response 函数登录所需的负载。

let payload = ["rawId": passkeyAssertion.credentialID.base64URLEncode(),
               "id": passkeyAssertion.credentialID.base64URLEncode(),
               "authenticatorAttachment": "platform",
               "clientExtensionResults": [String: Any](),
               "type": "public-key",
               "response": [
                   "clientDataJSON": passkeyAssertion.rawClientDataJSON.base64URLEncode()
                   "authenticatorData": passkeyAssertion.rawAuthenticatorData.base64URLEncode(),
                   "signature": passkeyAssertion.signature.base64URLEncode(),
                   "userHandle": passkeyAssertion.userID.base64URLEncode()
                   ]
               ] as [String: Any]

希望这对其他人也有帮助。如果我不够清楚,请告诉我。

© www.soinside.com 2019 - 2024. All rights reserved.