From 668390a8bcf8b5fc1b1b9a520d1bc87a4cb94b26 Mon Sep 17 00:00:00 2001 From: Eric Stern Date: Mon, 22 Jul 2024 18:33:18 -0700 Subject: [PATCH 1/5] Update package.swift tools version --- Package.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Package.swift b/Package.swift index aa5694a..f7f3429 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version: 5.10 +// swift-tools-version: 6.0 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription From 5a411e656ba8740262fe358a12c1ed13b7e4cbae Mon Sep 17 00:00:00 2001 From: Eric Stern Date: Tue, 23 Jul 2024 08:15:01 -0700 Subject: [PATCH 2/5] Set language version to 6 --- Package.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Package.swift b/Package.swift index f7f3429..f3fa273 100644 --- a/Package.swift +++ b/Package.swift @@ -24,7 +24,8 @@ let package = Package( .target( name: "SnapAuth", swiftSettings: [ - .define("HARDWARE_KEY_SUPPORT", .when(platforms: [.iOS, .macOS])) + .define("HARDWARE_KEY_SUPPORT", .when(platforms: [.iOS, .macOS])), + .swiftLanguageVersion(.v6), ]), .testTarget( name: "SnapAuthTests", From dafd8adc211b3ec05d2c10080cf7bdd4d91fb3f2 Mon Sep 17 00:00:00 2001 From: Eric Stern Date: Tue, 23 Jul 2024 08:27:10 -0700 Subject: [PATCH 3/5] Confirm Authenticator to Sendable --- Sources/SnapAuth/SnapAuth.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/SnapAuth/SnapAuth.swift b/Sources/SnapAuth/SnapAuth.swift index f578372..313235b 100644 --- a/Sources/SnapAuth/SnapAuth.swift +++ b/Sources/SnapAuth/SnapAuth.swift @@ -44,7 +44,7 @@ public class SnapAuth: NSObject { // NSObject for ASAuthorizationControllerDeleg } /// Permitted authenticator types - public enum Authenticator: CaseIterable { + public enum Authenticator: CaseIterable, Sendable { /// Allow all available authenticator types to be used public static let all = Set(Authenticator.allCases) From 9e94fb12e216c6fc6e3ec535752a39119ef4d5a1 Mon Sep 17 00:00:00 2001 From: Eric Stern Date: Sun, 28 Jul 2024 13:14:39 -0700 Subject: [PATCH 4/5] Add some @MainActor annotations --- Sources/SnapAuth/Errors.swift | 22 +++++++++ Sources/SnapAuth/PresentationAnchor.swift | 22 +++++---- Sources/SnapAuth/SnapAuth+ASACD.swift | 55 +++++++++-------------- Sources/SnapAuth/SnapAuth.swift | 3 +- 4 files changed, 59 insertions(+), 43 deletions(-) diff --git a/Sources/SnapAuth/Errors.swift b/Sources/SnapAuth/Errors.swift index 808c1cc..06d655e 100644 --- a/Sources/SnapAuth/Errors.swift +++ b/Sources/SnapAuth/Errors.swift @@ -1,3 +1,5 @@ +import AuthenticationServices + public enum SnapAuthError: Error { /// The network request was disrupted. This is generally safe to retry. case networkInterruption @@ -54,4 +56,24 @@ public enum SnapAuthError: Error { // (Usage unknown, Apple docs are not clear) case notInteractive + + /// Registration matched an excluded credential. Typically this means that + /// the credential has already been registered. + case matchedExcludedCredential +} + +/// Extension to standardize converstion of AS error codes into SnapAuth codes +extension ASAuthorizationError.Code { + var snapAuthError: SnapAuthError { + switch self { + case .canceled: return .canceled + case .failed: return .failed + case .unknown: return .unknown + case .invalidResponse: return .invalidResponse + case .notHandled: return .notHandled + case .notInteractive: return .notInteractive + case .matchedExcludedCredential: return .matchedExcludedCredential + @unknown default: return .unknown + } + } } diff --git a/Sources/SnapAuth/PresentationAnchor.swift b/Sources/SnapAuth/PresentationAnchor.swift index 6ddc55d..52fb7bd 100644 --- a/Sources/SnapAuth/PresentationAnchor.swift +++ b/Sources/SnapAuth/PresentationAnchor.swift @@ -1,14 +1,20 @@ import AuthenticationServices +extension ASPresentationAnchor { + /// A platform-specific anchor, intended to be used by ASAuthorizationController + static var `default`: ASPresentationAnchor { #if os(macOS) -// FIXME: Figure out better fallback mechanisms here. -// This will cause a new window to open _and remain open_ -fileprivate let defaultPresentationAnchor: ASPresentationAnchor = NSApplication.shared.mainWindow ?? ASPresentationAnchor() + // FIXME: Figure out better fallback mechanisms here. + // This will cause a new window to open _and remain open_ + return NSApplication.shared.mainWindow ?? ASPresentationAnchor() #else -fileprivate let defaultPresentationAnchor: ASPresentationAnchor = (UIApplication.shared.connectedScenes.first as? UIWindowScene)?.windows.first?.rootViewController?.view.window ?? ASPresentationAnchor() + return (UIApplication.shared.connectedScenes.first as? UIWindowScene)? + .windows + .first? + .rootViewController? + .view + .window + ?? ASPresentationAnchor() #endif - -extension ASPresentationAnchor { - /// A platform-specific anchor, intended to be used by ASAuthorizationController - static let `default` = defaultPresentationAnchor + } } diff --git a/Sources/SnapAuth/SnapAuth+ASACD.swift b/Sources/SnapAuth/SnapAuth+ASACD.swift index 9f8c50d..25a614a 100644 --- a/Sources/SnapAuth/SnapAuth+ASACD.swift +++ b/Sources/SnapAuth/SnapAuth+ASACD.swift @@ -4,57 +4,45 @@ import AuthenticationServices @available(macOS 12.0, iOS 15.0, visionOS 1.0, tvOS 16.0, *) extension SnapAuth: ASAuthorizationControllerDelegate { - public func authorizationController( + nonisolated public func authorizationController( controller: ASAuthorizationController, didCompleteWithError error: Error ) { logger.debug("ASACD error") - guard let asError = error as? ASAuthorizationError else { - logger.error("authorizationController didCompleteWithError error was not an ASAuthorizationError") - sendError(.unknown) - return - } + Task { @MainActor in + guard let asError = error as? ASAuthorizationError else { + logger.error("authorizationController didCompleteWithError error was not an ASAuthorizationError") + sendError(.unknown) + return + } - switch asError.code { - case .canceled: - sendError(.canceled) - case .failed: - sendError(.failed) - case .invalidResponse: - sendError(.invalidResponse) - case .notHandled: - sendError(.notHandled) - case .notInteractive: - sendError(.notInteractive) - @unknown default: - sendError(.unknown) + sendError(asError.code.snapAuthError) + // The start call can SILENTLY produce this error which never makes it into this handler + // ASAuthorizationController credential request failed with error: Error Domain=com.apple.AuthenticationServices.AuthorizationError Code=1004 "(null)" } - // The start call can SILENTLY produce this error which never makes it into this handler - // ASAuthorizationController credential request failed with error: Error Domain=com.apple.AuthenticationServices.AuthorizationError Code=1004 "(null)" } - public func authorizationController( + nonisolated public func authorizationController( controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization ) { logger.debug("ASACD did complete") - - switch authorization.credential { - case is ASAuthorizationPublicKeyCredentialAssertion: - handleAssertion(authorization.credential as! ASAuthorizationPublicKeyCredentialAssertion) - case is ASAuthorizationPublicKeyCredentialRegistration: - handleRegistration(authorization.credential as! ASAuthorizationPublicKeyCredentialRegistration) - default: - logger.error("Unexpected credential type \(String(describing: type(of: authorization.credential)))") - sendError(.unexpectedAuthorizationType) + Task { @MainActor in + switch authorization.credential { + case is ASAuthorizationPublicKeyCredentialAssertion: + handleAssertion(authorization.credential as! ASAuthorizationPublicKeyCredentialAssertion) + case is ASAuthorizationPublicKeyCredentialRegistration: + handleRegistration(authorization.credential as! ASAuthorizationPublicKeyCredentialRegistration) + default: + logger.error("Unexpected credential type \(String(describing: type(of: authorization.credential)))") + sendError(.unexpectedAuthorizationType) + } } } /// Sends the error to the appropriate delegate method and resets the internal state back to idle private func sendError(_ error: SnapAuthError) { - // One or the other should eb set, but not both - assert(continuation != nil) continuation?.resume(returning: .failure(error)) continuation = nil } @@ -170,4 +158,3 @@ extension SnapAuth: ASAuthorizationControllerDelegate { // } // } } - diff --git a/Sources/SnapAuth/SnapAuth.swift b/Sources/SnapAuth/SnapAuth.swift index 218bb4c..16b3e24 100644 --- a/Sources/SnapAuth/SnapAuth.swift +++ b/Sources/SnapAuth/SnapAuth.swift @@ -7,6 +7,7 @@ import os /// This is used to start the passkey registration and authentication processes, /// typically in the `action` of a `Button` @available(macOS 12.0, iOS 15.0, tvOS 16.0, *) +@MainActor public class SnapAuth: NSObject { // NSObject for ASAuthorizationControllerDelegate internal let api: SnapAuthClient @@ -223,7 +224,7 @@ public class SnapAuth: NSObject { // NSObject for ASAuthorizationControllerDeleg } } -public enum AuthenticatingUser { +public enum AuthenticatingUser: Sendable { /// Your application's internal identifier for the user (usually a primary key) case id(String) /// The user's handle, such as a username or email address From cb7e5e6e3ebd3ff9a32d86abb0d61339a7789bf2 Mon Sep 17 00:00:00 2001 From: Eric Stern Date: Sun, 28 Jul 2024 15:03:21 -0700 Subject: [PATCH 5/5] Re-enable the iOS 18 error handling path --- Sources/SnapAuth/Errors.swift | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/Sources/SnapAuth/Errors.swift b/Sources/SnapAuth/Errors.swift index 584dbe6..6d5e0d6 100644 --- a/Sources/SnapAuth/Errors.swift +++ b/Sources/SnapAuth/Errors.swift @@ -72,16 +72,13 @@ extension ASAuthorizationError.Code { case .invalidResponse: return .invalidResponse case .notHandled: return .notHandled case .notInteractive: return .notInteractive - @unknown default: - /* This is (AFAICT) correct, but doesn't seem to work on the Github - Actions runner version. + default: // This case only exists on new OS platforms if #available(iOS 18, visionOS 2, macOS 15, tvOS 18, *) { if case .matchedExcludedCredential = self { return .matchedExcludedCredential } } - */ return .unknown } }