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
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ import android.content.Intent
import com.salesforce.androidsdk.accounts.UserAccountManager.getInstance
import com.salesforce.androidsdk.app.SalesforceSDKManager
import com.salesforce.androidsdk.auth.ScopeParser.Companion.toScopeParser
import com.salesforce.androidsdk.auth.dpop.DPoPKeyManager
import com.salesforce.androidsdk.auth.dpop.DPoPNonceCache
import com.salesforce.androidsdk.config.OAuthConfig
import com.salesforce.androidsdk.ui.TokenMigrationActivity
import com.salesforce.androidsdk.util.SalesforceSDKLogger
Expand Down Expand Up @@ -147,13 +149,21 @@ fun UserAccountManager.migrateRefreshToken(
* null-check failure below runs on the caller's thread, but the OAuth-config resolution and
* migration below it run on [Default]. Callers that touch UI from these callbacks must marshal
* to the main thread themselves.
*
* No-op: if [userAccount] is already DPoP-bound, there's nothing to upgrade — [onSuccess] is
* invoked synchronously with the unchanged account and no migration is attempted.
*/
@Suppress("UnusedReceiverParameter")
fun UserAccountManager.upgradeToDPoP(
userAccount: UserAccount,
onSuccess: (userAccount: UserAccount) -> Unit,
onFailure: (error: String, errorDesc: String?, e: Throwable?) -> Unit,
) {
if (userAccount.tokenType == DPoPKeyManager.DPOP_TOKEN_TYPE) {
onSuccess(userAccount)
return
}

val clientId = userAccount.clientId
val loginServer = userAccount.loginServer

Expand Down Expand Up @@ -205,6 +215,117 @@ fun UserAccountManager.upgradeToDPoP(
}
}

/**
* Downgrades the [userAccount]'s existing DPoP-bound refresh token to a Bearer (non-DPoP) one,
* in place — same consumer key, redirect URI, and scopes the account already uses. This is a
* same-config convenience over [migrateRefreshToken] with `useDPoP = false`: no re-consent is
* expected because nothing about the connected app / External Client App configuration changes.
*
* This works regardless of the global [SalesforceSDKManager.useDPoP] flag: that flag only sets
* the default DPoP posture for brand-new logins, while this call is an explicit action on an
* already-authenticated session — an app can leave the global flag on and still roll one user
* back to Bearer. The connected app / External Client App must accept Bearer tokens for the
* downgrade to succeed; a DPoP-enforcing app will reject the resulting session.
*
* On success, the pre-downgrade DPoP key pair and DPoP nonce-cache entries (keyed by the
* account's pre-migration [UserAccount.credentialsIdentifier], since migration mints a new one)
* are deleted — mirroring the teardown [SalesforceSDKManager] performs on logout. On failure or
* cancellation, that state is left untouched so the original DPoP-bound session keeps working.
*
* Callers wanting to migrate to a *different* consumer key, redirect URI, or scopes (or to
* explicitly upgrade a Bearer session to DPoP) should call [migrateRefreshToken] directly with
* their own [OAuthConfig] and `useDPoP` value, or see [upgradeToDPoP].
*
* Note: [onFailure] (and [onSuccess]) may be invoked off the main thread — the synchronous
* null-check failure below runs on the caller's thread, but the OAuth-config resolution and
* migration below it run on [Default]. Callers that touch UI from these callbacks must marshal
* to the main thread themselves.
*
* No-op: if [userAccount] is already Bearer (non-DPoP), there's nothing to downgrade —
* [onSuccess] is invoked synchronously with the unchanged account, no migration is attempted,
* and no DPoP state cleanup runs (there's none to clean up).
*/
@Suppress("UnusedReceiverParameter")
fun UserAccountManager.downgradeFromDPoP(
userAccount: UserAccount,
onSuccess: (userAccount: UserAccount) -> Unit,
onFailure: (error: String, errorDesc: String?, e: Throwable?) -> Unit,
) {
if (userAccount.tokenType != DPoPKeyManager.DPOP_TOKEN_TYPE) {
onSuccess(userAccount)
return
}

val clientId = userAccount.clientId
val loginServer = userAccount.loginServer

if (clientId == null || loginServer == null) {
val message = "User account clientId or loginServer is null."
SalesforceSDKLogger.e(TAG, message)
onFailure(message, null, null)
return
}

// Capture the pre-migration credentials identifier now: migration mints a NEW identifier for
// the account, so reading userAccount.credentialsIdentifier after the migration completes
// would return the wrong (or a null) value. This is what obsolete-DPoP-state cleanup below is
// keyed on. The account is guaranteed DPoP-bound here (the non-DPoP case returned early above),
// so cleanup always applies on success.
val oldCredId = userAccount.credentialsIdentifier

val onSuccessWithCleanup: (userAccount: UserAccount) -> Unit = { migratedUser ->
oldCredId?.takeIf { it.isNotEmpty() }?.let { id ->
runCatching {
DPoPKeyManager.deleteKeyPair(DPoPKeyManager.aliasForCredentialsIdentifier(id))
DPoPNonceCache.clear(id)
}.onFailure { e ->
SalesforceSDKLogger.w(TAG, "Failed to delete obsolete DPoP state on downgrade", e)
}
}
onSuccess(migratedUser)
}

// Prefer the redirect URI persisted on the account at login time: it's the exact value the
// connected app / External Client App was configured with for this user, and it doesn't
// change over time. Only fall back to resolving the OAuth configuration for the user's login
// server (debug override, per-host app config, or boot config) for accounts persisted before
// redirect URI was captured on UserAccount. Either way, keep the user's own consumer key and
// scopes so the downgrade is a true same-config, in-place operation.
CoroutineScope(Default).launch {
runCatching {
val persistedRedirectUri = userAccount.redirectUri
val redirectUri = if (!persistedRedirectUri.isNullOrBlank()) {
persistedRedirectUri
} else {
SalesforceSDKManager.getInstance()
.resolveOAuthConfigForLoginServer(loginServer)
.redirectUri
}

OAuthConfig(
consumerKey = clientId,
redirectUri = redirectUri,
scopes = userAccount.scope?.toScopeParser()?.scopes?.toList(),
)
}.fold(
onSuccess = { appConfig ->
migrateRefreshToken(
userAccount = userAccount,
appConfig = appConfig,
useDPoP = false,
onMigrationSuccess = onSuccessWithCleanup,
onMigrationError = onFailure,
)
},
onFailure = { e ->
val message = "Failed to resolve OAuth configuration for login server."
SalesforceSDKLogger.e(TAG, message, e)
onFailure(message, e.message, e)
},
)
}
}

/*
This mechanism is used to pass a _string_ id to the Activity to retrieve callback functions.

Expand Down
Loading
Loading