From 92e25bcd5911c821c96e2acac732ee74ebd9c9fc Mon Sep 17 00:00:00 2001 From: Eric Stern Date: Fri, 6 Sep 2024 11:39:39 -0700 Subject: [PATCH 01/11] Add upgrade boolean flag to send to backend --- Sources/SnapAuth/API.swift | 1 + Sources/SnapAuth/SnapAuth+Upgrades.swift | 0 2 files changed, 1 insertion(+) create mode 100644 Sources/SnapAuth/SnapAuth+Upgrades.swift diff --git a/Sources/SnapAuth/API.swift b/Sources/SnapAuth/API.swift index bb40ffe..b6a3369 100644 --- a/Sources/SnapAuth/API.swift +++ b/Sources/SnapAuth/API.swift @@ -16,6 +16,7 @@ struct SAWrappedResponse: Decodable where T: Decodable { struct SACreateRegisterOptionsRequest: Encodable { let user: AuthenticatingUser? + let upgrade: Bool } struct SACreateRegisterOptionsResponse: Decodable { let publicKey: PublicKeyOptions diff --git a/Sources/SnapAuth/SnapAuth+Upgrades.swift b/Sources/SnapAuth/SnapAuth+Upgrades.swift new file mode 100644 index 0000000..e69de29 From 58565ef18ad1463515673714fa1added0158aafd Mon Sep 17 00:00:00 2001 From: Eric Stern Date: Fri, 6 Sep 2024 11:41:09 -0700 Subject: [PATCH 02/11] Start to use it upstream --- Sources/SnapAuth/SnapAuth.swift | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Sources/SnapAuth/SnapAuth.swift b/Sources/SnapAuth/SnapAuth.swift index 20faf1c..b44c0ef 100644 --- a/Sources/SnapAuth/SnapAuth.swift +++ b/Sources/SnapAuth/SnapAuth.swift @@ -114,12 +114,13 @@ public class SnapAuth: NSObject { // NSObject for ASAuthorizationControllerDeleg name: String, anchor: ASPresentationAnchor, displayName: String? = nil, - authenticators: Set = Authenticator.all + authenticators: Set = Authenticator.all, + upgrade: Bool ) async -> SnapAuthResult { reset() self.anchor = anchor - let body = SACreateRegisterOptionsRequest(user: nil) + let body = SACreateRegisterOptionsRequest(user: nil, upgrade: upgrade) let response = await api.makeRequest( path: "/attestation/options", body: body, From c19d74e77295f696b6f818f8d33ed8dcedad6925 Mon Sep 17 00:00:00 2001 From: Eric Stern Date: Fri, 6 Sep 2024 11:45:31 -0700 Subject: [PATCH 03/11] Get the plumbing to work on the outbound side --- Sources/SnapAuth/SnapAuth+Upgrades.swift | 24 ++++++++++++++++++++++++ Sources/SnapAuth/SnapAuth.swift | 3 ++- 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/Sources/SnapAuth/SnapAuth+Upgrades.swift b/Sources/SnapAuth/SnapAuth+Upgrades.swift index e69de29..aee8d88 100644 --- a/Sources/SnapAuth/SnapAuth+Upgrades.swift +++ b/Sources/SnapAuth/SnapAuth+Upgrades.swift @@ -0,0 +1,24 @@ +extension SnapAuth { + /// Attempts to upgrade an existing account to use passkeys by creating one + /// in the background. + /// + /// This should be called after a user signs in. Errors should not be + /// displayed to the user, though may be logged. + /// + /// - Parameters: + /// - name: The name of the user. This should be a username or handle. + /// - displayName: The proper name of the user. If omitted, name will be used. + public func upgradeToPasskey( + username: String, + displayName: String? = nil + ) async -> SnapAuthResult { + + await startRegister( + name: username, + anchor: .default, + displayName: displayName, + authenticators: Authenticator.all, + upgrade: true + ) + } +} diff --git a/Sources/SnapAuth/SnapAuth.swift b/Sources/SnapAuth/SnapAuth.swift index b44c0ef..ae61cd4 100644 --- a/Sources/SnapAuth/SnapAuth.swift +++ b/Sources/SnapAuth/SnapAuth.swift @@ -106,7 +106,8 @@ public class SnapAuth: NSObject { // NSObject for ASAuthorizationControllerDeleg name: name, anchor: .default, displayName: displayName, - authenticators: authenticators) + authenticators: authenticators, + upgrade: false) } // TODO: Only make this public if needed? From 44f9c39564160c3904a004ef5f32c10125c282dc Mon Sep 17 00:00:00 2001 From: Eric Stern Date: Fri, 6 Sep 2024 11:49:06 -0700 Subject: [PATCH 04/11] Only send passkey requests for upgrades, they will never work for hardware keys --- Sources/SnapAuth/SnapAuth+Upgrades.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/SnapAuth/SnapAuth+Upgrades.swift b/Sources/SnapAuth/SnapAuth+Upgrades.swift index aee8d88..684337b 100644 --- a/Sources/SnapAuth/SnapAuth+Upgrades.swift +++ b/Sources/SnapAuth/SnapAuth+Upgrades.swift @@ -17,7 +17,7 @@ extension SnapAuth { name: username, anchor: .default, displayName: displayName, - authenticators: Authenticator.all, + authenticators: [.passkey], upgrade: true ) } From e48df00f92a556aef2934f2991ca4e28ebde86a6 Mon Sep 17 00:00:00 2001 From: Eric Stern Date: Fri, 6 Sep 2024 11:58:41 -0700 Subject: [PATCH 05/11] Define mediation responses in api --- Sources/SnapAuth/API.swift | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/Sources/SnapAuth/API.swift b/Sources/SnapAuth/API.swift index b6a3369..a4bc933 100644 --- a/Sources/SnapAuth/API.swift +++ b/Sources/SnapAuth/API.swift @@ -94,7 +94,7 @@ enum Transport: String, Encodable { struct SACreateAuthOptionsResponse: Decodable { let publicKey: PublicKeyOptions - // mediation + let mediation: CredentialMediationRequirement struct PublicKeyOptions: Decodable { @@ -132,3 +132,15 @@ struct SAProcessAuthResponse: Decodable { let token: String let expiresAt: Date } + +/// https://www.w3.org/TR/credential-management-1/#mediation-requirements +enum CredentialMediationRequirement: String, Decodable { + /// Default behavior: present requests in foreground if needed. + case optional = "optional" + /// Used to indicate operation should be done in the background. + case conditional = "conditional" + /// Fail if operation cannot be performed without user involvement. Unused; only present for future-proofing. + case silent = "silent" + /// Fail if operation cannot be performed with user involvement. Unused; only present for future-proofing. + case required = "required" +} From 575ae1a8fb57c9826b6cc7d36421f4a94ed34039 Mon Sep 17 00:00:00 2001 From: Eric Stern Date: Mon, 9 Sep 2024 10:02:57 -0700 Subject: [PATCH 06/11] Convert WebAuthn mediation enum into Apple format --- Sources/SnapAuth/API.swift | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/Sources/SnapAuth/API.swift b/Sources/SnapAuth/API.swift index a4bc933..b576d21 100644 --- a/Sources/SnapAuth/API.swift +++ b/Sources/SnapAuth/API.swift @@ -1,4 +1,5 @@ import Foundation +import AuthenticationServices /// Wrapper that matches the API wire format /// @@ -20,6 +21,7 @@ struct SACreateRegisterOptionsRequest: Encodable { } struct SACreateRegisterOptionsResponse: Decodable { let publicKey: PublicKeyOptions + let mediation: CredentialMediationRequirement struct PublicKeyOptions: Decodable { let rp: RPInfo @@ -143,4 +145,12 @@ enum CredentialMediationRequirement: String, Decodable { case silent = "silent" /// Fail if operation cannot be performed with user involvement. Unused; only present for future-proofing. case required = "required" + + @available(iOS 18, visionOS 2.0, macOS 15.0, *) + var requestStyle: ASAuthorizationPlatformPublicKeyCredentialRegistrationRequest.RequestStyle { + if (self == .conditional) { + return .conditional + } + return .standard + } } From ee90eb23f01b9c51713ac2acd282d7e0e7088140 Mon Sep 17 00:00:00 2001 From: Eric Stern Date: Mon, 9 Sep 2024 10:27:59 -0700 Subject: [PATCH 07/11] Call conditional requests when possible during registration --- Sources/SnapAuth/SnapAuth+BuildRequests.swift | 28 +++++++++++++++---- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/Sources/SnapAuth/SnapAuth+BuildRequests.swift b/Sources/SnapAuth/SnapAuth+BuildRequests.swift index aad7843..be20eba 100644 --- a/Sources/SnapAuth/SnapAuth+BuildRequests.swift +++ b/Sources/SnapAuth/SnapAuth+BuildRequests.swift @@ -14,12 +14,30 @@ extension SnapAuth { if authenticators.contains(.passkey) { let provider = ASAuthorizationPlatformPublicKeyCredentialProvider( relyingPartyIdentifier: options.publicKey.rp.id) - let request = provider.createCredentialRegistrationRequest( - challenge: challenge, - name: name, - userID: options.publicKey.user.id.data) - requests.append(request) + // This is a little clumsy: the conditional request API wasn't added + // to tvOS at all, and a simple if #available can't block an entire + // platform. + var request: ASAuthorizationPlatformPublicKeyCredentialRegistrationRequest? = nil + #if (os(iOS) || os(macOS) || os(visionOS)) + if #available(iOS 18, macOS 15, visionOS 2, *) { + request = provider.createCredentialRegistrationRequest( + challenge: challenge, + name: name, + userID: options.publicKey.user.id.data, + requestStyle: options.mediation.requestStyle) + } + // TODO: if conditional and unsupported platform, short-circuit to ensure there's no false-positive modals + #endif + if request == nil { + // tvOS and previous other platforms + request = provider.createCredentialRegistrationRequest( + challenge: challenge, + name: name, + userID: options.publicKey.user.id.data) + } + + requests.append(request!) } #if HARDWARE_KEY_SUPPORT From 563d029272efd05035ee9cd23665065679728ef1 Mon Sep 17 00:00:00 2001 From: Eric Stern Date: Mon, 9 Sep 2024 11:03:15 -0700 Subject: [PATCH 08/11] Add availability APIs --- Sources/SnapAuth/Availability.swift | 33 +++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 Sources/SnapAuth/Availability.swift diff --git a/Sources/SnapAuth/Availability.swift b/Sources/SnapAuth/Availability.swift new file mode 100644 index 0000000..cfda031 --- /dev/null +++ b/Sources/SnapAuth/Availability.swift @@ -0,0 +1,33 @@ +/// Platform availability hints for passkeys and hardware authenticators +struct SAAvailability { + + /// Indicates whether passkey autofill requests are supported on the current + /// platform/device. + static var autofill: Bool { +#if (os(iOS) || os(visionOS)) + return #available(iOS 16, visionOS 1, *) +#else + return false +#endif + } + + /// Indicates whether external security keys are supported on the current + /// platform/device. + static var securityKeys: Bool { +#if HARDWARE_KEY_SUPPORT + return true +#else + return false +#endif + } + + /// Indicates whether automatic passkey upgrades are supported on the + /// current platform/device. + static var passkeyUpgrades: Bool { +#if (os(iOS) || os(macOS) || os(visionOS)) + return #available(iOS 18, macOS 15, visionOS 2, *) +#else + return false +#endif + } +} From 4c6cfc27215aa58a10048a1df7d5d1a6b7697717 Mon Sep 17 00:00:00 2001 From: Eric Stern Date: Mon, 9 Sep 2024 11:08:10 -0700 Subject: [PATCH 09/11] Fix syntax that xcode was swallpwing --- Sources/SnapAuth/Availability.swift | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/Sources/SnapAuth/Availability.swift b/Sources/SnapAuth/Availability.swift index cfda031..250aec2 100644 --- a/Sources/SnapAuth/Availability.swift +++ b/Sources/SnapAuth/Availability.swift @@ -5,10 +5,11 @@ struct SAAvailability { /// platform/device. static var autofill: Bool { #if (os(iOS) || os(visionOS)) - return #available(iOS 16, visionOS 1, *) -#else - return false + if #available(iOS 16, visionOS 1, *) { + return true + } #endif + return false } /// Indicates whether external security keys are supported on the current @@ -25,9 +26,10 @@ struct SAAvailability { /// current platform/device. static var passkeyUpgrades: Bool { #if (os(iOS) || os(macOS) || os(visionOS)) - return #available(iOS 18, macOS 15, visionOS 2, *) -#else - return false + if #available(iOS 18, macOS 15, visionOS 2, *) { + return true + } #endif + return false } } From a00ab0f675ab7ebc0064dfee573e4783251332d5 Mon Sep 17 00:00:00 2001 From: Eric Stern Date: Mon, 9 Sep 2024 11:15:46 -0700 Subject: [PATCH 10/11] Short-circuit impossible upgrade requests --- Sources/SnapAuth/SnapAuth+Upgrades.swift | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Sources/SnapAuth/SnapAuth+Upgrades.swift b/Sources/SnapAuth/SnapAuth+Upgrades.swift index 684337b..11ed4ee 100644 --- a/Sources/SnapAuth/SnapAuth+Upgrades.swift +++ b/Sources/SnapAuth/SnapAuth+Upgrades.swift @@ -12,8 +12,11 @@ extension SnapAuth { username: String, displayName: String? = nil ) async -> SnapAuthResult { + if !SAAvailability.passkeyUpgrades { + return .failure(.unsupportedOnPlatform) + } - await startRegister( + return await startRegister( name: username, anchor: .default, displayName: displayName, From 94982cde1a3fe1f8310819e26480843e83af563b Mon Sep 17 00:00:00 2001 From: Eric Stern Date: Tue, 22 Oct 2024 12:49:59 -0700 Subject: [PATCH 11/11] Finish merge manually --- Sources/SnapAuth/SnapAuth+Upgrades.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/SnapAuth/SnapAuth+Upgrades.swift b/Sources/SnapAuth/SnapAuth+Upgrades.swift index 11ed4ee..2de3b4c 100644 --- a/Sources/SnapAuth/SnapAuth+Upgrades.swift +++ b/Sources/SnapAuth/SnapAuth+Upgrades.swift @@ -6,7 +6,7 @@ extension SnapAuth { /// displayed to the user, though may be logged. /// /// - Parameters: - /// - name: The name of the user. This should be a username or handle. + /// - username: The username of the user, such as an email address or handle /// - displayName: The proper name of the user. If omitted, name will be used. public func upgradeToPasskey( username: String, @@ -17,7 +17,7 @@ extension SnapAuth { } return await startRegister( - name: username, + username: username, anchor: .default, displayName: displayName, authenticators: [.passkey],