aws-amplify / amplify-swift

A declarative library for application development using cloud services.
Apache License 2.0
443 stars 192 forks source link

Intermittent Session Dumping from Keychain on Unexpected Error in Amplify Auth #3540

Open ostanik opened 6 months ago

ostanik commented 6 months ago

Describe the bug

We are encountering issues with Amplify's session management where several users experience their sessions being removed from the keychain following an unexpected error. This results in a 'signedOut' state error when attempting to fetch the auth session from Amplify. The problem does not occur consistently but has been observed under certain conditions.

Steps To Reproduce

(Note: Since the issue does not consistently occur, list the conditions or actions that preceded the occurrence if any patterns were noticed.)

1 - Perform operations that require authentication.
2 - Observe the application's behavior and logs for any unexpected errors leading to session dumping.
3 - Attempt to fetch the Cognito token from Amplify using the recommended API method.

Expected behavior

The auth session should be reliably fetched from the keychain without being inadvertently dumped, allowing for uninterrupted user authentication and session management.

Amplify Framework Version

2.26.4

Amplify Categories

Auth

Dependency manager

Swift PM

Swift version

5

CLI version

12.4.0

Xcode version

15.0.1

Relevant log output

<details>
<summary>Log Messages</summary>

Adding plugin: AWSCognitoAuthPlugin.AWSCognitoAuthPlugin)
Configuring
Configuration: Optional(Amplify.AmplifyConfiguration(analytics: nil, api: nil, auth: Optional(Amplify.AuthCategoryConfiguration(plugins: ["awsCognitoAuthPlugin": Amplify.JSONValue.object(["Auth": Amplify.JSONValue.object(["Default": Amplify.JSONValue.object(["authenticationFlowType": Amplify.JSONValue.string("CUSTOM_AUTH_WITHOUT_SRP")])]), "IdentityManager": Amplify.JSONValue.object(["Default": Amplify.JSONValue.array([])]), "CognitoUserPool": Amplify.JSONValue.object(["Default": Amplify.JSONValue.object(["AppClientId": Amplify.JSONValue.string("g15tgsjnc501c72gibm7qpkr0"), "PoolId": Amplify.JSONValue.string("eu-west-1_Q1M8Rca52"), "Region": Amplify.JSONValue.string("eu-west-1")])])])])), dataStore: nil, geo: nil, hub: nil, logging: nil, notifications: nil, predictions: nil, storage: nil))
Could not find Cognito Identity Pool configuration
Auth state change:

{
    "AuthState.notConfigured" =     {
    };
}
Credential Store state change:

{
    "CredentialStoreState.notConfigured" =     {
    };
}
Auth state change:

{
    "AuthState.configuringAuth" =     {
    };
}
Starting execution for Auth.fetchSessionAPI
Starting execution
Check if authstate configured
AWSCognitoAuthPlugin/InitializeAuthConfiguration.swift Starting execution
Credential Store state change:

{
    "CredentialStoreState.migratingLegacyStore" =     {
    };
}
AWSCognitoAuthPlugin/MigrateLegacyCredentialStore.swift Starting execution
AWSCognitoAuthPlugin/MigrateLegacyCredentialStore.swift Sending event CredentialStoreEvent.loadCredentialStore
Credential Store state change:

{
    "CredentialStoreState.loadingStoredCredentials" =     {
    };
}
AWSCognitoAuthPlugin/LoadCredentialStore.swift Starting execution
AWSCognitoAuthPlugin/LoadCredentialStore.swift Retreiving credential amplifyCredentials
AWSCognitoAuthPlugin/LoadCredentialStore.swift Sending event CredentialStoreEvent.throwError
CoreData: debug: PostSaveMaintenance: incremental_vacuum with freelist_count - 18 and pages_to_free 3
No existing session found.
AWSCognitoAuthPlugin/IdleCredentialStore.swift Starting execution
AWSCognitoAuthPlugin/IdleCredentialStore.swift Sending event CredentialStoreEvent.moveToIdleState
AWSCognitoAuthPlugin/InitializeAuthConfiguration.swift Sending event AuthEvent.validateCredentialAndConfiguration
Auth state change:

{
    "AuthState.validatingCredentialsAndConfiguration" =     {
    };
}
Credential Store state change:

{
    "CredentialStoreState.error" =     {
        errorType = "KeychainStoreError: Unable to find the keychain item";
    };
}
Credential Store state change:

{
    "CredentialStoreState.idle" =     {
    };
}
AWSCognitoAuthPlugin/ValidateCredentialsAndConfiguration.swift Starting execution
AWSCognitoAuthPlugin/ValidateCredentialsAndConfiguration.swift Sending event AuthEvent.configureAuthentication
Auth state change:

{
    "AuthState.configuringAuthentication" =     {
        "AuthenticationState.notConfigured" =         {
        };
    };
}
AWSCognitoAuthPlugin/InitializeAuthenticationConfiguration.swift Starting execution
AWSCognitoAuthPlugin/InitializeAuthenticationConfiguration.swift Sending event AuthenticationEvent.configure
Auth state change:

{
    "AuthState.configuringAuthentication" =     {
        "AuthenticationState.configured" =         {
        };
    };
}
AWSCognitoAuthPlugin/ConfigureAuthentication.swift Start execution
AWSCognitoAuthPlugin/ConfigureAuthentication.swift Sending event AuthenticationEvent.initializedSignedOut
Auth state change:

{
    "AuthState.configuringAuthentication" =     {
        "AuthenticationState.signedOut" =         {
            lastKnownUserName = "(nil)";
        };
    };
}
AWSCognitoAuthPlugin/ConfigureAuthentication.swift Sending event AuthEvent.authenticationConfigured
AWSCognitoAuthPlugin/InitializeAuthorizationConfiguration.swift Starting execution
AWSCognitoAuthPlugin/InitializeAuthorizationConfiguration.swift Sending event AuthorizationEvent.configure
Auth state change:

{
    "AuthState.configuringAuthorization" =     {
        "AuthenticationState.signedOut" =         {
            lastKnownUserName = "(nil)";
        };
        "AuthorizationState.notConfigured" =         {
        };
    };
}
Auth state change:

