Skip to content

updated authenticator and doc #259

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Feb 16, 2023
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,9 @@ struct AuthenticationApp: App {
// If you want to use OAuth, uncomment this code:
//oAuthUserConfigurations: [.arcgisDotCom]
)
// Set the ArcGIS and Network challenge handlers to be the authenticator we just created.
ArcGISEnvironment.authenticationManager.arcGISAuthenticationChallengeHandler = authenticator
ArcGISEnvironment.authenticationManager.networkAuthenticationChallengeHandler = authenticator
// Sets authenticator as ArcGIS and Network challenge handlers to handle authentication
// challenges.
ArcGISEnvironment.authenticationManager.handleChallenges(using: authenticator)
}

var body: some SwiftUI.Scene {
Expand All @@ -50,12 +50,12 @@ struct AuthenticationApp: App {
.environmentObject(authenticator)
.task {
isSettingUp = true
// Here we make the authenticator persistent, which means that it will synchronize
// with they keychain for storing credentials.
// Here we setup credential stores to be persistent, which means that it will
// synchronize with the keychain for storing credentials.
// It also means that a user can sign in without having to be prompted for
// credentials. Once credentials are cleared from the stores ("sign-out"),
// then the user will need to be prompted once again.
try? await authenticator.setupPersistentCredentialStorage(access: .whenUnlockedThisDeviceOnly)
try? await ArcGISEnvironment.authenticationManager.setupPersistentCredentialStorage(access: .whenUnlockedThisDeviceOnly)
isSettingUp = false
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,8 @@ struct ProfileView: View {
func signOut() {
isSigningOut = true
Task {
await authenticator.clearCredentialStores()
await ArcGISEnvironment.authenticationManager.revokeOAuthTokens()
await ArcGISEnvironment.authenticationManager.clearCredentialStores()
isSigningOut = false
signOutAction()
}
Expand Down
29 changes: 21 additions & 8 deletions Documentation/Authenticator/README.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
# Authenticator

The `Authenticator` is a configurable object that handles authentication challenges. It will display a user interface when network and ArcGIS authentication challenges occur.
The `Authenticator` is a configurable object that handles authentication challenges. It will display a user interface when network and ArcGIS authentication challenges occur.

![image](https://user-images.githubusercontent.com/3998072/203615041-c887d5e3-bb64-469a-a76b-126059329e92.png)

## Features

The `Authenticator` has a view modifier that will display a prompt when the `Authenticator` is asked to handle an authentication challenge. This will handle many different types of authentication, for example:
The `Authenticator` has a view modifier that will display a prompt when the `Authenticator` is asked to handle an authentication challenge. This will handle many different types of authentication, for example:
- ArcGIS authentication (token and OAuth)
- Integrated Windows Authentication (IWA)
- Client Certificate (PKI)
Expand All @@ -23,7 +23,7 @@ The `Authenticator` can be configured to support securely persisting credentials
@ViewBuilder func authenticator(_ authenticator: Authenticator) -> some View
```

To securely store credentials in the keychain, use the following instance method on `Authenticator`:
To securely store credentials in the keychain, use the following extension method of `AuthenticationManager`:

```swift
/// Sets up new credential stores that will be persisted to the keychain.
Expand All @@ -39,6 +39,18 @@ To securely store credentials in the keychain, use the following instance method
) async throws
```

During sign-out, use the following extension methods of `AuthenticationManager`:

```swift
/// Revokes tokens of OAuth user credentials.
func revokeOAuthTokens() async

/// Clears all ArcGIS and network credentials from the respective stores.
/// Note: This sets up new `URLSessions` so that removed network credentials are respected
/// right away.
func clearCredentialStores() async
```

## Behavior:

The Authenticator view modifier will display an alert prompting the user for credentials. If credentials were persisted to the keychain, the Authenticator will use those instead of requiring the user to reenter credentials.
Expand All @@ -56,21 +68,22 @@ init() {
// If you want to use OAuth, uncomment this code:
//oAuthConfigurations: [.arcgisDotCom]
)
// Set the challenge handler to be the authenticator we just created.
ArcGISEnvironment.authenticationChallengeHandler = authenticator
// Sets authenticator as ArcGIS and Network challenge handlers to handle authentication
// challenges.
ArcGISEnvironment.authenticationManager.handleChallenges(using: authenticator)
}

var body: some SwiftUI.Scene {
WindowGroup {
HomeView()
.authenticator(authenticator)
.task {
// Here we make the authenticator persistent, which means that it will synchronize
// with the keychain for storing credentials.
// Here we setup credential stores to be persistent, which means that it will
// synchronize with the keychain for storing credentials.
// It also means that a user can sign in without having to be prompted for
// credentials. Once credentials are cleared from the stores ("sign-out"),
// then the user will need to be prompted once again.
try? await authenticator.setupPersistentCredentialStorage(access: .whenUnlockedThisDeviceOnly)
try? await ArcGISEnvironment.authenticationManager.setupPersistentCredentialStorage(access: .whenUnlockedThisDeviceOnly)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
// 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 ArcGIS

public extension AuthenticationManager {
/// Sets up authenticator as ArcGIS and Network challenge handlers to handle authentication
/// challenges.
/// - Parameter authenticator: The authenticator to be used for handling challenges.
func handleChallenges(using authenticator: Authenticator) {
arcGISAuthenticationChallengeHandler = authenticator
networkAuthenticationChallengeHandler = authenticator
}

/// Sets up new credential stores that will be persisted to the keychain.
/// - Remark: The credentials will be stored in the default access group of the keychain.
/// You can find more information about what the default group would be here:
/// https://developer.apple.com/documentation/security/keychain_services/keychain_items/sharing_access_to_keychain_items_among_a_collection_of_apps
/// - Parameters:
/// - access: When the credentials stored in the keychain can be accessed.
/// - synchronizesWithiCloud: A Boolean value indicating whether the credentials are synchronized with iCloud.
func setupPersistentCredentialStorage(
access: ArcGIS.KeychainAccess,
synchronizesWithiCloud: Bool = false
) async throws {
let previousArcGISCredentialStore = arcGISCredentialStore

// Set a persistent ArcGIS credential store on the ArcGIS environment.
arcGISCredentialStore = try await .makePersistent(
access: access,
synchronizesWithiCloud: synchronizesWithiCloud
)

do {
// Set a persistent network credential store on the ArcGIS environment.
await setNetworkCredentialStore(
try await .makePersistent(access: access, synchronizesWithiCloud: synchronizesWithiCloud)
)
} catch {
// If making the shared network credential store persistent fails,
// then restore the ArcGIS credential store.
arcGISCredentialStore = previousArcGISCredentialStore
throw error
}
}

/// Clears all ArcGIS and network credentials from the respective stores.
/// Note: This sets up new `URLSessions` so that removed network credentials are respected
/// right away.
func clearCredentialStores() async {
// Clear ArcGIS Credentials.
arcGISCredentialStore.removeAll()

// Clear network credentials.
await networkCredentialStore.removeAll()
}

/// Revokes tokens of OAuth user credentials.
/// - Returns: `true` if successfully revokes tokens for all `OAuthUserCredential`, otherwise
/// `false`.
@discardableResult
func revokeOAuthTokens() async -> Bool {
let oAuthUserCredentials = arcGISCredentialStore.credentials.compactMap { $0 as? OAuthUserCredential }
return await withTaskGroup(of: Bool.self, returning: Bool.self) { group in
for credential in oAuthUserCredentials {
group.addTask {
do {
try await credential.revokeToken()
return true
} catch {
return false
}
}
}

return await group.allSatisfy { $0 == true }
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,49 +37,6 @@ public final class Authenticator: ObservableObject {
self.oAuthUserConfigurations = oAuthUserConfigurations
}

/// Sets up new credential stores that will be persisted to the keychain.
/// - Remark: The credentials will be stored in the default access group of the keychain.
/// You can find more information about what the default group would be here:
/// https://developer.apple.com/documentation/security/keychain_services/keychain_items/sharing_access_to_keychain_items_among_a_collection_of_apps
/// - Parameters:
/// - access: When the credentials stored in the keychain can be accessed.
/// - synchronizesWithiCloud: A Boolean value indicating whether the credentials are synchronized with iCloud.
public func setupPersistentCredentialStorage(
access: ArcGIS.KeychainAccess,
synchronizesWithiCloud: Bool = false
) async throws {
let previousArcGISCredentialStore = ArcGISEnvironment.authenticationManager.arcGISCredentialStore

// Set a persistent ArcGIS credential store on the ArcGIS environment.
ArcGISEnvironment.authenticationManager.arcGISCredentialStore = try await .makePersistent(
access: access,
synchronizesWithiCloud: synchronizesWithiCloud
)

do {
// Set a persistent network credential store on the ArcGIS environment.
await ArcGISEnvironment.authenticationManager.setNetworkCredentialStore(
try await .makePersistent(access: access, synchronizesWithiCloud: synchronizesWithiCloud)
)
} catch {
// If making the shared network credential store persistent fails,
// then restore the ArcGIS credential store.
ArcGISEnvironment.authenticationManager.arcGISCredentialStore = previousArcGISCredentialStore
throw error
}
}

/// Clears all ArcGIS and network credentials from the respective stores.
/// Note: This sets up new `URLSessions` so that removed network credentials are respected
/// right away.
public func clearCredentialStores() async {
// Clear ArcGIS Credentials.
ArcGISEnvironment.authenticationManager.arcGISCredentialStore.removeAll()

// Clear network credentials.
await ArcGISEnvironment.authenticationManager.networkCredentialStore.removeAll()
}

/// The current challenge.
/// This property is not set for OAuth challenges.
@Published var currentChallenge: ChallengeContinuation?
Expand Down
7 changes: 2 additions & 5 deletions Tests/ArcGISToolkitTests/AuthenticatorTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,8 @@ import Combine
}

// This tests that calling setupPersistentCredentialStorage tries to sync with the keychain.
let authenticator = Authenticator()
do {
try await authenticator.setupPersistentCredentialStorage(access: .whenUnlocked)
try await ArcGISEnvironment.authenticationManager.setupPersistentCredentialStorage(access: .whenUnlocked)
XCTFail("Expected an error to be thrown as unit tests should not have access to the keychain")
} catch {}
}
Expand All @@ -55,12 +54,10 @@ import Combine
)
)

let authenticator = Authenticator()

var arcGISCreds = ArcGISEnvironment.authenticationManager.arcGISCredentialStore.credentials
XCTAssertEqual(arcGISCreds.count, 1)

await authenticator.clearCredentialStores()
await ArcGISEnvironment.authenticationManager.clearCredentialStores()

arcGISCreds = ArcGISEnvironment.authenticationManager.arcGISCredentialStore.credentials
XCTAssertTrue(arcGISCreds.isEmpty)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,19 @@ import XCTest
import ArcGIS

extension XCTestCase {
func setChallengeHandler(_ challengeHandler: ArcGISAuthenticationChallengeHandler) {
/// Sets up an ArcGIS challenge handler on the `ArcGISURLSession` and registers a tear-down block to
/// reset it to previous handler.
func setArcGISChallengeHandler(_ challengeHandler: ArcGISAuthenticationChallengeHandler) {
let previous = ArcGISEnvironment.authenticationManager.arcGISAuthenticationChallengeHandler
ArcGISEnvironment.authenticationManager.arcGISAuthenticationChallengeHandler = challengeHandler
addTeardownBlock { ArcGISEnvironment.authenticationManager.arcGISAuthenticationChallengeHandler = previous }
}

/// Sets up a network challenge handler on the `ArcGISURLSession` and registers a tear-down block to
/// reset it to previous handler.
func setNetworkChallengeHandler(_ challengeHandler: NetworkAuthenticationChallengeHandler) {
let previous = ArcGISEnvironment.authenticationManager.networkAuthenticationChallengeHandler
ArcGISEnvironment.authenticationManager.networkAuthenticationChallengeHandler = challengeHandler
addTeardownBlock { ArcGISEnvironment.authenticationManager.networkAuthenticationChallengeHandler = previous }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
//
// COPYRIGHT 1995-2022 ESRI
//
// TRADE SECRETS: ESRI PROPRIETARY AND CONFIDENTIAL
// Unpublished material - all rights reserved under the
// Copyright Laws of the United States and applicable international
// laws, treaties, and conventions.
//
// For additional information, contact:
// Environmental Systems Research Institute, Inc.
// Attn: Contracts and Legal Services Department
// 380 New York Street
// Redlands, California, 92373
// USA
//
// email: [email protected]
//

import Foundation
import ArcGIS

/// An `ArcGISChallengeHandler` that can handle challenges using ArcGIS credential.
final class ArcGISChallengeHandler: ArcGISAuthenticationChallengeHandler {
/// The arcgis credential used when an ArcGIS challenge is received.
let credentialProvider: ((ArcGISAuthenticationChallenge) async throws -> ArcGISCredential?)

/// The ArcGIS authentication challenges.
private(set) var challenges: [ArcGISAuthenticationChallenge] = []

init(
credentialProvider: @escaping ((ArcGISAuthenticationChallenge) async throws -> ArcGISCredential?)
) {
self.credentialProvider = credentialProvider
}

func handleArcGISAuthenticationChallenge(
_ challenge: ArcGISAuthenticationChallenge
) async throws -> ArcGISAuthenticationChallenge.Disposition {
challenges.append(challenge)

if let arcgisCredential = try await credentialProvider(challenge) {
return .continueWithCredential(arcgisCredential)
} else {
return .continueWithoutCredential
}
}
}
Loading