Skip to content

fix(auth): confirmSignIn identity pool errors, sign-in log redaction and the access-group signed-out record - #4405

Merged
harsh62 merged 57 commits into
mainfrom
fix/cognito-plugin-followups
Oct 30, 2026
Merged

harsh62 merged 57 commits into
mainfrom
fix/cognito-plugin-followups

Conversation

@harsh62

@harsh62 harsh62 commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Auth plugin fixes found in the review of the AmplifyCognitoClient beta (#4349) and its follow-ups (#4401). Each one changes what an app on the plugin sees:

  • confirmSignIn now throws the identity pool's error when the user pool accepts the answer but the identity pool step after it fails, and signs the user out first, as signIn does. Before, it never returned, and every Auth call queued behind it waited too.
  • Three secrets that 2.63.0 logged are now redacted: the user's confirmSignIn answer (verbose), the TOTP setup's verified session (verbose), and the auto sign-in session of a sign-up result (info). The last one adds conformances to Amplify's public AuthSignUpStep, recorded in api-dump/Amplify.json.
  • The sign-in's verbose lines no longer print the challenge session. They print <REDACTED> in its place, and the usernames masked.
  • The MFA-type and auth-factor parsers' error line no longer repeats the value it was given. On the plugin's confirmSignIn that value is the user's answer.
  • With a keychain access group set, the plugin's sign-out of a login saves the signed-out record, {"noCredentials":{}}, in the shared keychain service instead of deleting the session record. That sign-out then stops an app extension's first access-group migration, as a login does. A guest's sign-out, and one with nothing saved, write no record.

It also carries the follow-ups to nine review comments left on #4401 after approval, all in AmplifyCognitoClient and its docs (below), the fixes for this PR's own review (below), and five follow-ups deferred from the reviews of #4401 and #4402 (below): the client keeps a session's expiry across launches, two tests for a resumed migration, CI provisioner changes, a sandbox guard and test selector changes. The CI provisioner changes need one provisioning run in the CI account after merge.

Public API: AuthSignUpStep gains CustomStringConvertible, CustomDebugStringConvertible and CustomReflectable, with description, debugDescription and customMirror. It's an additive change, recorded in api-dump/Amplify.json the same way TOTPSetupDetails's conformances were (#4349). This PR is fix(auth), so the release that ships it needs a feat commit too (#4403, say) to bump the minor version. An app's own description conformance for AuthSignUpStep now gets a compiler warning and Amplify's is used; the release note tells apps to remove theirs.

The parser fix changes one line of a locked golden fixture and its two pins, so this PR needs an admin merge past the "Auth | Plugin Behavior Snapshots" check (details below).

Changes

confirmSignIn throws the identity pool's error instead of never returning

AWSAuthConfirmSignInTask waits for the sign-in to finish. Its loop had no arm for the authorization error state, so when the user pool accepted the answer and GetId or GetCredentialsForIdentity then failed, the call never returned, and the Auth calls queued behind it waited too. The loop now throws the error, as AWSAuthSignInTask does for the same state. For example, it throws AuthError.service with AWSCognitoAuthError.invalidParameter underneath. It also logs the verbose line Authorization reached an error state …, which signIn logs there too.

Before it throws, it cancels the sign-in and waits for the sign-out, as signIn, signInWithWebUI and autoSignIn do on that failure. Otherwise the user stays signed in in memory with tokens that were never saved. The sign-in's own refresh token is revoked, not a login the credential store holds, and nothing is saved, so fetchAuthSession() reports signed out, getCurrentUser() throws signedOut, the next signIn starts a new sign-in, and another confirmSignIn throws invalidState. It cancels only when this call sent the answer. A sign-in that a shared-keychain reconcile adopted while the call waited is left as it is. A sign-out that fails ends the wait too, so the call can't hang.

The cancel's sign-out no longer reads the saved session first. InitiateSignOut prefers the user pool login the store holds over the tokens it was given, which is right for a signOut, but a sign-in cancelled from .signedIn (only ever after the identity pool step failed) never saved its tokens. So the store holds nothing or a guest, or, under a keychain access group, possibly a login another app saved during the sign-in: the cancel revoked that login and left this sign-in's refresh token valid. The engine's cancel now revokes the tokens it was given. That covers signIn, autoSignIn and signInWithWebUI too, which cancel the same way; with nothing saved it is the same token as before, minus the error line Unable to update credentials with error: … the failed read logged. Under an access group the sign-out still writes the signed-out record over the other app's login, so that app is signed out at its next fetchAuthSession; leaving the record alone, as the client does, would mean carrying the cancel through the revoke and the local sign-out to the store, so this PR documents it instead.

Who sees it: apps with an identity pool that sign in with a challenge: MFA, a new password, custom auth or a factor selection. The cancel's revoke: apps with an identity pool that share a keychain access group, on every sign-in that fails at the identity pool step.

The challenge session is redacted in every printed form

The challenge a sign-in answers (the engine's RespondToAuthChallenge) had no description of its own. So every verbose line that printed it by reflection showed its session and the username in full. These lines are the "Sending event" lines of the sign-in actions when Cognito answers with a challenge (SRP, custom auth, USER_AUTH, migrate auth, auto sign-in after sign-up, and each confirmSignIn answer), and the lines that print a sign-in state holding one. It now has description, debug description and mirror conformances. They print the session as <REDACTED> and mask the challenge's two username fields (alice as al*ce), as the sign-in data already did, in print, debugPrint, interpolation and dump.

What else changes in the printed form:

  • The other fields. They print as they did when the challenge was nested in an event or state, which is how every log line printed it. parameters, Cognito's challenge parameters, still print in full, and they can hold a username (USERNAME on a custom challenge).
  • The type name. Printed on its own, the challenge now names its type in full: InternalAWSCognitoAuth.RespondToAuthChallenge(…), where default printing gave RespondToAuthChallenge(…).
  • What it doesn't cover. The user's answer and the TOTP setup's session were logged through other values; the next sections cover them.

Who sees it: anyone reading or collecting verbose logs, from the plugin or from AmplifyCognitoClient, which logs the same events.

Three secrets 2.63.0 logged are redacted

These were found in this PR's review. All three are on main and in 2.63.0, and they show only with the log level raised (the default is .error). Apps that forward verbose or info logs are affected.

  • The user's confirmSignIn answer (verbose). While Cognito checks an answer, the sign-in's challenge state (SignInChallengeState.verifying) held it in full, and the plugin's verbose Checking next step for: line prints that state by reflection. So every answer confirmSignIn sent through a challenge was logged: an SMS, email or TOTP code, a password after a PASSWORD or PASSWORD_SRP factor selection, a new password, a custom challenge answer. Nothing reads the answer from that state, so it no longer holds it. The answer's own printed form (ConfirmSignInEventData) is printed by the TOTP setup's verbose lines (Checking next step for:, Auth state change) and by the Sending event line of an answer sent again after Cognito refused the saved device. It masked the answer, keeping four characters of a six-digit code, and now prints <REDACTED> in print, debugPrint, interpolation and dump.
  • The TOTP setup's verified session (verbose). VerifyTOTPSetup logs the event it sends, and that event carries the session VerifySoftwareToken returns, which answers the MFA setup challenge and finishes the sign-in. The event type now redacts it in every printed form: the line shows respondToAuthChallenge("<REDACTED>"). Its other cases print as before. AmplifyCognitoClient logs the same line.
  • The auto sign-in session of a sign-up result (info). The plugin's tasks log their result at info ("Successfully completed execution for … with result:"). AuthSignUpStep.completeAutoSignIn(Session) had no description of its own, so confirmSignUp, and a signUp that Cognito confirms at once, logged the session in full. With the username, that session signs the user in. AuthSignUpStep now prints completeAutoSignIn("<REDACTED>") in every printed form, the step and the AuthSignUpResult holding it alike. The other steps print as default printing does, and case .completeAutoSignIn(let session) still reads the session. The engine's copy of the step prints the same way, so the payload log gate's normalised texts still match. AmplifyCognitoClient's AuthClientSignUpStep already redacted it.

I also looked for other states and events that hold an answer, a password or a session and that a "Sending event", "Checking next step for:" or "Auth state change" line prints. The sign-in data prints its password redacted. The sign-up state's Auth state change line prints its session masked, as before. The sign-up password and confirmation code ride only in events that no line prints. No golden changes: the log transcript has no challenge, TOTP setup or sign-up scenario.

Who sees it: anyone reading or collecting verbose logs (the answer and the TOTP session), or info-level logs (the sign-up session), from the plugin, and from AmplifyCognitoClient for the TOTP session and the retried answer. Also apps that print or dump a sign-up result or step.

The MFA-type and auth-factor parsers log a fixed text

EngineMFAType(rawValue:logger:), EngineAuthFactorType, and the plugin's MFAType and AuthFactorType extensions logged Tried to initialize an unsupported MFA type with value: <value> at error level. On confirmSignIn the value is the user's answer: an answer to an MFA selection, an MFA setup selection or a first-factor selection that is not a known type. The line is now the fixed text Tried to initialize an unsupported MFA type, for every value. It keeps the same level (error) and category: MFAType and AuthFactorType in the plugin, and AmplifyCognitoClient.MFAType and AmplifyCognitoClient.AuthFactorType in the client, which logs the same line.

Who sees it: anyone reading or collecting error-level logs from the plugin or the client, or matching that line's text.

Under an access group, the plugin's sign-out of a login saves the signed-out record

With a keychain access group set, every plugin path that ends a login saves {"noCredentials":{}} in the shared service instead of deleting the session record. That covers signOut (local or global), clearFederationToIdentityPool and the local sign-out after deleteUser. All of them go through AWSCognitoAuthCredentialStore.deleteCredential(). Under an access group, deleteCredential() now reads the record first. It replaces the record with the signed-out record only when the record holds a login (user pool tokens or a federated identity). The replace is one keychain update (replaceIfPresent) that writes only over a record that's still there. So when another process in the group deletes the record after the read (an older plugin's or a guest's sign-out, or the client's purge), the sign-out still completes and writes nothing. A record that's already signed out is left as it is. Other cases keep the old behaviour:

  • A guest's record (identityPoolOnly) is deleted, as is a record the store can't read.
  • With nothing saved, nothing is written.

So a guest's sign-out, or a signOut() from .signedOut with nothing saved, never stops a login's migration. This matters when an app extension in the group signs out that way before the app's first launch with the access group: the app's migration still moves its login. Without an access group nothing changes: the record is deleted.

These are the bytes AmplifyCognitoClient's default session saves when it signs a session out (it writes nothing when there is nothing to sign out). After a plugin sign-out, an extension's first access-group migration (migrateKeychainItems: true) used to stop only at a device record. So an extension whose own keychain still held a stale login moved it in over the user who had signed out. Now the signed-out record stops that migration, as a login does. Every user-pool sign-in saves the advanced-security device ID, which already stopped it after a user-pool user's sign-out. So the record matters mainly after a federated identity's sign-out, which leaves no device record, and that's the case the tests cover. The migration rule itself is unchanged: a login or a signed-out record stops it, and a guest or a configuration record alone doesn't stop a login. The next sign-in replaces the record.

How it combines with #4401's AWSMobileClient (Amplify v1) change. In #4401, a stored signed-out record stops the AWSMobileClient migration, and a guest the client fetches over a signed-out .default clears those logins first.

  • Next plugin launch. The plugin's own record now stops the AWSMobileClient migration under an access group. The sign-out has already cleared the old keychain locations of the app that signed out, which are named after its bundle ID. So this matters for a login that survived that clear or was saved there since, and for one that another app in the group still holds in its own old locations. That login is now dropped and cleared at that app's next launch instead of migrated, so the user stays signed out. In 2.63.0 a login the group held already dropped it the same way. Without an access group the plugin's sign-out deletes the record, and such a login is migrated, as before. One exception: an app that later leaves the group with .none(migrateKeychainItemsOfUserSession: true) carries the record into its unshared service, where it stops the migration too.
  • Until a guest replaces the record. A guest that the plugin or an app extension saves over the record clears nothing. The migration reads a guest as no session, so after it only the sign-out's earlier clear protects the user, as before. A guest the client's .default fetches over the record clears the AWSMobileClient logins first, best effort. Before this change, the client found no record after a plugin sign-out, and left those logins.

Other effects, all under an access group:

  • A plugin sign-out after a configuration change that copied the login now survives a rollback to the build with the earlier configuration. That build's rule copies the signed-out record over the earlier copy, as after a sign-out through the client. Without an access group the record is deleted, and the documented caveat stays.
  • An extension's first access-group transition without migration also counts the record, as it counts a login. So it keeps the extension's own old keychain items instead of clearing them.
  • AmplifyCognitoClient.storedSessions(…, includingSignedOut: true) in that access group now lists .default as a signed-out row after a plugin sign-out of a login, with no label or username. Before, it listed no .default row. When the client saved a label or username beside the record, an absent record was already listed as a signed-out row with them, and it still is. The default listing is unchanged.
  • A process that reads a record another process signed out sees the signed-out record instead of none. So clearFederationToIdentityPool() there, while still federated in memory, throws AuthError.service("Unable to find credentials to clear for federation.") where it threw AuthError.unknown("Unable to clear credential store: …"), and a sign-out there no longer logs the error line Unable to update credentials with error: ….
  • Logs. Every plugin sign-out under an access group, a guest's and one with nothing saved included, first reads the record. That read logs about three of the keychain store's verbose [KeychainStore] lines: Initialized keychain… for the shared service, Started retrieving…, then Successfully retrieved… or No Keychain item found…. A read failure is logged at verbose only, and the record's contents are not logged. A sign-out of a login then logs the lines for replacing an item only if present (Replacing `Data` only if present…, then Conditional write succeeded…, or Conditional write refused… when the record is gone), in place of those for removing one. A sign-out over a record that's already signed out logs neither. At the next launch the plugin reads the signed-out record instead of finding none, which changes two lines:
    • the info line No existing session found. is no longer logged;
    • the verbose credential-store line sends completedOperation with no credentials, where it sent throwError(itemNotFound).

Compatibility with older plugins. Every plugin release reads {"noCredentials":{}} as signed out. AmplifyCredentials has had a noCredentials case with synthesized Codable back to 2.10.0 (checked at that tag), so those releases decode the record as .noCredentials, the value for no saved session. A rollback after such a sign-out starts signed out. The rollback matrix already pins the same bytes, written by the client, as read signed out by 2.62.0 and the current plugin (testMatrix_clientSignOut_readByEveryPluginAsSignedOut).

Who sees it: apps with a keychain access group, especially with app extensions, and app suites whose apps share one. Also apps that list stored sessions with includingSignedOut: true, and apps under an access group that were upgraded from AWSMobileClient.

Docs

  • docs/design/RELEASE-NOTES-PENDING.md: a row for each change above, a Security row for each of the three leaks, and the client's rows for the redactions and the parser text. The client's parser row says that the client never logged a user's answer through the parsers: it refuses an unknown answer before the engine runs. The AWSMobileClient row added in fix(auth): AmplifyCognitoClient follow-ups from the beta review #4401 said "the plugin's own sign-out deletes the record, so an app that uses only the plugin is not affected". It now says when that holds: without an access group.
  • The client's README, the design, the decisions doc and the rollback notes said the plugin's sign-out deletes the record, everywhere it came up. They now give both cases, with the plugin releases each applies to. The decisions doc records the access-group sign-out as decision 40, which changes decision 21, and corrects decision 36's note.
  • Comments in the access-group migration check, the AWSMobileClient migration's existing-session rule, the plugin's legacy-store clear and the client's default-session store say the same. The integration-test notes say their plugin sign-outs run without an access group.
  • The credential store's log-category comment no longer names an internal milestone.

Locked golden change: admin merge required

The parser fix changes one recorded log line, so the GoldenLogs tree and its two pins change together:

  • AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/TestResources/GoldenLogs/transcript.json:1005, scenario 7-unsupportedAuthFactorType, category AuthFactorType, level error:
    "Tried to initialize an unsupported MFA type with value: X" → "Tried to initialize an unsupported MFA type".
  • AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/GoldenTests/LogTranscriptSnapshotTests.swift:44: pinnedTranscriptSHA256 6193c4562559fb8602ba4516c11ec533f8ee1e57cf65f851fe19afebed925afa → 5daa06cfb709db3ce7b2540a9af5566bdf24de306bd20ca7246b909a3596c796. The doc comment at lines 26-29 lists the re-pin.
  • scripts/cognito-engine/check_fixture_lock.sh:72: the GoldenLogs tree 14aefeb712286ee38af02d883a50f3bcc774a610 → 7ef93ecd4b541aebfd5d32a78a385d62357d3065. The comments at lines 23-24 and 67-68 record the reason.

No other locked tree changes: GoldenStoredFormat, GoldenErrors, GoldenKeychainQueries and GoldenConfiguration keep their pinned hashes.

Why it needs an admin merge: the lock check runs the base branch's copy of check_fixture_lock.sh. That copy still pins the old GoldenLogs tree, and this PR also changes the script itself, which the self-file check refuses. Both are failures by design for a re-lock. The three files are admin-owned in .github/CODEOWNERS, so the change needs admin review. An admin then merges past the failing check, and the push run of the merge on main fails once. The next push runs the merged script and passes. Open PRs fail the check until they merge main.

On this branch, check_fixture_lock.sh HEAD passes (all five trees match). Run against origin/main, it fails in two ways. With this branch's script and origin/main as the base, it fails with check_fixture_lock.sh changed. With origin/main's script, it fails with GoldenLogs is locked (tree 14aefeb7…), but HEAD has 7ef93ecd… (M transcript.json) and the same self-file error.

Also in this PR: review follow-ups from #4401

These are fixes for the nine comments left on #4401 after it was approved. All of them are in AmplifyCognitoClient and its docs:

  1. The guest fetch's side effect is documented. The first fetchAuthSession() that saves a guest over .default's signed-out record runs the sign-out's AWSMobileClient clear again. So it also removes whatever AWSMobileClient, or the AWS SDK for iOS used beside the client, saved in those services since the sign-out. An app's AWSCognitoCredentialsProvider loses the identity ID it cached for any identity pool then. The README's Sign-out section, "Ship the plugin's migrations first" and the differences table now say so, as do design §4.8, the pending release-note row and the store and guest-path comments. Behaviour is unchanged.
  2. The identity-pool-only case is documented. The README's rollback row and its Sign-out section, and design §4.8, now say that a user pool login left by an identity-pool-only sign-out is still migrated once a guest replaces the signed-out login. Neither clear touches it.
  3. "Until a guest replaces it" is added to the later releases' protection in the rollback notes and the decisions doc.
  4. A new test pins the order of the guest path's clear. DefaultSessionGuestLegacyLoginTests.testAGuestClearsThemBeforeItIsWritten checks that the guest path clears the AWSMobileClient logins before it writes the guest.
  5. The .resetPassword and .confirmSignUp rows say what confirmSignIn does with them: it throws .invalidState, except after Cognito refused a challenge answer or a TOTP code with that step, where it sends the answer again.
  6. The errors table and the design's browserBusy(holder:) rows cover a hosted-UI sign-in already in progress, where holder is the session itself and no sheet is held.
  7. Fix: the "already waiting" message no longer names the session as holding its own sheet. A double tap on a session whose retry is queued behind its own closing sheet now gets .stillClosing ("Session "work"'s previous system sheet is still closing."). Before, it said the session was waiting for a sheet it held itself. This applies to the hosted-UI in-progress check and to the lock's own repeat-call refusal. A pending release-note row records it.
  8. A test pins what "queued" means. testADoubleTapWhileAnotherSessionHoldsTheSheetDoesNotSayItIsQueued now queues a third session first, so "queued" must mean this session is queued.
  9. Two docs no longer describe a sign-out that never happens. AuthClientError.service's doc, and decision 37, now say "In a .partial sign-out". signOutStoredSession's Returns doc no longer names a global sign-out, which that call never sends.

Also in this PR: review follow-ups from this PR

Besides the three leaks and the confirmSignIn sign-out above:

  1. The access-group write tolerates a concurrent delete. The login is replaced with replaceIfPresent, not a check-then-update set. Outside native macOS, a delete by another process between the check and the update used to fail the sign-out after the revoke, and put the session back to .signedIn. A record that's already signed out is no longer written again.
  2. A failed write and the queries under a group are pinned. A failed write fails the sign-out and keeps the login. The keychain queries of signOut and deleteUser under an access group are pinned outside the locked goldens.
  3. The "still closing" refusal of a session whose own call is queued now suggests waiting for that call, which starts by itself, or cancelling it, instead of retrying.
  4. Docs. The release notes no longer describe an AuthError.unknown text that no sign-in reaches. They say the sign-out clears only the signing-out app's own AWSMobileClient locations (decision 36 too), give the exception of an app that leaves its group with migration, and correct what the record changes for storedSessions and an extension's migration. They also list what a process that reads another process's signed-out record now sees, and quote 2.63.0's hosted-UI text. The client's parser row and two comments no longer say that the client logged an answer through the parsers. signOutStoredSession's Returns doc names the no-window hosted-UI error a sign-out that couldn't finish can carry.
  5. The confirmSignIn sign-out's failure exit has a test. When the credential store refuses the local clear, the sign-out that cancels the sign-in ends in an error and goes back to signed in, never to signed out. The wait ends on that error too, but no test covered it: removing it left every confirmSignIn test passing. A new test pins that the call returns the identity pool's error instead of hanging.
  6. The cancel revokes the sign-in's own tokens. Under an access group the cancel's sign-out revoked the login the store held, which can be another app's, and left this sign-in's refresh token valid. It now revokes the tokens it was given (above).
  7. Docs. The client README's differences row, decision 32 and a test's Given now say the plugin's confirmSignIn hung only in 2.63.0 and earlier, and the row says the plugin's cancel writes the signed-out record over another app's login under an access group. Decision 40 and AccessGroupSignedOutRecordTests no longer say a signed-out record is written over: it is left as it is. A release-note row covers the cancel's revoke and the error line it no longer logs, and the sign-up row tells apps to remove their own AuthSignUpStep description conformance.

Also in this PR: deferred follow-ups

Five follow-ups that the reviews of #4401 and #4402 deferred. Only the first changes what an app sees, and only in AmplifyCognitoClient; the others are tests, CI and the integration-test scripts. None changes public API.

  1. The client keeps a session's expiry across launches. When a refresh proves the refresh token dead (refused as reused twice, 30 seconds apart, or refused as expired or revoked, including a federated session's refused provider token), the client now saves that beside the session's record: a deadToken key in a named session's envelope, or in .default's sidecar. Both are additive keys, written only when set, so there is no schema change. So a new client of the session, or the app after a relaunch, answers the same sessionExpired without refreshing the dead token again, and signIn is allowed on it without signing out. Before, the expiry lived in memory only: the next launch refreshed the dead token once more, got the same refusal and started the 30-second rule over.
    • The marker names the dead token (a SHA-256 of the refresh token, or of the credentials when they hold none), not the record. So a new login or federation by any writer ends it, including the Auth plugin, which never reads or writes it. The plugin's record is never rewritten for it, and the plugin's own expired flag is not read: the plugin sets it on whatever record is stored after its own refusal, which may be another writer's newer login.
    • The save is best effort. A failed save logs one warning, and the expiry then stays in memory, as before. A marker this build can't read is ignored.
    • Still per launch: the count before the proof. A relaunch between the first and the second reuse refusal starts the count again. The README's known limitation now says only that.
    • Docs: the client README (sign-in, token fetches, hosted-UI sign-out, known limitations), the design's storage model, a new decision 41 (refining 28 and 35), and the release notes.
  2. Two tests for a resumed access-group migration. A resumed migration killed before the login moved keeps the configuration record in the unshared service, so the next launch still carries the login after an app extension with the new configuration saved its record again. And a resumed migration in the same update as a configuration change keeps a login that an app extension saved under the new configuration's account, not only a signed-out record. No code change.
  3. CI provisioner (infra/ci/provision-ci.sh).
    • The code sink's API key extends itself. A new Lambda, ccit-ci-extend-api-key, runs weekly from an EventBridge rule and extends the code-reading tests' read-only AppSync key in place, to 365 days from now, once 120 or fewer days are left. The key keeps its id, so the uploaded test configuration stays valid. Its role, ccit-ci-key-exec, may only write its own logs and list and update the code sink API's keys. It logs dates, never the key: when AppSync refuses an extension, it fails with a fixed text that carries only the error's name, never the SDK's message. The Lambda drift check covers it as the ninth CI Lambda, so a Lambda baseline can be recorded only after the post-merge --apply --scope all has made it.
    • An expired key is extended in place first. AppSync keeps an expired key for 60 days and, per its API reference, still lets it be updated. A rerun now tries that before making a new key. Only when AppSync refuses, or the key is gone, does the rerun make a new one and, as before, refuse until the uploaded files that name the old key are replaced.
    • A phase-1 --apply is refused once phase 2 exists. It stops before its dry run when any phase-2 pool, Lambda, role or the code sink's API exists, names what it found, and says to use --scope all. A plain dry run prints the same as a note and plans nothing.
    • replace <pool>. A guarded command replaces one of the script's pools after a template change. It takes only the script's four pool names. After each tag check, it deletes the pool and what names it (its uploaded file and, for the default pool, the rotation file, the identity pool and the new-password reset Lambda), then reruns --apply --scope all --upload. It is a dry run by default. With --apply it runs both dry runs again first and deletes nothing if either refuses. It refuses while gh shows a queued or running CI run that reads the pools. When gh is missing, or can't list the runs (not logged in, say), it only warns and goes on, so check that no run is going first.
    • Tests. The fake AWS now checks the SNS caller role's trust when an SMS pool is made, so that check no longer needs the git history. Another test checks that replace refuses an uploaded file without the purpose tag.
    • This needs a provisioning run after merge. Until an --apply --scope all runs in the CI account, the key extension doesn't exist and nothing else changes. That run makes only the new Lambda, its role, its rule and its log group, all ccit-ci- additions. Only after it can the Lambda baseline be recorded again: a record needs every CI Lambda deployed, the new one included. The steps are in the team's internal runbook.
  4. The sandbox's first run refuses a profile without its own credentials. With no account recorded yet, lib.sh's check now also reads aws configure list. It goes on only when the profile AWS_PROFILE names supplies the credentials itself: keys in the credentials or config file, sso, login, a credential_process, or a role_arn it assumes (assume-role or assume-role-with-web-identity). An EC2 instance role, container credentials, a legacy credentials file, no credentials, or a profile the CLI can't resolve is refused before any AWS call. So a profile that holds only a region can no longer build the sandbox in an instance role's account. The exported-keys and web identity refusals are unchanged, and a recorded account is still checked against the caller's identity. The integration README lists what is accepted.
  5. Test selector.
    • A host app's README that an Xcode project copies into a test bundle (twelve host-app READMEs are Copy Bundle Resources) could be deleted or renamed while the PR selected nothing, breaking the build of the target that copies it. The detect steps of the seven workflows that feed the selector (integ_test.yml, integ_test_api.yml, integ_test_datastore.yml, unit_test.yml, build_amplify_swift_platforms.yml, build_minimum_supported_swift_platforms.yml, build_xcode_preview.yml) now run affected_targets.py --check-bundled before selecting. It reads every Xcode project in the checkout and fails with an ::error on the project file for each file a project copies into a bundle that is missing from the checkout, other than the ones git ignores (the integration-test configurations CI writes before building). A project it can't read, a root with no project and a failed git query fail it too. That covers the six READMEs only stress-test targets copy (GraphQLAPIStressTests, AnalyticsStressTests, AuthStressTests, DatastoreStressTests, GeoStressTests, StorageStressTests), which no PR job builds. Documentation stays ignored by the selection wherever it lives, so editing a bundled README selects nothing (with the unit groups it no longer selects AWSPluginsCore, which the three build_* workflows turned into a full package build).
    • The check's --repo-root defaults to the checkout that holds the script rather than the current directory, so a run from another directory still reads this checkout's projects.
    • The selector's self-test moves out of ci_script_self_tests.yml into its own ci_selector_self_tests.yml, which also runs on a change to any project.pbxproj, since the check's self-test reads the real Xcode projects. A project change no longer starts the other script self-tests. The job keeps its name, Test selector; no branch protection rule or ruleset requires it.
    • The seven detect steps now also pass each rename's old and new paths, through a new --renames file (git diff --name-status -M --diff-filter=R | cut -f2-, tab-separated); the changed list keeps rename detection. The selector attributes an old path like any changed file, so a file moved out of a target still selects that target's group. An old path that nothing owns, covers or ignores is skipped only when the rename's new path is itself attributed (owned by a target, or under a group's paths); otherwise it fails closed, as its deletion does, so an unknown file moved into scripts/ or .github/ selects every group rather than none. A move that changes a target's directory selects what the new paths select: moving RecordValidationTests.swift into AmplifyRecordCache (c6bc9e3) selects 7 unit groups, not all 22. A plain deletion still fails closed.

