From 29ef147eb11a4d49400a3adcb8dfba90004f09c4 Mon Sep 17 00:00:00 2001 From: Eric Lewis Date: Wed, 19 Jun 2024 11:13:42 -0400 Subject: [PATCH 1/2] feat: make register / challenge async This uses continuations for register / challenge requests. Note: I did not update the example app to utilize these. --- Sources/Shared/PasskeyManager.swift | 140 +++++++++------------------- Sources/Shared/Stamper.swift | 52 +++-------- 2 files changed, 59 insertions(+), 133 deletions(-) diff --git a/Sources/Shared/PasskeyManager.swift b/Sources/Shared/PasskeyManager.swift index 7d73f6d..89d7268 100644 --- a/Sources/Shared/PasskeyManager.swift +++ b/Sources/Shared/PasskeyManager.swift @@ -2,19 +2,6 @@ import AuthenticationServices import Foundation import os -extension Notification.Name { - static let PasskeyManagerModalSheetCanceled = Notification.Name( - "PasskeyManagerModalSheetCanceledNotification") - static let PasskeyManagerError = Notification.Name("PasskeyManagerErrorNotification") - static let PasskeyRegistrationCompleted = Notification.Name( - "PasskeyRegistrationCompletedNotification") - static let PasskeyRegistrationFailed = Notification.Name("PasskeyRegistrationFailedNotification") - static let PasskeyRegistrationCanceled = Notification.Name( - "PasskeyRegistrationCanceledNotification") - static let PasskeyAssertionCompleted = Notification.Name( - "PasskeyAssertionCompletedNotification") -} - public struct Attestation { public let credentialId: String public let clientDataJson: String @@ -44,7 +31,8 @@ public class PasskeyManager: NSObject, ASAuthorizationControllerDelegate, { private let rpId: String private var presentationAnchor: ASPresentationAnchor? - private var isPerformingModalRequest = false + private var registrationContinuation: CheckedContinuation? + private var assertionContinuation: CheckedContinuation? /// Initializes a new instance of `PasskeyManager` with the specified relying party identifier and presentation anchor. /// - Parameters: @@ -58,45 +46,40 @@ public class PasskeyManager: NSObject, ASAuthorizationControllerDelegate, /// Initiates the registration of a new passkey. /// - Parameter email: The email address associated with the new passkey. - public func registerPasskey(email: String) { - - let challenge = generateRandomBuffer() - let userID = Data(UUID().uuidString.utf8) - - let publicKeyCredentialProvider = ASAuthorizationPlatformPublicKeyCredentialProvider( - relyingPartyIdentifier: rpId) - - let registrationRequest = publicKeyCredentialProvider.createCredentialRegistrationRequest( - challenge: challenge, - name: email.components(separatedBy: "@").first ?? "", - userID: userID - ) - - let authorizationController = ASAuthorizationController(authorizationRequests: [ - registrationRequest - ]) - authorizationController.delegate = self - authorizationController.presentationContextProvider = self - authorizationController.performRequests() - - isPerformingModalRequest = true + public func registerPasskey(email: String) async throws -> PasskeyRegistrationResult { + return try await withCheckedThrowingContinuation { continuation in + self.registrationContinuation = continuation + let challenge = generateRandomBuffer() + let userID = Data(UUID().uuidString.utf8) + + let publicKeyCredentialProvider = ASAuthorizationPlatformPublicKeyCredentialProvider(relyingPartyIdentifier: rpId) + let registrationRequest = publicKeyCredentialProvider.createCredentialRegistrationRequest( + challenge: challenge, + name: email.components(separatedBy: "@").first ?? "", + userID: userID + ) + + let authorizationController = ASAuthorizationController(authorizationRequests: [ + registrationRequest + ]) + authorizationController.delegate = self + authorizationController.presentationContextProvider = self + authorizationController.performRequests() + } } /// Initiates the assertion of a passkey using the specified challenge. /// - Parameter challenge: The challenge data used for passkey assertion. - public func assertPasskey(challenge: Data) { - let publicKeyCredentialProvider = ASAuthorizationPlatformPublicKeyCredentialProvider( - relyingPartyIdentifier: rpId) - - let assertionRequest = publicKeyCredentialProvider.createCredentialAssertionRequest( - challenge: challenge) - - let authController = ASAuthorizationController(authorizationRequests: [assertionRequest]) - authController.delegate = self - authController.presentationContextProvider = self - authController.performRequests() - - isPerformingModalRequest = true + public func assertPasskey(challenge: Data) async throws -> ASAuthorizationPlatformPublicKeyCredentialAssertion { + return try await withCheckedThrowingContinuation { continuation in + self.assertionContinuation = continuation + let publicKeyCredentialProvider = ASAuthorizationPlatformPublicKeyCredentialProvider(relyingPartyIdentifier: rpId) + let assertionRequest = publicKeyCredentialProvider.createCredentialAssertionRequest(challenge: challenge) + let authController = ASAuthorizationController(authorizationRequests: [assertionRequest]) + authController.delegate = self + authController.presentationContextProvider = self + authController.performRequests() + } } /// Generates a random buffer to be used as a challenge in passkey operations. @@ -133,7 +116,7 @@ public class PasskeyManager: NSObject, ASAuthorizationControllerDelegate, case let credentialRegistration as ASAuthorizationPlatformPublicKeyCredentialRegistration: guard let rawAttestationObject = credentialRegistration.rawAttestationObject else { - notifyRegistrationFailed(error: PasskeyRegistrationError.invalidAttestation) + registrationContinuation?.resume(throwing: PasskeyRegistrationError.invalidAttestation) return } @@ -141,7 +124,7 @@ public class PasskeyManager: NSObject, ASAuthorizationControllerDelegate, let clientDataJSON = try? JSONDecoder().decode( ClientDataJSON.self, from: credentialRegistration.rawClientDataJSON) else { - notifyRegistrationFailed(error: PasskeyRegistrationError.invalidClientDataJSON) + registrationContinuation?.resume(throwing: PasskeyRegistrationError.invalidClientDataJSON) return } @@ -158,16 +141,15 @@ public class PasskeyManager: NSObject, ASAuthorizationControllerDelegate, let registrationResult = PasskeyRegistrationResult( challenge: challenge, attestation: attestation) - notifyRegistrationCompleted(result: registrationResult) + registrationContinuation?.resume(returning: registrationResult) return case let credentialAssertion as ASAuthorizationPlatformPublicKeyCredentialAssertion: logger.log("A passkey was used to sign in: \(credentialAssertion)") - notifyPasskeyAssertionCompleted(result: credentialAssertion) + assertionContinuation?.resume(returning: credentialAssertion) default: - notifyPasskeyManagerError(error: PasskeyManagerError.unknownAuthorizationType) + assertionContinuation?.resume(throwing: PasskeyManagerError.unknownAuthorizationType) + registrationContinuation?.resume(throwing: PasskeyManagerError.unknownAuthorizationType) } - - isPerformingModalRequest = false } /// Handles the completion of an authorization request that ended with an error. @@ -183,22 +165,20 @@ public class PasskeyManager: NSObject, ASAuthorizationControllerDelegate, ) { let logger = Logger() guard let authorizationError = error as? ASAuthorizationError else { - isPerformingModalRequest = false logger.error("Unexpected authorization error: \(error.localizedDescription)") - notifyPasskeyManagerError(error: PasskeyManagerError.authorizationFailed(error)) + assertionContinuation?.resume(throwing: PasskeyManagerError.authorizationFailed(error)) + registrationContinuation?.resume(throwing: PasskeyManagerError.authorizationFailed(error)) return } if authorizationError.code == .canceled { - if isPerformingModalRequest { - notifyModalSheetCanceled() - } + registrationContinuation?.resume(throwing: CancellationError()) + assertionContinuation?.resume(throwing: CancellationError()) } else { logger.error("Error: \((error as NSError).userInfo)") - notifyPasskeyManagerError(error: PasskeyManagerError.authorizationFailed(error)) + assertionContinuation?.resume(throwing: PasskeyManagerError.authorizationFailed(error)) + registrationContinuation?.resume(throwing: PasskeyManagerError.authorizationFailed(error)) } - - isPerformingModalRequest = false } struct ClientDataJSON: Codable { @@ -211,36 +191,4 @@ public class PasskeyManager: NSObject, ASAuthorizationControllerDelegate, { return presentationAnchor! } - - // MARK: - Notifications - - private func notifyRegistrationCompleted(result: PasskeyRegistrationResult) { - NotificationCenter.default.post( - name: .PasskeyRegistrationCompleted, object: self, userInfo: ["result": result]) - } - - private func notifyRegistrationFailed(error: PasskeyRegistrationError) { - NotificationCenter.default.post( - name: .PasskeyRegistrationFailed, object: self, userInfo: ["error": error]) - } - - private func notifyRegistrationCanceled() { - NotificationCenter.default.post(name: .PasskeyRegistrationCanceled, object: self) - } - - private func notifyModalSheetCanceled() { - NotificationCenter.default.post(name: .PasskeyManagerModalSheetCanceled, object: self) - } - - private func notifyPasskeyManagerError(error: PasskeyManagerError) { - NotificationCenter.default.post( - name: .PasskeyManagerError, object: self, userInfo: ["error": error]) - } - - private func notifyPasskeyAssertionCompleted( - result: ASAuthorizationPlatformPublicKeyCredentialAssertion - ) { - NotificationCenter.default.post( - name: .PasskeyAssertionCompleted, object: self, userInfo: ["result": result]) - } } diff --git a/Sources/Shared/Stamper.swift b/Sources/Shared/Stamper.swift index 8006b69..2204455 100644 --- a/Sources/Shared/Stamper.swift +++ b/Sources/Shared/Stamper.swift @@ -7,7 +7,6 @@ public class Stamper { private let apiPrivateKey: String? private let presentationAnchor: ASPresentationAnchor? private let passkeyManager: PasskeyManager? - private var observer: NSObjectProtocol? // TODO: We will want to in the future create a Stamper super class // and then create subclasses APIKeyStamper, and PasskeyStamper @@ -83,42 +82,21 @@ public class Stamper { /// - Returns: A JSON string representing the stamp. /// - Throws: `PasskeyStampError` on failure. public func passkeyStamp(payload: SHA256Digest) async throws -> String { - return try await withCheckedThrowingContinuation { continuation in - self.observer = NotificationCenter.default.addObserver( - forName: .PasskeyAssertionCompleted, object: nil, queue: nil - ) { [weak self] notification in - guard let self = self else { return } - NotificationCenter.default.removeObserver(self.observer!) - self.observer = nil - - if let assertionResult = notification.userInfo?["result"] - as? ASAuthorizationPlatformPublicKeyCredentialAssertion - { - // Construct the result from the assertion - let assertionInfo = [ - "authenticatorData": assertionResult.rawAuthenticatorData.base64URLEncodedString(), - "clientDataJson": assertionResult.rawClientDataJSON.base64URLEncodedString(), - "credentialId": assertionResult.credentialID.base64URLEncodedString(), - "signature": assertionResult.signature.base64URLEncodedString(), - ] - - do { - let jsonData = try JSONSerialization.data(withJSONObject: assertionInfo, options: []) - if let jsonString = String(data: jsonData, encoding: .utf8) { - continuation.resume(returning: jsonString) - } - } catch { - continuation.resume(throwing: error) - } - } else if let error = notification.userInfo?["error"] as? Error { - continuation.resume(throwing: error) - } else { - continuation.resume(throwing: StampError.assertionFailed) - } - } - - self.passkeyManager?.assertPasskey(challenge: Data(payload)) - } + guard let passkeyManager else { throw StampError.assertionFailed } + let assertionResult = try await passkeyManager.assertPasskey( + challenge: payload.compactMap { String(format: "%02x", $0) }.joined().data(using: .utf8)! + ) + + let assertionInfo = [ + "authenticatorData": assertionResult.rawAuthenticatorData.base64URLEncodedString(), + "clientDataJson": assertionResult.rawClientDataJSON.base64URLEncodedString(), + "credentialId": assertionResult.credentialID.base64URLEncodedString(), + "signature": assertionResult.signature.base64URLEncodedString(), + ] + + let jsonData = try JSONSerialization.data(withJSONObject: assertionInfo, options: []) + guard let result = String(data: jsonData, encoding: .utf8) else { throw StampError.assertionFailed } + return result } public enum APIKeyStampError: Error { From 4cbd6797639f331228df5b6ca6c67f37d03fa24a Mon Sep 17 00:00:00 2001 From: Eric Lewis Date: Fri, 21 Jun 2024 13:46:56 -0400 Subject: [PATCH 2/2] make stamp error public so we can handle cancellations --- Sources/Middleware/AuthStampMiddleware.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Middleware/AuthStampMiddleware.swift b/Sources/Middleware/AuthStampMiddleware.swift index 206ea63..7bc1c1a 100644 --- a/Sources/Middleware/AuthStampMiddleware.swift +++ b/Sources/Middleware/AuthStampMiddleware.swift @@ -12,7 +12,7 @@ package struct AuthStampMiddleware { } } -enum AuthStampError: Error { +public enum AuthStampError: Error { case failedToStampAndSendRequest(String, Error) }