hasCredentials() returns stale results after authentication/revoke
Summary
hasCredentials() on an external auth provider appears to be eventually
consistent rather than strongly consistent. This causes two opposite bugs in
user-facing flows:
- False negative after login β immediately after a user completes the
OAuth consent flow,hasCredentials()returnsfalsefor a short period (2 minutes). - False positive after revoke β after the user revokes the app via
id.atlassian.com/manage-profile/apps(or at the provider side),
hasCredentials()keeps returningtruefor roughly 5 minutes.
Because there is no revokeCredentials() / invalidateCache() method on the
provider object, there is no API-level way to force a fresh read.
Reproduction
Environment
@forge/api: 7.1.3- Context: Custom UI
- Provider type: OAuth 2.0 external auth (e.g. Google, Bitbucket)
Case A - false negative after login
- User has no credentials.
hasCredentials()βfalse. - User completes consent flow via
requestCredentials(). - Frontend calls
checkAuthresolver immediately after redirect. hasCredentials()βfalse. (expectedtrue)- Waiting a few seconds and retrying β
true.
Case B - false positive after revoke
- User is authenticated.
hasCredentials()βtrue. - User revokes app access at
id.atlassian.com/manage-profile/apps. - Frontend calls
checkAuth. hasCredentials()βtrue. (expectedfalse)- State persists for ~5 minutes before
hasCredentials()finally flips
tofalse.
During the stale window in case B, calling provider.fetch(...) correctly
returns 401 - so the ground truth is available via fetch, itβs just not
reflected in hasCredentials().
Expected behaviour
hasCredentials() should reflect the current credential state
read-after-write within a single user session - i.e. after
requestCredentials() resolves, the next hasCredentials() call should
return true; after a user revokes consent, the next call should return
false (or at least within seconds, not minutes).
Questions
- Is this eventual consistency documented somewhere Iβve missed?
- Is there a planned
revokeCredentials()/ account removal method, or is revoking always user-initiated via the Connected Apps page?
One more thing - Iβve been using external auth for a while and only started
seeing this recently. Did anything change on the platform side? New
caching layer, backend migration, or a change in how hasCredentials() is
resolved? If there was a recent rollout, knowing about it would help
narrow down whether this is expected new behaviour.
Thanks!