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
3 changes: 2 additions & 1 deletion .github/DangerFiles/TestOrchestrator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
warn("Big PR, try to keep changes smaller if you can.", sticky: true) if git.lines_of_code > 1000

# Redirect contributors to PR to dev.
fail("Please re-submit this PR to the dev branch, we may have already fixed your issue.", sticky: true) if github.branch_for_base != "dev"
# dpop is a temporary exception for the multi-PR DPoP rollout. Remove once DPoP merges back to dev.
fail("Please re-submit this PR to the dev branch, we may have already fixed your issue.", sticky: true) if !["dev", "dpop"].include?(github.branch_for_base)

# List of Android libraries for testing
LIBS = ['SalesforceAnalytics', 'SalesforceSDK', 'SmartStore', 'MobileSync', 'SalesforceHybrid']
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -556,6 +556,8 @@ public Bundle updateAccount(Account account, UserAccount userAccount) {
final String beaconChildConsumerKey = decryptUserData(account, AuthenticatorService.KEY_BEACON_CHILD_CONSUMER_KEY, encryptionKey);
final String beaconChildConsumerSecret = decryptUserData(account, AuthenticatorService.KEY_BEACON_CHILD_CONSUMER_SECRET, encryptionKey);
final String scope = decryptUserData(account, AuthenticatorService.KEY_SCOPE, encryptionKey);
final String credentialsIdentifier = decryptUserData(account, AuthenticatorService.KEY_CREDENTIALS_IDENTIFIER, encryptionKey);
final String tokenType = decryptUserData(account, AuthenticatorService.KEY_TOKEN_TYPE, encryptionKey);
final String featureFlagsRaw = decryptUserData(account, AuthenticatorService.KEY_FEATURE_FLAGS, encryptionKey);

Map<String, String> additionalOauthValues = null;
Expand Down Expand Up @@ -612,6 +614,8 @@ public Bundle updateAccount(Account account, UserAccount userAccount) {
.beaconChildConsumerKey(beaconChildConsumerKey)
.beaconChildConsumerSecret(beaconChildConsumerSecret)
.scope(scope)
.credentialsIdentifier(credentialsIdentifier)
.tokenType(tokenType)
.additionalOauthValues(additionalOauthValues)
.build();
if (!TextUtils.isEmpty(featureFlagsRaw)) {
Expand Down Expand Up @@ -766,6 +770,12 @@ private Bundle buildAuthBundle(UserAccount userAccount) {
extras.putString(AuthenticatorService.KEY_BEACON_CHILD_CONSUMER_KEY, SalesforceSDKManager.encrypt(userAccount.getBeaconChildConsumerKey(), encryptionKey));
extras.putString(AuthenticatorService.KEY_BEACON_CHILD_CONSUMER_SECRET, SalesforceSDKManager.encrypt(userAccount.getBeaconChildConsumerSecret(), encryptionKey));
extras.putString(AuthenticatorService.KEY_SCOPE, SalesforceSDKManager.encrypt(userAccount.getScope(), encryptionKey));
if (userAccount.getCredentialsIdentifier() != null) {
extras.putString(AuthenticatorService.KEY_CREDENTIALS_IDENTIFIER, SalesforceSDKManager.encrypt(userAccount.getCredentialsIdentifier(), encryptionKey));
}
if (userAccount.getTokenType() != null) {
extras.putString(AuthenticatorService.KEY_TOKEN_TYPE, SalesforceSDKManager.encrypt(userAccount.getTokenType(), encryptionKey));
}
final Set<String> featureFlags = userAccount.getFeatureFlags();
if (!featureFlags.isEmpty()) {
extras.putString(AuthenticatorService.KEY_FEATURE_FLAGS,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,8 @@ private suspend fun fetchUserIdentity(
HttpAccess.DEFAULT,
tokenResponse.idUrlWithInstance,
tokenResponse.authToken,
tokenResponse.tokenType,
tokenResponse.credentialsIdentifier,
)
}
}.onFailure { throwable ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,8 @@ public class AuthenticatorService extends Service {
public static final String KEY_BEACON_CHILD_CONSUMER_SECRET = "auto_installed_app_org_consumer_secret";
public static final String KEY_SCOPE = "scope";
public static final String KEY_FEATURE_FLAGS = "feature_flags";
public static final String KEY_CREDENTIALS_IDENTIFIER = "credentialsIdentifier";
public static final String KEY_TOKEN_TYPE = "tokenType";

private static final String TAG = "AuthenticatorService";

Expand Down
40 changes: 38 additions & 2 deletions libs/SalesforceSDK/src/com/salesforce/androidsdk/auth/OAuth2.java
Original file line number Diff line number Diff line change
Expand Up @@ -598,8 +598,41 @@ public static IdServiceResponse callIdentityService(HttpAccess httpAccessor,
String identityServiceIdUrl,
String authToken)
throws IOException {
return callIdentityService(httpAccessor, identityServiceIdUrl, authToken, null, null);
}

/**
* Calls the identity service to determine the username of the user and the mobile policy, given
* their identity service ID and an access token. When the token is DPoP-bound, attaches a DPoP
* proof header.
*
* @param httpAccessor HttpAccessor instance.
* @param identityServiceIdUrl Identity service URL.
* @param authToken Access token.
* @param tokenType Token type (e.g. "Bearer" or "DPoP"), or null for default Bearer.
* @param credentialsIdentifier Identifier used to look up the DPoP keypair, or null.
* @return IdServiceResponse instance.
*
* @throws IOException See {@link IOException}.
*/
public static IdServiceResponse callIdentityService(HttpAccess httpAccessor,
String identityServiceIdUrl,
String authToken,
@Nullable String tokenType,
@Nullable String credentialsIdentifier)
throws IOException {
final Request.Builder builder = new Request.Builder().url(identityServiceIdUrl).get();
addAuthorizationHeader(builder, authToken);
addAuthorizationHeader(builder, authToken, tokenType);
if (DPOP.equals(tokenType) && credentialsIdentifier != null && SalesforceSDKManager.getInstance().isUseDPoP()) {
// Fail-closed: a DPoP-bound identity request without a proof header will be rejected
// by the server regardless, so surface the error rather than sending an unusable request.
// This matches iOS SFIdentityCoordinator behaviour (commit 97a62410a).
final String htu = DPoPURLHelper.INSTANCE.canonicalize(identityServiceIdUrl);
final String alias = DPoPKeyManager.INSTANCE.aliasForCredentialsIdentifier(credentialsIdentifier);
final KeyPair keyPair = DPoPKeyManager.INSTANCE.generateOrLoadKeyPair(alias);
final String proof = DPoPProofBuilder.INSTANCE.buildProof("GET", htu, keyPair, null, authToken);
builder.header(DPOP, proof);
}
final Request request = builder.build();
final Response response = httpAccessor.getOkHttpClient().newCall(request).execute();
return new IdServiceResponse(response);
Expand Down Expand Up @@ -683,7 +716,9 @@ public static TokenEndpointResponse makeTokenEndpointRequest(HttpAccess httpAcce
final Request request = requestBuilder.build();
final Response response = httpAccessor.getOkHttpClient().newCall(request).execute();
if (response.isSuccessful()) {
return new TokenEndpointResponse(response);
final TokenEndpointResponse tokenResponse = new TokenEndpointResponse(response);
tokenResponse.credentialsIdentifier = credentialsIdentifier;
return tokenResponse;
} else {
throw new OAuthFailedException(new TokenErrorResponse(response), response.code());
}
Expand Down Expand Up @@ -1031,6 +1066,7 @@ public static class TokenEndpointResponse {
public String beaconChildConsumerSecret;
public String scope;
public String tokenType;
public String credentialsIdentifier;

/**
* Parameterized constructor built from params during user agent login flow.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ package com.salesforce.androidsdk.auth.dpop
import android.util.Base64
import org.json.JSONObject
import java.security.KeyPair
import java.security.MessageDigest
import java.security.SecureRandom
import java.security.Signature
import java.security.interfaces.ECPublicKey
Expand All @@ -45,7 +46,7 @@ object DPoPProofBuilder {
val publicKey = keyPair.public as ECPublicKey
val jwk = buildJwk(publicKey)
val header = buildHeader(jwk)
val payload = buildPayload(httpMethod, htu)
val payload = buildPayload(httpMethod, htu, nonce, accessToken)
val headerEncoded = base64url(header.toString().toByteArray(Charsets.UTF_8))
val payloadEncoded = base64url(payload.toString().toByteArray(Charsets.UTF_8))
val signingInput = "$headerEncoded.$payloadEncoded"
Expand Down Expand Up @@ -73,13 +74,18 @@ object DPoPProofBuilder {
put("jwk", jwk)
}

private fun buildPayload(httpMethod: String, htu: String): JSONObject {
private fun buildPayload(httpMethod: String, htu: String, nonce: String?, accessToken: String?): JSONObject {
val jti = ByteArray(12).also { SecureRandom().nextBytes(it) }
return JSONObject().apply {
put("htm", httpMethod.uppercase())
put("htu", htu)
put("iat", System.currentTimeMillis() / 1000L)
put("jti", base64url(jti))
if (!nonce.isNullOrEmpty()) put("nonce", nonce)
if (!accessToken.isNullOrEmpty()) {
val digest = MessageDigest.getInstance("SHA-256").digest(accessToken.toByteArray(Charsets.UTF_8))
put("ath", base64url(digest))
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,7 @@ public RestClient peekRestClient(Account acc) {
userAccount.getUserId(), userAccount.getOrgId(), userAccount.getCommunityId(), userAccount.getCommunityUrl(),
userAccount.getFirstName(), userAccount.getLastName(), userAccount.getDisplayName(), userAccount.getEmail(), userAccount.getPhotoUrl(), userAccount.getThumbnailUrl(), userAccount.getAdditionalOauthValues(),
userAccount.getLightningDomain(), userAccount.getLightningSid(), userAccount.getVFDomain(), userAccount.getVFSid(), userAccount.getContentDomain(), userAccount.getContentSid(), userAccount.getCSRFToken());
return new RestClient(clientInfo, userAccount.getAuthToken(), HttpAccess.DEFAULT, authTokenProvider);
return new RestClient(clientInfo, userAccount.getAuthToken(), userAccount.getTokenType(), userAccount.getCredentialsIdentifier(), HttpAccess.DEFAULT, authTokenProvider);
} catch (URISyntaxException e) {
SalesforceSDKLogger.w(TAG, "Invalid server URL", e);
throw new AccountInfoNotFoundException("invalid server url", e);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@
import com.salesforce.androidsdk.app.SalesforceSDKManager;
import com.salesforce.androidsdk.auth.HttpAccess;
import com.salesforce.androidsdk.auth.OAuth2;
import com.salesforce.androidsdk.auth.dpop.DPoPKeyManager;
import com.salesforce.androidsdk.auth.dpop.DPoPProofBuilder;
import com.salesforce.androidsdk.auth.dpop.DPoPURLHelper;
import com.salesforce.androidsdk.security.BiometricAuthenticationManager;
import com.salesforce.androidsdk.util.SalesforceSDKLogger;

Expand Down Expand Up @@ -69,6 +72,7 @@ public class RestClient {
private static final String COMMUNITY_ID = "communityId";
private static final String COMMUNITY_URL = "communityUrl";
private static final String TAG = "RestClient";
private static final String DPOP = "DPoP";

private static final Map<String, OAuthRefreshInterceptor> OAUTH_REFRESH_INTERCEPTORS = new HashMap<>();
private static final Map<String, OkHttpClient.Builder> OK_CLIENT_BUILDERS = new HashMap<>();
Expand Down Expand Up @@ -160,10 +164,26 @@ public RestClient(ClientInfo clientInfo, String authToken, HttpAccess httpAccess
* @param authTokenProvider
*/
public RestClient(ClientInfo clientInfo, String authToken, String tokenType, HttpAccess httpAccessor, AuthTokenProvider authTokenProvider) {
this(clientInfo, authToken, tokenType, null, httpAccessor, authTokenProvider);
}

/**
* Constructs a RestClient with the given clientInfo, authToken, tokenType, credentialsIdentifier,
* httpAccessor and authTokenProvider. The tokenType determines the Authorization header scheme
* (e.g. "Bearer" or "DPoP"). The credentialsIdentifier is used to look up the DPoP key pair.
*
* @param clientInfo
* @param authToken
* @param tokenType
* @param credentialsIdentifier
* @param httpAccessor
* @param authTokenProvider
*/
public RestClient(ClientInfo clientInfo, String authToken, String tokenType, String credentialsIdentifier, HttpAccess httpAccessor, AuthTokenProvider authTokenProvider) {
this.clientInfo = clientInfo;
this.httpAccessor = httpAccessor;
this.authTokenProvider = authTokenProvider;
setOAuthRefreshInterceptor(authToken, tokenType);
setOAuthRefreshInterceptor(authToken, tokenType, credentialsIdentifier);
setOkHttpClientBuilder();
setOkHttpClient(null);
}
Expand Down Expand Up @@ -204,12 +224,19 @@ private static String computeCacheKey(String orgId, String userId) {
* Sets the OAuthRefreshInterceptor associated with this user account.
*/
private synchronized void setOAuthRefreshInterceptor(String authToken, String tokenType) {
setOAuthRefreshInterceptor(authToken, tokenType, null);
}

/**
* Sets the OAuthRefreshInterceptor associated with this user account.
*/
private synchronized void setOAuthRefreshInterceptor(String authToken, String tokenType, String credentialsIdentifier) {
final String cacheKey = getCacheKey();
OAuthRefreshInterceptor oAuthRefreshInterceptor = OAUTH_REFRESH_INTERCEPTORS.get(cacheKey);

// If none cached, create new one
if (oAuthRefreshInterceptor == null) {
oAuthRefreshInterceptor = new OAuthRefreshInterceptor(clientInfo, authToken, tokenType, authTokenProvider);
oAuthRefreshInterceptor = new OAuthRefreshInterceptor(clientInfo, authToken, tokenType, credentialsIdentifier, authTokenProvider);
OAUTH_REFRESH_INTERCEPTORS.put(cacheKey, oAuthRefreshInterceptor);
}
this.oAuthRefreshInterceptor = oAuthRefreshInterceptor;
Expand Down Expand Up @@ -380,6 +407,7 @@ public Request buildRequest(RestRequest restRequest) {
builder.addHeader(entry.getKey(), entry.getValue());
}
}

return builder.build();
}

Expand Down Expand Up @@ -724,7 +752,8 @@ public static class OAuthRefreshInterceptor implements Interceptor {

private final AuthTokenProvider authTokenProvider;
private String authToken;
private String tokenType;
String tokenType;
String credentialsIdentifier;
private ClientInfo clientInfo;

/**
Expand Down Expand Up @@ -753,9 +782,23 @@ public OAuthRefreshInterceptor(ClientInfo clientInfo, String authToken, AuthToke
* @param authTokenProvider
*/
public OAuthRefreshInterceptor(ClientInfo clientInfo, String authToken, String tokenType, AuthTokenProvider authTokenProvider) {
this(clientInfo, authToken, tokenType, null, authTokenProvider);
}

/**
* Overload that accepts a token type and credentials identifier for DPoP key lookup.
*
* @param clientInfo
* @param authToken
* @param tokenType
* @param credentialsIdentifier
* @param authTokenProvider
*/
public OAuthRefreshInterceptor(ClientInfo clientInfo, String authToken, String tokenType, String credentialsIdentifier, AuthTokenProvider authTokenProvider) {
this.clientInfo = clientInfo;
this.authToken = authToken;
this.tokenType = tokenType;
this.credentialsIdentifier = credentialsIdentifier;
this.authTokenProvider = authTokenProvider;
}

Expand Down Expand Up @@ -860,9 +903,25 @@ private Request adjustHostInRequest(Request request, final String host) {
private Request buildAuthenticatedRequest(Request request) {
Request.Builder builder = request.newBuilder();
setAuthHeader(builder);
attachDPoPProofIfNeeded(builder, request.method(), request.url().toString());
return builder.build();
}

private void attachDPoPProofIfNeeded(Request.Builder builder, String method, String url) {
if (!DPOP.equals(tokenType)) return;
if (!SalesforceSDKManager.getInstance().isUseDPoP()) return;
if (credentialsIdentifier == null || credentialsIdentifier.isEmpty()) return;
try {
final String htu = DPoPURLHelper.INSTANCE.canonicalize(url);
final String alias = DPoPKeyManager.INSTANCE.aliasForCredentialsIdentifier(credentialsIdentifier);
final java.security.KeyPair keyPair = DPoPKeyManager.INSTANCE.generateOrLoadKeyPair(alias);
final String proof = DPoPProofBuilder.INSTANCE.buildProof(method, htu, keyPair, null, authToken);
builder.header(DPOP, proof);
} catch (Exception e) {
SalesforceSDKLogger.e(TAG, "Failed to attach DPoP header in interceptor, proceeding without it", e);
}
}

/**
* @return The authToken for this RestClient.
*/
Expand Down
Loading
Loading