diff --git a/src/client/Microsoft.Identity.Client/Instance/Oidc/OidcRetrieverWithCache.cs b/src/client/Microsoft.Identity.Client/Instance/Oidc/OidcRetrieverWithCache.cs index 2e774dcddf..9c93866c69 100644 --- a/src/client/Microsoft.Identity.Client/Instance/Oidc/OidcRetrieverWithCache.cs +++ b/src/client/Microsoft.Identity.Client/Instance/Oidc/OidcRetrieverWithCache.cs @@ -87,13 +87,17 @@ private static void ValidateIssuer(Uri authority, string issuer) string normalizedAuthority = authority.AbsoluteUri.TrimEnd('/'); string normalizedIssuer = issuer?.TrimEnd('/'); - // Primary validation: check if normalized authority starts with normalized issuer (case-insensitive comparison) - if (normalizedAuthority.StartsWith(normalizedIssuer, StringComparison.OrdinalIgnoreCase)) + // OIDC validation: if the issuer's scheme and host match the authority's, consider it valid + if (!string.IsNullOrEmpty(issuer) && Uri.TryCreate(issuer, UriKind.Absolute, out Uri issuerUri)) { - return; + if (string.Equals(authority.Scheme, issuerUri.Scheme, StringComparison.OrdinalIgnoreCase) && + string.Equals(authority.Host, issuerUri.Host, StringComparison.OrdinalIgnoreCase)) + { + return; + } } - // Extract tenant for CIAM scenarios. In a CIAM scenario the issuer is expected to have "{tenant}.ciamlogin.com" + // CIAM-specific validation: In a CIAM scenario the issuer is expected to have "{tenant}.ciamlogin.com" // as the host, even when using a custom domain. string tenant = null; try diff --git a/tests/Microsoft.Identity.Test.Unit/CoreTests/InstanceTests/GenericAuthorityTests.cs b/tests/Microsoft.Identity.Test.Unit/CoreTests/InstanceTests/GenericAuthorityTests.cs index 9f138860d4..e9b91667e0 100644 --- a/tests/Microsoft.Identity.Test.Unit/CoreTests/InstanceTests/GenericAuthorityTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/CoreTests/InstanceTests/GenericAuthorityTests.cs @@ -333,6 +333,56 @@ public async Task BadOidcResponse_ThrowsException_Async(string badOidcResponseTy } } + [TestMethod] + public async Task OidcIssuerValidation_AcceptsDifferentPath_Async() + { + using (var httpManager = new MockHttpManager()) + { + // This test was made to cover an issue that realistically would only happen with Microsoft authorities in multi-tenant scenarios, + // so it uses a Microsoft host instead of the custom domain used in other tests. + string microsoftHost = "login.microsoftonline.com"; + string authority = $"https://{microsoftHost}/organizations/2.0/"; + string issuerWithDifferentPath = $"https://{microsoftHost}/someTenant/2.0/"; + + IConfidentialClientApplication app = ConfidentialClientApplicationBuilder + .Create(TestConstants.ClientId) + .WithHttpManager(httpManager) + .WithOidcAuthority(authority) + .WithClientSecret(TestConstants.ClientSecret) + .Build(); + + // Create OIDC document with a Microsoft host and an issuer that has matching host but different path + string oidcDocumentWithDifferentPath = TestConstants.GenericOidcResponse.Replace( + $"\"issuer\":\"{TestConstants.GenericAuthority}\"", + $"\"issuer\":\"{issuerWithDifferentPath}\""); + oidcDocumentWithDifferentPath = oidcDocumentWithDifferentPath.Replace( + "demo.duendesoftware.com", + microsoftHost); + + // Mock OIDC endpoint response + httpManager.AddMockHandler(new MockHttpMessageHandler + { + ExpectedMethod = HttpMethod.Get, + ExpectedUrl = $"{authority}{Constants.WellKnownOpenIdConfigurationPath}", + ResponseMessage = MockHelpers.CreateSuccessResponseMessage(oidcDocumentWithDifferentPath) + }); + + // Mock token endpoint response + httpManager.AddMockHandler( + CreateTokenResponseHttpHandler( + $"https://{microsoftHost}/connect/token", + scopesInRequest: "api", + scopesInResponse: "api", + grant: "client_credentials")); + + // Should not throw an exception with our updated validation + var result = await app.AcquireTokenForClient(new[] { "api" }).ExecuteAsync().ConfigureAwait(false); + + Assert.IsNotNull(result); + Assert.IsNotNull(result.AccessToken); + } + } + [TestMethod] public async Task OidcIssuerValidation_ThrowsForNonMatchingIssuer_Async() {