diff --git a/src/client/Microsoft.Identity.Client/ApiConfig/AcquireTokenForClientParameterBuilder.cs b/src/client/Microsoft.Identity.Client/ApiConfig/AcquireTokenForClientParameterBuilder.cs index b954bef968..195b02a17a 100644 --- a/src/client/Microsoft.Identity.Client/ApiConfig/AcquireTokenForClientParameterBuilder.cs +++ b/src/client/Microsoft.Identity.Client/ApiConfig/AcquireTokenForClientParameterBuilder.cs @@ -15,6 +15,8 @@ using Microsoft.Identity.Client.Utils; using Microsoft.Identity.Client.Extensibility; using Microsoft.Identity.Client.OAuth2; +using System.Security.Cryptography; +using System.Text; namespace Microsoft.Identity.Client { @@ -44,9 +46,9 @@ internal static AcquireTokenForClientParameterBuilder Create( if (!string.IsNullOrEmpty(confidentialClientApplicationExecutor.ServiceBundle.Config.CertificateIdToAssociateWithToken)) { - builder.WithAdditionalCacheKeyComponents(new SortedList + builder.WithAdditionalCacheKeyComponents(new SortedList>> { - { Constants.CertSerialNumber, confidentialClientApplicationExecutor.ServiceBundle.Config.CertificateIdToAssociateWithToken } + { Constants.CertSerialNumber, (CancellationToken ct) => { return Task.FromResult(confidentialClientApplicationExecutor.ServiceBundle.Config.CertificateIdToAssociateWithToken); } } }); } @@ -141,9 +143,9 @@ public AcquireTokenForClientParameterBuilder WithFmiPath(string pathSuffix) throw new ArgumentNullException(nameof(pathSuffix)); } - var cacheKey = new SortedList + var cacheKey = new SortedList>> { - { OAuth2Parameter.FmiPath, pathSuffix } + { OAuth2Parameter.FmiPath, (CancellationToken ct) => {return Task.FromResult(pathSuffix);} } }; this.WithAdditionalCacheKeyComponents(cacheKey); diff --git a/src/client/Microsoft.Identity.Client/ApiConfig/Executors/ClientApplicationBaseExecutor.cs b/src/client/Microsoft.Identity.Client/ApiConfig/Executors/ClientApplicationBaseExecutor.cs index 383244480c..0556dd2569 100644 --- a/src/client/Microsoft.Identity.Client/ApiConfig/Executors/ClientApplicationBaseExecutor.cs +++ b/src/client/Microsoft.Identity.Client/ApiConfig/Executors/ClientApplicationBaseExecutor.cs @@ -33,7 +33,8 @@ public async Task ExecuteAsync( var requestParameters = await _clientApplicationBase.CreateRequestParametersAsync( commonParameters, requestContext, - _clientApplicationBase.UserTokenCacheInternal).ConfigureAwait(false); + _clientApplicationBase.UserTokenCacheInternal, + cancellationToken).ConfigureAwait(false); requestParameters.SendX5C = silentParameters.SendX5C ?? false; @@ -59,7 +60,8 @@ public async Task ExecuteAsync( var requestParameters = await _clientApplicationBase.CreateRequestParametersAsync( commonParameters, requestContext, - _clientApplicationBase.UserTokenCacheInternal).ConfigureAwait(false); + _clientApplicationBase.UserTokenCacheInternal, + cancellationToken).ConfigureAwait(false); requestContext.Logger.Info(() => LogMessages.UsingXScopesForRefreshTokenRequest(commonParameters.Scopes.Count())); diff --git a/src/client/Microsoft.Identity.Client/ApiConfig/Executors/ConfidentialClientExecutor.cs b/src/client/Microsoft.Identity.Client/ApiConfig/Executors/ConfidentialClientExecutor.cs index 7c0d7fe342..93e8eaf785 100644 --- a/src/client/Microsoft.Identity.Client/ApiConfig/Executors/ConfidentialClientExecutor.cs +++ b/src/client/Microsoft.Identity.Client/ApiConfig/Executors/ConfidentialClientExecutor.cs @@ -39,7 +39,8 @@ public async Task ExecuteAsync( AuthenticationRequestParameters requestParams = await _confidentialClientApplication.CreateRequestParametersAsync( commonParameters, requestContext, - _confidentialClientApplication.UserTokenCacheInternal).ConfigureAwait(false); + _confidentialClientApplication.UserTokenCacheInternal, + cancellationToken).ConfigureAwait(false); requestParams.SendX5C = authorizationCodeParameters.SendX5C ?? false; var handler = new ConfidentialAuthCodeRequest( @@ -60,7 +61,8 @@ public async Task ExecuteAsync( AuthenticationRequestParameters requestParams = await _confidentialClientApplication.CreateRequestParametersAsync( commonParameters, requestContext, - _confidentialClientApplication.AppTokenCacheInternal).ConfigureAwait(false); + _confidentialClientApplication.AppTokenCacheInternal, + cancellationToken).ConfigureAwait(false); requestParams.SendX5C = clientParameters.SendX5C ?? false; @@ -82,7 +84,8 @@ public async Task ExecuteAsync( AuthenticationRequestParameters requestParams = await _confidentialClientApplication.CreateRequestParametersAsync( commonParameters, requestContext, - _confidentialClientApplication.UserTokenCacheInternal).ConfigureAwait(false); + _confidentialClientApplication.UserTokenCacheInternal, + cancellationToken).ConfigureAwait(false); requestParams.SendX5C = onBehalfOfParameters.SendX5C ?? false; requestParams.UserAssertion = onBehalfOfParameters.UserAssertion; @@ -106,7 +109,8 @@ public async Task ExecuteAsync( AuthenticationRequestParameters requestParameters = await _confidentialClientApplication.CreateRequestParametersAsync( commonParameters, requestContext, - _confidentialClientApplication.UserTokenCacheInternal).ConfigureAwait(false); + _confidentialClientApplication.UserTokenCacheInternal, + cancellationToken).ConfigureAwait(false); requestParameters.Account = authorizationRequestUrlParameters.Account; requestParameters.LoginHint = authorizationRequestUrlParameters.LoginHint; @@ -142,7 +146,8 @@ public async Task ExecuteAsync( AuthenticationRequestParameters requestParams = await _confidentialClientApplication.CreateRequestParametersAsync( commonParameters, requestContext, - _confidentialClientApplication.UserTokenCacheInternal).ConfigureAwait(false); + _confidentialClientApplication.UserTokenCacheInternal, + cancellationToken).ConfigureAwait(false); requestParams.SendX5C = usernamePasswordParameters.SendX5C ?? false; diff --git a/src/client/Microsoft.Identity.Client/ApiConfig/Executors/ManagedIdentityExecutor.cs b/src/client/Microsoft.Identity.Client/ApiConfig/Executors/ManagedIdentityExecutor.cs index 6b56a13a0b..ff1ba983c6 100644 --- a/src/client/Microsoft.Identity.Client/ApiConfig/Executors/ManagedIdentityExecutor.cs +++ b/src/client/Microsoft.Identity.Client/ApiConfig/Executors/ManagedIdentityExecutor.cs @@ -38,7 +38,8 @@ public async Task ExecuteAsync( var requestParams = await _managedIdentityApplication.CreateRequestParametersAsync( commonParameters, requestContext, - _managedIdentityApplication.AppTokenCacheInternal).ConfigureAwait(false); + _managedIdentityApplication.AppTokenCacheInternal, + cancellationToken).ConfigureAwait(false); var handler = new ManagedIdentityAuthRequest( ServiceBundle, diff --git a/src/client/Microsoft.Identity.Client/ApiConfig/Executors/PublicClientExecutor.cs b/src/client/Microsoft.Identity.Client/ApiConfig/Executors/PublicClientExecutor.cs index f839fd1fc7..28e25abf65 100644 --- a/src/client/Microsoft.Identity.Client/ApiConfig/Executors/PublicClientExecutor.cs +++ b/src/client/Microsoft.Identity.Client/ApiConfig/Executors/PublicClientExecutor.cs @@ -31,7 +31,8 @@ public async Task ExecuteAsync( AuthenticationRequestParameters requestParams = await _publicClientApplication.CreateRequestParametersAsync( commonParameters, requestContext, - _publicClientApplication.UserTokenCacheInternal).ConfigureAwait(false); + _publicClientApplication.UserTokenCacheInternal, + cancellationToken).ConfigureAwait(false); requestParams.LoginHint = interactiveParameters.LoginHint; requestParams.Account = interactiveParameters.Account; @@ -52,7 +53,8 @@ public async Task ExecuteAsync( var requestParams = await _publicClientApplication.CreateRequestParametersAsync( commonParameters, requestContext, - _publicClientApplication.UserTokenCacheInternal).ConfigureAwait(false); + _publicClientApplication.UserTokenCacheInternal, + cancellationToken).ConfigureAwait(false); var handler = new DeviceCodeRequest( ServiceBundle, @@ -72,7 +74,8 @@ public async Task ExecuteAsync( var requestParams = await _publicClientApplication.CreateRequestParametersAsync( commonParameters, requestContext, - _publicClientApplication.UserTokenCacheInternal).ConfigureAwait(false); + _publicClientApplication.UserTokenCacheInternal, + cancellationToken).ConfigureAwait(false); var handler = new IntegratedWindowsAuthRequest( ServiceBundle, @@ -92,7 +95,8 @@ public async Task ExecuteAsync( var requestParams = await _publicClientApplication.CreateRequestParametersAsync( commonParameters, requestContext, - _publicClientApplication.UserTokenCacheInternal).ConfigureAwait(false); + _publicClientApplication.UserTokenCacheInternal, + cancellationToken).ConfigureAwait(false); var handler = new UsernamePasswordRequest( ServiceBundle, diff --git a/src/client/Microsoft.Identity.Client/ApiConfig/Parameters/AcquireTokenCommonParameters.cs b/src/client/Microsoft.Identity.Client/ApiConfig/Parameters/AcquireTokenCommonParameters.cs index c8415871ee..d9820cef8e 100644 --- a/src/client/Microsoft.Identity.Client/ApiConfig/Parameters/AcquireTokenCommonParameters.cs +++ b/src/client/Microsoft.Identity.Client/ApiConfig/Parameters/AcquireTokenCommonParameters.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Security.Cryptography.X509Certificates; +using System.Threading; using System.Threading.Tasks; using Microsoft.Identity.Client.AppConfig; using Microsoft.Identity.Client.AuthScheme; @@ -27,10 +28,10 @@ internal class AcquireTokenCommonParameters public IAuthenticationOperation AuthenticationOperation { get; set; } = new BearerAuthenticationOperation(); public IDictionary ExtraHttpHeaders { get; set; } public PoPAuthenticationConfiguration PopAuthenticationConfiguration { get; set; } - public Func OnBeforeTokenRequestHandler { get; internal set; } + public IList> OnBeforeTokenRequestHandler { get; internal set; } public X509Certificate2 MtlsCertificate { get; internal set; } public List AdditionalCacheParameters { get; set; } - public SortedList CacheKeyComponents { get; internal 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/ApplicationBase.cs b/src/client/Microsoft.Identity.Client/ApplicationBase.cs index e0cff901c8..5608eea888 100644 --- a/src/client/Microsoft.Identity.Client/ApplicationBase.cs +++ b/src/client/Microsoft.Identity.Client/ApplicationBase.cs @@ -2,6 +2,8 @@ // Licensed under the MIT License. using System; +using System.Collections.Generic; +using System.Threading; using System.Threading.Tasks; using Microsoft.Identity.Client.ApiConfig.Parameters; using Microsoft.Identity.Client.Internal; @@ -27,18 +29,41 @@ internal ApplicationBase(ApplicationConfiguration config) internal virtual async Task CreateRequestParametersAsync( AcquireTokenCommonParameters commonParameters, RequestContext requestContext, - ITokenCacheInternal cache) + ITokenCacheInternal cache, + CancellationToken cancellationToken) { Instance.Authority authority = await Instance.Authority.CreateAuthorityForRequestAsync( requestContext, commonParameters.AuthorityOverride).ConfigureAwait(false); + var cacheKeyComponents = await InitializeCacheKeyComponentsAsync(commonParameters.CacheKeyComponents, cancellationToken).ConfigureAwait(false); + return new AuthenticationRequestParameters( ServiceBundle, cache, commonParameters, requestContext, - authority); + authority, + cacheKeyComponents: cacheKeyComponents); + } + + internal async Task> InitializeCacheKeyComponentsAsync(SortedList>> cacheKeyComponents, CancellationToken cancellationToken) + { + if (cacheKeyComponents != null && cacheKeyComponents.Count > 0) + { + var initializedCacheKeyComponents = new SortedList(); + + foreach (var kvp in cacheKeyComponents) + { + if (kvp.Value != null) + { + initializedCacheKeyComponents.Add(kvp.Key, await kvp.Value.Invoke(cancellationToken).ConfigureAwait(false)); + } + } + return initializedCacheKeyComponents; + } + + return null; } internal static void GuardMobileFrameworks() diff --git a/src/client/Microsoft.Identity.Client/ConfidentialClientApplication.cs b/src/client/Microsoft.Identity.Client/ConfidentialClientApplication.cs index 2443a865a7..cea2e10ccb 100644 --- a/src/client/Microsoft.Identity.Client/ConfidentialClientApplication.cs +++ b/src/client/Microsoft.Identity.Client/ConfidentialClientApplication.cs @@ -207,9 +207,10 @@ AcquireTokenByRefreshTokenParameterBuilder IByRefreshToken.AcquireTokenByRefresh internal override async Task CreateRequestParametersAsync( AcquireTokenCommonParameters commonParameters, RequestContext requestContext, - ITokenCacheInternal cache) + ITokenCacheInternal cache, + CancellationToken cancellationToken) { - AuthenticationRequestParameters requestParams = await base.CreateRequestParametersAsync(commonParameters, requestContext, cache).ConfigureAwait(false); + AuthenticationRequestParameters requestParams = await base.CreateRequestParametersAsync(commonParameters, requestContext, cache, cancellationToken).ConfigureAwait(false); return requestParams; } } diff --git a/src/client/Microsoft.Identity.Client/Extensibility/AbstractConfidentialClientAcquireTokenParameterBuilderExtension.cs b/src/client/Microsoft.Identity.Client/Extensibility/AbstractConfidentialClientAcquireTokenParameterBuilderExtension.cs index ffb1381e03..be3c53c80c 100644 --- a/src/client/Microsoft.Identity.Client/Extensibility/AbstractConfidentialClientAcquireTokenParameterBuilderExtension.cs +++ b/src/client/Microsoft.Identity.Client/Extensibility/AbstractConfidentialClientAcquireTokenParameterBuilderExtension.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Linq; using System.Runtime.CompilerServices; +using System.Threading; using System.Threading.Tasks; using Microsoft.Identity.Client.OAuth2; @@ -29,12 +30,14 @@ public static AbstractAcquireTokenParameterBuilder OnBeforeTokenRequest( Func onBeforeTokenRequestHandler) where T : AbstractAcquireTokenParameterBuilder { - if (builder.CommonParameters.OnBeforeTokenRequestHandler != null && onBeforeTokenRequestHandler != null) + if (builder.CommonParameters.OnBeforeTokenRequestHandler == null) { - throw new InvalidOperationException("Cannot set OnBeforeTokenRequest handler twice."); + builder.CommonParameters.OnBeforeTokenRequestHandler = new List> { onBeforeTokenRequestHandler }; + } + else + { + builder.CommonParameters.OnBeforeTokenRequestHandler.Add(onBeforeTokenRequestHandler); } - - builder.CommonParameters.OnBeforeTokenRequestHandler = onBeforeTokenRequestHandler; return builder; } @@ -75,13 +78,18 @@ public static AbstractAcquireTokenParameterBuilder WithAuthenticationExtensio MsalAuthenticationExtension authenticationExtension) where T : AbstractAcquireTokenParameterBuilder { - if (builder.CommonParameters.OnBeforeTokenRequestHandler != null && authenticationExtension.OnBeforeTokenRequestHandler != null) + if (authenticationExtension.OnBeforeTokenRequestHandler != null) { - throw new InvalidOperationException("Cannot set both an AuthenticationOperation and an OnBeforeTokenRequestHandler"); + if (builder.CommonParameters.OnBeforeTokenRequestHandler == null) + { + builder.CommonParameters.OnBeforeTokenRequestHandler = new List> { authenticationExtension.OnBeforeTokenRequestHandler }; + } + else + { + builder.CommonParameters.OnBeforeTokenRequestHandler.Add(authenticationExtension.OnBeforeTokenRequestHandler); + } } - builder.CommonParameters.OnBeforeTokenRequestHandler = authenticationExtension.OnBeforeTokenRequestHandler; - if (authenticationExtension.AuthenticationOperation != null) builder.WithAuthenticationOperation(authenticationExtension.AuthenticationOperation); @@ -137,7 +145,7 @@ public static AbstractAcquireTokenParameterBuilder WithAdditionalCacheParamet /// internal static AbstractAcquireTokenParameterBuilder WithAdditionalCacheKeyComponents( this AbstractAcquireTokenParameterBuilder builder, - IDictionary cacheKeyComponents) + IDictionary>> cacheKeyComponents) where T : AbstractAcquireTokenParameterBuilder { if (cacheKeyComponents == null || cacheKeyComponents.Count == 0) @@ -148,7 +156,7 @@ internal static AbstractAcquireTokenParameterBuilder WithAdditionalCacheKeyCo if (builder.CommonParameters.CacheKeyComponents == null) { - builder.CommonParameters.CacheKeyComponents = new SortedList(cacheKeyComponents); + builder.CommonParameters.CacheKeyComponents = new SortedList>>(cacheKeyComponents); } else { @@ -187,9 +195,9 @@ public static AbstractAcquireTokenParameterBuilder WithFmiPathForClientAssert builder.CommonParameters.ClientAssertionFmiPath = fmiPath; // Add the fmi_path to the cache key so that it is used for cache lookups - var cacheKey = new SortedList + var cacheKey = new SortedList>> { - { "credential_fmi_path", fmiPath } + { "credential_fmi_path", (CancellationToken ct) => Task.FromResult(fmiPath) } }; WithAdditionalCacheKeyComponents(builder, cacheKey); diff --git a/src/client/Microsoft.Identity.Client/Extensibility/AcquireTokenForClientBuilderExtensions.cs b/src/client/Microsoft.Identity.Client/Extensibility/AcquireTokenForClientBuilderExtensions.cs index 56dcc61aab..032d77d60e 100644 --- a/src/client/Microsoft.Identity.Client/Extensibility/AcquireTokenForClientBuilderExtensions.cs +++ b/src/client/Microsoft.Identity.Client/Extensibility/AcquireTokenForClientBuilderExtensions.cs @@ -6,6 +6,8 @@ using System.ComponentModel; using System.Diagnostics.CodeAnalysis; using System.Text; +using System.Threading; +using System.Threading.Tasks; using Microsoft.Identity.Client.Cache; using Microsoft.Identity.Client.OAuth2; @@ -40,5 +42,35 @@ public static AcquireTokenForClientParameterBuilder WithProofOfPosessionKeyId( return builder; } + + /// + /// Add extra body parameters to the token request. These parameters are added to the cache key to associate these parameters with the acquired token. + /// + /// + /// List of additional body parameters + /// + public static AcquireTokenForClientParameterBuilder WithExtraBodyParameters( + this AcquireTokenForClientParameterBuilder builder, + Dictionary>> extrabodyparams) + { + builder.ValidateUseOfExperimentalFeature(); + if (extrabodyparams == null || extrabodyparams.Count == 0) + { + return builder; + } + builder.OnBeforeTokenRequest(async (data) => + { + foreach (var param in extrabodyparams) + { + if (param.Value != null) + { + data.BodyParameters.Add(param.Key, await param.Value(data.CancellationToken).ConfigureAwait(false)); + } + } + }); + + builder.WithAdditionalCacheKeyComponents(extrabodyparams); + return builder; + } } } diff --git a/src/client/Microsoft.Identity.Client/Internal/Requests/AuthenticationRequestParameters.cs b/src/client/Microsoft.Identity.Client/Internal/Requests/AuthenticationRequestParameters.cs index 5547605959..a497158134 100644 --- a/src/client/Microsoft.Identity.Client/Internal/Requests/AuthenticationRequestParameters.cs +++ b/src/client/Microsoft.Identity.Client/Internal/Requests/AuthenticationRequestParameters.cs @@ -35,7 +35,8 @@ public AuthenticationRequestParameters( AcquireTokenCommonParameters commonParameters, RequestContext requestContext, Authority initialAuthority, - string homeAccountId = null) + string homeAccountId = null, + SortedList cacheKeyComponents = null) { _serviceBundle = serviceBundle; _commonParameters = commonParameters; @@ -73,6 +74,7 @@ public AuthenticationRequestParameters( _serviceBundle.Config.ClientCapabilities); HomeAccountId = homeAccountId; + CacheKeyComponents = cacheKeyComponents; } public ApplicationConfiguration AppConfig => _serviceBundle.Config; @@ -127,7 +129,7 @@ public string Claims public IEnumerable PersistedCacheParameters => _commonParameters.AdditionalCacheParameters; - public SortedList CacheKeyComponents => _commonParameters.CacheKeyComponents; + public SortedList CacheKeyComponents {get; private set; } #region TODO REMOVE FROM HERE AND USE FROM SPECIFIC REQUEST PARAMETERS // TODO: ideally, these can come from the particular request instance and not be in RequestBase since it's not valid for all requests. @@ -156,7 +158,7 @@ public string LoginHint /// /// If set, MSAL should add the key / value pairs from the provider to the token endpoint instead of generating a client assertion /// - public Func OnBeforeTokenRequestHandler { get => _commonParameters.OnBeforeTokenRequestHandler; } + public IList> OnBeforeTokenRequestHandler { get => _commonParameters.OnBeforeTokenRequestHandler; } public IDictionary ExtraHttpHeaders => _commonParameters.ExtraHttpHeaders; diff --git a/src/client/Microsoft.Identity.Client/OAuth2/OAuth2Client.cs b/src/client/Microsoft.Identity.Client/OAuth2/OAuth2Client.cs index ae6384fe5f..31128b3b6e 100644 --- a/src/client/Microsoft.Identity.Client/OAuth2/OAuth2Client.cs +++ b/src/client/Microsoft.Identity.Client/OAuth2/OAuth2Client.cs @@ -89,7 +89,7 @@ internal Task GetTokenAsync( Uri endPoint, RequestContext requestContext, bool addCommonHeaders, - Func onBeforePostRequestHandler) + IList> onBeforePostRequestHandler) { return ExecuteRequestAsync( endPoint, @@ -106,7 +106,7 @@ internal async Task ExecuteRequestAsync( RequestContext requestContext, bool expectErrorsOn200OK = false, bool addCommonHeaders = true, - Func onBeforePostRequestData = null) + IList> onBeforePostRequestHandlers = null) { //Requests that are replayed by PKeyAuth do not need to have headers added because they already exist if (addCommonHeaders) @@ -126,11 +126,16 @@ internal async Task ExecuteRequestAsync( { if (method == HttpMethod.Post) { - if (onBeforePostRequestData != null) + if (onBeforePostRequestHandlers != null) { requestContext.Logger.Verbose(() => "[Oauth2Client] Processing onBeforePostRequestData "); var requestData = new OnBeforeTokenRequestData(_bodyParameters, _headers, endpointUri, requestContext.UserCancellationToken); - await onBeforePostRequestData(requestData).ConfigureAwait(false); + + foreach(var handler in onBeforePostRequestHandlers) + { + await handler(requestData).ConfigureAwait(false); + } + endpointUri = requestData.RequestUri; } 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 e69de29bb2..d02c6b8081 100644 --- a/src/client/Microsoft.Identity.Client/PublicApi/net462/PublicAPI.Unshipped.txt +++ b/src/client/Microsoft.Identity.Client/PublicApi/net462/PublicAPI.Unshipped.txt @@ -0,0 +1 @@ +static Microsoft.Identity.Client.Extensibility.AcquireTokenForClientBuilderExtensions.WithExtraBodyParameters(this Microsoft.Identity.Client.AcquireTokenForClientParameterBuilder builder, System.Collections.Generic.Dictionary>> extrabodyparams) -> Microsoft.Identity.Client.AcquireTokenForClientParameterBuilder 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 e69de29bb2..d02c6b8081 100644 --- a/src/client/Microsoft.Identity.Client/PublicApi/net472/PublicAPI.Unshipped.txt +++ b/src/client/Microsoft.Identity.Client/PublicApi/net472/PublicAPI.Unshipped.txt @@ -0,0 +1 @@ +static Microsoft.Identity.Client.Extensibility.AcquireTokenForClientBuilderExtensions.WithExtraBodyParameters(this Microsoft.Identity.Client.AcquireTokenForClientParameterBuilder builder, System.Collections.Generic.Dictionary>> extrabodyparams) -> Microsoft.Identity.Client.AcquireTokenForClientParameterBuilder 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 e69de29bb2..d02c6b8081 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 @@ -0,0 +1 @@ +static Microsoft.Identity.Client.Extensibility.AcquireTokenForClientBuilderExtensions.WithExtraBodyParameters(this Microsoft.Identity.Client.AcquireTokenForClientParameterBuilder builder, System.Collections.Generic.Dictionary>> extrabodyparams) -> Microsoft.Identity.Client.AcquireTokenForClientParameterBuilder 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 e69de29bb2..d02c6b8081 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 @@ -0,0 +1 @@ +static Microsoft.Identity.Client.Extensibility.AcquireTokenForClientBuilderExtensions.WithExtraBodyParameters(this Microsoft.Identity.Client.AcquireTokenForClientParameterBuilder builder, System.Collections.Generic.Dictionary>> extrabodyparams) -> Microsoft.Identity.Client.AcquireTokenForClientParameterBuilder 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 e69de29bb2..d02c6b8081 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 @@ -0,0 +1 @@ +static Microsoft.Identity.Client.Extensibility.AcquireTokenForClientBuilderExtensions.WithExtraBodyParameters(this Microsoft.Identity.Client.AcquireTokenForClientParameterBuilder builder, System.Collections.Generic.Dictionary>> extrabodyparams) -> Microsoft.Identity.Client.AcquireTokenForClientParameterBuilder 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 e69de29bb2..d02c6b8081 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 @@ -0,0 +1 @@ +static Microsoft.Identity.Client.Extensibility.AcquireTokenForClientBuilderExtensions.WithExtraBodyParameters(this Microsoft.Identity.Client.AcquireTokenForClientParameterBuilder builder, System.Collections.Generic.Dictionary>> extrabodyparams) -> Microsoft.Identity.Client.AcquireTokenForClientParameterBuilder diff --git a/tests/Microsoft.Identity.Test.Unit/PublicApiTests/CacheKeyExtensionTests.cs b/tests/Microsoft.Identity.Test.Unit/PublicApiTests/CacheKeyExtensionTests.cs index a74b36f713..3591be8c3e 100644 --- a/tests/Microsoft.Identity.Test.Unit/PublicApiTests/CacheKeyExtensionTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/PublicApiTests/CacheKeyExtensionTests.cs @@ -24,6 +24,30 @@ namespace Microsoft.Identity.Test.Unit.PublicApiTests public class CacheKeyExtensionTests : TestBase { private byte[] _serializedCache; + private Dictionary>> _additionalCacheKeysAsync1 = new Dictionary>> + { + { "key1", (CancellationToken ct) => { return Task.FromResult("value1"); } }, + { "key2", (CancellationToken ct) => { return Task.FromResult("value2"); } } + }; + private Dictionary>> _additionalCacheKeysAsync2 = new Dictionary>> + { + { "key3", (CancellationToken ct) => { return Task.FromResult("value3"); } }, + { "key4", (CancellationToken ct) => { return Task.FromResult("value4"); } } + }; + private Dictionary>> _additionalCacheKeysAsync3 = new Dictionary>> + { + { "key2", (CancellationToken ct) => { return Task.FromResult("value2"); } }, + { "key1", (CancellationToken ct) => { return Task.FromResult("value1"); } } + }; + + private Dictionary>> _additionalCacheKeysCombinedAsync = new Dictionary>> + { + { "key1", (CancellationToken ct) => { return Task.FromResult("value1"); } }, + { "key2", (CancellationToken ct) => { return Task.FromResult("value2"); } }, + { "key3", (CancellationToken ct) => { return Task.FromResult("value3"); } }, + { "key4", (CancellationToken ct) => { return Task.FromResult("value4"); } } + }; + private Dictionary _additionalCacheKeys1 = new Dictionary { { "key1", "value1" }, @@ -47,7 +71,6 @@ public class CacheKeyExtensionTests : TestBase { "key3", "value3" }, { "key4", "value4" } }; - [TestMethod] public async Task CacheExtWithInMemoryTestAsync() { @@ -101,7 +124,7 @@ private async Task RunHappyPathTest(ConfidentialClientApplication app, MockHttpM expectedCacheKeyHash = CoreHelpers.ComputeAccessTokenExtCacheKey(new(_additionalCacheKeys1)); var result = await app.AcquireTokenForClient(TestConstants.s_scope.ToArray()) - .WithAdditionalCacheKeyComponents(_additionalCacheKeys1) + .WithAdditionalCacheKeyComponents(_additionalCacheKeysAsync1) .ExecuteAsync(CancellationToken.None) .ConfigureAwait(false); @@ -113,7 +136,7 @@ private async Task RunHappyPathTest(ConfidentialClientApplication app, MockHttpM //Ensure that the order of the keys does not matter expectedCacheKeyHash = CoreHelpers.ComputeAccessTokenExtCacheKey(new(_additionalCacheKeys3)); result = await app.AcquireTokenForClient(TestConstants.s_scope.ToArray()) - .WithAdditionalCacheKeyComponents(_additionalCacheKeys3) + .WithAdditionalCacheKeyComponents(_additionalCacheKeysAsync3) .ExecuteAsync(CancellationToken.None) .ConfigureAwait(false); @@ -127,7 +150,7 @@ private async Task RunHappyPathTest(ConfidentialClientApplication app, MockHttpM httpManager.AddMockHandlerSuccessfulClientCredentialTokenResponseMessage(); expectedCacheKeyHash = null; result = await app.AcquireTokenForClient(TestConstants.s_scope.ToArray()) - .WithAdditionalCacheKeyComponents(_additionalCacheKeys2) + .WithAdditionalCacheKeyComponents(_additionalCacheKeysAsync2) .ExecuteAsync(CancellationToken.None) .ConfigureAwait(false); @@ -185,7 +208,7 @@ public async Task CacheExtEnsureStandardTokensDoNotClashTestAsync() httpManager.AddMockHandlerSuccessfulClientCredentialTokenResponseMessage(); result = await app.AcquireTokenForClient(TestConstants.s_scope.ToArray()) - .WithAdditionalCacheKeyComponents(_additionalCacheKeys1) + .WithAdditionalCacheKeyComponents(_additionalCacheKeysAsync1) .ExecuteAsync(CancellationToken.None) .ConfigureAwait(false); @@ -203,7 +226,7 @@ public async Task CacheExtEnsureStandardTokensDoNotClashTestAsync() //Ensure that extended tokens are retrivable result = await app.AcquireTokenForClient(TestConstants.s_scope.ToArray()) - .WithAdditionalCacheKeyComponents(_additionalCacheKeys1) + .WithAdditionalCacheKeyComponents(_additionalCacheKeysAsync1) .ExecuteAsync(CancellationToken.None) .ConfigureAwait(false); @@ -230,7 +253,7 @@ public async Task CacheExtEnsureNoComponentsAreAddedWithEmptyArrayTestAsync() httpManager.AddMockHandlerSuccessfulClientCredentialTokenResponseMessage(); var result = await app.AcquireTokenForClient(TestConstants.s_scope.ToArray()) - .WithAdditionalCacheKeyComponents(new SortedList()) + .WithAdditionalCacheKeyComponents(new SortedList>>()) .ExecuteAsync(CancellationToken.None) .ConfigureAwait(false); @@ -280,7 +303,7 @@ public async Task CacheExtEnsurePopKeysFunctionAsync() var result = await app.AcquireTokenForClient(TestConstants.s_scope.ToArray()) .WithTenantId(TestConstants.Utid) .WithSignedHttpRequestProofOfPossession(popConfig) - .WithAdditionalCacheKeyComponents(_additionalCacheKeys1) + .WithAdditionalCacheKeyComponents(_additionalCacheKeysAsync1) .ExecuteAsync() .ConfigureAwait(false); @@ -293,7 +316,7 @@ public async Task CacheExtEnsurePopKeysFunctionAsync() result = await app.AcquireTokenForClient(TestConstants.s_scope.ToArray()) .WithTenantId(TestConstants.Utid) .WithSignedHttpRequestProofOfPossession(popConfig) - .WithAdditionalCacheKeyComponents(_additionalCacheKeys1) + .WithAdditionalCacheKeyComponents(_additionalCacheKeysAsync1) .ExecuteAsync() .ConfigureAwait(false); @@ -324,8 +347,8 @@ public async Task CacheExtEnsureInputKeysAddedCorrectlyTestAsync() //Ensure cache key components are added correctly var result = await app.AcquireTokenForClient(TestConstants.s_scope.ToArray()) .WithForceRefresh(true) - .WithAdditionalCacheKeyComponents(_additionalCacheKeys1) - .WithAdditionalCacheKeyComponents(_additionalCacheKeys2) + .WithAdditionalCacheKeyComponents(_additionalCacheKeysAsync1) + .WithAdditionalCacheKeyComponents(_additionalCacheKeysAsync2) .ExecuteAsync(CancellationToken.None) .ConfigureAwait(false); @@ -335,8 +358,8 @@ public async Task CacheExtEnsureInputKeysAddedCorrectlyTestAsync() ValidateCacheKeyComponents(app.AppTokenCacheInternal.Accessor.GetAllAccessTokens().First(), _additionalCacheKeysCombined, expectedPopCacheKey); result = await app.AcquireTokenForClient(TestConstants.s_scope.ToArray()) - .WithAdditionalCacheKeyComponents(_additionalCacheKeys1) - .WithAdditionalCacheKeyComponents(_additionalCacheKeys2) + .WithAdditionalCacheKeyComponents(_additionalCacheKeysAsync1) + .WithAdditionalCacheKeyComponents(_additionalCacheKeysAsync2) .ExecuteAsync(CancellationToken.None) .ConfigureAwait(false); diff --git a/tests/Microsoft.Identity.Test.Unit/PublicApiTests/ExtraBodyParametersTests.cs b/tests/Microsoft.Identity.Test.Unit/PublicApiTests/ExtraBodyParametersTests.cs new file mode 100644 index 0000000000..d03d147c01 --- /dev/null +++ b/tests/Microsoft.Identity.Test.Unit/PublicApiTests/ExtraBodyParametersTests.cs @@ -0,0 +1,295 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Identity.Client; +using Microsoft.Identity.Test.Common.Core.Mocks; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.Identity.Test.Common.Core.Helpers; +using Microsoft.Identity.Client.Extensibility; + +namespace Microsoft.Identity.Test.Unit.PublicApiTests +{ + [TestClass] + public class ExtraBodyParametersTests : TestBase + { + private string _clientId = "4df2cbbb-8612-49c1-87c8-f334d6d065ad"; + private string _scope = "api://AzureFMITokenExchange/.default"; + private string _tenantId = "tenantid"; + private string _expectedParameterHash = ""; + + string _expectedExternalCacheKey => $"{_clientId}_{_tenantId}_{_expectedParameterHash}_AppTokenCache"; + + [TestMethod] + public async Task ValidateExtraBodyParameters() + { + using (var httpManager = new MockHttpManager()) + { + //Arrange + var extraBodyParams = new Dictionary>> + { + { "attributetoken", (CancellationToken ct) => GetComputedValue() }, + { "attributetoken2", (CancellationToken ct) => GetComputedValue() } + }; + + var extraBodyParams2 = new Dictionary>> + { + { "attributetoken", (CancellationToken ct) => GetComputedValue() }, + { "attributetoken2", (CancellationToken ct) => GetComputedValue() }, + { "attributetoken3", (CancellationToken ct) => GetComputedValue() } + }; + + //Act + //Create application + var confidentialApp = ConfidentialClientApplicationBuilder + .Create(_clientId) + .WithAuthority("https://login.microsoftonline.com/", _tenantId) + .WithClientSecret("ClientSecret") + .WithHttpManager(httpManager) + .WithExperimentalFeatures(true) // Enable experimental features to use WithExtraBodyParameters + .BuildConcrete(); + + //Recording test data for Asserts + _expectedParameterHash = "8cY9AFTXo3uSqueI1A_HPiX0j66dJXB-3c3BTDcJVxE"; + var appCacheAccess = confidentialApp.AppTokenCache.RecordAccess( + (args) => + { + Assert.AreEqual(_expectedExternalCacheKey, args.SuggestedCacheKey); } + ); + + //Acquire AuthN + httpManager.AddInstanceDiscoveryMockHandler(); + httpManager.AddMockHandlerSuccessfulClientCredentialTokenResponseMessage( + expectedPostData: new Dictionary + { + { "attributetoken", "AttributeToken" }, + { "attributetoken2", "AttributeToken" } + }); + + var authResult = await confidentialApp.AcquireTokenForClient(new[] { _scope }) + .WithFmiPath("SomeFmiPath/FmiCredentialPath") //Sets fmi path in client credential request. + .WithExtraBodyParameters(extraBodyParams) //Sets attributes in client credential request. + .ExecuteAsync() + .ConfigureAwait(false); + + //Assert + Assert.IsNotNull(authResult); + Assert.AreEqual(authResult.AuthenticationResultMetadata.TokenSource, TokenSource.IdentityProvider); + + //Ensure the extra body parameters are present in the cache key + authResult = await confidentialApp.AcquireTokenForClient(new[] { _scope }) + .WithFmiPath("SomeFmiPath/FmiCredentialPath") //Sets fmi path in client credential request. + .WithExtraBodyParameters(extraBodyParams) //Sets attributes in client credential request. + .ExecuteAsync() + .ConfigureAwait(false); + + //Assert + Assert.IsNotNull(authResult); + Assert.AreEqual(authResult.AuthenticationResultMetadata.TokenSource, TokenSource.Cache); + + //Ensure the same extra body parameters are needed get the cache key + httpManager.AddMockHandlerSuccessfulClientCredentialTokenResponseMessage( + expectedPostData: new Dictionary + { + { "attributetoken", "AttributeToken" }, + { "attributetoken2", "AttributeToken" }, + { "attributetoken3", "AttributeToken" } + }); + + _expectedParameterHash = "aPnz3SdIoSMmI5yKcFs9h2vMKdZB_vahvt61jBrsCIE"; + authResult = await confidentialApp.AcquireTokenForClient(new[] { _scope }) + .WithFmiPath("SomeFmiPath/FmiCredentialPath") //Sets fmi path in client credential request. + .WithExtraBodyParameters(extraBodyParams2) //Sets attributes in client credential request. + .ExecuteAsync() + .ConfigureAwait(false); + + //Assert + Assert.IsNotNull(authResult); + Assert.AreEqual(authResult.AuthenticationResultMetadata.TokenSource, TokenSource.IdentityProvider); + + //Ensure the first token can still be retrieved from the cache + _expectedParameterHash = "8cY9AFTXo3uSqueI1A_HPiX0j66dJXB-3c3BTDcJVxE"; + authResult = await confidentialApp.AcquireTokenForClient(new[] { _scope }) + .WithFmiPath("SomeFmiPath/FmiCredentialPath") //Sets fmi path in client credential request. + .WithExtraBodyParameters(extraBodyParams) //Sets attributes in client credential request. + .ExecuteAsync() + .ConfigureAwait(false); + + //Assert + Assert.IsNotNull(authResult); + Assert.AreEqual(authResult.AuthenticationResultMetadata.TokenSource, TokenSource.Cache); + } + } + + [TestMethod] + public async Task ValidateExtraBodyParametersAreCombined() + { + using (var httpManager = new MockHttpManager()) + { + //Arrange + var clientId = "4df2cbbb-8612-49c1-87c8-f334d6d065ad"; + var scope = "api://AzureFMITokenExchange/.default"; + var tenantId = "tenantid"; + + //Act + //Create application + var confidentialApp = ConfidentialClientApplicationBuilder + .Create(clientId) + .WithAuthority("https://login.microsoftonline.com/", tenantId) + .WithClientSecret("ClientSecret") + .WithHttpManager(httpManager) + .WithExperimentalFeatures(true) // Enable experimental features to use WithExtraBodyParameters + .BuildConcrete(); + + //Recording test data for Asserts + _expectedParameterHash = "zl6sDTLdSw06EytxoYRBItblGzbgi4qzQ8gvIyRywxc"; + var appCacheAccess = confidentialApp.AppTokenCache.RecordAccess( + (args) => + { + Assert.AreEqual(_expectedExternalCacheKey, args.SuggestedCacheKey); + } + ); + + //Acquire AuthN + httpManager.AddInstanceDiscoveryMockHandler(); + httpManager.AddMockHandlerSuccessfulClientCredentialTokenResponseMessage( + expectedPostData: new Dictionary + { + { "attributetoken1", "AttributeToken" }, + { "attributetoken2", "AttributeToken" }, + { "attributetoken3", "AttributeToken" }, + { "attributetoken4", "AttributeToken" }, + { "attributetoken5", "AttributeToken" } + }); + + var extraBodyParams = new Dictionary>> + { + { "attributetoken1", (CancellationToken ct) => GetComputedValue() }, + { "attributetoken2", (CancellationToken ct) => GetComputedValue() } + }; + + var extraBodyParams2 = new Dictionary>> + { + { "attributetoken3", (CancellationToken ct) => GetComputedValue() }, + { "attributetoken4", (CancellationToken ct) => GetComputedValue() }, + { "attributetoken5", (CancellationToken ct) => GetComputedValue() } + }; + + var authResult = await confidentialApp.AcquireTokenForClient(new[] { scope }) + .WithFmiPath("SomeFmiPath/FmiCredentialPath") //Sets fmi path in client credential request. + .WithExtraBodyParameters(extraBodyParams) //Sets attributes in client credential request. + .WithExtraBodyParameters(extraBodyParams2) + .ExecuteAsync() + .ConfigureAwait(false); + + //Assert + Assert.IsNotNull(authResult); + Assert.AreEqual(authResult.AuthenticationResultMetadata.TokenSource, TokenSource.IdentityProvider); + + //Ensure the extra body parameters are present in the cache key + authResult = await confidentialApp.AcquireTokenForClient(new[] { scope }) + .WithFmiPath("SomeFmiPath/FmiCredentialPath") //Sets fmi path in client credential request. + .WithExtraBodyParameters(extraBodyParams) //Sets attributes in client credential request. + .WithExtraBodyParameters(extraBodyParams2) + .ExecuteAsync() + .ConfigureAwait(false); + + //Assert + Assert.IsNotNull(authResult); + Assert.AreEqual(authResult.AuthenticationResultMetadata.TokenSource, TokenSource.Cache); + + //Ensure the same extra body parameters are needed get the cache key + httpManager.AddMockHandlerSuccessfulClientCredentialTokenResponseMessage( + expectedPostData: new Dictionary + { + { "attributetoken3", "AttributeToken" }, + { "attributetoken4", "AttributeToken" }, + { "attributetoken5", "AttributeToken" } + }); + + _expectedParameterHash = "y6I4j3oaWfbZglcRJJsyBj7ROXrfqSMdYoglx8Fdp4A"; + authResult = await confidentialApp.AcquireTokenForClient(new[] { scope }) + .WithFmiPath("SomeFmiPath/FmiCredentialPath") //Sets fmi path in client credential request. + .WithExtraBodyParameters(extraBodyParams2) //Sets attributes in client credential request. + .ExecuteAsync() + .ConfigureAwait(false); + + //Assert + Assert.IsNotNull(authResult); + Assert.AreEqual(authResult.AuthenticationResultMetadata.TokenSource, TokenSource.IdentityProvider); + + httpManager.AddMockHandlerSuccessfulClientCredentialTokenResponseMessage( + expectedPostData: new Dictionary + { + { "attributetoken1", "AttributeToken" }, + { "attributetoken2", "AttributeToken" } + }); + + _expectedParameterHash = "9VQZ-ObNKwbMeTs51ehDhjCE2mB4n5N_KoAy85Sr3yQ"; + authResult = await confidentialApp.AcquireTokenForClient(new[] { scope }) + .WithFmiPath("SomeFmiPath/FmiCredentialPath") //Sets fmi path in client credential request. + .WithExtraBodyParameters(extraBodyParams) //Sets attributes in client credential request. + .ExecuteAsync() + .ConfigureAwait(false); + + //Assert + Assert.IsNotNull(authResult); + Assert.AreEqual(authResult.AuthenticationResultMetadata.TokenSource, TokenSource.IdentityProvider); + } + } + + [TestMethod] + public async Task WithExtraBodyParameters_NullInput_ReturnToken() + { + using (var httpManager = new MockHttpManager()) + { + // Arrange + httpManager.AddInstanceDiscoveryMockHandler(); + httpManager.AddMockHandlerSuccessfulClientCredentialTokenResponseMessage(); + + var confidentialApp = ConfidentialClientApplicationBuilder + .Create(_clientId) + .WithAuthority("https://login.microsoftonline.com/", _tenantId) + .WithClientSecret("ClientSecret") + .WithHttpManager(httpManager) + .WithExperimentalFeatures(true) // Enable experimental features to use WithExtraBodyParameters + .BuildConcrete(); + + // Act & Assert + // Ensure that the token is returned even when no extra body parameters are provided + var authResult = await confidentialApp.AcquireTokenForClient(new[] { _scope }) + .WithExtraBodyParameters(null) + .ExecuteAsync() + .ConfigureAwait(false); + + Assert.IsNotNull(authResult); + Assert.AreEqual(authResult.AuthenticationResultMetadata.TokenSource, TokenSource.IdentityProvider); + + authResult = await confidentialApp.AcquireTokenForClient(new[] { _scope }) + .WithExtraBodyParameters(null) + .ExecuteAsync() + .ConfigureAwait(false); + + Assert.IsNotNull(authResult); + Assert.AreEqual(authResult.AuthenticationResultMetadata.TokenSource, TokenSource.Cache); + + //Ensure that the token can still be retrieved from the cache when the input is an empty dictionary + authResult = await confidentialApp.AcquireTokenForClient(new[] { _scope }) + .WithExtraBodyParameters(new Dictionary>>()) + .ExecuteAsync() + .ConfigureAwait(false); + + Assert.IsNotNull(authResult); + Assert.AreEqual(authResult.AuthenticationResultMetadata.TokenSource, TokenSource.Cache); + } + } + + private Task GetComputedValue() + { + return Task.FromResult("AttributeToken"); + } + } +}