Skip to content
Open
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 @@ -134,8 +134,14 @@ public Bundle getAuthToken(AccountAuthenticatorResponse response, Account accoun

try {
final Map<String,String> addlParamsMap = originalUserAccount.getAdditionalOauthValues();
final URI tokenServer = OAuth2.overrideLoginServerIfNeeded(
originalUserAccount.getLoginServer(),
originalUserAccount.getInstanceServer(),
originalUserAccount.getCommunityId(),
originalUserAccount.getCommunityUrl());
SalesforceSDKLogger.i(TAG, "Initiating token refresh to host: " + tokenServer.getHost());
final OAuth2.TokenEndpointResponse tr = OAuth2.refreshAuthToken(HttpAccess.DEFAULT,
new URI(originalUserAccount.getLoginServer()), originalUserAccount.getClientIdForRefresh(), originalUserAccount.getRefreshToken(), addlParamsMap);
tokenServer, originalUserAccount.getClientIdForRefresh(), originalUserAccount.getRefreshToken(), addlParamsMap);

UserAccount updatedUserAccount = UserAccountBuilder.getInstance()
.populateFromUserAccount(originalUserAccount)
Expand Down
46 changes: 44 additions & 2 deletions libs/SalesforceSDK/src/com/salesforce/androidsdk/auth/OAuth2.java
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
import android.text.TextUtils;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.annotation.WorkerThread;

Expand Down Expand Up @@ -606,21 +607,62 @@ public static TokenEndpointResponse makeTokenEndpointRequest(HttpAccess httpAcce
}
}

/**
* Selects the server URI to target for token endpoint requests.
* Precedence: communityUrl &gt; instanceServer &gt; loginServer.
* instanceServer is populated only after the first token response, so a null instanceServer
* naturally identifies the code-exchange path which must target the login pool.
*
* @param loginServer Login server URL string (fallback, never null).
* @param instanceServer Instance server URL string, or null if not yet known.
* @param communityId Community ID, or null if not a community user.
* @param communityUrl Community URL string, or null if not a community user.
* @return The URI to use as the token endpoint base.
*/
public static URI overrideLoginServerIfNeeded(String loginServer, @Nullable String instanceServer,
@Nullable String communityId, @Nullable String communityUrl) {
if (communityId != null && !communityId.isEmpty() && communityUrl != null && !communityUrl.isEmpty()) {
try {
return new URI(communityUrl);
} catch (URISyntaxException e) {
SalesforceSDKLogger.w(TAG, "Invalid community URL, falling through to instanceServer", e);
}
}
if (instanceServer != null && !instanceServer.isEmpty()) {
try {
return new URI(instanceServer);
} catch (URISyntaxException e) {
SalesforceSDKLogger.w(TAG, "Invalid instance server URL, falling through to loginServer", e);
}
}
try {
return new URI(loginServer);
} catch (URISyntaxException e) {
throw new IllegalArgumentException("Invalid login server URL: " + loginServer, e);
}
}

