aws-amplify / aws-amplify/amplify-android

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

Open
#3,301 2 comments 0 reactions 0 assignees View on GitHub
auth
Dominant language
Java
Stars
287
Forks
132
Avg merge
2d 2h
Merged PRs (30d)
45

Description

## 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

**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

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_AUTH` → **new device is created instead of reusing existing one**
6. `fetchDevices()` returns 2 devices instead of 1

## Affected Platforms

- **Android**: `aws-auth-cognito` (this issue)
- **iOS**: `AWSCognitoAuthPlugin` — fixed in aws-amplify/amplify-swift#4196

## 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

Contributor guide

Open the contributing guide

Research direction

Start by reviewing the device metadata lookups in RememberDeviceUseCase.kt, ForgetDeviceUseCase.kt, FetchAuthSessionCognitoActions.kt, AuthenticationCognitoActions.kt, SignInChallengeCognitoActions.kt, SRPCognitoActions.kt, and DeviceSRPCognitoSignInActions.kt alongside PR #3288. Trace how inputUsername is threaded through each flow and verify that all device operations use a consistent username and that USER_PASSWORD_AUTH sends DEVICE_KEY.

Written by the indexing model from the issue text.

Assessment

Tech stack
android, aws, kotlin
Domain
authentication, mobile
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
45/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.