Select to view content in your preferred language

Credentials persistence on iOS: 'Invalid token.' error in particular case

513
7
05-14-2025 02:15 AM
Labels (1)
EgorFedorov
Occasional Contributor

Turning creds persistence on/off causes an exception.

Prerequisites:
- Set up OAuth2 sign in to ArcGIS Online.
- Set up request to secured resource ("https://www.arcgis.com/sharing/rest/portals/self" in my case).
- For clean reproducing create new iOS emulator or use one where creds persistence was NOT enabled previously.


Steps to reproduce:
1. Run app with persistence DISABLED. It works OK: shows prompt for login/password and performs authorized requests after that.
2. Close app and launch it with persistence ENABLED. It will fail with an exception:

ArcGISException: code=18006; Invalid token.; An invalid token was used to access https://www.arcgis.com/sharing/rest/oauth2/token.


3. Subsequent runs witn persistence DISABLED will work OK.
4. Subsequent runs witn persistence ENABLED will throw same exception again.


At the same time, running app with enabled OR disabled persistence ONLY (without switching between modes) works well.
App reinstalling does not help.


Code that enables persistence in my app is right from docs and is executed right before 'runApp()' call:

ArcGISEnvironment.authenticationManager.arcGISCredentialStore =
await ArcGISCredentialStore.initPersistentStore();

 

Exception is thrown by 'oauthCredential.getTokenInfo()' call inside my internal class responsible for extracting OAuth2 token for my own server requests. Not sure why this is happening. I use all methods from SDK. Possibly, there is a call to 'oauth2/token' endpoint with outdated token, and server just returns an error instead of new token? Could you please advice something on this?

creds-pers-token-issue.png

 


Tested via simulators:
- iPhone 15 w/ iOS 17.5
- iPhone 16 w/ iOS 18.4
- iPhone 16 Plus w/ iOS 18.4

Flutter 3.29.3, Dart 3.7.2, ArcGIS Flutter SDK 200.7.

0 Kudos
7 Replies
JenMerritt
Esri Contributor

Thanks for the report  @EgorFedorov - the team is investigating if there is a bug or a workflow issue. We'll get back again when we've looked into it.

0 Kudos
HarishK
Esri Contributor

Thanks for the detailed breakdown and reproduction steps — that’s very helpful. Based on your description and the provided code, here’s what’s likely happening and how you can address it:

1. Avoid Mixing Manual Token Handling with Challenge Handler

You're currently using both:
- getToken() to manually fetch tokens.
- handleArcGISAuthenticationChallenge() to respond to SDK challenges.

This can lead to confusion and race conditions. Instead, have your challenge handler use your getToken() method so that there is only ever a single call to OAuthUserCredential.create().

2. Simplify Token Management

Instead of using a CompleterList, consider using a single shared Completer<String>? to avoid redundant token requests:

Completer<String>? _tokenCompleter;

Future<String> getToken() {
if (_tokenCompleter != null) return _tokenCompleter!.future;

_tokenCompleter = Completer<String>();

OAuthUserCredential.create(configuration: _oAuthUserConfiguration)
.then((credential) => credential.getTokenInfo())
.then((tokenInfo) {
_tokenCompleter!.complete(tokenInfo.accessToken);
})
.catchError((e, stackTrace) {
_tokenCompleter!.completeError(e, stackTrace);
});

return _tokenCompleter!.future;
}

This ensures only one token request is active at a time.

3. Ensure Authentication Flow Completes Before Token Use

Make sure that:
- The user is signed in.
- The credential is valid.
- The SDK has had a chance to invoke the challenge handler (if needed).

If you call getToken() too early (e.g., before the user signs in), it may fail.

4. Manually Validate or Clear Invalid Credentials

If you're switching between persistent and non-persistent modes, consider validating or clearing credentials before use:

final credential = ArcGISEnvironment
.authenticationManager.arcGISCredentialStore
.getCredential(uri: _oAuthUserConfiguration.portalUri);

if (credential is OAuthUserCredential) {
try {
await credential.getTokenInfo(); // Validate token
} catch (_) {
// Token is invalid, remove it
ArcGISEnvironment.authenticationManager.arcGISCredentialStore
.remove(credential: credential);
}
}

 

Suggestion

If possible, please share a minimal reproducible example (MRE) that isolates the issue. This will help confirm whether the problem lies in credential reuse, timing, or SDK behavior.


Summary:

