Skip to content

Commit 3faf17b

Browse files
p3dr0rvCopilot
andauthored
Implement TenantUtil object with methods to extract tenant and tenantID from identifiers, Fixes AB#3370810 (#2761)
[AB#3370810](https://identitydivision.visualstudio.com/fac9d424-53d2-45c0-91b5-ef6ba7a6bf26/_workitems/edit/3370810) Move getTenantFromIdentifier from WorkPlaceJoinUtil getTenantIdFromLoginHint from AccountChooser see https://github.com/AzureAD/microsoft-authentication-library-common-for-android/pull/2761/files --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent 0a5a5e7 commit 3faf17b

3 files changed

Lines changed: 382 additions & 0 deletions

File tree

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] Implement TenantUtil (#2761)
34
- [MAJOR] Update proguard rules in common (#2756)
45
- [MINOR] Add query parameter for Android Release OS Version (#2754)
56
- [MINOR] Add client scenario to JwtRequestBody (#2755)
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
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.java.util
24+
25+
import com.microsoft.identity.common.java.authorities.AzureActiveDirectoryAudience
26+
import com.microsoft.identity.common.java.logging.Logger
27+
import com.microsoft.identity.common.java.providers.microsoft.azureactivedirectory.AzureActiveDirectory
28+
29+
/**
30+
* Utility object for tenant-related operations.
31+
*
32+
* Provides methods to extract tenant information from various identifier formats
33+
* such as email addresses, UPNs (User Principal Names), and tenant GUIDs.
34+
*/
35+
object TenantUtil {
36+
private const val TAG: String = "TenantUtil"
37+
private val EMAIL_REGEX = Regex("""^[^@]+@[^@]+\.[^@]+$""")
38+
private val UUID_REGEX = Regex("""^[0-9A-Fa-f\-]{36}$""")
39+
40+
41+
/**
42+
* Extracts tenant information from an identifier.
43+
*
44+
* This method can handle two types of identifiers:
45+
* - Email addresses/UPNs: Returns the domain part after the "@" symbol
46+
* - Tenant GUIDs: Returns the GUID as-is
47+
*
48+
* @param identifier The identifier string which could be:
49+
* - An email address or UPN (e.g., "user@contoso.com")
50+
* - A GUID representing a tenant ID (e.g., "12345678-1234-1234-1234-123456789012")
51+
* - Can be null or blank
52+
* @return The extracted tenant (hostname for UPNs or tenant ID for GUIDs),
53+
* or null if the identifier is invalid, null, or blank
54+
*/
55+
fun getTenantFromIdentifier(identifier: String?): String? {
56+
val methodTag = "$TAG:getTenantFromIdentifier"
57+
if (identifier.isNullOrBlank()) {
58+
return null
59+
}
60+
61+
if (UUID_REGEX.matches(identifier)) {
62+
return identifier
63+
}
64+
65+
if (EMAIL_REGEX.matches(identifier)) {
66+
return identifier.substringAfter("@").trim()
67+
}
68+
69+
Logger.warn(methodTag, "Identifier is neither a valid email/UPN nor a GUID.")
70+
return null
71+
}
72+
73+
74+
/**
75+
* Extracts tenant ID from a login hint by resolving the tenant information.
76+
*
77+
* This method first extracts the tenant name from the login hint, then attempts to
78+
* resolve it to a tenant ID by loading the OpenID provider configuration metadata
79+
* for the specified tenant.
80+
*
81+
* @param loginHint The login hint string (e.g., "user@contoso.com")
82+
* @param correlationId Correlation ID for the request, u
83+
* @return The resolved tenant ID if successful, null if the login hint is invalid,
84+
* the tenant cannot be resolved, or if an error occurs during resolution
85+
*/
86+
fun getTenantIdFromLoginHint(loginHint: String?, correlationId: String?): String? {
87+
val methodTag = "$TAG:getTenantIdFromLoginHint"
88+
val tenantName = getTenantFromIdentifier(loginHint) ?: run {
89+
Logger.warn(methodTag, correlationId, "Login hint is invalid or empty.")
90+
return null
91+
}
92+
try {
93+
val configuration =
94+
AzureActiveDirectory.loadOpenIdProviderConfigurationMetadataForTenant(tenantName)
95+
val tenantId =
96+
AzureActiveDirectoryAudience.getTenantIdFromOpenIdProviderConfiguration(configuration)
97+
Logger.info(methodTag, correlationId, "Successfully got tenant ID from login hint.")
98+
return tenantId
99+
} catch (e: Exception) {
100+
Logger.error(methodTag, correlationId, "Failed to get tenant ID from login hint.", e)
101+
return null
102+
}
103+
}
104+
}
Lines changed: 277 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,277 @@
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.java.util
24+
25+
import com.microsoft.identity.common.java.authorities.AzureActiveDirectoryAudience
26+
import com.microsoft.identity.common.java.providers.microsoft.azureactivedirectory.AzureActiveDirectory
27+
import com.microsoft.identity.common.java.providers.oauth2.OpenIdProviderConfiguration
28+
import io.mockk.*
29+
import org.junit.After
30+
import org.junit.Assert.*
31+
import org.junit.Before
32+
import org.junit.Test
33+
34+
/**
35+
* Unit tests for [TenantUtil]
36+
*/
37+
class TenantUtilTest {
38+
39+
@Before
40+
fun setUp() {
41+
// Clear all mocks before each test
42+
clearAllMocks()
43+
}
44+
45+
@After
46+
fun tearDown() {
47+
// Clear all mocks after each test
48+
clearAllMocks()
49+
}
50+
51+
// ===== Tests for getTenantFromIdentifier =====
52+
53+
@Test
54+
fun `getTenantFromIdentifier returns null for null identifier`() {
55+
val result = TenantUtil.getTenantFromIdentifier(null)
56+
assertNull(result)
57+
}
58+
59+
@Test
60+
fun `getTenantFromIdentifier returns null for blank identifier`() {
61+
val result = TenantUtil.getTenantFromIdentifier("")
62+
assertNull(result)
63+
}
64+
65+
@Test
66+
fun `getTenantFromIdentifier returns null for whitespace only identifier`() {
67+
val result = TenantUtil.getTenantFromIdentifier(" ")
68+
assertNull(result)
69+
}
70+
71+
@Test
72+
fun `getTenantFromIdentifier returns tenant ID for valid GUID`() {
73+
val tenantId = "12345678-1234-1234-1234-123456789012"
74+
val result = TenantUtil.getTenantFromIdentifier(tenantId)
75+
assertEquals(tenantId, result)
76+
}
77+
78+
@Test
79+
fun `getTenantFromIdentifier returns tenant ID for valid GUID with uppercase letters`() {
80+
val tenantId = "12345678-ABCD-EFAB-CDEF-123456789012"
81+
val result = TenantUtil.getTenantFromIdentifier(tenantId)
82+
assertEquals(tenantId, result)
83+
}
84+
85+
@Test
86+
fun `getTenantFromIdentifier returns tenant ID for valid GUID with mixed case`() {
87+
val tenantId = "12345678-AbCd-EfAb-CdEf-123456789012"
88+
val result = TenantUtil.getTenantFromIdentifier(tenantId)
89+
assertEquals(tenantId, result)
90+
}
91+
92+
@Test
93+
fun `getTenantFromIdentifier returns domain for valid email address`() {
94+
val email = "user@contoso.com"
95+
val expectedDomain = "contoso.com"
96+
val result = TenantUtil.getTenantFromIdentifier(email)
97+
assertEquals(expectedDomain, result)
98+
}
99+
100+
@Test
101+
fun `getTenantFromIdentifier returns domain for valid UPN with subdomain`() {
102+
val upn = "john.doe@sub.contoso.com"
103+
val expectedDomain = "sub.contoso.com"
104+
val result = TenantUtil.getTenantFromIdentifier(upn)
105+
assertEquals(expectedDomain, result)
106+
}
107+
108+
@Test
109+
fun `getTenantFromIdentifier trims whitespace from extracted domain`() {
110+
val upn = "user@contoso.com "
111+
val expectedDomain = "contoso.com"
112+
val result = TenantUtil.getTenantFromIdentifier(upn)
113+
assertEquals(expectedDomain, result)
114+
}
115+
116+
@Test
117+
fun `getTenantFromIdentifier returns domain for email with multiple dots`() {
118+
val email = "user.name@mail.contoso.com"
119+
val expectedDomain = "mail.contoso.com"
120+
val result = TenantUtil.getTenantFromIdentifier(email)
121+
assertEquals(expectedDomain, result)
122+
}
123+
124+
@Test
125+
fun `getTenantFromIdentifier returns null for invalid GUID format`() {
126+
val invalidGuid = "12345678-1234-1234-1234-12345678901" // Too short
127+
val result = TenantUtil.getTenantFromIdentifier(invalidGuid)
128+
assertNull(result)
129+
}
130+
131+
@Test
132+
fun `getTenantFromIdentifier returns null for GUID with invalid characters`() {
133+
val invalidGuid = "12345678-1234-1234-1234-12345678901G" // Contains 'G'
134+
val result = TenantUtil.getTenantFromIdentifier(invalidGuid)
135+
assertNull(result)
136+
}
137+
138+
@Test
139+
fun `getTenantFromIdentifier returns null for invalid email format missing domain`() {
140+
val invalidEmail = "user@"
141+
val result = TenantUtil.getTenantFromIdentifier(invalidEmail)
142+
assertNull(result)
143+
}
144+
145+
@Test
146+
fun `getTenantFromIdentifier returns null for invalid email format missing at symbol`() {
147+
val invalidEmail = "usercontoso.com"
148+
val result = TenantUtil.getTenantFromIdentifier(invalidEmail)
149+
assertNull(result)
150+
}
151+
152+
@Test
153+
fun `getTenantFromIdentifier returns null for invalid email format missing TLD`() {
154+
val invalidEmail = "user@contoso"
155+
val result = TenantUtil.getTenantFromIdentifier(invalidEmail)
156+
assertNull(result)
157+
}
158+
159+
@Test
160+
fun `getTenantFromIdentifier returns null for malformed identifier`() {
161+
val malformedIdentifier = "not-an-email-or-guid"
162+
val result = TenantUtil.getTenantFromIdentifier(malformedIdentifier)
163+
assertNull(result)
164+
}
165+
166+
// ===== Tests for getTenantIdFromLoginHint =====
167+
168+
@Test
169+
fun `getTenantIdFromLoginHint returns null for null login hint`() {
170+
val result = TenantUtil.getTenantIdFromLoginHint(null, "correlation-id")
171+
assertNull(result)
172+
}
173+
174+
@Test
175+
fun `getTenantIdFromLoginHint returns null for blank login hint`() {
176+
val result = TenantUtil.getTenantIdFromLoginHint("", "correlation-id")
177+
assertNull(result)
178+
}
179+
180+
@Test
181+
fun `getTenantIdFromLoginHint returns null for invalid login hint`() {
182+
val result = TenantUtil.getTenantIdFromLoginHint("invalid-hint", "correlation-id")
183+
assertNull(result)
184+
}
185+
186+
@Test
187+
fun `getTenantIdFromLoginHint successfully resolves tenant ID from email`() {
188+
val loginHint = "user@contoso.com"
189+
val correlationId = "correlation-id"
190+
val expectedTenantId = "12345678-1234-1234-1234-123456789012"
191+
val mockConfiguration = mockk<OpenIdProviderConfiguration>()
192+
193+
mockkStatic(AzureActiveDirectory::class)
194+
mockkStatic(AzureActiveDirectoryAudience::class)
195+
196+
every { AzureActiveDirectory.loadOpenIdProviderConfigurationMetadataForTenant("contoso.com") } returns mockConfiguration
197+
every { AzureActiveDirectoryAudience.getTenantIdFromOpenIdProviderConfiguration(mockConfiguration) } returns expectedTenantId
198+
199+
val result = TenantUtil.getTenantIdFromLoginHint(loginHint, correlationId)
200+
201+
assertEquals(expectedTenantId, result)
202+
verify { AzureActiveDirectory.loadOpenIdProviderConfigurationMetadataForTenant("contoso.com") }
203+
verify { AzureActiveDirectoryAudience.getTenantIdFromOpenIdProviderConfiguration(mockConfiguration) }
204+
}
205+
206+
@Test
207+
fun `getTenantIdFromLoginHint returns null when configuration loading fails`() {
208+
val loginHint = "user@contoso.com"
209+
val correlationId = "correlation-id"
210+
val exception = RuntimeException("Failed to load configuration")
211+
212+
mockkStatic(AzureActiveDirectory::class)
213+
214+
every { AzureActiveDirectory.loadOpenIdProviderConfigurationMetadataForTenant("contoso.com") } throws exception
215+
216+
val result = TenantUtil.getTenantIdFromLoginHint(loginHint, correlationId)
217+
218+
assertNull(result)
219+
verify { AzureActiveDirectory.loadOpenIdProviderConfigurationMetadataForTenant("contoso.com") }
220+
}
221+
222+
@Test
223+
fun `getTenantIdFromLoginHint returns null when tenant ID extraction fails`() {
224+
val loginHint = "user@contoso.com"
225+
val correlationId = "correlation-id"
226+
val mockConfiguration = mockk<OpenIdProviderConfiguration>()
227+
val exception = RuntimeException("Failed to extract tenant ID")
228+
229+
mockkStatic(AzureActiveDirectory::class)
230+
mockkStatic(AzureActiveDirectoryAudience::class)
231+
232+
every { AzureActiveDirectory.loadOpenIdProviderConfigurationMetadataForTenant("contoso.com") } returns mockConfiguration
233+
every { AzureActiveDirectoryAudience.getTenantIdFromOpenIdProviderConfiguration(mockConfiguration) } throws exception
234+
235+
val result = TenantUtil.getTenantIdFromLoginHint(loginHint, correlationId)
236+
237+
assertNull(result)
238+
verify { AzureActiveDirectory.loadOpenIdProviderConfigurationMetadataForTenant("contoso.com") }
239+
verify { AzureActiveDirectoryAudience.getTenantIdFromOpenIdProviderConfiguration(mockConfiguration) }
240+
}
241+
242+
@Test
243+
fun `getTenantIdFromLoginHint works with null correlation ID`() {
244+
val loginHint = "user@contoso.com"
245+
val expectedTenantId = "12345678-1234-1234-1234-123456789012"
246+
val mockConfiguration = mockk<OpenIdProviderConfiguration>()
247+
248+
mockkStatic(AzureActiveDirectory::class)
249+
mockkStatic(AzureActiveDirectoryAudience::class)
250+
251+
every { AzureActiveDirectory.loadOpenIdProviderConfigurationMetadataForTenant("contoso.com") } returns mockConfiguration
252+
every { AzureActiveDirectoryAudience.getTenantIdFromOpenIdProviderConfiguration(mockConfiguration) } returns expectedTenantId
253+
254+
val result = TenantUtil.getTenantIdFromLoginHint(loginHint, null)
255+
256+
assertEquals(expectedTenantId, result)
257+
}
258+
259+
@Test
260+
fun `getTenantIdFromLoginHint handles complex email domains correctly`() {
261+
val loginHint = "user.name@sub.domain.contoso.com"
262+
val correlationId = "correlation-id"
263+
val expectedTenantId = "12345678-1234-1234-1234-123456789012"
264+
val mockConfiguration = mockk<OpenIdProviderConfiguration>()
265+
266+
mockkStatic(AzureActiveDirectory::class)
267+
mockkStatic(AzureActiveDirectoryAudience::class)
268+
269+
every { AzureActiveDirectory.loadOpenIdProviderConfigurationMetadataForTenant("sub.domain.contoso.com") } returns mockConfiguration
270+
every { AzureActiveDirectoryAudience.getTenantIdFromOpenIdProviderConfiguration(mockConfiguration) } returns expectedTenantId
271+
272+
val result = TenantUtil.getTenantIdFromLoginHint(loginHint, correlationId)
273+
274+
assertEquals(expectedTenantId, result)
275+
verify { AzureActiveDirectory.loadOpenIdProviderConfigurationMetadataForTenant("sub.domain.contoso.com") }
276+
}
277+
}

0 commit comments

Comments
 (0)