You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

服务器获取的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编码不一致。
  • 修正步骤:
    1. 实现URL安全Base64字符串到Data的解码逻辑(需要将URL安全字符替换为标准Base64字符,并补全必要的=填充符)
    2. 替换原来错误的challengeData生成代码

修正后的代码

首先添加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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.17 17:40:31