diff --git a/src/client/Microsoft.Identity.Client/ApiConfig/Parameters/AcquireTokenCommonParameters.cs b/src/client/Microsoft.Identity.Client/ApiConfig/Parameters/AcquireTokenCommonParameters.cs index 15a0a887f1..c8415871ee 100644 --- a/src/client/Microsoft.Identity.Client/ApiConfig/Parameters/AcquireTokenCommonParameters.cs +++ b/src/client/Microsoft.Identity.Client/ApiConfig/Parameters/AcquireTokenCommonParameters.cs @@ -32,5 +32,6 @@ internal class AcquireTokenCommonParameters public List AdditionalCacheParameters { get; set; } public SortedList CacheKeyComponents { get; internal set; } public string FmiPathSuffix { get; internal set; } + public string ClientAssertionFmiPath { get; internal set; } } } diff --git a/src/client/Microsoft.Identity.Client/AppConfig/AssertionRequestOptions.cs b/src/client/Microsoft.Identity.Client/AppConfig/AssertionRequestOptions.cs index 6e7bf17853..388fa77bf9 100644 --- a/src/client/Microsoft.Identity.Client/AppConfig/AssertionRequestOptions.cs +++ b/src/client/Microsoft.Identity.Client/AppConfig/AssertionRequestOptions.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Threading; + namespace Microsoft.Identity.Client { /// @@ -38,5 +39,10 @@ public class AssertionRequestOptions { /// (e.g. ManagedIdentityApplication or ConfidentialClientApplication), the same capabilities should be used there. /// public IEnumerable ClientCapabilities { get; set; } + + /// + /// FMI path to be used for client assertion. Tokens are assocaited with this path in the cache. + /// + public string ClientAssertionFmiPath { get; set; } } } diff --git a/src/client/Microsoft.Identity.Client/Extensibility/AbstractConfidentialClientAcquireTokenParameterBuilderExtension.cs b/src/client/Microsoft.Identity.Client/Extensibility/AbstractConfidentialClientAcquireTokenParameterBuilderExtension.cs index edf7ac6737..33cd5ebaf7 100644 --- a/src/client/Microsoft.Identity.Client/Extensibility/AbstractConfidentialClientAcquireTokenParameterBuilderExtension.cs +++ b/src/client/Microsoft.Identity.Client/Extensibility/AbstractConfidentialClientAcquireTokenParameterBuilderExtension.cs @@ -6,6 +6,7 @@ using System.Linq; using System.Runtime.CompilerServices; using System.Threading.Tasks; +using Microsoft.Identity.Client.OAuth2; namespace Microsoft.Identity.Client.Extensibility { @@ -121,5 +122,79 @@ public static AbstractAcquireTokenParameterBuilder WithAdditionalCacheParamet } return builder; } + + /// + /// Specifies additional cache key components to use when caching and retrieving tokens. + /// + /// The list of additional cache key components. + /// + /// The builder. + /// + /// + /// This api can be used to associate certificate key identifiers along with other keys with a particular token. + /// In order for the tokens to be successfully retrieved from the cache, all components used to cache the token must be provided. + /// + /// + internal static AbstractAcquireTokenParameterBuilder WithAdditionalCacheKeyComponents( + this AbstractAcquireTokenParameterBuilder builder, + IDictionary cacheKeyComponents) + where T : AbstractAcquireTokenParameterBuilder + { + if (cacheKeyComponents == null || cacheKeyComponents.Count == 0) + { + //no-op + return builder; + } + + if (builder.CommonParameters.CacheKeyComponents == null) + { + builder.CommonParameters.CacheKeyComponents = new SortedList(cacheKeyComponents); + } + else + { + foreach (var kvp in cacheKeyComponents) + { + // Key conflicts are not allowed, it is expected for this method to fail. + builder.CommonParameters.CacheKeyComponents.Add(kvp.Key, kvp.Value); + } + } + + return builder; + } + + /// + /// Specifies an FMI path to be used for the client assertion. This lets higher level APIs like Id.Web + /// provide credentials which are FMI sensitive. + /// Important: tokens are associated with the credential FMI path, which impacts cache lookups + /// This is an extensibility API and should not be used by applications. + /// + /// The builder. + /// The FMI path to use for client assertion. + /// The builder to chain the .With methods + /// Thrown when fmiPath is null or whitespace. + public static AbstractAcquireTokenParameterBuilder WithFmiPathForClientAssertion( + this AbstractAcquireTokenParameterBuilder builder, + string fmiPath) + where T : AbstractAcquireTokenParameterBuilder + { + builder.ValidateUseOfExperimentalFeature(); + + if (string.IsNullOrWhiteSpace(fmiPath)) + { + throw new ArgumentNullException(nameof(fmiPath)); + } + + builder.CommonParameters.ClientAssertionFmiPath = fmiPath; + + // Add the fmi_path to the cache key so that it is used for cache lookups + var cacheKey = new SortedList + { + { "credential_fmi_path", fmiPath } + }; + + WithAdditionalCacheKeyComponents(builder, cacheKey); + + return builder; + } } } diff --git a/src/client/Microsoft.Identity.Client/Extensibility/AcquireTokenForClientBuilderExtensions.cs b/src/client/Microsoft.Identity.Client/Extensibility/AcquireTokenForClientBuilderExtensions.cs index 83f748d0c0..56dcc61aab 100644 --- a/src/client/Microsoft.Identity.Client/Extensibility/AcquireTokenForClientBuilderExtensions.cs +++ b/src/client/Microsoft.Identity.Client/Extensibility/AcquireTokenForClientBuilderExtensions.cs @@ -7,6 +7,7 @@ using System.Diagnostics.CodeAnalysis; using System.Text; using Microsoft.Identity.Client.Cache; +using Microsoft.Identity.Client.OAuth2; namespace Microsoft.Identity.Client.Extensibility { @@ -15,42 +16,6 @@ namespace Microsoft.Identity.Client.Extensibility /// public static class AcquireTokenForClientBuilderExtensions { - /// - /// Specifies additional cache key components to use when caching and retrieving tokens. - /// - /// The list of additional cache key components. - /// - /// The builder. - /// - /// - /// This api can be used to associate certificate key identifiers along with other keys with a particular token. - /// In order for the tokens to be successfully retrieved from the cache, all components used to cache the token must be provided. - /// - /// - internal static AcquireTokenForClientParameterBuilder WithAdditionalCacheKeyComponents(this AcquireTokenForClientParameterBuilder builder, - IDictionary cacheKeyComponents) - { - if (cacheKeyComponents == null || cacheKeyComponents.Count == 0) - { - //no-op - return builder; - } - - if (builder.CommonParameters.CacheKeyComponents == null) - { - builder.CommonParameters.CacheKeyComponents = new SortedList(cacheKeyComponents); - } - else - { - foreach (var kvp in cacheKeyComponents) - { - builder.CommonParameters.CacheKeyComponents.Add(kvp.Key, kvp.Value); - } - } - - return builder; - } - /// /// Binds the token to a key in the cache. L2 cache keys contain the key id. /// No cryptographic operations is performed on the token. diff --git a/src/client/Microsoft.Identity.Client/Extensibility/AcquireTokenOnBehalfOfParameterBuilderExtensions.cs b/src/client/Microsoft.Identity.Client/Extensibility/AcquireTokenOnBehalfOfParameterBuilderExtensions.cs index d8d742d62a..1aa60f5ade 100644 --- a/src/client/Microsoft.Identity.Client/Extensibility/AcquireTokenOnBehalfOfParameterBuilderExtensions.cs +++ b/src/client/Microsoft.Identity.Client/Extensibility/AcquireTokenOnBehalfOfParameterBuilderExtensions.cs @@ -1,8 +1,10 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +using System; using System.Collections.Generic; using System.ComponentModel; +using Microsoft.Identity.Client.OAuth2; namespace Microsoft.Identity.Client.Extensibility { diff --git a/src/client/Microsoft.Identity.Client/Internal/ClientCredential/SignedAssertionDelegateClientCredential.cs b/src/client/Microsoft.Identity.Client/Internal/ClientCredential/SignedAssertionDelegateClientCredential.cs index b4031a223d..d1dd0e3d9b 100644 --- a/src/client/Microsoft.Identity.Client/Internal/ClientCredential/SignedAssertionDelegateClientCredential.cs +++ b/src/client/Microsoft.Identity.Client/Internal/ClientCredential/SignedAssertionDelegateClientCredential.cs @@ -66,6 +66,9 @@ public async Task AddConfidentialClientParametersAsync( //Set claims assertionOptions.Claims = requestParameters.Claims; + + //Set client assertion FMI path + assertionOptions.ClientAssertionFmiPath = requestParameters.ClientAssertionFmiPath; // Delegate that uses AssertionRequestOptions string signedAssertion = await _signedAssertionWithInfoDelegate(assertionOptions).ConfigureAwait(false); diff --git a/src/client/Microsoft.Identity.Client/Internal/Requests/AuthenticationRequestParameters.cs b/src/client/Microsoft.Identity.Client/Internal/Requests/AuthenticationRequestParameters.cs index 1bf0338394..5547605959 100644 --- a/src/client/Microsoft.Identity.Client/Internal/Requests/AuthenticationRequestParameters.cs +++ b/src/client/Microsoft.Identity.Client/Internal/Requests/AuthenticationRequestParameters.cs @@ -177,6 +177,8 @@ public string LoginHint public KeyValuePair? CcsRoutingHint { get; set; } public string FmiPathSuffix => _commonParameters.FmiPathSuffix; + + public string ClientAssertionFmiPath => _commonParameters.ClientAssertionFmiPath; #endregion public void LogParameters() @@ -206,6 +208,8 @@ public void LogParameters() builder.AppendLine("UserAssertion set: " + (UserAssertion != null)); builder.AppendLine("LongRunningOboCacheKey set: " + !string.IsNullOrWhiteSpace(LongRunningOboCacheKey)); builder.AppendLine("Region configured: " + AppConfig.AzureRegion); + builder.AppendLine("FMI Path: " + FmiPathSuffix); + builder.AppendLine("Credential FMI Path: " + ClientAssertionFmiPath); string messageWithPii = builder.ToString(); @@ -226,6 +230,8 @@ public void LogParameters() builder.AppendLine("UserAssertion set: " + (UserAssertion != null)); builder.AppendLine("LongRunningOboCacheKey set: " + !string.IsNullOrWhiteSpace(LongRunningOboCacheKey)); builder.AppendLine("Region configured: " + AppConfig.AzureRegion); + builder.AppendLine("FMI Path: " + FmiPathSuffix); + builder.AppendLine("Credential FMI Path: " + ClientAssertionFmiPath); logger.InfoPii(messageWithPii, builder.ToString()); } diff --git a/src/client/Microsoft.Identity.Client/PublicApi/net462/PublicAPI.Unshipped.txt b/src/client/Microsoft.Identity.Client/PublicApi/net462/PublicAPI.Unshipped.txt index 9b7aaf512c..e821cb00cf 100644 --- a/src/client/Microsoft.Identity.Client/PublicApi/net462/PublicAPI.Unshipped.txt +++ b/src/client/Microsoft.Identity.Client/PublicApi/net462/PublicAPI.Unshipped.txt @@ -1,4 +1,5 @@ Microsoft.Identity.Client.Utils.MacMainThreadScheduler +static Microsoft.Identity.Client.Extensibility.AbstractConfidentialClientAcquireTokenParameterBuilderExtension.WithFmiPathForClientAssertion(this Microsoft.Identity.Client.AbstractAcquireTokenParameterBuilder builder, string fmiPath) -> Microsoft.Identity.Client.AbstractAcquireTokenParameterBuilder static Microsoft.Identity.Client.Utils.MacMainThreadScheduler.Instance() -> Microsoft.Identity.Client.Utils.MacMainThreadScheduler Microsoft.Identity.Client.Utils.MacMainThreadScheduler.IsCurrentlyOnMainThread() -> bool Microsoft.Identity.Client.Utils.MacMainThreadScheduler.IsRunning() -> bool @@ -6,4 +7,6 @@ Microsoft.Identity.Client.Utils.MacMainThreadScheduler.Stop() -> void Microsoft.Identity.Client.Utils.MacMainThreadScheduler.RunOnMainThreadAsync(System.Func asyncAction) -> System.Threading.Tasks.Task Microsoft.Identity.Client.Utils.MacMainThreadScheduler.StartMessageLoop() -> void Microsoft.Identity.Client.AuthenticationResultMetadata.CachedAccessTokenCount.get -> int -Microsoft.Identity.Client.AuthenticationResultMetadata.CachedAccessTokenCount.set -> void \ No newline at end of file +Microsoft.Identity.Client.AuthenticationResultMetadata.CachedAccessTokenCount.set -> void +Microsoft.Identity.Client.AssertionRequestOptions.ClientAssertionFmiPath.get -> string +Microsoft.Identity.Client.AssertionRequestOptions.ClientAssertionFmiPath.set -> void diff --git a/src/client/Microsoft.Identity.Client/PublicApi/net472/PublicAPI.Unshipped.txt b/src/client/Microsoft.Identity.Client/PublicApi/net472/PublicAPI.Unshipped.txt index 9b7aaf512c..e821cb00cf 100644 --- a/src/client/Microsoft.Identity.Client/PublicApi/net472/PublicAPI.Unshipped.txt +++ b/src/client/Microsoft.Identity.Client/PublicApi/net472/PublicAPI.Unshipped.txt @@ -1,4 +1,5 @@ Microsoft.Identity.Client.Utils.MacMainThreadScheduler +static Microsoft.Identity.Client.Extensibility.AbstractConfidentialClientAcquireTokenParameterBuilderExtension.WithFmiPathForClientAssertion(this Microsoft.Identity.Client.AbstractAcquireTokenParameterBuilder builder, string fmiPath) -> Microsoft.Identity.Client.AbstractAcquireTokenParameterBuilder static Microsoft.Identity.Client.Utils.MacMainThreadScheduler.Instance() -> Microsoft.Identity.Client.Utils.MacMainThreadScheduler Microsoft.Identity.Client.Utils.MacMainThreadScheduler.IsCurrentlyOnMainThread() -> bool Microsoft.Identity.Client.Utils.MacMainThreadScheduler.IsRunning() -> bool @@ -6,4 +7,6 @@ Microsoft.Identity.Client.Utils.MacMainThreadScheduler.Stop() -> void Microsoft.Identity.Client.Utils.MacMainThreadScheduler.RunOnMainThreadAsync(System.Func asyncAction) -> System.Threading.Tasks.Task Microsoft.Identity.Client.Utils.MacMainThreadScheduler.StartMessageLoop() -> void Microsoft.Identity.Client.AuthenticationResultMetadata.CachedAccessTokenCount.get -> int -Microsoft.Identity.Client.AuthenticationResultMetadata.CachedAccessTokenCount.set -> void \ No newline at end of file +Microsoft.Identity.Client.AuthenticationResultMetadata.CachedAccessTokenCount.set -> void +Microsoft.Identity.Client.AssertionRequestOptions.ClientAssertionFmiPath.get -> string +Microsoft.Identity.Client.AssertionRequestOptions.ClientAssertionFmiPath.set -> void diff --git a/src/client/Microsoft.Identity.Client/PublicApi/net8.0-android/PublicAPI.Unshipped.txt b/src/client/Microsoft.Identity.Client/PublicApi/net8.0-android/PublicAPI.Unshipped.txt index 9b7aaf512c..e821cb00cf 100644 --- a/src/client/Microsoft.Identity.Client/PublicApi/net8.0-android/PublicAPI.Unshipped.txt +++ b/src/client/Microsoft.Identity.Client/PublicApi/net8.0-android/PublicAPI.Unshipped.txt @@ -1,4 +1,5 @@ Microsoft.Identity.Client.Utils.MacMainThreadScheduler +static Microsoft.Identity.Client.Extensibility.AbstractConfidentialClientAcquireTokenParameterBuilderExtension.WithFmiPathForClientAssertion(this Microsoft.Identity.Client.AbstractAcquireTokenParameterBuilder builder, string fmiPath) -> Microsoft.Identity.Client.AbstractAcquireTokenParameterBuilder static Microsoft.Identity.Client.Utils.MacMainThreadScheduler.Instance() -> Microsoft.Identity.Client.Utils.MacMainThreadScheduler Microsoft.Identity.Client.Utils.MacMainThreadScheduler.IsCurrentlyOnMainThread() -> bool Microsoft.Identity.Client.Utils.MacMainThreadScheduler.IsRunning() -> bool @@ -6,4 +7,6 @@ Microsoft.Identity.Client.Utils.MacMainThreadScheduler.Stop() -> void Microsoft.Identity.Client.Utils.MacMainThreadScheduler.RunOnMainThreadAsync(System.Func asyncAction) -> System.Threading.Tasks.Task Microsoft.Identity.Client.Utils.MacMainThreadScheduler.StartMessageLoop() -> void Microsoft.Identity.Client.AuthenticationResultMetadata.CachedAccessTokenCount.get -> int -Microsoft.Identity.Client.AuthenticationResultMetadata.CachedAccessTokenCount.set -> void \ No newline at end of file +Microsoft.Identity.Client.AuthenticationResultMetadata.CachedAccessTokenCount.set -> void +Microsoft.Identity.Client.AssertionRequestOptions.ClientAssertionFmiPath.get -> string +Microsoft.Identity.Client.AssertionRequestOptions.ClientAssertionFmiPath.set -> void diff --git a/src/client/Microsoft.Identity.Client/PublicApi/net8.0-ios/PublicAPI.Unshipped.txt b/src/client/Microsoft.Identity.Client/PublicApi/net8.0-ios/PublicAPI.Unshipped.txt index 9b7aaf512c..e821cb00cf 100644 --- a/src/client/Microsoft.Identity.Client/PublicApi/net8.0-ios/PublicAPI.Unshipped.txt +++ b/src/client/Microsoft.Identity.Client/PublicApi/net8.0-ios/PublicAPI.Unshipped.txt @@ -1,4 +1,5 @@ Microsoft.Identity.Client.Utils.MacMainThreadScheduler +static Microsoft.Identity.Client.Extensibility.AbstractConfidentialClientAcquireTokenParameterBuilderExtension.WithFmiPathForClientAssertion(this Microsoft.Identity.Client.AbstractAcquireTokenParameterBuilder builder, string fmiPath) -> Microsoft.Identity.Client.AbstractAcquireTokenParameterBuilder static Microsoft.Identity.Client.Utils.MacMainThreadScheduler.Instance() -> Microsoft.Identity.Client.Utils.MacMainThreadScheduler Microsoft.Identity.Client.Utils.MacMainThreadScheduler.IsCurrentlyOnMainThread() -> bool Microsoft.Identity.Client.Utils.MacMainThreadScheduler.IsRunning() -> bool @@ -6,4 +7,6 @@ Microsoft.Identity.Client.Utils.MacMainThreadScheduler.Stop() -> void Microsoft.Identity.Client.Utils.MacMainThreadScheduler.RunOnMainThreadAsync(System.Func asyncAction) -> System.Threading.Tasks.Task Microsoft.Identity.Client.Utils.MacMainThreadScheduler.StartMessageLoop() -> void Microsoft.Identity.Client.AuthenticationResultMetadata.CachedAccessTokenCount.get -> int -Microsoft.Identity.Client.AuthenticationResultMetadata.CachedAccessTokenCount.set -> void \ No newline at end of file +Microsoft.Identity.Client.AuthenticationResultMetadata.CachedAccessTokenCount.set -> void +Microsoft.Identity.Client.AssertionRequestOptions.ClientAssertionFmiPath.get -> string +Microsoft.Identity.Client.AssertionRequestOptions.ClientAssertionFmiPath.set -> void diff --git a/src/client/Microsoft.Identity.Client/PublicApi/net8.0/PublicAPI.Unshipped.txt b/src/client/Microsoft.Identity.Client/PublicApi/net8.0/PublicAPI.Unshipped.txt index 9b7aaf512c..e821cb00cf 100644 --- a/src/client/Microsoft.Identity.Client/PublicApi/net8.0/PublicAPI.Unshipped.txt +++ b/src/client/Microsoft.Identity.Client/PublicApi/net8.0/PublicAPI.Unshipped.txt @@ -1,4 +1,5 @@ Microsoft.Identity.Client.Utils.MacMainThreadScheduler +static Microsoft.Identity.Client.Extensibility.AbstractConfidentialClientAcquireTokenParameterBuilderExtension.WithFmiPathForClientAssertion(this Microsoft.Identity.Client.AbstractAcquireTokenParameterBuilder builder, string fmiPath) -> Microsoft.Identity.Client.AbstractAcquireTokenParameterBuilder static Microsoft.Identity.Client.Utils.MacMainThreadScheduler.Instance() -> Microsoft.Identity.Client.Utils.MacMainThreadScheduler Microsoft.Identity.Client.Utils.MacMainThreadScheduler.IsCurrentlyOnMainThread() -> bool Microsoft.Identity.Client.Utils.MacMainThreadScheduler.IsRunning() -> bool @@ -6,4 +7,6 @@ Microsoft.Identity.Client.Utils.MacMainThreadScheduler.Stop() -> void Microsoft.Identity.Client.Utils.MacMainThreadScheduler.RunOnMainThreadAsync(System.Func asyncAction) -> System.Threading.Tasks.Task Microsoft.Identity.Client.Utils.MacMainThreadScheduler.StartMessageLoop() -> void Microsoft.Identity.Client.AuthenticationResultMetadata.CachedAccessTokenCount.get -> int -Microsoft.Identity.Client.AuthenticationResultMetadata.CachedAccessTokenCount.set -> void \ No newline at end of file +Microsoft.Identity.Client.AuthenticationResultMetadata.CachedAccessTokenCount.set -> void +Microsoft.Identity.Client.AssertionRequestOptions.ClientAssertionFmiPath.get -> string +Microsoft.Identity.Client.AssertionRequestOptions.ClientAssertionFmiPath.set -> void diff --git a/src/client/Microsoft.Identity.Client/PublicApi/netstandard2.0/PublicAPI.Unshipped.txt b/src/client/Microsoft.Identity.Client/PublicApi/netstandard2.0/PublicAPI.Unshipped.txt index 9b7aaf512c..e821cb00cf 100644 --- a/src/client/Microsoft.Identity.Client/PublicApi/netstandard2.0/PublicAPI.Unshipped.txt +++ b/src/client/Microsoft.Identity.Client/PublicApi/netstandard2.0/PublicAPI.Unshipped.txt @@ -1,4 +1,5 @@ Microsoft.Identity.Client.Utils.MacMainThreadScheduler +static Microsoft.Identity.Client.Extensibility.AbstractConfidentialClientAcquireTokenParameterBuilderExtension.WithFmiPathForClientAssertion(this Microsoft.Identity.Client.AbstractAcquireTokenParameterBuilder builder, string fmiPath) -> Microsoft.Identity.Client.AbstractAcquireTokenParameterBuilder static Microsoft.Identity.Client.Utils.MacMainThreadScheduler.Instance() -> Microsoft.Identity.Client.Utils.MacMainThreadScheduler Microsoft.Identity.Client.Utils.MacMainThreadScheduler.IsCurrentlyOnMainThread() -> bool Microsoft.Identity.Client.Utils.MacMainThreadScheduler.IsRunning() -> bool @@ -6,4 +7,6 @@ Microsoft.Identity.Client.Utils.MacMainThreadScheduler.Stop() -> void Microsoft.Identity.Client.Utils.MacMainThreadScheduler.RunOnMainThreadAsync(System.Func asyncAction) -> System.Threading.Tasks.Task Microsoft.Identity.Client.Utils.MacMainThreadScheduler.StartMessageLoop() -> void Microsoft.Identity.Client.AuthenticationResultMetadata.CachedAccessTokenCount.get -> int -Microsoft.Identity.Client.AuthenticationResultMetadata.CachedAccessTokenCount.set -> void \ No newline at end of file +Microsoft.Identity.Client.AuthenticationResultMetadata.CachedAccessTokenCount.set -> void +Microsoft.Identity.Client.AssertionRequestOptions.ClientAssertionFmiPath.get -> string +Microsoft.Identity.Client.AssertionRequestOptions.ClientAssertionFmiPath.set -> void diff --git a/tests/Microsoft.Identity.Test.Unit/PublicApiTests/ClientAssertionTests.cs b/tests/Microsoft.Identity.Test.Unit/PublicApiTests/ClientAssertionTests.cs new file mode 100644 index 0000000000..d4abb9fd06 --- /dev/null +++ b/tests/Microsoft.Identity.Test.Unit/PublicApiTests/ClientAssertionTests.cs @@ -0,0 +1,334 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; +using Microsoft.Identity.Client; +using Microsoft.Identity.Client.Extensibility; +using Microsoft.Identity.Client.OAuth2; +using Microsoft.Identity.Test.Common.Core.Mocks; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Microsoft.Identity.Test.Unit.PublicApiTests +{ + [TestClass] + public class ClientAssertionTests : TestBase + { + private const string AssertionFmiPath1 = "test-client-assertion1"; + private const string AssertionFmiPath2 = "test-client-assertion2"; + + [TestMethod] + public async Task SignedAssertionDelegateClientCredential_NoClaims() + { + using (var httpManager = new MockHttpManager()) + { + httpManager.AddInstanceDiscoveryMockHandler(); + + var handler = httpManager.AddMockHandlerSuccessfulClientCredentialTokenResponseMessage(); + handler.ExpectedPostData = new Dictionary(); + + var app = ConfidentialClientApplicationBuilder + .Create(TestConstants.ClientId) + .WithHttpManager(httpManager) + .WithClientAssertion(async (AssertionRequestOptions options) => + { + // Ensure claims are set when WithClaims is called + Assert.IsNull(options.Claims); + Assert.IsNull(options.ClientCapabilities); + Assert.IsNull(options.ClientAssertionFmiPath); + Assert.AreEqual(TestConstants.ClientId, options.ClientID); + return await Task.FromResult("dummy_assertion").ConfigureAwait(false); + }) + .BuildConcrete(); + + var result = await app.AcquireTokenForClient(TestConstants.s_scope) + .ExecuteAsync() + .ConfigureAwait(false); + + Assert.IsNotNull(result); + Assert.IsFalse(handler.ActualRequestPostData.ContainsKey("claims")); + } + } + + [TestMethod] + public async Task SignedAssertionDelegateClientCredential_WithClaims() + { + using (var httpManager = new MockHttpManager()) + { + httpManager.AddInstanceDiscoveryMockHandler(); + + var handler = httpManager.AddMockHandlerSuccessfulClientCredentialTokenResponseMessage(); + handler.ExpectedPostData = new Dictionary(); + + var app = ConfidentialClientApplicationBuilder + .Create(TestConstants.ClientId) + .WithHttpManager(httpManager) + .WithClientAssertion(async (AssertionRequestOptions options) => + { + // Ensure claims are NOT set when WithClaims is not called + Assert.IsNull(options.Claims); + return await Task.FromResult("dummy_assertion").ConfigureAwait(false); + }) + .BuildConcrete(); + + var result = await app.AcquireTokenForClient(TestConstants.s_scope) + .ExecuteAsync() + .ConfigureAwait(false); + + Assert.IsNotNull(result); + Assert.IsFalse(handler.ActualRequestPostData.ContainsKey("claims")); + } + } + + [TestMethod] + public async Task FmiPathClientAssertion() + { + // Arrange + using (var httpManager = new MockHttpManager()) + { + httpManager.AddInstanceDiscoveryMockHandler(); + httpManager.AddMockHandlerSuccessfulClientCredentialTokenResponseMessage(); + httpManager.AddMockHandlerSuccessfulClientCredentialTokenResponseMessage(); + httpManager.AddMockHandlerSuccessfulClientCredentialTokenResponseMessage( + expectedPostData: new Dictionary() { { OAuth2Parameter.FmiPath, "fmiPath" } }); + + string actualAssertionFmiPath = null; + var app = ConfidentialClientApplicationBuilder + .Create(TestConstants.ClientId) + .WithAuthority(TestConstants.AuthorityTestTenant) + .WithExperimentalFeatures(true) + .WithHttpManager(httpManager) + .WithClientAssertion(async o => + { + actualAssertionFmiPath = o.ClientAssertionFmiPath; + return await Task.FromResult("dummy_assertion").ConfigureAwait(false); + + }) + .Build(); + + // Act 1 + actualAssertionFmiPath = null; + var result = await app.AcquireTokenForClient(TestConstants.s_scope) + .WithFmiPathForClientAssertion(AssertionFmiPath1) + .ExecuteAsync() + .ConfigureAwait(false); + + // Assert + Assert.AreEqual(TokenSource.IdentityProvider, result.AuthenticationResultMetadata.TokenSource); + Assert.AreEqual(AssertionFmiPath1, actualAssertionFmiPath); + + // Act 2 - request a token, with a different cred fmi path, expect a new token + actualAssertionFmiPath = null; + result = await app.AcquireTokenForClient(TestConstants.s_scope) + .WithFmiPathForClientAssertion(AssertionFmiPath2) + .ExecuteAsync() + .ConfigureAwait(false); + + // Assert + Assert.AreEqual(TokenSource.IdentityProvider, result.AuthenticationResultMetadata.TokenSource); + Assert.AreEqual(AssertionFmiPath2, actualAssertionFmiPath); + + // Act 3 - request the token with the same path, expect cached token + actualAssertionFmiPath = null; + result = await app.AcquireTokenForClient(TestConstants.s_scope) + .WithFmiPathForClientAssertion(AssertionFmiPath2) + .ExecuteAsync() + .ConfigureAwait(false); + + Assert.AreEqual(TokenSource.Cache, result.AuthenticationResultMetadata.TokenSource); + Assert.IsNull(actualAssertionFmiPath); + + // Act 4 - request the token with the same path 2, expect cached token + actualAssertionFmiPath = null; + result = await app.AcquireTokenForClient(TestConstants.s_scope) + .WithFmiPathForClientAssertion(AssertionFmiPath1) + .ExecuteAsync() + .ConfigureAwait(false); + Assert.AreEqual(TokenSource.Cache, result.AuthenticationResultMetadata.TokenSource); + Assert.IsNull(actualAssertionFmiPath); + + // Act 4 - request the token with the same path, and now add FMI path too + actualAssertionFmiPath = null; + result = await app.AcquireTokenForClient(TestConstants.s_scope) + .WithFmiPath("fmiPath") + .WithFmiPathForClientAssertion(AssertionFmiPath1) + .ExecuteAsync() + .ConfigureAwait(false); + + Assert.AreEqual(TokenSource.IdentityProvider, result.AuthenticationResultMetadata.TokenSource); + Assert.AreEqual(AssertionFmiPath1, actualAssertionFmiPath); + } + } + + [TestMethod] + public async Task FmiPathClientAssertionObo() + { + const string AssertionFmiPath = "test-client-assertion"; + + // Arrange + using (var httpManager = new MockHttpManager()) + { + httpManager.AddInstanceDiscoveryMockHandler(); + httpManager.AddMockHandler(new MockHttpMessageHandler() + { + ExpectedMethod = HttpMethod.Post, + ResponseMessage = MockHelpers.CreateSuccessTokenResponseMessage() + }); + + bool verified = false; + var app = ConfidentialClientApplicationBuilder + .Create(TestConstants.ClientId) + .WithAuthority(TestConstants.AuthorityTestTenant) + .WithHttpManager(httpManager) + .WithExperimentalFeatures(true) + .WithClientAssertion(async o => + { + Assert.AreEqual(AssertionFmiPath, o.ClientAssertionFmiPath); + verified = true; + return await Task.FromResult("dummy_assertion").ConfigureAwait(false); + }) + .Build(); + + var userAssertion = new UserAssertion(TestConstants.DefaultAccessToken); + + // Act + var result = await app.AcquireTokenOnBehalfOf(TestConstants.s_scope, userAssertion) + .WithFmiPathForClientAssertion(AssertionFmiPath) + .ExecuteAsync() + .ConfigureAwait(false); + + // Assert + Assert.AreEqual(TokenSource.IdentityProvider, result.AuthenticationResultMetadata.TokenSource); + Assert.IsTrue(verified, "The client assertion delegate should have been called with the correct FMI path."); + } + } + + [TestMethod] + public async Task FmiPathClientAssertionLongRunningObo() + { + // Arrange + using (var httpManager = new MockHttpManager()) + { + httpManager.AddInstanceDiscoveryMockHandler(); + httpManager.AddMockHandler(new MockHttpMessageHandler() + { + ExpectedMethod = HttpMethod.Post, + ResponseMessage = MockHelpers.CreateSuccessTokenResponseMessage() + }); + httpManager.AddMockHandler(new MockHttpMessageHandler() + { + ExpectedMethod = HttpMethod.Post, + ResponseMessage = MockHelpers.CreateSuccessTokenResponseMessage() + }); + + string actualAssertionFmiPath = null; + var app = ConfidentialClientApplicationBuilder + .Create(TestConstants.ClientId) + .WithAuthority(TestConstants.AuthorityTestTenant) + .WithExperimentalFeatures(true) + .WithHttpManager(httpManager) + .WithClientAssertion(async o => + { + actualAssertionFmiPath = o.ClientAssertionFmiPath; + return await Task.FromResult("dummy_assertion").ConfigureAwait(false); + + }) + .BuildConcrete(); + + string oboCacheKey = "test-obo-cache-key"; + + // Act + actualAssertionFmiPath = null; + var result = await app.InitiateLongRunningProcessInWebApi(TestConstants.s_scope, TestConstants.DefaultAccessToken, ref oboCacheKey) + .WithFmiPathForClientAssertion(AssertionFmiPath1) + .ExecuteAsync() + .ConfigureAwait(false); + + // Assert + Assert.AreEqual(TokenSource.IdentityProvider, result.AuthenticationResultMetadata.TokenSource); + Assert.AreEqual(AssertionFmiPath1, actualAssertionFmiPath); + + // Act 2 - different path + actualAssertionFmiPath = null; + result = await app.InitiateLongRunningProcessInWebApi(TestConstants.s_scope, TestConstants.DefaultAccessToken, ref oboCacheKey) + .WithFmiPathForClientAssertion(AssertionFmiPath2) + .ExecuteAsync() + .ConfigureAwait(false); + + Assert.AreEqual(TokenSource.IdentityProvider, result.AuthenticationResultMetadata.TokenSource); + Assert.AreEqual(AssertionFmiPath2, actualAssertionFmiPath); + } + } + + [TestMethod] + public async Task LongRunningObo_RunsSuccessfully_TestAsync() + { + using (var httpManager = new MockHttpManager()) + { + httpManager.AddInstanceDiscoveryMockHandler(); + httpManager.AddMockHandler(new MockHttpMessageHandler() + { + ExpectedMethod = HttpMethod.Post, + ResponseMessage = MockHelpers.CreateSuccessTokenResponseMessage() + }); + + bool verified = false; + + var cca = ConfidentialClientApplicationBuilder + .Create(TestConstants.ClientId) + .WithAuthority(TestConstants.AuthorityTestTenant) + .WithHttpManager(httpManager) + .WithExperimentalFeatures(true) + .WithClientAssertion(async o => + { + Assert.AreEqual(AssertionFmiPath1, o.ClientAssertionFmiPath); + verified = true; + return await Task.FromResult("dummy_assertion").ConfigureAwait(false); + }) + .BuildConcrete(); + + string oboCacheKey = "obo-cache-key"; + var result = await cca.InitiateLongRunningProcessInWebApi( + TestConstants.s_scope, + TestConstants.DefaultAccessToken, + ref oboCacheKey) + .WithFmiPathForClientAssertion(AssertionFmiPath1) + .ExecuteAsync().ConfigureAwait(false); + + // Token's not in cache, searched by user assertion hash, retrieved from IdP, saved with the provided OBO cache key + Assert.AreEqual(TestConstants.ATSecret, result.AccessToken); + Assert.AreEqual(TokenSource.IdentityProvider, result.AuthenticationResultMetadata.TokenSource); + Assert.IsTrue(verified, "The client assertion delegate should have been called with the correct FMI path."); + + verified = false; + result = await cca.AcquireTokenInLongRunningProcess(TestConstants.s_scope, oboCacheKey) + .WithFmiPathForClientAssertion(AssertionFmiPath1) + .ExecuteAsync().ConfigureAwait(false); + + // Token is in the cache, retrieved by the provided OBO cache key + Assert.AreEqual(TokenSource.Cache, result.AuthenticationResultMetadata.TokenSource); + Assert.IsFalse(verified, "The client assertion delegate should not have been called."); + + TokenCacheHelper.ExpireAllAccessTokens(cca.UserTokenCacheInternal); + httpManager.AddMockHandler(new MockHttpMessageHandler() + { + ExpectedMethod = HttpMethod.Post, + ResponseMessage = MockHelpers.CreateSuccessTokenResponseMessage() + }); + verified = false; + + result = await cca.AcquireTokenInLongRunningProcess(TestConstants.s_scope, oboCacheKey) + .WithFmiPathForClientAssertion(AssertionFmiPath1) + .ExecuteAsync().ConfigureAwait(false); + + // Cached AT is expired, RT used to retrieve new AT + Assert.AreEqual(TokenSource.IdentityProvider, result.AuthenticationResultMetadata.TokenSource); + Assert.IsTrue(verified, "The client assertion delegate should have been called with the correct FMI path."); + } + } + } +} diff --git a/tests/Microsoft.Identity.Test.Unit/PublicApiTests/ConfidentialClientApplicationTests.cs b/tests/Microsoft.Identity.Test.Unit/PublicApiTests/ConfidentialClientApplicationTests.cs index 94e3b321bd..2666f67bd2 100644 --- a/tests/Microsoft.Identity.Test.Unit/PublicApiTests/ConfidentialClientApplicationTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/PublicApiTests/ConfidentialClientApplicationTests.cs @@ -2066,65 +2066,7 @@ public async Task SignedAssertionDelegateClientCredential_Claims_TestAsync() } } - [TestMethod] - public async Task SignedAssertionDelegateClientCredential_NoClaims_TestAsync() - { - using (var httpManager = new MockHttpManager()) - { - httpManager.AddInstanceDiscoveryMockHandler(); - - var handler = httpManager.AddMockHandlerSuccessfulClientCredentialTokenResponseMessage(); - handler.ExpectedPostData = new Dictionary(); - - var app = ConfidentialClientApplicationBuilder - .Create(TestConstants.ClientId) - .WithHttpManager(httpManager) - .WithClientAssertion(async (AssertionRequestOptions options) => - { - // Ensure claims are set when WithClaims is called - Assert.IsNull(options.Claims); - return await Task.FromResult("dummy_assertion").ConfigureAwait(false); - }) - .BuildConcrete(); - - var result = await app.AcquireTokenForClient(TestConstants.s_scope) - .ExecuteAsync() - .ConfigureAwait(false); - - Assert.IsNotNull(result); - Assert.IsFalse(handler.ActualRequestPostData.ContainsKey("claims")); - } - } - - [TestMethod] - public async Task SignedAssertionDelegateClientCredential_WithClaims_TestAsync() - { - using (var httpManager = new MockHttpManager()) - { - httpManager.AddInstanceDiscoveryMockHandler(); - - var handler = httpManager.AddMockHandlerSuccessfulClientCredentialTokenResponseMessage(); - handler.ExpectedPostData = new Dictionary(); - - var app = ConfidentialClientApplicationBuilder - .Create(TestConstants.ClientId) - .WithHttpManager(httpManager) - .WithClientAssertion(async (AssertionRequestOptions options) => - { - // Ensure claims are NOT set when WithClaims is not called - Assert.IsNull(options.Claims); - return await Task.FromResult("dummy_assertion").ConfigureAwait(false); - }) - .BuildConcrete(); - - var result = await app.AcquireTokenForClient(TestConstants.s_scope) - .ExecuteAsync() - .ConfigureAwait(false); - - Assert.IsNotNull(result); - Assert.IsFalse(handler.ActualRequestPostData.ContainsKey("claims")); - } - } + [TestMethod] public async Task AcquireTokenByAuthorizationCode_NullOrEmptyCode_ThrowsAsync()