Skip to content

Commit 09f0583

Browse files
committed
feat(Auth) Keychain Sharing (App Reload Required)
* Remove migrateKeychainItemsOfUserSession bool from SecureStoragePreferences
1 parent 81a4864 commit 09f0583

File tree

19 files changed

+794
-24
lines changed

19 files changed

+794
-24
lines changed
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
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+
public struct AccessGroup {
11+
public let name: String?
12+
public let migrateKeychainItems: Bool
13+
14+
public init(name: String, migrateKeychainItemsOfUserSession: Bool = false) {
15+
self.init(name: name, migrateKeychainItems: migrateKeychainItemsOfUserSession)
16+
}
17+
18+
public static func none(migrateKeychainItemsOfUserSession: Bool) -> AccessGroup {
19+
return .init(name: nil, migrateKeychainItems: migrateKeychainItemsOfUserSession)
20+
}
21+
22+
public static var none: AccessGroup {
23+
return .none(migrateKeychainItemsOfUserSession: false)
24+
}
25+
26+
private init(name: String?, migrateKeychainItems: Bool) {
27+
self.name = name
28+
self.migrateKeychainItems = migrateKeychainItems
29+
}
30+
}

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,8 @@ extension AWSCognitoAuthPlugin {
177177
}
178178

179179
private func makeCredentialStore() -> AmplifyAuthCredentialStoreBehavior {
180-
AWSCognitoAuthCredentialStore(authConfiguration: authConfiguration)
180+
return AWSCognitoAuthCredentialStore(authConfiguration: authConfiguration, accessGroup: secureStoragePreferences?.accessGroup?.name,
181+
migrateKeychainItemsOfUserSession: secureStoragePreferences?.accessGroup?.migrateKeychainItems ?? false)
181182
}
182183

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

AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/AWSCognitoAuthPlugin.swift

Lines changed: 9 additions & 7 deletions
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 optionally custom network
50+
/// preferences and 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/CredentialStorage/AWSCognitoAuthCredentialStore.swift

