amplify-swift/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/CredentialStorage/AWSCognitoAuthCredentialSto...

208 lines
7.9 KiB
Swift

//
// Copyright Amazon.com Inc. or its affiliates.
// All Rights Reserved.
//
// SPDX-License-Identifier: Apache-2.0
//
import Foundation
import Amplify
@_spi(KeychainStore) import AWSPluginsCore
struct AWSCognitoAuthCredentialStore {
// Credential store constants
private let service = "com.amplify.awsCognitoAuthPlugin"
private let sessionKey = "session"
private let deviceMetadataKey = "deviceMetadata"
private let deviceASFKey = "deviceASF"
private let authConfigurationKey = "authConfiguration"
// User defaults constants
private let userDefaultsNameSpace = "amplify_secure_storage_scopes.awsCognitoAuthPlugin"
/// This UserDefault Key is used to check if Keychain already has items stored on a fresh install
/// If this flag doesn't exist, previous keychain values for Amplify would be wiped out
private var isKeychainConfiguredKey: String {
"\(userDefaultsNameSpace).isKeychainConfigured"
}
private let authConfiguration: AuthConfiguration
private let keychain: KeychainStoreBehavior
private let userDefaults = UserDefaults.standard
init(authConfiguration: AuthConfiguration, accessGroup: String? = nil) {
self.authConfiguration = authConfiguration
self.keychain = KeychainStore(service: service, accessGroup: accessGroup)
if !userDefaults.bool(forKey: isKeychainConfiguredKey) {
try? clearAllCredentials()
userDefaults.set(true, forKey: isKeychainConfiguredKey)
}
restoreCredentialsOnConfigurationChanges(currentAuthConfig: authConfiguration)
// Save the current configuration
saveAuthConfiguration(authConfig: authConfiguration)
}
// The method is responsible for migrating any old credentials to the new namespace
private func restoreCredentialsOnConfigurationChanges(currentAuthConfig: AuthConfiguration) {
guard let oldAuthConfigData = getAuthConfiguration() else {
return
}
let oldNameSpace = generateSessionKey(for: oldAuthConfigData)
let newNameSpace = generateSessionKey(for: currentAuthConfig)
let oldUserPoolConfiguration = oldAuthConfigData.getUserPoolConfiguration()
let oldIdentityPoolConfiguration = oldAuthConfigData.getIdentityPoolConfiguration()
let newIdentityConfigData = currentAuthConfig.getIdentityPoolConfiguration()
/// Only migrate if
/// - Old User Pool Config didn't exist
/// - New Identity Config Data exists
/// - Old Identity Pool Config == New Identity Pool Config
if oldUserPoolConfiguration == nil &&
newIdentityConfigData != nil &&
oldIdentityPoolConfiguration == newIdentityConfigData
{
// retrieve data from the old namespace and save with the new namespace
if let oldCognitoCredentialsData = try? keychain._getData(oldNameSpace) {
try? keychain._set(oldCognitoCredentialsData, key: newNameSpace)
}
} else if oldAuthConfigData != currentAuthConfig {
// Clear the old credentials
try? keychain._remove(oldNameSpace)
}
}
private func storeKey(for authConfiguration: AuthConfiguration) -> String {
let prefix = "amplify"
var suffix = ""
switch authConfiguration {
case .userPools(let userPoolConfigurationData):
suffix = userPoolConfigurationData.poolId
case .identityPools(let identityPoolConfigurationData):
suffix = identityPoolConfigurationData.poolId
case .userPoolsAndIdentityPools(let userPoolConfigurationData, let identityPoolConfigurationData):
suffix = "\(userPoolConfigurationData.poolId).\(identityPoolConfigurationData.poolId)"
}
return "\(prefix).\(suffix)"
}
private func generateSessionKey(for authConfiguration: AuthConfiguration) -> String {
return "\(storeKey(for: authConfiguration)).\(sessionKey)"
}
private func generateDeviceMetadataKey(
for username: String,
with configuration: AuthConfiguration) -> String {
return "\(storeKey(for: authConfiguration)).\(username).\(deviceMetadataKey)"
}
private func generateASFDeviceKey(
for username: String,
with configuration: AuthConfiguration) -> String {
return "\(storeKey(for: authConfiguration)).\(username).\(deviceASFKey)"
}
private func saveAuthConfiguration(authConfig: AuthConfiguration) {
if let encodedAuthConfigData = try? encode(object: authConfig) {
try? keychain._set(encodedAuthConfigData, key: authConfigurationKey)
}
}
private func getAuthConfiguration() -> AuthConfiguration? {
if let userPoolConfigData = try? keychain._getData(authConfigurationKey) {
return try? decode(data: userPoolConfigData)
}
return nil
}
}
extension AWSCognitoAuthCredentialStore: AmplifyAuthCredentialStoreBehavior {
func saveCredential(_ credential: AmplifyCredentials) throws {
let authCredentialStoreKey = generateSessionKey(for: authConfiguration)
let encodedCredentials = try encode(object: credential)
try keychain._set(encodedCredentials, key: authCredentialStoreKey)
}
func retrieveCredential() throws -> AmplifyCredentials {
let authCredentialStoreKey = generateSessionKey(for: authConfiguration)
let authCredentialData = try keychain._getData(authCredentialStoreKey)
let awsCredential: AmplifyCredentials = try decode(data: authCredentialData)
return awsCredential
}
func deleteCredential() throws {
let authCredentialStoreKey = generateSessionKey(for: authConfiguration)
try keychain._remove(authCredentialStoreKey)
}
func saveDevice(_ deviceMetadata: DeviceMetadata, for username: String) throws {
let key = generateDeviceMetadataKey(for: username, with: authConfiguration)
let encodedMetadata = try encode(object: deviceMetadata)
try keychain._set(encodedMetadata, key: key)
}
func retrieveDevice(for username: String) throws -> DeviceMetadata {
let key = generateDeviceMetadataKey(for: username, with: authConfiguration)
let encodedDeviceMetadata = try keychain._getData(key)
let deviceMetadata: DeviceMetadata = try decode(data: encodedDeviceMetadata)
return deviceMetadata
}
func removeDevice(for username: String) throws {
let key = generateDeviceMetadataKey(for: username, with: authConfiguration)
try keychain._remove(key)
}
func saveASFDevice(_ deviceId: String, for username: String) throws {
let key = generateASFDeviceKey(for: username, with: authConfiguration)
let encodedMetadata = try encode(object: deviceId)
try keychain._set(encodedMetadata, key: key)
}
func retrieveASFDevice(for username: String) throws -> String {
let key = generateASFDeviceKey(for: username, with: authConfiguration)
let encodedData = try keychain._getData(key)
let asfID: String = try decode(data: encodedData)
return asfID
}
func removeASFDevice(for username: String) throws {
let key = generateASFDeviceKey(for: username, with: authConfiguration)
try keychain._remove(key)
}
private func clearAllCredentials() throws {
try keychain._removeAll()
}
}
/// Helpers for encode and decoding
private extension AWSCognitoAuthCredentialStore {
func encode<T: Codable>(object: T) throws -> Data {
do {
return try JSONEncoder().encode(object)
} catch {
throw KeychainStoreError.codingError("Error occurred while encoding AWSCredentials", error)
}
}
func decode<T: Decodable>(data: Data) throws -> T {
do {
return try JSONDecoder().decode(T.self, from: data)
} catch {
throw KeychainStoreError.codingError("Error occurred while decoding AWSCredentials", error)
}
}
}