import AuthenticationServices import Foundation #if os(macOS) import AppKit #else import UIKit #endif @MainActor final class PasskeyManager: NSObject { private var continuation: CheckedContinuation? func register(options: RegistrationOptionsResponse) async throws -> PasskeyRegistrationPayload { guard let challengeData = Data(base64URLEncoded: options.challenge), let userData = Data(base64URLEncoded: options.user.id) else { throw APIErrorResponse(message: "The backend returned malformed access key registration options.") } let provider = ASAuthorizationPlatformPublicKeyCredentialProvider(relyingPartyIdentifier: options.rp.id) let clientData = ASPublicKeyCredentialClientData( challenge: challengeData, origin: options.expectedOrigin ?? "http://localhost:4200" ) let request = provider.createCredentialRegistrationRequest( clientData: clientData, name: options.user.name, userID: userData ) let authorization = try await perform(requests: [request]) guard let registration = authorization.credential as? ASAuthorizationPlatformPublicKeyCredentialRegistration else { throw APIErrorResponse(message: "The platform did not return a valid access key registration.") } return PasskeyRegistrationPayload( id: registration.credentialID.base64URLEncodedString(), rawId: registration.credentialID.base64URLEncodedString(), response: .init( clientDataJSON: registration.rawClientDataJSON.base64URLEncodedString(), attestationObject: registration.rawAttestationObject?.base64URLEncodedString() ?? "", transports: nil ), clientExtensionResults: [:], type: "public-key" ) } func authenticate(options: AuthenticationOptionsResponse) async throws -> PasskeyAuthenticationPayload { guard let challengeData = Data(base64URLEncoded: options.challenge) else { throw APIErrorResponse(message: "The backend returned malformed access key sign-in options.") } let rpId = options.rpId ?? URL(string: options.expectedOrigin ?? "")?.host ?? "localhost" let provider = ASAuthorizationPlatformPublicKeyCredentialProvider(relyingPartyIdentifier: rpId) let clientData = ASPublicKeyCredentialClientData( challenge: challengeData, origin: options.expectedOrigin ?? "http://localhost:4200" ) let request = provider.createCredentialAssertionRequest(clientData: clientData) let authorization = try await perform(requests: [request]) guard let assertion = authorization.credential as? ASAuthorizationPlatformPublicKeyCredentialAssertion else { throw APIErrorResponse(message: "The platform did not return a valid access key assertion.") } return PasskeyAuthenticationPayload( id: assertion.credentialID.base64URLEncodedString(), rawId: assertion.credentialID.base64URLEncodedString(), response: .init( clientDataJSON: assertion.rawClientDataJSON.base64URLEncodedString(), authenticatorData: assertion.rawAuthenticatorData.base64URLEncodedString(), signature: assertion.signature.base64URLEncodedString(), userHandle: assertion.userID.base64URLEncodedString() ), clientExtensionResults: [:], type: "public-key" ) } private func perform(requests: [ASAuthorizationRequest]) async throws -> ASAuthorization { try await withCheckedThrowingContinuation { continuation in self.continuation = continuation let controller = ASAuthorizationController(authorizationRequests: requests) controller.delegate = self controller.presentationContextProvider = self controller.performRequests() } } } extension PasskeyManager: ASAuthorizationControllerDelegate { func authorizationController( controller _: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization ) { continuation?.resume(returning: authorization) continuation = nil } func authorizationController( controller _: ASAuthorizationController, didCompleteWithError error: Error ) { continuation?.resume(throwing: error) continuation = nil } } extension PasskeyManager: ASAuthorizationControllerPresentationContextProviding { func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor { #if os(macOS) return NSApplication.shared.keyWindow ?? NSApplication.shared.windows.first ?? ASPresentationAnchor() #else return UIApplication.shared.connectedScenes .compactMap { $0 as? UIWindowScene } .flatMap(\.windows) .first(where: \.isKeyWindow) ?? ASPresentationAnchor() #endif } }