77
88import Amplify
99import Foundation
10+ @_spi ( KeychainStore) import AWSPluginsCore
1011
1112class AWSAuthFetchSessionTask : AuthFetchSessionTask , DefaultLogger {
1213 private let request : AuthFetchSessionRequest
1314 private let authStateMachine : AuthStateMachine
1415 private let fetchAuthSessionHelper : FetchAuthSessionOperationHelper
1516 private let taskHelper : AWSAuthTaskHelper
1617 private let configuration : AuthConfiguration
17- private let forceReconfigure : Bool
18+ private let credentialsClient : CredentialStoreStateBehavior ?
19+ private let isKeychainSharingEnabled : Bool
1820
1921 var eventName : HubPayloadEventName {
2022 HubPayload . EventName. Auth. fetchSessionAPI
@@ -25,30 +27,134 @@ class AWSAuthFetchSessionTask: AuthFetchSessionTask, DefaultLogger {
2527 authStateMachine: AuthStateMachine ,
2628 configuration: AuthConfiguration ,
2729 environment: Environment ,
28- forceReconfigure : Bool = false
30+ isKeychainSharingEnabled : Bool = false
2931 ) {
3032 self . request = request
3133 self . authStateMachine = authStateMachine
3234 self . fetchAuthSessionHelper = FetchAuthSessionOperationHelper ( )
3335 fetchAuthSessionHelper. environment = environment
3436 self . taskHelper = AWSAuthTaskHelper ( authStateMachine: authStateMachine)
3537 self . configuration = configuration
36- self . forceReconfigure = forceReconfigure
38+ self . credentialsClient = ( environment as? AuthEnvironment ) ? . credentialsClient
39+ self . isKeychainSharingEnabled = isKeychainSharingEnabled
3740 }
3841
3942 func execute( ) async throws -> AuthSession {
4043 log. verbose ( " Starting execution " )
41- if forceReconfigure {
42- log. verbose ( " Reconfiguring auth state machine for keychain sharing " )
43- let event = AuthEvent ( eventType: . reconfigure( configuration) )
44- await authStateMachine. send ( event)
45- }
4644 await taskHelper. didStateMachineConfigured ( )
45+ if isKeychainSharingEnabled {
46+ await reconcileWithSharedKeychainIfNeeded ( )
47+ }
4748 let doesNeedForceRefresh = request. options. forceRefresh
4849 return try await fetchAuthSessionHelper. fetch (
4950 authStateMachine,
5051 forceRefresh: doesNeedForceRefresh
5152 )
5253 }
5354
55+ /// When the plugin is configured with a shared keychain access group, the
56+ /// keychain is the source of truth across processes. Reconcile the local
57+ /// state machine with whatever is currently in the keychain — but only
58+ /// when doing so won't destroy locally-originated in-flight work.
59+ ///
60+ /// Decision matrix (auth state vs. remote-vs-local credentials):
61+ /// - quiescent (.signedIn / .signedOut / .error / .configured / .notConfigured)
62+ /// - differ: reconfigure (sibling wrote — pick it up)
63+ /// - match: no-op
64+ /// - .signingIn
65+ /// - remote has user-pool tokens: reconfigure (adopt sibling's sign-in;
66+ /// confirmSignIn will resolve to .done)
67+ /// - remote has no user-pool tokens: defer (sign-in flow finishes;
68+ /// last-writer-wins on the keychain)
69+ /// - .signingOut / .deletingUser / .federatingToIdentityPool /
70+ /// .clearingFederation: defer (in-flight side effects must run; same end
71+ /// state is reached by the local flow)
72+ func reconcileWithSharedKeychainIfNeeded( ) async {
73+ guard let keychainCredentials = await fetchCredentialsFromKeychain ( ) else {
74+ return
75+ }
76+ guard case . configured( let authNState, let authZState, _) = await authStateMachine. currentState else {
77+ return
78+ }
79+ let stateMachineCredentials = fetchCredentialsFromStateMachine ( authZState)
80+ if let stateMachineCredentials, stateMachineCredentials == keychainCredentials {
81+ return
82+ }
83+
84+ if shouldDeferReconcile ( authNState: authNState, remote: keychainCredentials) {
85+ log. verbose ( " Deferring keychain reconcile while auth flow is in progress " )
86+ return
87+ }
88+
89+ log. verbose ( " Reconfiguring auth state machine for keychain sharing " )
90+ let event = AuthEvent ( eventType: . reconfigure( configuration) )
91+ await authStateMachine. send ( event)
92+ await taskHelper. didStateMachineConfigured ( )
93+ }
94+
95+ private func fetchCredentialsFromKeychain( ) async -> AmplifyCredentials ? {
96+ do {
97+ let data = try await credentialsClient? . fetchData ( type: . amplifyCredentials)
98+ if case . amplifyCredentials( let credentials) = data {
99+ return credentials
100+ }
101+ return nil
102+ } catch KeychainStoreError . itemNotFound {
103+ return . noCredentials
104+ } catch {
105+ log. verbose ( " Could not read shared keychain credentials: \( error) " )
106+ return nil
107+ }
108+ }
109+
110+ /// Best-effort snapshot of the credentials the local state machine last
111+ /// observed. Returns nil for transient authZ states where we can't make a
112+ /// reliable comparison; in those cases callers fall back to deferring (the
113+ /// transient state will resolve shortly and a later fetch will reconcile).
114+ private func fetchCredentialsFromStateMachine( _ authZState: AuthorizationState ) -> AmplifyCredentials ? {
115+ switch authZState {
116+ case . sessionEstablished( let credentials) ,
117+ . storingCredentials( let credentials) :
118+ return credentials
119+ case . refreshingSession( existingCredentials: let credentials, _) :
120+ return credentials
121+ case . federatingToIdentityPool( _, _, existingCredentials: let credentials) :
122+ return credentials
123+ case . signingOut( let credentials) :
124+ return credentials ?? . noCredentials
125+ case . configured:
126+ return . noCredentials
127+ case . notConfigured,
128+ . clearingFederation,
129+ . fetchingUnAuthSession,
130+ . fetchingAuthSessionWithUserPool,
131+ . deletingUser,
132+ . error:
133+ return nil
134+ }
135+ }
136+
137+ private func shouldDeferReconcile(
138+ authNState: AuthenticationState ,
139+ remote: AmplifyCredentials
140+ ) -> Bool {
141+ switch authNState {
142+ case . signingIn:
143+ // Adopt sibling sign-in; otherwise defer until local flow completes
144+ return !remote. hasUserPoolTokens
145+ case . signingOut,
146+ . deletingUser,
147+ . federatingToIdentityPool,
148+ . clearingFederation:
149+ return true
150+ case . notConfigured,
151+ . configured,
152+ . signedIn,
153+ . signedOut,
154+ . federatedToIdentityPool,
155+ . error:
156+ return false
157+ }
158+ }
159+
54160}
0 commit comments