{
    "AuthState.configuringAuthorization" =     {
        "AuthenticationState.signedOut" =         {
            lastKnownUserName = "(nil)";
        };
        "AuthorizationState.configured" =         {
        };
    };
}
AWSCognitoAuthPlugin/ConfigureAuthorization.swift Starting execution
AWSCognitoAuthPlugin/ConfigureAuthorization.swift Sending event AuthEvent.authorizationConfigured
Auth state change:

{
    "AuthState.configured" =     {
        "AuthenticationState.signedOut" =         {
            lastKnownUserName = "(nil)";
        };
        "AuthorizationState.configured" =         {
        };
    };
}
Auth state configured
Fetching current state
No session found, fetching unauth session
Waiting for session to establish
Auth state change:

{
    "AuthState.configured" =     {
        "AuthenticationState.signedOut" =         {
            lastKnownUserName = "(nil)";
        };
        "AuthorizationState.fetchingUnAuthSession" =         {
            "FetchSessionState.notStarted" =             {
            };
        };
    };
}
AWSCognitoAuthPlugin/InitializeFetchUnAuthSession.swift Starting execution
AWSCognitoAuthPlugin/InitializeFetchUnAuthSession.swift Sending event FetchAuthSessionEvent.throwError
AWSCognitoAuthPlugin/InformSessionError.swift Starting execution
AWSCognitoAuthPlugin/InformSessionError.swift Sending event AuthorizationEvent.receivedSessionError
Auth state change:

{
    "AuthState.configured" =     {
        "AuthenticationState.signedOut" =         {
            lastKnownUserName = "(nil)";
        };
        "AuthorizationState.fetchingUnAuthSession" =         {
            "FetchSessionState.error" =             {
                error = "AWSCognitoAuthPlugin.FetchSessionError.noIdentityPool";
            };
        };
    };
}
Received error - sessionError(AWSCognitoAuthPlugin.FetchSessionError.noIdentityPool, noCredentials)
Auth state change:

{
    "AuthState.configured" =     {
        "AuthenticationState.signedOut" =         {
            lastKnownUserName = "(nil)";
        };
        "AuthorizationState.error" =         {
            Error = "AWSCognitoAuthPlugin.AuthorizationError.sessionError(AWSCognitoAuthPlugin.FetchSessionError.noIdentityPool, noCredentials)";
        };
    };
}
Successfully completed execution for Auth.fetchSessionAPI with result:
{
    awsCredentialsError = "AuthError: Unexpected error occurred with message: Unknown error occurred\nRecovery suggestion: This should not happen. There is a possibility that there is a bug if this error persists. Please take a look at https://github.com/aws-amplify/amplify-swift/issues to see if there are any existing issues that match your scenario, and file an issue with the details of the bug if there isn't.";
    cognitoTokensError = "AuthError: Unexpected error occurred with message: Unknown error occurred\nRecovery suggestion: This should not happen. There is a possibility that there is a bug if this error persists. Please take a look at https://github.com/aws-amplify/amplify-swift/issues to see if there are any existing issues that match your scenario, and file an issue with the details of the bug if there isn't.";
    identityIdError = "AuthError: Unexpected error occurred with message: Unknown error occurred\nRecovery suggestion: This should not happen. There is a possibility that there is a bug if this error persists. Please take a look at https://github.com/aws-amplify/amplify-swift/issues to see if there are any existing issues that match your scenario, and file an issue with the details of the bug if there isn't.";
    isSignedIn = false;
    userSubError = "AuthError: Unexpected error occurred with message: Unknown error occurred\nRecovery suggestion: This should not happen. There is a possibility that there is a bug if this error persists. Please take a look at https://github.com/aws-amplify/amplify-swift/issues to see if there are any existing issues that match your scenario, and file an issue with the details of the bug if there isn't.";
}
Adding plugin: AWSCognitoAuthPlugin.AWSCognitoAuthPlugin)
Starting execution for Auth.fetchSessionAPI
Starting execution
Check if authstate configured
Auth state configured
Fetching current state
Waiting for session to establish
Auth state change:

{
    "AuthState.configured" =     {
        "AuthenticationState.signedOut" =         {
            lastKnownUserName = "(nil)";
        };
        "AuthorizationState.refreshingSession" =         {
            existing = noCredentials;
            refreshState =             {
                "RefreshSessionState.notStarted" =                 {
                };
            };
        };
    };
}
AWSCognitoAuthPlugin/InitializeRefreshSession.swift Starting execution
AWSCognitoAuthPlugin/InitializeRefreshSession.swift Sending event RefreshSessionEvent.throwError
AWSCognitoAuthPlugin/InformSessionError.swift Starting execution
AWSCognitoAuthPlugin/InformSessionError.swift Sending event AuthorizationEvent.receivedSessionError
Auth state change:

{
    "AuthState.configured" =     {
        "AuthenticationState.signedOut" =         {
            lastKnownUserName = "(nil)";
        };
        "AuthorizationState.refreshingSession" =         {
            existing = noCredentials;
            refreshState =             {
                "RefreshSessionState.error" =                 {
                    error = "AWSCognitoAuthPlugin.FetchSessionError.noCredentialsToRefresh";
                };
            };
        };
    };
}
Received error - sessionError(AWSCognitoAuthPlugin.FetchSessionError.noCredentialsToRefresh, noCredentials)
Auth state change:

{
    "AuthState.configured" =     {
        "AuthenticationState.signedOut" =         {
            lastKnownUserName = "(nil)";
        };
        "AuthorizationState.error" =         {
            Error = "AWSCognitoAuthPlugin.AuthorizationError.sessionError(AWSCognitoAuthPlugin.FetchSessionError.noCredentialsToRefresh, noCredentials)";
        };
    };
}
Successfully completed execution for Auth.fetchSessionAPI with result:
{
    awsCredentialsError = "AuthError: There is no user signed in to retreive AWS credentials\nRecovery suggestion: Call Auth.signIn to sign in a user or enable unauthenticated access in AWS Cognito Identity Pool\nCaused by:\ninvalidAccountTypeException";
    cognitoTokensError = "AuthError: There is no user signed in to retreive cognito tokens\nRecovery suggestion: Call Auth.signIn to sign in a user and then call Auth.fetchSession";
    identityIdError = "AuthError: There is no user signed in to retreive identity id\nRecovery suggestion: Call Auth.signIn to sign in a user or enable unauthenticated access in AWS Cognito Identity Pool\nCaused by:\ninvalidAccountTypeException";
    isSignedIn = false;
    userSubError = "AuthError: There is no user signed in to retreive user sub\nRecovery suggestion: Call Auth.signIn to sign in a user and then call Auth.fetchSession";
}
Adding plugin: AWSCognitoAuthPlugin.AWSCognitoAuthPlugin)
Starting execution for Auth.fetchSessionAPI
Starting execution
Check if authstate configured
Auth state configured
Fetching current state
Waiting for session to establish
AWSCognitoAuthPlugin/InitializeRefreshSession.swift Starting execution
Auth state change:

