Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,14 @@ class AadInstanceDiscoveryProvider {
static {
TRUSTED_SOVEREIGN_HOSTS_SET.addAll(Arrays.asList(
"login.chinacloudapi.cn",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added in the latest commit, Java's list should now match .NET's

"login.partner.microsoftonline.cn",
"login-us.microsoftonline.com",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should this be https://login.microsoftonline.us instead? I am not sure if login-us.microsoftonline.com exist?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Java has both, and so does .NET: https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/blob/112f8228349e96c25846883ed3fd95067d7fda59/src/client/Microsoft.Identity.Client/Instance/Discovery/KnownMetadataProvider.cs#L68

It still could be outdated though, we may need to audit our list of well-known hosts at some point to make sure they're still up to date.

"login.microsoftonline.de",
"login.microsoftonline.us"));
"login.microsoftonline.us",
"login.usgovcloudapi.net",
"login.sovcloud-identity.fr",
"login.sovcloud-identity.de",
"login.sovcloud-identity.sg"));

TRUSTED_HOSTS_SET.addAll(Arrays.asList(
DEFAULT_TRUSTED_HOST,
Expand Down Expand Up @@ -142,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){
Expand Down Expand Up @@ -234,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);
}
Expand Down Expand Up @@ -340,7 +352,26 @@ 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) {
// 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. " +
"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
// entry so subsequent calls don't retry the failing network call.
LOG.warn("Instance discovery network request failed. " +
"MSAL will use fallback instance metadata for {}. Exception: {}", authorityUrl.getHost(), e.getMessage());
cacheInstanceDiscoveryMetadata(authorityUrl.getHost());
return;
}

if (validateAuthority) {
validate(aadInstanceDiscoveryResponse);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
}
Original file line number Diff line number Diff line change
@@ -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<String, InstanceDiscoveryMetadataEntry> KNOWN_ENTRIES;

static {
Map<String, InstanceDiscoveryMetadataEntry> 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<String, InstanceDiscoveryMetadataEntry> entries,
String preferredNetwork,
String preferredCache,
String... aliases) {
Set<String> 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);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
// 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_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"));
}
}
Loading
Loading