1

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?

letsbondiway
  • 470
  • 3
  • 18

1 Answers1

1

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.

agl
  • 1,129
  • 5
  • 6
  • Hi, if server challenge is base64decoded of API challenge, should not it be other way around? As in, server challenge should be base64encoded, not base64decoded before passing to API? Anyway, your answer was perfect in giving me the direction as I never thought it from base64 perspective. I did not do any change to server challenge. I base64decoded API challenge before sending for server verification. For me, FIDO credential registration was earlier failing due to mismatching challenges. With this change, verification is successful and I am able to generate and authenticate with credential. – letsbondiway Oct 08 '22 at 15:17
  • The WebAuthn API expects a binary challenge, while your server is giving you back (text) JSON. The server is most likely giving you a base64url-encoded string holding a binary challenge. You'll need to decode that; I don't believe base64url decoding is standard on the platform, but there are plenty of one-pager extensions to Data out there to add it. – David Waite Oct 12 '22 at 04:50