diff --git a/libs/SalesforceSDK/src/com/salesforce/androidsdk/accounts/UserAccount.java b/libs/SalesforceSDK/src/com/salesforce/androidsdk/accounts/UserAccount.java index f779bc212c..ac3bce9b0c 100644 --- a/libs/SalesforceSDK/src/com/salesforce/androidsdk/accounts/UserAccount.java +++ b/libs/SalesforceSDK/src/com/salesforce/androidsdk/accounts/UserAccount.java @@ -96,6 +96,7 @@ public class UserAccount { public static final String TOKEN_FORMAT = "tokenFormat"; public static final String BEACON_CHILD_CONSUMER_KEY = "beacon_child_consumer_key"; public static final String BEACON_CHILD_CONSUMER_SECRET = "beacon_child_consumer_secret"; + public static final String SCOPE = "scope"; private static final String TAG = "UserAccount"; private static final String FORWARD_SLASH = "/"; @@ -142,6 +143,7 @@ public class UserAccount { private Map additionalOauthValues; private String beaconChildConsumerKey; private String beaconChildConsumerSecret; + private String scope; /** * Parameterized constructor. @@ -182,6 +184,7 @@ public class UserAccount { * @param beaconChildConsumerKey beacon child consumer key * @param beaconChildConsumerSecret beacon child consumer secret * @param apiInstanceServer API instance server + * @param scope Scope */ UserAccount(String authToken, String refreshToken, String loginServer, String idUrl, String instanceServer, @@ -193,7 +196,7 @@ public class UserAccount { 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 beaconChildConsumerKey, String beaconChildConsumerSecret, String apiInstanceServer) { + String beaconChildConsumerKey, String beaconChildConsumerSecret, String apiInstanceServer, String scope) { this.authToken = authToken; this.refreshToken = refreshToken; this.loginServer = loginServer; @@ -231,6 +234,7 @@ public class UserAccount { this.tokenFormat = tokenFormat; this.beaconChildConsumerKey = beaconChildConsumerKey; this.beaconChildConsumerSecret = beaconChildConsumerSecret; + this.scope = scope; SalesforceSDKManager.getInstance().registerUsedAppFeature(Features.FEATURE_USER_AUTH); } @@ -281,6 +285,7 @@ public class UserAccount { tokenFormat = object.optString(TOKEN_FORMAT, null); beaconChildConsumerKey = object.optString(BEACON_CHILD_CONSUMER_KEY, null); beaconChildConsumerSecret = object.optString(BEACON_CHILD_CONSUMER_SECRET, null); + scope = object.optString(SCOPE, null); additionalOauthValues = MapUtil.addJSONObjectToMap(object, additionalOauthKeys, additionalOauthValues); } } @@ -337,6 +342,7 @@ public UserAccount(JSONObject object) { tokenFormat = bundle.getString(TOKEN_FORMAT); beaconChildConsumerKey = bundle.getString(BEACON_CHILD_CONSUMER_KEY); beaconChildConsumerSecret = bundle.getString(BEACON_CHILD_CONSUMER_SECRET); + scope = bundle.getString(SCOPE); additionalOauthValues = MapUtil.addBundleToMap(bundle, additionalOauthKeys, additionalOauthValues); } } @@ -667,7 +673,49 @@ public String getTokenFormat() { } /** - * Returns the beacon child consumer key . + * Returns the OAuth scopes returned by the token endpoint. + * + * @return scope string. + */ + public String getScope() { + return scope; + } + + /** + * Parses the space-delimited scope string into its individual components. + * + * @return Array of scope strings (empty if scope is null/empty). + */ + public String[] parseScopes() { + if (TextUtils.isEmpty(scope)) { + return new String[0]; + } + final String trimmed = scope.trim(); + if (trimmed.isEmpty()) { + return new String[0]; + } + return trimmed.split("\\s+"); + } + + /** + * Checks whether the provided scope exists in this account's scope list. + * + * @param scopeToCheck Scope name to check. + * @return True if present, false otherwise. + */ + public boolean hasScope(String scopeToCheck) { + if (TextUtils.isEmpty(scopeToCheck)) { + return false; + } + for (final String s : parseScopes()) { + if (scopeToCheck.equals(s)) { + return true; + } + } + return false; + } + /** + * Returns the beacon child consumer key. * * @return beacon child consumer key. */ @@ -956,6 +1004,7 @@ JSONObject toJson(List additionalOauthKeys) { object.put(TOKEN_FORMAT, tokenFormat); object.put(BEACON_CHILD_CONSUMER_KEY, beaconChildConsumerKey); object.put(BEACON_CHILD_CONSUMER_SECRET, beaconChildConsumerSecret); + object.put(SCOPE, scope); object = MapUtil.addMapToJSONObject(additionalOauthValues, additionalOauthKeys, object); } catch (JSONException e) { SalesforceSDKLogger.e(TAG, "Unable to convert to JSON", e); @@ -1016,6 +1065,7 @@ Bundle toBundle(List additionalOauthKeys) { object.putString(TOKEN_FORMAT, tokenFormat); object.putString(BEACON_CHILD_CONSUMER_KEY, beaconChildConsumerKey); object.putString(BEACON_CHILD_CONSUMER_SECRET, beaconChildConsumerSecret); + object.putString(SCOPE, scope); object = MapUtil.addMapToBundle(additionalOauthValues, additionalOauthKeys, object); return object; } diff --git a/libs/SalesforceSDK/src/com/salesforce/androidsdk/accounts/UserAccountBuilder.kt b/libs/SalesforceSDK/src/com/salesforce/androidsdk/accounts/UserAccountBuilder.kt index 96a04aa6cc..1bdc7539cb 100644 --- a/libs/SalesforceSDK/src/com/salesforce/androidsdk/accounts/UserAccountBuilder.kt +++ b/libs/SalesforceSDK/src/com/salesforce/androidsdk/accounts/UserAccountBuilder.kt @@ -71,6 +71,7 @@ class UserAccountBuilder private constructor() { private var allowUnset = true private var beaconChildConsumerKey: String? = null private var beaconChildConsumerSecret: String? = null + private var scope: String? = null /** * Set fields from token end point response @@ -106,6 +107,7 @@ class UserAccountBuilder private constructor() { .tokenFormat(tr.tokenFormat) .beaconChildConsumerKey(tr.beaconChildConsumerKey) .beaconChildConsumerSecret(tr.beaconChildConsumerSecret) + .scope(tr.scope) } /** @@ -173,6 +175,7 @@ class UserAccountBuilder private constructor() { .tokenFormat(userAccount.tokenFormat) .beaconChildConsumerKey(userAccount.beaconChildConsumerKey) .beaconChildConsumerSecret(userAccount.beaconChildConsumerSecret) + .scope(userAccount.scope) } /** @@ -572,6 +575,16 @@ class UserAccountBuilder private constructor() { return if (!allowUnset && beaconChildConsumerSecret == null) this else apply { this.beaconChildConsumerSecret = beaconChildConsumerSecret } } + /** + * Sets scope returned by token endpoint. + * + * @param scope OAuth scopes string. + * @return Instance of this class. + */ + fun scope(scope: String?): UserAccountBuilder { + return if (!allowUnset && scope == null) this else apply { this.scope = scope } + } + /** * Builds and returns a UserAccount object. * @@ -616,6 +629,7 @@ class UserAccountBuilder private constructor() { beaconChildConsumerKey, beaconChildConsumerSecret, apiInstanceServer, + scope, ) } diff --git a/libs/SalesforceSDK/src/com/salesforce/androidsdk/accounts/UserAccountManager.java b/libs/SalesforceSDK/src/com/salesforce/androidsdk/accounts/UserAccountManager.java index 8b72af7f86..15255dd677 100644 --- a/libs/SalesforceSDK/src/com/salesforce/androidsdk/accounts/UserAccountManager.java +++ b/libs/SalesforceSDK/src/com/salesforce/androidsdk/accounts/UserAccountManager.java @@ -539,6 +539,7 @@ public Bundle updateAccount(Account account, UserAccount userAccount) { final String tokenFormat = decryptUserData(account, AuthenticatorService.KEY_TOKEN_FORMAT, encryptionKey); 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); Map additionalOauthValues = null; List additionalOauthKeys = SalesforceSDKManager.getInstance().getAdditionalOauthKeys(); @@ -593,6 +594,7 @@ public Bundle updateAccount(Account account, UserAccount userAccount) { .tokenFormat(tokenFormat) .beaconChildConsumerKey(beaconChildConsumerKey) .beaconChildConsumerSecret(beaconChildConsumerSecret) + .scope(scope) .additionalOauthValues(additionalOauthValues) .build(); } @@ -742,6 +744,7 @@ private Bundle buildAuthBundle(UserAccount userAccount) { extras.putString(AuthenticatorService.KEY_TOKEN_FORMAT, SalesforceSDKManager.encrypt(userAccount.getTokenFormat(), encryptionKey)); 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)); final List additionalOauthKeys = SalesforceSDKManager.getInstance().getAdditionalOauthKeys(); if (additionalOauthKeys != null && !additionalOauthKeys.isEmpty()) { diff --git a/libs/SalesforceSDK/src/com/salesforce/androidsdk/auth/AuthenticatorService.java b/libs/SalesforceSDK/src/com/salesforce/androidsdk/auth/AuthenticatorService.java index beb9888957..01decc97e6 100644 --- a/libs/SalesforceSDK/src/com/salesforce/androidsdk/auth/AuthenticatorService.java +++ b/libs/SalesforceSDK/src/com/salesforce/androidsdk/auth/AuthenticatorService.java @@ -89,6 +89,7 @@ public class AuthenticatorService extends Service { public static final String KEY_TOKEN_FORMAT = "tokenFormat"; public static final String KEY_BEACON_CHILD_CONSUMER_KEY = "beacon_child_consumer_key"; public static final String KEY_BEACON_CHILD_CONSUMER_SECRET = "beacon_child_consumer_secret"; + public static final String KEY_SCOPE = "scope"; private static final String TAG = "AuthenticatorService"; diff --git a/libs/SalesforceSDK/src/com/salesforce/androidsdk/auth/OAuth2.java b/libs/SalesforceSDK/src/com/salesforce/androidsdk/auth/OAuth2.java index 7962a882fb..d7c207efce 100644 --- a/libs/SalesforceSDK/src/com/salesforce/androidsdk/auth/OAuth2.java +++ b/libs/SalesforceSDK/src/com/salesforce/androidsdk/auth/OAuth2.java @@ -842,6 +842,7 @@ public static class TokenEndpointResponse { public String tokenFormat; public String beaconChildConsumerKey; public String beaconChildConsumerSecret; + public String scope; /** * Parameterized constructor built from params during user agent login flow. @@ -882,6 +883,7 @@ public TokenEndpointResponse(Map callbackUrlParams, List sidCookieName = callbackUrlParams.get(SID_COOKIE_NAME); parentSid = callbackUrlParams.get(PARENT_SID); tokenFormat = callbackUrlParams.get(TOKEN_FORMAT); + scope = callbackUrlParams.get(SCOPE); // NB: beacon apps not supported with user agent flow so no beacon child fields expected @@ -959,6 +961,7 @@ public TokenEndpointResponse(Response response, List additionalOauthKeys if (parsedResponse.has(BEACON_CHILD_CONSUMER_SECRET)) { beaconChildConsumerSecret = parsedResponse.getString(BEACON_CHILD_CONSUMER_SECRET); } + scope = parsedResponse.optString(SCOPE); } catch (Exception e) { SalesforceSDKLogger.w(TAG, "Could not parse token endpoint response", e); diff --git a/libs/test/SalesforceSDKTest/src/com/salesforce/androidsdk/accounts/UserAccountTest.java b/libs/test/SalesforceSDKTest/src/com/salesforce/androidsdk/accounts/UserAccountTest.java index b1c5ecce5d..205bddded4 100644 --- a/libs/test/SalesforceSDKTest/src/com/salesforce/androidsdk/accounts/UserAccountTest.java +++ b/libs/test/SalesforceSDKTest/src/com/salesforce/androidsdk/accounts/UserAccountTest.java @@ -104,12 +104,14 @@ public class UserAccountTest { public static final String TEST_TOKEN_FORMAT = "test-token-format"; public static final String TEST_BEACON_CHILD_CONSUMER_KEY = "test-beacon-child-consumer-key"; public static final String TEST_BEACON_CHILD_CONSUMER_SECRET = "test-beacon-child-consumer-secret"; + public static final String TEST_SCOPE = "api web openid refresh_token"; // other user public static final String TEST_ORG_ID_2 = "test_org_id_2"; public static final String TEST_USER_ID_2 = "test_user_id_2"; public static final String TEST_ACCOUNT_NAME_2 = "test_username_2 (https://cs1.salesforce.com) (SalesforceSDKTest)"; public static final String TEST_USERNAME_2 = "test_username_2"; + public static final String TEST_SCOPE_2 = "api web refresh_token sfap_api"; private EventsListenerQueue eq; @@ -125,6 +127,43 @@ public void testConvertAccountToBundle() { BundleTestHelper.checkSameBundle("UserAccount bundles do not match", expected, actual); } + @Test + public void testParseScopes() { + UserAccount account = createTestAccount(); + String[] scopes = account.parseScopes(); + // Our TEST_SCOPE is "api web openid refresh_token" + Assert.assertArrayEquals(new String[]{"api", "web", "openid", "refresh_token"}, scopes); + + // Empty / null handling + UserAccount emptyScope = UserAccountBuilder.getInstance() + .populateFromUserAccount(account) + .scope(null) + .build(); + Assert.assertArrayEquals(new String[]{}, emptyScope.parseScopes()); + + UserAccount blankScope = UserAccountBuilder.getInstance() + .populateFromUserAccount(account) + .scope(" ") + .build(); + Assert.assertArrayEquals(new String[]{}, blankScope.parseScopes()); + } + + @Test + public void testHasScope() { + UserAccount account = createTestAccount(); + Assert.assertTrue(account.hasScope("api")); + Assert.assertTrue(account.hasScope("web")); + Assert.assertTrue(account.hasScope("openid")); + Assert.assertTrue(account.hasScope("refresh_token")); + Assert.assertFalse(account.hasScope("unknown")); + + UserAccount emptyScope = UserAccountBuilder.getInstance() + .populateFromUserAccount(account) + .scope("") + .build(); + Assert.assertFalse(emptyScope.hasScope("api")); + } + /** * Tests user account to json conversion. */ @@ -203,6 +242,7 @@ public void testPopulateFromUserAccount() { .orgId(TEST_ORG_ID_2) .username(TEST_USERNAME_2) .accountName(TEST_ACCOUNT_NAME_2) + .scope(TEST_SCOPE_2) .build(); checkOtherTestAccount(otherUserAccount); } @@ -403,6 +443,7 @@ private JSONObject createTestAccountJSON() throws JSONException{ object.put(UserAccount.SID_COOKIE_NAME, TEST_SID_COOKIE_NAME); object.put(UserAccount.PARENT_SID, TEST_PARENT_SID); object.put(UserAccount.TOKEN_FORMAT, TEST_TOKEN_FORMAT); + object.put(UserAccount.SCOPE, TEST_SCOPE); object.put(UserAccount.BEACON_CHILD_CONSUMER_KEY, TEST_BEACON_CHILD_CONSUMER_KEY); object.put(UserAccount.BEACON_CHILD_CONSUMER_SECRET, TEST_BEACON_CHILD_CONSUMER_SECRET); object = MapUtil.addMapToJSONObject(createAdditionalOauthValues(), createAdditionalOauthKeys(), object); @@ -452,6 +493,7 @@ private Bundle createTestAccountBundle() { object.putString(UserAccount.TOKEN_FORMAT, TEST_TOKEN_FORMAT); object.putString(UserAccount.BEACON_CHILD_CONSUMER_KEY, TEST_BEACON_CHILD_CONSUMER_KEY); object.putString(UserAccount.BEACON_CHILD_CONSUMER_SECRET, TEST_BEACON_CHILD_CONSUMER_SECRET); + object.putString(UserAccount.SCOPE, TEST_SCOPE); object = MapUtil.addMapToBundle(createAdditionalOauthValues(), createAdditionalOauthKeys(), object); return object; } @@ -497,6 +539,7 @@ public static UserAccount createTestAccount() { .tokenFormat(TEST_TOKEN_FORMAT) .beaconChildConsumerKey(TEST_BEACON_CHILD_CONSUMER_KEY) .beaconChildConsumerSecret(TEST_BEACON_CHILD_CONSUMER_SECRET) + .scope(TEST_SCOPE) .additionalOauthValues(createAdditionalOauthValues()) .build(); } @@ -511,6 +554,7 @@ public static UserAccount createOtherTestAccount() { .orgId(TEST_ORG_ID_2) .username(TEST_USERNAME_2) .accountName(TEST_ACCOUNT_NAME_2) + .scope(TEST_SCOPE_2) .build(); } @@ -567,6 +611,7 @@ void checkTestAccount(UserAccount account, boolean expectBeaconChildFields) { Assert.assertNull("Beacon child consumer key should be null", account.getBeaconChildConsumerKey()); Assert.assertNull("Beacon child consumer secret should be null", account.getBeaconChildConsumerSecret()); } + Assert.assertEquals("Scope should match", TEST_SCOPE, account.getScope()); Assert.assertEquals("Additional OAuth values should match", createAdditionalOauthValues(), account.getAdditionalOauthValues()); } @@ -609,6 +654,7 @@ void checkOtherTestAccount(UserAccount account) { Assert.assertEquals("Token format should match", TEST_TOKEN_FORMAT, account.getTokenFormat()); Assert.assertEquals("Beacon child consumer key should match", TEST_BEACON_CHILD_CONSUMER_KEY, account.getBeaconChildConsumerKey()); Assert.assertEquals("Beacon child consumer secret should match", TEST_BEACON_CHILD_CONSUMER_SECRET, account.getBeaconChildConsumerSecret()); + Assert.assertEquals("Scope should match", TEST_SCOPE_2, account.getScope()); Assert.assertEquals("Additional OAuth values should match", createAdditionalOauthValues(), account.getAdditionalOauthValues()); } @@ -669,6 +715,7 @@ private Map createTokenEndpointParams() { params.put("sidCookieName", TEST_SID_COOKIE_NAME); params.put("parent_sid", TEST_PARENT_SID); params.put("token_format", TEST_TOKEN_FORMAT); + params.put("scope", TEST_SCOPE); return params; }