Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Sources/Middleware/AuthStampMiddleware.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ package struct AuthStampMiddleware {
}
}

enum AuthStampError: Error {
public enum AuthStampError: Error {
case failedToStampAndSendRequest(String, Error)
}

Expand Down
140 changes: 44 additions & 96 deletions Sources/Shared/PasskeyManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<PasskeyRegistrationResult, Error>?
private var assertionContinuation: CheckedContinuation<ASAuthorizationPlatformPublicKeyCredentialAssertion, Error>?

/// Initializes a new instance of `PasskeyManager` with the specified relying party identifier and presentation anchor.
/// - Parameters:
Expand All @@ -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.
Expand Down Expand Up @@ -133,15 +116,15 @@ 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
}

guard
let clientDataJSON = try? JSONDecoder().decode(
ClientDataJSON.self, from: credentialRegistration.rawClientDataJSON)
else {
notifyRegistrationFailed(error: PasskeyRegistrationError.invalidClientDataJSON)
registrationContinuation?.resume(throwing: PasskeyRegistrationError.invalidClientDataJSON)
return
}

Expand All @@ -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.
Expand All @@ -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 {
Expand All @@ -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])
}
}
52 changes: 15 additions & 37 deletions Sources/Shared/Stamper.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down