Initial commit
This commit is contained in:
126
apple-client/Sources/App/PasskeyManager.swift
Normal file
126
apple-client/Sources/App/PasskeyManager.swift
Normal 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
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user