/**
* Fetches an OpenID token from the Salesforce backend. This requires an OpenID token to be
* configured on the Salesforce connected app in the backend. It also requires the "openid"
* scope to be added on the client side through bootconfig and on the connected app.
*
* @param loginServer Login server.
* @param instanceServer Instance server, or null if not yet known.
* @param clientId Client ID.
* @param communityId Community ID, or null if not a community user.
* @param communityUrl Community URL, or null if not a community user.
* @param refreshToken Refresh token.
* @return OpenID token.
*/
public static String getOpenIDToken(String loginServer, String clientId, String refreshToken) {
public static String getOpenIDToken(String loginServer, @Nullable String instanceServer,
String clientId, @Nullable String communityId,
@Nullable String communityUrl, String refreshToken) {
String idToken = null;
try {
final URI tokenServer = overrideLoginServerIfNeeded(loginServer, instanceServer, communityId, communityUrl);
final TokenEndpointResponse tr = refreshAuthToken(HttpAccess.DEFAULT,
new URI(loginServer), clientId, refreshToken, null);
tokenServer, clientId, refreshToken, null);
idToken = tr.idToken;
} catch (Exception e) {
SalesforceSDKLogger.e(TAG, "Exception thrown while fetching OpenID token", e);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
import com.salesforce.androidsdk.app.SalesforceSDKManager;
import com.salesforce.androidsdk.auth.AuthenticatorService;
import com.salesforce.androidsdk.auth.HttpAccess;
import com.salesforce.androidsdk.auth.OAuth2;
import com.salesforce.androidsdk.auth.OAuth2.LogoutReason;
import com.salesforce.androidsdk.auth.OAuth2.OAuthFailedException;
import com.salesforce.androidsdk.auth.OAuth2.TokenEndpointResponse;
Expand Down Expand Up @@ -762,8 +763,14 @@ private UserAccount refreshStaleToken(Account account) throws NetworkErrorExcept
// value avoids POSTing a stale token that would fail with invalid_grant.
final String currentRefreshToken = originalUserAccount.getRefreshToken();
try {
final URI tokenServer = OAuth2.overrideLoginServerIfNeeded(
originalUserAccount.getLoginServer(),
originalUserAccount.getInstanceServer(),
originalUserAccount.getCommunityId(),
originalUserAccount.getCommunityUrl());
SalesforceSDKLogger.i(TAG, "Initiating token refresh to host: " + tokenServer.getHost());
final TokenEndpointResponse tr = refreshAuthToken(HttpAccess.DEFAULT,
new URI(originalUserAccount.getLoginServer()), originalUserAccount.getClientIdForRefresh(), currentRefreshToken, addlParamsMap);
tokenServer, originalUserAccount.getClientIdForRefresh(), currentRefreshToken, addlParamsMap);

if (tr.authToken == null) {
throw new MalformedTokenException("Token endpoint returned null access token");
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/*
* Copyright (c) 2026-present, salesforce.com, inc.
* All rights reserved.
* Redistribution and use of this software in source and binary forms, with or
* without modification, are permitted provided that the following conditions
* are met:
* - Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
* - Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
* - Neither the name of salesforce.com, inc. nor the names of its contributors
* may be used to endorse or promote products derived from this software without
* specific prior written permission of salesforce.com, inc.
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
package com.salesforce.androidsdk.auth

import com.salesforce.androidsdk.auth.OAuth2.overrideLoginServerIfNeeded
import org.junit.Assert.assertEquals
import org.junit.Test

class OAuth2OverrideLoginServerTests {

private val loginServer = "https://login.salesforce.com"
private val instanceServer = "https://myco.my.salesforce.com"
private val communityUrl = "https://myco.force.com/community"
private val communityId = "0DBxx0000000001"

@Test
fun test_givenInstanceServerPopulated_whenOverrideLoginServerIfNeeded_thenReturnsInstanceServer() {
val result = overrideLoginServerIfNeeded(loginServer, instanceServer, null, null)
assertEquals(instanceServer, result.toString())
}

@Test
fun test_givenInstanceServerNull_whenOverrideLoginServerIfNeeded_thenReturnsLoginServer() {
val result = overrideLoginServerIfNeeded(loginServer, null, null, null)
assertEquals(loginServer, result.toString())
}

@Test
fun test_givenCommunityIdAndUrl_whenOverrideLoginServerIfNeeded_thenReturnsCommunityUrl() {
// communityUrl wins even when instanceServer is also set
val result = overrideLoginServerIfNeeded(loginServer, instanceServer, communityId, communityUrl)
assertEquals(communityUrl, result.toString())
}

@Test
fun test_givenCommunityIdButNullCommunityUrl_whenOverrideLoginServerIfNeeded_thenFallsBackToInstanceServer() {
val result = overrideLoginServerIfNeeded(loginServer, instanceServer, communityId, null)
assertEquals(instanceServer, result.toString())
}

@Test
fun test_givenAllNull_codeExchangePath_whenOverrideLoginServerIfNeeded_thenReturnsLoginServer() {
val result = overrideLoginServerIfNeeded(loginServer, null, null, null)
assertEquals(loginServer, result.toString())
}

@Test
fun test_givenMalformedCommunityUrl_whenOverrideLoginServerIfNeeded_thenFallsBackToInstanceServer() {
val result = overrideLoginServerIfNeeded(loginServer, instanceServer, communityId, "not a valid uri ://")
assertEquals(instanceServer, result.toString())
}

@Test
fun test_givenEmptyInstanceServer_whenOverrideLoginServerIfNeeded_thenReturnsLoginServer() {
val result = overrideLoginServerIfNeeded(loginServer, "", null, null)
assertEquals(loginServer, result.toString())
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -545,7 +545,7 @@ public void testParseIdentityServiceResponse() throws JSONException {
@Test
public void testGetOpenIDToken() {
final String openIdToken = OAuth2.getOpenIDToken(TestCredentials.LOGIN_URL,
TestCredentials.CLIENT_ID, TestCredentials.REFRESH_TOKEN);
null, TestCredentials.CLIENT_ID, null, null, TestCredentials.REFRESH_TOKEN);
Assert.assertNotNull("OpenID token should not be null", openIdToken);
}

Expand Down
Loading