127 lines
4.6 KiB
Swift
127 lines
4.6 KiB
Swift
import AuthenticationServices
|
|
import Foundation
|
|
|
|
#if os(macOS)
|
|
import AppKit
|
|
#else
|
|
import UIKit
|
|
#endif
|
|
|
|
@MainActor
|
|
final class PasskeyManager: NSObject {
|
|
private var continuation: CheckedContinuation<ASAuthorization, Error>?
|
|
|
|
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
|
|
}
|
|
}
|
|
|