Stale hasCredentials() results on external auth provider

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:

  1. False negative after login β€” immediately after a user completes the
    OAuth consent flow, hasCredentials() returns false for a short period (2 minutes).
  2. False positive after revoke β€” after the user revokes the app via
    id.atlassian.com/manage-profile/apps (or at the provider side),
    hasCredentials() keeps returning true for 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

  1. User has no credentials. hasCredentials() β†’ false.
  2. User completes consent flow via requestCredentials().
  3. Frontend calls checkAuth resolver immediately after redirect.
  4. hasCredentials() β†’ false. (expected true)
  5. Waiting a few seconds and retrying β†’ true.

Case B - false positive after revoke

  1. User is authenticated. hasCredentials() β†’ true.
  2. User revokes app access at id.atlassian.com/manage-profile/apps.
  3. Frontend calls checkAuth.
  4. hasCredentials() β†’ true. (expected false)
  5. State persists for ~5 minutes before hasCredentials() finally flips
    to false.

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

  1. Is this eventual consistency documented somewhere I’ve missed?
  2. 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!

1 Like