aws-amplify / aws-amplify/amplify-android
Device tracking fails across authentication flows — duplicate device IDs generated on every login
- 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
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