Select to view content in your preferred language

Stored credential is lost upon app restart

229
3
07-22-2024 08:34 AM
zdtorok
Occasional Contributor

I am using OAuth user authentication and when the user have authenticated successfully, I store their credential in the `arcGISCredentialStore`.

Something like this:

let oAuthUserConfig = OAuthUserConfiguration(portalURL: portalUrl, clientID: authConfig.clientId, redirectURL: redirectUrl)
ArcGISEnvironment.authenticationManager.arcGISAuthenticationChallengeHandler = EsriChallengeHandler(oAuthUserConfigurations: [oAuthUserConfig])

Task {
    do {
        let oAuthUserCredential = try await OAuthUserCredential.credential(for: oAuthUserConfig)
        ArcGISEnvironment.authenticationManager.arcGISCredentialStore.add(oAuthUserCredential)
    } catch {
        print(" >> Error obtaining OAuthUserCredential: \(error)")
    }
}

My problem is that after the user is authenticated, the credentials are stored in the credential store properly but when I restart the app, the credential store is empty and it triggers for authentication again.

Am I doing something wrong or is there maybe a bug? I'm using 200.4.0.

FYI This is the challenge handler:

@MainActor
final class EsriChallengeHandler: ArcGISAuthenticationChallengeHandler {
    let oAuthUserConfigurations: [OAuthUserConfiguration]
    
    public init(oAuthUserConfigurations: [OAuthUserConfiguration] = []) {
        self.oAuthUserConfigurations = oAuthUserConfigurations
    }

    func handleArcGISAuthenticationChallenge(_ challenge: ArcGISAuthenticationChallenge) async throws -> ArcGISAuthenticationChallenge.Disposition {
        if let configuration = oAuthUserConfigurations.first(where: { $0.canBeUsed(for: challenge.requestURL) }) {
            do {
                let credential = try await OAuthUserCredential.credential(for: configuration)
                ArcGISEnvironment.authenticationManager.arcGISCredentialStore.add(credential)
                return .continueWithCredential(credential)
            } catch {
                return .cancel
            }
        }

        print("No matching configuration found for challenge")
        return .cancel
    }
}

 Thank you.

Tags (1)
0 Kudos
3 Replies
NimeshJarecha
Esri Regular Contributor

Hi @zdtorok ,

The credential store available here is just an in-memory credential store so they are available for the lifetime of that app session only. 

 

ArcGISEnvironment.authenticationManager.arcGISCredentialStore.

 

If you want to persist the credentials between app sessions then you should create a persistent credential store and set on the authentication manager. 

 

ArcGISEnvironment.authenticationManager.arcGISCredentialStore = try await .makePersistent(
            access: .afterFirstUnlockThisDeviceOnly,
            synchronizesWithiCloud: true
        )

 

Please refer details in the documentation. 

Also, you do not need to store the credential in credential store explicitly. Since, you are providing credential while handling challenge, it will be added to credential store by the SDK.

ArcGISEnvironment.authenticationManager.arcGISCredentialStore.add(credential)

Hope this helps!

Regards,

Nimesh

0 Kudos
zdtorok
Occasional Contributor

Thanks a lot @NimeshJarecha !

Unfortunately there still seems to be some issue with my code. I adapted it the way you proposed, now having this:

@MainActor
final class EsriChallengeHandler: ArcGISAuthenticationChallengeHandler {
    let oAuthUserConfigurations: [OAuthUserConfiguration]
    
    public init(oAuthUserConfigurations: [OAuthUserConfiguration] = []) {
        self.oAuthUserConfigurations = oAuthUserConfigurations
    }

