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 @@ -99,6 +99,7 @@ public class UserAccount {
public static final String COOKIE_SID_CLIENT = "cookie-sid_Client";
public static final String SID_COOKIE_NAME = "sidCookieName";
public static final String CLIENT_ID = "clientId";
public static final String REDIRECT_URI = "redirectUri";
public static final String PARENT_SID = "parentSid";
public static final String TOKEN_FORMAT = "tokenFormat";
public static final String BEACON_CHILD_CONSUMER_KEY = "auto_installed_app_org_consumer_key";
Expand Down Expand Up @@ -149,6 +150,7 @@ public class UserAccount {
private String cookieSidClient;
private String sidCookieName;
private String clientId;
private String redirectUri;
private String parentSid;
private String tokenFormat;
private Map<String, String> additionalOauthValues;
Expand Down Expand Up @@ -194,6 +196,7 @@ public class UserAccount {
* @param cookieSidClient cookie sid client
* @param sidCookieName sid cookie name
* @param clientId oauth client id
* @param redirectUri oauth redirect uri
* @param parentSid parent sid
* @param tokenFormat token format
* @param beaconChildConsumerKey beacon child consumer key
Expand All @@ -210,7 +213,7 @@ public class UserAccount {
String lightningDomain, String lightningSid, String vfDomain, String vfSid,
String contentDomain, String contentSid, String csrfToken, Boolean nativeLogin,
String language, String locale, String cookieClientSrc, String cookieSidClient,
String sidCookieName, String clientId, String parentSid, String tokenFormat,
String sidCookieName, String clientId, String redirectUri, String parentSid, String tokenFormat,
String beaconChildConsumerKey, String beaconChildConsumerSecret, String apiInstanceServer, String scope) {
this.authToken = authToken;
this.refreshToken = refreshToken;
Expand Down Expand Up @@ -245,6 +248,7 @@ public class UserAccount {
this.cookieSidClient = cookieSidClient;
this.sidCookieName = sidCookieName;
this.clientId = clientId;
this.redirectUri = redirectUri;
this.parentSid = parentSid;
this.tokenFormat = tokenFormat;
this.beaconChildConsumerKey = beaconChildConsumerKey;
Expand Down Expand Up @@ -296,6 +300,7 @@ public class UserAccount {
cookieSidClient = object.optString(COOKIE_SID_CLIENT, null);
sidCookieName = object.optString(SID_COOKIE_NAME, null);
clientId = object.optString(CLIENT_ID, null);
redirectUri = object.optString(REDIRECT_URI, null);
parentSid = object.optString(PARENT_SID, null);
tokenFormat = object.optString(TOKEN_FORMAT, null);
beaconChildConsumerKey = object.optString(BEACON_CHILD_CONSUMER_KEY, null);
Expand Down Expand Up @@ -356,6 +361,7 @@ public UserAccount(JSONObject object) {
cookieSidClient = bundle.getString(COOKIE_SID_CLIENT);
sidCookieName = bundle.getString(SID_COOKIE_NAME);
clientId = bundle.getString(CLIENT_ID);
redirectUri = bundle.getString(REDIRECT_URI);
parentSid = bundle.getString(PARENT_SID);
tokenFormat = bundle.getString(TOKEN_FORMAT);
beaconChildConsumerKey = bundle.getString(BEACON_CHILD_CONSUMER_KEY);
Expand Down Expand Up @@ -706,6 +712,15 @@ public String getClientId() {
return clientId;
}

/**
* Returns the oauth redirect uri.
*
* @return redirect uri.
*/
public String getRedirectUri() {
return redirectUri;
}

/**
* Returns the oauth client id to use for refresh
* In the case of beacon app, the beacon child consumer key returned during login should be used instead of the configured consumer key
Expand Down Expand Up @@ -1116,6 +1131,7 @@ JSONObject toJson(List<String> additionalOauthKeys) {
object.put(COOKIE_CLIENT_SRC, cookieClientSrc);
object.put(COOKIE_SID_CLIENT, cookieSidClient);
object.put(SID_COOKIE_NAME, sidCookieName);
object.put(REDIRECT_URI, redirectUri);
object.put(PARENT_SID, parentSid);
object.put(TOKEN_FORMAT, tokenFormat);
object.put(BEACON_CHILD_CONSUMER_KEY, beaconChildConsumerKey);
Expand Down Expand Up @@ -1185,6 +1201,7 @@ Bundle toBundle(List<String> additionalOauthKeys) {
object.putString(COOKIE_SID_CLIENT, cookieSidClient);
object.putString(SID_COOKIE_NAME, sidCookieName);
object.putString(CLIENT_ID, clientId);
object.putString(REDIRECT_URI, redirectUri);
object.putString(PARENT_SID, parentSid);
object.putString(TOKEN_FORMAT, tokenFormat);
object.putString(BEACON_CHILD_CONSUMER_KEY, beaconChildConsumerKey);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ class UserAccountBuilder private constructor() {
private var cookieSidClient: String? = null
private var sidCookieName: String? = null
private var clientId: String? = null
private var redirectUri: String? = null
private var parentSid: String? = null
private var tokenFormat: String? = null
private var additionalOauthValues: Map<String, String>? = null
Expand Down Expand Up @@ -175,6 +176,7 @@ class UserAccountBuilder private constructor() {
.cookieSidClient(userAccount.cookieSidClient)
.sidCookieName(userAccount.sidCookieName)
.clientId(userAccount.clientId)
.redirectUri(userAccount.redirectUri)
.parentSid(userAccount.parentSid)
.tokenFormat(userAccount.tokenFormat)
.beaconChildConsumerKey(userAccount.beaconChildConsumerKey)
Expand Down Expand Up @@ -539,6 +541,16 @@ class UserAccountBuilder private constructor() {
return if (!allowUnset && clientId == null) this else apply { this.clientId = clientId }
}

/**
* Sets oauth redirect uri
*
* @param redirectUri oauth redirect uri.
* @return Instance of this class.
*/
fun redirectUri(redirectUri: String?): UserAccountBuilder {
return if (!allowUnset && redirectUri == null) this else apply { this.redirectUri = redirectUri }
}

/**
* Sets additional OAuth values.
*
Expand Down Expand Up @@ -663,6 +675,7 @@ class UserAccountBuilder private constructor() {
cookieSidClient,
sidCookieName,
clientId,
redirectUri,
parentSid,
tokenFormat,
beaconChildConsumerKey,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -565,6 +565,7 @@ public Bundle updateAccount(Account account, UserAccount userAccount) {
final String cookieSidClient = decryptUserData(account, AuthenticatorService.KEY_COOKIE_SID_CLIENT, encryptionKey);
final String sidCookieName = decryptUserData(account, AuthenticatorService.KEY_SID_COOKIE_NAME, encryptionKey);
final String clientId = decryptUserData(account, AuthenticatorService.KEY_CLIENT_ID, encryptionKey);
final String redirectUri = decryptUserData(account, AuthenticatorService.KEY_REDIRECT_URI, encryptionKey);

final String parentSid = decryptUserData(account, AuthenticatorService.KEY_PARENT_SID, encryptionKey);
final String tokenFormat = decryptUserData(account, AuthenticatorService.KEY_TOKEN_FORMAT, encryptionKey);
Expand Down Expand Up @@ -625,6 +626,7 @@ public Bundle updateAccount(Account account, UserAccount userAccount) {
.cookieSidClient(cookieSidClient)
.sidCookieName(sidCookieName)
.clientId(clientId)
.redirectUri(redirectUri)
.parentSid(parentSid)
.tokenFormat(tokenFormat)
.beaconChildConsumerKey(beaconChildConsumerKey)
Expand Down Expand Up @@ -742,6 +744,7 @@ private Bundle buildAuthBundle(UserAccount userAccount) {
extras.putString(AuthenticatorService.KEY_INSTANCE_URL, SalesforceSDKManager.encrypt(userAccount.getInstanceServer(), encryptionKey));
extras.putString(AuthenticatorService.KEY_API_INSTANCE_URL, SalesforceSDKManager.encrypt(userAccount.getApiInstanceServer(), encryptionKey));
extras.putString(AuthenticatorService.KEY_CLIENT_ID, SalesforceSDKManager.encrypt(userAccount.getClientId(), encryptionKey));
extras.putString(AuthenticatorService.KEY_REDIRECT_URI, SalesforceSDKManager.encrypt(userAccount.getRedirectUri(), encryptionKey));
extras.putString(AuthenticatorService.KEY_ORG_ID, SalesforceSDKManager.encrypt(userAccount.getOrgId(), encryptionKey));
extras.putString(AuthenticatorService.KEY_USER_ID, SalesforceSDKManager.encrypt(userAccount.getUserId(), encryptionKey));
extras.putString(AuthenticatorService.KEY_COMMUNITY_ID, SalesforceSDKManager.encrypt(userAccount.getCommunityId(), encryptionKey));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,13 @@ package com.salesforce.androidsdk.accounts
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.config.OAuthConfig
import com.salesforce.androidsdk.ui.TokenMigrationActivity
import com.salesforce.androidsdk.util.SalesforceSDKLogger
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers.Default
import kotlinx.coroutines.launch
import java.util.UUID

const val TAG = "UserAccountManager"
Expand All @@ -43,11 +47,43 @@ const val TAG = "UserAccountManager"
* This might cause the approve/deny screen to be presented to the user to authorize the
* new app. If successful a new set of credentials (refresh token, access token) are obtained
* and replace the existing credentials for the user.
*
* This overload preserves the original (pre-DPoP) behavior: the migrated session defers to the
* global [SalesforceSDKManager.useDPoP] flag for its DPoP posture. To express a per-call DPoP
* intent, use the [useDPoP]-carrying overload; for the common same-config, DPoP-upgrade case,
* see [upgradeToDPoP].
*/
fun UserAccountManager.migrateRefreshToken(
userAccount: UserAccount? = getInstance().currentUser,
appConfig: OAuthConfig,
onMigrationSuccess: (userAccount: UserAccount) -> Unit,
onMigrationError: (error: String, errorDesc: String?, e: Throwable?) -> Unit,
) = migrateRefreshToken(
userAccount = userAccount,
appConfig = appConfig,
useDPoP = null,
onMigrationSuccess = onMigrationSuccess,
onMigrationError = onMigrationError,
)

/**
* Attempts to migrate the [userAccount] to the provided Connected App or
* External Client Application [appConfig], with an explicit per-call DPoP intent.
*
* This might cause the approve/deny screen to be presented to the user to authorize the
* new app. If successful a new set of credentials (refresh token, access token) are obtained
* and replace the existing credentials for the user.
*
* [useDPoP] expresses the DPoP intent for this specific migration call: `true` binds the
* migrated session to DPoP, `false` migrates it unbound, and `null` defers to the global
* [SalesforceSDKManager.useDPoP] flag (the behavior of the overload without this parameter).
* See [upgradeToDPoP] for the common same-config, `useDPoP = true` case.
*/
@Suppress("UnusedReceiverParameter")
fun UserAccountManager.migrateRefreshToken(
userAccount: UserAccount? = getInstance().currentUser,
appConfig: OAuthConfig,
useDPoP: Boolean?,
onMigrationSuccess: (userAccount: UserAccount) -> Unit,
onMigrationError: (error: String, errorDesc: String?, e: Throwable?) -> Unit,
) {
Expand Down Expand Up @@ -81,11 +117,94 @@ fun UserAccountManager.migrateRefreshToken(
putExtra(TokenMigrationActivity.EXTRA_USER_ID, userId)
putExtra(TokenMigrationActivity.EXTRA_OAUTH_CONFIG, appConfig)
putExtra(TokenMigrationActivity.EXTRA_CALLBACK_ID, callbackKey)
// Only carry a per-call DPoP intent when the caller expressed one; omitting the
// extra lets TokenMigrationActivity defer to the global SalesforceSDKManager.useDPoP
// flag (prior behavior).
useDPoP?.let { putExtra(TokenMigrationActivity.EXTRA_USE_DPOP, it) }
}
)
}
}

/**
* Upgrades the [userAccount]'s existing Bearer (non-DPoP) refresh token to a DPoP-bound one,
* in place — same consumer key, redirect URI, and scopes the account already uses. This is a
* same-config convenience over [migrateRefreshToken] with `useDPoP = true`: no re-consent is
* expected because nothing about the connected app / External Client App configuration changes.
*
* The redirect URI used is the one persisted on [userAccount] at login time (the exact value
* the connected app / External Client App was configured with for this user); it only falls
* back to resolving the OAuth configuration for the account's login server for accounts that
* were persisted before the redirect URI was captured on [UserAccount].
*
* 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. Callers wanting to migrate to a *different* consumer key,
* redirect URI, or scopes (or to explicitly downgrade a DPoP-bound session back to Bearer)
* should call [migrateRefreshToken] directly with their own [OAuthConfig] and `useDPoP` value.
*
* 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.
*/
@Suppress("UnusedReceiverParameter")
fun UserAccountManager.upgradeToDPoP(
userAccount: UserAccount,
onSuccess: (userAccount: UserAccount) -> Unit,
onFailure: (error: String, errorDesc: String?, e: Throwable?) -> Unit,
) {
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
}

// 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 upgrade 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 = true,
onMigrationSuccess = onSuccess,
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
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ import com.salesforce.androidsdk.app.Features.FEATURE_AUTH_TYPE_WEB_SERVER_HYBRI
import com.salesforce.androidsdk.app.Features.FEATURE_AUTH_TYPE_WEB_SERVER_NON_HYBRID
import com.salesforce.androidsdk.app.Features.FEATURE_BEACON
import com.salesforce.androidsdk.app.Features.FEATURE_BIOMETRIC_AUTH
import com.salesforce.androidsdk.app.Features.FEATURE_DPOP
import com.salesforce.androidsdk.app.Features.FEATURE_SCREEN_LOCK
import com.salesforce.androidsdk.app.Features.FEATURE_TOKEN_FORMAT_JWT
import com.salesforce.androidsdk.app.Features.FEATURE_TOKEN_FORMAT_OPAQUE
Expand Down Expand Up @@ -107,6 +108,7 @@ internal suspend fun onAuthFlowComplete(
tokenResponse: TokenEndpointResponse,
loginServer: String,
consumerKey: String,
redirectUri: String? = null,
onAuthFlowError: (error: String, errorDesc: String?, e: Throwable?) -> Unit,
onAuthFlowSuccess: (userAccount: UserAccount) -> Unit,
buildAccountName: (username: String?, instanceServer: String?) -> String = ::defaultBuildAccountName,
Expand Down Expand Up @@ -175,6 +177,7 @@ internal suspend fun onAuthFlowComplete(
.accountName(buildAccountName(userIdentity?.username, tokenResponse.instanceUrl))
.loginServer(loginServer)
.clientId(consumerKey)
.redirectUri(redirectUri)
.nativeLogin(nativeLogin)
.credentialsIdentifier(credentialsIdentifier)
.build()
Expand Down Expand Up @@ -210,6 +213,15 @@ internal suspend fun onAuthFlowComplete(
} else {
SalesforceSDKManager.getInstance().unregisterUsedAppFeature(FEATURE_BEACON, account)
}

// DP: DPoP-bound session. Token migration bypasses LoginActivity.onAuthFlowSuccess (the
// usual site of this marker), so an in-place upgrade to DPoP would otherwise never advertise
// the flag. tokenType is a per-session property, so mirror it onto the migrated account here.
if ("DPoP" == account.tokenType) {
SalesforceSDKManager.getInstance().registerUsedAppFeature(FEATURE_DPOP, account)
} else {
SalesforceSDKManager.getInstance().unregisterUsedAppFeature(FEATURE_DPOP, account)
}
} else {
if (nativeLogin) {
// Native login bypasses LoginActivity.onAuthFlowSuccess, so A-marker per-user
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ public class AuthenticatorService extends Service {
public static final String KEY_API_INSTANCE_URL = "apiInstanceUrl";
public static final String KEY_USER_ID = "userId";
public static final String KEY_CLIENT_ID = "clientId";
public static final String KEY_REDIRECT_URI = "redirectUri";
public static final String KEY_ORG_ID = "orgId";
public static final String KEY_USERNAME = "username";
public static final String KEY_ID_URL = "id";
Expand Down
Loading
Loading