Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -136,13 +136,13 @@ extension AWSCognitoAuthPlugin: AuthCategoryBehavior {
public func fetchAuthSession(options: AuthFetchSessionRequest.Options?) async throws -> AuthSession {
let options = options ?? AuthFetchSessionRequest.Options()
let request = AuthFetchSessionRequest(options: options)
let forceReconfigure = secureStoragePreferences?.accessGroup?.name != nil
let isKeychainSharingEnabled = secureStoragePreferences?.accessGroup?.name != nil
let task = AWSAuthFetchSessionTask(
request,
authStateMachine: authStateMachine,
configuration: authConfiguration,
environment: authEnvironment,
forceReconfigure: forceReconfigure
isKeychainSharingEnabled: isKeychainSharingEnabled
)
return try await taskQueue.sync {
return try await task.value
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,14 @@ extension AmplifyCredentials: CustomDebugStringConvertible {
}

}

extension AmplifyCredentials {
var hasUserPoolTokens: Bool {
switch self {
case .userPoolOnly, .userPoolAndIdentityPool:
return true
case .identityPoolOnly, .identityPoolWithFederation, .noCredentials:
return false
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -52,12 +52,11 @@ class AWSAuthConfirmSignInTask: AuthConfirmSignInTask, DefaultLogger {
AuthPluginErrorConstants.invalidStateError, nil
)

guard case .configured(let authNState, _, _) = await authStateMachine.currentState,
case .signingIn(let signInState) = authNState else {
guard case .configured(let authNState, _, _) = await authStateMachine.currentState else {
throw invalidStateError
}

try await analyzeCurrentStateAndCreateEvent(signInState, invalidStateError)
try await analyzeCurrentStateAndCreateEvent(authNState, invalidStateError)

let stateSequences = await authStateMachine.listen()
log.verbose("Waiting for response")
Expand Down Expand Up @@ -91,7 +90,19 @@ class AWSAuthConfirmSignInTask: AuthConfirmSignInTask, DefaultLogger {
throw invalidStateError
}

fileprivate func analyzeCurrentStateAndCreateEvent(_ signInState: SignInState, _ invalidStateError: AuthError) async throws {
fileprivate func analyzeCurrentStateAndCreateEvent(_ authNState: AuthenticationState, _ invalidStateError: AuthError) async throws {
// The shared keychain reconcile may have adopted a sibling app's
// sign-in while this flow was waiting for an answer. No event needs
// to be dispatched; the listener loop in execute() observes
// .signedIn and returns .done.
if case .signedIn = authNState {
return
}

guard case .signingIn(let signInState) = authNState else {
throw invalidStateError
}

switch signInState {
case .resolvingChallenge(let challengeState, let challengeType, _):
// Validate if request valid MFA selection
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,16 @@

import Amplify
import Foundation
@_spi(KeychainStore) import AWSPluginsCore

class AWSAuthFetchSessionTask: AuthFetchSessionTask, DefaultLogger {
private let request: AuthFetchSessionRequest
private let authStateMachine: AuthStateMachine
private let fetchAuthSessionHelper: FetchAuthSessionOperationHelper
private let taskHelper: AWSAuthTaskHelper
private let configuration: AuthConfiguration
private let forceReconfigure: Bool
private let credentialsClient: CredentialStoreStateBehavior?
private let isKeychainSharingEnabled: Bool

var eventName: HubPayloadEventName {
HubPayload.EventName.Auth.fetchSessionAPI
Expand All @@ -25,30 +27,134 @@ class AWSAuthFetchSessionTask: AuthFetchSessionTask, DefaultLogger {
authStateMachine: AuthStateMachine,
configuration: AuthConfiguration,
environment: Environment,
forceReconfigure: Bool = false
isKeychainSharingEnabled: Bool = false
) {
self.request = request
self.authStateMachine = authStateMachine
self.fetchAuthSessionHelper = FetchAuthSessionOperationHelper()
fetchAuthSessionHelper.environment = environment
self.taskHelper = AWSAuthTaskHelper(authStateMachine: authStateMachine)
self.configuration = configuration
self.forceReconfigure = forceReconfigure
self.credentialsClient = (environment as? AuthEnvironment)?.credentialsClient
self.isKeychainSharingEnabled = isKeychainSharingEnabled
}

func execute() async throws -> AuthSession {
log.verbose("Starting execution")
if forceReconfigure {
log.verbose("Reconfiguring auth state machine for keychain sharing")
let event = AuthEvent(eventType: .reconfigure(configuration))
await authStateMachine.send(event)
}
await taskHelper.didStateMachineConfigured()
if isKeychainSharingEnabled {
await reconcileWithSharedKeychainIfNeeded()
}
let doesNeedForceRefresh = request.options.forceRefresh
return try await fetchAuthSessionHelper.fetch(
authStateMachine,
forceRefresh: doesNeedForceRefresh
)
}

/// When the plugin is configured with a shared keychain access group, the
/// keychain is the source of truth across processes. Reconcile the local
/// state machine with whatever is currently in the keychain — but only
/// when doing so won't destroy locally-originated in-flight work.
///
/// Decision matrix (auth state vs. remote-vs-local credentials):
/// - quiescent (.signedIn / .signedOut / .error / .configured / .notConfigured)
/// - differ: reconfigure (sibling wrote — pick it up)
/// - match: no-op
/// - .signingIn
/// - remote has user-pool tokens: reconfigure (adopt sibling's sign-in;
/// confirmSignIn will resolve to .done)
/// - remote has no user-pool tokens: defer (sign-in flow finishes;
/// last-writer-wins on the keychain)
/// - .signingOut / .deletingUser / .federatingToIdentityPool /
/// .clearingFederation: defer (in-flight side effects must run; same end
/// state is reached by the local flow)
func reconcileWithSharedKeychainIfNeeded() async {
guard let keychainCredentials = await fetchCredentialsFromKeychain() else {
return
}
guard case .configured(let authNState, let authZState, _) = await authStateMachine.currentState else {
return
}
let stateMachineCredentials = fetchCredentialsFromStateMachine(authZState)
if let stateMachineCredentials, stateMachineCredentials == keychainCredentials {
return
}

if shouldDeferReconcile(authNState: authNState, remote: keychainCredentials) {
log.verbose("Deferring keychain reconcile while auth flow is in progress")
return
}

log.verbose("Reconfiguring auth state machine for keychain sharing")
let event = AuthEvent(eventType: .reconfigure(configuration))
await authStateMachine.send(event)
await taskHelper.didStateMachineConfigured()
}

private func fetchCredentialsFromKeychain() async -> AmplifyCredentials? {
do {
let data = try await credentialsClient?.fetchData(type: .amplifyCredentials)
if case .amplifyCredentials(let credentials) = data {
return credentials
}
return nil
} catch KeychainStoreError.itemNotFound {
return .noCredentials
} catch {
log.verbose("Could not read shared keychain credentials: \(error)")
return nil
}
}

/// Best-effort snapshot of the credentials the local state machine last
/// observed. Returns nil for transient authZ states where we can't make a
/// reliable comparison; in those cases callers fall back to deferring (the
/// transient state will resolve shortly and a later fetch will reconcile).
private func fetchCredentialsFromStateMachine(_ authZState: AuthorizationState) -> AmplifyCredentials? {
switch authZState {
case .sessionEstablished(let credentials),
.storingCredentials(let credentials):
return credentials
case .refreshingSession(existingCredentials: let credentials, _):
return credentials
case .federatingToIdentityPool(_, _, existingCredentials: let credentials):
return credentials
case .signingOut(let credentials):
return credentials ?? .noCredentials
case .configured:
return .noCredentials
case .notConfigured,
.clearingFederation,
.fetchingUnAuthSession,
.fetchingAuthSessionWithUserPool,
.deletingUser,
.error:
return nil
}
}

private func shouldDeferReconcile(
authNState: AuthenticationState,
remote: AmplifyCredentials
) -> Bool {
switch authNState {
case .signingIn:
// Adopt sibling sign-in; otherwise defer until local flow completes
return !remote.hasUserPoolTokens
case .signingOut,
.deletingUser,
.federatingToIdentityPool,
.clearingFederation:
return true
case .notConfigured,
.configured,
.signedIn,
.signedOut,
.federatedToIdentityPool,
.error:
return false
}
}

}
Loading
Loading