Skip to content

Commit 0b88339

Browse files
authored
Web Apps account id registry, Fixes AB#3406762 (#2787)
[AB#3406762](https://identitydivision.visualstudio.com/Engineering/_workitems/edit/3406762) ### Summary The WebAppsAccountIdRegistry maps account ids to sets of client ids. This class is created for the purpose of the Edge TB APIs work, where it will be used for the getToken operation (silent case where we want to check that the user has signed into this webapp before) and the SignOut operation (to remember which web apps we need to sign out of). Broker PR for the SignOut operation using this registry will follow.
1 parent afbc7b2 commit 0b88339

File tree

3 files changed

+333
-0
lines changed

3 files changed

+333
-0
lines changed

changelog.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ vNext
1212
- [MINOR] Added error handling when webcp redirects have browser protocol #2767
1313
- [PATCH] Fix for app link redirect from CCT due to forced browser preference (#2775)
1414
- [MINOR] getAllSsoTokens method for Edge (#2774)
15+
- [MINOR] WebApps AccountId Registry (#2787)
1516

1617
Version 22.1.3
1718
----------
Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// All rights reserved.
3+
//
4+
// This code is licensed under the MIT License.
5+
//
6+
// Permission is hereby granted, free of charge, to any person obtaining a copy
7+
// of this software and associated documentation files(the "Software"), to deal
8+
// in the Software without restriction, including without limitation the rights
9+
// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
10+
// copies of the Software, and to permit persons to whom the Software is
11+
// furnished to do so, subject to the following conditions :
12+
//
13+
// The above copyright notice and this permission notice shall be included in
14+
// all copies or substantial portions of the Software.
15+
//
16+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22+
// THE SOFTWARE.
23+
package com.microsoft.identity.common.internal.cache
24+
25+
import com.microsoft.identity.common.java.cache.IMultiTypeNameValueStorage
26+
import com.microsoft.identity.common.java.interfaces.IStorageSupplier
27+
import com.microsoft.identity.common.java.util.ObjectMapper
28+
import com.microsoft.identity.common.logging.Logger
29+
import java.util.concurrent.locks.ReentrantReadWriteLock
30+
import kotlin.concurrent.read
31+
import kotlin.concurrent.write
32+
33+
/**
34+
* A registry that maps home account IDs to sets of web app client IDs.
35+
*
36+
* This is used to track which web applications are associated with which accounts,
37+
* enabling coordinated sign-out and management of web app sessions.
38+
*/
39+
class WebAppsAccountIdRegistry private constructor(
40+
private val storage: IMultiTypeNameValueStorage
41+
){
42+
companion object {
43+
private val TAG = WebAppsAccountIdRegistry::class.java.simpleName
44+
private const val WEBAPPS_ACCOUNT_ID_REGISTRY_STORAGE_KEY = "WebAppsAccountIdRegistryStorageKey"
45+
private val rwLock = ReentrantReadWriteLock()
46+
47+
/**
48+
* Factory method to create an instance of [WebAppsAccountIdRegistry].
49+
*
50+
* @param supplier The storage supplier.
51+
* @return A new instance of [WebAppsAccountIdRegistry].
52+
*/
53+
fun create(supplier: IStorageSupplier): WebAppsAccountIdRegistry {
54+
val store = supplier.getEncryptedFileStore(WEBAPPS_ACCOUNT_ID_REGISTRY_STORAGE_KEY)
55+
return WebAppsAccountIdRegistry(store)
56+
}
57+
}
58+
59+
/**
60+
* Deserialize a JSON string into a set of client IDs.
61+
*
62+
* @param raw The JSON string to deserialize.
63+
* @return A mutable set of client IDs.
64+
*/
65+
private fun deserializeSet(raw: String?): MutableSet<String> {
66+
if (raw.isNullOrBlank()) return mutableSetOf()
67+
return try {
68+
ObjectMapper.deserializeJsonStringToObject(raw, Array<String>::class.java).toMutableSet()
69+
} catch (e: Exception) {
70+
Logger.warn(TAG, "Failed to deserialize set: ${e.message}")
71+
mutableSetOf()
72+
}
73+
}
74+
75+
/**
76+
* Serialize the set of client IDs to a JSON string.
77+
*
78+
* @param set The set of client IDs to serialize.
79+
* @return The JSON string representation of the set.
80+
*/
81+
private fun serializeSet(set: Set<String>): String {
82+
return ObjectMapper.serializeObjectToJsonString(set)
83+
}
84+
85+
/**
86+
* Load the account entry from storage.
87+
*
88+
* @param homeAccountId The home account ID to load.
89+
* @return A set of client IDs associated with the given account ID. Or an empty set if none exist.
90+
*/
91+
private fun loadClientIdsForAccount(homeAccountId: String): MutableSet<String>{
92+
return deserializeSet(storage.getString(homeAccountId))
93+
}
94+
95+
/**
96+
* Save the account entry to storage.
97+
*
98+
* @param homeAccountId The home account ID.
99+
* @param set The set of client IDs to associate with the account ID.
100+
*/
101+
private fun saveAccount(homeAccountId: String, set: Set<String>) {
102+
storage.putString(homeAccountId, serializeSet(set))
103+
}
104+
105+
106+
/**
107+
* Remove the account entry from storage.
108+
*
109+
* @param homeAccountId The account ID to remove.
110+
*/
111+
private fun removeAccountStorage(homeAccountId: String) {
112+
try {
113+
storage.remove(homeAccountId)
114+
} catch (e: Exception) {
115+
storage.putString(homeAccountId, null)
116+
}
117+
}
118+
119+
/**
120+
* Add a client ID to the set associated with the given account ID.
121+
*
122+
* @param homeAccountId The account ID.
123+
* @param clientId The client ID to add.
124+
*/
125+
fun addClient(homeAccountId: String, clientId: String) {
126+
rwLock.write {
127+
val set = loadClientIdsForAccount(homeAccountId)
128+
if (set.add(clientId)) {
129+
saveAccount(homeAccountId, set)
130+
}
131+
}
132+
}
133+
/**
134+
* Remove a client ID from the set associated with the given account ID.
135+
* If the set becomes empty after removal, the account ID is also removed from the registry.
136+
*
137+
* @param homeAccountId The account ID.
138+
* @param clientId The client ID to remove.
139+
*/
140+
fun removeClient(homeAccountId: String, clientId: String) {
141+
rwLock.write {
142+
val set = loadClientIdsForAccount(homeAccountId)
143+
if (!set.remove(clientId)) return
144+
if (set.isEmpty()) {
145+
removeAccountStorage(homeAccountId)
146+
} else {
147+
saveAccount(homeAccountId, set)
148+
}
149+
}
150+
}
151+
152+
/** Get the set of client IDs associated with the given account ID.
153+
*
154+
* @param homeAccountId The account ID.
155+
* @return A set of client IDs associated with the account ID, or an empty set if none exist.
156+
*/
157+
fun getClients(homeAccountId: String): Set<String> {
158+
return rwLock.read {
159+
loadClientIdsForAccount(homeAccountId).toSet()
160+
}
161+
}
162+
163+
/** Check if the registry contains the given client ID for the specified account ID.
164+
*
165+
* @param homeAccountId The account ID.
166+
* @param clientId The client ID to check for.
167+
* @return True if the client ID is associated with the account ID, false otherwise.
168+
*/
169+
fun contains(homeAccountId: String, clientId: String): Boolean {
170+
return rwLock.read {
171+
loadClientIdsForAccount(homeAccountId).contains(clientId)
172+
}
173+
}
174+
175+
/**
176+
* Remove the given account ID and all its associated client IDs from the registry.
177+
*
178+
* @param homeAccountId The account ID to remove.
179+
*/
180+
fun removeAccount(homeAccountId: String) {
181+
rwLock.write {
182+
val set = loadClientIdsForAccount(homeAccountId)
183+
if (set.isEmpty()) return
184+
removeAccountStorage(homeAccountId)
185+
}
186+
}
187+
}
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// All rights reserved.
3+
//
4+
// This code is licensed under the MIT License.
5+
//
6+
// Permission is hereby granted, free of charge, to any person obtaining a copy
7+
// of this software and associated documentation files(the "Software"), to deal
8+
// in the Software without restriction, including without limitation the rights
9+
// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
10+
// copies of the Software, and to permit persons to whom the Software is
11+
// furnished to do so, subject to the following conditions :
12+
//
13+
// The above copyright notice and this permission notice shall be included in
14+
// all copies or substantial portions of the Software.
15+
//
16+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22+
// THE SOFTWARE.
23+
package com.microsoft.identity.common.internal.cache
24+
25+
import com.microsoft.identity.common.components.InMemoryStorageSupplier
26+
import org.junit.Assert
27+
import org.junit.Test
28+
29+
class WebAppsAccountIdRegistryTest {
30+
private val accountId1 = "11111111-1111-1111-1111-111111111111.aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"
31+
private val accountId2 = "22222222-2222-2222-2222-222222222222.bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb"
32+
private val accountId3 = "33333333-3333-3333-3333-333333333333.cccccccc-cccc-cccc-cccc-cccccccccccc"
33+
34+
private val clientId1 = "99999999-1111-4444-8888-121212121212"
35+
private val clientId2 = "aaaaaaaa-2222-5555-9999-131313131313"
36+
private val clientId3 = "bbbbbbbb-3333-6666-aaaa-141414141414"
37+
38+
@Test
39+
fun testAddClient_veryBasicTest() {
40+
val registry = createRegistry()
41+
registry.addClient(accountId1, clientId1)
42+
Assert.assertEquals(1, registry.getClients(accountId1).size)
43+
}
44+
45+
@Test
46+
fun testRemoveClient_veryBasicTest() {
47+
val registry = createRegistry()
48+
registry.addClient(accountId1, clientId1)
49+
registry.addClient(accountId1, clientId2)
50+
Assert.assertEquals(2, registry.getClients(accountId1).size)
51+
registry.removeClient(accountId1, clientId1)
52+
Assert.assertEquals(1, registry.getClients(accountId1).size)
53+
}
54+
55+
@Test
56+
fun testRemoveClient_accountEntryCleanedUp() {
57+
val registry = createRegistry()
58+
registry.addClient(accountId1, clientId1)
59+
registry.removeClient(accountId1, clientId1)
60+
Assert.assertEquals(0, registry.getClients(accountId1).size)
61+
}
62+
63+
@Test
64+
fun testManyCombinedAddAndRemove() {
65+
val registry = createRegistry()
66+
registry.addClient(accountId1, clientId1)
67+
registry.addClient(accountId1, clientId2)
68+
registry.addClient(accountId2, clientId1)
69+
registry.addClient(accountId2, clientId3)
70+
registry.addClient(accountId3, clientId1)
71+
Assert.assertEquals(2, registry.getClients(accountId1).size)
72+
Assert.assertEquals(2, registry.getClients(accountId2).size)
73+
Assert.assertEquals(1, registry.getClients(accountId3).size)
74+
75+
registry.removeClient(accountId1, clientId1)
76+
Assert.assertEquals(1, registry.getClients(accountId1).size)
77+
78+
registry.removeClient(accountId1, clientId2)
79+
Assert.assertEquals(0, registry.getClients(accountId1).size)
80+
81+
registry.removeClient(accountId2, clientId3)
82+
Assert.assertEquals(1, registry.getClients(accountId2).size)
83+
84+
registry.removeClient(accountId2, clientId1)
85+
Assert.assertEquals(0, registry.getClients(accountId2).size)
86+
87+
registry.removeClient(accountId3, clientId1)
88+
Assert.assertEquals(0, registry.getClients(accountId3).size)
89+
}
90+
91+
@Test
92+
fun testAddClient_addSameClient() {
93+
val registry = createRegistry()
94+
registry.addClient(accountId1, clientId1)
95+
registry.addClient(accountId1, clientId1)
96+
Assert.assertEquals(1, registry.getClients(accountId1).size)
97+
}
98+
99+
@Test
100+
fun testRemoveAccount_removeAccount() {
101+
val registry = createRegistry()
102+
registry.addClient(accountId1, clientId1)
103+
registry.addClient(accountId1, clientId2)
104+
registry.addClient(accountId2, clientId1)
105+
registry.removeAccount(accountId1)
106+
Assert.assertEquals(0, registry.getClients(accountId1).size)
107+
Assert.assertEquals(1, registry.getClients(accountId2).size)
108+
}
109+
110+
@Test
111+
fun testContains_containsClientId() {
112+
val registry = createRegistry()
113+
registry.addClient(accountId1, clientId1)
114+
Assert.assertTrue(registry.contains(accountId1, clientId1))
115+
Assert.assertFalse(registry.contains(accountId1, clientId2))
116+
Assert.assertFalse(registry.contains(accountId2, clientId1))
117+
}
118+
119+
@Test
120+
fun testPersistenceAcrossInstances() {
121+
val storageSupplier = InMemoryStorageSupplier()
122+
val registry1 = WebAppsAccountIdRegistry.create(storageSupplier)
123+
registry1.addClient(accountId1, clientId1)
124+
registry1.addClient(accountId1, clientId2)
125+
registry1.addClient(accountId2, clientId1)
126+
127+
val registry2 = WebAppsAccountIdRegistry.create(storageSupplier)
128+
Assert.assertEquals(2, registry2.getClients(accountId1).size)
129+
Assert.assertEquals(1, registry2.getClients(accountId2).size)
130+
131+
registry2.removeClient(accountId1, clientId1)
132+
Assert.assertEquals(1, registry2.getClients(accountId1).size)
133+
134+
val registry3 = WebAppsAccountIdRegistry.create(storageSupplier)
135+
Assert.assertEquals(1, registry3.getClients(accountId1).size)
136+
Assert.assertEquals(1, registry3.getClients(accountId2).size)
137+
138+
registry3.removeAccount(accountId2)
139+
Assert.assertEquals(0, registry3.getClients(accountId2).size)
140+
}
141+
142+
private fun createRegistry(): WebAppsAccountIdRegistry {
143+
return WebAppsAccountIdRegistry.create(InMemoryStorageSupplier())
144+
}
145+
}

0 commit comments

Comments
 (0)