diff --git a/changelog.txt b/changelog.txt index b16cf62403..3c152fea46 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,11 +1,13 @@ vNext ---------- +- [MINOR] Add sovereign cloud (Bleu/Delos/SovSG) instance discovery support with pre-seeded cloud metadata, cache-aware discovery routing, and ensureCloudDiscoveryForAuthority API +- [PATCH] Fix bug in Authority.getKnownAuthorityResult where cloud discovery failure would skip knownAuthorities check +- [PATCH] Fix thread safety in Authority.isKnownAuthority and getEquivalentConfiguredAuthority with synchronized block - [MINOR] Add AIDL interface for device registration service.(#2926) - [MINOR] Move debugIntuneCE and prodIntuneCE from BrokerData to AppRegistry as App instances (#3012) - [MINOR] Remove LruCache from SharedPreferencesFileManager (#2910) - [MINOR] Edge TB: Claims (#2925) - [PATCH] Update Moshi to 1.15.2 to resolve okio CVE-2023-3635 vulnerability (#3005) -- [MINOR] Edge TB: PoP support (#3006) - [MINOR] Handle target="_blank" links in authorization WebView (#3010) - [MINOR] Handle openid-vc urls in webview (#3013) diff --git a/common/src/main/java/com/microsoft/identity/common/internal/migration/AdalMigrationAdapter.java b/common/src/main/java/com/microsoft/identity/common/internal/migration/AdalMigrationAdapter.java index fe08235409..79cbb72b2a 100644 --- a/common/src/main/java/com/microsoft/identity/common/internal/migration/AdalMigrationAdapter.java +++ b/common/src/main/java/com/microsoft/identity/common/internal/migration/AdalMigrationAdapter.java @@ -237,18 +237,16 @@ Map deserialize(final Map tokenCache public static boolean loadCloudDiscoveryMetadata() { final String methodTag = TAG + ":loadCloudDiscoveryMetadata"; - if (!AzureActiveDirectory.isInitialized()) { - try { - AzureActiveDirectory.performCloudDiscovery(); - } catch (final ClientException e) { - Logger.error( - methodTag, - "Failed to load instance discovery metadata", - e - ); - } + try { + AzureActiveDirectory.ensureCloudDiscoveryComplete(); + return true; + } catch (final ClientException e) { + Logger.error( + methodTag, + "Failed to load instance discovery metadata", + e + ); + return false; } - - return AzureActiveDirectory.isInitialized(); } } diff --git a/common/src/main/java/com/microsoft/identity/common/internal/request/AuthenticationSchemeTypeAdapter.java b/common/src/main/java/com/microsoft/identity/common/internal/request/AuthenticationSchemeTypeAdapter.java index 52ea9ab768..5756631455 100644 --- a/common/src/main/java/com/microsoft/identity/common/internal/request/AuthenticationSchemeTypeAdapter.java +++ b/common/src/main/java/com/microsoft/identity/common/internal/request/AuthenticationSchemeTypeAdapter.java @@ -26,7 +26,6 @@ import static com.microsoft.identity.common.java.authscheme.BearerAuthenticationSchemeInternal.SCHEME_BEARER; import static com.microsoft.identity.common.java.authscheme.PopAuthenticationSchemeInternal.SCHEME_POP; import static com.microsoft.identity.common.java.authscheme.PopAuthenticationSchemeWithClientKeyInternal.SCHEME_POP_WITH_CLIENT_KEY; -import static com.microsoft.identity.common.java.authscheme.WebAppsPopAuthenticationSchemeInternal.SCHEME_POP_PREGENERATED; import androidx.annotation.NonNull; @@ -43,7 +42,6 @@ import com.microsoft.identity.common.java.authscheme.BearerAuthenticationSchemeInternal; import com.microsoft.identity.common.java.authscheme.PopAuthenticationSchemeInternal; import com.microsoft.identity.common.java.authscheme.PopAuthenticationSchemeWithClientKeyInternal; -import com.microsoft.identity.common.java.authscheme.WebAppsPopAuthenticationSchemeInternal; import com.microsoft.identity.common.logging.Logger; import java.lang.reflect.Type; @@ -87,9 +85,6 @@ public AbstractAuthenticationScheme deserialize(@NonNull final JsonElement json, case SCHEME_POP_WITH_CLIENT_KEY: return context.deserialize(json, PopAuthenticationSchemeWithClientKeyInternal.class); - case SCHEME_POP_PREGENERATED: - return context.deserialize(json, WebAppsPopAuthenticationSchemeInternal.class); - default: Logger.warn( methodTag, @@ -116,9 +111,6 @@ public JsonElement serialize(@NonNull final AbstractAuthenticationScheme src, case SCHEME_POP_WITH_CLIENT_KEY: return context.serialize(src, PopAuthenticationSchemeWithClientKeyInternal.class); - case SCHEME_POP_PREGENERATED: - return context.serialize(src, WebAppsPopAuthenticationSchemeInternal.class); - default: Logger.warn( methodTag, diff --git a/common/src/main/java/com/microsoft/identity/common/internal/ui/webview/switchbrowser/SwitchBrowserUriHelper.kt b/common/src/main/java/com/microsoft/identity/common/internal/ui/webview/switchbrowser/SwitchBrowserUriHelper.kt index 320292d0c0..0abbad26ee 100644 --- a/common/src/main/java/com/microsoft/identity/common/internal/ui/webview/switchbrowser/SwitchBrowserUriHelper.kt +++ b/common/src/main/java/com/microsoft/identity/common/internal/ui/webview/switchbrowser/SwitchBrowserUriHelper.kt @@ -232,24 +232,19 @@ object SwitchBrowserUriHelper { * @throws ClientException with error code [ClientException.MALFORMED_URL] if the URI string is malformed * @throws ClientException with error code [ClientException.UNKNOWN_AUTHORITY] if the URI host is not a valid AAD authority * - * @see AzureActiveDirectory.performCloudDiscovery + * @see AzureActiveDirectory.ensureCloudDiscoveryForAuthority * @see AzureActiveDirectory.isValidCloudHost */ private fun validateActionUri(actionUriString: String) { val methodTag = "$TAG:validateActionUri" - // Check if AzureActiveDirectory is initialized, if not, perform cloud discovery. - if (!AzureActiveDirectory.isInitialized()) { - Logger.warn( - methodTag, - "AzureActiveDirectory is not initialized. Performing cloud discovery." - ) - try { - AzureActiveDirectory.performCloudDiscovery() - } catch (e: Exception) { - val errorMessage = "Failed to perform cloud discovery for AAD authorities." - Logger.error(methodTag, errorMessage, e) - throw ClientException(ClientException.IO_ERROR, errorMessage, e) - } + // Ensure cloud discovery is complete for this authority. + try { + val actionUrlForDiscovery = URL(actionUriString) + AzureActiveDirectory.ensureCloudDiscoveryForAuthority(actionUrlForDiscovery) + } catch (e: Exception) { + val errorMessage = "Failed to perform cloud discovery for AAD authorities." + Logger.error(methodTag, errorMessage, e) + throw ClientException(ClientException.IO_ERROR, errorMessage, e) } // Validate the action uri is not null or empty. val actionUrl: URL diff --git a/common/src/test/java/com/microsoft/identity/common/internal/controllers/BrokerMsalControllerTest.java b/common/src/test/java/com/microsoft/identity/common/internal/controllers/BrokerMsalControllerTest.java index bd8664ab3b..28cdc8955b 100644 --- a/common/src/test/java/com/microsoft/identity/common/internal/controllers/BrokerMsalControllerTest.java +++ b/common/src/test/java/com/microsoft/identity/common/internal/controllers/BrokerMsalControllerTest.java @@ -335,9 +335,7 @@ private static String buildInteractiveGetTokenRequestJson(final boolean isSts) { null, false, null, - null, - null, // tokenType - null // reqCnf + null ); WebAppsGetTokenSubOperationEnvelope envelope = new WebAppsGetTokenSubOperationEnvelope( @@ -363,9 +361,7 @@ private String buildStrictlySilentGetTokenRequestJson(final boolean isSts) throw null, // loginHint false, // instanceAware null, // extraParameters - null, // claims - null, // tokenType - null // reqCnf + null // claims ); WebAppsGetTokenSubOperationEnvelope envelope = diff --git a/common/src/test/java/com/microsoft/identity/common/internal/request/AuthenticationSchemeTypeAdapterTests.java b/common/src/test/java/com/microsoft/identity/common/internal/request/AuthenticationSchemeTypeAdapterTests.java index 5538604250..d4e63d6aee 100644 --- a/common/src/test/java/com/microsoft/identity/common/internal/request/AuthenticationSchemeTypeAdapterTests.java +++ b/common/src/test/java/com/microsoft/identity/common/internal/request/AuthenticationSchemeTypeAdapterTests.java @@ -26,7 +26,6 @@ import com.microsoft.identity.common.java.authscheme.BearerAuthenticationSchemeInternal; import com.microsoft.identity.common.java.authscheme.PopAuthenticationSchemeInternal; import com.microsoft.identity.common.java.authscheme.PopAuthenticationSchemeWithClientKeyInternal; -import com.microsoft.identity.common.java.authscheme.WebAppsPopAuthenticationSchemeInternal; import com.microsoft.identity.common.java.crypto.IDevicePopManager; import org.junit.Assert; @@ -77,17 +76,6 @@ public void testSerialize_PopAuthenticationSchemeWithClientKeyInternal() throws Assert.assertEquals(expectedJson, json); } - @Test - public void testSerialize_WebAppsPopAuthenticationSchemeInternal() throws MalformedURLException, IllegalArgumentException { - final String expectedJson = - "{\"req_cnf\":\"eyJraWQiOiJteS1rZXktaWQifQ\",\"kid\":\"my-key-id\",\"name\":\"PoP_Pregenerated\"}"; - final WebAppsPopAuthenticationSchemeInternal scheme = - new WebAppsPopAuthenticationSchemeInternal("eyJraWQiOiJteS1rZXktaWQifQ"); - - final String json = AuthenticationSchemeTypeAdapter.getGsonInstance().toJson(scheme); - Assert.assertEquals(expectedJson, json); - } - @Test public void testDeserialize_BearerAuthenticationSchemeInternal() { final String json = "{\"name\":\"Bearer\"}"; @@ -115,14 +103,4 @@ public void testDeserialize_PopAuthenticationSchemeWithClientKeyInternal() { Assert.assertTrue(authenticationScheme instanceof PopAuthenticationSchemeWithClientKeyInternal); } - - @Test - public void testDeserialize_WebAppsPopAuthenticationSchemeInternal() { - final String json = - "{\"http_method\":\"GET\",\"url\":\"https://xyz.com\",\"nonce\":\"nonce_test\",\"client_claims\":\"clientClaims_test\",\"req_cnf\":\"eyJraWQiOiJteS1rZXktaWQifQ\",\"name\":\"PoP_Pregenerated\"}"; - final AbstractAuthenticationScheme authenticationScheme = - AuthenticationSchemeTypeAdapter.getGsonInstance().fromJson(json, AbstractAuthenticationScheme.class); - - Assert.assertTrue(authenticationScheme instanceof WebAppsPopAuthenticationSchemeInternal); - } } diff --git a/common4j/src/main/com/microsoft/identity/common/java/authorities/Authority.java b/common4j/src/main/com/microsoft/identity/common/java/authorities/Authority.java index 4506cc7eed..fd916e9976 100644 --- a/common4j/src/main/com/microsoft/identity/common/java/authorities/Authority.java +++ b/common4j/src/main/com/microsoft/identity/common/java/authorities/Authority.java @@ -223,14 +223,16 @@ private static Authority getEquivalentConfiguredAuthority(@NonNull final String // Iterate over all of the developer trusted authorities and check if the authorities // are the same... - for (final Authority currentAuthority : knownAuthorities) { - if (!StringUtil.isNullOrEmpty(currentAuthority.mAuthorityUrlString)) { - final URL currentAuthorityUrl = new URL(currentAuthority.mAuthorityUrlString); - final String currentHttpAuthority = currentAuthorityUrl.getAuthority(); - - if (httpAuthority.equalsIgnoreCase(currentHttpAuthority)) { - result = currentAuthority; - break; + synchronized (sLock) { + for (final Authority currentAuthority : knownAuthorities) { + if (!StringUtil.isNullOrEmpty(currentAuthority.mAuthorityUrlString)) { + final URL currentAuthorityUrl = new URL(currentAuthority.mAuthorityUrlString); + final String currentHttpAuthority = currentAuthorityUrl.getAuthority(); + + if (httpAuthority.equalsIgnoreCase(currentHttpAuthority)) { + result = currentAuthority; + break; + } } } } @@ -309,22 +311,6 @@ public int hashCode() { private static final List knownAuthorities = new ArrayList<>(); private static final Object sLock = new Object(); - private static void performCloudDiscovery() - throws ClientException { - final String methodName = ":performCloudDiscovery"; - Logger.verbose( - TAG + methodName, - "Performing cloud discovery..." - ); - synchronized (sLock) { - if (!AzureActiveDirectory.isInitialized()) { - Logger.verbose(TAG + methodName, "Not initialized. Starting request."); - AzureActiveDirectory.performCloudDiscovery(); - Logger.info(TAG + methodName, "Loaded cloud metadata."); - } - } - } - public static void addKnownAuthorities(List authorities) { synchronized (sLock) { knownAuthorities.addAll(authorities); @@ -356,17 +342,19 @@ public static boolean isKnownAuthority(Authority authority) { } //Check if authority was added to configuration - for (final Authority currentAuthority : knownAuthorities) { - if (currentAuthority.mAuthorityUrlString != null && - authority.getAuthorityURL() != null && - authority.getAuthorityURL().getAuthority() != null && - currentAuthority.mAuthorityUrlString.toLowerCase(Locale.ROOT).contains( - authority - .getAuthorityURL() - .getAuthority() - .toLowerCase(Locale.ROOT))) { - knownToDeveloper = true; - break; + synchronized (sLock) { + for (final Authority currentAuthority : knownAuthorities) { + if (currentAuthority.mAuthorityUrlString != null && + authority.getAuthorityURL() != null && + authority.getAuthorityURL().getAuthority() != null && + currentAuthority.mAuthorityUrlString.toLowerCase(Locale.ROOT).contains( + authority + .getAuthorityURL() + .getAuthority() + .toLowerCase(Locale.ROOT))) { + knownToDeveloper = true; + break; + } } } @@ -399,23 +387,26 @@ public static KnownAuthorityResult getKnownAuthorityResult(Authority authority) boolean known = false; try { - performCloudDiscovery(); + AzureActiveDirectory.ensureCloudDiscoveryForAuthority(authority); } catch (final ClientException ex) { - clientException = ex; + // Cloud discovery failed (e.g. network error). + // Log but continue — the authority may still be known via hardcoded + // metadata or developer configuration. + Logger.warn(TAG + methodName, + "Cloud discovery failed, will check hardcoded/configured authorities. Error: " + + ex.getErrorCode()); } Logger.info(TAG + methodName, "Cloud discovery complete."); - if (clientException == null) { - if (!isKnownAuthority(authority)) { - clientException = new ClientException( - ClientException.UNKNOWN_AUTHORITY, - "Provided authority is not known. MSAL will only make requests to known authorities" - ); - } else { - Logger.info(TAG + methodName, "Cloud is known."); - known = true; - } + if (!isKnownAuthority(authority)) { + clientException = new ClientException( + ClientException.UNKNOWN_AUTHORITY, + "Provided authority is not known. MSAL will only make requests to known authorities" + ); + } else { + Logger.info(TAG + methodName, "Cloud is known."); + known = true; } return new KnownAuthorityResult(known, clientException); diff --git a/common4j/src/main/com/microsoft/identity/common/java/authorities/AzureActiveDirectoryAuthority.java b/common4j/src/main/com/microsoft/identity/common/java/authorities/AzureActiveDirectoryAuthority.java index 4d3d9abf8a..695ec2fcd5 100644 --- a/common4j/src/main/com/microsoft/identity/common/java/authorities/AzureActiveDirectoryAuthority.java +++ b/common4j/src/main/com/microsoft/identity/common/java/authorities/AzureActiveDirectoryAuthority.java @@ -177,10 +177,8 @@ public OAuth2Strategy createOAuth2Strategy(@NonNull final OAuth2StrategyParamete //@WorkerThread public synchronized boolean isSameCloudAsAuthority(@NonNull final AzureActiveDirectoryAuthority authorityToCheck) throws ClientException { - if (!AzureActiveDirectory.isInitialized()) { - // Cloud discovery is needed in order to make sure that we have a preferred_network_host_name to cloud aliases mappings - AzureActiveDirectory.performCloudDiscovery(); - } + // Cloud discovery is needed to make sure that we have preferred_network_host_name to cloud aliases mappings + AzureActiveDirectory.ensureCloudDiscoveryForAuthority(this); final AzureActiveDirectoryCloud cloudOfThisAuthority = getAzureActiveDirectoryCloud(mAudience); final AzureActiveDirectoryCloud cloudOfAuthorityToCheck = getAzureActiveDirectoryCloud(authorityToCheck.getAudience()); diff --git a/common4j/src/main/com/microsoft/identity/common/java/authscheme/AuthenticationSchemeFactory.java b/common4j/src/main/com/microsoft/identity/common/java/authscheme/AuthenticationSchemeFactory.java index c80ac5d54b..afc9a13544 100644 --- a/common4j/src/main/com/microsoft/identity/common/java/authscheme/AuthenticationSchemeFactory.java +++ b/common4j/src/main/com/microsoft/identity/common/java/authscheme/AuthenticationSchemeFactory.java @@ -106,7 +106,6 @@ public static AbstractAuthenticationScheme createScheme(@NonNull final IPlatform * @return boolean indicating if the the authentication scheme is a PoP authentication scheme */ public static boolean isPopAuthenticationScheme(@NonNull final AbstractAuthenticationScheme authenticationScheme) { - return authenticationScheme instanceof IPoPAuthenticationSchemeParams - || authenticationScheme instanceof WebAppsPopAuthenticationSchemeInternal; + return authenticationScheme instanceof IPoPAuthenticationSchemeParams; } } diff --git a/common4j/src/main/com/microsoft/identity/common/java/authscheme/WebAppsPopAuthenticationSchemeInternal.java b/common4j/src/main/com/microsoft/identity/common/java/authscheme/WebAppsPopAuthenticationSchemeInternal.java deleted file mode 100644 index ebee5a5e85..0000000000 --- a/common4j/src/main/com/microsoft/identity/common/java/authscheme/WebAppsPopAuthenticationSchemeInternal.java +++ /dev/null @@ -1,116 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// All rights reserved. -// -// This code is licensed under the MIT License. -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files(the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions : -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. -package com.microsoft.identity.common.java.authscheme; - -import static com.microsoft.identity.common.java.authscheme.PopAuthenticationSchemeInternal.SerializedNames.KID; -import static com.microsoft.identity.common.java.util.StringUtil.isNullOrEmpty; - -import com.google.gson.JsonObject; -import com.google.gson.JsonParser; -import com.google.gson.annotations.SerializedName; -import com.microsoft.identity.common.java.base64.Base64Util; -import com.microsoft.identity.common.java.logging.Logger; - -import lombok.EqualsAndHashCode; -import lombok.Getter; -import lombok.NonNull; -import lombok.experimental.Accessors; - -/** - * Internal representation of a PoP Authentication Scheme for WebApps (Edge Token Broker). - * Unlike {@link PopAuthenticationSchemeInternal}, this scheme uses a pre-generated req_cnf - * provided in the token request, rather than one generated from the device's key store. - */ -@Getter -@EqualsAndHashCode(callSuper = true) -@Accessors(prefix = "m") -public class WebAppsPopAuthenticationSchemeInternal - extends TokenAuthenticationScheme - implements ITokenAuthenticationSchemeInternal { - private static final String TAG = WebAppsPopAuthenticationSchemeInternal.class.getSimpleName(); - private static final long serialVersionUID = 1L; - - /** - * The name of this auth scheme. Used to distinguish it from other PoP schemes. - */ - public static final String SCHEME_POP_PREGENERATED = "PoP_Pregenerated"; - - public static class SerializedNames { - public static final String REQUEST_CONFIRMATION = "req_cnf"; - } - - @SerializedName(SerializedNames.REQUEST_CONFIRMATION) - private String mRequestConfirmation; - - @SerializedName(KID) - private String mKid; - - /** - * Constructor for gson use. Package-private to restrict direct instantiation - * while allowing Gson's reflective deserialization to construct instances. - * {@code mRequestConfirmation} will be populated by Gson during deserialization - * and must not be used until the object is fully initialized. - */ - WebAppsPopAuthenticationSchemeInternal() { - super(SCHEME_POP_PREGENERATED); - } - - /** - * Constructs a new WebAppsPopAuthenticationSchemeInternal. - * - * @param requestConfirmation The pre-generated request confirmation (req_cnf) value. - * @throws IllegalArgumentException if requestConfirmation is null or empty. - */ - public WebAppsPopAuthenticationSchemeInternal(@NonNull final String requestConfirmation) { - super(SCHEME_POP_PREGENERATED); - - if (isNullOrEmpty(requestConfirmation)) { - throw new IllegalArgumentException( - "requestConfirmation (req_cnf) cannot be null or empty for WebAppsPopAuthenticationSchemeInternal" - ); - } - - mRequestConfirmation = requestConfirmation; - try { - final String reqCnfJson = Base64Util.decodeUrlSafeStringToString(requestConfirmation); - final JsonObject jsonObject = JsonParser.parseString(reqCnfJson).getAsJsonObject(); - if (jsonObject != null && jsonObject.has(KID)) { - mKid = jsonObject.get(KID).getAsString(); - } - } catch (Exception e) { - Logger.warn(TAG, "Failed to parse kid from reqCnf. Exception type and message: " + e.getClass().getSimpleName() + ": " + e.getMessage()); - } - } - - /** - * Returns the access token as-is. For WebApps PoP, the access token returned from ESTS - * is already in the appropriate PoP format. - * - * @param accessToken The access token to return. - * @return The access token unchanged. - */ - @Override - public String getAccessTokenForScheme(@NonNull final String accessToken) { - return accessToken; - } -} diff --git a/common4j/src/main/com/microsoft/identity/common/java/base64/Base64Util.kt b/common4j/src/main/com/microsoft/identity/common/java/base64/Base64Util.kt index 17c75c664d..78b4534208 100644 --- a/common4j/src/main/com/microsoft/identity/common/java/base64/Base64Util.kt +++ b/common4j/src/main/com/microsoft/identity/common/java/base64/Base64Util.kt @@ -208,31 +208,6 @@ class Base64Util { } } - /** - * Decodes a base64url-encoded string (URL-safe, no padding, no wrap). - * - * @param input The base64url-encoded string - * @return The decoded byte array - */ - @JvmStatic - fun decodeUrlSafeString(input: String): ByteArray { - return decode( - input, - Base64Flags.URL_SAFE, Base64Flags.NO_PADDING, Base64Flags.NO_WRAP - ) - } - - /** - * Decodes a base64url-encoded string and returns it as a UTF-8 string. - * - * @param input The base64url-encoded string - * @return The decoded string - */ - @JvmStatic - fun decodeUrlSafeStringToString(input: String): String { - return String(decodeUrlSafeString(input), StandardCharsets.UTF_8) - } - //endregion } } diff --git a/common4j/src/main/com/microsoft/identity/common/java/cache/AbstractAccountCredentialCache.java b/common4j/src/main/com/microsoft/identity/common/java/cache/AbstractAccountCredentialCache.java index de0a162fdb..34b1c3e018 100644 --- a/common4j/src/main/com/microsoft/identity/common/java/cache/AbstractAccountCredentialCache.java +++ b/common4j/src/main/com/microsoft/identity/common/java/cache/AbstractAccountCredentialCache.java @@ -26,7 +26,6 @@ import com.microsoft.identity.common.java.authscheme.PopAuthenticationSchemeInternal; import com.microsoft.identity.common.java.authscheme.PopAuthenticationSchemeWithClientKeyInternal; -import com.microsoft.identity.common.java.authscheme.WebAppsPopAuthenticationSchemeInternal; import com.microsoft.identity.common.java.dto.AccessTokenRecord; import com.microsoft.identity.common.java.dto.AccountRecord; import com.microsoft.identity.common.java.dto.Credential; @@ -240,8 +239,7 @@ protected List getCredentialsFilteredByInternal(@NonNull final List< if (TokenRequest.TokenType.POP.equalsIgnoreCase(atType)) { if (!(authScheme.equalsIgnoreCase(PopAuthenticationSchemeWithClientKeyInternal.SCHEME_POP_WITH_CLIENT_KEY) - || authScheme.equalsIgnoreCase(PopAuthenticationSchemeInternal.SCHEME_POP) - || authScheme.equalsIgnoreCase(WebAppsPopAuthenticationSchemeInternal.SCHEME_POP_PREGENERATED))) { + || authScheme.equalsIgnoreCase(PopAuthenticationSchemeInternal.SCHEME_POP))) { continue; } } else if (!authScheme.equalsIgnoreCase(atType)) continue; diff --git a/common4j/src/main/com/microsoft/identity/common/java/cache/MicrosoftStsAccountCredentialAdapter.java b/common4j/src/main/com/microsoft/identity/common/java/cache/MicrosoftStsAccountCredentialAdapter.java index 340b4a3850..05cdc998aa 100644 --- a/common4j/src/main/com/microsoft/identity/common/java/cache/MicrosoftStsAccountCredentialAdapter.java +++ b/common4j/src/main/com/microsoft/identity/common/java/cache/MicrosoftStsAccountCredentialAdapter.java @@ -24,13 +24,10 @@ import static com.microsoft.identity.common.java.AuthenticationConstants.DEFAULT_SCOPES; -import com.google.gson.JsonObject; -import com.google.gson.JsonParser; import com.microsoft.identity.common.java.authorities.Authority; import com.microsoft.identity.common.java.authscheme.AbstractAuthenticationScheme; import com.microsoft.identity.common.java.authscheme.PopAuthenticationSchemeInternal; import com.microsoft.identity.common.java.authscheme.PopAuthenticationSchemeWithClientKeyInternal; -import com.microsoft.identity.common.java.authscheme.WebAppsPopAuthenticationSchemeInternal; import com.microsoft.identity.common.java.commands.parameters.TokenCommandParameters; import com.microsoft.identity.common.java.crypto.IDevicePopManager; import com.microsoft.identity.common.java.dto.AccessTokenRecord; @@ -358,9 +355,6 @@ public AccessTokenRecord createAccessTokenRecord( } else if (authenticationScheme instanceof PopAuthenticationSchemeWithClientKeyInternal) { accessTokenRecord.setCredentialType(CredentialType.AccessToken_With_AuthScheme.name()); accessTokenRecord.setKid(((PopAuthenticationSchemeWithClientKeyInternal) authenticationScheme).getKid()); - } else if (authenticationScheme instanceof WebAppsPopAuthenticationSchemeInternal) { - accessTokenRecord.setCredentialType(CredentialType.AccessToken_With_AuthScheme.name()); - accessTokenRecord.setKid(((WebAppsPopAuthenticationSchemeInternal) authenticationScheme).getKid()); } else { accessTokenRecord.setCredentialType(CredentialType.AccessToken.name()); } diff --git a/common4j/src/main/com/microsoft/identity/common/java/commands/parameters/SilentTokenCommandParameters.java b/common4j/src/main/com/microsoft/identity/common/java/commands/parameters/SilentTokenCommandParameters.java index e88657e00b..055b6f2d24 100644 --- a/common4j/src/main/com/microsoft/identity/common/java/commands/parameters/SilentTokenCommandParameters.java +++ b/common4j/src/main/com/microsoft/identity/common/java/commands/parameters/SilentTokenCommandParameters.java @@ -42,8 +42,6 @@ public class SilentTokenCommandParameters extends TokenCommandParameters { private static final String TAG = SilentTokenCommandParameters.class.getSimpleName(); - private static final Object sLock = new Object(); - @Override public void validate() throws ArgumentException, ClientException { super.validate(); @@ -82,9 +80,7 @@ private boolean authorityMatchesAccountEnvironment() { final String errorCode; try { - if (!AzureActiveDirectory.isInitialized()) { - performCloudDiscovery(); - } + AzureActiveDirectory.ensureCloudDiscoveryForAuthority(getAuthority()); final AzureActiveDirectoryCloud cloud = AzureActiveDirectory.getAzureActiveDirectoryCloudFromHostName(getAccount().getEnvironment()); return cloud != null && cloud.getPreferredNetworkHostName().equals(getAuthority().getAuthorityURL().getAuthority()); } catch (final ClientException e) { @@ -101,16 +97,4 @@ private boolean authorityMatchesAccountEnvironment() { cause, errorCode); } - - private static void performCloudDiscovery() - throws ClientException { - final String methodName = ":performCloudDiscovery"; - Logger.verbose( - TAG + methodName, - "Performing cloud discovery..." - ); - synchronized (sLock) { - AzureActiveDirectory.performCloudDiscovery(); - } - } } diff --git a/common4j/src/main/com/microsoft/identity/common/java/commands/webapps/WebAppsGetTokenSubOperationRequest.kt b/common4j/src/main/com/microsoft/identity/common/java/commands/webapps/WebAppsGetTokenSubOperationRequest.kt index cb94c65cbc..fc35ac5d34 100644 --- a/common4j/src/main/com/microsoft/identity/common/java/commands/webapps/WebAppsGetTokenSubOperationRequest.kt +++ b/common4j/src/main/com/microsoft/identity/common/java/commands/webapps/WebAppsGetTokenSubOperationRequest.kt @@ -77,23 +77,13 @@ data class WebAppsGetTokenSubOperationRequest( val instanceAware : Boolean = false, // Optional; additional extra query parameters to include in the token request. - // Note: PoP token parameters may come through here as a fallback if not set at the top level. + // Note: PoP token parameters will come through here. @SerializedName(FIELD_EXTRA_PARAMETERS) val extraParameters: Map? = null, // Optional. @SerializedName(FIELD_CLAIMS) - val claims: String? = null, - - // Optional; token type for PoP support. Values: "pop" or "bearer" (case-insensitive). - // Top-level value takes priority over extraParameters. - @SerializedName(FIELD_TOKEN_TYPE) - val tokenType: String? = null, - - // Optional; request confirmation value for PoP support. - // Top-level value takes priority over extraParameters. - @SerializedName(FIELD_REQ_CNF) - val reqCnf: String? = null + val claims: String? = null ) { companion object { const val FIELD_HOME_ACCOUNT_ID = "accountId" @@ -111,7 +101,5 @@ data class WebAppsGetTokenSubOperationRequest( const val FIELD_EXTRA_PARAMETERS = "extraParameters" const val DEFAULT_AUTHORITY = "https://login.microsoftonline.com/common" const val FIELD_CLAIMS = "claims" - const val FIELD_TOKEN_TYPE = "tokenType" - const val FIELD_REQ_CNF = "reqCnf" } } diff --git a/common4j/src/main/com/microsoft/identity/common/java/nativeauth/commands/parameters/AcquireTokenNoFixedScopesCommandParameters.java b/common4j/src/main/com/microsoft/identity/common/java/nativeauth/commands/parameters/AcquireTokenNoFixedScopesCommandParameters.java index e3d807d124..3072498b7e 100644 --- a/common4j/src/main/com/microsoft/identity/common/java/nativeauth/commands/parameters/AcquireTokenNoFixedScopesCommandParameters.java +++ b/common4j/src/main/com/microsoft/identity/common/java/nativeauth/commands/parameters/AcquireTokenNoFixedScopesCommandParameters.java @@ -47,8 +47,6 @@ public class AcquireTokenNoFixedScopesCommandParameters extends BaseNativeAuthCo private static final String TAG = AcquireTokenNoFixedScopesCommandParameters.class.getSimpleName(); - private static final Object sLock = new Object(); - private final IAccountRecord account; @NonNull @@ -102,48 +100,4 @@ public void validate() throws ArgumentException { // This logic should also apply to CIAM authorities } } - - /** - * Note - this method may throw a variety of RuntimeException if we cannot perform cloud - * discovery to determine the set of cloud aliases. - * @return true if the authority matches the cloud environment that the account is homed in. - */ - private boolean authorityMatchesAccountEnvironment() { - final String methodName = ":authorityMatchesAccountEnvironment"; - - final Exception cause; - final String errorCode; - - try { - if (!AzureActiveDirectory.isInitialized()) { - performCloudDiscovery(); - } - final AzureActiveDirectoryCloud cloud = AzureActiveDirectory.getAzureActiveDirectoryCloudFromHostName(getAccount().getEnvironment()); - return cloud != null && cloud.getPreferredNetworkHostName().equals(getAuthority().getAuthorityURL().getAuthority()); - } catch (final ClientException e) { - cause = e; - errorCode = e.getErrorCode(); - } - - Logger.error( - TAG + methodName, - "Unable to perform cloud discovery", - cause); - throw new TerminalException( - "Unable to perform cloud discovery in order to validate request authority", - cause, - errorCode); - } - - private static void performCloudDiscovery() - throws ClientException { - final String methodName = ":performCloudDiscovery"; - Logger.verbose( - TAG + methodName, - "Performing cloud discovery..." - ); - synchronized (sLock) { - AzureActiveDirectory.performCloudDiscovery(); - } - } } diff --git a/common4j/src/main/com/microsoft/identity/common/java/providers/microsoft/azureactivedirectory/AzureActiveDirectory.java b/common4j/src/main/com/microsoft/identity/common/java/providers/microsoft/azureactivedirectory/AzureActiveDirectory.java index a041d6f8c8..7b226b5471 100644 --- a/common4j/src/main/com/microsoft/identity/common/java/providers/microsoft/azureactivedirectory/AzureActiveDirectory.java +++ b/common4j/src/main/com/microsoft/identity/common/java/providers/microsoft/azureactivedirectory/AzureActiveDirectory.java @@ -26,6 +26,7 @@ import com.google.gson.Gson; import com.google.gson.reflect.TypeToken; +import com.microsoft.identity.common.java.authorities.Authority; import com.microsoft.identity.common.java.authorities.Environment; import com.microsoft.identity.common.java.cache.HttpCache; import com.microsoft.identity.common.java.exception.ClientException; @@ -53,7 +54,10 @@ import java.net.URISyntaxException; import java.net.URL; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Map; @@ -64,6 +68,8 @@ import lombok.NonNull; +import javax.annotation.Nullable; + /** * Implements the IdentityProvider base class... */ @@ -79,13 +85,46 @@ public class AzureActiveDirectory private static final String API_VERSION = "api-version"; private static final String API_VERSION_VALUE = "1.1"; private static final String AUTHORIZATION_ENDPOINT = "authorization_endpoint"; - private static final String AUTHORIZATION_ENDPOINT_VALUE = "https://login.microsoftonline.com/common/oauth2/v2.0/authorize"; private static ConcurrentMap sAadClouds = new ConcurrentHashMap<>(); - private static boolean sIsInitialized = false; private static Environment sEnvironment = Environment.Production; private static final HttpClient httpClient = UrlConnectionHttpClient.getDefaultInstance(); + // Sovereign cloud hosts that have their own instance discovery endpoints. + // These hosts serve discovery metadata directly instead of going through + // the default global endpoint (login.microsoftonline.com). + private static final Set SOV_CLOUD_DISCOVERY_HOSTS = Collections.unmodifiableSet(new HashSet<>(Arrays.asList( + AzureActiveDirectoryCloud.BLEU.getPreferredNetworkHostName(), + AzureActiveDirectoryCloud.DELOS.getPreferredNetworkHostName(), + AzureActiveDirectoryCloud.SOVSG.getPreferredNetworkHostName() + ))); + + static { + // Pre-seed sAadClouds with sovereign cloud metadata so they are recognized + // without a network call to the global instance discovery endpoint. + for (final AzureActiveDirectoryCloud cloud : new AzureActiveDirectoryCloud[]{ + AzureActiveDirectoryCloud.BLEU, + AzureActiveDirectoryCloud.DELOS, + AzureActiveDirectoryCloud.SOVSG + }) { + sAadClouds.put( + cloud.getPreferredNetworkHostName().toLowerCase(Locale.US), + cloud + ); + } + } + + /** + * Returns true if the given host is a sovereign cloud host that has its own + * instance discovery endpoint. + * + * @param host The hostname to check. + * @return true if the host is in the sovereign cloud discovery hosts list. + */ + public static boolean isSovCloudDiscoveryHost(@NonNull final String host) { + return SOV_CLOUD_DISCOVERY_HOSTS.contains(host.toLowerCase(Locale.US)); + } + @Override public AzureActiveDirectoryOAuth2Strategy createOAuth2Strategy(@NonNull final AzureActiveDirectoryOAuth2Configuration config, @NonNull final IPlatformComponents commonComponents) throws ClientException { @@ -104,18 +143,10 @@ public static synchronized boolean isValidCloudHost(@NonNull final URL authority return hasCloudHost(authorityUrl) && getAzureActiveDirectoryCloud(authorityUrl).isValidated(); } - public static synchronized boolean isInitialized() { - return sIsInitialized; - } - public static synchronized void setEnvironment(@NonNull final Environment environment) { if (environment != sEnvironment) { - // Environment changed, so mark sIsInitialized to false - // to make a instance discovery network request for this environment. - sIsInitialized = false; sEnvironment = environment; } - } public static synchronized Environment getEnvironment() { @@ -188,8 +219,6 @@ public static synchronized void initializeCloudMetadata(@NonNull final String au sAadClouds.put(alias.toLowerCase(Locale.US), cloud); } } - - sIsInitialized = true; } public static synchronized String getDefaultCloudUrl() { @@ -204,12 +233,23 @@ public static synchronized String getDefaultCloudUrl() { public static synchronized void performCloudDiscovery() throws ClientException { - final String methodName = ":performCloudDiscovery"; + performCloudDiscoveryForCloudUrl(getDefaultCloudUrl()); + } + + /** + * Performs instance discovery using the specified cloud URL as the discovery endpoint host. + * + * @param cloudUrl The base cloud URL to use for the discovery request + * (e.g. "https://login.sovcloud-identity.fr"). + */ + public static synchronized void performCloudDiscoveryForCloudUrl(@NonNull final String cloudUrl) + throws ClientException { + final String methodName = ":performCloudDiscoveryForCloudUrl"; final URI instanceDiscoveryRequestUri; try { - instanceDiscoveryRequestUri = new CommonURIBuilder(getDefaultCloudUrl() + AAD_INSTANCE_DISCOVERY_ENDPOINT) + instanceDiscoveryRequestUri = new CommonURIBuilder(cloudUrl + AAD_INSTANCE_DISCOVERY_ENDPOINT) .setParameter(API_VERSION, API_VERSION_VALUE) - .setParameter(AUTHORIZATION_ENDPOINT, AUTHORIZATION_ENDPOINT_VALUE) + .setParameter(AUTHORIZATION_ENDPOINT, cloudUrl + "/common/oauth2/v2.0/authorize") .build(); } catch (URISyntaxException e) { throw new ClientException(ClientException.MALFORMED_URL, e.getMessage(), e); @@ -220,7 +260,7 @@ public static synchronized void performCloudDiscovery() new HashMap<>()); if (response.getStatusCode() >= HttpURLConnection.HTTP_BAD_REQUEST) { - Logger.warn(TAG + methodName, "Error getting cloud information"); + Logger.warn(TAG + methodName, "Error getting cloud information from " + cloudUrl); } else { // Our request was successful. Flush the HTTP cache to disk. Should only happen once // per app launch. Instance Discovery Metadata will be cached in-memory @@ -242,18 +282,80 @@ public static synchronized void performCloudDiscovery() sAadClouds.put(alias.toLowerCase(Locale.US), cloud); } } - - sIsInitialized = true; } } /** - * Ensures that cloud discovery has been completed. If not, it will perform cloud discovery. + * Returns the host portion of the default cloud URL for the current environment. + */ + private static String getDefaultCloudHost() { + return URI.create(getDefaultCloudUrl()).getHost().toLowerCase(Locale.US); + } + + /** + * Ensures that cloud discovery has been completed using the default global endpoint. + * Delegates to {@link #ensureCloudDiscoveryForAuthority(URL)} with the default cloud URL. */ public static synchronized void ensureCloudDiscoveryComplete() throws ClientException { - final String methodTag = TAG + ":ensureCloudDiscoveryComplete"; - if (!sIsInitialized) { - Logger.info(methodTag, "Cloud metadata is not initialized. Performing cloud discovery."); + try { + ensureCloudDiscoveryForAuthority(new URL(getDefaultCloudUrl())); + } catch (final MalformedURLException e) { + throw new ClientException(ClientException.MALFORMED_URL, e.getMessage(), e); + } + } + + /** + * Ensures that cloud discovery has been completed for the given authority. + * Extracts the authority URL and delegates to {@link #ensureCloudDiscoveryForAuthority(URL)}. + * If authority is null or has no URL, falls back to the default global endpoint. + * + * @param authority The authority whose URL determines the discovery endpoint, or null to use the default. + */ + public static synchronized void ensureCloudDiscoveryForAuthority(@Nullable final Authority authority) + throws ClientException { + ensureCloudDiscoveryForAuthority( + authority != null ? authority.getAuthorityURL() : null + ); + } + + /** + * Ensures that cloud discovery has been completed for the given authority URL. + * If authorityUrl is null, falls back to the default global endpoint. + * If the authority's host is already present in the cloud metadata cache, this is a no-op. + * For unknown hosts that route to the global endpoint, if the global default host + * is already cached then global discovery has already been performed — also a no-op. + * Otherwise, performs discovery using the appropriate endpoint: + *
    + *
  • For sovereign cloud hosts — queries the cloud's own discovery endpoint.
  • + *
  • For all other hosts — queries the default global discovery endpoint.
  • + *
+ * + * @param authorityUrl The authority URL whose host determines the discovery endpoint, or null to use the default. + */ + public static synchronized void ensureCloudDiscoveryForAuthority(@Nullable final URL authorityUrl) + throws ClientException { + if (authorityUrl == null) { + ensureCloudDiscoveryComplete(); + return; + } + final String host = authorityUrl.getHost(); + if (host == null) { + return; + } + final String hostLower = host.toLowerCase(Locale.US); + // Already in cache — no discovery needed. + if (sAadClouds.containsKey(hostLower)) { + return; + } + if (isSovCloudDiscoveryHost(hostLower)) { + // Sovereign cloud host not yet in cache — discover from its own endpoint. + performCloudDiscoveryForCloudUrl(authorityUrl.getProtocol() + "://" + host); + } else { + // not a sovereign cloud host — keep existing behavior would route to global + // Check if global is already cached. + if (sAadClouds.containsKey(getDefaultCloudHost())) { + return; + } performCloudDiscovery(); } } @@ -335,10 +437,10 @@ public static String buildAndValidateAuthorityFromWebAppSender(final String send } final URI normalized = new URI(scheme + "://" + host + "/common"); - - ensureCloudDiscoveryComplete(); final URL authorityUrl = normalized.toURL(); + ensureCloudDiscoveryForAuthority(authorityUrl); + if (!hasCloudHost(authorityUrl)) { Logger.warn(methodTag, "Host not found in known AAD clouds: " + host); throw new ClientException( diff --git a/common4j/src/main/com/microsoft/identity/common/java/providers/microsoft/azureactivedirectory/AzureActiveDirectoryCloud.java b/common4j/src/main/com/microsoft/identity/common/java/providers/microsoft/azureactivedirectory/AzureActiveDirectoryCloud.java index bdef187bd3..f575a48000 100644 --- a/common4j/src/main/com/microsoft/identity/common/java/providers/microsoft/azureactivedirectory/AzureActiveDirectoryCloud.java +++ b/common4j/src/main/com/microsoft/identity/common/java/providers/microsoft/azureactivedirectory/AzureActiveDirectoryCloud.java @@ -25,6 +25,7 @@ import com.google.gson.annotations.SerializedName; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import lombok.EqualsAndHashCode; @@ -115,4 +116,30 @@ public boolean isValidated() { void setIsValidated(final boolean isValidated) { mIsValidated = isValidated; } + + // Sovereign cloud host name constants + public static final String BLEU_CLOUD_HOST = "login.sovcloud-identity.fr"; + public static final String DELOS_CLOUD_HOST = "login.sovcloud-identity.de"; + public static final String SOVSG_CLOUD_HOST = "login.sovcloud-identity.sg"; + + /** Bleu sovereign cloud (France). */ + public static final AzureActiveDirectoryCloud BLEU = new AzureActiveDirectoryCloud( + BLEU_CLOUD_HOST, + BLEU_CLOUD_HOST, + Collections.singletonList(BLEU_CLOUD_HOST) + ); + + /** Delos sovereign cloud (Germany). */ + public static final AzureActiveDirectoryCloud DELOS = new AzureActiveDirectoryCloud( + DELOS_CLOUD_HOST, + DELOS_CLOUD_HOST, + Collections.singletonList(DELOS_CLOUD_HOST) + ); + + /** SovSG sovereign cloud (Singapore). */ + public static final AzureActiveDirectoryCloud SOVSG = new AzureActiveDirectoryCloud( + SOVSG_CLOUD_HOST, + SOVSG_CLOUD_HOST, + Collections.singletonList(SOVSG_CLOUD_HOST) + ); } diff --git a/common4j/src/main/com/microsoft/identity/common/java/providers/microsoft/microsoftsts/MicrosoftStsOAuth2Strategy.java b/common4j/src/main/com/microsoft/identity/common/java/providers/microsoft/microsoftsts/MicrosoftStsOAuth2Strategy.java index 1409949d61..f0a940b344 100644 --- a/common4j/src/main/com/microsoft/identity/common/java/providers/microsoft/microsoftsts/MicrosoftStsOAuth2Strategy.java +++ b/common4j/src/main/com/microsoft/identity/common/java/providers/microsoft/microsoftsts/MicrosoftStsOAuth2Strategy.java @@ -35,7 +35,6 @@ import com.microsoft.identity.common.java.authscheme.AuthenticationSchemeFactory; import com.microsoft.identity.common.java.authscheme.PopAuthenticationSchemeInternal; import com.microsoft.identity.common.java.authscheme.PopAuthenticationSchemeWithClientKeyInternal; -import com.microsoft.identity.common.java.authscheme.WebAppsPopAuthenticationSchemeInternal; import com.microsoft.identity.common.java.cache.ICacheRecord; import com.microsoft.identity.common.java.challengehandlers.PKeyAuthChallenge; import com.microsoft.identity.common.java.challengehandlers.PKeyAuthChallengeFactory; @@ -428,9 +427,6 @@ public MicrosoftStsTokenRequest createTokenRequest(@NonNull final MicrosoftStsAu } else if (authScheme instanceof PopAuthenticationSchemeWithClientKeyInternal) { tokenRequest.setTokenType(TokenRequest.TokenType.POP); tokenRequest.setRequestConfirmation(((PopAuthenticationSchemeWithClientKeyInternal) authScheme).getRequestConfirmation()); - } else if (authScheme instanceof WebAppsPopAuthenticationSchemeInternal) { - tokenRequest.setTokenType(TokenRequest.TokenType.POP); - tokenRequest.setRequestConfirmation(((WebAppsPopAuthenticationSchemeInternal) authScheme).getRequestConfirmation()); } return tokenRequest; @@ -480,9 +476,6 @@ public MicrosoftStsTokenRequest createRefreshTokenRequest(@NonNull final Abstrac } else if (authScheme instanceof PopAuthenticationSchemeWithClientKeyInternal) { request.setTokenType(TokenRequest.TokenType.POP); request.setRequestConfirmation(((PopAuthenticationSchemeWithClientKeyInternal) authScheme).getRequestConfirmation()); - } else if (authScheme instanceof WebAppsPopAuthenticationSchemeInternal) { - request.setTokenType(TokenRequest.TokenType.POP); - request.setRequestConfirmation(((WebAppsPopAuthenticationSchemeInternal) authScheme).getRequestConfirmation()); } return request; diff --git a/common4j/src/main/com/microsoft/identity/common/java/providers/oauth2/OpenIdProviderConfigurationClient.java b/common4j/src/main/com/microsoft/identity/common/java/providers/oauth2/OpenIdProviderConfigurationClient.java index ecc8daa8b7..252d59bcd6 100644 --- a/common4j/src/main/com/microsoft/identity/common/java/providers/oauth2/OpenIdProviderConfigurationClient.java +++ b/common4j/src/main/com/microsoft/identity/common/java/providers/oauth2/OpenIdProviderConfigurationClient.java @@ -226,8 +226,8 @@ Attributes validateIssuer( // 2. Known Microsoft Cloud issuer validation try { - AzureActiveDirectory.ensureCloudDiscoveryComplete(); final URL requestAuthorityUrl = new URL(requestAuthorityStr); + AzureActiveDirectory.ensureCloudDiscoveryForAuthority(requestAuthorityUrl); final AzureActiveDirectoryCloud requestCloud = AzureActiveDirectory.getAzureActiveDirectoryCloud(requestAuthorityUrl); if (requestCloud != null && requestCloud.isValidated()) { final AzureActiveDirectoryCloud issuerCloud = AzureActiveDirectory.getAzureActiveDirectoryCloud(issuerUrl); diff --git a/common4j/src/test/com/microsoft/identity/common/java/authorities/AuthorityKnownAuthorityTest.java b/common4j/src/test/com/microsoft/identity/common/java/authorities/AuthorityKnownAuthorityTest.java new file mode 100644 index 0000000000..e1abad3f42 --- /dev/null +++ b/common4j/src/test/com/microsoft/identity/common/java/authorities/AuthorityKnownAuthorityTest.java @@ -0,0 +1,91 @@ +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +package com.microsoft.identity.common.java.authorities; + +import com.microsoft.identity.common.java.providers.microsoft.azureactivedirectory.AzureActiveDirectoryCloud; + +import org.junit.Assert; +import org.junit.Test; + +/** + * Tests for {@link Authority#isKnownAuthority} and {@link Authority#getKnownAuthorityResult} + * focusing on sovereign cloud recognition via pre-seeded cloud metadata. + */ +public class AuthorityKnownAuthorityTest { + + /** + * Verifies that a sovereign cloud authority (Bleu) is recognized as known + * through the pre-seeded cloud metadata, without requiring a network call. + */ + @Test + public void testIsKnownAuthority_bleuSovereignCloud() { + final Authority authority = Authority.getAuthorityFromAuthorityUrl( + "https://" + AzureActiveDirectoryCloud.BLEU_CLOUD_HOST + "/common"); + Assert.assertTrue(Authority.isKnownAuthority(authority)); + } + + /** + * Verifies that a sovereign cloud authority (Delos) is recognized as known. + */ + @Test + public void testIsKnownAuthority_delosSovereignCloud() { + final Authority authority = Authority.getAuthorityFromAuthorityUrl( + "https://" + AzureActiveDirectoryCloud.DELOS_CLOUD_HOST + "/common"); + Assert.assertTrue(Authority.isKnownAuthority(authority)); + } + + /** + * Verifies that a sovereign cloud authority (SovSG) is recognized as known. + */ + @Test + public void testIsKnownAuthority_sovsgSovereignCloud() { + final Authority authority = Authority.getAuthorityFromAuthorityUrl( + "https://" + AzureActiveDirectoryCloud.SOVSG_CLOUD_HOST + "/common"); + Assert.assertTrue(Authority.isKnownAuthority(authority)); + } + + /** + * Verifies getKnownAuthorityResult returns known=true for a sovereign cloud + * authority. This exercises the showstopper fix: even if cloud discovery + * throws (no network), the pre-seeded metadata ensures the authority is + * still recognized as known. + */ + @Test + public void testGetKnownAuthorityResult_bleuSovereignCloud_isKnown() { + final Authority authority = Authority.getAuthorityFromAuthorityUrl( + "https://" + AzureActiveDirectoryCloud.BLEU_CLOUD_HOST + "/common"); + final Authority.KnownAuthorityResult result = Authority.getKnownAuthorityResult(authority); + Assert.assertTrue(result.getKnown()); + } + + /** + * A completely unknown authority should NOT be recognized as known + * (assumes no developer-configured knownAuthorities match). + */ + @Test + public void testIsKnownAuthority_unknownAuthority() { + final Authority authority = Authority.getAuthorityFromAuthorityUrl( + "https://login.unknown-test.example/common"); + Assert.assertFalse(Authority.isKnownAuthority(authority)); + } +} diff --git a/common4j/src/test/com/microsoft/identity/common/java/authorities/AzureActiveDirectoryAuthorityCloudTest.java b/common4j/src/test/com/microsoft/identity/common/java/authorities/AzureActiveDirectoryAuthorityCloudTest.java new file mode 100644 index 0000000000..e5fd4e2f4f --- /dev/null +++ b/common4j/src/test/com/microsoft/identity/common/java/authorities/AzureActiveDirectoryAuthorityCloudTest.java @@ -0,0 +1,83 @@ +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +package com.microsoft.identity.common.java.authorities; + +import com.microsoft.identity.common.java.providers.microsoft.azureactivedirectory.AzureActiveDirectoryCloud; + +import org.junit.Assert; +import org.junit.Test; + +/** + * Tests for {@link AzureActiveDirectoryAuthority#isSameCloudAsAuthority} with + * sovereign clouds. Since sovereign clouds are pre-seeded in the cloud metadata + * cache, these tests do not require network access. + */ +public class AzureActiveDirectoryAuthorityCloudTest { + + private static AzureActiveDirectoryAuthority createAuthority(final String host) { + return (AzureActiveDirectoryAuthority) Authority.getAuthorityFromAuthorityUrl( + "https://" + host + "/common"); + } + + @Test + public void testIsSameCloudAsAuthority_bothBleu_returnsTrue() throws Exception { + final AzureActiveDirectoryAuthority authority1 = createAuthority(AzureActiveDirectoryCloud.BLEU_CLOUD_HOST); + final AzureActiveDirectoryAuthority authority2 = createAuthority(AzureActiveDirectoryCloud.BLEU_CLOUD_HOST); + Assert.assertTrue(authority1.isSameCloudAsAuthority(authority2)); + } + + @Test + public void testIsSameCloudAsAuthority_bothDelos_returnsTrue() throws Exception { + final AzureActiveDirectoryAuthority authority1 = createAuthority(AzureActiveDirectoryCloud.DELOS_CLOUD_HOST); + final AzureActiveDirectoryAuthority authority2 = createAuthority(AzureActiveDirectoryCloud.DELOS_CLOUD_HOST); + Assert.assertTrue(authority1.isSameCloudAsAuthority(authority2)); + } + + @Test + public void testIsSameCloudAsAuthority_bothSovsg_returnsTrue() throws Exception { + final AzureActiveDirectoryAuthority authority1 = createAuthority(AzureActiveDirectoryCloud.SOVSG_CLOUD_HOST); + final AzureActiveDirectoryAuthority authority2 = createAuthority(AzureActiveDirectoryCloud.SOVSG_CLOUD_HOST); + Assert.assertTrue(authority1.isSameCloudAsAuthority(authority2)); + } + + @Test + public void testIsSameCloudAsAuthority_bleuVsDelos_returnsFalse() throws Exception { + final AzureActiveDirectoryAuthority bleu = createAuthority(AzureActiveDirectoryCloud.BLEU_CLOUD_HOST); + final AzureActiveDirectoryAuthority delos = createAuthority(AzureActiveDirectoryCloud.DELOS_CLOUD_HOST); + Assert.assertFalse(bleu.isSameCloudAsAuthority(delos)); + } + + @Test + public void testIsSameCloudAsAuthority_bleuVsSovsg_returnsFalse() throws Exception { + final AzureActiveDirectoryAuthority bleu = createAuthority(AzureActiveDirectoryCloud.BLEU_CLOUD_HOST); + final AzureActiveDirectoryAuthority sovsg = createAuthority(AzureActiveDirectoryCloud.SOVSG_CLOUD_HOST); + Assert.assertFalse(bleu.isSameCloudAsAuthority(sovsg)); + } + + @Test + public void testIsSameCloudAsAuthority_delosVsSovsg_returnsFalse() throws Exception { + final AzureActiveDirectoryAuthority delos = createAuthority(AzureActiveDirectoryCloud.DELOS_CLOUD_HOST); + final AzureActiveDirectoryAuthority sovsg = createAuthority(AzureActiveDirectoryCloud.SOVSG_CLOUD_HOST); + Assert.assertFalse(delos.isSameCloudAsAuthority(sovsg)); + } +} diff --git a/common4j/src/test/com/microsoft/identity/common/java/authscheme/WebAppsPopAuthenticationSchemeInternalTest.java b/common4j/src/test/com/microsoft/identity/common/java/authscheme/WebAppsPopAuthenticationSchemeInternalTest.java deleted file mode 100644 index 4b88421e4c..0000000000 --- a/common4j/src/test/com/microsoft/identity/common/java/authscheme/WebAppsPopAuthenticationSchemeInternalTest.java +++ /dev/null @@ -1,171 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// All rights reserved. -// -// This code is licensed under the MIT License. -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files(the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions : -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. -package com.microsoft.identity.common.java.authscheme; - -import com.microsoft.identity.common.java.base64.Base64Util; - -import org.junit.Assert; -import org.junit.Test; - -import java.util.HashMap; -import java.util.Map; - -public class WebAppsPopAuthenticationSchemeInternalTest { - - // Base64url-encoded req_cnf with kid: {"kid":"test-kid-1"} - private static final String REQ_CNF_ONE = Base64Util.encodeUrlSafeString("{\"kid\":\"test-kid-1\"}".getBytes()); - - // Base64url-encoded req_cnf with kid: {"kid":"test-kid-1"} (same as ONE) - private static final String REQ_CNF_ONE_CLONE = Base64Util.encodeUrlSafeString("{\"kid\":\"test-kid-1\"}".getBytes()); - - // Base64url-encoded req_cnf with kid: {"kid":"test-kid-2"} - private static final String REQ_CNF_TWO = Base64Util.encodeUrlSafeString("{\"kid\":\"test-kid-2\"}".getBytes()); - - private static final WebAppsPopAuthenticationSchemeInternal AUTHSCHEME_ONE = - new WebAppsPopAuthenticationSchemeInternal(REQ_CNF_ONE); - - private static final WebAppsPopAuthenticationSchemeInternal AUTHSCHEME_ONE_CLONE = - new WebAppsPopAuthenticationSchemeInternal(REQ_CNF_ONE_CLONE); - - private static final WebAppsPopAuthenticationSchemeInternal AUTHSCHEME_TWO = - new WebAppsPopAuthenticationSchemeInternal(REQ_CNF_TWO); - - @Test - public void testConstructor_validRequestConfirmation() { - final String reqCnf = Base64Util.encodeUrlSafeString("{\"kid\":\"my-key-id\"}".getBytes()); - final WebAppsPopAuthenticationSchemeInternal scheme = new WebAppsPopAuthenticationSchemeInternal(reqCnf); - - Assert.assertNotNull(scheme); - Assert.assertEquals(reqCnf, scheme.getRequestConfirmation()); - Assert.assertEquals("my-key-id", scheme.getKid()); - Assert.assertEquals(WebAppsPopAuthenticationSchemeInternal.SCHEME_POP_PREGENERATED, scheme.getName()); - } - - @Test(expected = IllegalArgumentException.class) - public void testConstructor_emptyRequestConfirmation() { - new WebAppsPopAuthenticationSchemeInternal(""); - } - - @Test - public void testConstructor_invalidBase64() { - // Invalid base64 should not throw, but kid will be null - final WebAppsPopAuthenticationSchemeInternal scheme = - new WebAppsPopAuthenticationSchemeInternal("not-valid-base64!@#$"); - - Assert.assertNotNull(scheme); - Assert.assertEquals("not-valid-base64!@#$", scheme.getRequestConfirmation()); - Assert.assertNull(scheme.getKid()); - } - - @Test - public void testConstructor_invalidJson() { - // Invalid JSON should not throw, but kid will be null - final String invalidJson = Base64Util.encodeUrlSafeString("{invalid json}".getBytes()); - final WebAppsPopAuthenticationSchemeInternal scheme = - new WebAppsPopAuthenticationSchemeInternal(invalidJson); - - Assert.assertNotNull(scheme); - Assert.assertEquals(invalidJson, scheme.getRequestConfirmation()); - Assert.assertNull(scheme.getKid()); - } - - @Test - public void testConstructor_reqCnfWithoutKid() { - final String reqCnfWithoutKid = Base64Util.encodeUrlSafeString("{\"other\":\"value\"}".getBytes()); - final WebAppsPopAuthenticationSchemeInternal scheme = new WebAppsPopAuthenticationSchemeInternal(reqCnfWithoutKid); - - Assert.assertNotNull(scheme); - Assert.assertEquals(reqCnfWithoutKid, scheme.getRequestConfirmation()); - Assert.assertNull(scheme.getKid()); - } - - @Test - public void testGetAccessTokenForScheme() { - final String accessToken = "test-access-token"; - final String result = AUTHSCHEME_ONE.getAccessTokenForScheme(accessToken); - - Assert.assertEquals("Access token should be returned as-is", accessToken, result); - } - - @Test - public void testMappability() { - Map testMap = new HashMap<>(); - - testMap.put(AUTHSCHEME_ONE, true); - Assert.assertEquals(1, testMap.size()); - testMap.put(AUTHSCHEME_TWO, true); - Assert.assertEquals(2, testMap.size()); - } - - @Test - public void testHashCode_equals() { - Assert.assertEquals(AUTHSCHEME_ONE.hashCode(), AUTHSCHEME_ONE_CLONE.hashCode()); - } - - @Test - public void testHashCode_notEquals() { - Assert.assertNotEquals(AUTHSCHEME_ONE.hashCode(), AUTHSCHEME_TWO.hashCode()); - } - - @Test - public void testEquals_equals() { - Assert.assertEquals(AUTHSCHEME_ONE, AUTHSCHEME_ONE_CLONE); - } - - @Test - public void testEquals_notEqualNull() { - Assert.assertNotEquals(AUTHSCHEME_ONE, null); - } - - @Test - public void testEquals_equalsSame() { - Assert.assertEquals(AUTHSCHEME_ONE, AUTHSCHEME_ONE); - } - - @Test - public void testEquals_notEqualDifferenceInKid() { - Assert.assertNotEquals(AUTHSCHEME_ONE, AUTHSCHEME_TWO); - } - - @Test - public void testEquals_notEqualDifferentType() { - Assert.assertNotEquals(AUTHSCHEME_ONE, new BearerAuthenticationSchemeInternal()); - } - - @Test - public void testSchemeName() { - Assert.assertEquals(WebAppsPopAuthenticationSchemeInternal.SCHEME_POP_PREGENERATED, - AUTHSCHEME_ONE.getName()); - } - - @Test - public void testBase64UrlDecoding() { - // Test with various req_cnf values including special characters - final String jsonWithSpecialChars = "{\"kid\":\"test+kid/with=special\"}"; - final String reqCnf = Base64Util.encodeUrlSafeString(jsonWithSpecialChars.getBytes()); - final WebAppsPopAuthenticationSchemeInternal scheme = new WebAppsPopAuthenticationSchemeInternal(reqCnf); - - Assert.assertNotNull(scheme); - Assert.assertEquals("test+kid/with=special", scheme.getKid()); - } -} diff --git a/common4j/src/test/com/microsoft/identity/common/java/providers/microsoft/azureactivedirectory/AzureActiveDirectoryCloudTest.java b/common4j/src/test/com/microsoft/identity/common/java/providers/microsoft/azureactivedirectory/AzureActiveDirectoryCloudTest.java new file mode 100644 index 0000000000..b08120d3d4 --- /dev/null +++ b/common4j/src/test/com/microsoft/identity/common/java/providers/microsoft/azureactivedirectory/AzureActiveDirectoryCloudTest.java @@ -0,0 +1,87 @@ +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +package com.microsoft.identity.common.java.providers.microsoft.azureactivedirectory; + +import org.junit.Assert; +import org.junit.Test; + +public class AzureActiveDirectoryCloudTest { + + @Test + public void testBleuCloudHostConstant() { + Assert.assertEquals("login.sovcloud-identity.fr", AzureActiveDirectoryCloud.BLEU_CLOUD_HOST); + } + + @Test + public void testDelosCloudHostConstant() { + Assert.assertEquals("login.sovcloud-identity.de", AzureActiveDirectoryCloud.DELOS_CLOUD_HOST); + } + + @Test + public void testSovsgCloudHostConstant() { + Assert.assertEquals("login.sovcloud-identity.sg", AzureActiveDirectoryCloud.SOVSG_CLOUD_HOST); + } + + @Test + public void testBleuCloudInstance() { + final AzureActiveDirectoryCloud bleu = AzureActiveDirectoryCloud.BLEU; + Assert.assertNotNull(bleu); + Assert.assertEquals(AzureActiveDirectoryCloud.BLEU_CLOUD_HOST, bleu.getPreferredNetworkHostName()); + Assert.assertEquals(AzureActiveDirectoryCloud.BLEU_CLOUD_HOST, bleu.getPreferredCacheHostName()); + Assert.assertNotNull(bleu.getHostAliases()); + Assert.assertEquals(1, bleu.getHostAliases().size()); + Assert.assertEquals(AzureActiveDirectoryCloud.BLEU_CLOUD_HOST, bleu.getHostAliases().get(0)); + Assert.assertTrue(bleu.isValidated()); + } + + @Test + public void testDelosCloudInstance() { + final AzureActiveDirectoryCloud delos = AzureActiveDirectoryCloud.DELOS; + Assert.assertNotNull(delos); + Assert.assertEquals(AzureActiveDirectoryCloud.DELOS_CLOUD_HOST, delos.getPreferredNetworkHostName()); + Assert.assertEquals(AzureActiveDirectoryCloud.DELOS_CLOUD_HOST, delos.getPreferredCacheHostName()); + Assert.assertNotNull(delos.getHostAliases()); + Assert.assertEquals(1, delos.getHostAliases().size()); + Assert.assertEquals(AzureActiveDirectoryCloud.DELOS_CLOUD_HOST, delos.getHostAliases().get(0)); + Assert.assertTrue(delos.isValidated()); + } + + @Test + public void testSovsgCloudInstance() { + final AzureActiveDirectoryCloud sovsg = AzureActiveDirectoryCloud.SOVSG; + Assert.assertNotNull(sovsg); + Assert.assertEquals(AzureActiveDirectoryCloud.SOVSG_CLOUD_HOST, sovsg.getPreferredNetworkHostName()); + Assert.assertEquals(AzureActiveDirectoryCloud.SOVSG_CLOUD_HOST, sovsg.getPreferredCacheHostName()); + Assert.assertNotNull(sovsg.getHostAliases()); + Assert.assertEquals(1, sovsg.getHostAliases().size()); + Assert.assertEquals(AzureActiveDirectoryCloud.SOVSG_CLOUD_HOST, sovsg.getHostAliases().get(0)); + Assert.assertTrue(sovsg.isValidated()); + } + + @Test + public void testSovereignCloudInstancesAreDistinct() { + Assert.assertNotEquals(AzureActiveDirectoryCloud.BLEU, AzureActiveDirectoryCloud.DELOS); + Assert.assertNotEquals(AzureActiveDirectoryCloud.BLEU, AzureActiveDirectoryCloud.SOVSG); + Assert.assertNotEquals(AzureActiveDirectoryCloud.DELOS, AzureActiveDirectoryCloud.SOVSG); + } +} diff --git a/common4j/src/test/com/microsoft/identity/common/java/providers/microsoft/azureactivedirectory/AzureActiveDirectoryTest.java b/common4j/src/test/com/microsoft/identity/common/java/providers/microsoft/azureactivedirectory/AzureActiveDirectoryTest.java new file mode 100644 index 0000000000..7564e5e28a --- /dev/null +++ b/common4j/src/test/com/microsoft/identity/common/java/providers/microsoft/azureactivedirectory/AzureActiveDirectoryTest.java @@ -0,0 +1,253 @@ +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +package com.microsoft.identity.common.java.providers.microsoft.azureactivedirectory; + +import com.microsoft.identity.common.java.authorities.Authority; +import com.microsoft.identity.common.java.authorities.Environment; + +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import java.net.URL; + +/** + * Tests for sovereign cloud discovery support in {@link AzureActiveDirectory}. + * Focuses on pre-seeding, isSovCloudDiscoveryHost, cache-based discovery gating, + * and environment changes. + */ +public class AzureActiveDirectoryTest { + + @Before + public void setup() { + // Reset to production environment before each test. + AzureActiveDirectory.setEnvironment(Environment.Production); + } + + @After + public void tearDown() { + // Ensure environment is restored. + AzureActiveDirectory.setEnvironment(Environment.Production); + } + + // --- isSovCloudDiscoveryHost tests --- + + @Test + public void testIsSovCloudDiscoveryHost_bleu() { + Assert.assertTrue(AzureActiveDirectory.isSovCloudDiscoveryHost("login.sovcloud-identity.fr")); + } + + @Test + public void testIsSovCloudDiscoveryHost_delos() { + Assert.assertTrue(AzureActiveDirectory.isSovCloudDiscoveryHost("login.sovcloud-identity.de")); + } + + @Test + public void testIsSovCloudDiscoveryHost_sovsg() { + Assert.assertTrue(AzureActiveDirectory.isSovCloudDiscoveryHost("login.sovcloud-identity.sg")); + } + + @Test + public void testIsSovCloudDiscoveryHost_caseInsensitive() { + Assert.assertTrue(AzureActiveDirectory.isSovCloudDiscoveryHost("LOGIN.SOVCLOUD-IDENTITY.FR")); + Assert.assertTrue(AzureActiveDirectory.isSovCloudDiscoveryHost("Login.SovCloud-Identity.De")); + } + + @Test + public void testIsSovCloudDiscoveryHost_nonSovCloud() { + Assert.assertFalse(AzureActiveDirectory.isSovCloudDiscoveryHost("login.microsoftonline.com")); + Assert.assertFalse(AzureActiveDirectory.isSovCloudDiscoveryHost("login.chinacloudapi.cn")); + Assert.assertFalse(AzureActiveDirectory.isSovCloudDiscoveryHost("example.com")); + } + + // --- Pre-seeding tests --- + + @Test + public void testSovereignCloudsPreSeededInCache() throws Exception { + // The static init block should have pre-seeded these hosts. + Assert.assertTrue(AzureActiveDirectory.hasCloudHost(new URL("https://login.sovcloud-identity.fr/common"))); + Assert.assertTrue(AzureActiveDirectory.hasCloudHost(new URL("https://login.sovcloud-identity.de/common"))); + Assert.assertTrue(AzureActiveDirectory.hasCloudHost(new URL("https://login.sovcloud-identity.sg/common"))); + } + + @Test + public void testPreSeededCloudMetadataIsCorrect() throws Exception { + final AzureActiveDirectoryCloud bleuCloud = AzureActiveDirectory.getAzureActiveDirectoryCloud( + new URL("https://login.sovcloud-identity.fr/common")); + Assert.assertNotNull(bleuCloud); + Assert.assertEquals("login.sovcloud-identity.fr", bleuCloud.getPreferredNetworkHostName()); + Assert.assertEquals("login.sovcloud-identity.fr", bleuCloud.getPreferredCacheHostName()); + } + + @Test + public void testPreSeededCloudsAreValidated() throws Exception { + Assert.assertTrue(AzureActiveDirectory.isValidCloudHost(new URL("https://login.sovcloud-identity.fr/common"))); + Assert.assertTrue(AzureActiveDirectory.isValidCloudHost(new URL("https://login.sovcloud-identity.de/common"))); + Assert.assertTrue(AzureActiveDirectory.isValidCloudHost(new URL("https://login.sovcloud-identity.sg/common"))); + } + + // --- getDefaultCloudUrl tests --- + + @Test + public void testGetDefaultCloudUrl_production() { + AzureActiveDirectory.setEnvironment(Environment.Production); + Assert.assertEquals(AzureActiveDirectoryEnvironment.PRODUCTION_CLOUD_URL, AzureActiveDirectory.getDefaultCloudUrl()); + } + + @Test + public void testGetDefaultCloudUrl_preProduction() { + AzureActiveDirectory.setEnvironment(Environment.PreProduction); + Assert.assertEquals(AzureActiveDirectoryEnvironment.PREPRODUCTION_CLOUD_URL, AzureActiveDirectory.getDefaultCloudUrl()); + } + + // --- hasCloudHost / isValidCloudHost tests --- + + @Test + public void testHasCloudHost_unknownHost() throws Exception { + Assert.assertFalse(AzureActiveDirectory.hasCloudHost(new URL("https://login.example.com/common"))); + } + + @Test + public void testPutCloud_makesHostAvailable() throws Exception { + final String host = "login.testcloud.com"; + final AzureActiveDirectoryCloud cloud = new AzureActiveDirectoryCloud(host, host); + AzureActiveDirectory.putCloud(host, cloud); + + Assert.assertTrue(AzureActiveDirectory.hasCloudHost(new URL("https://" + host + "/common"))); + Assert.assertTrue(AzureActiveDirectory.isValidCloudHost(new URL("https://" + host + "/common"))); + } + + @Test + public void testIsValidCloudHost_unvalidatedCloud() throws Exception { + final String host = "login.unvalidated.com"; + AzureActiveDirectory.putCloud(host, new AzureActiveDirectoryCloud(false)); + + Assert.assertTrue(AzureActiveDirectory.hasCloudHost(new URL("https://" + host + "/common"))); + Assert.assertFalse(AzureActiveDirectory.isValidCloudHost(new URL("https://" + host + "/common"))); + } + + // --- ensureCloudDiscoveryForAuthority caching behavior --- + + @Test + public void testEnsureCloudDiscoveryForAuthority_sovCloudAlreadyCached_noOp() throws Exception { + // Sovereign clouds are pre-seeded, so calling ensure should be a no-op (no network). + // This verifies it doesn't throw and the cloud remains accessible. + final URL sovUrl = new URL("https://login.sovcloud-identity.fr/common"); + AzureActiveDirectory.ensureCloudDiscoveryForAuthority(sovUrl); + + Assert.assertTrue(AzureActiveDirectory.hasCloudHost(sovUrl)); + Assert.assertTrue(AzureActiveDirectory.isValidCloudHost(sovUrl)); + } + + @Test + public void testEnsureCloudDiscoveryForAuthority_nullUrl_doesNotThrow() throws Exception { + // Passing null URL should fall back to ensureCloudDiscoveryComplete (which calls global). + // In test without network, this may throw — but we verify it doesn't NPE. + try { + AzureActiveDirectory.ensureCloudDiscoveryForAuthority((URL) null); + } catch (final Exception e) { + // Expected in test environment without network — just verify no NPE. + Assert.assertFalse("Should not be NPE", e instanceof NullPointerException); + } + } + + @Test + public void testEnsureCloudDiscoveryForAuthority_unknownHostAfterGlobalCached_noOp() throws Exception { + // Simulate global discovery already cached by putting the default host. + final String defaultHost = "login.microsoftonline.com"; + AzureActiveDirectory.putCloud(defaultHost, new AzureActiveDirectoryCloud(defaultHost, defaultHost)); + + // Now calling with an unknown host should be a no-op (global already cached). + final URL unknownUrl = new URL("https://login.example.com/common"); + AzureActiveDirectory.ensureCloudDiscoveryForAuthority(unknownUrl); + + // The unknown host shouldn't be added to cache. + Assert.assertFalse(AzureActiveDirectory.hasCloudHost(unknownUrl)); + // But global is still there. + Assert.assertTrue(AzureActiveDirectory.hasCloudHost(new URL("https://" + defaultHost + "/common"))); + } + + // --- getAzureActiveDirectoryCloudFromHostName tests --- + + @Test + public void testGetAzureActiveDirectoryCloudFromHostName_sovCloud() { + final AzureActiveDirectoryCloud cloud = AzureActiveDirectory.getAzureActiveDirectoryCloudFromHostName( + "login.sovcloud-identity.fr"); + Assert.assertNotNull(cloud); + Assert.assertEquals("login.sovcloud-identity.fr", cloud.getPreferredNetworkHostName()); + } + + @Test + public void testGetAzureActiveDirectoryCloudFromHostName_caseInsensitive() { + final AzureActiveDirectoryCloud cloud = AzureActiveDirectory.getAzureActiveDirectoryCloudFromHostName( + "LOGIN.SOVCLOUD-IDENTITY.DE"); + Assert.assertNotNull(cloud); + Assert.assertEquals("login.sovcloud-identity.de", cloud.getPreferredNetworkHostName()); + } + + @Test + public void testGetAzureActiveDirectoryCloudFromHostName_unknown() { + final AzureActiveDirectoryCloud cloud = AzureActiveDirectory.getAzureActiveDirectoryCloudFromHostName( + "login.unknown.com"); + Assert.assertNull(cloud); + } + + // --- ensureCloudDiscoveryForAuthority(Authority) overload tests --- + + @Test + public void testEnsureCloudDiscoveryForAuthority_nullAuthority_doesNotThrow() throws Exception { + // Passing null Authority should fall back to global discovery. + // May throw due to no network, but must not NPE. + try { + AzureActiveDirectory.ensureCloudDiscoveryForAuthority((Authority) null); + } catch (final Exception e) { + Assert.assertFalse("Should not be NPE", e instanceof NullPointerException); + } + } + + @Test + public void testEnsureCloudDiscoveryForAuthority_sovCloudAuthority_isNoOp() throws Exception { + // Sovereign cloud authority should be already cached (pre-seeded), so no network call. + final Authority authority = Authority.getAuthorityFromAuthorityUrl( + "https://" + AzureActiveDirectoryCloud.BLEU_CLOUD_HOST + "/common"); + AzureActiveDirectory.ensureCloudDiscoveryForAuthority(authority); + Assert.assertTrue(AzureActiveDirectory.hasCloudHost( + new URL("https://" + AzureActiveDirectoryCloud.BLEU_CLOUD_HOST + "/common"))); + } + + // --- Environment switching with sovereign clouds --- + + @Test + public void testSovCloudsRemainAfterEnvironmentSwitch() throws Exception { + // Sovereign clouds are pre-seeded in the static init block. + // Switching environment should NOT remove them from the cache. + AzureActiveDirectory.setEnvironment(Environment.PreProduction); + Assert.assertTrue(AzureActiveDirectory.hasCloudHost( + new URL("https://login.sovcloud-identity.fr/common"))); + Assert.assertTrue(AzureActiveDirectory.hasCloudHost( + new URL("https://login.sovcloud-identity.de/common"))); + Assert.assertTrue(AzureActiveDirectory.hasCloudHost( + new URL("https://login.sovcloud-identity.sg/common"))); + } +}