Testing

  • swift build --build-tests

  • swift test --filter AWSCognitoAuthPluginUnitTests: 1221 tests, 2 skipped, 0 failures

  • swift test --filter AmplifyCognitoClientTests: 1465 tests, 0 failures

  • swift test --filter InternalAmplifyKeychainTests: 104 tests, 0 failures

  • swift test --filter AmplifyTests --skip 'testPreconditionFailure|FatalError|shouldThrowFatalError': 473 tests, 10 skipped, 0 failures

  • scripts/cognito-engine/check_engine_deps.py --stage final, error_match_gate.sh check S9, check_engine_privacy_manifest.py, and engine_paths.txt against its generator: clean

  • check_fixture_lock.sh HEAD: passes

  • SwiftFormat 0.60.1 on the 69 changed Swift files: no changes. SwiftLint on the 20 changed Amplify/ and AmplifyPlugins/ files: no errors.

  • Offline, with every AWS_* variable unset, AWS_CONFIG_FILE and AWS_SHARED_CREDENTIALS_FILE set to /dev/null, and IMDS off:

    • selector self-tests, python3 -m unittest discover -s scripts/python -p 'test_*.py': 17 tests, also on macOS's /usr/bin/python3 (3.9);
    • affected_targets.py --check-bundled over this checkout: 50 bundled files in 16 projects, none missing;
    • the infra self-tests as auth_client_infra_self_tests.yml runs them: python3 -m unittest discover -s infra -p 'test_*.py' (173 tests), every infra/test_*.sh and infra/ci/test_*.sh (test_provision_ci.sh: 43 checks, its rerun over the CI resources as first applied included), test_lambda_code.sh also on /usr/bin/python3 (3.9), and node --test on the Lambda tests (20);
    • a dry run of this branch's provision-ci.sh --scope all over the fake account as ci(auth): AmplifyCognitoClient CI and integration-test infrastructure follow-ups #4402's script left it plans exactly the key extension's 8 calls, all ccit-ci-; the apply makes them, and a second dry run plans nothing.
  • actionlint on the nine changed and new workflows: each reports only what it reports on main, and the new ci_selector_self_tests.yml nothing.

  • xcodebuild build-for-testing -skipPackagePluginValidation for the client host app's four test schemes (CognitoClientIntegrationTests, CognitoClientPluginInteropTests, CognitoClientUITests, CognitoClientWebAuthnUITests), with an empty COGNITO_CLIENT_INTEG_DIR: succeeded.

New tests:

  • AWSAuthConfirmSignInTaskTests.testConfirmSignInThrowsTheIdentityPoolsError: GetId fails after the user pool accepted the answer. confirmSignIn throws .service with invalidParameter. Afterwards the machine is signed out, the refresh token was revoked once, fetchAuthSession() reports signed out, getCurrentUser() throws signedOut, and a second confirmSignIn throws invalidState without calling Cognito.
  • AWSAuthConfirmSignInTaskTests.testConfirmSignInLeavesAnAdoptedSignInWhoseAuthorizationFailed: an adopted sign-in whose authorization failed stays signed in, and nothing is revoked.
  • SignInAnswerRedactionTests: the answer's printed forms, the events and states that carry it (the Checking next step for: and Auth state change lines included), and the TOTP setup event's session, in every printed form; and VerifyTOTPSetup's line.
  • SignInSecretLogTests, end to end through the plugin with a capturing logging plugin at verbose: a password confirmSignIn, a TOTP setup's confirmSignIn, and a signUp and confirmSignUp that return .completeAutoSignIn. No line holds the answer (in full or masked), the TOTP code or either session in full. The result lines show completeAutoSignIn("<REDACTED>").
  • AuthSignUpStepDescriptionTests (AmplifyTests): the step and the result print the session redacted, and the other steps print as default printing does.
  • AccessGroupSignedOutRecordTests+Writes: a sign-out over a store that checks and then updates completes when the record is deleted in between. A delete after the read leaves nothing written. A signed-out record is not written again. A failed write fails the sign-out and keeps the login. The keychain queries of signOut and deleteUser under an access group are pinned.
  • ChallengeSessionRedactionTests: the challenge, the events and states that carry it, and the SRP verify-password line print without the session, in every printed form.
  • EngineStaticLogSiteTests, EngineStaticSiteRoutingTests and the two fork tests: the parsers' error line no longer holds the answer on the plugin's and the engine's confirmSignIn paths.
  • ChallengeSessionRedactionTests.testAnUnresolvableMFASetupErrorHoldsTheChallengeWithoutItsSession: the SignInError.unknown of an SMS-only MFA setup, and the AuthError the plugin throws with it, hold the challenge with the session <REDACTED> and no username in full. No sign-in reaches that throw today, so the test runs the action on its own.
  • AccessGroupSignedOutRecordTests (and its +Migrations file): each path that ends a login saves the record under an access group and deletes it without one. The tests also cover:
    • over each kind of record: a login or a federated identity is replaced by the signed-out record, and the signed-out record stays; a guest's or an unreadable record is deleted; with nothing saved, nothing is written;
    • from the reviewer's scenario, an extension's signOut() with nothing saved, and a guest's signOut(): neither writes a record, and the app's first migration then moves its login;
    • the next launch, and an extension's first migration and first transition;
    • a sign-in after the sign-out;
    • the client's .default reading the record;
    • the AWSMobileClient migration at the next launch;
    • a guest the client fetches over the record;
    • a rollback after a configuration carry.
  • AWSAuthConfirmSignInTaskTests.testConfirmSignInCancelRevokesTheSignInsOwnTokensNotTheSavedLogin: with a credential store that holds another user pool login, the cancel after a failed GetId revokes only refreshTokenXXX, the sign-in's own, and the machine is signed out.
  • AWSAuthConfirmSignInTaskTests.testConfirmSignInThrowsTheIdentityPoolsErrorWhenTheSignOutCannotClearTheStore: with a credential store that refuses the local clear, confirmSignIn still throws the identity pool's error within the timeout, after one revoke and one refused delete, and the state machine goes back to signed in.
  • From the deferred follow-ups:
    • SavedExpiryTests: a released client, a relaunch over the same keychain for a named session and for .default, a refused token, another process's core, a sign-in, another process's new login, the plugin's new login beside a stale sidecar marker, a new federation, a failed save, and the stored format. Also WebUISignOutTests.testClosingTheLogoutPageOfASessionExpiredBeforeARelaunchStillSignsOut. The reuse-reset test of the refused-token branch is now CredentialProviderTests.testARefusalAsInvalidEndsTheReuseCount: the text comes from the saved cause, so the reset only ends the count of reuses in a row, which shows when the expiry could not be saved.
    • AWSCognitoAuthCredentialStoreInterruptedMigrationTests.testAResumedMigrationKilledBeforeTheLoginMovedStillCarriesItAfterAnExtensionStartsAgain and GuestFirstAccessGroupMigrationTests.testAResumedMigrationDoesNotCarryTheLoginOverALoginAnExtensionSavedSince.
    • test_provision_ci.sh: the key extension's function, role and rule; the key at, within and past the reuse threshold; an expired key extended in place, or replaced when AppSync refuses; the phase-1 refusal; the fake's SNS caller-role trust check and the remade SMS pool, without git history; and replace (its names, its dry run and CI-run refusal, its tag checks, the default pool, and phase 1 alone). test_extend_api_key.mjs for the Lambda itself, including that a refused extension of a live key fails with a fixed text that doesn't name the key.
    • test_provision.sh: a first run goes on with the profile's own credentials, refuses an instance role or another fallback, and a recorded account is still checked against the caller's identity.
    • test_affected_targets.py: the rename rule, the default repo root, a bundled README renamed away from .md, and the bundled-file check, including a missing file whose name holds % and a newline, named on one escaped ::error line.
  • From the fix(auth): AmplifyCognitoClient follow-ups from the beta review #4401 follow-ups:
    • DefaultSessionGuestLegacyLoginTests.testAGuestClearsThemBeforeItIsWritten: the guest path clears the AWSMobileClient logins while .default still holds the signed-out record, before it writes the guest.
    • WebUISignInTests.testADoubleTapQueuedBehindItsOwnClosingSheetSaysItIsStillClosing: a double tap after a cancel, with a .wait(timeout:) retry queued behind the session's own closing sheet, gets .stillClosing with each policy. The retry then signs in.
    • SystemSheetLockTests.testARepeatCallQueuedBehindItsOwnClosingSheetIsToldItIsStillClosing: the lock's own repeat-call refusal, in the same case.
    • WebUISignInInProgressTests.testADoubleTapWhileAnotherSessionHoldsTheSheetDoesNotSayItIsQueued now queues a third session before the double tap.

