Initial commit

This commit is contained in:
2026-03-09 19:35:08 +01:00
commit f6b790a515
64 changed files with 18778 additions and 0 deletions

View File

@@ -0,0 +1,126 @@
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
}
}