{
    "AuthState.configured" =     {
        "AuthenticationState.signedOut" =         {
            lastKnownUserName = "(nil)";
        };
        "AuthorizationState.refreshingSession" =         {
            existing = noCredentials;
            refreshState =             {
                "RefreshSessionState.notStarted" =                 {
                };
            };
        };
    };
}
AWSCognitoAuthPlugin/InitializeRefreshSession.swift Sending event RefreshSessionEvent.throwError
AWSCognitoAuthPlugin/InformSessionError.swift Starting execution
AWSCognitoAuthPlugin/InformSessionError.swift Sending event AuthorizationEvent.receivedSessionError
Received error - sessionError(AWSCognitoAuthPlugin.FetchSessionError.noCredentialsToRefresh, noCredentials)
Auth state change:

{
    "AuthState.configured" =     {
        "AuthenticationState.signedOut" =         {
            lastKnownUserName = "(nil)";
        };
        "AuthorizationState.refreshingSession" =         {
            existing = noCredentials;
            refreshState =             {
                "RefreshSessionState.error" =                 {
                    error = "AWSCognitoAuthPlugin.FetchSessionError.noCredentialsToRefresh";
                };
            };
        };
    };
}
Successfully completed execution for Auth.fetchSessionAPI with result:
{
    awsCredentialsError = "AuthError: There is no user signed in to retreive AWS credentials\nRecovery suggestion: Call Auth.signIn to sign in a user or enable unauthenticated access in AWS Cognito Identity Pool\nCaused by:\ninvalidAccountTypeException";
    cognitoTokensError = "AuthError: There is no user signed in to retreive cognito tokens\nRecovery suggestion: Call Auth.signIn to sign in a user and then call Auth.fetchSession";
    identityIdError = "AuthError: There is no user signed in to retreive identity id\nRecovery suggestion: Call Auth.signIn to sign in a user or enable unauthenticated access in AWS Cognito Identity Pool\nCaused by:\ninvalidAccountTypeException";
    isSignedIn = false;
    userSubError = "AuthError: There is no user signed in to retreive user sub\nRecovery suggestion: Call Auth.signIn to sign in a user and then call Auth.fetchSession";
}
Auth state change:

{
    "AuthState.configured" =     {
        "AuthenticationState.signedOut" =         {
            lastKnownUserName = "(nil)";
        };
        "AuthorizationState.error" =         {
            Error = "AWSCognitoAuthPlugin.AuthorizationError.sessionError(AWSCognitoAuthPlugin.FetchSessionError.noCredentialsToRefresh, noCredentials)";
        };
    };
}
Adding plugin: AWSCognitoAuthPlugin.AWSCognitoAuthPlugin)
Adding plugin: AWSCognitoAuthPlugin.AWSCognitoAuthPlugin)
Starting execution for Auth.fetchSessionAPI
Starting execution
Check if authstate configured
Auth state configured
Fetching current state
Waiting for session to establish
AWSCognitoAuthPlugin/InitializeRefreshSession.swift Starting execution
AWSCognitoAuthPlugin/InitializeRefreshSession.swift Sending event RefreshSessionEvent.throwError
Auth state change:

{
    "AuthState.configured" =     {
        "AuthenticationState.signedOut" =         {
            lastKnownUserName = "(nil)";
        };
        "AuthorizationState.refreshingSession" =         {
            existing = noCredentials;
            refreshState =             {
                "RefreshSessionState.notStarted" =                 {
                };
            };
        };
    };
}
AWSCognitoAuthPlugin/InformSessionError.swift Starting execution
AWSCognitoAuthPlugin/InformSessionError.swift Sending event AuthorizationEvent.receivedSessionError
Auth state change:

{
    "AuthState.configured" =     {
        "AuthenticationState.signedOut" =         {
            lastKnownUserName = "(nil)";
        };
        "AuthorizationState.refreshingSession" =         {
            existing = noCredentials;
            refreshState =             {
                "RefreshSessionState.error" =                 {
                    error = "AWSCognitoAuthPlugin.FetchSessionError.noCredentialsToRefresh";
                };
            };
        };
    };
}
Received error - sessionError(AWSCognitoAuthPlugin.FetchSessionError.noCredentialsToRefresh, noCredentials)
Auth state change:

{
    "AuthState.configured" =     {
        "AuthenticationState.signedOut" =         {
            lastKnownUserName = "(nil)";
        };
        "AuthorizationState.error" =         {
            Error = "AWSCognitoAuthPlugin.AuthorizationError.sessionError(AWSCognitoAuthPlugin.FetchSessionError.noCredentialsToRefresh, noCredentials)";
        };
    };
}
Successfully completed execution for Auth.fetchSessionAPI with result:
{
    awsCredentialsError = "AuthError: There is no user signed in to retreive AWS credentials\nRecovery suggestion: Call Auth.signIn to sign in a user or enable unauthenticated access in AWS Cognito Identity Pool\nCaused by:\ninvalidAccountTypeException";
    cognitoTokensError = "AuthError: There is no user signed in to retreive cognito tokens\nRecovery suggestion: Call Auth.signIn to sign in a user and then call Auth.fetchSession";
    identityIdError = "AuthError: There is no user signed in to retreive identity id\nRecovery suggestion: Call Auth.signIn to sign in a user or enable unauthenticated access in AWS Cognito Identity Pool\nCaused by:\ninvalidAccountTypeException";
    isSignedIn = false;
    userSubError = "AuthError: There is no user signed in to retreive user sub\nRecovery suggestion: Call Auth.signIn to sign in a user and then call Auth.fetchSession";
}


### Is this a regression?

