diff --git a/changelog.txt b/changelog.txt index 7b728f8c96..8f1335a49c 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,4 +1,9 @@ -vNext +Version 23.1.1 +---------- +- [PATCH] Share SharedPreferencesInMemoryCache across instances of BrokerOAuth2TokenCache (#2813) +- [PATCH] Use SharedPreferencesInMemoryCache implementation in Broker (#2802) + +Version 23.1.0 ---------- - [MINOR] Add OpenTelemetry support for passkey operations (#2795) - [MINOR] Add passkey registration support for WebView (#2769) @@ -1193,4 +1198,4 @@ Version 0.0.3 * Separate methods for PII/OII logging - Initial Exception model implemented * BaseException + Client & Service subclasses -- Substantial portions of HTTP/S networking code migrated from ADAL & MSAL to this module \ No newline at end of file +- Substantial portions of HTTP/S networking code migrated from ADAL & MSAL to this module diff --git a/common/build.gradle b/common/build.gradle index d58d1dbe5b..59337f3b4a 100644 --- a/common/build.gradle +++ b/common/build.gradle @@ -40,7 +40,7 @@ tasks.withType(Test) { // In dev, we want to keep the dependencies(common4j, broker4j, common) to 1.0.+ to be able to be consumed by daily dev pipeline. // In release/*, we change these to specific versions being consumed. -def common4jVersion = "1.0.+" +def common4jVersion = "23.1.1" if (project.hasProperty("distCommon4jVersion") && project.distCommon4jVersion != '') { common4jVersion = project.distCommon4jVersion } diff --git a/common/src/test/java/com/microsoft/identity/common/BrokerOAuth2TokenCacheTest.java b/common/src/test/java/com/microsoft/identity/common/BrokerOAuth2TokenCacheTest.java index 4390454ab4..d80577195e 100644 --- a/common/src/test/java/com/microsoft/identity/common/BrokerOAuth2TokenCacheTest.java +++ b/common/src/test/java/com/microsoft/identity/common/BrokerOAuth2TokenCacheTest.java @@ -42,16 +42,17 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNotSame; import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import static org.mockito.Mockito.when; -import android.content.Context; +import androidx.annotation.NonNull; import androidx.test.core.app.ApplicationProvider; -import com.microsoft.identity.common.components.AndroidPlatformComponentsFactory; import com.microsoft.identity.common.components.MockPlatformComponentsFactory; import com.microsoft.identity.common.internal.platform.AndroidPlatformUtil; import com.microsoft.identity.common.java.cache.BrokerApplicationMetadata; @@ -66,10 +67,15 @@ import com.microsoft.identity.common.java.cache.SharedPreferencesAccountCredentialCache; import com.microsoft.identity.common.java.cache.AccountDeletionRecord; import com.microsoft.identity.common.java.cache.ICacheRecord; +import com.microsoft.identity.common.java.cache.SharedPreferencesAccountCredentialCacheWithMemoryCache; import com.microsoft.identity.common.java.dto.AccountRecord; import com.microsoft.identity.common.java.dto.Credential; import com.microsoft.identity.common.java.dto.CredentialType; import com.microsoft.identity.common.java.exception.ClientException; +import com.microsoft.identity.common.java.flighting.CommonFlight; +import com.microsoft.identity.common.java.flighting.CommonFlightsManager; +import com.microsoft.identity.common.java.flighting.IFlightsManager; +import com.microsoft.identity.common.java.flighting.IFlightsProvider; import com.microsoft.identity.common.java.interfaces.INameValueStorage; import com.microsoft.identity.common.java.interfaces.IPlatformComponents; import com.microsoft.identity.common.java.providers.microsoft.MicrosoftAccount; @@ -79,14 +85,15 @@ import com.microsoft.identity.common.java.providers.oauth2.OAuth2TokenCache; import com.microsoft.identity.common.shadows.ShadowAndroidSdkStorageEncryptionManager; +import org.jetbrains.annotations.NotNull; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.Mockito; import org.powermock.api.mockito.PowerMockito; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; -import org.robolectric.shadows.ShadowSharedPreferences; import java.util.ArrayList; import java.util.List; @@ -236,6 +243,7 @@ public void tearDown() throws Exception { } mApplicationMetadataCache.clear(); + CommonFlightsManager.INSTANCE.resetFlightsManager(); } private void initOtherCaches(final IPlatformComponents components) { @@ -1241,4 +1249,120 @@ public void testClearAll() throws ClientException { assertEquals(false, mBrokerOAuth2TokenCache.isClientIdKnownToCache(clientId)); } } + + @Test + public void testSingleCacheInstancePerStoreName_FlightEnabled() { + // Enable the flight + updateUseInMemoryCacheFlight(true); + + final String storeName = "test_store_name"; + final IPlatformComponents components1 = mPlatformComponents; + final IPlatformComponents components2 = mPlatformComponents; + + // Call getCacheToBeUsed twice with the same storeName + final IAccountCredentialCache cache1 = BrokerOAuth2TokenCache.getCacheToBeUsed(components1, storeName); + final IAccountCredentialCache cache2 = BrokerOAuth2TokenCache.getCacheToBeUsed(components2, storeName); + + // Verify both references point to the same instance + assertNotNull(cache1); + assertNotNull(cache2); + assertSame("Expected same cache instance for same storeName", cache1, cache2); + assertTrue("Cache should be of type SharedPreferencesAccountCredentialCacheWithMemoryCache", + cache1 instanceof SharedPreferencesAccountCredentialCacheWithMemoryCache); + } + + @Test + public void testDifferentCacheInstancesPerStoreName_FlightEnabled() { + // Enable the flight + updateUseInMemoryCacheFlight(true); + + final String storeName1 = "test_store_name_1"; + final String storeName2 = "test_store_name_2"; + final IPlatformComponents components = mPlatformComponents; + + // Call getCacheToBeUsed with different storeNames + final IAccountCredentialCache cache1 = BrokerOAuth2TokenCache.getCacheToBeUsed(components, storeName1); + final IAccountCredentialCache cache2 = BrokerOAuth2TokenCache.getCacheToBeUsed(components, storeName2); + + // Verify both are valid but different instances + assertNotNull(cache1); + assertNotNull(cache2); + assertNotSame("Expected different cache instances for different storeNames", cache1, cache2); + assertTrue("Cache should be of type SharedPreferencesAccountCredentialCacheWithMemoryCache", + cache1 instanceof SharedPreferencesAccountCredentialCacheWithMemoryCache); + assertTrue("Cache should be of type SharedPreferencesAccountCredentialCacheWithMemoryCache", + cache2 instanceof SharedPreferencesAccountCredentialCacheWithMemoryCache); + } + + @Test + public void testCacheInstanceReusedAcrossMultipleBrokerTokenCaches_FlightEnabled() { + // Enable the flight + updateUseInMemoryCacheFlight(true); + + final String storeName = getBrokerUidSequesteredFilename(TEST_APP_UID); + + // Create multiple BrokerOAuth2TokenCache instances + final BrokerOAuth2TokenCache tokenCache1 = new BrokerOAuth2TokenCache + (mPlatformComponents, + TEST_APP_UID, + new NameValueStorageBrokerApplicationMetadataCache(mPlatformComponents)); + final BrokerOAuth2TokenCache tokenCache2 = new BrokerOAuth2TokenCache(mPlatformComponents, TEST_APP_UID, + new NameValueStorageBrokerApplicationMetadataCache(mPlatformComponents)); + + // Get the underlying account credential caches + final IAccountCredentialCache cache1 = tokenCache1.getCacheToBeUsed(mPlatformComponents, storeName); + final IAccountCredentialCache cache2 = tokenCache2.getCacheToBeUsed(mPlatformComponents, storeName); + + // Verify same instance is reused + assertNotNull(cache1); + assertNotNull(cache2); + assertSame(cache1, cache2); + } + + @Test + public void testFociCacheInstanceReused_FlightEnabled() { + // Enable the flight + updateUseInMemoryCacheFlight(true); + + final String fociStoreName = BROKER_FOCI_ACCOUNT_CREDENTIAL_SHARED_PREFERENCES; + + // Call getCacheToBeUsed multiple times for FOCI cache + final IAccountCredentialCache fociCache1 = BrokerOAuth2TokenCache.getCacheToBeUsed(mPlatformComponents, fociStoreName); + final IAccountCredentialCache fociCache2 = BrokerOAuth2TokenCache.getCacheToBeUsed(mPlatformComponents, fociStoreName); + + // Verify same FOCI cache instance is reused + assertNotNull(fociCache1); + assertNotNull(fociCache2); + assertSame(fociCache1, fociCache2); + } + + private void updateUseInMemoryCacheFlight(boolean enabled) { + final IFlightsProvider mockFlightsProvider = Mockito.mock(IFlightsProvider.class); + Mockito.when(mockFlightsProvider.isFlightEnabled(CommonFlight.USE_IN_MEMORY_CACHE_FOR_ACCOUNTS_AND_CREDENTIALS)) + .thenReturn(enabled); + + // Create anonymous IFlightsManager + IFlightsManager anonymousFlightsManager = new IFlightsManager() { + @Override + public @NotNull IFlightsProvider getFlightsProvider(long waitForConfigsWithTimeoutInMs) { + return mockFlightsProvider; + } + @Override + public @NotNull IFlightsProvider getFlightsProviderForTenant(@NotNull String tenantId, long waitForConfigsWithTimeoutInMs) { + return mockFlightsProvider; + } + @Override + public @NotNull IFlightsProvider getFlightsProviderForTenant(@NotNull String tenantId) { + return mockFlightsProvider; + } + @NonNull + @Override + public IFlightsProvider getFlightsProvider() { + return mockFlightsProvider; + } + }; + + // Initialize CommonFlightsManager with the anonymous implementation + CommonFlightsManager.INSTANCE.initializeCommonFlightsManager(anonymousFlightsManager); + } } diff --git a/common/src/test/java/com/microsoft/identity/common/BrokerOAuth2TokenCacheWithInMemoryCacheTest.java b/common/src/test/java/com/microsoft/identity/common/BrokerOAuth2TokenCacheWithInMemoryCacheTest.java new file mode 100644 index 0000000000..97fbecfc70 --- /dev/null +++ b/common/src/test/java/com/microsoft/identity/common/BrokerOAuth2TokenCacheWithInMemoryCacheTest.java @@ -0,0 +1,1240 @@ +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +package com.microsoft.identity.common; + +import static com.microsoft.identity.common.MicrosoftStsAccountCredentialAdapterTest.MOCK_ID_TOKEN_WITH_CLAIMS; +import static com.microsoft.identity.common.SharedPreferencesAccountCredentialCacheTest.APPLICATION_IDENTIFIER_SHA512; +import static com.microsoft.identity.common.SharedPreferencesAccountCredentialCacheTest.BEARER_AUTHENTICATION_SCHEME; +import static com.microsoft.identity.common.SharedPreferencesAccountCredentialCacheTest.CACHED_AT; +import static com.microsoft.identity.common.SharedPreferencesAccountCredentialCacheTest.CLIENT_ID; +import static com.microsoft.identity.common.SharedPreferencesAccountCredentialCacheTest.ENVIRONMENT; +import static com.microsoft.identity.common.SharedPreferencesAccountCredentialCacheTest.EXPIRES_ON; +import static com.microsoft.identity.common.SharedPreferencesAccountCredentialCacheTest.HOME_ACCOUNT_ID; +import static com.microsoft.identity.common.SharedPreferencesAccountCredentialCacheTest.LOCAL_ACCOUNT_ID; +import static com.microsoft.identity.common.SharedPreferencesAccountCredentialCacheTest.MAM_ENROLLMENT_IDENTIFIER; +import static com.microsoft.identity.common.SharedPreferencesAccountCredentialCacheTest.REALM; +import static com.microsoft.identity.common.SharedPreferencesAccountCredentialCacheTest.SECRET; +import static com.microsoft.identity.common.SharedPreferencesAccountCredentialCacheTest.SESSION_KEY; +import static com.microsoft.identity.common.SharedPreferencesAccountCredentialCacheTest.TARGET; +import static com.microsoft.identity.common.SharedPreferencesAccountCredentialCacheTest.USERNAME; +import static com.microsoft.identity.common.java.cache.SharedPreferencesAccountCredentialCache.BROKER_FOCI_ACCOUNT_CREDENTIAL_SHARED_PREFERENCES; +import static com.microsoft.identity.common.java.cache.SharedPreferencesAccountCredentialCache.getBrokerUidSequesteredFilename; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.mockito.Mockito.when; + +import androidx.test.core.app.ApplicationProvider; + +import com.microsoft.identity.common.components.MockPlatformComponentsFactory; +import com.microsoft.identity.common.internal.platform.AndroidPlatformUtil; +import com.microsoft.identity.common.java.cache.BrokerApplicationMetadata; +import com.microsoft.identity.common.java.cache.BrokerOAuth2TokenCache; +import com.microsoft.identity.common.java.cache.CacheKeyValueDelegate; +import com.microsoft.identity.common.java.cache.IAccountCredentialAdapter; +import com.microsoft.identity.common.java.cache.IAccountCredentialCache; +import com.microsoft.identity.common.java.cache.IBrokerApplicationMetadataCache; +import com.microsoft.identity.common.java.cache.MicrosoftFamilyOAuth2TokenCache; +import com.microsoft.identity.common.java.cache.MsalOAuth2TokenCache; +import com.microsoft.identity.common.java.cache.NameValueStorageBrokerApplicationMetadataCache; +import com.microsoft.identity.common.java.cache.AccountDeletionRecord; +import com.microsoft.identity.common.java.cache.ICacheRecord; +import com.microsoft.identity.common.java.cache.SharedPreferencesAccountCredentialCacheWithMemoryCache; +import com.microsoft.identity.common.java.dto.AccountRecord; +import com.microsoft.identity.common.java.dto.Credential; +import com.microsoft.identity.common.java.dto.CredentialType; +import com.microsoft.identity.common.java.exception.ClientException; +import com.microsoft.identity.common.java.interfaces.INameValueStorage; +import com.microsoft.identity.common.java.interfaces.IPlatformComponents; +import com.microsoft.identity.common.java.providers.microsoft.MicrosoftAccount; +import com.microsoft.identity.common.java.providers.microsoft.microsoftsts.MicrosoftStsAuthorizationRequest; +import com.microsoft.identity.common.java.providers.microsoft.microsoftsts.MicrosoftStsOAuth2Strategy; +import com.microsoft.identity.common.java.providers.microsoft.microsoftsts.MicrosoftStsTokenResponse; +import com.microsoft.identity.common.java.providers.oauth2.OAuth2TokenCache; +import com.microsoft.identity.common.shadows.ShadowAndroidSdkStorageEncryptionManager; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.powermock.api.mockito.PowerMockito; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +@SuppressWarnings({"rawtypes", "unchecked"}) +@RunWith(RobolectricTestRunner.class) +@Config(shadows = {ShadowAndroidSdkStorageEncryptionManager.class}) +public class BrokerOAuth2TokenCacheWithInMemoryCacheTest { + + private static final int TEST_APP_UID = 1337; + + private IPlatformComponents mPlatformComponents; + + private MicrosoftStsOAuth2Strategy mockStrategy; + private MicrosoftStsAuthorizationRequest mockRequest; + private MicrosoftStsTokenResponse mockResponse; + private IAccountCredentialAdapter mMockCredentialAdapter; + + private MicrosoftFamilyOAuth2TokenCache mFociCache; + private IAccountCredentialCache mFociCredentialCache; + private IAccountCredentialCache mAppUidCredentialCache; + private List mOtherAppTokenCaches; + private List mOtherAppCredentialCaches; + private BrokerOAuth2TokenCache mBrokerOAuth2TokenCache; + + private MsalOAuth2TokenCacheTest.AccountCredentialTestBundle mDefaultFociTestBundle; + private MsalOAuth2TokenCacheTest.AccountCredentialTestBundle mDefaultAppUidTestBundle; + private List mOtherCacheTestBundles; + + private IBrokerApplicationMetadataCache mApplicationMetadataCache; + private int[] testAppUids; + + @Before + public void setUp() { + + mockStrategy = PowerMockito.mock(MicrosoftStsOAuth2Strategy.class); + mockRequest = PowerMockito.mock(MicrosoftStsAuthorizationRequest.class); + mockResponse = PowerMockito.mock(MicrosoftStsTokenResponse.class); + mMockCredentialAdapter = PowerMockito.mock(IAccountCredentialAdapter.class); + + mPlatformComponents = MockPlatformComponentsFactory.getNonFunctionalBuilder() + .platformUtil(new AndroidPlatformUtil(ApplicationProvider.getApplicationContext(), null)) + .build(); + + mApplicationMetadataCache = new NameValueStorageBrokerApplicationMetadataCache(mPlatformComponents); + + initFociCache(mPlatformComponents); + initOtherCaches(mPlatformComponents); + + mBrokerOAuth2TokenCache = new BrokerOAuth2TokenCache( + mPlatformComponents, + TEST_APP_UID, + mApplicationMetadataCache, + new BrokerOAuth2TokenCache.ProcessUidCacheFactory() { + @Override + public MsalOAuth2TokenCache getTokenCache(final IPlatformComponents context, + final int bindingProcessUid) { + return initAppUidCache(context, bindingProcessUid); + } + }, + mFociCache + ); + + mDefaultFociTestBundle = new MsalOAuth2TokenCacheTest.AccountCredentialTestBundle( + MicrosoftAccount.AUTHORITY_TYPE_MS_STS, + LOCAL_ACCOUNT_ID, + USERNAME, + HOME_ACCOUNT_ID, + ENVIRONMENT, + REALM, + TARGET, + CACHED_AT, + EXPIRES_ON, + SECRET, + CLIENT_ID, + APPLICATION_IDENTIFIER_SHA512, + MAM_ENROLLMENT_IDENTIFIER, + SECRET, + MOCK_ID_TOKEN_WITH_CLAIMS, + "1", + SESSION_KEY, + CredentialType.IdToken + ); + + mDefaultAppUidTestBundle = new MsalOAuth2TokenCacheTest.AccountCredentialTestBundle( + MicrosoftAccount.AUTHORITY_TYPE_MS_STS, + LOCAL_ACCOUNT_ID, + USERNAME, + HOME_ACCOUNT_ID, + ENVIRONMENT, + REALM, + TARGET, + CACHED_AT, + EXPIRES_ON, + SECRET, + CLIENT_ID, + APPLICATION_IDENTIFIER_SHA512, + MAM_ENROLLMENT_IDENTIFIER, + SECRET, + MOCK_ID_TOKEN_WITH_CLAIMS, + null, + SESSION_KEY, + CredentialType.IdToken + ); + + mOtherCacheTestBundles = new ArrayList<>(); + + for (int ii = 0; ii < mOtherAppTokenCaches.size(); ii++) { + mOtherCacheTestBundles.add( + new MsalOAuth2TokenCacheTest.AccountCredentialTestBundle( + MicrosoftAccount.AUTHORITY_TYPE_MS_STS, + UUID.randomUUID().toString(), + "test.user@tenant.onmicrosoft.com", + HOME_ACCOUNT_ID, + ENVIRONMENT, + UUID.randomUUID().toString(), + TARGET, + CACHED_AT, + EXPIRES_ON, + SECRET, + UUID.randomUUID().toString(), + APPLICATION_IDENTIFIER_SHA512, + MAM_ENROLLMENT_IDENTIFIER, + SECRET, + MOCK_ID_TOKEN_WITH_CLAIMS, + null, + SESSION_KEY, + CredentialType.IdToken + ) + ); + } + } + + @After + public void tearDown() throws Exception { + if (null != mAppUidCredentialCache) { + mAppUidCredentialCache.clearAll(); + } + + if (null != mFociCredentialCache) { + mFociCredentialCache.clearAll(); + } + + for (final IAccountCredentialCache cache : mOtherAppCredentialCaches) { + cache.clearAll(); + } + + mApplicationMetadataCache.clear(); + } + + private void initOtherCaches(final IPlatformComponents components) { + testAppUids = new int[]{ + 1338, + 1339, + 1340, + 1341 + }; + + final List> fileManagers = getAppUidFileManagers( + components, + testAppUids + ); + + mOtherAppCredentialCaches = getAccountCredentialCaches( + fileManagers + ); + + mOtherAppTokenCaches = new ArrayList<>(); + + for (final IAccountCredentialCache cache : mOtherAppCredentialCaches) { + mOtherAppTokenCaches.add( + getTokenCache( + components, + cache, + false + ) + ); + } + } + + private List getAccountCredentialCaches(final List> fileManagers) { + final List accountCredentialCaches = new ArrayList<>(); + + for (final INameValueStorage fileManager : fileManagers) { + accountCredentialCaches.add( + getAccountCredentialCache(fileManager) + ); + } + + return accountCredentialCaches; + } + + private List> getAppUidFileManagers(final IPlatformComponents components, + final int[] testAppUids) { + final List> fileManagers = new ArrayList<>(); + + for (final int currentAppUid : testAppUids) { + fileManagers.add( + getAppUidFileManager( + components, + currentAppUid + ) + ); + } + + return fileManagers; + } + + private INameValueStorage getAppUidFileManager(final IPlatformComponents components, + final int appUid) { + return components.getStorageSupplier().getEncryptedNameValueStore( + getBrokerUidSequesteredFilename(appUid), + String.class); + } + + private INameValueStorage getFociFileManager(final IPlatformComponents components) { + return components.getStorageSupplier().getEncryptedNameValueStore( + BROKER_FOCI_ACCOUNT_CREDENTIAL_SHARED_PREFERENCES, + String.class + ); + } + + private SharedPreferencesAccountCredentialCacheWithMemoryCache getAccountCredentialCache( + final INameValueStorage fm) { + return new SharedPreferencesAccountCredentialCacheWithMemoryCache( + new CacheKeyValueDelegate(), + fm + ); + } + + @SuppressWarnings("unchecked") + private T getTokenCache(final IPlatformComponents components, + final IAccountCredentialCache cache, + boolean isFoci) { + return (T) (isFoci ? + new MicrosoftFamilyOAuth2TokenCache<>( + components, + cache, + mMockCredentialAdapter + ) : + new MsalOAuth2TokenCache( + components, + cache, + mMockCredentialAdapter + ) + ); + } + + + private MsalOAuth2TokenCache initAppUidCache(final IPlatformComponents components, final int uid) { + final INameValueStorage appUidCacheFileManager = getAppUidFileManager( + components, + uid + ); + + mAppUidCredentialCache = getAccountCredentialCache(appUidCacheFileManager); + + return getTokenCache(components, mAppUidCredentialCache, false); + } + + private void initFociCache(final IPlatformComponents components) { + @SuppressWarnings("unchecked") + final INameValueStorage fociCacheFileManager = getFociFileManager(components); + + mFociCredentialCache = getAccountCredentialCache(fociCacheFileManager); + + mFociCache = getTokenCache(components, mFociCredentialCache, true); + } + + @SuppressWarnings("unchecked") + private void configureMocks(final MsalOAuth2TokenCacheTest.AccountCredentialTestBundle testBundle) { + when( + mMockCredentialAdapter.createAccount( + mockStrategy, + mockRequest, + mockResponse + ) + ).thenReturn(testBundle.mGeneratedAccount); + + when( + mMockCredentialAdapter.createAccessToken( + mockStrategy, + mockRequest, + mockResponse + ) + ).thenReturn(testBundle.mGeneratedAccessToken); + + when( + mMockCredentialAdapter.createRefreshToken( + mockStrategy, + mockRequest, + mockResponse + ) + ).thenReturn(testBundle.mGeneratedRefreshToken); + + when( + mMockCredentialAdapter.createIdToken( + mockStrategy, + mockRequest, + mockResponse + ) + ).thenReturn(testBundle.mGeneratedIdToken); + } + + private void configureMocksForFoci() { + configureMocks(mDefaultFociTestBundle); + when(mockResponse.getFamilyId()).thenReturn("1"); + } + + private void configureMocksForAppUid() { + configureMocks(mDefaultAppUidTestBundle); + } + + @Test + @SuppressWarnings("unchecked") + public void testKnownClientIdsNonFoci() throws ClientException { + configureMocksForAppUid(); + + final ICacheRecord result = mBrokerOAuth2TokenCache.save( + mockStrategy, + mockRequest, + mockResponse + ); + + final String targetClientId = result.getRefreshToken().getClientId(); + assertTrue(mBrokerOAuth2TokenCache.isClientIdKnownToCache(targetClientId)); + } + + @Test + @SuppressWarnings("unchecked") + public void testKnownClientIdsFoci() throws ClientException { + configureMocksForFoci(); + + final ICacheRecord result = mBrokerOAuth2TokenCache.save( + mockStrategy, + mockRequest, + mockResponse + ); + + final String targetClientId = result.getRefreshToken().getClientId(); + assertTrue(mBrokerOAuth2TokenCache.isClientIdKnownToCache(targetClientId)); + } + + @Test + @SuppressWarnings("unchecked") + public void testGetFociCacheRecords() throws ClientException { + configureMocksForFoci(); + + final ICacheRecord result = mBrokerOAuth2TokenCache.save( + mockStrategy, + mockRequest, + mockResponse + ); + + final List fociCacheRecords = mBrokerOAuth2TokenCache.getFociCacheRecords(); + + assertNotNull(fociCacheRecords); + assertFalse(fociCacheRecords.isEmpty()); + assertEquals( + result.getRefreshToken(), + fociCacheRecords.get(0).getRefreshToken() + ); + assertEquals( + result.getIdToken(), + fociCacheRecords.get(0).getIdToken() + ); + } + + @Test + @SuppressWarnings("unchecked") + public void testGetFociCacheRecordsEmpty() throws ClientException { + configureMocksForAppUid(); + + final ICacheRecord result = mBrokerOAuth2TokenCache.save( + mockStrategy, + mockRequest, + mockResponse + ); + + final List fociCacheRecords = mBrokerOAuth2TokenCache.getFociCacheRecords(); + + assertNotNull(fociCacheRecords); + assertTrue(fociCacheRecords.isEmpty()); + } + + @Test + @SuppressWarnings("unchecked") + public void testCanSaveIntoAppUidCache() throws ClientException { + configureMocksForAppUid(); + + mBrokerOAuth2TokenCache.save( + mockStrategy, + mockRequest, + mockResponse + ); + + final List accounts = mAppUidCredentialCache.getAccounts(); + assertEquals(1, accounts.size()); + assertEquals(mDefaultAppUidTestBundle.mGeneratedAccount, accounts.get(0)); + + final List credentials = mAppUidCredentialCache.getCredentials(); + assertEquals(3, credentials.size()); + + final List rts = new ArrayList<>(); + final List ats = new ArrayList<>(); + final List ids = new ArrayList<>(); + + for (final Credential credential : credentials) { + if (credential.getCredentialType().equalsIgnoreCase(CredentialType.AccessToken.name())) { + ats.add(credential); + } else if (credential.getCredentialType().equalsIgnoreCase(CredentialType.RefreshToken.name())) { + rts.add(credential); + } else if (credential.getCredentialType().equalsIgnoreCase(CredentialType.IdToken.name())) { + ids.add(credential); + } else { + fail(); + } + } + + assertEquals(mDefaultAppUidTestBundle.mGeneratedAccessToken, ats.get(0)); + assertEquals(mDefaultAppUidTestBundle.mGeneratedRefreshToken, rts.get(0)); + assertEquals(mDefaultAppUidTestBundle.mGeneratedIdToken, ids.get(0)); + } + + @Test + public void testCanSaveIntoFociCache() throws ClientException { + configureMocksForFoci(); + + mBrokerOAuth2TokenCache.save( + mockStrategy, + mockRequest, + mockResponse + ); + + final List accounts = mFociCredentialCache.getAccounts(); + assertEquals(1, accounts.size()); + assertEquals(mDefaultFociTestBundle.mGeneratedAccount, accounts.get(0)); + + final List credentials = mFociCredentialCache.getCredentials(); + assertEquals(3, credentials.size()); + + final List rts = new ArrayList<>(); + final List ats = new ArrayList<>(); + final List ids = new ArrayList<>(); + + for (final Credential credential : credentials) { + if (credential.getCredentialType().equalsIgnoreCase(CredentialType.AccessToken.name())) { + ats.add(credential); + } else if (credential.getCredentialType().equalsIgnoreCase(CredentialType.RefreshToken.name())) { + rts.add(credential); + } else if (credential.getCredentialType().equalsIgnoreCase(CredentialType.IdToken.name())) { + ids.add(credential); + } else { + fail(); + } + } + + assertEquals(mDefaultFociTestBundle.mGeneratedAccessToken, ats.get(0)); + assertEquals(mDefaultFociTestBundle.mGeneratedRefreshToken, rts.get(0)); + assertEquals(mDefaultFociTestBundle.mGeneratedIdToken, ids.get(0)); + } + + @Test + public void testCacheMiss() { + final ICacheRecord cacheRecord = mBrokerOAuth2TokenCache.load( + CLIENT_ID, + APPLICATION_IDENTIFIER_SHA512, + MAM_ENROLLMENT_IDENTIFIER, + TARGET, + mDefaultAppUidTestBundle.mGeneratedAccount, + BEARER_AUTHENTICATION_SCHEME + ); + + assertNotNull(cacheRecord); + assertNotNull(cacheRecord.getAccount()); + assertNull(cacheRecord.getAccessToken()); + assertNull(cacheRecord.getRefreshToken()); + assertNull(cacheRecord.getIdToken()); + } + + @Test + public void testRemoveCredentialAppUidCache() throws ClientException { + configureMocksForAppUid(); + + mBrokerOAuth2TokenCache.save( + mockStrategy, + mockRequest, + mockResponse + ); + + final ICacheRecord cacheRecord = mBrokerOAuth2TokenCache.load( + CLIENT_ID, + APPLICATION_IDENTIFIER_SHA512, + MAM_ENROLLMENT_IDENTIFIER, + TARGET, + mDefaultAppUidTestBundle.mGeneratedAccount, + BEARER_AUTHENTICATION_SCHEME + ); + + assertTrue( + mBrokerOAuth2TokenCache.removeCredential( + mDefaultAppUidTestBundle.mGeneratedAccessToken + ) + ); + } + + @Test + public void testRemoveCredentialFociCache() throws ClientException { + configureMocksForFoci(); + + mBrokerOAuth2TokenCache.save( + mockStrategy, + mockRequest, + mockResponse + ); + + final ICacheRecord cacheRecord = mBrokerOAuth2TokenCache.load( + CLIENT_ID, + APPLICATION_IDENTIFIER_SHA512, + MAM_ENROLLMENT_IDENTIFIER, + TARGET, + mDefaultFociTestBundle.mGeneratedAccount, + BEARER_AUTHENTICATION_SCHEME + ); + + assertTrue( + mBrokerOAuth2TokenCache.removeCredential( + mDefaultFociTestBundle.mGeneratedAccessToken + ) + ); + } + + @Test + public void testRemoveCredentialMiss() { + assertFalse( + mBrokerOAuth2TokenCache.removeCredential( + mDefaultFociTestBundle.mGeneratedAccessToken + ) + ); + } + + @Test + public void testGetAccountAppUidCache() throws ClientException { + configureMocksForAppUid(); + + mBrokerOAuth2TokenCache.save( + mockStrategy, + mockRequest, + mockResponse + ); + + assertNotNull( + mBrokerOAuth2TokenCache.getAccount( + ENVIRONMENT, + CLIENT_ID, + HOME_ACCOUNT_ID, + REALM + ) + ); + + assertNull( + mFociCache.getAccount( + ENVIRONMENT, + CLIENT_ID, + HOME_ACCOUNT_ID, + REALM + ) + ); + } + + @Test + public void testGetAccountFociCache() throws ClientException { + configureMocksForFoci(); + + mBrokerOAuth2TokenCache.save( + mockStrategy, + mockRequest, + mockResponse + ); + + assertNotNull( + mBrokerOAuth2TokenCache.getAccount( + ENVIRONMENT, + CLIENT_ID, + HOME_ACCOUNT_ID, + REALM + ) + ); + } + + @Test + public void testGetAccountWithLocalAccountIdAppUidCache() throws ClientException { + configureMocksForAppUid(); + + mBrokerOAuth2TokenCache.save( + mockStrategy, + mockRequest, + mockResponse + ); + + final AccountRecord account = mBrokerOAuth2TokenCache.getAccountByLocalAccountId( + ENVIRONMENT, + CLIENT_ID, + LOCAL_ACCOUNT_ID + ); + + assertNotNull(account); + } + + @Test + public void testGetAccountWithLocalAccountIdFociCache() throws ClientException { + configureMocksForFoci(); + + mBrokerOAuth2TokenCache.save( + mockStrategy, + mockRequest, + mockResponse + ); + + final AccountRecord account = mBrokerOAuth2TokenCache.getAccountByLocalAccountId( + ENVIRONMENT, + CLIENT_ID, + LOCAL_ACCOUNT_ID + ); + + assertNotNull(account); + } + + @Test + public void testRemoveAccountFromDevice() throws ClientException { + // Load up the 'other caches' which a bunch of test credentials, see if we can get them out... + int ii = 0; + for (final OAuth2TokenCache cache : mOtherAppTokenCaches) { + configureMocks(mOtherCacheTestBundles.get(ii)); + + final ICacheRecord cacheRecord = cache.save( + mockStrategy, + mockRequest, + mockResponse + ); + + final BrokerApplicationMetadata applicationMetadata = new BrokerApplicationMetadata(); + applicationMetadata.setClientId(cacheRecord.getIdToken().getClientId()); + applicationMetadata.setEnvironment(cacheRecord.getIdToken().getEnvironment()); + applicationMetadata.setFoci(cacheRecord.getRefreshToken().getFamilyId()); + applicationMetadata.setUid(testAppUids[ii++]); + + mApplicationMetadataCache.insert(applicationMetadata); + } + + final List clientIds = new ArrayList<>(); + + for (final MsalOAuth2TokenCacheTest.AccountCredentialTestBundle testBundle : mOtherCacheTestBundles) { + clientIds.add( + testBundle.mGeneratedRefreshToken.getClientId() + ); + } + + final List xAppAccounts = mBrokerOAuth2TokenCache.getAccounts(); + + // Deleting one of these AccountRecords should remove all of them... + final AccountDeletionRecord deletionRecord = mBrokerOAuth2TokenCache.removeAccountFromDevice( + xAppAccounts.get(0) + ); + + assertEquals(xAppAccounts.size(), deletionRecord.size()); + assertEquals(0, mBrokerOAuth2TokenCache.getAccounts().size()); + } + + @Test + public void testGetAccountsAdal() throws ClientException { + // Load up the 'other caches' which a bunch of test credentials, see if we can get them out... + int ii = 0; + for (final OAuth2TokenCache cache : mOtherAppTokenCaches) { + configureMocks(mOtherCacheTestBundles.get(ii)); + + final ICacheRecord cacheRecord = cache.save( + mockStrategy, + mockRequest, + mockResponse + ); + + final BrokerApplicationMetadata applicationMetadata = new BrokerApplicationMetadata(); + applicationMetadata.setClientId(cacheRecord.getIdToken().getClientId()); + applicationMetadata.setEnvironment(cacheRecord.getIdToken().getEnvironment()); + applicationMetadata.setFoci(cacheRecord.getRefreshToken().getFamilyId()); + applicationMetadata.setUid(testAppUids[ii++]); + + mApplicationMetadataCache.insert(applicationMetadata); + } + + final List clientIds = new ArrayList<>(); + + for (final MsalOAuth2TokenCacheTest.AccountCredentialTestBundle testBundle : mOtherCacheTestBundles) { + clientIds.add( + testBundle.mGeneratedRefreshToken.getClientId() + ); + } + + final List xAppAccounts = new ArrayList<>(); + + for (final int testUid : testAppUids) { + // Create the cache to query... + mBrokerOAuth2TokenCache = new BrokerOAuth2TokenCache( + mPlatformComponents, + testUid, + mApplicationMetadataCache, + new BrokerOAuth2TokenCache.ProcessUidCacheFactory() { + @Override + public MsalOAuth2TokenCache getTokenCache(IPlatformComponents context, int bindingProcessUid) { + return initAppUidCache(context, bindingProcessUid); + } + }, + mFociCache + ); + + for (final String clientId : clientIds) { + final List accountsInCache = mBrokerOAuth2TokenCache.getAccounts( + ENVIRONMENT, + clientId + ); + + xAppAccounts.addAll(accountsInCache); + } + } + + assertEquals( + clientIds.size(), + xAppAccounts.size() + ); + + final List xAppAccountsNoParam = new ArrayList<>( + mBrokerOAuth2TokenCache.getAccounts() + ); + + assertEquals(xAppAccounts.size(), xAppAccountsNoParam.size()); + } + + @Test + public void testGetAccountsMsal() throws ClientException { + // Load up the 'other caches' which a bunch of test credentials, see if we can get them out... + int ii = 0; + for (final OAuth2TokenCache cache : mOtherAppTokenCaches) { + configureMocks(mOtherCacheTestBundles.get(ii)); + + final ICacheRecord cacheRecord = cache.save( + mockStrategy, + mockRequest, + mockResponse + ); + + + final BrokerApplicationMetadata applicationMetadata = new BrokerApplicationMetadata(); + applicationMetadata.setClientId(cacheRecord.getIdToken().getClientId()); + applicationMetadata.setEnvironment(cacheRecord.getIdToken().getEnvironment()); + applicationMetadata.setFoci(cacheRecord.getRefreshToken().getFamilyId()); + applicationMetadata.setUid(testAppUids[ii++]); + + mApplicationMetadataCache.insert(applicationMetadata); + } + + final List clientIds = new ArrayList<>(); + + for (final MsalOAuth2TokenCacheTest.AccountCredentialTestBundle testBundle : mOtherCacheTestBundles) { + clientIds.add( + testBundle.mGeneratedRefreshToken.getClientId() + ); + } + + final List xAppAccounts = new ArrayList<>(); + + for (final int testUid : testAppUids) { + // Create the cache to query... + mBrokerOAuth2TokenCache = new BrokerOAuth2TokenCache( + mPlatformComponents, + testUid, + mApplicationMetadataCache, + new BrokerOAuth2TokenCache.ProcessUidCacheFactory() { + @Override + public MsalOAuth2TokenCache getTokenCache(IPlatformComponents context, int bindingProcessUid) { + return initAppUidCache(context, bindingProcessUid); + } + }, + mFociCache + ); + + for (final String clientId : clientIds) { + final List accountsInCache = mBrokerOAuth2TokenCache.getAccounts( + ENVIRONMENT, + clientId + ); + + xAppAccounts.addAll(accountsInCache); + } + } + + assertEquals( + clientIds.size(), + xAppAccounts.size() + ); + + final List xAppAccountsNoParam = new ArrayList<>( + mBrokerOAuth2TokenCache.getAccounts() + ); + + assertEquals(xAppAccounts.size(), xAppAccountsNoParam.size()); + + final BrokerOAuth2TokenCache brokerOAuth2TokenCache = new BrokerOAuth2TokenCache( + mPlatformComponents, + TEST_APP_UID, + new NameValueStorageBrokerApplicationMetadataCache(mPlatformComponents) + ); + + assertEquals( + 0, + brokerOAuth2TokenCache.getAccounts(ENVIRONMENT, CLIENT_ID).size() + ); + + final BrokerOAuth2TokenCache brokerOAuth2TokenCache2 = new BrokerOAuth2TokenCache( + mPlatformComponents, + TEST_APP_UID, + new NameValueStorageBrokerApplicationMetadataCache(mPlatformComponents) + ); + + assertEquals( + xAppAccounts.size(), + brokerOAuth2TokenCache2.getAccounts().size() + ); + } + + @Test + public void testWPJSaveNonFoci_deprecated() throws ClientException { + final ICacheRecord saveResult = mBrokerOAuth2TokenCache.save( + mDefaultAppUidTestBundle.mGeneratedAccount, + mDefaultAppUidTestBundle.mGeneratedIdToken, + mDefaultAppUidTestBundle.mGeneratedAccessToken, + null + ); + + assertNotNull(saveResult); + assertNotNull(saveResult.getAccount()); + assertNotNull(saveResult.getIdToken()); + assertNotNull(saveResult.getAccessToken()); + assertNull(saveResult.getRefreshToken()); + + assertEquals( + mDefaultAppUidTestBundle.mGeneratedAccount, + saveResult.getAccount() + ); + + assertEquals( + mDefaultAppUidTestBundle.mGeneratedIdToken, + saveResult.getIdToken() + ); + + assertEquals( + mDefaultAppUidTestBundle.mGeneratedAccessToken, + saveResult.getAccessToken() + ); + + final ICacheRecord retrievedResult = mBrokerOAuth2TokenCache.load( + mDefaultAppUidTestBundle.mGeneratedIdToken.getClientId(), + mDefaultAppUidTestBundle.mGeneratedAccessToken.getApplicationIdentifier(), + mDefaultAppUidTestBundle.mGeneratedAccessToken.getMamEnrollmentIdentifier(), + mDefaultAppUidTestBundle.mGeneratedAccessToken.getTarget(), + mDefaultAppUidTestBundle.mGeneratedAccount, + BEARER_AUTHENTICATION_SCHEME + ); + + assertNotNull(retrievedResult); + assertNotNull(retrievedResult.getAccount()); + assertNotNull(retrievedResult.getIdToken()); + assertNotNull(retrievedResult.getAccessToken()); + assertNull(retrievedResult.getRefreshToken()); + + assertEquals( + mDefaultAppUidTestBundle.mGeneratedAccount, + retrievedResult.getAccount() + ); + + assertEquals( + mDefaultAppUidTestBundle.mGeneratedIdToken, + retrievedResult.getIdToken() + ); + + assertEquals( + mDefaultAppUidTestBundle.mGeneratedAccessToken, + retrievedResult.getAccessToken() + ); + } + + @Test + public void testWPJSaveNonFoci() throws ClientException { + final ICacheRecord saveResult = mBrokerOAuth2TokenCache.save( + mDefaultAppUidTestBundle.mGeneratedAccount, + mDefaultAppUidTestBundle.mGeneratedIdToken, + mDefaultAppUidTestBundle.mGeneratedAccessToken, + mDefaultAppUidTestBundle.mGeneratedRefreshToken, + null + ); + + assertNotNull(saveResult); + assertNotNull(saveResult.getAccount()); + assertNotNull(saveResult.getIdToken()); + assertNotNull(saveResult.getAccessToken()); + assertNotNull(saveResult.getRefreshToken()); + + assertEquals( + mDefaultAppUidTestBundle.mGeneratedAccount, + saveResult.getAccount() + ); + + assertEquals( + mDefaultAppUidTestBundle.mGeneratedIdToken, + saveResult.getIdToken() + ); + + assertEquals( + mDefaultAppUidTestBundle.mGeneratedAccessToken, + saveResult.getAccessToken() + ); + + assertEquals( + mDefaultAppUidTestBundle.mGeneratedRefreshToken, + saveResult.getRefreshToken() + ); + + final ICacheRecord retrievedResult = mBrokerOAuth2TokenCache.load( + mDefaultAppUidTestBundle.mGeneratedIdToken.getClientId(), + mDefaultAppUidTestBundle.mGeneratedAccessToken.getApplicationIdentifier(), + mDefaultAppUidTestBundle.mGeneratedAccessToken.getMamEnrollmentIdentifier(), + mDefaultAppUidTestBundle.mGeneratedAccessToken.getTarget(), + mDefaultAppUidTestBundle.mGeneratedAccount, + BEARER_AUTHENTICATION_SCHEME + ); + + assertNotNull(retrievedResult); + assertNotNull(retrievedResult.getAccount()); + assertNotNull(retrievedResult.getIdToken()); + assertNotNull(retrievedResult.getAccessToken()); + assertNotNull(retrievedResult.getRefreshToken()); + + assertEquals( + mDefaultAppUidTestBundle.mGeneratedAccount, + retrievedResult.getAccount() + ); + + assertEquals( + mDefaultAppUidTestBundle.mGeneratedIdToken, + retrievedResult.getIdToken() + ); + + assertEquals( + mDefaultAppUidTestBundle.mGeneratedAccessToken, + retrievedResult.getAccessToken() + ); + + assertEquals( + mDefaultAppUidTestBundle.mGeneratedRefreshToken, + saveResult.getRefreshToken() + ); + } + + @Test + public void testWPJSaveFoci_deprecated() throws ClientException { + final ICacheRecord saveResult = mBrokerOAuth2TokenCache.save( + mDefaultFociTestBundle.mGeneratedAccount, + mDefaultFociTestBundle.mGeneratedIdToken, + mDefaultFociTestBundle.mGeneratedAccessToken, + "1" + ); + + assertNotNull(saveResult); + assertNotNull(saveResult.getAccount()); + assertNotNull(saveResult.getIdToken()); + assertNotNull(saveResult.getAccessToken()); + assertNull(saveResult.getRefreshToken()); + + assertEquals( + mDefaultFociTestBundle.mGeneratedAccount, + saveResult.getAccount() + ); + + assertEquals( + mDefaultFociTestBundle.mGeneratedIdToken, + saveResult.getIdToken() + ); + + assertEquals( + mDefaultFociTestBundle.mGeneratedAccessToken, + saveResult.getAccessToken() + ); + + + final ICacheRecord retrievedResult = mBrokerOAuth2TokenCache.load( + mDefaultFociTestBundle.mGeneratedIdToken.getClientId(), + mDefaultFociTestBundle.mGeneratedAccessToken.getApplicationIdentifier(), + mDefaultFociTestBundle.mGeneratedAccessToken.getMamEnrollmentIdentifier(), + mDefaultFociTestBundle.mGeneratedAccessToken.getTarget(), + mDefaultFociTestBundle.mGeneratedAccount, + BEARER_AUTHENTICATION_SCHEME + ); + + assertNotNull(retrievedResult); + assertNotNull(retrievedResult.getAccount()); + assertNotNull(retrievedResult.getIdToken()); + assertNotNull(retrievedResult.getAccessToken()); + assertNull(retrievedResult.getRefreshToken()); + + assertEquals( + mDefaultFociTestBundle.mGeneratedAccount, + retrievedResult.getAccount() + ); + + assertEquals( + mDefaultFociTestBundle.mGeneratedIdToken, + retrievedResult.getIdToken() + ); + + assertEquals( + mDefaultFociTestBundle.mGeneratedAccessToken, + retrievedResult.getAccessToken() + ); + } + + @Test + public void testWPJSaveFoci() throws ClientException { + final ICacheRecord saveResult = mBrokerOAuth2TokenCache.save( + mDefaultFociTestBundle.mGeneratedAccount, + mDefaultFociTestBundle.mGeneratedIdToken, + mDefaultFociTestBundle.mGeneratedAccessToken, + mDefaultFociTestBundle.mGeneratedRefreshToken, + "1" + ); + + assertNotNull(saveResult); + assertNotNull(saveResult.getAccount()); + assertNotNull(saveResult.getIdToken()); + assertNotNull(saveResult.getAccessToken()); + assertNotNull(saveResult.getRefreshToken()); + + assertEquals( + mDefaultFociTestBundle.mGeneratedAccount, + saveResult.getAccount() + ); + + assertEquals( + mDefaultFociTestBundle.mGeneratedIdToken, + saveResult.getIdToken() + ); + + assertEquals( + mDefaultFociTestBundle.mGeneratedAccessToken, + saveResult.getAccessToken() + ); + + assertEquals( + mDefaultFociTestBundle.mGeneratedRefreshToken, + saveResult.getRefreshToken() + ); + + final ICacheRecord retrievedResult = mBrokerOAuth2TokenCache.load( + mDefaultFociTestBundle.mGeneratedIdToken.getClientId(), + mDefaultFociTestBundle.mGeneratedAccessToken.getApplicationIdentifier(), + mDefaultFociTestBundle.mGeneratedAccessToken.getAccessTokenType(), + mDefaultFociTestBundle.mGeneratedAccessToken.getTarget(), + mDefaultFociTestBundle.mGeneratedAccount, + BEARER_AUTHENTICATION_SCHEME + ); + + assertNotNull(retrievedResult); + assertNotNull(retrievedResult.getAccount()); + assertNotNull(retrievedResult.getIdToken()); + assertNotNull(retrievedResult.getAccessToken()); + assertNotNull(retrievedResult.getRefreshToken()); + + assertEquals( + mDefaultFociTestBundle.mGeneratedAccount, + retrievedResult.getAccount() + ); + + assertEquals( + mDefaultFociTestBundle.mGeneratedIdToken, + retrievedResult.getIdToken() + ); + + assertEquals( + mDefaultFociTestBundle.mGeneratedAccessToken, + retrievedResult.getAccessToken() + ); + + assertEquals( + mDefaultFociTestBundle.mGeneratedRefreshToken, + retrievedResult.getRefreshToken() + ); + } + + @Test + public void testClearAll() throws ClientException { + int appIndex = 0; + for (final OAuth2TokenCache cache : mOtherAppTokenCaches) { + configureMocks(mOtherCacheTestBundles.get(appIndex)); + + final ICacheRecord cacheRecord = cache.save(mockStrategy, mockRequest, mockResponse); + + final BrokerApplicationMetadata applicationMetadata = new BrokerApplicationMetadata(); + applicationMetadata.setClientId(cacheRecord.getIdToken().getClientId()); + applicationMetadata.setEnvironment(cacheRecord.getIdToken().getEnvironment()); + applicationMetadata.setFoci(cacheRecord.getRefreshToken().getFamilyId()); + applicationMetadata.setUid(testAppUids[appIndex++]); + + mApplicationMetadataCache.insert(applicationMetadata); + } + + final List clientIds = new ArrayList<>(); + + for (final MsalOAuth2TokenCacheTest.AccountCredentialTestBundle testBundle : mOtherCacheTestBundles) { + clientIds.add( + testBundle.mGeneratedRefreshToken.getClientId() + ); + } + + configureMocksForFoci(); + final ICacheRecord fociCacheRecord = mBrokerOAuth2TokenCache.save(mockStrategy, mockRequest, mockResponse); + final BrokerApplicationMetadata applicationMetadata = new BrokerApplicationMetadata(); + applicationMetadata.setClientId(fociCacheRecord.getIdToken().getClientId()); + applicationMetadata.setEnvironment(fociCacheRecord.getIdToken().getEnvironment()); + applicationMetadata.setFoci(fociCacheRecord.getRefreshToken().getFamilyId()); + applicationMetadata.setUid(0); + + mApplicationMetadataCache.insert(applicationMetadata); + clientIds.add(fociCacheRecord.getIdToken().getClientId()); + + // Verify the broker cache is populated + assertEquals(true, mBrokerOAuth2TokenCache.getAccounts().size() > 0); + assertEquals(true, mBrokerOAuth2TokenCache.getFociCacheRecords().size() > 0); + assertEquals(true, mApplicationMetadataCache.getAll().size() > 0); + + for( final String clientId :clientIds) { + assertEquals(true, mBrokerOAuth2TokenCache.isClientIdKnownToCache(clientId)); + } + + // Clear Broker Cache + mBrokerOAuth2TokenCache.clearAll(); + + // Verify Broker cache is cleared + assertEquals(0, mBrokerOAuth2TokenCache.getAccounts().size()); + assertEquals(0, mBrokerOAuth2TokenCache.getFociCacheRecords().size()); + assertEquals(0, mApplicationMetadataCache.getAll().size()); + for( final String clientId :clientIds) { + assertEquals(false, mBrokerOAuth2TokenCache.isClientIdKnownToCache(clientId)); + } + } +} diff --git a/common4j/src/main/com/microsoft/identity/common/java/cache/BrokerOAuth2TokenCache.java b/common4j/src/main/com/microsoft/identity/common/java/cache/BrokerOAuth2TokenCache.java index 91cf2b29ea..3c56100217 100644 --- a/common4j/src/main/com/microsoft/identity/common/java/cache/BrokerOAuth2TokenCache.java +++ b/common4j/src/main/com/microsoft/identity/common/java/cache/BrokerOAuth2TokenCache.java @@ -24,8 +24,12 @@ import com.microsoft.identity.common.java.authscheme.AbstractAuthenticationScheme; +import com.microsoft.identity.common.java.flighting.CommonFlight; +import com.microsoft.identity.common.java.flighting.CommonFlightsManager; import com.microsoft.identity.common.java.interfaces.INameValueStorage; import com.microsoft.identity.common.java.interfaces.IPlatformComponents; +import com.microsoft.identity.common.java.opentelemetry.AttributeName; +import com.microsoft.identity.common.java.opentelemetry.SpanExtension; import com.microsoft.identity.common.java.providers.oauth2.OAuth2Strategy; import com.microsoft.identity.common.java.providers.oauth2.OAuth2TokenCache; import com.microsoft.identity.common.java.WarningType; @@ -48,7 +52,9 @@ import java.util.HashSet; import java.util.Iterator; import java.util.List; +import java.util.Map; import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; import edu.umd.cs.findbugs.annotations.Nullable; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; @@ -100,6 +106,20 @@ public class BrokerOAuth2TokenCache private final MicrosoftFamilyOAuth2TokenCache mFociCache; private final int mUid; private ProcessUidCacheFactory mDelegate = null; + /** + * Shared, process-wide registry of in-memory augmented account/credential caches keyed by + * storage (SharedPreferences) name. + *

+ * Populated lazily via computeIfAbsent in getCacheToBeUsed(...). Allows multiple + * BrokerOAuth2TokenCache instances to reuse the same in-memory layer for the same underlying + * encrypted name-value store, reducing disk I/O and serialization overhead. + *

+ * Thread-safety: ConcurrentHashMap ensures safe concurrent access and publication. + * Lifecycle: Entries are never removed for the lifetime of the process. + */ + private static final Map + inMemoryCacheMapByStorage = new ConcurrentHashMap<>(); + /** * Constructs a new BrokerOAuth2TokenCache. @@ -1333,6 +1353,7 @@ public void clearAll() { this.mFociCache.clearAll(); this.mApplicationMetadataCache.clear(); + inMemoryCacheMapByStorage.clear(); } /** @@ -1607,14 +1628,10 @@ private MsalOAuth2TokenCache initializeProcessUidCache(@NonNull final IPlatformC return mDelegate.getTokenCache(components, uid); } - final INameValueStorage sharedPreferencesFileManager = - components.getStorageSupplier().getEncryptedNameValueStore( - SharedPreferencesAccountCredentialCache - .getBrokerUidSequesteredFilename(uid), - String.class - ); + final String storeName = SharedPreferencesAccountCredentialCache + .getBrokerUidSequesteredFilename(uid); - return getTokenCache(components, sharedPreferencesFileManager, false); + return getTokenCache(components, storeName, false); } private static MicrosoftFamilyOAuth2TokenCache initializeFociCache(@NonNull final IPlatformComponents components) { @@ -1624,25 +1641,16 @@ private static MicrosoftFamilyOAuth2TokenCache initializeFociCache(@NonNull fina "Initializing foci cache" ); - final INameValueStorage sharedPreferencesFileManager = - components.getStorageSupplier().getEncryptedNameValueStore( - SharedPreferencesAccountCredentialCache.BROKER_FOCI_ACCOUNT_CREDENTIAL_SHARED_PREFERENCES, - String.class - ); + final String storeName = SharedPreferencesAccountCredentialCache.BROKER_FOCI_ACCOUNT_CREDENTIAL_SHARED_PREFERENCES; - return getTokenCache(components, sharedPreferencesFileManager, true); + return getTokenCache(components, storeName, true); } @SuppressWarnings(UNCHECKED) private static T getTokenCache(@NonNull final IPlatformComponents components, - @NonNull final INameValueStorage spfm, + String storeName, boolean isFoci) { - final ICacheKeyValueDelegate cacheKeyValueDelegate = new CacheKeyValueDelegate(); - final IAccountCredentialCache accountCredentialCache = - new SharedPreferencesAccountCredentialCache( - cacheKeyValueDelegate, - spfm - ); + final IAccountCredentialCache accountCredentialCache = getCacheToBeUsed(components, storeName); final MicrosoftStsAccountCredentialAdapter accountCredentialAdapter = new MicrosoftStsAccountCredentialAdapter(); @@ -1662,6 +1670,54 @@ private static T getTokenCache(@NonNull final I ); } + + /** + * Determines which cache implementation to use based on flighting. + *

+ * When the flight {@code USE_IN_MEMORY_CACHE_FOR_ACCOUNTS_AND_CREDENTIALS} is enabled, + * returns a shared, cached instance of {@link SharedPreferencesAccountCredentialCacheWithMemoryCache} + * for the given storage, improving performance by reusing the in-memory cache layer across cache instances. + * When disabled, returns a new {@link SharedPreferencesAccountCredentialCache} instance. + *

+ * Critical behavior: When flight is enabled, the same {@code SharedPreferencesAccountCredentialCacheWithMemoryCache} + * instance is returned for the same {@code spfm} reference, meaning the cache is shared across multiple + * {@code BrokerOAuth2TokenCache} instances. + *

+ * Thread-safety: This method is thread-safe via {@code ConcurrentHashMap.computeIfAbsent}. + *

+ * Lifecycle: Returned cached instances are never removed from the static map. + * + * @return A cached shared in-memory cache instance (flight enabled) or a new + * non-cached instance (flight disabled). + */ + public static IAccountCredentialCache getCacheToBeUsed(@NonNull final IPlatformComponents components, + final String storeName) { + final boolean isFlightEnabled = CommonFlightsManager.INSTANCE + .getFlightsProvider() + .isFlightEnabled(CommonFlight.USE_IN_MEMORY_CACHE_FOR_ACCOUNTS_AND_CREDENTIALS); + SpanExtension.current().setAttribute(AttributeName.in_memory_cache_used_for_accounts_and_credentials.name(), isFlightEnabled); + if (isFlightEnabled) { + return inMemoryCacheMapByStorage.computeIfAbsent(storeName, s -> + new SharedPreferencesAccountCredentialCacheWithMemoryCache( + new CacheKeyValueDelegate(), + components.getStorageSupplier().getEncryptedNameValueStore( + storeName, + String.class + )) + ); + } else { + final INameValueStorage sharedPreferencesFileManager = + components.getStorageSupplier().getEncryptedNameValueStore( + storeName, + String.class + ); + return new SharedPreferencesAccountCredentialCache( + new CacheKeyValueDelegate(), + sharedPreferencesFileManager + ); + } + } + @Nullable private MsalOAuth2TokenCache getTokenCacheForClient(@Nullable final BrokerApplicationMetadata metadata) { final String methodName = ":getTokenCacheForClient(bam)"; @@ -1720,53 +1776,4 @@ private MsalOAuth2TokenCache getTokenCacheForClient(@NonNull final String client return getTokenCacheForClient(metadata); } - - /** - * Sets the SSO state for the supplied Account, relative to the provided uid. - * - * @param uidStr The uid of the app whose SSO token is being inserted. - * @param account The account for which the supplied token is being inserted. - * @param refreshToken The token to insert. - */ - public void setSingleSignOnState(@NonNull final String uidStr, - @NonNull final GenericAccount account, - @NonNull final GenericRefreshToken refreshToken) { - final String methodName = ":setSingleSignOnState"; - - final boolean isFrt = refreshToken.getIsFamilyRefreshToken(); - - MsalOAuth2TokenCache targetCache; - - final int uid = Integer.parseInt(uidStr); - - if (isFrt) { - Logger.verbose(TAG + methodName, "Saving tokens to foci cache."); - - targetCache = mFociCache; - } else { - // If there is an existing cache for this client id, use it. Otherwise, create a new - // one based on the supplied uid. - targetCache = initializeProcessUidCache(getComponents(), uid); - } - try { - targetCacheSetSingleSignOnState(account, refreshToken, targetCache); - updateApplicationMetadataCache( - refreshToken.getClientId(), - refreshToken.getEnvironment(), - refreshToken.getFamilyId(), - uid - ); - } catch (ClientException e) { - Logger.warn( - TAG + methodName, - "Failed to save account/refresh token. Skipping." - ); - } - } - - // Suppressing unchecked warning as the generic type was not provided for targetCache - @SuppressWarnings(WarningType.unchecked_warning) - private void targetCacheSetSingleSignOnState(@NonNull GenericAccount account, @NonNull GenericRefreshToken refreshToken, MsalOAuth2TokenCache targetCache) throws ClientException { - targetCache.setSingleSignOnState(account, refreshToken); - } } diff --git a/common4j/src/main/com/microsoft/identity/common/java/flighting/CommonFlight.java b/common4j/src/main/com/microsoft/identity/common/java/flighting/CommonFlight.java index 8715031ece..ea736d04ca 100644 --- a/common4j/src/main/com/microsoft/identity/common/java/flighting/CommonFlight.java +++ b/common4j/src/main/com/microsoft/identity/common/java/flighting/CommonFlight.java @@ -180,7 +180,12 @@ public enum CommonFlight implements IFlightConfig { * Flight to enable OpenID issuer validation code which validates issuer against the open id well known * config endpoint and only reports the failure result. */ - ENABLE_OPENID_ISSUER_VALIDATION_REPORTING("EnableOpenIdIssuerValidationReporting", true); + ENABLE_OPENID_ISSUER_VALIDATION_REPORTING("EnableOpenIdIssuerValidationReporting", true), + + /** + * Flight to control whether or not to use in memory cache for accounts and credentials. + */ + USE_IN_MEMORY_CACHE_FOR_ACCOUNTS_AND_CREDENTIALS("UseInMemoryCacheForAccountsAndCredentials", false); private String key; private Object defaultValue; diff --git a/common4j/src/main/com/microsoft/identity/common/java/opentelemetry/AttributeName.java b/common4j/src/main/com/microsoft/identity/common/java/opentelemetry/AttributeName.java index ec86d11b18..007fc894fb 100644 --- a/common4j/src/main/com/microsoft/identity/common/java/opentelemetry/AttributeName.java +++ b/common4j/src/main/com/microsoft/identity/common/java/opentelemetry/AttributeName.java @@ -489,6 +489,11 @@ public enum AttributeName { */ is_in_web_cp_flow, + /** + * Indicates whether or not in memory cache is used for accounts and credentials. + */ + in_memory_cache_used_for_accounts_and_credentials, + /** * Passkey operation type (e.g., registration, authentication). */ @@ -497,5 +502,5 @@ public enum AttributeName { /** * Passkey DOM exception name (if any). */ - passkey_dom_exception_name, + passkey_dom_exception_name } diff --git a/common4j/versioning/version.properties b/common4j/versioning/version.properties index bb5b1c93f6..4abce1fb15 100644 --- a/common4j/versioning/version.properties +++ b/common4j/versioning/version.properties @@ -1,4 +1,4 @@ #Wed May 12 20:08:39 UTC 2021 -versionName=23.0.2 +versionName=23.1.1 versionCode=1 latestPatchVersion=227 diff --git a/versioning/version.properties b/versioning/version.properties index de21aafe25..b22efa905f 100644 --- a/versioning/version.properties +++ b/versioning/version.properties @@ -1,4 +1,4 @@ #Tue Apr 06 22:55:08 UTC 2021 -versionName=23.0.2 +versionName=23.1.1 versionCode=1 latestPatchVersion=234