Skip to content

fix(auth): key device metadata by inputUsername so alias sign-in pools remember devices - #4254

Merged
harsh62 merged 5 commits into
aws-amplify:mainfrom
paywithextend:fix/device-metadata-alias-username
Aug 4, 2026
Merged

fix(auth): key device metadata by inputUsername so alias sign-in pools remember devices#4254
harsh62 merged 5 commits into
aws-amplify:mainfrom
paywithextend:fix/device-metadata-alias-username

Conversation

@dmorrow

@dmorrow dmorrow commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Issue

Related to #4196, which fixed the device-metadata keychain key for the write path (ConfirmDevice) but left several read paths keyed off a different value.

Description

On a user pool configured with alias sign-in (e.g. email alias), RespondToAuthChallenge returns a USERNAME challenge parameter equal to the user's sub, not the value the caller signed in with. #4196 made ConfirmDevice store device metadata under signedInData.inputUsername (the caller-supplied username), but the sign-in actions still read that metadata back using the echoed USERNAME (the sub). The read key never matched the write key, so:

  • DEVICE_KEY was omitted from the password-verifier response, and Cognito treated every sign-in as a new device — MFA on every sign-in, and a new device record accumulating per attempt.
  • On pools where the username is the sub, the two values coincide, which is why this isn't seen everywhere.

This PR aligns every device-metadata read (and the two deviceNotFound deletes) with the inputUsername used by the write, across the SRP, challenge, and device-SRP flows:

  • VerifyPasswordSRP — read and the deviceNotFound delete now use inputUsername. (The delete previously used the echoed username; when a stored device key was stale, Cognito returned ResourceNotFoundException, the recovery path deleted the wrong keychain entry, and the retry re-read the same stale key and looped indefinitely.)
  • VerifySignInChallenge — both reads and the deviceNotFound delete now use challenge.inputUsername ?? username.
  • UserPoolSignInHelper — carries inputUsername ?? username into .initiateDeviceSRP, so InitiateAuthDeviceSRP looks the metadata up (and re-sends DEVICE_KEY) under the right key. Without this the device-SRP flow fails with Missing required parameter DEVICE_KEY.
  • VerifyDevicePasswordSRP — reads metadata with inputUsername; the outgoing request still sends username (the echoed value), which is what Cognito expects.

Nothing sent to Cognito changes semantically — username still supplies the request USERNAME on every request; only the keychain lookups moved to the value the metadata was written under.

Verification

Manually verified end-to-end on a real device against an alias-sign-in pool (us-east-1 email alias, MFA via email OTP):

  • Fresh device → MFA challenge → rememberDevice → subsequent sign-in takes DEVICE_SRP_AUTH and completes with no MFA.
  • Stale local device key → a single deviceNotFound recovery + retry, then a normal MFA challenge (no infinite loop).
  • Device removed / password reset → MFA challenge, then re-remembered on confirm.

General Checklist

  • Added new tests to cover change, if needed
  • Build succeeds with all target using Swift Package Manager
  • All unit tests pass
  • All integration tests pass
  • Security oriented best practices and standards are followed (e.g. using input sanitization, principle of least privilege, etc)
  • Documentation update for the change if required
  • PR title conforms to conventional commit style
  • New or updated tests include Given When Then inline code documentation and are named accordingly testThing_condition_expectation()
  • If breaking change, documentation/changelog update with migration instructions

By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.

dmorrow and others added 3 commits July 22, 2026 19:35
…n-in

`ConfirmDevice` stores device metadata under `signedInData.inputUsername`
(the username the caller signed in with), but the sign-in flow read it back
using `parameters["USERNAME"]` — the value Cognito echoes, which is the sub
for pools configured with alias sign-in.

On those pools the read never matched the write, so `DEVICE_KEY` was omitted
from the password-verifier response. Cognito treated every sign-in as a new
device: it returned fresh `NewDeviceMetadata`, `ConfirmDevice` stored it under
the email again, and the next sign-in repeated the cycle — MFA on every
sign-in and a new device record per attempt (one user had accumulated 50).

Pools where the username *is* the email are unaffected, since the two values
coincide there.

Reads now use the same value as the write:
- VerifyPasswordSRP: `inputUsername`
- VerifySignInChallenge (both call sites): `challenge.inputUsername ?? username`

Nothing sent to Cognito changes — `username` still supplies `USERNAME` on
every request; only the keychain lookups moved.

Known gap: the DeviceSRP actions (InitiateAuthDeviceSRP,
VerifyDevicePasswordSRP) still look up by the echoed username. That path only
executes once Cognito starts accepting the device key, so it is now reachable
and tracked separately.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Commit 122689e changed the sign-in flow to read device metadata with the
caller's `inputUsername` (matching how `ConfirmDevice` writes it), but left the
`deviceNotFound` recovery paths deleting under `username` — the value Cognito
echoes, which is the sub for pools with alias sign-in.

When a stored device key is stale, Cognito rejects the password verifier with
`ResourceNotFoundException`. The recovery path then deletes the wrong keychain
entry, leaving the real one in place, so the retry re-reads the same stale key,
Cognito rejects it again, and sign-in loops indefinitely.

Read and delete now use the same value in both actions:
- VerifyPasswordSRP: `inputUsername`
- VerifySignInChallenge: `challenge.inputUsername ?? username`

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Once a device is remembered, Cognito answers the password verifier with
DEVICE_SRP_AUTH instead of an MFA challenge, routing sign-in through
InitiateAuthDeviceSRP / VerifyDevicePasswordSRP. Both looked the stored device
metadata up by the Cognito-echoed username (the sub, on pools with alias
sign-in) rather than the `inputUsername` it was written under, so the lookup
missed and the request omitted DEVICE_KEY — Cognito then rejected sign-in with
"Missing required parameter DEVICE_KEY".

- UserPoolSignInHelper: carry `inputUsername ?? username` into `.initiateDeviceSRP`
  so InitiateAuthDeviceSRP reads (and re-sends) under the right key.
- VerifyDevicePasswordSRP: read metadata with `inputUsername`; the request still
  sends `username` (the echoed value), which is what Cognito expects.

Completes the alias-pool device-remembering fix (see prior two commits); the
DEVICE_SRP_AUTH path is only reachable once DEVICE_KEY is accepted, so it could
not be exercised until the earlier read/delete fixes landed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@dmorrow
dmorrow requested a review from a team as a code owner July 22, 2026 23:37
@dmorrow

dmorrow commented Jul 29, 2026

Copy link
Copy Markdown
Contributor Author

@thisisabhash This fixes a bug in production. Would it be best for me to submit a bug report as well? Our iOS users devices are never saved, leading to MFA every time.

@harsh62
harsh62 temporarily deployed to IntegrationTest August 4, 2026 14:48 — with GitHub Actions Inactive
@harsh62
harsh62 temporarily deployed to IntegrationTest August 4, 2026 14:48 — with GitHub Actions Inactive
@harsh62

harsh62 commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

All checks passed on #4263.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants