Skip to content

fix(auth): Consistent device metadata storage key across auth flows#3288

Draft
harsh62 wants to merge 10 commits into
mainfrom
fix/device-key-persistence-across-auth-flows
Draft

fix(auth): Consistent device metadata storage key across auth flows#3288
harsh62 wants to merge 10 commits into
mainfrom
fix/device-key-persistence-across-auth-flows

Conversation

@harsh62
Copy link
Copy Markdown
Member

@harsh62 harsh62 commented Apr 17, 2026

Summary

Fixes device key persistence when switching between authentication flows (USER_PASSWORD_AUTH and USER_SRP_AUTH). Three root causes identified and fixed:

1. Device metadata storage key mismatch across auth flows

The device metadata storage key was derived from the JWT username claim (via AuthHelper.getActiveUsername()), which can differ between USER_PASSWORD_AUTH (returns internal UUID) and USER_SRP_AUTH (returns user alias like email). This caused device key lookup failures on subsequent logins when auth flows change, generating duplicate device IDs.

Fix: Thread the original user-typed username (inputUsername) through SignedInData, AuthChallenge, SRPEvent, and the evaluateNextStep helper chain so ConfirmDevice stores device metadata under the same key used for retrieval.

2. Case-sensitive storage key mismatch

Cognito normalizes usernames to lowercase in JWT claims and challenge parameters, but the user-typed input preserves original casing. This caused a storage key mismatch when storing with inputUsername (mixed case) and retrieving with the JWT username (lowercase).

Fix: Lowercase the username in all three device metadata storage operations (save, retrieve, delete) in AWSCognitoAuthCredentialStore.

3. USER_PASSWORD_AUTH flow missing DEVICE_KEY in InitiateAuth request

MigrateAuthCognitoActions (USER_PASSWORD_AUTH flow) never retrieved stored device metadata or sent DEVICE_KEY in the InitiateAuth request. This caused Cognito to issue a new device on every USER_PASSWORD_AUTH sign-in, even when a confirmed device already existed from a prior sign-in. SRPCognitoActions already had this logic.

Fix: Add getDeviceMetadata() retrieval and DEVICE_KEY parameter injection to MigrateAuthCognitoActions, matching the existing pattern in SRPCognitoActions.

Changes

Data classes (3 files): Added inputUsername: String? = null to SignedInData, AuthChallenge, and SRPEvent.RespondPasswordVerifier/RetryRespondPasswordVerifier. All optional with null default for backwards-compatible serialization.

Helper (1 file): SignInChallengeHelper.evaluateNextStep accepts and propagates inputUsername.

Actions (8 files): All callers of evaluateNextStep pass the original event.username as inputUsername. ConfirmDevice uses inputUsername ?: username as the storage key. MigrateAuthCognitoActions now retrieves and sends DEVICE_KEY.

Credential store (1 file): AWSCognitoAuthCredentialStore lowercases username in device metadata key generation.

State machine (2 files): SRPActions interface and SRPSignInState resolver pass inputUsername through SRP event chain.

Test plan

  • Unit tests: SignedInData/AuthChallenge serialization backwards compat
  • Unit tests: inputUsername threading through evaluateNextStep
  • Unit tests: MigrateAuthCognitoActions with device metadata mock
  • ktlint passes
  • All unit tests pass locally (clean build)
  • Integration test: deviceKey_stays_consistent_across_alternating_auth_flowsFAILS on main (expected 1 device, got 2), PASSES on this branch
  • Integration test: deviceKey_persists_across_same_flow_signIn_signOutFAILS on main (expected 1, got 3), PASSES on this branch
  • Existing test: rememberDevice_succeeds_after_signIn_and_signOut — PASSES (no regression)

Addresses #3301

The device metadata storage key was derived from the JWT username claim
(via AuthHelper.getActiveUsername), which differs between auth flows:
USER_PASSWORD_AUTH returns the internal sub-style UUID while
USER_SRP_AUTH returns the user alias (email, phone, etc). This caused
device metadata stored during one flow to be unretrievable during
another, resulting in duplicate device IDs on every login when switching
between auth flows.

Thread the original user-typed username (inputUsername) through
SignedInData, AuthChallenge, SRPEvent, and the evaluateNextStep helper
so that ConfirmDevice always stores device metadata under the same key
that will be used for retrieval on subsequent logins.

The inputUsername field is optional with null default on all data classes
to maintain backwards compatibility with existing persisted data
(kotlinx.serialization handles missing fields gracefully).

Includes unit tests for serialization backwards compatibility,
inputUsername threading through evaluateNextStep, and cross-flow
integration tests.
@harsh62 harsh62 requested a review from a team as a code owner April 17, 2026 18:31
harsh62 added 3 commits April 18, 2026 00:31
Explicitly cast null to String? to disambiguate between the primary
constructor (IdToken?, AccessToken?, RefreshToken?, Long?) and the
secondary string constructor (String?, String?, String?, Long?).
Add inputUsername to signedInData and authChallenge blocks in all
feature test JSON state fixtures and the AuthStateJsonGenerator to
match the new runtime behavior.

Also fix CognitoUserPoolTokens overload ambiguity in unit tests by
using explicit null as String? casts.
@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 17, 2026

Codecov Report

❌ Patch coverage is 81.42857% with 13 lines in your changes missing coverage. Please review.
✅ Project coverage is 55.91%. Comparing base (59a0879) to head (c4b5841).
⚠️ Report is 11 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #3288      +/-   ##
==========================================
+ Coverage   55.76%   55.91%   +0.15%     
==========================================
  Files        1082     1084       +2     
  Lines       31913    32537     +624     
  Branches     4760     4763       +3     
==========================================
+ Hits        17795    18194     +399     
- Misses      12266    12488     +222     
- Partials     1852     1855       +3     
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

harsh62 added 6 commits April 20, 2026 15:25
- Remove unused imports in DeviceKeyPersistenceInstrumentationTest
- Wrap CognitoUserPoolTokens constructor args across lines (max-line-length)
- Wrap AuthChallenge constructor args in SignInChallengeHelper (argument-list-wrapping)
Cognito normalizes usernames to lowercase in JWT claims and challenge
parameters, but the user-typed input preserves original casing. This
caused a storage key mismatch when storing device metadata with
inputUsername (mixed case) and retrieving with the JWT username
(lowercase).

Lowercase the username in all three device metadata storage operations
(save, retrieve, delete) in AWSCognitoAuthCredentialStore.
MigrateAuthCognitoActions (USER_PASSWORD_AUTH flow) was not retrieving
or sending the stored device key in the InitiateAuth request. This
caused Cognito to issue a new device on every USER_PASSWORD_AUTH
sign-in, even when a confirmed device already existed from a prior
USER_SRP_AUTH sign-in.

Add device metadata retrieval and DEVICE_KEY parameter injection to
MigrateAuthCognitoActions, matching the existing pattern in
SRPCognitoActions.

Also update integration tests to verify device key stays consistent
when alternating between USER_PASSWORD_AUTH and USER_SRP_AUTH flows.
Comment on lines +85 to +91
/**
* Sign in with USER_PASSWORD_AUTH, remember device (1 device).
* Sign out, sign in with USER_SRP_AUTH — device count should stay 1, same device key.
* Sign out, sign in with USER_PASSWORD_AUTH again — still 1, same key.
* Sign out, sign in with USER_SRP_AUTH again — still 1, same key.
*/
@Test
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mattcreaser the test steps that are failing on main.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant