Skip to content

fix(auth): defer keychain-sharing reconfigure during in-flight sign-in - #4231

Merged
harsh62 merged 3 commits into
mainfrom
fix/auth-fetchsession-reconfigure-signin
Jun 10, 2026
Merged

fix(auth): defer keychain-sharing reconfigure during in-flight sign-in#4231
harsh62 merged 3 commits into
mainfrom
fix/auth-fetchsession-reconfigure-signin

Conversation

@harsh62

@harsh62 harsh62 commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

Issue

Fixes #4224

Description

When AWSCognitoAuthPlugin is configured with a shared keychain access group, every call to Amplify.Auth.fetchAuthSession unconditionally sent a .reconfigure event to the auth state machine. If the call landed between a sign-in step that returned .confirmSignInWithCustomChallenge and the caller's confirmSignIn, the reconfigure tore down the in-flight .signingIn substate and the subsequent confirmSignIn threw:

AuthError.invalidState(
  "User is not attempting signIn operation",
  "Operation performed is not a valid operation for the current auth state"
)

The user could not complete sign-in until the entire flow was restarted. The repro is most visible with Custom Auth flows (which routinely park .signingIn for tens of seconds while waiting for human approval, push delivery, or third-party verification), but it affects any in-flight sign-in flow.

Why was the reconfigure there?

The keychain-sharing feature (#3947) needs sibling-app keychain mutations to be picked up without an app relaunch. The original implementation answered "did anything change?" with a coarse proxy: accessGroup != nil. That predicate is true for the lifetime of the process, so reconfigure fires on every fetchAuthSession regardless of whether the keychain actually changed — and regardless of whether the local state machine has work in flight.

Fix

Replace the unconditional reconfigure with a reconcile that:

  1. Reads the shared keychain credentials and compares to local state — skip reconfigure when they match. Eliminates wasted reconfigures on every fetch.
  2. Defers reconfigure during locally-originated in-flight flows when the remote keychain has no signed-in credentials:
    • .signingIn — would destroy the user's pending confirmSignIn flow
    • .signingOut, .deletingUser, .federatingToIdentityPool, .clearingFederation — would skip in-flight side effects (token revoke, HostedUI session clearing, credential clearing, Hub events)
      The local flow runs to completion; the next fetchAuthSession reconciles with whatever is in the keychain at that point.
  3. Adopts a sibling app's sign-in mid-flow when remote has user-pool tokens. The reconfigure runs even during .signingIn because the new credentials are strictly more useful than the in-flight challenge. To make the adoption succeed end-to-end, AWSAuthConfirmSignInTask now accepts .signedIn at its top-level guard and returns AuthSignInResult(nextStep: .done) instead of throwing invalidState.

Decision matrix

Local AuthenticationState Remote vs local Action
.signedIn / .signedOut / .error / etc. match no-op
.signedIn / .signedOut / .error / etc. differ reconfigure
.signingIn remote has user-pool tokens reconfigure (adopt)
.signingIn remote = noCredentials defer
.signingOut / .deletingUser / .federatingToIdentityPool / .clearingFederation any defer

Files changed

  • AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Task/AWSAuthFetchSessionTask.swift — replace forceReconfigure boolean with isKeychainSharingEnabled + reconcileWithSharedKeychainIfNeeded() decision logic.
  • AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Task/AWSAuthConfirmSignInTask.swift — accept .signedIn at top-level guard, return .done so adoption resolves cleanly.
  • AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/ClientBehavior/AWSCognitoAuthPlugin+ClientBehavior.swift — rename forceReconfigureisKeychainSharingEnabled at the call site.
  • AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/CredentialStorage/AmplifyCredentials.swift — add hasUserPoolTokens helper for the adopt predicate.

Tests

New AWSAuthFetchSessionTaskKeychainSharingTests covers:

testConfirmSignInReturnsDoneWhenStateIsAlreadySignedIn covers the relaxed confirmSignIn guard producing .done after adoption.

All existing AWSCognitoAuthPluginUnitTests continue to pass.

General Checklist

  • Added new tests to cover change
  • Build succeeds with all targets using Swift Package Manager
  • All unit tests pass
  • All integration tests pass
  • Security oriented best practices and standards are followed
  • PR title conforms to conventional commit style

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

When AWSCognitoAuthPlugin is configured with a shared keychain access
group, every fetchAuthSession unconditionally sent a .reconfigure event
to the auth state machine. If a fetchAuthSession landed between a
sign-in step that returned .confirmSignInWithCustomChallenge and the
caller's confirmSignIn, the reconfigure tore down the .signingIn
substate and the next confirmSignIn threw
AuthError.invalidState("User is not attempting signIn operation").

Replace the unconditional reconfigure with a reconcile that:

- Reads the shared keychain credentials, compares against the local
  state machine's authZ credentials, and skips reconfigure when they
  match — eliminating wasted reconfigures on every fetch.
- Defers reconfigure during locally-originated in-flight flows
  (.signingIn, .signingOut, .deletingUser, .federatingToIdentityPool,
  .clearingFederation) when the keychain has no signed-in credentials.
- Adopts a sibling app's sign-in by reconfiguring even during
  .signingIn when the remote keychain has user-pool tokens. Relax the
  AWSAuthConfirmSignInTask top-level guard to accept .signedIn and
  return .done so the pending confirmSignIn resolves cleanly.

Fixes #4224.
@harsh62
harsh62 requested a review from a team as a code owner June 4, 2026 16:46
@harsh62
harsh62 temporarily deployed to IntegrationTest June 4, 2026 16:47 — with GitHub Actions Inactive
@harsh62
harsh62 temporarily deployed to IntegrationTest June 4, 2026 16:47 — with GitHub Actions Inactive
@harsh62
harsh62 temporarily deployed to IntegrationTest June 4, 2026 16:47 — with GitHub Actions Inactive
@harsh62
harsh62 temporarily deployed to IntegrationTest June 4, 2026 16:47 — with GitHub Actions Inactive
@harsh62
harsh62 temporarily deployed to IntegrationTest June 4, 2026 16:47 — with GitHub Actions Inactive
@harsh62
harsh62 temporarily deployed to IntegrationTest June 4, 2026 16:47 — with GitHub Actions Inactive
@harsh62
harsh62 temporarily deployed to IntegrationTest June 4, 2026 16:47 — with GitHub Actions Inactive
@harsh62
harsh62 temporarily deployed to IntegrationTest June 4, 2026 16:47 — with GitHub Actions Inactive
@harsh62
harsh62 temporarily deployed to IntegrationTest June 4, 2026 16:47 — with GitHub Actions Inactive
@harsh62
harsh62 temporarily deployed to IntegrationTest June 4, 2026 16:47 — with GitHub Actions Inactive
@harsh62
harsh62 temporarily deployed to IntegrationTest June 4, 2026 16:47 — with GitHub Actions Inactive
@harsh62
harsh62 temporarily deployed to IntegrationTest June 4, 2026 16:47 — with GitHub Actions Inactive
@harsh62
harsh62 temporarily deployed to IntegrationTest June 4, 2026 16:47 — with GitHub Actions Inactive
@harsh62
harsh62 temporarily deployed to IntegrationTest June 4, 2026 16:47 — with GitHub Actions Inactive
@harsh62
harsh62 temporarily deployed to IntegrationTest June 4, 2026 16:47 — with GitHub Actions Inactive
@harsh62
harsh62 temporarily deployed to IntegrationTest June 4, 2026 16:47 — with GitHub Actions Inactive
@harsh62
harsh62 temporarily deployed to IntegrationTest June 4, 2026 16:47 — with GitHub Actions Inactive
@harsh62
harsh62 temporarily deployed to IntegrationTest June 4, 2026 16:47 — with GitHub Actions Inactive
@harsh62
harsh62 temporarily deployed to IntegrationTest June 4, 2026 16:47 — with GitHub Actions Inactive
@harsh62
harsh62 temporarily deployed to IntegrationTest June 4, 2026 16:47 — with GitHub Actions Inactive
@harsh62
harsh62 temporarily deployed to IntegrationTest June 4, 2026 16:47 — with GitHub Actions Inactive
Fold the .signedIn early-return into the same switch that handles
.signingIn dispatch. The listener loop already returns .done on
.signedIn(.sessionEstablished), so the early return was redundant —
the only thing the .signedIn branch needs to do is skip the event
dispatch that's specific to the .signingIn path.
…CreateEvent and clarify keychain/state-machine fetch helper names

Move the .signedIn early-return for shared-keychain adoption into
analyzeCurrentStateAndCreateEvent so execute() has a single guard +
single dispatch. Rename fetchRemoteCredentials and the local-state
snapshot helper to fetchCredentialsFromKeychain and
fetchCredentialsFromStateMachine to make the source-of-truth pairing
explicit at the call site.
@codecov

codecov Bot commented Jun 4, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 79.06977% with 18 lines in your changes missing coverage. Please review.
✅ Project coverage is 66.78%. Comparing base (7428f64) to head (cfdd1c5).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
...gnitoAuthPlugin/Task/AWSAuthFetchSessionTask.swift 76.11% 16 Missing ⚠️
...hPlugin/CredentialStorage/AmplifyCredentials.swift 83.33% 1 Missing ⚠️
...nitoAuthPlugin/Task/AWSAuthConfirmSignInTask.swift 90.90% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #4231      +/-   ##
==========================================
+ Coverage   66.72%   66.78%   +0.05%     
==========================================
  Files        1151     1151              
  Lines       43817    43892      +75     
==========================================
+ Hits        29239    29312      +73     
- Misses      14578    14580       +2     
Flag Coverage Δ
API_plugin_unit_test 68.42% <ø> (+0.02%) ⬆️
AWSPluginsCore 68.48% <ø> (ø)
Amplify 47.47% <ø> (ø)
Amplify_Foundation_Bridge_unit_test 62.28% <ø> (ø)
Amplify_Foundation_unit_test 67.64% <ø> (ø)
Analytics_plugin_unit_test 83.43% <ø> (ø)
Auth_plugin_unit_test 72.38% <79.06%> (+0.09%) ⬆️
DataStore_plugin_unit_test 81.76% <ø> (+0.06%) ⬆️
Firehose_plugin_unit_test 53.15% <ø> (ø)
Geo_plugin_unit_test 73.39% <ø> (ø)
Kinesis_plugin_unit_test 52.17% <ø> (ø)
Logging_plugin_unit_test 64.86% <ø> (ø)
Predictions_plugin_unit_test 33.89% <ø> (ø)
PushNotifications_plugin_unit_test 85.66% <ø> (ø)
RecordCache_unit_test 76.40% <ø> (ø)
Storage_plugin_unit_test 78.67% <ø> (ø)
unit_tests 66.78% <79.06%> (+0.05%) ⬆️

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.

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.

Auth.fetchSession triggers state machine reconfigure that interrupts in-flight Custom Auth sign-in when keychain sharing is enabled

2 participants