简介
我有自己的后端服务 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 上运行良好:
问题
当我尝试在 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 也可以使用了。我不是 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]
希望这对其他人也有帮助。如果我不够清楚,请告诉我。