Skip to content

Commit a42e596

Browse files
authored
Merge branch 'dev' into somalaya/takeFlightValFromOneAuth
2 parents bc1aafc + 0b88339 commit a42e596

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)