Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 22 additions & 1 deletion docs/auth/token-lifecycle.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,8 @@ static final ConcurrentHashMap<String, RefreshState> REFRESH_STATES

class RefreshState {
final Object lock // coordination primitive
int activeCallers // callers holding a lifecycle lease
boolean cleanupRequested // logout has scrubbed and retired this state
boolean refreshing // true while winner is in-flight
long publishGeneration // incremented only on successful publish
String newAuthToken // last successfully refreshed token
Expand All @@ -165,6 +167,19 @@ class RefreshState {
}
```

Each caller acquires a lifecycle lease before using the state and releases it on every exit
path. Logout immediately marks the state for cleanup and scrubs its published access token,
refresh token, instance URL, token type, and UI session ID. An idle state is removed immediately.
If callers are still active, the scrubbed state remains mapped until the final lease is released;
new callers that encounter it fail closed. This prevents a quick same-identity login from creating
a second coordinator while an old-session refresh is still in flight.

The same state lock also fences every post-response side effect. After the token endpoint returns,
the old winner checks `cleanupRequested` while holding `state.lock` before persisting credentials,
logging out, registering RTR, or broadcasting. If logout retired the state while the request was
in flight, the response is discarded even when a quick relogin has recreated the same backing
Android Account.

### Flow

```
Expand All @@ -186,7 +201,11 @@ refreshStaleToken()
→ POST /token (DPoP proof + nonce)
→ 400 use_dpop_nonce → cache nonce → retry
→ 200: new access_token, rotated refresh_token
broadcast ACCESS_TOKEN_REFRESH_INTENT
synchronized(state.lock)
cleanupRequested == false
→ persist refreshed credentials
→ register RTR and broadcast ACCESS_TOKEN_REFRESH_INTENT
lock released

synchronized(state.lock)
state.refreshing = false
Expand Down Expand Up @@ -295,6 +314,8 @@ therefore share credential state but do not perform network I/O while holding th
### Logout

`SalesforceSDKManager.removeAccount()` calls:
- `AccMgrAuthTokenProvider.clearRefreshState(user)` — scrubs published refresh results and removes
the per-account coordinator immediately or after its last active lifecycle lease is released
- `DPoPKeyManager.deleteKeyPair(alias)` — evicts the process-local handle and attempts to destroy
the EC keypair from the Android Keystore
- `DPoPNonceCache.clear(credentialsIdentifier)` — evicts cached nonces for this session
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ import com.salesforce.androidsdk.rest.ClientManager
import com.salesforce.androidsdk.rest.NotificationsActionsResponseBody
import com.salesforce.androidsdk.rest.NotificationsApiClient
import com.salesforce.androidsdk.rest.RestClient
import com.salesforce.androidsdk.rest.clearRefreshStateForUser
import com.salesforce.androidsdk.rest.peekRestClientWithResolvedUser
import com.salesforce.androidsdk.security.BiometricAuthenticationManager
import com.salesforce.androidsdk.security.SalesforceKeyGenerator
Expand Down Expand Up @@ -1067,6 +1068,8 @@ open class SalesforceSDKManager protected constructor(
cachedClientManager = null

userAccount?.let { userAccountResolved ->
clearRefreshStateForUser(userAccountResolved)

/*
* Drops this user's persisted feature markers so a later login
* as the same identity starts from an empty set rather than
Expand Down
Loading
Loading