From fa9c98581d0fc4009b9069220d476a811dcf6088 Mon Sep 17 00:00:00 2001 From: Nimesh Jarecha Date: Fri, 30 Jun 2023 15:31:07 -0700 Subject: [PATCH 1/8] added initial smart card support --- .../project.pbxproj | 8 +++---- .../AuthenticationExample.entitlements | 4 ++++ .../AuthenticationExample/HomeView.swift | 2 +- .../AuthenticationExample/SignInView.swift | 2 ++ ...aturedMapsView.swift => WebMapsView.swift} | 21 ++++++++++++------- .../Authentication/Authenticator.swift | 10 ++++++++- 6 files changed, 34 insertions(+), 13 deletions(-) rename AuthenticationExample/AuthenticationExample/{FeaturedMapsView.swift => WebMapsView.swift} (79%) diff --git a/AuthenticationExample/AuthenticationExample.xcodeproj/project.pbxproj b/AuthenticationExample/AuthenticationExample.xcodeproj/project.pbxproj index 2235b68cc..3f03c16f9 100644 --- a/AuthenticationExample/AuthenticationExample.xcodeproj/project.pbxproj +++ b/AuthenticationExample/AuthenticationExample.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + 21450B722A4F470B00A81AB8 /* WebMapsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21450B712A4F470B00A81AB8 /* WebMapsView.swift */; }; 88AD13792834355000500B2E /* AuthenticationApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88AD13782834355000500B2E /* AuthenticationApp.swift */; }; 88AD137B2834355000500B2E /* SigninView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88AD137A2834355000500B2E /* SigninView.swift */; }; 88AD137D2834355100500B2E /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 88AD137C2834355100500B2E /* Assets.xcassets */; }; @@ -15,12 +16,12 @@ 88AD138E283443F800500B2E /* MapItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88AD138D283443F800500B2E /* MapItemView.swift */; }; 88D24DF0288F062D007A539C /* ProfileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88D24DEF288F062D007A539C /* ProfileView.swift */; }; 88D24DF2288F2FA1007A539C /* HomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88D24DF1288F2FA1007A539C /* HomeView.swift */; }; - 88D24DF4288F4BEB007A539C /* FeaturedMapsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88D24DF3288F4BEB007A539C /* FeaturedMapsView.swift */; }; 88D24DF6288F5BAE007A539C /* UserView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88D24DF5288F5BAE007A539C /* UserView.swift */; }; 88D24DF8288F6002007A539C /* LoadableImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88D24DF7288F6002007A539C /* LoadableImageView.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ + 21450B712A4F470B00A81AB8 /* WebMapsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WebMapsView.swift; sourceTree = ""; }; 21B31E8B29EF53BE00A40B10 /* AuthenticationExample.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = AuthenticationExample.entitlements; sourceTree = ""; }; 88AD13752834355000500B2E /* AuthenticationExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = AuthenticationExample.app; sourceTree = BUILT_PRODUCTS_DIR; }; 88AD13782834355000500B2E /* AuthenticationApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationApp.swift; sourceTree = ""; }; @@ -32,7 +33,6 @@ 88AD138D283443F800500B2E /* MapItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapItemView.swift; sourceTree = ""; }; 88D24DEF288F062D007A539C /* ProfileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileView.swift; sourceTree = ""; }; 88D24DF1288F2FA1007A539C /* HomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeView.swift; sourceTree = ""; }; - 88D24DF3288F4BEB007A539C /* FeaturedMapsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeaturedMapsView.swift; sourceTree = ""; }; 88D24DF5288F5BAE007A539C /* UserView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserView.swift; sourceTree = ""; }; 88D24DF7288F6002007A539C /* LoadableImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadableImageView.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -75,7 +75,7 @@ 88AD13782834355000500B2E /* AuthenticationApp.swift */, 88D24DF1288F2FA1007A539C /* HomeView.swift */, 88D24DF7288F6002007A539C /* LoadableImageView.swift */, - 88D24DF3288F4BEB007A539C /* FeaturedMapsView.swift */, + 21450B712A4F470B00A81AB8 /* WebMapsView.swift */, 88AD138D283443F800500B2E /* MapItemView.swift */, 88AD137A2834355000500B2E /* SigninView.swift */, 88D24DEF288F062D007A539C /* ProfileView.swift */, @@ -176,10 +176,10 @@ files = ( 88AD138E283443F800500B2E /* MapItemView.swift in Sources */, 88AD137B2834355000500B2E /* SigninView.swift in Sources */, - 88D24DF4288F4BEB007A539C /* FeaturedMapsView.swift in Sources */, 88D24DF2288F2FA1007A539C /* HomeView.swift in Sources */, 88D24DF8288F6002007A539C /* LoadableImageView.swift in Sources */, 88D24DF0288F062D007A539C /* ProfileView.swift in Sources */, + 21450B722A4F470B00A81AB8 /* WebMapsView.swift in Sources */, 88AD13792834355000500B2E /* AuthenticationApp.swift in Sources */, 88D24DF6288F5BAE007A539C /* UserView.swift in Sources */, ); diff --git a/AuthenticationExample/AuthenticationExample/AuthenticationExample.entitlements b/AuthenticationExample/AuthenticationExample/AuthenticationExample.entitlements index ee95ab7e5..ea9358ba7 100644 --- a/AuthenticationExample/AuthenticationExample/AuthenticationExample.entitlements +++ b/AuthenticationExample/AuthenticationExample/AuthenticationExample.entitlements @@ -6,5 +6,9 @@ com.apple.security.network.client + keychain-access-groups + + com.apple.token + diff --git a/AuthenticationExample/AuthenticationExample/HomeView.swift b/AuthenticationExample/AuthenticationExample/HomeView.swift index 1d91fe654..c95cd234f 100644 --- a/AuthenticationExample/AuthenticationExample/HomeView.swift +++ b/AuthenticationExample/AuthenticationExample/HomeView.swift @@ -25,7 +25,7 @@ struct HomeView: View { var body: some View { if let portal = portal { NavigationView{ - FeaturedMapsView(portal: portal) + WebMapsView(portal: portal) .toolbar { ToolbarItem(placement: .navigationBarTrailing) { Button { diff --git a/AuthenticationExample/AuthenticationExample/SignInView.swift b/AuthenticationExample/AuthenticationExample/SignInView.swift index b2abd75e1..e9374e5ed 100644 --- a/AuthenticationExample/AuthenticationExample/SignInView.swift +++ b/AuthenticationExample/AuthenticationExample/SignInView.swift @@ -71,6 +71,8 @@ struct SignInView: View { return "" case .serverTrust: return nil + case .smartCard: + return "" @unknown default: fatalError("Unknown NetworkCredential") } diff --git a/AuthenticationExample/AuthenticationExample/FeaturedMapsView.swift b/AuthenticationExample/AuthenticationExample/WebMapsView.swift similarity index 79% rename from AuthenticationExample/AuthenticationExample/FeaturedMapsView.swift rename to AuthenticationExample/AuthenticationExample/WebMapsView.swift index b4c7300c6..24b943d40 100644 --- a/AuthenticationExample/AuthenticationExample/FeaturedMapsView.swift +++ b/AuthenticationExample/AuthenticationExample/WebMapsView.swift @@ -14,23 +14,23 @@ import SwiftUI import ArcGIS -/// A view that displays the featured maps of a portal. -struct FeaturedMapsView: View { +/// A view that displays the web maps of a portal. +struct WebMapsView: View { /// The portal from which the featured content can be fetched. var portal: Portal /// A Boolean value indicating whether the featured content is being loaded. @State var isLoading = true - /// The featured items. - @State var featuredItems = [PortalItem]() + /// The web map portal items. + @State var webMapItems = [PortalItem]() var body: some View { VStack { if isLoading { ProgressView() } else { - List(featuredItems, id: \.id) { item in + List(webMapItems, id: \.id) { item in NavigationLink { MapItemView(map: Map(item: item)) } label: { @@ -40,10 +40,17 @@ struct FeaturedMapsView: View { } } .task { - guard featuredItems.isEmpty else { return } + guard webMapItems.isEmpty else { return } do { - featuredItems = try await portal.featuredItems + var items = try await portal.featuredItems .filter { $0.kind == .webMap } + if items.isEmpty { + items = try await portal.findItems( + queryParameters: .items(ofKinds: [.webMap]) + ) + .results + } + webMapItems = items } catch {} isLoading = false diff --git a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift index b2a9befe3..0aff8e36f 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift @@ -14,6 +14,7 @@ import ArcGIS import SwiftUI import Combine +import CryptoTokenKit /// A configurable object that handles authentication challenges. @MainActor @@ -67,13 +68,20 @@ extension Authenticator: ArcGISAuthenticationChallengeHandler { extension Authenticator: NetworkAuthenticationChallengeHandler { public func handleNetworkAuthenticationChallenge( _ challenge: NetworkAuthenticationChallenge - ) async -> NetworkAuthenticationChallenge.Disposition { + ) async -> NetworkAuthenticationChallenge.Disposition { // If `promptForUntrustedHosts` is `false` then perform default handling // for server trust challenges. guard promptForUntrustedHosts || challenge.kind != .serverTrust else { return .continueWithoutCredential } + // If smart card is connected to the device then continue with smart card network credential. + if challenge.kind == .clientCertificate, + let pivToken = TKTokenWatcher().tokenIDs.filter({ $0.localizedCaseInsensitiveContains("pivtoken") }).first, + let credential = try? NetworkCredential.smartCard(pivToken: pivToken) { + return .continueWithCredential(credential) + } + let challengeContinuation = NetworkChallengeContinuation(networkChallenge: challenge) // Alleviates an error with "already presenting". From b4d2c84225cab137c5f2df3345357dbaaa238967 Mon Sep 17 00:00:00 2001 From: Nimesh Jarecha Date: Mon, 17 Jul 2023 15:11:28 -0700 Subject: [PATCH 2/8] updated comment --- .../Components/Authentication/Authenticator.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift index 0aff8e36f..f539d8d99 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift @@ -75,7 +75,9 @@ extension Authenticator: NetworkAuthenticationChallengeHandler { return .continueWithoutCredential } - // If smart card is connected to the device then continue with smart card network credential. + // If smart card is connected to the device then a personal identity verification (PIV) token + // is available in the `TKTokenWatcher().tokenIDs`. Create a smart card network credential + // with first PIV token and continue with credential. if challenge.kind == .clientCertificate, let pivToken = TKTokenWatcher().tokenIDs.filter({ $0.localizedCaseInsensitiveContains("pivtoken") }).first, let credential = try? NetworkCredential.smartCard(pivToken: pivToken) { From 58837eedb92df8b2eaa681e18ca21e4c5e1a671f Mon Sep 17 00:00:00 2001 From: Nimesh Jarecha Date: Fri, 11 Aug 2023 14:51:30 -0700 Subject: [PATCH 3/8] added smart card manager, modifier and updated authenticator code --- .../Authentication/Authenticator.swift | 30 ++++- .../AuthenticatorModifier.swift | 32 +++--- .../Authentication/SmartCardManager.swift | 66 +++++++++++ .../SmartCardViewModifier.swift | 105 ++++++++++++++++++ 4 files changed, 213 insertions(+), 20 deletions(-) create mode 100644 Sources/ArcGISToolkit/Components/Authentication/SmartCardManager.swift create mode 100644 Sources/ArcGISToolkit/Components/Authentication/SmartCardViewModifier.swift diff --git a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift index e49e473cb..92de1125a 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift @@ -14,15 +14,21 @@ import ArcGIS import SwiftUI import Combine -import CryptoTokenKit /// A configurable object that handles authentication challenges. @MainActor public final class Authenticator: ObservableObject { /// A value indicating whether we should prompt the user when encountering an untrusted host. - let promptForUntrustedHosts: Bool + public let promptForUntrustedHosts: Bool + /// The OAuth configurations that this authenticator can work with. - let oAuthUserConfigurations: [OAuthUserConfiguration] + public var oAuthUserConfigurations: [OAuthUserConfiguration] + + /// The closure to call once the user has signed out. + public var signOutAction: (() async -> Void) = {} + + /// The smart card manager. + @ObservedObject public var smartCardManager: SmartCardManager /// Creates an authenticator. /// - Parameters: @@ -35,6 +41,7 @@ public final class Authenticator: ObservableObject { ) { self.promptForUntrustedHosts = promptForUntrustedHosts self.oAuthUserConfigurations = oAuthUserConfigurations + self.smartCardManager = SmartCardManager() } /// The current challenge. @@ -83,11 +90,13 @@ extension Authenticator: NetworkAuthenticationChallengeHandler { } // If smart card is connected to the device then a personal identity verification (PIV) token - // is available in the `TKTokenWatcher().tokenIDs`. Create a smart card network credential - // with first PIV token and continue with credential. + // is available then create a smart card network credential and continue. if challenge.kind == .clientCertificate, - let pivToken = TKTokenWatcher().tokenIDs.filter({ $0.localizedCaseInsensitiveContains("pivtoken") }).first, + let pivToken = smartCardManager.pivToken, let credential = try? NetworkCredential.smartCard(pivToken: pivToken) { + // Set last used PIV token on the manager. + smartCardManager.setLastUsedPIVToken(pivToken) + return .continueWithCredential(credential) } @@ -104,3 +113,12 @@ extension Authenticator: NetworkAuthenticationChallengeHandler { return await challengeContinuation.value } } + +public extension Authenticator { + /// Signs out by revoking tokens and clearing the credential stores. + func signOut() async { + await ArcGISEnvironment.authenticationManager.revokeOAuthTokens() + await ArcGISEnvironment.authenticationManager.clearCredentialStores() + smartCardManager.setLastUsedPIVToken(nil) + } +} diff --git a/Sources/ArcGISToolkit/Components/Authentication/AuthenticatorModifier.swift b/Sources/ArcGISToolkit/Components/Authentication/AuthenticatorModifier.swift index 2de80b6b2..620c038b5 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/AuthenticatorModifier.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/AuthenticatorModifier.swift @@ -46,22 +46,26 @@ private struct AuthenticatorModifier: ViewModifier { @ObservedObject var authenticator: Authenticator @ViewBuilder func body(content: Content) -> some View { - switch authenticator.currentChallenge { - case let challenge as TokenChallengeContinuation: - content.modifier(LoginViewModifier(challenge: challenge)) - case let challenge as NetworkChallengeContinuation: - switch challenge.kind { - case .serverTrust: - content.modifier(TrustHostViewModifier(challenge: challenge)) - case .certificate: - content.modifier(CertificatePickerViewModifier(challenge: challenge)) - case .login: + if let currentChallenge = authenticator.currentChallenge { + switch currentChallenge { + case let challenge as TokenChallengeContinuation: content.modifier(LoginViewModifier(challenge: challenge)) + case let challenge as NetworkChallengeContinuation: + switch challenge.kind { + case .serverTrust: + content.modifier(TrustHostViewModifier(challenge: challenge)) + case .certificate: + content.modifier(CertificatePickerViewModifier(challenge: challenge)) + case .login: + content.modifier(LoginViewModifier(challenge: challenge)) + } + default: + fatalError("unknown challenge type") } - case .none: - content - default: - fatalError("unknown challenge type") + } else { + content.modifier( + SmartCardViewModifier(authenticator: authenticator) + ) } } } diff --git a/Sources/ArcGISToolkit/Components/Authentication/SmartCardManager.swift b/Sources/ArcGISToolkit/Components/Authentication/SmartCardManager.swift new file mode 100644 index 000000000..ee7e63430 --- /dev/null +++ b/Sources/ArcGISToolkit/Components/Authentication/SmartCardManager.swift @@ -0,0 +1,66 @@ +// Copyright 2023 Esri. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 + +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import Foundation +import CryptoTokenKit +import SwiftUI + +@MainActor +public final class SmartCardManager: ObservableObject { + /// The smart card connection watcher. + private let watcher = TKTokenWatcher() + + /// The last used smart card personal identity verification (PIV) token. + public private(set) var lastUsedPIVToken: String? = nil + + /// A Boolean value indicating whether the smart card is disconnected. + @Published public internal(set) var isCardDisconnected: Bool = false + + /// A Boolean value indicating whether a different smart card is connected. + @Published public internal(set) var isDifferentCardConnected: Bool = false + + /// Creates smart card manager. + init() { + // Monitor the smart card connection for PIV tokens. + watcher.setInsertionHandler { [weak self] tokenID in + guard let self = self, tokenID.localizedCaseInsensitiveContains("pivtoken") else { return } + + if let lastUsedPIVToken, tokenID != lastUsedPIVToken { + DispatchQueue.main.async { + self.isDifferentCardConnected = true + } + } + + watcher.addRemovalHandler( { [weak self] tokenID in + guard let self = self else { return } + + if tokenID == lastUsedPIVToken { + DispatchQueue.main.async { + self.isCardDisconnected = true + } + } + }, forTokenID: tokenID) + } + } + + /// The first PIV token found the the token watcher. + /// - Note: The PIV token will be available only if smart card is connected to the device. + var pivToken: String? { + watcher.tokenIDs.filter({ $0.localizedCaseInsensitiveContains("pivtoken") }).first + } + + /// Sets the last used PIV token with given value. + func setLastUsedPIVToken(_ token: String?) { + lastUsedPIVToken = token + } +} diff --git a/Sources/ArcGISToolkit/Components/Authentication/SmartCardViewModifier.swift b/Sources/ArcGISToolkit/Components/Authentication/SmartCardViewModifier.swift new file mode 100644 index 000000000..c52749ebf --- /dev/null +++ b/Sources/ArcGISToolkit/Components/Authentication/SmartCardViewModifier.swift @@ -0,0 +1,105 @@ +// Copyright 2023 Esri. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 + +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import SwiftUI +import ArcGIS + +/// A view modifier that prompts the alerts for smart card. +struct SmartCardViewModifier: ViewModifier { + /// The authenticator. + let authenticator: Authenticator + + /// The smart card manager. + @ObservedObject var smartCardManager: SmartCardManager + + /// Creates smart card view modifier with given authenticator. + /// - Parameter authenticator: The authenticator. + init(authenticator: Authenticator) { + self.authenticator = authenticator + self.smartCardManager = authenticator.smartCardManager + } + + func body(content: Content) -> some View { + content + .promptDisconnectedCard( + isPresented: $smartCardManager.isCardDisconnected, + authenticator: authenticator + ) + .promptDifferentCardConnected( + isPresented: $smartCardManager.isDifferentCardConnected, + authenticator: authenticator + ) + } +} + +private extension View { + /// Displays an alert to the user to let them know that the smart card is disconnected. + /// - Parameters: + /// - isPresented: A binding to a Boolean value that determines whether to present an alert. + /// - authenticator: The authenticator. + @MainActor @ViewBuilder func promptDisconnectedCard( + isPresented: Binding, + authenticator: Authenticator + ) -> some View { + alert( + Text("Smart Card Disconnected", bundle: .toolkitModule), + isPresented: isPresented + ) { + Button(role: .cancel) { + Task { + await authenticator.signOutAction() + } + } label: { + Text("Sign Out", bundle: .toolkitModule) + } + Button(role: .destructive) { + print("Continue") + } label: { + Text("Continue", bundle: .toolkitModule) + } + } message: { + Text( + "Connect a smart card and continue or sign out to access a different account.", + bundle: .toolkitModule + ) + } + } +} + +private extension View { + /// Displays a prompt to the user to let them know that the smart card is disconnected. + /// - Parameters: isPresented: A Boolean value indicating if the view is presented. + @MainActor @ViewBuilder func promptDifferentCardConnected( + isPresented: Binding, + authenticator: Authenticator + ) -> some View { + alert( + Text("Different Card Connected", bundle: .toolkitModule), + isPresented: isPresented + ) { + Button(role: .cancel) { + Task { + await authenticator.signOutAction() + } + } label: { + Text("Sign Out", bundle: .toolkitModule) + } + } message: { + Text( + "You must sign out to access a different account.", + bundle: .toolkitModule + ) + } + } +} + From 94bef308347cbe9e83201e9124a2258023e71719 Mon Sep 17 00:00:00 2001 From: Nimesh Jarecha Date: Tue, 15 Aug 2023 15:13:09 -0700 Subject: [PATCH 4/8] updated smart card manager --- .../AuthenticationExample/SignInView.swift | 8 +++---- .../Authentication/Authenticator.swift | 5 +--- .../Authentication/SmartCardManager.swift | 24 +++++++++++-------- 3 files changed, 19 insertions(+), 18 deletions(-) diff --git a/AuthenticationExample/AuthenticationExample/SignInView.swift b/AuthenticationExample/AuthenticationExample/SignInView.swift index 2c7f8c9fa..0a020008f 100644 --- a/AuthenticationExample/AuthenticationExample/SignInView.swift +++ b/AuthenticationExample/AuthenticationExample/SignInView.swift @@ -67,12 +67,12 @@ struct SignInView: View { switch credential { case .password(let passwordCredential): return passwordCredential.username - case .certificate: - return "" + case .certificate(let certificateCredential): + return certificateCredential.identityName case .serverTrust: return nil - case .smartCard: - return "" + case .smartCard(let smartCardCredential): + return smartCardCredential.identityName @unknown default: fatalError("Unknown NetworkCredential") } diff --git a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift index 92de1125a..f4eacaa2d 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift @@ -94,9 +94,6 @@ extension Authenticator: NetworkAuthenticationChallengeHandler { if challenge.kind == .clientCertificate, let pivToken = smartCardManager.pivToken, let credential = try? NetworkCredential.smartCard(pivToken: pivToken) { - // Set last used PIV token on the manager. - smartCardManager.setLastUsedPIVToken(pivToken) - return .continueWithCredential(credential) } @@ -119,6 +116,6 @@ public extension Authenticator { func signOut() async { await ArcGISEnvironment.authenticationManager.revokeOAuthTokens() await ArcGISEnvironment.authenticationManager.clearCredentialStores() - smartCardManager.setLastUsedPIVToken(nil) + smartCardManager.reset() } } diff --git a/Sources/ArcGISToolkit/Components/Authentication/SmartCardManager.swift b/Sources/ArcGISToolkit/Components/Authentication/SmartCardManager.swift index ee7e63430..002077c05 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/SmartCardManager.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/SmartCardManager.swift @@ -13,15 +13,15 @@ import Foundation import CryptoTokenKit -import SwiftUI +import ArcGIS @MainActor public final class SmartCardManager: ObservableObject { /// The smart card connection watcher. private let watcher = TKTokenWatcher() - /// The last used smart card personal identity verification (PIV) token. - public private(set) var lastUsedPIVToken: String? = nil + /// The last connected smart card. + public private(set) var lastConnectedCard: String? = nil /// A Boolean value indicating whether the smart card is disconnected. @Published public internal(set) var isCardDisconnected: Bool = false @@ -31,20 +31,22 @@ public final class SmartCardManager: ObservableObject { /// Creates smart card manager. init() { - // Monitor the smart card connection for PIV tokens. + // Monitor the smart card connection. watcher.setInsertionHandler { [weak self] tokenID in guard let self = self, tokenID.localizedCaseInsensitiveContains("pivtoken") else { return } - if let lastUsedPIVToken, tokenID != lastUsedPIVToken { + if let lastConnectedCard, tokenID != lastConnectedCard { DispatchQueue.main.async { self.isDifferentCardConnected = true } + } else { + lastConnectedCard = tokenID } watcher.addRemovalHandler( { [weak self] tokenID in guard let self = self else { return } - if tokenID == lastUsedPIVToken { + if tokenID == lastConnectedCard { DispatchQueue.main.async { self.isCardDisconnected = true } @@ -53,14 +55,16 @@ public final class SmartCardManager: ObservableObject { } } - /// The first PIV token found the the token watcher. + /// The first PIV token found in the token watcher. /// - Note: The PIV token will be available only if smart card is connected to the device. var pivToken: String? { watcher.tokenIDs.filter({ $0.localizedCaseInsensitiveContains("pivtoken") }).first } - /// Sets the last used PIV token with given value. - func setLastUsedPIVToken(_ token: String?) { - lastUsedPIVToken = token + /// Resets the smart card manager. + func reset() { + lastConnectedCard = nil + isCardDisconnected = false + isDifferentCardConnected = false } } From 7df6e6730b1f7a25144d99c49c7e2ba742df52d2 Mon Sep 17 00:00:00 2001 From: Nimesh Jarecha Date: Wed, 16 Aug 2023 14:36:52 -0700 Subject: [PATCH 5/8] changes for smart card apis --- .../Authentication/Authenticator.swift | 6 +++ .../AuthenticatorModifier.swift | 4 +- .../Authentication/SmartCardManager.swift | 45 ++++++++++++++++--- .../SmartCardViewModifier.swift | 11 +---- 4 files changed, 48 insertions(+), 18 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift index f4eacaa2d..d4138eefb 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift @@ -56,6 +56,9 @@ extension Authenticator: ArcGISAuthenticationChallengeHandler { // Alleviates an error with "already presenting". await Task.yield() + // Set last connected smart card. + smartCardManager.setLastConnectedCard() + // Create the correct challenge type. if let configuration = oAuthUserConfigurations.first(where: { $0.canBeUsed(for: challenge.requestURL) }) { do { @@ -83,6 +86,9 @@ extension Authenticator: NetworkAuthenticationChallengeHandler { public func handleNetworkAuthenticationChallenge( _ challenge: NetworkAuthenticationChallenge ) async -> NetworkAuthenticationChallenge.Disposition { + // Set last connected smart card. + smartCardManager.setLastConnectedCard() + // If `promptForUntrustedHosts` is `false` then perform default handling // for server trust challenges. guard promptForUntrustedHosts || challenge.kind != .serverTrust else { diff --git a/Sources/ArcGISToolkit/Components/Authentication/AuthenticatorModifier.swift b/Sources/ArcGISToolkit/Components/Authentication/AuthenticatorModifier.swift index 620c038b5..071c40d6b 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/AuthenticatorModifier.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/AuthenticatorModifier.swift @@ -63,9 +63,7 @@ private struct AuthenticatorModifier: ViewModifier { fatalError("unknown challenge type") } } else { - content.modifier( - SmartCardViewModifier(authenticator: authenticator) - ) + content.modifier(SmartCardViewModifier()) } } } diff --git a/Sources/ArcGISToolkit/Components/Authentication/SmartCardManager.swift b/Sources/ArcGISToolkit/Components/Authentication/SmartCardManager.swift index 002077c05..d3cc6b83e 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/SmartCardManager.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/SmartCardManager.swift @@ -17,23 +17,37 @@ import ArcGIS @MainActor public final class SmartCardManager: ObservableObject { - /// The smart card connection watcher. - private let watcher = TKTokenWatcher() + /// An enumeration of the various status values for a smart card connection. + public enum ConnectionStatus { + /// A smart card is connected to the device. + case connected + /// A smart card is disconnected from the device. + case disconnected + /// The connection is unspecified. + case unspecified + } - /// The last connected smart card. - public private(set) var lastConnectedCard: String? = nil + /// The current smart card connection status. + @Published public var connectionStatus: ConnectionStatus = .unspecified /// A Boolean value indicating whether the smart card is disconnected. - @Published public internal(set) var isCardDisconnected: Bool = false + @Published var isCardDisconnected: Bool = false /// A Boolean value indicating whether a different smart card is connected. - @Published public internal(set) var isDifferentCardConnected: Bool = false + @Published var isDifferentCardConnected: Bool = false + + /// The smart card connection watcher. + private let watcher = TKTokenWatcher() + + /// The last connected smart card. + private var lastConnectedCard: String? = nil /// Creates smart card manager. init() { // Monitor the smart card connection. watcher.setInsertionHandler { [weak self] tokenID in guard let self = self, tokenID.localizedCaseInsensitiveContains("pivtoken") else { return } + print("tokenID added: \(tokenID)") if let lastConnectedCard, tokenID != lastConnectedCard { DispatchQueue.main.async { @@ -42,12 +56,21 @@ public final class SmartCardManager: ObservableObject { } else { lastConnectedCard = tokenID } + + DispatchQueue.main.async { + if self.connectionStatus != .connected { + self.connectionStatus = .connected + } + } watcher.addRemovalHandler( { [weak self] tokenID in guard let self = self else { return } + print("tokenID removed: \(tokenID)") + if tokenID == lastConnectedCard { DispatchQueue.main.async { + self.connectionStatus = .disconnected self.isCardDisconnected = true } } @@ -61,10 +84,20 @@ public final class SmartCardManager: ObservableObject { watcher.tokenIDs.filter({ $0.localizedCaseInsensitiveContains("pivtoken") }).first } + /// Sets the last connected card if PIV token is available. This is being called from the + /// authentication challenge handler to monitor the smart card. + func setLastConnectedCard() { + guard let pivToken = pivToken else { return } + if lastConnectedCard == nil { + lastConnectedCard = pivToken + } + } + /// Resets the smart card manager. func reset() { lastConnectedCard = nil isCardDisconnected = false isDifferentCardConnected = false + connectionStatus = .unspecified } } diff --git a/Sources/ArcGISToolkit/Components/Authentication/SmartCardViewModifier.swift b/Sources/ArcGISToolkit/Components/Authentication/SmartCardViewModifier.swift index c52749ebf..8468a5501 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/SmartCardViewModifier.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/SmartCardViewModifier.swift @@ -17,17 +17,10 @@ import ArcGIS /// A view modifier that prompts the alerts for smart card. struct SmartCardViewModifier: ViewModifier { /// The authenticator. - let authenticator: Authenticator + @EnvironmentObject var authenticator: Authenticator /// The smart card manager. - @ObservedObject var smartCardManager: SmartCardManager - - /// Creates smart card view modifier with given authenticator. - /// - Parameter authenticator: The authenticator. - init(authenticator: Authenticator) { - self.authenticator = authenticator - self.smartCardManager = authenticator.smartCardManager - } + @EnvironmentObject var smartCardManager: SmartCardManager func body(content: Content) -> some View { content From 6ffe2ac8f0593496407af14982d4e442407e9bd0 Mon Sep 17 00:00:00 2001 From: Nimesh Jarecha Date: Wed, 16 Aug 2023 15:01:06 -0700 Subject: [PATCH 6/8] remove print statements --- .../Components/Authentication/SmartCardManager.swift | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Authentication/SmartCardManager.swift b/Sources/ArcGISToolkit/Components/Authentication/SmartCardManager.swift index d3cc6b83e..fc8b4aa63 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/SmartCardManager.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/SmartCardManager.swift @@ -47,7 +47,6 @@ public final class SmartCardManager: ObservableObject { // Monitor the smart card connection. watcher.setInsertionHandler { [weak self] tokenID in guard let self = self, tokenID.localizedCaseInsensitiveContains("pivtoken") else { return } - print("tokenID added: \(tokenID)") if let lastConnectedCard, tokenID != lastConnectedCard { DispatchQueue.main.async { @@ -62,12 +61,10 @@ public final class SmartCardManager: ObservableObject { self.connectionStatus = .connected } } - + watcher.addRemovalHandler( { [weak self] tokenID in guard let self = self else { return } - - print("tokenID removed: \(tokenID)") - + if tokenID == lastConnectedCard { DispatchQueue.main.async { self.connectionStatus = .disconnected From 9c1f2af839fba8517a43ab965382da1bc9f4c944 Mon Sep 17 00:00:00 2001 From: Nimesh Jarecha Date: Tue, 5 Sep 2023 10:55:39 -0700 Subject: [PATCH 7/8] fix sign out workflow --- .../AuthenticationExample/AuthenticationApp.swift | 8 ++++++++ .../AuthenticationExample/ProfileView.swift | 6 ++++-- .../Components/Authentication/Authenticator.swift | 1 + 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/AuthenticationExample/AuthenticationExample/AuthenticationApp.swift b/AuthenticationExample/AuthenticationExample/AuthenticationApp.swift index 8d0824cdc..031664479 100644 --- a/AuthenticationExample/AuthenticationExample/AuthenticationApp.swift +++ b/AuthenticationExample/AuthenticationExample/AuthenticationApp.swift @@ -48,6 +48,7 @@ struct AuthenticationApp: App { // - Client Certificate (PKI) .authenticator(authenticator) .environmentObject(authenticator) + .environmentObject(authenticator.smartCardManager) .task { isSettingUp = true // Here we setup credential stores to be persistent, which means that it will @@ -58,6 +59,13 @@ struct AuthenticationApp: App { try? await ArcGISEnvironment.authenticationManager.setupPersistentCredentialStorage(access: .whenUnlockedThisDeviceOnly) isSettingUp = false } + .task { + authenticator.signOutAction = { + isSettingUp = true + await authenticator.signOut() + isSettingUp = false + } + } } } } diff --git a/AuthenticationExample/AuthenticationExample/ProfileView.swift b/AuthenticationExample/AuthenticationExample/ProfileView.swift index 948aa2f6e..1c436282e 100644 --- a/AuthenticationExample/AuthenticationExample/ProfileView.swift +++ b/AuthenticationExample/AuthenticationExample/ProfileView.swift @@ -17,6 +17,9 @@ import ArcGISToolkit /// A view that displays the profile of a user. struct ProfileView: View { + /// The view model used by this selector. + @EnvironmentObject var authenticator: Authenticator + /// The portal that the user is signed in to. @State var portal: Portal @@ -58,8 +61,7 @@ struct ProfileView: View { func signOut() { isSigningOut = true Task { - await ArcGISEnvironment.authenticationManager.revokeOAuthTokens() - await ArcGISEnvironment.authenticationManager.clearCredentialStores() + await authenticator.signOutAction() isSigningOut = false signOutAction() } diff --git a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift index 965af7558..b098b99ec 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift @@ -14,6 +14,7 @@ import ArcGIS import Combine import CryptoTokenKit +import SwiftUI /// The `Authenticator` is a configurable object that handles authentication challenges. It will /// display a user interface when network and ArcGIS authentication challenges occur. From 14c638a3ae3c71e6773ecbb03fda23046c3ff292 Mon Sep 17 00:00:00 2001 From: Nimesh Jarecha Date: Tue, 5 Sep 2023 11:31:53 -0700 Subject: [PATCH 8/8] revert the properties change --- .../Components/Authentication/Authenticator.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift index b098b99ec..c3b2f9fbe 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift @@ -53,10 +53,10 @@ import SwiftUI @MainActor public final class Authenticator: ObservableObject { /// A value indicating whether we should prompt the user when encountering an untrusted host. - public let promptForUntrustedHosts: Bool + let promptForUntrustedHosts: Bool /// The OAuth configurations that this authenticator can work with. - public var oAuthUserConfigurations: [OAuthUserConfiguration] + let oAuthUserConfigurations: [OAuthUserConfiguration] /// The closure to call once the user has signed out. public var signOutAction: (() async -> Void) = {}