Skip to content

Commit f15cc45

Browse files
harsh62yaroluchkoaws-amplify-ops
authored
feat(auth): adding support for keychain sharing using app groups (#3947)
* feat(Auth) Keychain Sharing (App Reload Required) * Remove migrateKeychainItemsOfUserSession bool from SecureStoragePreferences * Reconfigure when fetching auth session if sharing keychain * Update API dumps for new version * Indentation, clean up, and batch migration to avoid inconsistent state * Update API dumps for new version * Addressing review comments: documentation, no more credentials valid check, only delete items if absolutely necessary * Style fixes --------- Co-authored-by: Yaro Luchko <[email protected]> Co-authored-by: aws-amplify-ops <[email protected]>
1 parent 695039d commit f15cc45

23 files changed

+1341
-2070
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
//
2+
// Copyright Amazon.com Inc. or its affiliates.
3+
// All Rights Reserved.
4+
//
5+
// SPDX-License-Identifier: Apache-2.0
6+
//
7+
8+
import Foundation
9+
10+
/// A structure representing an access group for managing keychain items.
11+
public struct AccessGroup {
12+
/// The name of the access group.
13+
public let name: String?
14+
15+
/// A flag indicating whether to migrate keychain items.
16+
public let migrateKeychainItems: Bool
17+
18+
/**
19+
Initializes an `AccessGroup` with the specified name and migration option.
20+
21+
- Parameter name: The name of the access group.
22+
- Parameter migrateKeychainItemsOfUserSession: A flag indicating whether to migrate keychain items. Defaults to `false`.
23+
*/
24+
public init(name: String, migrateKeychainItemsOfUserSession: Bool = false) {
25+
self.init(name: name, migrateKeychainItems: migrateKeychainItemsOfUserSession)
26+
}
27+
28+
/**
29+
Creates an `AccessGroup` instance with no specified name.
30+
31+
- Parameter migrateKeychainItemsOfUserSession: A flag indicating whether to migrate keychain items.
32+
- Returns: An `AccessGroup` instance with the migration option set.
33+
*/
34+
public static func none(migrateKeychainItemsOfUserSession: Bool) -> AccessGroup {
35+
return .init(migrateKeychainItems: migrateKeychainItemsOfUserSession)
36+
}
37+
38+
/**
39+
A static property representing an `AccessGroup` with no name and no migration.
40+
41+
- Returns: An `AccessGroup` instance with no name and the migration option set to `false`.
42+
*/
43+
public static var none: AccessGroup {
44+
return .none(migrateKeychainItemsOfUserSession: false)
45+
}
46+
47+
private init(name: String? = nil, migrateKeychainItems: Bool) {
48+
self.name = name
49+
self.migrateKeychainItems = migrateKeychainItems
50+
}
51+
}

AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/AWSCognitoAuthPlugin+Configure.swift

+5-1
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,11 @@ extension AWSCognitoAuthPlugin {
186186
}
187187

188188
private func makeCredentialStore() -> AmplifyAuthCredentialStoreBehavior {
189-
AWSCognitoAuthCredentialStore(authConfiguration: authConfiguration)
189+
return AWSCognitoAuthCredentialStore(
190+
authConfiguration: authConfiguration,
191+
accessGroup: secureStoragePreferences?.accessGroup?.name,
192+
migrateKeychainItemsOfUserSession: secureStoragePreferences?.accessGroup?.migrateKeychainItems ?? false
193+
)
190194
}
191195

192196
private func makeLegacyKeychainStore(service: String) -> KeychainStoreBehavior {

AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/AWSCognitoAuthPlugin.swift

+9-7
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ public final class AWSCognitoAuthPlugin: AWSCognitoAuthPluginBehavior {
3535
/// The user network preferences for timeout and retry
3636
let networkPreferences: AWSCognitoNetworkPreferences?
3737

38+
/// The user secure storage preferences for access group
39+
let secureStoragePreferences: AWSCognitoSecureStoragePreferences?
40+
3841
@_spi(InternalAmplifyConfiguration)
3942
internal(set) public var jsonConfiguration: JSONValue?
4043

@@ -43,15 +46,14 @@ public final class AWSCognitoAuthPlugin: AWSCognitoAuthPluginBehavior {
4346
return "awsCognitoAuthPlugin"
4447
}
4548

46-
/// Instantiates an instance of the AWSCognitoAuthPlugin.
47-
public init() {
48-
self.networkPreferences = nil
49-
}
50-
51-
/// Instantiates an instance of the AWSCognitoAuthPlugin with custom network preferences
49+
/// Instantiates an instance of the AWSCognitoAuthPlugin with optional custom network
50+
/// preferences and optional custom secure storage preferences
5251
/// - Parameters:
5352
/// - networkPreferences: network preferences
54-
public init(networkPreferences: AWSCognitoNetworkPreferences) {
53+
/// - secureStoragePreferences: secure storage preferences
54+
public init(networkPreferences: AWSCognitoNetworkPreferences? = nil,
55+
secureStoragePreferences: AWSCognitoSecureStoragePreferences = AWSCognitoSecureStoragePreferences()) {
5556
self.networkPreferences = networkPreferences
57+
self.secureStoragePreferences = secureStoragePreferences
5658
}
5759
}

AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/ClientBehavior/AWSCognitoAuthPlugin+ClientBehavior.swift

+5-1
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,11 @@ extension AWSCognitoAuthPlugin: AuthCategoryBehavior {
114114
public func fetchAuthSession(options: AuthFetchSessionRequest.Options?) async throws -> AuthSession {
115115
let options = options ?? AuthFetchSessionRequest.Options()
116116
let request = AuthFetchSessionRequest(options: options)
117-
let task = AWSAuthFetchSessionTask(request, authStateMachine: authStateMachine)
117+
let forceReconfigure = secureStoragePreferences?.accessGroup?.name != nil
118+
let task = AWSAuthFetchSessionTask(request,
119+
authStateMachine: authStateMachine,
120+
configuration: authConfiguration,
121+
forceReconfigure: forceReconfigure)
118122
return try await taskQueue.sync {
119123
return try await task.value
120124
} as! AuthSession

AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/CredentialStorage/AWSCognitoAuthCredentialStore.swift

+64-2
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ struct AWSCognitoAuthCredentialStore {
1313

1414
// Credential store constants
1515
private let service = "com.amplify.awsCognitoAuthPlugin"
16+
private let sharedService = "com.amplify.awsCognitoAuthPluginShared"
1617
private let sessionKey = "session"
1718
private let deviceMetadataKey = "deviceMetadata"
1819
private let deviceASFKey = "deviceASF"
@@ -25,14 +26,40 @@ struct AWSCognitoAuthCredentialStore {
2526
private var isKeychainConfiguredKey: String {
2627
"\(userDefaultsNameSpace).isKeychainConfigured"
2728
}
29+
/// This UserDefaults Key is use to retrieve the stored access group to determine
30+
/// which access group the migration should happen from
31+
/// If none is found, the unshared service is used for migration and all items
32+
/// under that service are queried
33+
private var accessGroupKey: String {
34+
"\(userDefaultsNameSpace).accessGroup"
35+
}
2836

2937
private let authConfiguration: AuthConfiguration
3038
private let keychain: KeychainStoreBehavior
3139
private let userDefaults = UserDefaults.standard
40+
private let accessGroup: String?
3241

33-
init(authConfiguration: AuthConfiguration, accessGroup: String? = nil) {
42+
init(
43+
authConfiguration: AuthConfiguration,
44+
accessGroup: String? = nil,
45+
migrateKeychainItemsOfUserSession: Bool = false
46+
) {
3447
self.authConfiguration = authConfiguration
35-
self.keychain = KeychainStore(service: service, accessGroup: accessGroup)
48+
self.accessGroup = accessGroup
49+
if let accessGroup {
50+
self.keychain = KeychainStore(service: sharedService, accessGroup: accessGroup)
51+
} else {
52+
self.keychain = KeychainStore(service: service)
53+
}
54+
55+
let oldAccessGroup = retrieveStoredAccessGroup()
56+
if migrateKeychainItemsOfUserSession {
57+
try? migrateKeychainItemsToAccessGroup()
58+
} else if oldAccessGroup == nil && oldAccessGroup != accessGroup {
59+
try? KeychainStore(service: service)._removeAll()
60+
}
61+
62+
saveStoredAccessGroup()
3663

3764
if !userDefaults.bool(forKey: isKeychainConfiguredKey) {
3865
try? clearAllCredentials()
@@ -181,6 +208,39 @@ extension AWSCognitoAuthCredentialStore: AmplifyAuthCredentialStoreBehavior {
181208
private func clearAllCredentials() throws {
182209
try keychain._removeAll()
183210
}
211+
212+
private func retrieveStoredAccessGroup() -> String? {
213+
return userDefaults.string(forKey: accessGroupKey)
214+
}
215+
216+
private func saveStoredAccessGroup() {
217+
if let accessGroup {
218+
userDefaults.set(accessGroup, forKey: accessGroupKey)
219+
} else {
220+
userDefaults.removeObject(forKey: accessGroupKey)
221+
}
222+
}
223+
224+
private func migrateKeychainItemsToAccessGroup() throws {
225+
let oldAccessGroup = retrieveStoredAccessGroup()
226+
227+
if oldAccessGroup == accessGroup {
228+
log.info("[AWSCognitoAuthCredentialStore] Stored access group is the same as current access group, aborting migration")
229+
return
230+
}
231+
232+
let oldService = oldAccessGroup != nil ? sharedService : service
233+
let newService = accessGroup != nil ? sharedService : service
234+
235+
do {
236+
try KeychainStoreMigrator(oldService: oldService, newService: newService, oldAccessGroup: oldAccessGroup, newAccessGroup: accessGroup).migrate()
237+
} catch {
238+
log.error("[AWSCognitoAuthCredentialStore] Migration has failed")
239+
return
240+
}
241+
242+
log.verbose("[AWSCognitoAuthCredentialStore] Migration of keychain items from old access group to new access group successful")
243+
}
184244

185245
}
186246

@@ -204,3 +264,5 @@ private extension AWSCognitoAuthCredentialStore {
204264
}
205265

206266
}
267+
268+
extension AWSCognitoAuthCredentialStore: DefaultLogger { }
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
//
2+
// Copyright Amazon.com Inc. or its affiliates.
3+
// All Rights Reserved.
4+
//
5+
// SPDX-License-Identifier: Apache-2.0
6+
//
7+
8+
import Foundation
9+
import Amplify
10+
11+
/// A struct to store preferences for how the plugin uses storage
12+
public struct AWSCognitoSecureStoragePreferences {
13+
14+
/// The access group that the keychain will use for auth items
15+
public let accessGroup: AccessGroup?
16+
17+
/// Creates an intstance of AWSCognitoSecureStoragePreferences
18+
/// - Parameters:
19+
/// - accessGroup: access group to be used
20+
public init(accessGroup: AccessGroup? = nil) {
21+
self.accessGroup = accessGroup
22+
}
23+
}

AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Task/AWSAuthFetchSessionTask.swift

+16-1
Original file line numberDiff line numberDiff line change
@@ -13,19 +13,34 @@ class AWSAuthFetchSessionTask: AuthFetchSessionTask, DefaultLogger {
1313
private let authStateMachine: AuthStateMachine
1414
private let fetchAuthSessionHelper: FetchAuthSessionOperationHelper
1515
private let taskHelper: AWSAuthTaskHelper
16+
private let configuration: AuthConfiguration
17+
private let forceReconfigure: Bool
1618

1719
var eventName: HubPayloadEventName {
1820
HubPayload.EventName.Auth.fetchSessionAPI
1921
}
2022

21-
init(_ request: AuthFetchSessionRequest, authStateMachine: AuthStateMachine) {
23+
init(
24+
_ request: AuthFetchSessionRequest,
25+
authStateMachine: AuthStateMachine,
26+
configuration: AuthConfiguration,
27+
forceReconfigure: Bool = false
28+
) {
2229
self.request = request
2330
self.authStateMachine = authStateMachine
2431
self.fetchAuthSessionHelper = FetchAuthSessionOperationHelper()
2532
self.taskHelper = AWSAuthTaskHelper(authStateMachine: authStateMachine)
33+
self.configuration = configuration
34+
self.forceReconfigure = forceReconfigure
2635
}
2736

2837
func execute() async throws -> AuthSession {
38+
log.verbose("Starting execution")
39+
if forceReconfigure {
40+
log.verbose("Reconfiguring auth state machine for keychain sharing")
41+
let event = AuthEvent(eventType: .reconfigure(configuration))
42+
await authStateMachine.send(event)
43+
}
2944
await taskHelper.didStateMachineConfigured()
3045
let doesNeedForceRefresh = request.options.forceRefresh
3146
return try await fetchAuthSessionHelper.fetch(authStateMachine,

AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/ConfigurationTests/AWSCognitoAuthPluginAmplifyOutputsConfigTests.swift

+80
Original file line numberDiff line numberDiff line change
@@ -123,4 +123,84 @@ class AWSCognitoAuthPluginAmplifyOutputsConfigTests: XCTestCase {
123123
XCTFail("Should not throw error. \(error)")
124124
}
125125
}
126+
127+
/// Test Auth configuration with valid config for user pool and identity pool, with secure storage preferences
128+
///
129+
/// - Given: Given valid config for user pool and identity pool with secure storage preferences
130+
/// - When:
131+
/// - I configure auth with the given configuration and secure storage preferences
132+
/// - Then:
133+
/// - I should not get any error while configuring auth
134+
///
135+
func testConfigWithUserPoolAndIdentityPoolWithSecureStoragePreferences() throws {
136+
let plugin = AWSCognitoAuthPlugin(
137+
secureStoragePreferences: .init(
138+
accessGroup: AccessGroup(name: "xx")
139+
)
140+
)
141+
try Amplify.add(plugin: plugin)
142+
143+
let amplifyConfig = AmplifyOutputsData(auth: .init(
144+
awsRegion: "us-east-1",
145+
userPoolId: "xx",
146+
userPoolClientId: "xx",
147+
identityPoolId: "xx"))
148+
149+
do {
150+
try Amplify.configure(amplifyConfig)
151+
152+
let escapeHatch = plugin.getEscapeHatch()
153+
guard case .userPoolAndIdentityPool(let userPoolClient, let identityPoolClient) = escapeHatch else {
154+
XCTFail("Expected .userPool, got \(escapeHatch)")
155+
return
156+
}
157+
XCTAssertNotNil(userPoolClient)
158+
XCTAssertNotNil(identityPoolClient)
159+
160+
} catch {
161+
XCTFail("Should not throw error. \(error)")
162+
}
163+
}
164+
165+
/// Test Auth configuration with valid config for user pool and identity pool, with network preferences and secure storage preferences
166+
///
167+
/// - Given: Given valid config for user pool and identity pool, network preferences, and secure storage preferences
168+
/// - When:
169+
/// - I configure auth with the given configuration, network preferences, and secure storage preferences
170+
/// - Then:
171+
/// - I should not get any error while configuring auth
172+
///
173+
func testConfigWithUserPoolAndIdentityPoolWithNetworkPreferencesAndSecureStoragePreferences() throws {
174+
let plugin = AWSCognitoAuthPlugin(
175+
networkPreferences: .init(
176+
maxRetryCount: 2,
177+
timeoutIntervalForRequest: 60,
178+
timeoutIntervalForResource: 60),
179+
secureStoragePreferences: .init(
180+
accessGroup: AccessGroup(name: "xx")
181+
)
182+
)
183+
try Amplify.add(plugin: plugin)
184+
185+
let amplifyConfig = AmplifyOutputsData(auth: .init(
186+
awsRegion: "us-east-1",
187+
userPoolId: "xx",
188+
userPoolClientId: "xx",
189+
identityPoolId: "xx"))
190+
191+
do {
192+
try Amplify.configure(amplifyConfig)
193+
194+
let escapeHatch = plugin.getEscapeHatch()
195+
guard case .userPoolAndIdentityPool(let userPoolClient, let identityPoolClient) = escapeHatch else {
196+
XCTFail("Expected .userPool, got \(escapeHatch)")
197+
return
198+
}
199+
XCTAssertNotNil(userPoolClient)
200+
XCTAssertNotNil(identityPoolClient)
201+
202+
} catch {
203+
XCTFail("Should not throw error. \(error)")
204+
}
205+
}
126206
}

0 commit comments

Comments
 (0)