    func handleArcGISAuthenticationChallenge(_ challenge: ArcGISAuthenticationChallenge) async throws -> ArcGISAuthenticationChallenge.Disposition {
        if let configuration = oAuthUserConfigurations.first(where: { $0.canBeUsed(for: challenge.requestURL) }) {
            do {
                let credential = try await OAuthUserCredential.credential(for: configuration)
                ArcGISEnvironment.authenticationManager.arcGISCredentialStore.add(credential)
                return .continueWithCredential(credential)
            } catch {
                return .cancel
            }
        }

        print("No matching configuration found for challenge")
        return .cancel
    }
}
let oAuthUserConfig = OAuthUserConfiguration(portalURL: portalUrl, clientID: authConfig.clientId, redirectURL: redirectUrl)
ArcGISEnvironment.authenticationManager.arcGISAuthenticationChallengeHandler = EsriChallengeHandler(oAuthUserConfigurations: [oAuthUserConfig])
ArcGISEnvironment.authenticationManager.arcGISCredentialStore = try await .makePersistent(access: .afterFirstUnlockThisDeviceOnly, synchronizesWithiCloud: true)

Task {
    do {
        let oAuthUserCredential = try await OAuthUserCredential.credential(for: oAuthUserConfig)
    } catch {
        print(" >> Error obtaining OAuthUserCredential: \(error)")
    }
}

 I expected that in line 7, when I trigger the OAuthUserCredential.credential, it invokes my challenge handler which should handle this. Instead it presents the login alert again and again when I trigger this code snippet again - even if I haven't restarted the app, and I'm in the same session.

Can you help what am I overlooking? I start to believe I might have a wrong flow in my head, but what I understood:

1. Set the credential store to persistent

2. Set an own challenge handler

3. Trigger the login with OAuthUserCredential. If there is no credential stored, show the login alert, otherwise silently proceed with the challenge handler. But, in my challenge handler I also have the same OAuthUserCredential call so now I definitely believe I'm doing something wrong. Or maybe the code is fine for this part, but I shouldn't expect the challenge handler to be fired here.

I also tried to check before triggering OAuthUserCredential.credental(for) if there is already a stored credential but it didn't work either (checked if ArcGISEnvironment.authenticationManager.arcGISCredentialStore.credential(for: portalUrl) is nil)

 

0 Kudos
NimeshJarecha
Esri Regular Contributor

Hi @zdtorok,

To build your understanding of how secured resources work, let's go through the process step-by-step.

Creating credentials does not trigger an authentication challenge in your EsriChallengeHandler. Therefore, you should remove the following code:

 

Task {
    do {
        let oAuthUserCredential = try await OAuthUserCredential.credential(for: oAuthUserConfig)
    } catch {
        print(" >> Error obtaining OAuthUserCredential: \(error)")
    }
}

 

 

An authentication challenge is issued when accessing secured resources such as a portal, portal item, or layer. You create a credential to fulfill this authentication challenge. The credentials provided during the challenge are added to the credential store by the SDK, so subsequent requests from the same source URL will reuse them. Here is the workflow you should try:

1. Set Persistent Credential Store and Challenge Handler: At the start of the application, set the persistent credential store and challenge handler on ArcGISEnvironment.authenticationManager.

 

let oAuthUserConfig = OAuthUserConfiguration(portalURL: portalUrl, clientID: authConfig.clientId, redirectURL: redirectUrl)
ArcGISEnvironment.authenticationManager.arcGISAuthenticationChallengeHandler = EsriChallengeHandler(oAuthUserConfigurations: [oAuthUserConfig])
ArcGISEnvironment.authenticationManager.arcGISCredentialStore = try await .makePersistent(access: .afterFirstUnlockThisDeviceOnly, synchronizesWithiCloud: true)

 

 

2. Create and Load a Portal: Create and load a portal, which should issue an authentication challenge.

 

let portal = Portal(url: portalUrl, connection: .authenticated)
try await portal.load()

 

 

3. Credential Storage: The portal is loaded using the provided credential from the authentication challenge, and the credential is stored. The credential creation shows you the login view to enter the username and password.

4. App Restart: Close and re-open the app, which will reinitialize the challenge handler and persistent store.

5. Credential Retrieval: When the persistent store is set, the SDK loads credentials from the keychain into the credential store. You should see the credential available in the store.

6. Portal Loading: The app will attempt to load the portal again. It will find the credential in the store and reuse it, allowing the portal to load without issuing a new authentication challenge.

 

I would recommend using Authenticator toolkit component which takes care of showing user interface for different authentication types and handle all the complexity. You can look at this example to know see how challenge handlers and persistent credential stores are setup.  

 

Hope this helps!

Regards,

Nimesh 

0 Kudos