- Let the SDK handle authentication via the challenge handler.
- Avoid mixing manual token fetching with SDK-managed flows.
- Use a single Completer to manage token requests.
- Validate or clear credentials when switching persistence modes.
- Share a minimal example if the issue persists.

0 Kudos
EgorFedorov
Occasional Contributor

Thank you very much for your response.

I've spent whole day trying to figure out how to fix this.

For MRE, I've created new Flutter app from scratch, added 'arcgis_maps' package and executed simple '/self' request to AGOL. Surprisingly, it worked well. I've copied code from MRE to my project, and it failed with the same exception.

Then I've updated flutter SDK in 'pubspec.yaml' in project from

environment:
sdk: '>=3.7.0 <4.0.0'

to 

environment:
sdk: ^3.7.2

 

After that:

- 'flutter clean' (several times)

- remove 'ios' folder and recreate it via 'flutter create . --platforms=ios'

- totally removed 'arcgis_maps' and installed again

 

Nothing helped. After weekend, I turn machine on and just re-run project. Surprisingly, it works well on all my simulators (listed above). So, I am totally not sure what fixed an issue. I guess, it should be some kind of update issue related to native project files.

0 Kudos
EgorFedorov
Occasional Contributor

It would be great if we could continue this discussion.

 


1. Avoid Mixing Manual Token Handling with Challenge Handler

You're currently using both:
- getToken() to manually fetch tokens.
- handleArcGISAuthenticationChallenge() to respond to SDK challenges.

This can lead to confusion and race conditions. Instead, have your challenge handler use your getToken() method so that there is only ever a single call to OAuthUserCredential.create().


Well, I follow the rule: all actual requests for token must be done by ArcGIS SDK. No one auth request from my code. This is my intentional decision: SDK is the single source of truth, and it is responsible for all token-related operations (request, store, refresh). No manual call to smth like 'https://www.arcgis.com/sharing/rest/oauth2/token' from my own code. Additionally, I am not always sure at which moment I need separate string token or just SDK-powered auth request.

Ideally, I want to achieve the following workflow:
- some object needs an access to secured resource
- it goes to SDK and asks for token
- SDK grabs token from internal storage or retrieves it from server and returns to object
- object uses it

Having this in mind, both 'getToken()' and 'handleArcGISAuthenticationChallenge()' call internal '_getCredential()' method that, in turn, calls 'OAuthUserCredential.create()'. Am I doing smth wrong?


This worked well with 200.6. I was using this approach for about 4 month almost every day.

0 Kudos
EgorFedorov
Occasional Contributor

2. Simplify Token Management

Instead of using a CompleterList, consider using a single shared Completer<String>? to avoid redundant token requests:

Completer<String>? _tokenCompleter;

Future<String> getToken() {
if (_tokenCompleter != null) return _tokenCompleter!.future;

_tokenCompleter = Completer<String>();

OAuthUserCredential.create(configuration: _oAuthUserConfiguration)
.then((credential) => credential.getTokenInfo())
.then((tokenInfo) {
_tokenCompleter!.complete(tokenInfo.accessToken);
})
.catchError((e, stackTrace) {
_tokenCompleter!.completeError(e, stackTrace);
});

return _tokenCompleter!.future;
}

This ensures only one token request is active at a time.


 

 

Looks like this won't work when token becomes invalid.

Imagine that after ~1h 'getToken()' is called again. In this case previously requested token is outdated (its lifetime is about 30 mins, but it depends). But '_tokenCompleter' is still not null. Moreover, '_tokenCompleter.future' is already fulfilled with old token. As a result, this invalid token will be returned.

I definitely don't want to check manually if token is valid or not. I want SDK to do this for me. It already implements (or will implement in future) all possible auth algorithms, not OAuth only: IWA, social, whatever.

So, this drives my current approach:
- 1 getToken() call = 1 NEW future returned
- 1 call for token to SDK = 1 list of completers (token waiters queue). All completers in this list are completed with result or error simultaneously, after that list is cleared.

Does this approach look correct?

0 Kudos
EgorFedorov
Occasional Contributor

3. Ensure Authentication Flow Completes Before Token Use

Make sure that:
- The user is signed in.
- The credential is valid.
- The SDK has had a chance to invoke the challenge handler (if needed).

If you call getToken() too early (e.g., before the user signs in), it may fail.

 


 

'getToken()' calls 'OAuthCredential.getTokenInfo()' internally. So, it should trigger the challenge handler regardless of when it is called.

0 Kudos
HarishK
Esri Contributor

Could you please provide a complete working example of the problematic code? This will help us reproduce the issue on our end.

0 Kudos