Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
ostanik opened this issue Feb 26, 2024 · 18 comments
Open
Assignees
Labels
auth Issues related to the Auth category question General question

Comments

@ostanik
Copy link

ostanik commented Feb 26, 2024

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:

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

Flow:

  • A request is made.
  • The Cognito token is fetched from Amplify.
  • The token is injected into the Bearer.

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
Copy link
Member

harsh62 commented Feb 26, 2024

@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.

@harsh62 harsh62 added auth Issues related to the Auth category question General question labels Feb 26, 2024
@ostanik
Copy link
Author

ostanik commented Feb 26, 2024

@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:

  • Observed Behavior: Users with previously valid sessions 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.
  • Verbose Logs Limitation: Unfortunately, we cannot provide verbose client-side logs due to the sporadic nature of this issue and our logging capabilities.
  • Authentication Flow: Our flow involves:
    1. User signs in with email using a custom sign-in method.
    2. Upon receiving a login link, we fetch the session and perform a sanity check.
    3. For every API call, we fetch the session to inject the Cognito token.

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
Copy link
Member

harsh62 commented Feb 26, 2024

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

  • How are you able to come to the above conclusion?
  • What keychain are you talking about here? Amplify generated keychain or your app generated keychain? If Amplify's keychain, how do you know its without session now?
  • When you say "prompting a fetchAuthSession:, can you share this code snippet?

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.

@harsh62 harsh62 self-assigned this Feb 26, 2024
@ostanik
Copy link
Author

ostanik commented Feb 27, 2024

  • Conclusion Basis: The conclusion about session data absence in the keychain was reached through extensive testing with multiple simulators. By debugging the Amplify SDK during a scenario where a user encountered the issue, it was observed that the signedOut event triggers due to an empty keychain where Amplify expects to find session data.

  • Keychain Clarification: The reference to "keychain" pertains specifically to the storage managed by Amplify (i.e., AWSCognitoAuthCredentialStore.swift). In this context, the Amplify-managed keychain and the app's keychain effectively serve the same purpose, with Amplify utilizing the app's keychain space for its data storage.

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
Copy link
Author

ostanik commented Feb 29, 2024

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
Copy link

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

@harsh62
Copy link
Member

harsh62 commented Mar 4, 2024

@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
Copy link
Member

harsh62 commented Mar 4, 2024

@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
Copy link

@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
Copy link

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

@victorkifer
Copy link

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
Copy link

@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
Copy link
Member

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

@harsh62
Copy link
Member

harsh62 commented Mar 19, 2024

@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?

  • Does your users have the ability to switch configurations at runtime?
  • Do you manipulate keychain within your app? Which could unintentionally leading to session dumping.
  • Do you use keychain sharing using App Groups?

@ostanik
Copy link
Author

ostanik commented Mar 19, 2024

Hey @harsh62,
Answering your questions:

  • The user cannot switch configurations at runtime. Configurations are defined at compile time. We only define configurations this way to take advantage of our different environments via xconfig instead of creating scripts to load different JSON files based on the build type.

  • We use the keychain to store other access tokens or sensitive information. However, when we manipulate the keychain, we are careful to only use our keys (if necessary, to clear the session, for example) instead of dumping all the values that we have. This was my first hypothesis before opening this issue.

  • We do not share the keychain with a group of apps. Only one app uses the keychain.

I hope this information helps you further investigate the issue.

Please let me know if you have any other questions.

@harsh62
Copy link
Member

harsh62 commented Apr 1, 2024

@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
Copy link
Author

ostanik commented Apr 4, 2024

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

@harsh62
Copy link
Member

harsh62 commented Apr 4, 2024

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
auth Issues related to the Auth category question General question
Projects
None yet
Development

No branches or pull requests

5 participants