Lines changed: 101 additions & 2 deletions
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,29 @@ struct AWSCognitoAuthCredentialStore {
2526
private var isKeychainConfiguredKey: String {
2627
"\(userDefaultsNameSpace).isKeychainConfigured"
2728
}
29+
private var accessGroupKey: String {
30+
"\(userDefaultsNameSpace).accessGroup"
31+
}
2832

2933
private let authConfiguration: AuthConfiguration
3034
private let keychain: KeychainStoreBehavior
3135
private let userDefaults = UserDefaults.standard
36+
private let accessGroup: String?
3237

33-
init(authConfiguration: AuthConfiguration, accessGroup: String? = nil) {
38+
init(authConfiguration: AuthConfiguration, accessGroup: String? = nil, migrateKeychainItemsOfUserSession: Bool = false) {
3439
self.authConfiguration = authConfiguration
35-
self.keychain = KeychainStore(service: service, accessGroup: accessGroup)
40+
self.accessGroup = accessGroup
41+
if let accessGroup {
42+
self.keychain = KeychainStore(service: sharedService, accessGroup: accessGroup)
43+
} else {
44+
self.keychain = KeychainStore(service: service)
45+
}
46+
47+
if migrateKeychainItemsOfUserSession {
48+
try? migrateKeychainItemsToAccessGroup()
49+
}
50+
51+
try? saveStoredAccessGroup()
3652

3753
if !userDefaults.bool(forKey: isKeychainConfiguredKey) {
3854
try? clearAllCredentials()
@@ -182,6 +198,81 @@ extension AWSCognitoAuthCredentialStore: AmplifyAuthCredentialStoreBehavior {
182198
private func clearAllCredentials() throws {
183199
try keychain._removeAll()
184200
}
201+
202+
private func retrieveStoredAccessGroup() throws -> String? {
203+
return userDefaults.string(forKey: accessGroupKey)
204+
}
205+
206+
private func saveStoredAccessGroup() throws {
207+
if let accessGroup {
208+
userDefaults.set(accessGroup, forKey: accessGroupKey)
209+
} else {
210+
userDefaults.removeObject(forKey: accessGroupKey)
211+
}
212+
}
213+
214+
private func migrateKeychainItemsToAccessGroup() throws {
215+
let oldAccessGroup = try? retrieveStoredAccessGroup()
216+
let oldKeychain: KeychainStoreBehavior
217+
218+
if oldAccessGroup == accessGroup {
219+
log.verbose("[AWSCognitoAuthCredentialStore] Stored access group is the same as current access group, aborting migration")
220+
return
221+
}
222+
223+
if let oldAccessGroup {
224+
oldKeychain = KeychainStore(service: sharedService, accessGroup: oldAccessGroup)
225+
} else {
226+
oldKeychain = KeychainStore(service: service)
227+
}
228+
229+
let authCredentialStoreKey = generateSessionKey(for: authConfiguration)
230+
let authCredentialData: Data
231+
let awsCredential: AmplifyCredentials
232+
do {
233+
authCredentialData = try oldKeychain._getData(authCredentialStoreKey)
234+
awsCredential = try decode(data: authCredentialData)
235+
} catch {
236+
log.verbose("[AWSCognitoAuthCredentialStore] Could not retrieve previous credentials in keychain under old access group, nothing to migrate")
237+
return
238+
}
239+
240+
guard awsCredential.areValid() else {
241+
log.verbose("[AWSCognitoAuthCredentialStore] Credentials found are not valid (expired) in old access group keychain, aborting migration")
242+
return
243+
}
244+
245+
let oldItems: [(key: String, value: Data)]
246+
do {
247+
oldItems = try oldKeychain._getAll()
248+
} catch {
249+
log.error("[AWSCognitoAuthCredentialStore] Error getting all items from keychain under old access group, aborting migration")
250+
return
251+
}
252+
253+
if oldItems.isEmpty {
254+
log.verbose("[AWSCognitoAuthCredentialStore] No items in keychain under old access group, clearing keychain items under new access group")
255+
return
256+
}
257+
258+
for item in oldItems {
259+
do {
260+
try keychain._set(item.value, key: item.key)
261+
} catch {
262+
log.error("[AWSCognitoAuthCredentialStore] Error migrating one of the items, aborting migration: \(error)")
263+
try? clearAllCredentials()
264+
return
265+
}
266+
}
267+
268+
do {
269+
try oldKeychain._removeAll()
270+
} catch {
271+
log.error("[AWSCognitoAuthCredentialStore] Error deleting all items from keychain under old access group after migration")
272+
}
273+
274+
log.verbose("[AWSCognitoAuthCredentialStore] Migration of keychain items from old access group to new access group successful")
275+
}
185276

186277
}
187278

@@ -205,3 +296,11 @@ private extension AWSCognitoAuthCredentialStore {
205296
}
206297

207298
}
299+
300+
extension AWSCognitoAuthCredentialStore: DefaultLogger {
301+
public static var log: Logger {
302+
Amplify.Logging.logger(forNamespace: String(describing: self))
303+
}
304+
305+
public nonisolated var log: Logger { Self.log }
306+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
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+
public struct AWSCognitoSecureStoragePreferences {
12+
13+
/// The access group that the keychain will use for auth items
14+
public let accessGroup: AccessGroup?
15+
16+
public init(accessGroup: AccessGroup? = nil) {
17+
self.accessGroup = accessGroup
18+
}
19+
}

AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/ActionTests/CredentialStore/MockCredentialStoreBehavior.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,15 @@ class MockKeychainStoreBehavior: KeychainStoreBehavior {
1515
typealias VoidHandler = () -> Void
1616

1717
let data: String
18+
let allData: [(key: String, value: Data)]
1819
let removeAllHandler: VoidHandler?
20+
let mockKey: String = "mockKey"
1921

2022
init(data: String,
2123
removeAllHandler: VoidHandler? = nil) {
2224
self.data = data
2325
self.removeAllHandler = removeAllHandler
26+
self.allData = [(key: mockKey, value: Data(data.utf8))]
2427
}
2528

2629
func _getString(_ key: String) throws -> String {
@@ -41,4 +44,8 @@ class MockKeychainStoreBehavior: KeychainStoreBehavior {
4144
func _removeAll() throws {
4245
removeAllHandler?()
4346
}
47+
48+
func _getAll() throws -> [(key: String, value: Data)] {
49+
return allData
50+
}
4451
}

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

Lines changed: 80 additions & 0 deletions
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)