From e8a601af7922a4990b7f99703a52ec599faabd1a Mon Sep 17 00:00:00 2001 From: avdunn Date: Thu, 12 Mar 2026 12:02:28 -0700 Subject: [PATCH 1/4] Improve sovereign cloud support and network error handling --- .../msal4j/AadInstanceDiscoveryProvider.java | 20 ++- .../aad/msal4j/AadInstanceDiscoveryTest.java | 143 ++++++++++++++++++ 2 files changed, 161 insertions(+), 2 deletions(-) diff --git a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AadInstanceDiscoveryProvider.java b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AadInstanceDiscoveryProvider.java index d7def744..33efd5bf 100644 --- a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AadInstanceDiscoveryProvider.java +++ b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AadInstanceDiscoveryProvider.java @@ -45,7 +45,10 @@ class AadInstanceDiscoveryProvider { "login.chinacloudapi.cn", "login-us.microsoftonline.com", "login.microsoftonline.de", - "login.microsoftonline.us")); + "login.microsoftonline.us", + "login.sovcloud-identity.fr", + "login.sovcloud-identity.de", + "login.sovcloud-identity.sg")); TRUSTED_HOSTS_SET.addAll(Arrays.asList( DEFAULT_TRUSTED_HOST, @@ -340,7 +343,20 @@ private static void doInstanceDiscoveryAndCache(URL authorityUrl, AadInstanceDiscoveryResponse aadInstanceDiscoveryResponse = null; if (msalRequest.application().authenticationAuthority.authorityType.equals(AuthorityType.AAD)) { - aadInstanceDiscoveryResponse = sendInstanceDiscoveryRequest(authorityUrl, msalRequest, serviceBundle); + try { + aadInstanceDiscoveryResponse = sendInstanceDiscoveryRequest(authorityUrl, msalRequest, serviceBundle); + } catch (MsalServiceException ex) { + // MsalServiceException indicates a definitive HTTP-level error from the server + // (e.g. invalid_instance, bad request, server error) — always propagate. + throw ex; + } catch (Exception e) { + // Network failures (timeout, DNS, connection refused) — cache a fallback + // self-entry so subsequent calls don't retry the failing network call. + LOG.warn("Instance discovery network request failed. " + + "MSAL will use fallback instance metadata with {} as the host. Exception: {}", authorityUrl.getHost(), e.getMessage()); + cacheInstanceDiscoveryMetadata(authorityUrl.getHost()); + return; + } if (validateAuthority) { validate(aadInstanceDiscoveryResponse); diff --git a/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/AadInstanceDiscoveryTest.java b/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/AadInstanceDiscoveryTest.java index f28aec53..65de45a8 100644 --- a/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/AadInstanceDiscoveryTest.java +++ b/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/AadInstanceDiscoveryTest.java @@ -11,11 +11,17 @@ import org.mockito.MockedStatic; import org.mockito.junit.jupiter.MockitoExtension; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.CALLS_REAL_METHODS; import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.times; +import java.lang.reflect.Method; import java.net.URI; import java.net.URL; @@ -137,6 +143,143 @@ void aadInstanceDiscoveryTest_AutoDetectRegion_NoRegionDetected() throws Excepti } } + @Test + void discoveryEndpoint_routesToSovereignHost() throws Exception { + // Arrange + URL sovereignUrl = new URL("https://login.sovcloud-identity.fr/my_tenant"); + Method method = AadInstanceDiscoveryProvider.class.getDeclaredMethod("getInstanceDiscoveryEndpoint", URL.class); + method.setAccessible(true); + + // Act + String endpoint = (String) method.invoke(null, sovereignUrl); + + // Assert + assertTrue(endpoint.contains("login.sovcloud-identity.fr"), + "Discovery endpoint should use the sovereign host, got: " + endpoint); + } + + @Test + void regionalEndpoint_usesSovereignTemplate() throws Exception { + // Arrange + Method method = AadInstanceDiscoveryProvider.class.getDeclaredMethod("getRegionalizedHost", String.class, String.class); + method.setAccessible(true); + + // Act + String result = (String) method.invoke(null, "login.sovcloud-identity.fr", "westeurope"); + + // Assert + assertEquals("westeurope.login.sovcloud-identity.fr", result); + } + + @Test + void networkException_cachesFallbackAndDoesNotPropagate() throws Exception { + // Arrange + PublicClientApplication app = PublicClientApplication.builder("client_id") + .correlationId("correlation_id") + .authority("https://login.microsoftonline.com/my_tenant") + .build(); + + MsalRequest msalRequest = new AuthorizationCodeRequest( + parameters, + app, + new RequestContext(app, PublicApi.ACQUIRE_TOKEN_BY_AUTHORIZATION_CODE, parameters)); + + URL authority = new URL(app.authority()); + + try (MockedStatic mocked = mockStatic(AadInstanceDiscoveryProvider.class, CALLS_REAL_METHODS)) { + + mocked.when(() -> AadInstanceDiscoveryProvider.sendInstanceDiscoveryRequest(authority, + msalRequest, + app.serviceBundle())).thenThrow(new MsalClientException("Network timeout", AuthenticationErrorCode.UNKNOWN)); + + // Act — should not throw + InstanceDiscoveryMetadataEntry entry = assertDoesNotThrow(() -> + AadInstanceDiscoveryProvider.getMetadataEntry( + authority, + false, + msalRequest, + app.serviceBundle())); + + // Assert — cache should contain a self-entry + assertNotNull(entry); + String host = authority.getHost(); + InstanceDiscoveryMetadataEntry cached = AadInstanceDiscoveryProvider.cache.get(host); + assertNotNull(cached, "Fallback entry should be cached"); + assertEquals(host, cached.preferredNetwork); + assertEquals(host, cached.preferredCache); + assertTrue(cached.aliases.contains(host)); + } + } + + @Test + void subsequentCallAfterNetworkFailure_usesCacheNoRetry() throws Exception { + // Arrange + PublicClientApplication app = PublicClientApplication.builder("client_id") + .correlationId("correlation_id") + .authority("https://login.microsoftonline.com/my_tenant") + .build(); + + MsalRequest msalRequest = new AuthorizationCodeRequest( + parameters, + app, + new RequestContext(app, PublicApi.ACQUIRE_TOKEN_BY_AUTHORIZATION_CODE, parameters)); + + URL authority = new URL(app.authority()); + + try (MockedStatic mocked = mockStatic(AadInstanceDiscoveryProvider.class, CALLS_REAL_METHODS)) { + + mocked.when(() -> AadInstanceDiscoveryProvider.sendInstanceDiscoveryRequest(any(URL.class), + any(MsalRequest.class), + any(ServiceBundle.class))).thenThrow(new MsalClientException("Network timeout", AuthenticationErrorCode.UNKNOWN)); + + // Act — first call triggers network failure + fallback cache + AadInstanceDiscoveryProvider.getMetadataEntry(authority, false, msalRequest, app.serviceBundle()); + + // Act — second call should hit cache, not retry network + InstanceDiscoveryMetadataEntry entry = AadInstanceDiscoveryProvider.getMetadataEntry(authority, false, msalRequest, app.serviceBundle()); + + // Assert — sendInstanceDiscoveryRequest should have been called only once + mocked.verify(() -> AadInstanceDiscoveryProvider.sendInstanceDiscoveryRequest(any(URL.class), + any(MsalRequest.class), + any(ServiceBundle.class)), times(1)); + assertNotNull(entry); + } + } + + @Test + void invalidInstanceException_stillPropagates() throws Exception { + // Arrange + PublicClientApplication app = PublicClientApplication.builder("client_id") + .correlationId("correlation_id") + .authority("https://login.microsoftonline.com/my_tenant") + .build(); + + MsalRequest msalRequest = new AuthorizationCodeRequest( + parameters, + app, + new RequestContext(app, PublicApi.ACQUIRE_TOKEN_BY_AUTHORIZATION_CODE, parameters)); + + URL authority = new URL(app.authority()); + + try (MockedStatic mocked = mockStatic(AadInstanceDiscoveryProvider.class, CALLS_REAL_METHODS)) { + + mocked.when(() -> AadInstanceDiscoveryProvider.sendInstanceDiscoveryRequest(authority, + msalRequest, + app.serviceBundle())).thenThrow(new MsalServiceException("invalid_instance", "invalid_instance")); + + // Act / Assert — MsalServiceException should propagate + assertThrows(MsalServiceException.class, () -> + AadInstanceDiscoveryProvider.getMetadataEntry( + authority, + false, + msalRequest, + app.serviceBundle())); + + // Assert — nothing should be cached + assertNull(AadInstanceDiscoveryProvider.cache.get(authority.getHost())); + } + } + void assertValidResponse(InstanceDiscoveryMetadataEntry entry) { assertEquals(entry.preferredNetwork(), "login.microsoftonline.com"); assertEquals(entry.preferredCache(), "login.windows.net"); From 966c52afc17bcdd4f638aa259b534529efe7ed7d Mon Sep 17 00:00:00 2001 From: avdunn Date: Fri, 13 Mar 2026 09:24:25 -0700 Subject: [PATCH 2/4] Address feedback and improve consistency with .NET --- .../msal4j/AadInstanceDiscoveryProvider.java | 28 +- .../aad/msal4j/KnownMetadataProvider.java | 81 +++++ .../aad/msal4j/AadInstanceDiscoveryTest.java | 113 ------ .../aad/msal4j/KnownMetadataProviderTest.java | 147 ++++++++ .../SovereignCloudInstanceDiscoveryTest.java | 327 ++++++++++++++++++ 5 files changed, 577 insertions(+), 119 deletions(-) create mode 100644 msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/KnownMetadataProvider.java create mode 100644 msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/KnownMetadataProviderTest.java create mode 100644 msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/SovereignCloudInstanceDiscoveryTest.java diff --git a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AadInstanceDiscoveryProvider.java b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AadInstanceDiscoveryProvider.java index 33efd5bf..72cf5102 100644 --- a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AadInstanceDiscoveryProvider.java +++ b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AadInstanceDiscoveryProvider.java @@ -43,9 +43,11 @@ class AadInstanceDiscoveryProvider { static { TRUSTED_SOVEREIGN_HOSTS_SET.addAll(Arrays.asList( "login.chinacloudapi.cn", + "login.partner.microsoftonline.cn", "login-us.microsoftonline.com", "login.microsoftonline.de", "login.microsoftonline.us", + "login.usgovcloudapi.net", "login.sovcloud-identity.fr", "login.sovcloud-identity.de", "login.sovcloud-identity.sg")); @@ -145,7 +147,14 @@ static void cacheInstanceDiscoveryResponse(String host, } static void cacheInstanceDiscoveryMetadata(String host) { - cache.putIfAbsent(host, new InstanceDiscoveryMetadataEntry(host, host, Collections.singleton(host))); + InstanceDiscoveryMetadataEntry knownEntry = KnownMetadataProvider.getMetadataEntry(host); + if (knownEntry != null) { + for (String alias : knownEntry.aliases()) { + cache.putIfAbsent(alias, knownEntry); + } + } else { + cache.putIfAbsent(host, new InstanceDiscoveryMetadataEntry(host, host, Collections.singleton(host))); + } } private static boolean shouldUseRegionalEndpoint(MsalRequest msalRequest){ @@ -346,14 +355,21 @@ private static void doInstanceDiscoveryAndCache(URL authorityUrl, try { aadInstanceDiscoveryResponse = sendInstanceDiscoveryRequest(authorityUrl, msalRequest, serviceBundle); } catch (MsalServiceException ex) { - // MsalServiceException indicates a definitive HTTP-level error from the server - // (e.g. invalid_instance, bad request, server error) — always propagate. - throw ex; + // Only propagate "invalid_instance" — this means the authority itself is invalid. + // All other HTTP-level errors (500, 502, 404, etc.) should fall through to the + // fallback path, matching the behavior of MSAL .NET's InstanceDiscoveryManager. + if ("invalid_instance".equals(ex.errorCode())) { + throw ex; + } + LOG.warn("Instance discovery request failed with a service error. " + + "MSAL will use fallback instance metadata for {}. Error: {}", authorityUrl.getHost(), ex.getMessage()); + cacheInstanceDiscoveryMetadata(authorityUrl.getHost()); + return; } catch (Exception e) { // Network failures (timeout, DNS, connection refused) — cache a fallback - // self-entry so subsequent calls don't retry the failing network call. + // entry so subsequent calls don't retry the failing network call. LOG.warn("Instance discovery network request failed. " + - "MSAL will use fallback instance metadata with {} as the host. Exception: {}", authorityUrl.getHost(), e.getMessage()); + "MSAL will use fallback instance metadata for {}. Exception: {}", authorityUrl.getHost(), e.getMessage()); cacheInstanceDiscoveryMetadata(authorityUrl.getHost()); return; } diff --git a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/KnownMetadataProvider.java b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/KnownMetadataProvider.java new file mode 100644 index 00000000..c7d38af7 --- /dev/null +++ b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/KnownMetadataProvider.java @@ -0,0 +1,81 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.aad.msal4j; + +import java.util.*; + +/** + * Provides hardcoded instance discovery metadata for well-known cloud environments. + * This allows correct alias resolution and cache behavior even when the network + * instance discovery endpoint is unreachable. + * + * Mirrors the KnownMetadataProvider in MSAL .NET. + */ +class KnownMetadataProvider { + + private static final Map KNOWN_ENTRIES; + + static { + Map entries = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); + + addEntry(entries, + "login.microsoftonline.com", "login.windows.net", + "login.microsoftonline.com", "login.windows.net", "login.microsoft.com", "sts.windows.net"); + + addEntry(entries, + "login.partner.microsoftonline.cn", "login.partner.microsoftonline.cn", + "login.partner.microsoftonline.cn", "login.chinacloudapi.cn"); + + addEntry(entries, + "login.microsoftonline.de", "login.microsoftonline.de", + "login.microsoftonline.de"); + + addEntry(entries, + "login.microsoftonline.us", "login.microsoftonline.us", + "login.microsoftonline.us", "login.usgovcloudapi.net"); + + addEntry(entries, + "login-us.microsoftonline.com", "login-us.microsoftonline.com", + "login-us.microsoftonline.com"); + + addEntry(entries, + "login.sovcloud-identity.fr", "login.sovcloud-identity.fr", + "login.sovcloud-identity.fr"); + + addEntry(entries, + "login.sovcloud-identity.de", "login.sovcloud-identity.de", + "login.sovcloud-identity.de"); + + addEntry(entries, + "login.sovcloud-identity.sg", "login.sovcloud-identity.sg", + "login.sovcloud-identity.sg"); + + KNOWN_ENTRIES = Collections.unmodifiableMap(entries); + } + + private static void addEntry(Map entries, + String preferredNetwork, + String preferredCache, + String... aliases) { + Set aliasSet = new LinkedHashSet<>(Arrays.asList(aliases)); + InstanceDiscoveryMetadataEntry entry = new InstanceDiscoveryMetadataEntry(preferredNetwork, preferredCache, aliasSet); + for (String alias : aliases) { + entries.put(alias, entry); + } + } + + /** + * Returns the known metadata entry for the given host, or null if unknown. + */ + static InstanceDiscoveryMetadataEntry getMetadataEntry(String host) { + return KNOWN_ENTRIES.get(host); + } + + /** + * Returns true if the host is a well-known cloud environment. + */ + static boolean isKnownEnvironment(String host) { + return KNOWN_ENTRIES.containsKey(host); + } +} diff --git a/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/AadInstanceDiscoveryTest.java b/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/AadInstanceDiscoveryTest.java index 65de45a8..af829d18 100644 --- a/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/AadInstanceDiscoveryTest.java +++ b/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/AadInstanceDiscoveryTest.java @@ -11,15 +11,11 @@ import org.mockito.MockedStatic; import org.mockito.junit.jupiter.MockitoExtension; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.CALLS_REAL_METHODS; import static org.mockito.Mockito.mockStatic; -import static org.mockito.Mockito.times; import java.lang.reflect.Method; import java.net.URI; @@ -171,115 +167,6 @@ void regionalEndpoint_usesSovereignTemplate() throws Exception { assertEquals("westeurope.login.sovcloud-identity.fr", result); } - @Test - void networkException_cachesFallbackAndDoesNotPropagate() throws Exception { - // Arrange - PublicClientApplication app = PublicClientApplication.builder("client_id") - .correlationId("correlation_id") - .authority("https://login.microsoftonline.com/my_tenant") - .build(); - - MsalRequest msalRequest = new AuthorizationCodeRequest( - parameters, - app, - new RequestContext(app, PublicApi.ACQUIRE_TOKEN_BY_AUTHORIZATION_CODE, parameters)); - - URL authority = new URL(app.authority()); - - try (MockedStatic mocked = mockStatic(AadInstanceDiscoveryProvider.class, CALLS_REAL_METHODS)) { - - mocked.when(() -> AadInstanceDiscoveryProvider.sendInstanceDiscoveryRequest(authority, - msalRequest, - app.serviceBundle())).thenThrow(new MsalClientException("Network timeout", AuthenticationErrorCode.UNKNOWN)); - - // Act — should not throw - InstanceDiscoveryMetadataEntry entry = assertDoesNotThrow(() -> - AadInstanceDiscoveryProvider.getMetadataEntry( - authority, - false, - msalRequest, - app.serviceBundle())); - - // Assert — cache should contain a self-entry - assertNotNull(entry); - String host = authority.getHost(); - InstanceDiscoveryMetadataEntry cached = AadInstanceDiscoveryProvider.cache.get(host); - assertNotNull(cached, "Fallback entry should be cached"); - assertEquals(host, cached.preferredNetwork); - assertEquals(host, cached.preferredCache); - assertTrue(cached.aliases.contains(host)); - } - } - - @Test - void subsequentCallAfterNetworkFailure_usesCacheNoRetry() throws Exception { - // Arrange - PublicClientApplication app = PublicClientApplication.builder("client_id") - .correlationId("correlation_id") - .authority("https://login.microsoftonline.com/my_tenant") - .build(); - - MsalRequest msalRequest = new AuthorizationCodeRequest( - parameters, - app, - new RequestContext(app, PublicApi.ACQUIRE_TOKEN_BY_AUTHORIZATION_CODE, parameters)); - - URL authority = new URL(app.authority()); - - try (MockedStatic mocked = mockStatic(AadInstanceDiscoveryProvider.class, CALLS_REAL_METHODS)) { - - mocked.when(() -> AadInstanceDiscoveryProvider.sendInstanceDiscoveryRequest(any(URL.class), - any(MsalRequest.class), - any(ServiceBundle.class))).thenThrow(new MsalClientException("Network timeout", AuthenticationErrorCode.UNKNOWN)); - - // Act — first call triggers network failure + fallback cache - AadInstanceDiscoveryProvider.getMetadataEntry(authority, false, msalRequest, app.serviceBundle()); - - // Act — second call should hit cache, not retry network - InstanceDiscoveryMetadataEntry entry = AadInstanceDiscoveryProvider.getMetadataEntry(authority, false, msalRequest, app.serviceBundle()); - - // Assert — sendInstanceDiscoveryRequest should have been called only once - mocked.verify(() -> AadInstanceDiscoveryProvider.sendInstanceDiscoveryRequest(any(URL.class), - any(MsalRequest.class), - any(ServiceBundle.class)), times(1)); - assertNotNull(entry); - } - } - - @Test - void invalidInstanceException_stillPropagates() throws Exception { - // Arrange - PublicClientApplication app = PublicClientApplication.builder("client_id") - .correlationId("correlation_id") - .authority("https://login.microsoftonline.com/my_tenant") - .build(); - - MsalRequest msalRequest = new AuthorizationCodeRequest( - parameters, - app, - new RequestContext(app, PublicApi.ACQUIRE_TOKEN_BY_AUTHORIZATION_CODE, parameters)); - - URL authority = new URL(app.authority()); - - try (MockedStatic mocked = mockStatic(AadInstanceDiscoveryProvider.class, CALLS_REAL_METHODS)) { - - mocked.when(() -> AadInstanceDiscoveryProvider.sendInstanceDiscoveryRequest(authority, - msalRequest, - app.serviceBundle())).thenThrow(new MsalServiceException("invalid_instance", "invalid_instance")); - - // Act / Assert — MsalServiceException should propagate - assertThrows(MsalServiceException.class, () -> - AadInstanceDiscoveryProvider.getMetadataEntry( - authority, - false, - msalRequest, - app.serviceBundle())); - - // Assert — nothing should be cached - assertNull(AadInstanceDiscoveryProvider.cache.get(authority.getHost())); - } - } - void assertValidResponse(InstanceDiscoveryMetadataEntry entry) { assertEquals(entry.preferredNetwork(), "login.microsoftonline.com"); assertEquals(entry.preferredCache(), "login.windows.net"); diff --git a/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/KnownMetadataProviderTest.java b/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/KnownMetadataProviderTest.java new file mode 100644 index 00000000..a11dd4d7 --- /dev/null +++ b/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/KnownMetadataProviderTest.java @@ -0,0 +1,147 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.aad.msal4j; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class KnownMetadataProviderTest { + + @Test + void isKnownEnvironment_allKnownHosts_returnsTrue() { + String[] knownHosts = { + "login.microsoftonline.com", "login.windows.net", "login.microsoft.com", "sts.windows.net", + "login.partner.microsoftonline.cn", "login.chinacloudapi.cn", + "login.microsoftonline.de", + "login.microsoftonline.us", "login.usgovcloudapi.net", + "login-us.microsoftonline.com", + "login.sovcloud-identity.fr", "login.sovcloud-identity.de", "login.sovcloud-identity.sg" + }; + for (String host : knownHosts) { + assertTrue(KnownMetadataProvider.isKnownEnvironment(host), + "Expected " + host + " to be a known environment"); + } + } + + @Test + void isKnownEnvironment_unknownHost_returnsFalse() { + assertFalse(KnownMetadataProvider.isKnownEnvironment("custom.authority.example.com")); + } + + @Test + void getMetadataEntry_publicCloud_returnsCorrectAliases() { + // Arrange / Act + InstanceDiscoveryMetadataEntry entry = KnownMetadataProvider.getMetadataEntry("login.microsoftonline.com"); + + // Assert + assertNotNull(entry); + assertEquals("login.microsoftonline.com", entry.preferredNetwork()); + assertEquals("login.windows.net", entry.preferredCache()); + assertEquals(4, entry.aliases().size()); + assertTrue(entry.aliases().contains("login.microsoftonline.com")); + assertTrue(entry.aliases().contains("login.windows.net")); + assertTrue(entry.aliases().contains("login.microsoft.com")); + assertTrue(entry.aliases().contains("sts.windows.net")); + } + + @Test + void getMetadataEntry_publicCloudAliases_resolvesToSameEntry() { + // All public cloud aliases should resolve to the same object + InstanceDiscoveryMetadataEntry entry1 = KnownMetadataProvider.getMetadataEntry("login.microsoftonline.com"); + InstanceDiscoveryMetadataEntry entry2 = KnownMetadataProvider.getMetadataEntry("login.windows.net"); + InstanceDiscoveryMetadataEntry entry3 = KnownMetadataProvider.getMetadataEntry("login.microsoft.com"); + InstanceDiscoveryMetadataEntry entry4 = KnownMetadataProvider.getMetadataEntry("sts.windows.net"); + + // Assert + assertSame(entry1, entry2); + assertSame(entry2, entry3); + assertSame(entry3, entry4); + } + + @Test + void getMetadataEntry_chinaCloud_returnsCorrectAliases() { + // Act + InstanceDiscoveryMetadataEntry entry = KnownMetadataProvider.getMetadataEntry("login.chinacloudapi.cn"); + + // Assert + assertNotNull(entry); + assertEquals("login.partner.microsoftonline.cn", entry.preferredNetwork()); + assertEquals("login.partner.microsoftonline.cn", entry.preferredCache()); + assertEquals(2, entry.aliases().size()); + assertTrue(entry.aliases().contains("login.partner.microsoftonline.cn")); + assertTrue(entry.aliases().contains("login.chinacloudapi.cn")); + + // Both aliases should resolve to the same entry + assertSame(entry, KnownMetadataProvider.getMetadataEntry("login.partner.microsoftonline.cn")); + } + + @Test + void getMetadataEntry_usGov_returnsCorrectAliases() { + // Act + InstanceDiscoveryMetadataEntry entry = KnownMetadataProvider.getMetadataEntry("login.microsoftonline.us"); + + // Assert + assertNotNull(entry); + assertEquals("login.microsoftonline.us", entry.preferredNetwork()); + assertEquals("login.microsoftonline.us", entry.preferredCache()); + assertEquals(2, entry.aliases().size()); + assertTrue(entry.aliases().contains("login.microsoftonline.us")); + assertTrue(entry.aliases().contains("login.usgovcloudapi.net")); + + // Both aliases should resolve to the same entry + assertSame(entry, KnownMetadataProvider.getMetadataEntry("login.usgovcloudapi.net")); + } + + @Test + void getMetadataEntry_unknownHost_returnsNull() { + assertNull(KnownMetadataProvider.getMetadataEntry("custom.authority.example.com")); + } + + @Test + void getMetadataEntry_caseInsensitive() { + // Assert — lookups should be case-insensitive + assertNotNull(KnownMetadataProvider.getMetadataEntry("LOGIN.MICROSOFTONLINE.COM")); + assertNotNull(KnownMetadataProvider.getMetadataEntry("Login.Partner.Microsoftonline.Cn")); + assertNotNull(KnownMetadataProvider.getMetadataEntry("LOGIN.SOVCLOUD-IDENTITY.FR")); + } + + @Test + void cacheInstanceDiscoveryMetadata_knownHost_cachesAllAliases() { + // Arrange + AadInstanceDiscoveryProvider.cache.clear(); + + // Act + AadInstanceDiscoveryProvider.cacheInstanceDiscoveryMetadata("login.microsoftonline.com"); + + // Assert — all public cloud aliases should be cached + assertNotNull(AadInstanceDiscoveryProvider.cache.get("login.microsoftonline.com")); + assertNotNull(AadInstanceDiscoveryProvider.cache.get("login.windows.net")); + assertNotNull(AadInstanceDiscoveryProvider.cache.get("login.microsoft.com")); + assertNotNull(AadInstanceDiscoveryProvider.cache.get("sts.windows.net")); + + // All should reference the same entry + InstanceDiscoveryMetadataEntry entry = AadInstanceDiscoveryProvider.cache.get("login.microsoftonline.com"); + assertSame(entry, AadInstanceDiscoveryProvider.cache.get("login.windows.net")); + assertSame(entry, AadInstanceDiscoveryProvider.cache.get("login.microsoft.com")); + assertSame(entry, AadInstanceDiscoveryProvider.cache.get("sts.windows.net")); + } + + @Test + void cacheInstanceDiscoveryMetadata_unknownHost_cachesSelfEntry() { + // Arrange + AadInstanceDiscoveryProvider.cache.clear(); + + // Act + AadInstanceDiscoveryProvider.cacheInstanceDiscoveryMetadata("custom.unknown.example.com"); + + // Assert — should get a self-referencing entry + InstanceDiscoveryMetadataEntry entry = AadInstanceDiscoveryProvider.cache.get("custom.unknown.example.com"); + assertNotNull(entry); + assertEquals("custom.unknown.example.com", entry.preferredNetwork()); + assertEquals("custom.unknown.example.com", entry.preferredCache()); + assertEquals(1, entry.aliases().size()); + assertTrue(entry.aliases().contains("custom.unknown.example.com")); + } +} diff --git a/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/SovereignCloudInstanceDiscoveryTest.java b/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/SovereignCloudInstanceDiscoveryTest.java new file mode 100644 index 00000000..4f45515b --- /dev/null +++ b/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/SovereignCloudInstanceDiscoveryTest.java @@ -0,0 +1,327 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.aad.msal4j; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.mockito.ArgumentCaptor; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +/** + * Acceptance tests for sovereign cloud instance discovery behavior. + * + * These tests create a real ConfidentialClientApplication with a sovereign authority, + * mock only the HTTP layer, and verify that all HTTP requests are routed to the + * correct sovereign host — not to login.microsoftonline.com or any other host. + */ +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class SovereignCloudInstanceDiscoveryTest { + + private static final String SOVEREIGN_HOST = "login.sovcloud-identity.fr"; + private static final String SOVEREIGN_AUTHORITY = "https://" + SOVEREIGN_HOST + "/my-tenant"; + private static final String CLIENT_ID = "test-client-id"; + private static final String CLIENT_SECRET = "test-client-secret"; + + // A valid instance discovery response for the sovereign cloud + private static final String SOVEREIGN_INSTANCE_DISCOVERY_RESPONSE = "{" + + "\"tenant_discovery_endpoint\":\"https://" + SOVEREIGN_HOST + "/my-tenant/.well-known/openid-configuration\"," + + "\"api-version\":\"1.1\"," + + "\"metadata\":[{" + + "\"preferred_network\":\"" + SOVEREIGN_HOST + "\"," + + "\"preferred_cache\":\"" + SOVEREIGN_HOST + "\"," + + "\"aliases\":[\"" + SOVEREIGN_HOST + "\"]" + + "}]}"; + + @BeforeEach + void setup() { + AadInstanceDiscoveryProvider.cache.clear(); + } + + @Test + void sovereignAuthority_allRequestsRouteToSovereignHost() throws Exception { + // Arrange — mock HTTP client that captures all request URLs + DefaultHttpClient httpClientMock = mock(DefaultHttpClient.class); + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(HttpRequest.class); + + when(httpClientMock.send(any(HttpRequest.class))).thenAnswer(invocation -> { + HttpRequest request = invocation.getArgument(0); + String url = request.url().toString(); + + if (url.contains("discovery/instance")) { + return TestHelper.expectedResponse(200, SOVEREIGN_INSTANCE_DISCOVERY_RESPONSE); + } + if (url.contains("oauth2/v2.0/token")) { + return TestHelper.expectedResponse(200, + TestHelper.getSuccessfulTokenResponse(new HashMap<>())); + } + + fail("Unexpected request URL: " + url); + return null; + }); + + ConfidentialClientApplication cca = ConfidentialClientApplication + .builder(CLIENT_ID, ClientCredentialFactory.createFromSecret(CLIENT_SECRET)) + .authority(SOVEREIGN_AUTHORITY) + .validateAuthority(false) + .httpClient(httpClientMock) + .build(); + + // Act + IAuthenticationResult result = cca.acquireToken( + ClientCredentialParameters.builder(Collections.singleton("https://resource/.default")).build() + ).get(); + + // Assert — token was acquired + assertNotNull(result); + assertNotNull(result.accessToken()); + + // Assert — every HTTP request went to the sovereign host + verify(httpClientMock, atLeastOnce()).send(requestCaptor.capture()); + List capturedRequests = requestCaptor.getAllValues(); + assertFalse(capturedRequests.isEmpty(), "At least one HTTP request should have been made"); + + for (HttpRequest req : capturedRequests) { + assertEquals(SOVEREIGN_HOST, req.url().getHost(), + "All requests must go to " + SOVEREIGN_HOST + ", but got: " + req.url()); + } + } + + @Test + void sovereignAuthority_instanceDiscoveryEndpointUsesOwnHost() throws Exception { + // Arrange — track which URLs are called + DefaultHttpClient httpClientMock = mock(DefaultHttpClient.class); + List capturedUrls = new ArrayList<>(); + + when(httpClientMock.send(any(HttpRequest.class))).thenAnswer(invocation -> { + HttpRequest request = invocation.getArgument(0); + String url = request.url().toString(); + capturedUrls.add(url); + + if (url.contains("discovery/instance")) { + return TestHelper.expectedResponse(200, SOVEREIGN_INSTANCE_DISCOVERY_RESPONSE); + } + if (url.contains("oauth2/v2.0/token")) { + return TestHelper.expectedResponse(200, + TestHelper.getSuccessfulTokenResponse(new HashMap<>())); + } + + fail("Unexpected request URL: " + url); + return null; + }); + + ConfidentialClientApplication cca = ConfidentialClientApplication + .builder(CLIENT_ID, ClientCredentialFactory.createFromSecret(CLIENT_SECRET)) + .authority(SOVEREIGN_AUTHORITY) + .validateAuthority(false) + .httpClient(httpClientMock) + .build(); + + // Act + cca.acquireToken( + ClientCredentialParameters.builder(Collections.singleton("https://resource/.default")).build() + ).get(); + + // Assert — instance discovery endpoint was called on the sovereign host + assertTrue(capturedUrls.stream().anyMatch(url -> + url.contains(SOVEREIGN_HOST) && url.contains("discovery/instance")), + "Instance discovery should be sent to " + SOVEREIGN_HOST + ". Captured URLs: " + capturedUrls); + + // Assert — no request went to the public cloud + assertTrue(capturedUrls.stream().noneMatch(url -> url.contains("login.microsoftonline.com")), + "No requests should go to login.microsoftonline.com. Captured URLs: " + capturedUrls); + } + + @Test + void sovereignAuthority_instanceDiscoveryFailure_usesKnownMetadataFallback() throws Exception { + // Arrange — instance discovery returns a server error (not invalid_instance) + DefaultHttpClient httpClientMock = mock(DefaultHttpClient.class); + List capturedUrls = new ArrayList<>(); + + String serverErrorResponse = "{\"error\":\"server_error\",\"error_description\":\"Internal server error\"}"; + + when(httpClientMock.send(any(HttpRequest.class))).thenAnswer(invocation -> { + HttpRequest request = invocation.getArgument(0); + String url = request.url().toString(); + capturedUrls.add(url); + + if (url.contains("discovery/instance")) { + // Return 500 server error — should NOT throw, should fall back + return TestHelper.expectedResponse(500, serverErrorResponse); + } + if (url.contains("oauth2/v2.0/token")) { + return TestHelper.expectedResponse(200, + TestHelper.getSuccessfulTokenResponse(new HashMap<>())); + } + + fail("Unexpected request URL: " + url); + return null; + }); + + ConfidentialClientApplication cca = ConfidentialClientApplication + .builder(CLIENT_ID, ClientCredentialFactory.createFromSecret(CLIENT_SECRET)) + .authority(SOVEREIGN_AUTHORITY) + .validateAuthority(false) + .httpClient(httpClientMock) + .build(); + + // Act — should succeed despite instance discovery failure + IAuthenticationResult result = cca.acquireToken( + ClientCredentialParameters.builder(Collections.singleton("https://resource/.default")).build() + ).get(); + + // Assert — token was still acquired + assertNotNull(result); + + // Assert — all requests went to the sovereign host + assertTrue(capturedUrls.stream().allMatch(url -> url.contains(SOVEREIGN_HOST)), + "All requests must go to " + SOVEREIGN_HOST + ". Captured URLs: " + capturedUrls); + + // Assert — known metadata for the sovereign host was cached + InstanceDiscoveryMetadataEntry cached = AadInstanceDiscoveryProvider.cache.get(SOVEREIGN_HOST); + assertNotNull(cached, "Fallback metadata should be cached for " + SOVEREIGN_HOST); + assertEquals(SOVEREIGN_HOST, cached.preferredNetwork()); + } + + @Test + void sovereignAuthority_invalidInstance_throws() throws Exception { + // Arrange — instance discovery returns invalid_instance error + DefaultHttpClient httpClientMock = mock(DefaultHttpClient.class); + + String invalidInstanceResponse = "{\"error\":\"invalid_instance\"," + + "\"error_description\":\"AADSTS50049: Unknown or invalid instance.\"," + + "\"error_codes\":[50049]}"; + + when(httpClientMock.send(any(HttpRequest.class))).thenAnswer(invocation -> { + HttpRequest request = invocation.getArgument(0); + String url = request.url().toString(); + + if (url.contains("discovery/instance")) { + return TestHelper.expectedResponse(400, invalidInstanceResponse); + } + + fail("No further requests should be made after invalid_instance. URL: " + url); + return null; + }); + + ConfidentialClientApplication cca = ConfidentialClientApplication + .builder(CLIENT_ID, ClientCredentialFactory.createFromSecret(CLIENT_SECRET)) + .authority(SOVEREIGN_AUTHORITY) + .validateAuthority(false) + .httpClient(httpClientMock) + .build(); + + // Act / Assert — should throw MsalServiceException + Exception thrown = assertThrows(Exception.class, () -> + cca.acquireToken( + ClientCredentialParameters.builder(Collections.singleton("https://resource/.default")).build() + ).get() + ); + + // The CompletableFuture wraps the exception in ExecutionException + Throwable cause = thrown.getCause(); + assertInstanceOf(MsalServiceException.class, cause, + "Should throw MsalServiceException for invalid_instance, got: " + cause); + } + + @Test + void sovereignAuthority_networkException_cachesFallbackAndProceeds() throws Exception { + // Arrange — instance discovery throws a network exception on first call, + // then succeeds for the token request + DefaultHttpClient httpClientMock = mock(DefaultHttpClient.class); + List capturedUrls = new ArrayList<>(); + + when(httpClientMock.send(any(HttpRequest.class))).thenAnswer(invocation -> { + HttpRequest request = invocation.getArgument(0); + String url = request.url().toString(); + capturedUrls.add(url); + + if (url.contains("discovery/instance")) { + throw new java.net.SocketException("Network is unreachable"); + } + if (url.contains("oauth2/v2.0/token")) { + return TestHelper.expectedResponse(200, + TestHelper.getSuccessfulTokenResponse(new HashMap<>())); + } + + fail("Unexpected request URL: " + url); + return null; + }); + + ConfidentialClientApplication cca = ConfidentialClientApplication + .builder(CLIENT_ID, ClientCredentialFactory.createFromSecret(CLIENT_SECRET)) + .authority(SOVEREIGN_AUTHORITY) + .validateAuthority(false) + .httpClient(httpClientMock) + .build(); + + // Act — should succeed despite network failure on instance discovery + IAuthenticationResult result = cca.acquireToken( + ClientCredentialParameters.builder(Collections.singleton("https://resource/.default")).build() + ).get(); + + // Assert — token was acquired + assertNotNull(result); + + // Assert — all requests went to the sovereign host + assertTrue(capturedUrls.stream().allMatch(url -> url.contains(SOVEREIGN_HOST)), + "All requests must go to " + SOVEREIGN_HOST + ". Captured URLs: " + capturedUrls); + + // Assert — fallback entry was cached with known metadata + InstanceDiscoveryMetadataEntry cached = AadInstanceDiscoveryProvider.cache.get(SOVEREIGN_HOST); + assertNotNull(cached, "Fallback entry should be cached for " + SOVEREIGN_HOST); + assertEquals(SOVEREIGN_HOST, cached.preferredNetwork()); + } + + @Test + void sovereignAuthority_nonInvalidInstanceServiceError_doesNotThrow() throws Exception { + // Arrange — instance discovery returns a 502 with a non-invalid_instance error. + // This should NOT throw (unlike invalid_instance) — it should fall back gracefully. + DefaultHttpClient httpClientMock = mock(DefaultHttpClient.class); + + String badGatewayResponse = "{\"error\":\"server_error\",\"error_description\":\"Bad Gateway\"}"; + + when(httpClientMock.send(any(HttpRequest.class))).thenAnswer(invocation -> { + HttpRequest request = invocation.getArgument(0); + String url = request.url().toString(); + + if (url.contains("discovery/instance")) { + return TestHelper.expectedResponse(502, badGatewayResponse); + } + if (url.contains("oauth2/v2.0/token")) { + return TestHelper.expectedResponse(200, + TestHelper.getSuccessfulTokenResponse(new HashMap<>())); + } + + fail("Unexpected request URL: " + url); + return null; + }); + + ConfidentialClientApplication cca = ConfidentialClientApplication + .builder(CLIENT_ID, ClientCredentialFactory.createFromSecret(CLIENT_SECRET)) + .authority(SOVEREIGN_AUTHORITY) + .validateAuthority(false) + .httpClient(httpClientMock) + .build(); + + // Act — should NOT throw, should fall back + IAuthenticationResult result = assertDoesNotThrow(() -> + cca.acquireToken( + ClientCredentialParameters.builder(Collections.singleton("https://resource/.default")).build() + ).get() + ); + + // Assert + assertNotNull(result); + assertNotNull(result.accessToken()); + } +} From 30a78048d2b367183df6b9a57c2ec7901cf8ea4f Mon Sep 17 00:00:00 2001 From: avdunn Date: Fri, 13 Mar 2026 09:26:17 -0700 Subject: [PATCH 3/4] Remove unnecessary tests --- .../aad/msal4j/AadInstanceDiscoveryTest.java | 30 ------------------- 1 file changed, 30 deletions(-) diff --git a/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/AadInstanceDiscoveryTest.java b/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/AadInstanceDiscoveryTest.java index af829d18..f28aec53 100644 --- a/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/AadInstanceDiscoveryTest.java +++ b/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/AadInstanceDiscoveryTest.java @@ -11,13 +11,11 @@ import org.mockito.MockedStatic; import org.mockito.junit.jupiter.MockitoExtension; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.Mockito.CALLS_REAL_METHODS; import static org.mockito.Mockito.mockStatic; -import java.lang.reflect.Method; import java.net.URI; import java.net.URL; @@ -139,34 +137,6 @@ void aadInstanceDiscoveryTest_AutoDetectRegion_NoRegionDetected() throws Excepti } } - @Test - void discoveryEndpoint_routesToSovereignHost() throws Exception { - // Arrange - URL sovereignUrl = new URL("https://login.sovcloud-identity.fr/my_tenant"); - Method method = AadInstanceDiscoveryProvider.class.getDeclaredMethod("getInstanceDiscoveryEndpoint", URL.class); - method.setAccessible(true); - - // Act - String endpoint = (String) method.invoke(null, sovereignUrl); - - // Assert - assertTrue(endpoint.contains("login.sovcloud-identity.fr"), - "Discovery endpoint should use the sovereign host, got: " + endpoint); - } - - @Test - void regionalEndpoint_usesSovereignTemplate() throws Exception { - // Arrange - Method method = AadInstanceDiscoveryProvider.class.getDeclaredMethod("getRegionalizedHost", String.class, String.class); - method.setAccessible(true); - - // Act - String result = (String) method.invoke(null, "login.sovcloud-identity.fr", "westeurope"); - - // Assert - assertEquals("westeurope.login.sovcloud-identity.fr", result); - } - void assertValidResponse(InstanceDiscoveryMetadataEntry entry) { assertEquals(entry.preferredNetwork(), "login.microsoftonline.com"); assertEquals(entry.preferredCache(), "login.windows.net"); From 01120559f5d279f57605a0b5997a1f0e6c9507cd Mon Sep 17 00:00:00 2001 From: avdunn Date: Fri, 13 Mar 2026 09:48:25 -0700 Subject: [PATCH 4/4] Small improvements --- .../msal4j/AadInstanceDiscoveryProvider.java | 9 +++-- .../aad/msal4j/AuthenticationErrorCode.java | 6 ++++ .../aad/msal4j/KnownMetadataProviderTest.java | 34 ------------------- 3 files changed, 10 insertions(+), 39 deletions(-) diff --git a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AadInstanceDiscoveryProvider.java b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AadInstanceDiscoveryProvider.java index 72cf5102..858d84db 100644 --- a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AadInstanceDiscoveryProvider.java +++ b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AadInstanceDiscoveryProvider.java @@ -246,7 +246,7 @@ static AadInstanceDiscoveryResponse sendInstanceDiscoveryRequest(URL authorityUr AadInstanceDiscoveryResponse response = JsonHelper.convertJsonStringToJsonSerializableObject(httpResponse.body(), AadInstanceDiscoveryResponse::fromJson); if (httpResponse.statusCode() != HttpStatus.HTTP_OK) { - if (httpResponse.statusCode() == HttpStatus.HTTP_BAD_REQUEST && response.error().equals("invalid_instance")) { + if (httpResponse.statusCode() == HttpStatus.HTTP_BAD_REQUEST && response.error().equals(AuthenticationErrorCode.INVALID_INSTANCE)) { // instance discovery failed due to an invalid authority, throw an exception. throw MsalServiceExceptionFactory.fromHttpResponse(httpResponse); } @@ -355,10 +355,9 @@ private static void doInstanceDiscoveryAndCache(URL authorityUrl, try { aadInstanceDiscoveryResponse = sendInstanceDiscoveryRequest(authorityUrl, msalRequest, serviceBundle); } catch (MsalServiceException ex) { - // Only propagate "invalid_instance" — this means the authority itself is invalid. - // All other HTTP-level errors (500, 502, 404, etc.) should fall through to the - // fallback path, matching the behavior of MSAL .NET's InstanceDiscoveryManager. - if ("invalid_instance".equals(ex.errorCode())) { + // Throw "invalid_instance" errors: this means the authority itself is invalid. + // All other HTTP-level errors (500, 502, 404, etc.) should fall through to the fallback path. + if (ex.errorCode().equals(AuthenticationErrorCode.INVALID_INSTANCE)) { throw ex; } LOG.warn("Instance discovery request failed with a service error. " + diff --git a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AuthenticationErrorCode.java b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AuthenticationErrorCode.java index 6e7d301c..d5fba3df 100644 --- a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AuthenticationErrorCode.java +++ b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AuthenticationErrorCode.java @@ -160,4 +160,10 @@ public class AuthenticationErrorCode { public static final String CRYPTO_ERROR = "crypto_error"; public static final String INVALID_TIMESTAMP_FORMAT = "invalid_timestamp_format"; + + /** + * Indicates that instance discovery failed because the authority is not a valid instance. + * This is returned by the instance discovery endpoint when the provided authority host is unknown. + */ + public static final String INVALID_INSTANCE = "invalid_instance"; } diff --git a/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/KnownMetadataProviderTest.java b/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/KnownMetadataProviderTest.java index a11dd4d7..ac1293ad 100644 --- a/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/KnownMetadataProviderTest.java +++ b/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/KnownMetadataProviderTest.java @@ -60,40 +60,6 @@ void getMetadataEntry_publicCloudAliases_resolvesToSameEntry() { assertSame(entry3, entry4); } - @Test - void getMetadataEntry_chinaCloud_returnsCorrectAliases() { - // Act - InstanceDiscoveryMetadataEntry entry = KnownMetadataProvider.getMetadataEntry("login.chinacloudapi.cn"); - - // Assert - assertNotNull(entry); - assertEquals("login.partner.microsoftonline.cn", entry.preferredNetwork()); - assertEquals("login.partner.microsoftonline.cn", entry.preferredCache()); - assertEquals(2, entry.aliases().size()); - assertTrue(entry.aliases().contains("login.partner.microsoftonline.cn")); - assertTrue(entry.aliases().contains("login.chinacloudapi.cn")); - - // Both aliases should resolve to the same entry - assertSame(entry, KnownMetadataProvider.getMetadataEntry("login.partner.microsoftonline.cn")); - } - - @Test - void getMetadataEntry_usGov_returnsCorrectAliases() { - // Act - InstanceDiscoveryMetadataEntry entry = KnownMetadataProvider.getMetadataEntry("login.microsoftonline.us"); - - // Assert - assertNotNull(entry); - assertEquals("login.microsoftonline.us", entry.preferredNetwork()); - assertEquals("login.microsoftonline.us", entry.preferredCache()); - assertEquals(2, entry.aliases().size()); - assertTrue(entry.aliases().contains("login.microsoftonline.us")); - assertTrue(entry.aliases().contains("login.usgovcloudapi.net")); - - // Both aliases should resolve to the same entry - assertSame(entry, KnownMetadataProvider.getMetadataEntry("login.usgovcloudapi.net")); - } - @Test void getMetadataEntry_unknownHost_returnsNull() { assertNull(KnownMetadataProvider.getMetadataEntry("custom.authority.example.com"));