Yes

### Regression additional context

_No response_

### Platforms

iOS

### OS Version

iOS 17.1.1

### Device

iPhone 17 - Simulator

### Specific to simulators

_No response_

### Additional context

At every single request that we make to our BE API we do run the following code to retrieve the Cognito id token:

```swift
let session = try await Amplify.Auth.fetchAuthSession()
if let cognitoTokenProvider = session as? AuthCognitoTokensProvider {
    let tokens = try cognitoTokenProvider.getCognitoTokens().get()
    promise(.success((tokens.idToken)))
}

Flow:

In some instances, following an unexpected error, the session gets removed from the keychain, and attempting to fetch the auth session results in a 'signedOut' state error.

harsh62 commented 6 months ago

@ostanik Thanks for opening up the issue. From the logs that you have shared, it don't see anything that should cause session being dumped. What I have observed from the logs is that a logged out user is trying to make fetchAuthSession requests. I don't see session being dumped.

Furthermore, Amplify never logs out the user by itself, even if the session has expired. The only occasion session is removed from keychain, is when Amplify determines that there was a change in user pool configuration.

To further investigate, I would need more concrete details of session dump that was observed. Either exact flow of the Amplify API calls or verbose logs when the issue occurred.

Lastly, the code snippet that you shared should not cause any issues, when retrieving the session any amount of times.

ostanik commented 6 months ago

@harsh62 , Thank you for your feedback. I understand there's no clear indication of session dumping from the logs. However, we're observing sessions disappearing unexpectedly, with users being treated as logged out without any action on their part or an explicit logout call.

Clarifications:

We're also encountering unexpected session expirations, which will be addressed separately.

Given these challenges, any further insights or advice would be greatly appreciated as we aim to resolve this perplexing issue.

harsh62 commented 6 months ago

Thanks for providing more context. Few more questions based on your answers:

find themselves without session data in the keychain, prompting a fetchAuthSession call, which then appears as if a logged-out user is trying to fetch a session

Authentication Flow: Our flow involves

Would you be able to provide more code snippets and surrounding code that could impact how Amplify works?

The new information you provide, could may be help me with a direction to investigate the issue you are seeing.

ostanik commented 6 months ago

Code Snippet for Auth handling:

nit(_ env: ConfigurationEnvironment) {
        Amplify.Logging.logLevel = env == .staging ? .verbose : .none
        // This is used only to capture traces of auth events
        unsubscribeToken = Amplify.Hub.listen(to: .auth) { payload in
            AppLogger.shared.debug("Received \(payload.eventName) event from Amplify: \nData: \(String(describing: payload.data))")
        }
    }

    deinit {
        Amplify.Hub.removeListener(unsubscribeToken)
    }

    /// Start the passwordless authentication process
    /// - Parameters:
    ///  - email: The email address to send the magic link to
    ///  - authType: The type of authentication to perform
    ///  > Note: This function will retry at most one time if the first attempt fails with a `invalidState` error.
    ///  This retry is required in order to "request a new link" feature works properly
    func startPasswordlessAuth(email: String, authType: PasswordlessAuthType) -> Future<Void, LoginFeature.RequestLinkError> {
        AppLogger.shared.debug("Passwordless Auth started")
        return Future { promise in
            Task { [weak self] in
                var hasRetried = false // Flag to track whether a retry has been attempted

                func attemptRequest() async {
                    do {
                        guard let self = self else {
                            AppLogger.shared.debug("Start Passwordless Auth: self is nil")
                            throw LoginFeature.RequestLinkError.authServiceUnavailable
                        }

                        try self.configureAmplifyIfNeeded()

                        // Forcing email to be lower because the user email was configured to be case sensitive
                        // and BE is lowering all the email addresses (we hope).
                        _ = try await self.signin(email: email.lowercased())
                        AppLogger.shared.debug("Passwordless Auth requested")
                        promise(.success(()))
                    } catch let error as AuthError {
                        if case .invalidState = error, !hasRetried {
                            AppLogger.shared.debug("Invalid state error occurred, attempting retry")
                            hasRetried = true
                            await Amplify.Auth.signOut()
                            await attemptRequest() // Retry request after sign out
                        } else {
                            AppLogger.shared.error("Start Passwordless Auth: \(error.debugDescription)", error: error)
                            promise(.failure(.other))
                        }
                    } catch {
                        AppLogger.shared.error("Start Passwordless Auth: \(error.localizedDescription)", error: error)
                        promise(.failure(.other))
                    }
                }

                await attemptRequest() // Initial call to the request function
            }
        }
    }

    func login(code: String) -> Future<Void, Error> {
        AppLogger.shared.debug("Claim login code started")
        return Future { promise in
            Task { [weak self] in
                guard let self else {
                    AppLogger.shared.debug("Claim login code: self is nil")
                    return promise(.failure(RequestLinkError.authServiceUnavailable))
                }

                do {
                    try self.configureAmplifyIfNeeded()

                    let signInResult = try await Amplify.Auth.confirmSignIn(challengeResponse: code)
                    AppLogger.shared.debug("Confirm sign in succeeded. Next step: \(signInResult.nextStep)")

                    // Fetch session right after login to avoid concurrency issues.
                    // this is a defensive way of working...Should we remove it?
                    let session = try await Amplify.Auth.fetchAuthSession()
                    guard let cognitoTokenProvider = session as? AuthCognitoTokensProvider else {
                        throw RequestLinkError.other
                    }
                    _ = try cognitoTokenProvider.getCognitoTokens().get()
                    AppLogger.shared.debug("Claim login code success")
                    promise(.success(()))
                } catch let error as AuthError {
                    AppLogger.shared.error("Claim login code: \(error.debugDescription)", error: error)
                    promise(.failure(error))
                } catch {
                    AppLogger.shared.error("Claim login code: \(error.localizedDescription)", error: error)
                    promise(.failure(error))
                }
            }
        }
    }

    func obtainRenewedCredentialsIfNeeded() async -> Bool {
        do {
            try self.configureAmplifyIfNeeded()
            _ = try await fetchCredentials().eraseToAnyPublisher().async()
            AppLogger.shared.debug("Obtain renewed Credentials if needed: success - Has Valid Credentials: \(hasValidCredentials)")
            return hasValidCredentials
        } catch let error as AuthError {
            AppLogger.shared.error("Obtain renewed Credentials if needed: \(error.debugDescription)", error: error)
        } catch {
            AppLogger.shared.error("Obtain renewed Credentials if needed: \(error.localizedDescription)", error: error)
        }
        return false
    }

    func fetchCredentials() -> Future<String, Error> {
        self.printKeychainItems()
        AppLogger.shared.debug("Fetch Credentials started")
        return Future { promise in
            Task { [weak self] in
                guard let self else {
                    AppLogger.shared.debug("Fetch Credentials: self is nil")
                    return promise(.failure(RequestLinkError.authServiceUnavailable))
                }

                do {
                    try self.configureAmplifyIfNeeded()

                    let session = try await Amplify.Auth.fetchAuthSession()
                    if let cognitoTokenProvider = session as? AuthCognitoTokensProvider {
                        let tokens = try cognitoTokenProvider.getCognitoTokens().get()
                        AppLogger.shared.debug("Fetch Credentials success")
                        self.hasValidCredentials = true
                        promise(.success((tokens.idToken)))
                    } else {
                        throw RequestLinkError.other
                    }
                } catch let error as AuthError {
                    AppLogger.shared.error("Fetch Credentials failed: \(error.debugDescription)", error: error)
                    switch error {
                    case .sessionExpired, .signedOut:
                        AppLogger.shared.debug("Session expired or user signed out, changing valid credentials to false")
                        self.hasValidCredentials = false
                    default: break
                    }
                    promise(.failure(error))
                } catch let error as ConfigurationError {
                    AppLogger.shared.error("Fetch credentials failed after retry: \(error.debugDescription)", error: error)
                    promise(.failure(error))
                } catch {
                    AppLogger.shared.error("Fetch Credentials failed: \(error.localizedDescription)", error: error)
                    promise(.failure(error))
                }
            }
        }
    }

    // Change this to be async
    func clearCredentials() {
        Task { [weak self] in
            guard let self else {
                AppLogger.shared.debug("Clear Credentials: self is nil")
                return
            }

            AppLogger.shared.debug("Cleaning credentials")
            do {
                // Performing sign out without configure the SDK will cause a crash.
                try self.configureAmplifyIfNeeded()
                _ = await Amplify.Auth.signOut()
                self.hasValidCredentials = false
            } catch let error as ConfigurationError {
                AppLogger.shared.error("Sign out error \(error.debugDescription)", error: error)
            } catch {
                AppLogger.shared.error("Sign out error \(error.localizedDescription)", error: error)
            }
        }
    }

    /// This method is used to configure Amplify with the Cognito plugin.
    /// Amplify configuration should only be called once otherwise it will thrown an error "amplifyAlreaadyConfigured"
    /// But on the catching of this function we are handling this error to return and not rethrows. So it should be safe to call this function multiple times.
    private func configureAmplifyIfNeeded() throws {
        do {
            // Creating plugin network preferences to retry 3 times before fail
            let cognitoNetworkPreferences = AWSCognitoNetworkPreferences(
                maxRetryCount: 3,
                timeoutIntervalForRequest: .seconds(60),
                timeoutIntervalForResource: .days(7)
            )

            try Amplify.add(plugin: AWSCognitoAuthPlugin(networkPreferences: cognitoNetworkPreferences))

            let userPoolId = BuildSetting(type: .cognitoUserPoolID).value
            let clientId = BuildSetting(type: .cognitoClientID).value
            let region = BuildSetting(type: .cognitoRegion).value

            let configuration = AmplifyConfiguration(
                auth: AuthCategoryConfiguration(
                    plugins: [
                        "awsCognitoAuthPlugin": [
                            "IdentityManager": [
                                "Default": []
                            ],
                            "CognitoUserPool": [
                                "Default": [
                                    "PoolId": .string(userPoolId),
                                    "Region": .string(region),
                                    "AppClientId": .string(clientId)
                                ]
                            ],
                            "Auth": [
                                "Default": [
                                    "authenticationFlowType": "CUSTOM_AUTH_WITHOUT_SRP"
                                ]
                            ]
                        ]
                    ]
                )
            )

            try Amplify.configure(configuration)
            AppLogger.shared.debug("Amplify is configured")
        } catch let error as ConfigurationError {
            if case .amplifyAlreadyConfigured = error {
                AppLogger.shared.debug("Amplify configuration: amplify already configured skipping")
                return
            }
            // Can we only use the configure amplify method once we are not throwing if already configured?
            AppLogger.shared.error("Amplify configuration: \(error.debugDescription)", error: error)
            throw error
        } catch {
            AppLogger.shared.error("Amplify configuration: \(error.localizedDescription)", error: error)
            throw error
        }
    }

    private func signin(email: String) async throws -> AuthSignInResult {
        let customWithoutSRP = AWSAuthSignInOptions(authFlowType: .customWithoutSRP)
        let options = AuthSignInRequest.Options(pluginOptions: customWithoutSRP)
        return try await Amplify.Auth.signIn(username: email, options: options)
    }

If there are specific areas of this code or the authentication flow you'd like more details on, or if there are specific concerns, I'd be happy to provide further explanations or adjust our approach based on your guidance.

ostanik commented 6 months ago

I don't know if this helps but here is one scenario from a real user who was authenticated before but right after an app update, got the error:

# Crashlytics - Custom logs
# Platform: apple
# Version: 1.105.0 (6648)
# Date: Thu Feb 29 2024 09:03:58 GMT+0000 (Western European Standard Time)

 0 | Thu Feb 29 2024 09:03:56 GMT+0000 (Western European Standard Time) | 2024/02/29 09:03:56:684  {REDACT}:35 - Updated user in the local data base
 1 | Thu Feb 29 2024 09:03:57 GMT+0000 (Western European Standard Time) | 2024/02/29 09:03:57:294  {REDACT}:64 - Starting refresh identity flow
 2 | Thu Feb 29 2024 09:03:57 GMT+0000 (Western European Standard Time) | 2024/02/29 09:03:57:306  CognitoServiceProd:133 - Fetch Credentials started
 3 | Thu Feb 29 2024 09:03:57 GMT+0000 (Western European Standard Time) | 2024/02/29 09:03:57:373  CognitoServiceProd:238 - Amplify is configured
 4 | Thu Feb 29 2024 09:03:57 GMT+0000 (Western European Standard Time) | 2024/02/29 09:03:57:374  CognitoServiceProd:28 - Received Amplify.configured event from Amplify:
Data: nil
 5 | Thu Feb 29 2024 09:03:57 GMT+0000 (Western European Standard Time) | 2024/02/29 09:03:57:461  CognitoServiceProd:28 - Received Auth.fetchSessionAPI event from Amplify:
Data: Optional(Swift.Result<Amplify.AuthSession, Amplify.AuthError>.success({
awsCredentialsError = "AuthError: Unexpected error occurred with message: Unknown error occurred\nRecovery suggestion: This should not happen. There is a possibility that there is a bug if this error persists. Please take a look at https://github.com/aws-amplify/amplify-swift/issues to see if there are any existing issues that match your scenario, and file an issue with the details of the bug if there isn't.";
cognitoTokensError = "AuthError: Unexpected error occurred with message: Unknown error occurred\nRecovery suggestion: This should not happen. There is a possibility that there is a bug if this error persists. Please take a look at https://github.com/aws-amplify/amplify-swift/issues to see if there are any existing issues that match your scenario, and file an issue with the details of the bug if there isn't.";
identityIdError = "AuthError: Unexpected error occurred with message: Unknown error occurred\nRecovery suggestion: This should not happen. There is a possibility that there is a bug if this error persists. Please take a look at https://github.com/aws-amplify/amplify-swift/issues to see if there are any existing issues that match your scenario, and file an issue with the details of the bug if there isn't.";
isSignedIn = false;
userSubError = "AuthError: Unexpected error occurred with message: Unknown error occurred\nRecovery suggestion: This should not happen. There is a possibility that there is a bug if this error persists. Please take a look at https://github.com/aws-amplify/amplify-swift/issues to see if there are any existing issues that match your scenario, and file an issue with the details of the bug if there isn't.";
}))
 6 | Thu Feb 29 2024 09:03:57 GMT+0000 (Western European Standard Time) | 2024/02/29 09:03:57:466  CognitoServiceProd:154 - Fetch Credentials failed: unkown error - description: Unknown error occurred: The operation couldn’t be completed. (Amplify.AuthError error 2.)
 7 | Thu Feb 29 2024 09:03:57 GMT+0000 (Western European Standard Time) | 2024/02/29 09:03:57:804  CognitoServiceProd:133 - Fetch Credentials started
 8 | Thu Feb 29 2024 09:03:57 GMT+0000 (Western European Standard Time) | 2024/02/29 09:03:57:806  CognitoServiceProd:241 - Amplify configuration: amplify already configured skipping
 9 | Thu Feb 29 2024 09:03:57 GMT+0000 (Western European Standard Time) | 2024/02/29 09:03:57:811  CognitoServiceProd:154 - Fetch Credentials failed: signedOut error - description: There is no user signed in to retreive cognito tokens, recoverySuggestion: Call Auth.signIn to sign in a user and then call Auth.fetchSession: The operation couldn’t be completed. (Amplify.AuthError error 6.)
10 | Thu Feb 29 2024 09:03:57 GMT+0000 (Western European Standard Time) | 2024/02/29 09:03:57:811  CognitoServiceProd:28 - Received Auth.fetchSessionAPI event from Amplify:
Data: Optional(Swift.Result<Amplify.AuthSession, Amplify.AuthError>.success({
awsCredentialsError = "AuthError: There is no user signed in to retreive AWS credentials\nRecovery suggestion: Call Auth.signIn to sign in a user or enable unauthenticated access in AWS Cognito Identity Pool\nCaused by:\ninvalidAccountTypeException";
cognitoTokensError = "AuthError: There is no user signed in to retreive cognito tokens\nRecovery suggestion: Call Auth.signIn to sign in a user and then call Auth.fetchSession";
identityIdError = "AuthError: There is no user signed in to retreive identity id\nRecovery suggestion: Call Auth.signIn to sign in a user or enable unauthenticated access in AWS Cognito Identity Pool\nCaused by:\ninvalidAccountTypeException";
isSignedIn = false;
userSubError = "AuthError: There is no user signed in to retreive user sub\nRecovery suggestion: Call Auth.signIn to sign in a user and then call Auth.fetchSession";
}))
11 | Thu Feb 29 2024 09:03:57 GMT+0000 (Western European Standard Time) | 2024/02/29 09:03:57:812  CognitoServiceProd:157 - Session expired or user signed out, changing valid credentials to false
12 | Thu Feb 29 2024 09:03:57 GMT+0000 (Western European Standard Time) | 2024/02/29 09:03:57:814  {REDACT}:102 - Get user info succeeded
13 | Thu Feb 29 2024 09:03:57 GMT+0000 (Western European Standard Time) | 2024/02/29 09:03:57:814  {REDACT}:139 - The persisted user has differet policy location as the fetched one
14 | Thu Feb 29 2024 09:03:57 GMT+0000 (Western European Standard Time) | 2024/02/29 09:03:57:816  {REDACT}:151 - Saving user: {REDACT}
15 | Thu Feb 29 2024 09:03:57 GMT+0000 (Western European Standard Time) | 2024/02/29 09:03:57:829  {REDACT}:35 - Updated user in the local data base
16 | Thu Feb 29 2024 09:03:58 GMT+0000 (Western European Standard Time) | 2024/02/29 09:03:58:095  {REDACT}:200 - Did receive value on refresh identity, starting initial flow
17 | Thu Feb 29 2024 09:03:58 GMT+0000 (Western European Standard Time) | 2024/02/29 09:03:58:099  {REDACT}:226 - User logged in, starting logged in flow
18 | Thu Feb 29 2024 09:03:58 GMT+0000 (Western European Standard Time) | 2024/02/29 09:03:58:100  {REDACT}:425 - User is allowed to access the app, continuing flow
19 | Thu Feb 29 2024 09:03:58 GMT+0000 (Western European Standard Time) | 2024/02/29 09:03:58:185  CognitoServiceProd:133 - Fetch Credentials started
20 | Thu Feb 29 2024 09:03:58 GMT+0000 (Western European Standard Time) | 2024/02/29 09:03:58:186  CognitoServiceProd:241 - Amplify configuration: amplify already configured skipping
21 | Thu Feb 29 2024 09:03:58 GMT+0000 (Western European Standard Time) | 2024/02/29 09:03:58:187  CognitoServiceProd:28 - Received Auth.fetchSessionAPI event from Amplify:
Data: Optional(Swift.Result<Amplify.AuthSession, Amplify.AuthError>.success({
awsCredentialsError = "AuthError: There is no user signed in to retreive AWS credentials\nRecovery suggestion: Call Auth.signIn to sign in a user or enable unauthenticated access in AWS Cognito Identity Pool\nCaused by:\ninvalidAccountTypeException";
cognitoTokensError = "AuthError: There is no user signed in to retreive cognito tokens\nRecovery suggestion: Call Auth.signIn to sign in a user and then call Auth.fetchSession";
identityIdError = "AuthError: There is no user signed in to retreive identity id\nRecovery suggestion: Call Auth.signIn to sign in a user or enable unauthenticated access in AWS Cognito Identity Pool\nCaused by:\ninvalidAccountTypeException";
isSignedIn = false;
userSubError = "AuthError: There is no user signed in to retreive user sub\nRecovery suggestion: Call Auth.signIn to sign in a user and then call Auth.fetchSession";
}))
22 | Thu Feb 29 2024 09:03:58 GMT+0000 (Western European Standard Time) | 2024/02/29 09:03:58:187  CognitoServiceProd:154 - Fetch Credentials failed: signedOut error - description: There is no user signed in to retreive cognito tokens, recoverySuggestion: Call Auth.signIn to sign in a user and then call Auth.fetchSession: The operation couldn’t be completed. (Amplify.AuthError error 6.)
23 | Thu Feb 29 2024 09:03:58 GMT+0000 (Western European Standard Time) | 2024/02/29 09:03:58:188  CognitoServiceProd:157 - Session expired or user signed out, changing valid credentials to false
24 | Thu Feb 29 2024 09:03:58 GMT+0000 (Western European Standard Time) | 2024/02/29 09:03:58:211  CognitoServiceProd:133 - Fetch Credentials started
25 | Thu Feb 29 2024 09:03:58 GMT+0000 (Western European Standard Time) | 2024/02/29 09:03:58:211  CognitoServiceProd:241 - Amplify configuration: amplify already configured skipping
26 | Thu Feb 29 2024 09:03:58 GMT+0000 (Western European Standard Time) | 2024/02/29 09:03:58:213  CognitoServiceProd:154 - Fetch Credentials failed: signedOut error - description: There is no user signed in to retreive cognito tokens, recoverySuggestion: Call Auth.signIn to sign in a user and then call Auth.fetchSession: The operation couldn’t be completed. (Amplify.AuthError error 6.)
27 | Thu Feb 29 2024 09:03:58 GMT+0000 (Western European Standard Time) | 2024/02/29 09:03:58:213  CognitoServiceProd:28 - Received Auth.fetchSessionAPI event from Amplify:
Data: Optional(Swift.Result<Amplify.AuthSession, Amplify.AuthError>.success({
awsCredentialsError = "AuthError: There is no user signed in to retreive AWS credentials\nRecovery suggestion: Call Auth.signIn to sign in a user or enable unauthenticated access in AWS Cognito Identity Pool\nCaused by:\ninvalidAccountTypeException";
cognitoTokensError = "AuthError: There is no user signed in to retreive cognito tokens\nRecovery suggestion: Call Auth.signIn to sign in a user and then call Auth.fetchSession";
identityIdError = "AuthError: There is no user signed in to retreive identity id\nRecovery suggestion: Call Auth.signIn to sign in a user or enable unauthenticated access in AWS Cognito Identity Pool\nCaused by:\ninvalidAccountTypeException";
isSignedIn = false;
userSubError = "AuthError: There is no user signed in to retreive user sub\nRecovery suggestion: Call Auth.signIn to sign in a user and then call Auth.fetchSession";
}))
28 | Thu Feb 29 2024 09:03:58 GMT+0000 (Western European Standard Time) | 2024/02/29 09:03:58:213  CognitoServiceProd:157 - Session expired or user signed out, changing valid credentials to false
29 | Thu Feb 29 2024 09:03:58 GMT+0000 (Western European Standard Time) | 2024/02/29 09:03:58:372  {REDACT}:196 - Refresh identity finished
30 | Thu Feb 29 2024 09:03:58 GMT+0000 (Western European Standard Time) | 2024/02/29 09:03:58:841  CognitoServiceProd:241 - Amplify configuration: amplify already configured skipping
31 | Thu Feb 29 2024 09:03:58 GMT+0000 (Western European Standard Time) | 2024/02/29 09:03:58:841  CognitoServiceProd:133 - Fetch Credentials started
32 | Thu Feb 29 2024 09:03:58 GMT+0000 (Western European Standard Time) | 2024/02/29 09:03:58:841  CognitoServiceProd:241 - Amplify configuration: amplify already configured skipping
33 | Thu Feb 29 2024 09:03:58 GMT+0000 (Western European Standard Time) | 2024/02/29 09:03:58:843  CognitoServiceProd:154 - Fetch Credentials failed: signedOut error - description: There is no user signed in to retreive cognito tokens, recoverySuggestion: Call Auth.signIn to sign in a user and then call Auth.fetchSession: The operation couldn’t be completed. (Amplify.AuthError error 6.)
34 | Thu Feb 29 2024 09:03:58 GMT+0000 (Western European Standard Time) | 2024/02/29 09:03:58:843  CognitoServiceProd:28 - Received Auth.fetchSessionAPI event from Amplify:
Data: Optional(Swift.Result<Amplify.AuthSession, Amplify.AuthError>.success({
awsCredentialsError = "AuthError: There is no user signed in to retreive AWS credentials\nRecovery suggestion: Call Auth.signIn to sign in a user or enable unauthenticated access in AWS Cognito Identity Pool\nCaused by:\ninvalidAccountTypeException";
cognitoTokensError = "AuthError: There is no user signed in to retreive cognito tokens\nRecovery suggestion: Call Auth.signIn to sign in a user and then call Auth.fetchSession";
identityIdError = "AuthError: There is no user signed in to retreive identity id\nRecovery suggestion: Call Auth.signIn to sign in a user or enable unauthenticated access in AWS Cognito Identity Pool\nCaused by:\ninvalidAccountTypeException";
isSignedIn = false;
userSubError = "AuthError: There is no user signed in to retreive user sub\nRecovery suggestion: Call Auth.signIn to sign in a user and then call Auth.fetchSession";
}))
35 | Thu Feb 29 2024 09:03:58 GMT+0000 (Western European Standard Time) | 2024/02/29 09:03:58:844  CognitoServiceProd:157 - Session expired or user signed out, changing valid credentials to false
36 | Thu Feb 29 2024 09:03:58 GMT+0000 (Western European Standard Time) | 2024/02/29 09:03:58:845  CognitoServiceProd:125 - Obtain renewed Credentials if needed: signedOut error - description: There is no user signed in to retreive cognito tokens, recoverySuggestion: Call Auth.signIn to sign in a user and then call Auth.fetchSession: The operation couldn’t be completed. (Amplify.AuthError error 6.)
37 | Thu Feb 29 2024 09:03:58 GMT+0000 (Western European Standard Time) | 2024/02/29 09:03:58:899  {REDACT}:125 - Starting reauthentication - {REDACT}
Shilaghae commented 6 months ago

@harsh62 does these log tell you anything? I am just wandering if you are able to spot any problem. Thanks for your help.

harsh62 commented 6 months ago

@Shilaghae @ostanik

I don't see anything in the logs other than that there no session. It would need to be verbose logs, which could help us in figuring out what is going on.

I also looked at your code, and one thing that popped out was configureAmplifyIfNeeded method. Ideally you would only need configure Amplify at app launch and then thats it. It would not be a best practice to reconfigure Amplify before each Amplify API call (Although I see that you are catching the error). So I would suggest to move your configuration to app launch and then use the API's without calling configure first.

The other thing that I see is that you are building the configuration directly in the code. While it is possible to build a configuration this way, the recommended way would be to use an amplifyconfiguration.json file to setup Amplify. Furthermore, are you using this way of setting up Amplify to manage different build environments in the same app?

Lastly, I would like to understand how clearCredentials() is being used within your app.

harsh62 commented 6 months ago

@ostanik Also, is this error only happening to users who update the app? If yes, was there a config change that happened between versions?

victorkifer commented 5 months ago

@harsh62 We added this lazy initialization cause we assumed Amplify.configure is making a network call, which might be failing. This assumption comes from the error in Crashlytics from the Android side where we saw this error

Screenshot 2024-03-13 at 09 02 53

In case Amplify.configure for iOS doesn't require network, I think it's fine to configure it at app launch.

victorkifer commented 5 months ago

We're building amplify configuration at runtime from build settings which depend on selected environment (staging/production).

victorkifer commented 5 months ago

clearCredentials() is called when user requests log out from the app or if user is not permitted to use the app anymore. None of these seem to be the reason of logouts we experience.

victorkifer commented 5 months ago

@ostanik Also, is this error only happening to users who update the app? If yes, was there a config change that happened between versions?

We cannot confirm this error only happening to users who update the app. We have some reports that this error happened after an update, but as far as we know this error happens randomly when user launch the app.

However, we can confirm there was no configuration change recently.

thisisabhash commented 5 months ago

Thank you - We will investigate and post followup questions here.

harsh62 commented 5 months ago

@victorkifer We are not able to reproduce this issue at our end. Would you be able to assist with any additional details that could help us further investigate the issue?

ostanik commented 5 months ago

Hey @harsh62, Answering your questions:

I hope this information helps you further investigate the issue.

Please let me know if you have any other questions.

harsh62 commented 5 months ago

@ostanik Unfortunately I am still not able to repro and not able to find any obvious problems in the code. At this moment, I would like you to reach out to us on discord (https://discord.com/channels/705853757799399426/1019643921137139772) tagging me (my username harsh62). Once you reach out to us, we can set up a meeting and go through the specifics of the code in more detail.

ostanik commented 5 months ago

@harsh62 I can't access the link that you shared.

harsh62 commented 5 months ago

@ostanik Can you try https://discord.com/invite/amplify?

dandreiolteanu commented 1 month ago

Hello, this issue sounds like something we're also experiencing, are there any news?

harsh62 commented 1 month ago

@dandreiolteanu I have been unable to reproduce this issue in a local environment, and also unable to find any obvious code issues with Keychain and State Machine implementation. If you are able to provide any logs or codepaths when you are experiencing this issue, it would be really helpful.

kparichan commented 1 month ago

We're also seeing behavior in our app which seems to line up with this issues. I don't have full logs unfortunately, only what I've been able to piece together via analytics.

We have the same testers on iOS and Android, and the code is the same across platforms, but we've only ever seen this issue on iOS.

The issue appears to happen when calling fetchAuthSession right after the device wakes up, and has been asleep for some time.

While that call fails attempting to fetch/refresh the auth token, and with Amplify.Hub.subscribe setup, AuthChannelEventName.SESSION_EXPIRED does not appear to be triggered.

The failure to fetchAuthSession reports: localizedDescription = "The operation couldn’t be completed. (Amplify.AuthError error 1.)" print() = "network(AuthError: Unknown service error occurred Recovery suggestion: See the attached error for mo" (Truncated by analytics)

kparichan commented 1 month ago

The only other info I have is a screenshot from a debug build which reports an error of "The network connection was lost." It was attempting to contact a url of "https://cognito-idp.us-west-2.amazonaws.com/".

kparichan commented 1 month ago

I don't know how the internal networking is handled, but is the framework attempting to preflight network connectivity before actually making a request?

In general, that goes against the recommended behavior and could lead to an issue where connectivity checking fails because the device radios haven't been powered up yet right after wake.

This may also relate to another closed issue that my coworker pointed out: https://github.com/aws-amplify/amplify-swift/issues/3437

Normally you'd just set waitsForConnectivity and a short timeoutIntervalForResource value for the URLSessionConfiguration and issue any calls without any preflight connectivity checking.

Apologies if this has been investigated previously.

harsh62 commented 1 month ago

@kparichan Would you be able to open another issue with the relevant details and verbose logs. With the brief description you've give us, your issue seems to be something else, which is different from the session dumping issue that is being discussed here in this one.