Skip to content

Commit de22fa8

Browse files
authored
fix(auth): Fix Device Metadata migration if alised userId was used (#2963)
1 parent 83605cb commit de22fa8

File tree

5 files changed

+226
-24
lines changed

5 files changed

+226
-24
lines changed

aws-auth-cognito/src/androidTest/java/com/amplifyframework/auth/cognito/AWSCognitoLegacyCredentialStoreInstrumentationTest.kt

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ import androidx.test.platform.app.InstrumentationRegistry
2020
import com.amplifyframework.auth.cognito.data.AWSCognitoLegacyCredentialStore
2121
import com.amplifyframework.auth.cognito.testutils.AuthConfigurationProvider
2222
import com.amplifyframework.auth.cognito.testutils.CredentialStoreUtil
23-
import org.junit.Assert.assertTrue
23+
import kotlin.test.assertEquals
24+
import org.junit.After
2425
import org.junit.Before
2526
import org.junit.Test
2627
import org.junit.runner.RunWith
@@ -31,20 +32,42 @@ class AWSCognitoLegacyCredentialStoreInstrumentationTest {
3132

3233
private val configuration: AuthConfiguration = AuthConfigurationProvider.getAuthConfiguration()
3334

34-
private val credential = CredentialStoreUtil.getDefaultCredential()
35+
private val credentialStoreUtil = CredentialStoreUtil()
36+
private val credential = credentialStoreUtil.getDefaultCredential()
3537

3638
private lateinit var store: AWSCognitoLegacyCredentialStore
3739

3840
@Before
3941
fun setup() {
4042
store = AWSCognitoLegacyCredentialStore(context, configuration)
4143
// TODO: Pull the appClientID from the configuration instead of hardcoding
42-
CredentialStoreUtil.setupLegacyStore(context, "userPoolAppClientId", "userPoolId", "identityPoolId")
44+
credentialStoreUtil.setupLegacyStore(context, "userPoolAppClientId", "userPoolId", "identityPoolId")
45+
}
46+
47+
@After
48+
fun tearDown() {
49+
credentialStoreUtil.clearSharedPreferences(context)
4350
}
4451

4552
@Test
4653
fun test_legacy_store_implementation_can_retrieve_credentials_stored_using_aws_sdk() {
47-
val creds = store.retrieveCredential()
48-
assertTrue(creds == credential)
54+
assertEquals(credential, store.retrieveCredential())
55+
}
56+
57+
@Test
58+
fun test_legacy_store_implementation_can_retrieve_device_metadata_using_aws_sdk() {
59+
val user1DeviceMetadata = store.retrieveDeviceMetadata(credentialStoreUtil.user1Username)
60+
val user2DeviceMetadata = store.retrieveDeviceMetadata(credentialStoreUtil.user2Username)
61+
62+
assertEquals(credentialStoreUtil.getUser1DeviceMetadata(), user1DeviceMetadata)
63+
assertEquals(credentialStoreUtil.getUser2DeviceMetadata(), user2DeviceMetadata)
64+
}
65+
66+
@Test
67+
fun test_legacy_store_implementation_can_retrieve_usernames_for_device_metadata() {
68+
val expectedUsernames = listOf(credentialStoreUtil.user1Username, credentialStoreUtil.user2Username)
69+
val deviceMetadataUsernames = store.retrieveDeviceMetadataUsernameList()
70+
71+
assertEquals(expectedUsernames, deviceMetadataUsernames)
4972
}
5073
}

aws-auth-cognito/src/androidTest/java/com/amplifyframework/auth/cognito/CredentialStoreStateMachineInstrumentationTest.kt

Lines changed: 84 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,27 +20,33 @@ import androidx.test.platform.app.InstrumentationRegistry
2020
import com.amplifyframework.auth.cognito.data.AWSCognitoAuthCredentialStore
2121
import com.amplifyframework.auth.cognito.testutils.AuthConfigurationProvider
2222
import com.amplifyframework.auth.cognito.testutils.CredentialStoreUtil
23+
import com.amplifyframework.statemachine.codegen.data.DeviceMetadata
2324
import com.google.gson.Gson
2425
import junit.framework.TestCase.assertEquals
2526
import org.json.JSONObject
27+
import org.junit.After
2628
import org.junit.Before
2729
import org.junit.Test
2830
import org.junit.runner.RunWith
2931

3032
@RunWith(AndroidJUnit4::class)
3133
class CredentialStoreStateMachineInstrumentationTest {
3234
private val context = InstrumentationRegistry.getInstrumentation().context
35+
private val credentialStoreUtil = CredentialStoreUtil()
3336

3437
private val configuration = AuthConfigurationProvider.getAuthConfigurationObject()
3538
private val userPoolId = configuration.userPool.userPool.PoolId
3639
private val identityPoolId = configuration.credentials.cognitoIdentity.identityData.PoolId
3740
private val userPoolAppClientId = configuration.userPool.userPool.AppClientId
3841

39-
private val credential = CredentialStoreUtil.getDefaultCredential()
40-
4142
@Before
4243
fun setup() {
43-
CredentialStoreUtil.setupLegacyStore(context, userPoolAppClientId, userPoolId, identityPoolId)
44+
credentialStoreUtil.setupLegacyStore(context, userPoolAppClientId, userPoolId, identityPoolId)
45+
}
46+
47+
@After
48+
fun tearDown() {
49+
credentialStoreUtil.clearSharedPreferences(context)
4450
}
4551

4652
private val authConfigJson = JSONObject(Gson().toJson(configuration))
@@ -51,11 +57,83 @@ class CredentialStoreStateMachineInstrumentationTest {
5157
plugin.configure(authConfigJson, context)
5258
plugin.initialize(context)
5359

54-
val receivedCredentials = AWSCognitoAuthCredentialStore(
60+
val credentialStore = AWSCognitoAuthCredentialStore(
61+
context,
62+
AuthConfiguration.fromJson(authConfigJson)
63+
)
64+
65+
assertEquals(credentialStoreUtil.getDefaultCredential(), credentialStore.retrieveCredential())
66+
assertEquals(
67+
credentialStoreUtil.getUser1DeviceMetadata(),
68+
credentialStore.retrieveDeviceMetadata(credentialStoreUtil.user1Username)
69+
)
70+
assertEquals(
71+
credentialStoreUtil.getUser2DeviceMetadata(),
72+
credentialStore.retrieveDeviceMetadata(credentialStoreUtil.user2Username)
73+
)
74+
}
75+
76+
@Test
77+
fun test_CredentialStore_Missing_DeviceMetadata_Migration_Succeeds_On_Plugin_Configuration() {
78+
// GIVEN
79+
val userAUsername = "userA"
80+
val expectedUserADeviceMetadata = DeviceMetadata.Metadata("A", "B", "C")
81+
val userBUsername = "userB"
82+
val expectedUserBDeviceMetadata = DeviceMetadata.Metadata("1", "2", "3")
83+
84+
AWSCognitoAuthPlugin().apply {
85+
configure(authConfigJson, context)
86+
initialize(context)
87+
}
88+
89+
AWSCognitoAuthCredentialStore(
90+
context,
91+
AuthConfiguration.fromJson(authConfigJson)
92+
).apply {
93+
saveDeviceMetadata("userA", expectedUserADeviceMetadata)
94+
}
95+
96+
// WHEN
97+
// Simulating missed device metadata migration from issue 2929
98+
// We expect this to not migrate as it will conflict with existing metadata already saved
99+
credentialStoreUtil.saveLegacyDeviceMetadata(
100+
context,
101+
userPoolId,
102+
userAUsername,
103+
DeviceMetadata.Metadata("X", "Y", "Z")
104+
)
105+
106+
// We expect this to migrate as it does not conflict with any existing saved metadata
107+
credentialStoreUtil.saveLegacyDeviceMetadata(
108+
context,
109+
userPoolId,
110+
userBUsername,
111+
expectedUserBDeviceMetadata
112+
)
113+
114+
// THEN
115+
116+
// Initialize plugin again to complete migration of missing device metadata
117+
AWSCognitoAuthPlugin().apply {
118+
configure(authConfigJson, context)
119+
initialize(context)
120+
}
121+
122+
// WHEN
123+
val credentialStore = AWSCognitoAuthCredentialStore(
55124
context,
56125
AuthConfiguration.fromJson(authConfigJson)
57-
).retrieveCredential()
126+
)
58127

59-
assertEquals(credential, receivedCredentials)
128+
// Expect the device metadata for user A to have not changed from data that was already saved in v2 store
129+
assertEquals(
130+
expectedUserADeviceMetadata,
131+
credentialStore.retrieveDeviceMetadata(userAUsername)
132+
)
133+
// Expect the device metadata for user A to have not changed from data that was already saved in v2 store
134+
assertEquals(
135+
expectedUserBDeviceMetadata,
136+
credentialStore.retrieveDeviceMetadata(userBUsername)
137+
)
60138
}
61139
}

aws-auth-cognito/src/androidTest/java/com/amplifyframework/auth/cognito/testutils/CredentialStoreUtil.kt

Lines changed: 76 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,14 @@ import com.amazonaws.internal.keyvaluestore.AWSKeyValueStore
2020
import com.amplifyframework.statemachine.codegen.data.AWSCredentials
2121
import com.amplifyframework.statemachine.codegen.data.AmplifyCredential
2222
import com.amplifyframework.statemachine.codegen.data.CognitoUserPoolTokens
23+
import com.amplifyframework.statemachine.codegen.data.DeviceMetadata
2324
import com.amplifyframework.statemachine.codegen.data.SignInMethod
2425
import com.amplifyframework.statemachine.codegen.data.SignedInData
26+
import java.io.File
2527
import java.util.Date
2628

27-
internal object CredentialStoreUtil {
28-
29-
private const val accessToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwidXNlcm5hbWUiO" +
29+
internal class CredentialStoreUtil {
30+
private val accessToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwidXNlcm5hbWUiO" +
3031
"iJhbXBsaWZ5X3VzZXIiLCJpYXQiOjE1MTYyMzkwMjJ9.zBiQ0guLRX34pUEYLPyDxQAyDDlXmL0JY7kgPWAHZos"
3132

3233
private val credential = AmplifyCredential.UserAndIdentityPool(
@@ -50,8 +51,29 @@ internal object CredentialStoreUtil {
5051
return credential
5152
}
5253

54+
val user1Username = "2924030b-54c0-48bc-8bff-948418fba949"
55+
val user2Username = "7e001127-5f11-41fb-9d10-ab9d6cf41dba"
56+
57+
fun getUser1DeviceMetadata(): DeviceMetadata.Metadata {
58+
return DeviceMetadata.Metadata(
59+
"DeviceKey1",
60+
"DeviceGroupKey1",
61+
"DeviceSecret1"
62+
)
63+
}
64+
65+
fun getUser2DeviceMetadata(): DeviceMetadata.Metadata {
66+
return DeviceMetadata.Metadata(
67+
"DeviceKey2",
68+
"DeviceGroupKey2",
69+
"DeviceSecret2"
70+
)
71+
}
72+
5373
fun setupLegacyStore(context: Context, appClientId: String, userPoolId: String, identityPoolId: String) {
5474

75+
clearSharedPreferences(context)
76+
5577
AWSKeyValueStore(context, "CognitoIdentityProviderCache", true).apply {
5678
put("CognitoIdentityProvider.$appClientId.testuser.idToken", "idToken")
5779
put("CognitoIdentityProvider.$appClientId.testuser.accessToken", accessToken)
@@ -60,10 +82,16 @@ internal object CredentialStoreUtil {
6082
put("CognitoIdentityProvider.$appClientId.LastAuthUser", "testuser")
6183
}
6284

63-
AWSKeyValueStore(context, "CognitoIdentityProviderDeviceCache.$userPoolId.testuser", true).apply {
64-
put("DeviceKey", "someDeviceKey")
65-
put("DeviceGroupKey", "someDeviceGroupKey")
66-
put("DeviceSecret", "someSecret")
85+
AWSKeyValueStore(context, "CognitoIdentityProviderDeviceCache.$userPoolId.$user1Username", true).apply {
86+
put("DeviceKey", "DeviceKey1")
87+
put("DeviceGroupKey", "DeviceGroupKey1")
88+
put("DeviceSecret", "DeviceSecret1")
89+
}
90+
91+
AWSKeyValueStore(context, "CognitoIdentityProviderDeviceCache.$userPoolId.$user2Username", true).apply {
92+
put("DeviceKey", "DeviceKey2")
93+
put("DeviceGroupKey", "DeviceGroupKey2")
94+
put("DeviceSecret", "DeviceSecret2")
6795
}
6896

6997
AWSKeyValueStore(context, "com.amazonaws.android.auth", true).apply {
@@ -73,5 +101,46 @@ internal object CredentialStoreUtil {
73101
put("$identityPoolId.expirationDate", "1212")
74102
put("$identityPoolId.identityId", "identityId")
75103
}
104+
105+
// we need to wait for shared prefs to actually hit filesystem as we always use apply instead of commit
106+
val beginWait = System.currentTimeMillis()
107+
while (System.currentTimeMillis() - beginWait < 3000) {
108+
if ((File(context.dataDir, "shared_prefs").listFiles()?.size ?: 0) >= 4) {
109+
break
110+
} else {
111+
Thread.sleep(50)
112+
}
113+
}
114+
}
115+
116+
fun saveLegacyDeviceMetadata(
117+
context: Context,
118+
userPoolId: String,
119+
username: String,
120+
deviceMetadata: DeviceMetadata.Metadata
121+
) {
122+
val prefsName = "CognitoIdentityProviderDeviceCache.$userPoolId.$username"
123+
AWSKeyValueStore(
124+
context,
125+
"CognitoIdentityProviderDeviceCache.$userPoolId.$username", true
126+
).apply {
127+
put("DeviceKey", deviceMetadata.deviceKey)
128+
put("DeviceGroupKey", deviceMetadata.deviceGroupKey)
129+
put("DeviceSecret", deviceMetadata.deviceSecret)
130+
}
131+
132+
// we need to wait for shared prefs to actually hit filesystem as we always use apply instead of commit
133+
val beginWait = System.currentTimeMillis()
134+
while (System.currentTimeMillis() - beginWait < 3000) {
135+
if (File(context.dataDir, "shared_prefs/$prefsName.xml").exists()) {
136+
break
137+
} else {
138+
Thread.sleep(50)
139+
}
140+
}
141+
}
142+
143+
fun clearSharedPreferences(context: Context) {
144+
File(context.dataDir, "shared_prefs").listFiles()?.forEach { it.delete() }
76145
}
77146
}

aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/actions/CredentialStoreCognitoActions.kt

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -36,13 +36,27 @@ internal object CredentialStoreCognitoActions : CredentialStoreActions {
3636
legacyCredentialStore.deleteCredential()
3737
}
3838

39-
// migrate device data
40-
val lastAuthUserId = legacyCredentialStore.retrieveLastAuthUserId()
41-
lastAuthUserId?.let {
42-
val deviceMetaData = legacyCredentialStore.retrieveDeviceMetadata(lastAuthUserId)
39+
/*
40+
Migrate Device Metadata
41+
1. We first need to get the list of usernames that contain device metadata on the device.
42+
2. For each username, we check to see if the current credential store has device metadata for that user.
43+
3. If the current user does not have device metadata in the current store, migrate from legacy.
44+
This is a possibility because of a bug where we were previously attempting to migrate using an aliased
45+
username lookup.
46+
4. If the current user has device metadata in the current credential store, do not migrate from legacy.
47+
This situation would happen if a user updated from legacy, signed out, then signed back in. Upon
48+
signing back in, they would be granted new device metadata. Since that new metadata is what is
49+
associated with the refresh token, we do not want to overwrite it with legacy metadata.
50+
5. Upon completed migration, we delete the legacy device metadata.
51+
*/
52+
legacyCredentialStore.retrieveDeviceMetadataUsernameList().forEach { username ->
53+
val deviceMetaData = legacyCredentialStore.retrieveDeviceMetadata(username)
4354
if (deviceMetaData != DeviceMetadata.Empty) {
44-
credentialStore.saveDeviceMetadata(lastAuthUserId, deviceMetaData)
45-
legacyCredentialStore.deleteDeviceKeyCredential(lastAuthUserId)
55+
credentialStore.retrieveDeviceMetadata(username)
56+
if (credentialStore.retrieveDeviceMetadata(username) == DeviceMetadata.Empty) {
57+
credentialStore.saveDeviceMetadata(username, deviceMetaData)
58+
}
59+
legacyCredentialStore.deleteDeviceKeyCredential(username)
4660
}
4761
}
4862

aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/data/AWSCognitoLegacyCredentialStore.kt

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import com.amplifyframework.statemachine.codegen.data.DeviceMetadata
2929
import com.amplifyframework.statemachine.codegen.data.FederatedToken
3030
import com.amplifyframework.statemachine.codegen.data.SignInMethod
3131
import com.amplifyframework.statemachine.codegen.data.SignedInData
32+
import java.io.File
3233
import java.time.Instant
3334
import java.time.temporal.ChronoUnit
3435
import java.util.Date
@@ -74,6 +75,7 @@ internal class AWSCognitoLegacyCredentialStore(
7475
const val TOKEN_KEY = "token"
7576
}
7677

78+
private val userDeviceDetailsCacheKeyPrefix = "$APP_DEVICE_INFO_CACHE.${authConfiguration.userPool?.poolId}."
7779
private val userDeviceDetailsCacheKey = "$APP_DEVICE_INFO_CACHE.${authConfiguration.userPool?.poolId}.%s"
7880

7981
private val idAndCredentialsKeyValue: KeyValueRepository by lazy {
@@ -229,6 +231,22 @@ internal class AWSCognitoLegacyCredentialStore(
229231
)
230232
}
231233

234+
/*
235+
During migration away from the legacy credential store, we need to find all shared preference files that store
236+
device metadata. These filenames contain the real username (not aliased) for the tracked device metadata.
237+
*/
238+
fun retrieveDeviceMetadataUsernameList(): List<String> {
239+
return try {
240+
val sharedPrefsSuffix = ".xml"
241+
File(context.dataDir, "shared_prefs").listFiles { _, filename ->
242+
filename.startsWith(userDeviceDetailsCacheKeyPrefix) && filename.endsWith(sharedPrefsSuffix)
243+
}?.map { it.name.substringAfter(userDeviceDetailsCacheKeyPrefix).substringBefore(sharedPrefsSuffix) }
244+
?.filter { it.isNotBlank() } ?: emptyList()
245+
} catch (e: Exception) {
246+
return emptyList()
247+
}
248+
}
249+
232250
@Synchronized
233251
override fun retrieveDeviceMetadata(username: String): DeviceMetadata {
234252
val deviceDetailsCacheKey = String.format(userDeviceDetailsCacheKey, username)

0 commit comments

Comments
 (0)