Bug Description
When a Cognito User Pool has device tracking enabled ("Always Remember"), the SDK generates a new device ID on every sign-in instead of reusing the existing one. This results in duplicate device records in Cognito, breaking any downstream logic that relies on stable device identity (e.g., payment card associations linked to device IDs, device-based MFA suppression).
Root Cause
Device metadata is stored locally using a username-based key. The username used to store device metadata (during ConfirmDevice after successful authentication) differs from the username used to retrieve it (during subsequent sign-in flows and post-sign-in operations like rememberDevice, forgetDevice, token refresh).
How the username diverges
The JWT username claim returned by Cognito differs depending on:
- Auth flow type:
USER_PASSWORD_AUTH may return the internal sub-style UUID while USER_SRP_AUTH returns the user alias (email, phone, etc.)
- Alias usage: When a user signs in with an email alias (e.g.,
user@example.com), Cognito may return a different canonical username in the JWT (e.g., a generated internal username)
- Case sensitivity: Cognito normalizes usernames to lowercase in JWT claims, but user input preserves original casing
Where the mismatch occurs
Storage — ConfirmDevice in SignInCognitoActions.kt stores device metadata using signedInData.username (derived from the JWT username claim of the current auth response).
Retrieval — Multiple consumers retrieve device metadata using different username sources:
| Consumer |
Username source |
When it runs |
InitializeSignInFlow / SRP initiation |
User-typed input |
Before authentication |
RememberDeviceUseCase |
signedInData.username (JWT) |
After sign-in |
ForgetDeviceUseCase |
signedInData.username (JWT) |
After sign-in |
FetchAuthSessionCognitoActions |
signedInData.username (JWT) |
Token refresh |
AuthenticationCognitoActions |
signedInData.username (JWT) |
Session restore on app launch |
SignInChallengeCognitoActions |
challenge.username (Cognito-canonical) |
During MFA/challenge responses |
SRPCognitoActions (post-response) |
AuthHelper.getActiveUsername() (Cognito-canonical) |
During SRP verification |
If the user signs in with USER_PASSWORD_AUTH first (JWT username = UUID), then signs in with USER_SRP_AUTH (JWT username = email), the device metadata stored under the UUID key cannot be found under the email key, causing a new device to be created.
Additional issue: USER_PASSWORD_AUTH does not send DEVICE_KEY
MigrateAuthCognitoActions (the USER_PASSWORD_AUTH flow) does not retrieve stored device metadata or include DEVICE_KEY in the InitiateAuth request. This means Cognito always treats USER_PASSWORD_AUTH sign-ins as a new device, even when a confirmed device already exists. SRPCognitoActions does include DEVICE_KEY — this is an inconsistency.
Customer Impact
- Duplicate device records created in Cognito on every login when switching auth flows
rememberDevice() / forgetDevice() may fail with InvalidParameterException (null deviceKey) if the storage key doesn't match
Reproduction Steps
- Configure a Cognito User Pool with device tracking set to "Always Remember"
- Enable both
USER_SRP_AUTH and USER_PASSWORD_AUTH on the app client
- Sign in with
USER_PASSWORD_AUTH → device is registered and confirmed
- Sign out
- Sign in with
USER_SRP_AUTH → new device is created instead of reusing existing one
fetchDevices() returns 2 devices instead of 1
Affected Platforms
Current Fix Attempt (PR #3288)
PR #3288 addresses parts of this issue:
- Threads
inputUsername (the original user-typed username) through SignedInData, AuthChallenge, SRPEvent, and evaluateNextStep so ConfirmDevice can store device metadata under a consistent key
- Lowercases the username in
AWSCognitoAuthCredentialStore device metadata operations to handle Cognito's case normalization
- Adds
DEVICE_KEY to USER_PASSWORD_AUTH (MigrateAuthCognitoActions) matching the existing SRP pattern
Remaining work
The inputUsername is currently only used by ConfirmDevice for storage. All post-sign-in consumers still read using signedInData.username (JWT-derived). These need to be updated to use signedInData.inputUsername ?: signedInData.username to ensure consistent key usage:
Post-sign-in use cases:
RememberDeviceUseCase.kt — uses signedInData.username
ForgetDeviceUseCase.kt — uses signedInData.username
FetchAuthSessionCognitoActions.kt — uses signedInData.username
AuthenticationCognitoActions.kt — uses signedInData.username
Sign-in challenge/SRP flows:
SignInChallengeCognitoActions.kt — uses challenge.username
SRPCognitoActions.kt (post-response lookups) — uses AuthHelper.getActiveUsername()
DeviceSRPCognitoSignInActions.kt — needs inputUsername threaded through
Bug Description
When a Cognito User Pool has device tracking enabled ("Always Remember"), the SDK generates a new device ID on every sign-in instead of reusing the existing one. This results in duplicate device records in Cognito, breaking any downstream logic that relies on stable device identity (e.g., payment card associations linked to device IDs, device-based MFA suppression).
Root Cause
Device metadata is stored locally using a username-based key. The username used to store device metadata (during
ConfirmDeviceafter successful authentication) differs from the username used to retrieve it (during subsequent sign-in flows and post-sign-in operations likerememberDevice,forgetDevice, token refresh).How the username diverges
The JWT
usernameclaim returned by Cognito differs depending on:USER_PASSWORD_AUTHmay return the internal sub-style UUID whileUSER_SRP_AUTHreturns the user alias (email, phone, etc.)user@example.com), Cognito may return a different canonical username in the JWT (e.g., a generated internal username)Where the mismatch occurs
Storage —
ConfirmDeviceinSignInCognitoActions.ktstores device metadata usingsignedInData.username(derived from the JWTusernameclaim of the current auth response).Retrieval — Multiple consumers retrieve device metadata using different username sources:
InitializeSignInFlow/ SRP initiationRememberDeviceUseCasesignedInData.username(JWT)ForgetDeviceUseCasesignedInData.username(JWT)FetchAuthSessionCognitoActionssignedInData.username(JWT)AuthenticationCognitoActionssignedInData.username(JWT)SignInChallengeCognitoActionschallenge.username(Cognito-canonical)SRPCognitoActions(post-response)AuthHelper.getActiveUsername()(Cognito-canonical)If the user signs in with
USER_PASSWORD_AUTHfirst (JWT username = UUID), then signs in withUSER_SRP_AUTH(JWT username = email), the device metadata stored under the UUID key cannot be found under the email key, causing a new device to be created.Additional issue: USER_PASSWORD_AUTH does not send DEVICE_KEY
MigrateAuthCognitoActions(the USER_PASSWORD_AUTH flow) does not retrieve stored device metadata or includeDEVICE_KEYin theInitiateAuthrequest. This means Cognito always treats USER_PASSWORD_AUTH sign-ins as a new device, even when a confirmed device already exists.SRPCognitoActionsdoes includeDEVICE_KEY— this is an inconsistency.Customer Impact
rememberDevice()/forgetDevice()may fail withInvalidParameterException(null deviceKey) if the storage key doesn't matchReproduction Steps
USER_SRP_AUTHandUSER_PASSWORD_AUTHon the app clientUSER_PASSWORD_AUTH→ device is registered and confirmedUSER_SRP_AUTH→ new device is created instead of reusing existing onefetchDevices()returns 2 devices instead of 1Affected Platforms
aws-auth-cognito(this issue)AWSCognitoAuthPlugin— fixed in fix(auth): Consistent device metadata keychain key across auth flows amplify-swift#4196Current Fix Attempt (PR #3288)
PR #3288 addresses parts of this issue:
inputUsername(the original user-typed username) throughSignedInData,AuthChallenge,SRPEvent, andevaluateNextStepsoConfirmDevicecan store device metadata under a consistent keyAWSCognitoAuthCredentialStoredevice metadata operations to handle Cognito's case normalizationDEVICE_KEYto USER_PASSWORD_AUTH (MigrateAuthCognitoActions) matching the existing SRP patternRemaining work
The
inputUsernameis currently only used byConfirmDevicefor storage. All post-sign-in consumers still read usingsignedInData.username(JWT-derived). These need to be updated to usesignedInData.inputUsername ?: signedInData.usernameto ensure consistent key usage:Post-sign-in use cases:
RememberDeviceUseCase.kt— usessignedInData.usernameForgetDeviceUseCase.kt— usessignedInData.usernameFetchAuthSessionCognitoActions.kt— usessignedInData.usernameAuthenticationCognitoActions.kt— usessignedInData.usernameSign-in challenge/SRP flows:
SignInChallengeCognitoActions.kt— useschallenge.usernameSRPCognitoActions.kt(post-response lookups) — usesAuthHelper.getActiveUsername()DeviceSRPCognitoSignInActions.kt— needsinputUsernamethreaded through