Skip to content

Device tracking fails across authentication flows — duplicate device IDs generated on every login #3301

@harsh62

Description

@harsh62

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:

  1. Auth flow type: USER_PASSWORD_AUTH may return the internal sub-style UUID while USER_SRP_AUTH returns the user alias (email, phone, etc.)
  2. 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)
  3. Case sensitivity: Cognito normalizes usernames to lowercase in JWT claims, but user input preserves original casing

Where the mismatch occurs

StorageConfirmDevice 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

  1. Configure a Cognito User Pool with device tracking set to "Always Remember"
  2. Enable both USER_SRP_AUTH and USER_PASSWORD_AUTH on the app client
  3. Sign in with USER_PASSWORD_AUTH → device is registered and confirmed
  4. Sign out
  5. Sign in with USER_SRP_AUTHnew device is created instead of reusing existing one
  6. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    authRelated to the Auth category/pluginspending-triageIssue is pending triage

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions