Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Challenge fetched from sever does not match challenge created by ASAuthorizationPlatformPublicKeyCredentialProvider credential creation request

I followed WWDC 2022 passkeys video and I am trying to register a passkey for my service in iOS as described in that video.

Below is the function where I obtain challenge from server and then make use of ASAuthorizationPlatformPublicKeyCredentialProvider to generate 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()
}

This works and I am able to obtain challenge and initiate local biometric authentication on iPhone to generate passkey for the service for given username.

Below is console print of challenge received from server :-

{
   "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
      }
   ]
}

However, in the delegate method when I decode the cliendDataJSON object, the challenge value is different.

Below is the delegate method handling :-

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
    }

The print in the delegate method outputs :-

{
   "type":"webauthn.create",
   "challenge":"ZlMtbWZ5amIzX3NCamdVMlgzeHA5OWp4ZEZjTlZxMmwxWW4tMDk3RldMOA",
   "origin":"https://<domain>"
}

What is it that I am doing wrong here? Why are the challenge values different?

like image 518
letsbondiway Avatar asked Jan 29 '26 17:01

letsbondiway


1 Answers

I think you're missing a base64url-decode before passing the challenge to the API. The challenge in the resulting clientDataJSON is base64url encoded itself:

ZlMtbWZ5amIzX3NCamdVMlgzeHA5OWp4ZEZjTlZxMmwxWW4tMDk3RldMOA

If we base64url-decode that, we get:

fS-mfyjb3_sBjgU2X3xp99jxdFcNVq2l1Yn-097FWL8

Which is the challenge from your server. So you should base64url-decode the challenge from the server yourself and pass a binary challenge into the API. Then the value in the clientDataJSON should match what you expect.

like image 109
agl Avatar answered Feb 01 '26 12:02

agl



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!