服务器获取的Challenge与ASAuthorization生成的不匹配问题
问题
我按照WWDC 2022的Passkeys视频教程,尝试在iOS端为我的服务注册Passkey。流程可以正常获取服务器返回的Challenge并触发iPhone本地生物识别验证生成Passkey,但在代理方法中解码clientDataJSON对象时,发现其中的Challenge值与服务器返回的不一致。
注册Passkey的实现函数
func signUpWith(userName: String, anchor: ASPresentationAnchor) { self.authenticationAnchor = anchor self.userName = userName let publicKeyCredentialProvider = ASAuthorizationPlatformPublicKeyCredentialProvider(relyingPartyIdentifier: self.domain) // Fetch the challenge from the server. The challenge needs to be unique for each request. // The userID is the identifier for the user's account. var urlRequst = URLRequest(url: URL(string: "https://<domain>/registration")!) urlRequst.httpMethod = "POST" urlRequst.setValue("application/json", forHTTPHeaderField: "Content-Type") do { let httpBody = try JSONSerialization.data(withJSONObject: ["registration": ["username": userName, "nickname": userName]], options: []) urlRequst.httpBody = httpBody } catch let error { print(error) } let urlSession = URLSession(configuration: .default) var task: URLSessionDataTask? task = urlSession.dataTask(with: urlRequst) { data, response, error in let challengeJson = try? JSONDecoder().decode(Challenge.self, from: data!) let challengeString = challengeJson!.challenge let userIdString = challengeJson!.user.id let challengeData = Data(challengeString.utf8) let userID = Data(userIdString.utf8) let registrationRequest = publicKeyCredentialProvider.createCredentialRegistrationRequest(challenge: challengeData, name: userName, userID: userID) // Use only ASAuthorizationPlatformPublicKeyCredentialRegistrationRequests or // ASAuthorizationSecurityKeyPublicKeyCredentialRegistrationRequests here. let authController = ASAuthorizationController(authorizationRequests: [ registrationRequest ] ) authController.delegate = self authController.presentationContextProvider = self authController.performRequests() self.isPerformingModalReqest = true } task?.resume() }
服务器返回的Challenge
{ "challenge":"fS-mfyjb3_sBjgU2X3xp99jxdFcNVq2l1Yn-097FWL8", "timeout":120000, "rp":{ "name":"Passkeys demo app" }, "user":{ "name":"letsbondiway", "id":"EU1BXzOQUYAE0_WbIM1LEdbhE2Y7tA-o8-gl6P27mAe_cV-Q3xKxFovyOV5cY_0kJm1z_mvOHft1AKE2AaW1sQ", "displayName":"letsbondiway" }, "pubKeyCredParams":[ { "type":"public-key", "alg":-7 }, { "type":"public-key", "alg":-37 }, { "type":"public-key", "alg":-257 } ] }
代理方法实现
func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) { let logger = Logger() switch authorization.credential { case let credentialRegistration as ASAuthorizationPlatformPublicKeyCredentialRegistration: logger.log("A new passkey was registered: \(credentialRegistration)") // Verify the attestationObject and clientDataJSON with your service. // The attestationObject contains the user's new public key to store and use for subsequent sign-ins. let attestationObject = credentialRegistration.rawAttestationObject let clientDataJSON = credentialRegistration.rawClientDataJSON let credentialId = credentialRegistration.credentialID print(String(data: clientDataJSON, encoding: .utf8) as Any) // After the server verifies the registration and creates the user account, sign in the user with the new account. didFinishSignIn() case let credentialAssertion as ASAuthorizationPlatformPublicKeyCredentialAssertion: logger.log("A passkey was used to sign in: \(credentialAssertion)") // Verify the below signature and clientDataJSON with your service for the given userID. // let signature = credentialAssertion.signature // let clientDataJSON = credentialAssertion.rawClientDataJSON // let userID = credentialAssertion.userID // After the server verifies the assertion, sign in the user. didFinishSignIn() case let passwordCredential as ASPasswordCredential: logger.log("A password was provided: \(passwordCredential)") // Verify the userName and password with your service. // let userName = passwordCredential.user // let password = passwordCredential.password // After the server verifies the userName and password, sign in the user. didFinishSignIn() default: fatalError("Received unknown authorization type.") } isPerformingModalReqest = false }
代理方法打印的clientDataJSON内容
{ "type":"webauthn.create", "challenge":"ZlMtbWZ5amIzX3NCamdVMlgzeHA5OWp4ZEZjTlZxMmwxWW4tMDk3RldMOA", "origin":"https://<domain>" }
解决方案
- 问题根源:服务器返回的Challenge是URL安全的Base64编码字符串,但你直接将其当作普通UTF-8字符串转成了
Data。这相当于把编码后的字符串本身当作了原始挑战数据,而非解码出服务器生成的原始二进制挑战值。系统在生成clientDataJSON时,会把你传入的错误数据重新编码为URL安全Base64,所以最终结果和服务器返回的原始Challenge编码不一致。 - 修正步骤:
- 实现URL安全Base64字符串到
Data的解码逻辑(需要将URL安全字符替换为标准Base64字符,并补全必要的=填充符) - 替换原来错误的
challengeData生成代码
- 实现URL安全Base64字符串到
修正后的代码
首先添加Data的扩展用于URL安全Base64解码:
extension Data { init?(base64URLEncoded string: String) { var base64 = string .replacingOccurrences(of: "-", with: "+") .replacingOccurrences(of: "_", with: "/") // 补全Base64所需的填充符 let paddingCount = base64.count % 4 if paddingCount > 0 { base64.append(String(repeating: "=", count: 4 - paddingCount)) } self.init(base64Encoded: base64) } }
然后修改注册函数中的challengeData生成部分:
// 替换原来的let challengeData = Data(challengeString.utf8) guard let challengeData = Data(base64URLEncoded: challengeString) else { print("Failed to decode challenge from base64URL format") return }
这样处理后,传入createCredentialRegistrationRequest的challengeData就是服务器生成的原始二进制挑战值,系统在生成clientDataJSON时会将其重新编码为URL安全Base64,此时就会和服务器返回的Challenge字符串完全一致了。
内容的提问来源于stack exchange,提问作者letsbondiway
相关产品推荐
相关产品推荐