Each fix's tests were checked to fail with only that fix reverted, in a scratch copy:

  • the cancel reading the saved login again: the new revoke test fails (it revokes anotherAppsRefreshToken).
  • the .signingOut(.error) exit removed from confirmSignIn's wait for the cancel: only the new failed-clear test fails (it never returns). The other 27 confirmSignIn tests pass.
  • the key extension Lambda passing on AppSync's refusal of a live key as it came: only its refusal test fails (1 of the 9 Lambda tests).
  • the bundled-file check's ::error message left unescaped: only the new %-and-newline test fails.
  • the confirmSignIn arm: the new test fails (it never returns).
  • the sign-in cancel before the throw, removed: testConfirmSignInThrowsTheIdentityPoolsError fails. Cancelling an adopted sign-in too: testConfirmSignInLeavesAnAdoptedSignInWhoseAuthorizationFailed fails.
  • the answer kept in .verifying: SignInSecretLogTests.testConfirmSignInLogsNoLineWithTheAnswer and the state check in SignInAnswerRedactionTests fail. The answer masked again in its own printed form: the two answer tests in SignInAnswerRedactionTests and the TOTP setup's end-to-end test fail.
  • the TOTP setup event printing its session: the two TOTP tests in SignInAnswerRedactionTests and the end-to-end TOTP test fail.
  • AuthSignUpStep printing its session, with the conformances kept: two of the three AuthSignUpStepDescriptionTests (the third checks the other steps), the sign-up end-to-end test and EnginePayloadLogTextTests' sign-up test fail. The engine's copy printing it alone: only EnginePayloadLogTextTests' sign-up test fails.
  • the login written with set again: the check-then-update race test, the delete-after-read test and the query test fail. A signed-out record written again: only its no-write test fails. A try? on the write: only the failed-write test fails.
  • .stillClosing used again for a queued call: in the lock, only the SystemSheetLockTests test fails; in the hosted-UI in-progress check, only the WebUISignInTests test fails.
  • the redaction, with the conformances kept but printing the session and usernames as-is: 4 of the 5 ChallengeSessionRedactionTests fail, the error-text test included. The fifth checks a challenge with no session.
  • the access-group save, with a login's record deleted again: 11 of the 14 AccessGroupSignedOutRecordTests fail. The 3 that pass are the no-access-group case, and the guest and nothing-saved sign-outs, which delete or write nothing either way.
  • the guard on what is saved, with a guest's record and an absent one replaced by the signed-out record: the guest, nothing-saved and per-record tests fail (3).
  • a record already signed out, deleted instead of left: the per-record test fails.
  • the parser text: 10 tests fail across LogTranscriptSnapshotTests, EngineStaticLogSiteTests, EngineStaticSiteRoutingTests and the two fork tests.
  • the guest path's clear, moved after the guest's write: only the new order test fails. The other five guest tests pass, as the reviewer found.
  • .alreadyQueued answered again when the session holds the sheet: in the in-progress check, only the new WebUISignInTests test fails; in the lock, only the new SystemSheetLockTests test fails.
  • holderAndQueue(for:) reporting any waiter as this session's (!waiters.isEmpty): only the updated in-progress test fails.

Follow-ups

@harsh62
harsh62 requested a review from thisisabhash October 28, 2026 22:54
@harsh62
harsh62 requested review from a team as code owners October 28, 2026 22:54
@harsh62
harsh62 deployed to IntegrationTest October 29, 2026 00:11 — with GitHub Actions Active
@harsh62
harsh62 deployed to IntegrationTest October 29, 2026 00:11 — with GitHub Actions Active
@harsh62
harsh62 deployed to IntegrationTest October 29, 2026 00:11 — with GitHub Actions Active
@harsh62
harsh62 deployed to IntegrationTest October 29, 2026 00:11 — with GitHub Actions Active
@harsh62
harsh62 deployed to IntegrationTest October 29, 2026 00:11 — with GitHub Actions Active
@harsh62
harsh62 force-pushed the fix/cognito-plugin-followups branch from 69b6234 to 1d6e23d Compare October 29, 2026 01:46
@harsh62
harsh62 deployed to IntegrationTest October 29, 2026 02:03 — with GitHub Actions Active
@harsh62
harsh62 deployed to IntegrationTest October 29, 2026 02:03 — with GitHub Actions Active
@harsh62
harsh62 deployed to IntegrationTest October 29, 2026 02:03 — with GitHub Actions Active
@harsh62
harsh62 deployed to IntegrationTest October 29, 2026 02:03 — with GitHub Actions Active
@harsh62
harsh62 deployed to IntegrationTest October 29, 2026 02:03 — with GitHub Actions Active
@harsh62
harsh62 deployed to IntegrationTest October 29, 2026 02:03 — with GitHub Actions Active
@harsh62
harsh62 deployed to IntegrationTest October 29, 2026 02:03 — with GitHub Actions Active
@harsh62
harsh62 deployed to IntegrationTest October 29, 2026 02:03 — with GitHub Actions Active
@harsh62
harsh62 deployed to IntegrationTest October 29, 2026 02:03 — with GitHub Actions Active
@harsh62
harsh62 deployed to IntegrationTest October 29, 2026 02:03 — with GitHub Actions Active
@harsh62
harsh62 deployed to IntegrationTest October 29, 2026 02:03 — with GitHub Actions Active
@codecov

codecov Bot commented Oct 29, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 93.31104% with 20 lines in your changes missing coverage. Please review.
✅ Project coverage is 79.50%. Comparing base (5f7a95d) to head (1d0b39a).

Files with missing lines Patch % Lines
...ognitoAuth/Sources/Models/EngineSignUpResult.swift 57.89% 8 Missing ⚠️
...es/Storage/SessionRecordStore+DefaultSession.swift 85.29% 5 Missing ⚠️
...s/StateMachine/CodeGen/Events/SetUpTOTPEvent.swift 93.33% 2 Missing ⚠️
...mplify/Categories/Auth/Models/AuthSignUpStep.swift 94.73% 1 Missing ⚠️
...gnitoClient/Sources/Core/SessionCore+Refresh.swift 96.29% 1 Missing ⚠️
...itoClient/Sources/Storage/SessionRecordStore.swift 87.50% 1 Missing ⚠️
...solvers/SignIn/SignInChallengeState+Resolver.swift 75.00% 1 Missing ⚠️
...nitoAuthPlugin/Task/AWSAuthConfirmSignInTask.swift 96.15% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #4405      +/-   ##
==========================================
+ Coverage   75.86%   79.50%   +3.63%     
==========================================
  Files        1360      936     -424     
  Lines       57623    35470   -22153     
==========================================
- Hits        43717    28199   -15518     
+ Misses      13906     7271    -6635     
Flag Coverage Δ
API_plugin_unit_test ?
AWSPluginsCore 77.28% <ø> (ø)
Amplify 48.69% <94.73%> (+0.20%) ⬆️
Amplify_Foundation_Bridge_unit_test ?
Amplify_Foundation_unit_test 81.35% <ø> (ø)
Amplify_Keychain_unit_test 83.93% <ø> (ø)
Analytics_plugin_unit_test ?
Auth_plugin_unit_test 84.12% <91.94%> (+0.53%) ⬆️
CloudWatchClient_unit_test ?
CognitoClient_unit_test 95.58% <94.65%> (-0.03%) ⬇️
Connect_unit_test ?
DataStore_plugin_unit_test ?
EventEnrichment_unit_test ?
Firehose_plugin_unit_test ?
Geo_plugin_unit_test ?
Kinesis_plugin_unit_test ?
Logging_plugin_unit_test ?
Predictions_plugin_unit_test ?
PushNotifications_plugin_unit_test ?
RecordCache_unit_test ?
Storage_plugin_unit_test ?
unit_tests 79.50% <93.31%> (+3.63%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Comment thread docs/design/RELEASE-NOTES-PENDING.md Resolved
Comment thread docs/design/RELEASE-NOTES-PENDING.md Resolved
Comment thread docs/design/RELEASE-NOTES-PENDING.md Resolved
…actor parsers' error line

On the plugin's confirmSignIn the value is the user's answer, so an answer that is not a known MFA type or factor was logged at error level. Log the fixed text "Tried to initialize an unsupported MFA type" for every value instead. The log-transcript golden's scenario 7 line changes accordingly, and its pins in LogTranscriptSnapshotTests and check_fixture_lock.sh are updated. The Cognito client refuses an answer that is not a known type before the engine runs, so its line only printed values from Cognito or a saved record; two of its comments that described the old line are updated.
@harsh62
harsh62 force-pushed the fix/cognito-plugin-followups branch from be068dc to 1d0b39a Compare October 29, 2026 23:36
@harsh62
harsh62 deployed to IntegrationTest October 29, 2026 23:47 — with GitHub Actions Active
@harsh62
harsh62 deployed to IntegrationTest October 29, 2026 23:47 — with GitHub Actions Active
@harsh62
harsh62 deployed to IntegrationTest October 29, 2026 23:47 — with GitHub Actions Active
@harsh62
harsh62 deployed to IntegrationTest October 29, 2026 23:47 — with GitHub Actions Active
@harsh62
harsh62 deployed to IntegrationTest October 29, 2026 23:47 — with GitHub Actions Active
@harsh62
harsh62 deployed to IntegrationTest October 29, 2026 23:47 — with GitHub Actions Active
@harsh62
harsh62 deployed to IntegrationTest October 29, 2026 23:47 — with GitHub Actions Active
@harsh62
harsh62 deployed to IntegrationTest October 29, 2026 23:47 — with GitHub Actions Active
@harsh62
harsh62 deployed to IntegrationTest October 29, 2026 23:47 — with GitHub Actions Active
@harsh62
harsh62 deployed to IntegrationTest October 29, 2026 23:47 — with GitHub Actions Active
@harsh62
harsh62 deployed to IntegrationTest October 29, 2026 23:47 — with GitHub Actions Active

This branch was successfully deployed

2 active deployments
Fortify — 1d0b39aa Deployed Oct 29, 2026 by harsh62 via fortify-scan #3013
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