Skip to content

Commit 228db78

Browse files
siddhijainCopilot
andauthored
Share SharedPreferencesInMemoryCache across cache instances Fixes AB#3428107 (#2813)
Fixes [AB#3428107](https://identitydivision.visualstudio.com/fac9d424-53d2-45c0-91b5-ef6ba7a6bf26/_workitems/edit/3428107) Proposing to use static instance of in-memory cache layer for performance benefits. --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent 4ab4b64 commit 228db78

File tree

3 files changed

+200
-84
lines changed

3 files changed

+200
-84
lines changed

changelog.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
vNext
22
----------
3+
- [MINOR] Share SharedPreferencesInMemoryCache across instances of BrokerOAuth2TokenCache
34
- [MINOR] Use SharedPreferencesInMemoryCache implementation in Broker (#2802)
45
- [MINOR] Add OpenTelemetry support for passkey operations (#2795)
56
- [MINOR] Add passkey registration support for WebView (#2769)

common/src/test/java/com/microsoft/identity/common/BrokerOAuth2TokenCacheTest.java

Lines changed: 127 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,16 +42,17 @@
4242
import static org.junit.Assert.assertEquals;
4343
import static org.junit.Assert.assertFalse;
4444
import static org.junit.Assert.assertNotNull;
45+
import static org.junit.Assert.assertNotSame;
4546
import static org.junit.Assert.assertNull;
47+
import static org.junit.Assert.assertSame;
4648
import static org.junit.Assert.assertTrue;
4749
import static org.junit.Assert.fail;
4850
import static org.mockito.Mockito.when;
4951

50-
import android.content.Context;
5152

53+
import androidx.annotation.NonNull;
5254
import androidx.test.core.app.ApplicationProvider;
5355

54-
import com.microsoft.identity.common.components.AndroidPlatformComponentsFactory;
5556
import com.microsoft.identity.common.components.MockPlatformComponentsFactory;
5657
import com.microsoft.identity.common.internal.platform.AndroidPlatformUtil;
5758
import com.microsoft.identity.common.java.cache.BrokerApplicationMetadata;
@@ -66,10 +67,15 @@
6667
import com.microsoft.identity.common.java.cache.SharedPreferencesAccountCredentialCache;
6768
import com.microsoft.identity.common.java.cache.AccountDeletionRecord;
6869
import com.microsoft.identity.common.java.cache.ICacheRecord;
70+
import com.microsoft.identity.common.java.cache.SharedPreferencesAccountCredentialCacheWithMemoryCache;
6971
import com.microsoft.identity.common.java.dto.AccountRecord;
7072
import com.microsoft.identity.common.java.dto.Credential;
7173
import com.microsoft.identity.common.java.dto.CredentialType;
7274
import com.microsoft.identity.common.java.exception.ClientException;
75+
import com.microsoft.identity.common.java.flighting.CommonFlight;
76+
import com.microsoft.identity.common.java.flighting.CommonFlightsManager;
77+
import com.microsoft.identity.common.java.flighting.IFlightsManager;
78+
import com.microsoft.identity.common.java.flighting.IFlightsProvider;
7379
import com.microsoft.identity.common.java.interfaces.INameValueStorage;
7480
import com.microsoft.identity.common.java.interfaces.IPlatformComponents;
7581
import com.microsoft.identity.common.java.providers.microsoft.MicrosoftAccount;
@@ -79,14 +85,15 @@
7985
import com.microsoft.identity.common.java.providers.oauth2.OAuth2TokenCache;
8086
import com.microsoft.identity.common.shadows.ShadowAndroidSdkStorageEncryptionManager;
8187

88+
import org.jetbrains.annotations.NotNull;
8289
import org.junit.After;
8390
import org.junit.Before;
8491
import org.junit.Test;
8592
import org.junit.runner.RunWith;
93+
import org.mockito.Mockito;
8694
import org.powermock.api.mockito.PowerMockito;
8795
import org.robolectric.RobolectricTestRunner;
8896
import org.robolectric.annotation.Config;
89-
import org.robolectric.shadows.ShadowSharedPreferences;
9097

9198
import java.util.ArrayList;
9299
import java.util.List;
@@ -236,6 +243,7 @@ public void tearDown() throws Exception {
236243
}
237244

238245
mApplicationMetadataCache.clear();
246+
CommonFlightsManager.INSTANCE.resetFlightsManager();
239247
}
240248

241249
private void initOtherCaches(final IPlatformComponents components) {
@@ -1241,4 +1249,120 @@ public void testClearAll() throws ClientException {
12411249
assertEquals(false, mBrokerOAuth2TokenCache.isClientIdKnownToCache(clientId));
12421250
}
12431251
}
1252+
1253+
@Test
1254+
public void testSingleCacheInstancePerStoreName_FlightEnabled() {
1255+
// Enable the flight
1256+
updateUseInMemoryCacheFlight(true);
1257+
1258+
final String storeName = "test_store_name";
1259+
final IPlatformComponents components1 = mPlatformComponents;
1260+
final IPlatformComponents components2 = mPlatformComponents;
1261+
1262+
// Call getCacheToBeUsed twice with the same storeName
1263+
final IAccountCredentialCache cache1 = BrokerOAuth2TokenCache.getCacheToBeUsed(components1, storeName);
1264+
final IAccountCredentialCache cache2 = BrokerOAuth2TokenCache.getCacheToBeUsed(components2, storeName);
1265+
1266+
// Verify both references point to the same instance
1267+
assertNotNull(cache1);
1268+
assertNotNull(cache2);
1269+
assertSame("Expected same cache instance for same storeName", cache1, cache2);
1270+
assertTrue("Cache should be of type SharedPreferencesAccountCredentialCacheWithMemoryCache",
1271+
cache1 instanceof SharedPreferencesAccountCredentialCacheWithMemoryCache);
1272+
}
1273+
1274+
@Test
1275+
public void testDifferentCacheInstancesPerStoreName_FlightEnabled() {
1276+
// Enable the flight
1277+
updateUseInMemoryCacheFlight(true);
1278+
1279+
final String storeName1 = "test_store_name_1";
1280+
final String storeName2 = "test_store_name_2";
1281+
final IPlatformComponents components = mPlatformComponents;
1282+
1283+
// Call getCacheToBeUsed with different storeNames
1284+
final IAccountCredentialCache cache1 = BrokerOAuth2TokenCache.getCacheToBeUsed(components, storeName1);
1285+
final IAccountCredentialCache cache2 = BrokerOAuth2TokenCache.getCacheToBeUsed(components, storeName2);
1286+
1287+
// Verify both are valid but different instances
1288+
assertNotNull(cache1);
1289+
assertNotNull(cache2);
1290+
assertNotSame("Expected different cache instances for different storeNames", cache1, cache2);
1291+
assertTrue("Cache should be of type SharedPreferencesAccountCredentialCacheWithMemoryCache",
1292+
cache1 instanceof SharedPreferencesAccountCredentialCacheWithMemoryCache);
1293+
assertTrue("Cache should be of type SharedPreferencesAccountCredentialCacheWithMemoryCache",
1294+
cache2 instanceof SharedPreferencesAccountCredentialCacheWithMemoryCache);
1295+
}
1296+
1297+
@Test
1298+
public void testCacheInstanceReusedAcrossMultipleBrokerTokenCaches_FlightEnabled() {
1299+
// Enable the flight
1300+
updateUseInMemoryCacheFlight(true);
1301+
1302+
final String storeName = getBrokerUidSequesteredFilename(TEST_APP_UID);
1303+
1304+
// Create multiple BrokerOAuth2TokenCache instances
1305+
final BrokerOAuth2TokenCache tokenCache1 = new BrokerOAuth2TokenCache
1306+
(mPlatformComponents,
1307+
TEST_APP_UID,
1308+
new NameValueStorageBrokerApplicationMetadataCache(mPlatformComponents));
1309+
final BrokerOAuth2TokenCache tokenCache2 = new BrokerOAuth2TokenCache(mPlatformComponents, TEST_APP_UID,
1310+
new NameValueStorageBrokerApplicationMetadataCache(mPlatformComponents));
1311+
1312+
// Get the underlying account credential caches
1313+
final IAccountCredentialCache cache1 = tokenCache1.getCacheToBeUsed(mPlatformComponents, storeName);
1314+
final IAccountCredentialCache cache2 = tokenCache2.getCacheToBeUsed(mPlatformComponents, storeName);
1315+
1316+
// Verify same instance is reused
1317+
assertNotNull(cache1);
1318+
assertNotNull(cache2);
1319+
assertSame(cache1, cache2);
1320+
}
1321+
1322+
@Test
1323+
public void testFociCacheInstanceReused_FlightEnabled() {
1324+
// Enable the flight
1325+
updateUseInMemoryCacheFlight(true);
1326+
1327+
final String fociStoreName = BROKER_FOCI_ACCOUNT_CREDENTIAL_SHARED_PREFERENCES;
1328+
1329+
// Call getCacheToBeUsed multiple times for FOCI cache
1330+
final IAccountCredentialCache fociCache1 = BrokerOAuth2TokenCache.getCacheToBeUsed(mPlatformComponents, fociStoreName);
1331+
final IAccountCredentialCache fociCache2 = BrokerOAuth2TokenCache.getCacheToBeUsed(mPlatformComponents, fociStoreName);
1332+
1333+
// Verify same FOCI cache instance is reused
1334+
assertNotNull(fociCache1);
1335+
assertNotNull(fociCache2);
1336+
assertSame(fociCache1, fociCache2);
1337+
}
1338+
1339+
private void updateUseInMemoryCacheFlight(boolean enabled) {
1340+
final IFlightsProvider mockFlightsProvider = Mockito.mock(IFlightsProvider.class);
1341+
Mockito.when(mockFlightsProvider.isFlightEnabled(CommonFlight.USE_IN_MEMORY_CACHE_FOR_ACCOUNTS_AND_CREDENTIALS))
1342+
.thenReturn(enabled);
1343+
1344+
// Create anonymous IFlightsManager
1345+
IFlightsManager anonymousFlightsManager = new IFlightsManager() {
1346+
@Override
1347+
public @NotNull IFlightsProvider getFlightsProvider(long waitForConfigsWithTimeoutInMs) {
1348+
return mockFlightsProvider;
1349+
}
1350+
@Override
1351+
public @NotNull IFlightsProvider getFlightsProviderForTenant(@NotNull String tenantId, long waitForConfigsWithTimeoutInMs) {
1352+
return mockFlightsProvider;
1353+
}
1354+
@Override
1355+
public @NotNull IFlightsProvider getFlightsProviderForTenant(@NotNull String tenantId) {
1356+
return mockFlightsProvider;
1357+
}
1358+
@NonNull
1359+
@Override
1360+
public IFlightsProvider getFlightsProvider() {
1361+
return mockFlightsProvider;
1362+
}
1363+
};
1364+
1365+
// Initialize CommonFlightsManager with the anonymous implementation
1366+
CommonFlightsManager.INSTANCE.initializeCommonFlightsManager(anonymousFlightsManager);
1367+
}
12441368
}

common4j/src/main/com/microsoft/identity/common/java/cache/BrokerOAuth2TokenCache.java

Lines changed: 72 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,9 @@
5252
import java.util.HashSet;
5353
import java.util.Iterator;
5454
import java.util.List;
55+
import java.util.Map;
5556
import java.util.Set;
57+
import java.util.concurrent.ConcurrentHashMap;
5658

5759
import edu.umd.cs.findbugs.annotations.Nullable;
5860
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
@@ -104,6 +106,20 @@ public class BrokerOAuth2TokenCache
104106
private final MicrosoftFamilyOAuth2TokenCache mFociCache;
105107
private final int mUid;
106108
private ProcessUidCacheFactory mDelegate = null;
109+
/**
110+
* Shared, process-wide registry of in-memory augmented account/credential caches keyed by
111+
* storage (SharedPreferences) name.
112+
* <p>
113+
* Populated lazily via computeIfAbsent in getCacheToBeUsed(...). Allows multiple
114+
* BrokerOAuth2TokenCache instances to reuse the same in-memory layer for the same underlying
115+
* encrypted name-value store, reducing disk I/O and serialization overhead.
116+
* <p>
117+
* Thread-safety: ConcurrentHashMap ensures safe concurrent access and publication.
118+
* Lifecycle: Entries are never removed for the lifetime of the process.
119+
*/
120+
private static final Map<String, SharedPreferencesAccountCredentialCacheWithMemoryCache>
121+
inMemoryCacheMapByStorage = new ConcurrentHashMap<>();
122+
107123

108124
/**
109125
* Constructs a new BrokerOAuth2TokenCache.
@@ -1337,6 +1353,7 @@ public void clearAll() {
13371353

13381354
this.mFociCache.clearAll();
13391355
this.mApplicationMetadataCache.clear();
1356+
inMemoryCacheMapByStorage.clear();
13401357
}
13411358

13421359
/**
@@ -1611,14 +1628,10 @@ private MsalOAuth2TokenCache initializeProcessUidCache(@NonNull final IPlatformC
16111628
return mDelegate.getTokenCache(components, uid);
16121629
}
16131630

1614-
final INameValueStorage<String> sharedPreferencesFileManager =
1615-
components.getStorageSupplier().getEncryptedNameValueStore(
1616-
SharedPreferencesAccountCredentialCache
1617-
.getBrokerUidSequesteredFilename(uid),
1618-
String.class
1619-
);
1631+
final String storeName = SharedPreferencesAccountCredentialCache
1632+
.getBrokerUidSequesteredFilename(uid);
16201633

1621-
return getTokenCache(components, sharedPreferencesFileManager, false);
1634+
return getTokenCache(components, storeName, false);
16221635
}
16231636

16241637
private static MicrosoftFamilyOAuth2TokenCache initializeFociCache(@NonNull final IPlatformComponents components) {
@@ -1628,37 +1641,16 @@ private static MicrosoftFamilyOAuth2TokenCache initializeFociCache(@NonNull fina
16281641
"Initializing foci cache"
16291642
);
16301643

1631-
final INameValueStorage<String> sharedPreferencesFileManager =
1632-
components.getStorageSupplier().getEncryptedNameValueStore(
1633-
SharedPreferencesAccountCredentialCache.BROKER_FOCI_ACCOUNT_CREDENTIAL_SHARED_PREFERENCES,
1634-
String.class
1635-
);
1644+
final String storeName = SharedPreferencesAccountCredentialCache.BROKER_FOCI_ACCOUNT_CREDENTIAL_SHARED_PREFERENCES;
16361645

1637-
return getTokenCache(components, sharedPreferencesFileManager, true);
1646+
return getTokenCache(components, storeName,true);
16381647
}
16391648

16401649
@SuppressWarnings(UNCHECKED)
16411650
private static <T extends MsalOAuth2TokenCache> T getTokenCache(@NonNull final IPlatformComponents components,
1642-
@NonNull final INameValueStorage<String> spfm,
1651+
String storeName,
16431652
boolean isFoci) {
1644-
final ICacheKeyValueDelegate cacheKeyValueDelegate = new CacheKeyValueDelegate();
1645-
final boolean isFlightEnabled = CommonFlightsManager.INSTANCE
1646-
.getFlightsProvider()
1647-
.isFlightEnabled(CommonFlight.USE_IN_MEMORY_CACHE_FOR_ACCOUNTS_AND_CREDENTIALS);
1648-
final IAccountCredentialCache accountCredentialCache;
1649-
if (isFlightEnabled) {
1650-
accountCredentialCache = new SharedPreferencesAccountCredentialCacheWithMemoryCache(
1651-
cacheKeyValueDelegate,
1652-
spfm
1653-
);
1654-
SpanExtension.current().setAttribute(AttributeName.in_memory_cache_used_for_accounts_and_credentials.name(), true);
1655-
} else {
1656-
accountCredentialCache = new SharedPreferencesAccountCredentialCache(
1657-
cacheKeyValueDelegate,
1658-
spfm
1659-
);
1660-
SpanExtension.current().setAttribute(AttributeName.in_memory_cache_used_for_accounts_and_credentials.name(), false);
1661-
}
1653+
final IAccountCredentialCache accountCredentialCache = getCacheToBeUsed(components, storeName);
16621654
final MicrosoftStsAccountCredentialAdapter accountCredentialAdapter =
16631655
new MicrosoftStsAccountCredentialAdapter();
16641656

@@ -1678,6 +1670,54 @@ private static <T extends MsalOAuth2TokenCache> T getTokenCache(@NonNull final I
16781670
);
16791671
}
16801672

1673+
1674+
/**
1675+
* Determines which cache implementation to use based on flighting.
1676+
* <p>
1677+
* When the flight {@code USE_IN_MEMORY_CACHE_FOR_ACCOUNTS_AND_CREDENTIALS} is enabled,
1678+
* returns a shared, cached instance of {@link SharedPreferencesAccountCredentialCacheWithMemoryCache}
1679+
* for the given storage, improving performance by reusing the in-memory cache layer across cache instances.
1680+
* When disabled, returns a new {@link SharedPreferencesAccountCredentialCache} instance.
1681+
* <p>
1682+
* Critical behavior: When flight is enabled, the same {@code SharedPreferencesAccountCredentialCacheWithMemoryCache}
1683+
* instance is returned for the same {@code spfm} reference, meaning the cache is shared across multiple
1684+
* {@code BrokerOAuth2TokenCache} instances.
1685+
* <p>
1686+
* Thread-safety: This method is thread-safe via {@code ConcurrentHashMap.computeIfAbsent}.
1687+
* <p>
1688+
* Lifecycle: Returned cached instances are never removed from the static map.
1689+
*
1690+
* @return A cached shared in-memory cache instance (flight enabled) or a new
1691+
* non-cached instance (flight disabled).
1692+
*/
1693+
public static IAccountCredentialCache getCacheToBeUsed(@NonNull final IPlatformComponents components,
1694+
final String storeName) {
1695+
final boolean isFlightEnabled = CommonFlightsManager.INSTANCE
1696+
.getFlightsProvider()
1697+
.isFlightEnabled(CommonFlight.USE_IN_MEMORY_CACHE_FOR_ACCOUNTS_AND_CREDENTIALS);
1698+
SpanExtension.current().setAttribute(AttributeName.in_memory_cache_used_for_accounts_and_credentials.name(), isFlightEnabled);
1699+
if (isFlightEnabled) {
1700+
return inMemoryCacheMapByStorage.computeIfAbsent(storeName, s ->
1701+
new SharedPreferencesAccountCredentialCacheWithMemoryCache(
1702+
new CacheKeyValueDelegate(),
1703+
components.getStorageSupplier().getEncryptedNameValueStore(
1704+
storeName,
1705+
String.class
1706+
))
1707+
);
1708+
} else {
1709+
final INameValueStorage<String> sharedPreferencesFileManager =
1710+
components.getStorageSupplier().getEncryptedNameValueStore(
1711+
storeName,
1712+
String.class
1713+
);
1714+
return new SharedPreferencesAccountCredentialCache(
1715+
new CacheKeyValueDelegate(),
1716+
sharedPreferencesFileManager
1717+
);
1718+
}
1719+
}
1720+
16811721
@Nullable
16821722
private MsalOAuth2TokenCache getTokenCacheForClient(@Nullable final BrokerApplicationMetadata metadata) {
16831723
final String methodName = ":getTokenCacheForClient(bam)";
@@ -1736,53 +1776,4 @@ private MsalOAuth2TokenCache getTokenCacheForClient(@NonNull final String client
17361776

17371777
return getTokenCacheForClient(metadata);
17381778
}
1739-
1740-
/**
1741-
* Sets the SSO state for the supplied Account, relative to the provided uid.
1742-
*
1743-
* @param uidStr The uid of the app whose SSO token is being inserted.
1744-
* @param account The account for which the supplied token is being inserted.
1745-
* @param refreshToken The token to insert.
1746-
*/
1747-
public void setSingleSignOnState(@NonNull final String uidStr,
1748-
@NonNull final GenericAccount account,
1749-
@NonNull final GenericRefreshToken refreshToken) {
1750-
final String methodName = ":setSingleSignOnState";
1751-
1752-
final boolean isFrt = refreshToken.getIsFamilyRefreshToken();
1753-
1754-
MsalOAuth2TokenCache targetCache;
1755-
1756-
final int uid = Integer.parseInt(uidStr);
1757-
1758-
if (isFrt) {
1759-
Logger.verbose(TAG + methodName, "Saving tokens to foci cache.");
1760-
1761-
targetCache = mFociCache;
1762-
} else {
1763-
// If there is an existing cache for this client id, use it. Otherwise, create a new
1764-
// one based on the supplied uid.
1765-
targetCache = initializeProcessUidCache(getComponents(), uid);
1766-
}
1767-
try {
1768-
targetCacheSetSingleSignOnState(account, refreshToken, targetCache);
1769-
updateApplicationMetadataCache(
1770-
refreshToken.getClientId(),
1771-
refreshToken.getEnvironment(),
1772-
refreshToken.getFamilyId(),
1773-
uid
1774-
);
1775-
} catch (ClientException e) {
1776-
Logger.warn(
1777-
TAG + methodName,
1778-
"Failed to save account/refresh token. Skipping."
1779-
);
1780-
}
1781-
}
1782-
1783-
// Suppressing unchecked warning as the generic type was not provided for targetCache
1784-
@SuppressWarnings(WarningType.unchecked_warning)
1785-
private void targetCacheSetSingleSignOnState(@NonNull GenericAccount account, @NonNull GenericRefreshToken refreshToken, MsalOAuth2TokenCache targetCache) throws ClientException {
1786-
targetCache.setSingleSignOnState(account, refreshToken);
1787-
}
17881779
}

0 commit comments

Comments
 (0)