diff --git a/src/client/Microsoft.Identity.Client/ApiConfig/BaseAbstractAcquireTokenParameterBuilder.cs b/src/client/Microsoft.Identity.Client/ApiConfig/BaseAbstractAcquireTokenParameterBuilder.cs index c2f50c33cc..4c331558d9 100644 --- a/src/client/Microsoft.Identity.Client/ApiConfig/BaseAbstractAcquireTokenParameterBuilder.cs +++ b/src/client/Microsoft.Identity.Client/ApiConfig/BaseAbstractAcquireTokenParameterBuilder.cs @@ -135,6 +135,7 @@ public T WithExtraQueryParameters(IDictionary /// Validates the parameters of the AcquireToken operation. /// diff --git a/src/client/Microsoft.Identity.Client/ApiConfig/Parameters/AcquireTokenCommonParameters.cs b/src/client/Microsoft.Identity.Client/ApiConfig/Parameters/AcquireTokenCommonParameters.cs index 1bd046d6ea..13d7137b47 100644 --- a/src/client/Microsoft.Identity.Client/ApiConfig/Parameters/AcquireTokenCommonParameters.cs +++ b/src/client/Microsoft.Identity.Client/ApiConfig/Parameters/AcquireTokenCommonParameters.cs @@ -39,6 +39,7 @@ internal class AcquireTokenCommonParameters public X509Certificate2 MtlsCertificate { get; internal set; } public List AdditionalCacheParameters { get; set; } public SortedList>> CacheKeyComponents { get; internal set; } + public bool? SendOfflineAccessScope { get; set; } public string FmiPathSuffix { get; internal set; } public string ClientAssertionFmiPath { get; internal set; } public bool IsMtlsPopRequested { get; set; } diff --git a/src/client/Microsoft.Identity.Client/Extensibility/AcquireTokenParameterBuilderExtensions.cs b/src/client/Microsoft.Identity.Client/Extensibility/AcquireTokenParameterBuilderExtensions.cs index 1d318946f8..4726af213e 100644 --- a/src/client/Microsoft.Identity.Client/Extensibility/AcquireTokenParameterBuilderExtensions.cs +++ b/src/client/Microsoft.Identity.Client/Extensibility/AcquireTokenParameterBuilderExtensions.cs @@ -4,6 +4,8 @@ using System; using System.Collections.Generic; using System.ComponentModel; +using System.Threading; +using System.Threading.Tasks; namespace Microsoft.Identity.Client.Advanced { @@ -44,9 +46,64 @@ public static T WithExtraHttpHeaders( this AbstractAcquireTokenParameterBuilder builder, IDictionary extraHttpHeaders) where T : AbstractAcquireTokenParameterBuilder - { + { builder.CommonParameters.ExtraHttpHeaders = extraHttpHeaders; return (T)builder; } + + /// + /// Adds a key-value pair to the token cache key without sending it as a query parameter. + /// Use this to partition cached tokens (e.g., isolating short-lived sessions from regular + /// sessions for the same user). Both AcquireTokenByAuthorizationCode and + /// AcquireTokenSilent must use the same partition key to match cached entries. + /// + /// The builder to chain .With methods. + /// The partition key name. + /// The partition key value. + /// The builder to chain .With methods. + public static T WithCachePartitionKey( + this BaseAbstractAcquireTokenParameterBuilder builder, + string key, + string value) + where T : BaseAbstractAcquireTokenParameterBuilder + { + if (key is null) + { + throw new ArgumentNullException(nameof(key)); + } + + if (key.Length == 0) + { + throw new ArgumentException("Value cannot be empty.", nameof(key)); + } + + if (value is null) + { + throw new ArgumentNullException(nameof(value)); + } + + builder.CommonParameters.CacheKeyComponents ??= new SortedList>>(); + string capturedValue = value; + builder.CommonParameters.CacheKeyComponents[key] = (CancellationToken _) => Task.FromResult(capturedValue); + return (T)builder; + } + + /// + /// Controls whether MSAL sends the reserved offline_access scope while continuing to + /// send openid and profile. Only applicable to authorization code redemption flows. + /// + /// The builder to chain .With methods. + /// + /// Set to to omit offline_access. Set to + /// to preserve the default MSAL behavior of sending all reserved scopes. + /// + /// The builder to chain .With methods. + public static AcquireTokenByAuthorizationCodeParameterBuilder WithReservedScopes( + this AcquireTokenByAuthorizationCodeParameterBuilder builder, + bool offlineAccessScope) + { + builder.CommonParameters.SendOfflineAccessScope = offlineAccessScope; + 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 9eab9f0bc8..24a5d5c9bf 100644 --- a/src/client/Microsoft.Identity.Client/Internal/Requests/AuthenticationRequestParameters.cs +++ b/src/client/Microsoft.Identity.Client/Internal/Requests/AuthenticationRequestParameters.cs @@ -75,6 +75,7 @@ public AuthenticationRequestParameters( HomeAccountId = homeAccountId; CacheKeyComponents = cacheKeyComponents; + SendOfflineAccessScope = commonParameters.SendOfflineAccessScope; } public ApplicationConfiguration AppConfig => _serviceBundle.Config; @@ -152,6 +153,7 @@ public IAuthenticationOperation AuthenticationScheme public IEnumerable PersistedCacheParameters => _commonParameters.AdditionalCacheParameters; public SortedList CacheKeyComponents {get; private set; } + public bool? SendOfflineAccessScope { 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. diff --git a/src/client/Microsoft.Identity.Client/OAuth2/TokenClient.cs b/src/client/Microsoft.Identity.Client/OAuth2/TokenClient.cs index 14b17c72cc..4a98bd6f7a 100644 --- a/src/client/Microsoft.Identity.Client/OAuth2/TokenClient.cs +++ b/src/client/Microsoft.Identity.Client/OAuth2/TokenClient.cs @@ -63,6 +63,12 @@ public async Task SendTokenRequestAsync( string scopes = !string.IsNullOrEmpty(scopeOverride) ? scopeOverride : GetDefaultScopes(_requestParams.Scope); + if (_requestParams.SendOfflineAccessScope is false) + { + scopes = string.Join(" ", scopes.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries) + .Where(s => !string.Equals(s, OAuth2Value.ScopeOfflineAccess, StringComparison.OrdinalIgnoreCase))); + } + await AddBodyParamsAndHeadersAsync(additionalBodyParameters, scopes, tokenEndpoint, cancellationToken).ConfigureAwait(false); AddThrottlingHeader(); 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 1ace583cda..99e725f55b 100644 --- a/src/client/Microsoft.Identity.Client/PublicApi/net462/PublicAPI.Unshipped.txt +++ b/src/client/Microsoft.Identity.Client/PublicApi/net462/PublicAPI.Unshipped.txt @@ -3,3 +3,5 @@ Microsoft.Identity.Client.AuthScheme.IAuthenticationOperation3.AfterCredentialEv Microsoft.Identity.Client.AuthScheme.CredentialEvaluationContext Microsoft.Identity.Client.AuthScheme.CredentialEvaluationContext.CredentialEvaluationContext(System.Security.Cryptography.X509Certificates.X509Certificate2 mtlsCertificate) -> void Microsoft.Identity.Client.AuthScheme.CredentialEvaluationContext.MtlsCertificate.get -> System.Security.Cryptography.X509Certificates.X509Certificate2 +static Microsoft.Identity.Client.Extensibility.AcquireTokenParameterBuilderExtensions.WithCachePartitionKey(this Microsoft.Identity.Client.BaseAbstractAcquireTokenParameterBuilder builder, string key, string value) -> T +static Microsoft.Identity.Client.Extensibility.AcquireTokenParameterBuilderExtensions.WithReservedScopes(this Microsoft.Identity.Client.AcquireTokenByAuthorizationCodeParameterBuilder builder, bool offlineAccessScope) -> Microsoft.Identity.Client.AcquireTokenByAuthorizationCodeParameterBuilder 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 1ace583cda..99e725f55b 100644 --- a/src/client/Microsoft.Identity.Client/PublicApi/net472/PublicAPI.Unshipped.txt +++ b/src/client/Microsoft.Identity.Client/PublicApi/net472/PublicAPI.Unshipped.txt @@ -3,3 +3,5 @@ Microsoft.Identity.Client.AuthScheme.IAuthenticationOperation3.AfterCredentialEv Microsoft.Identity.Client.AuthScheme.CredentialEvaluationContext Microsoft.Identity.Client.AuthScheme.CredentialEvaluationContext.CredentialEvaluationContext(System.Security.Cryptography.X509Certificates.X509Certificate2 mtlsCertificate) -> void Microsoft.Identity.Client.AuthScheme.CredentialEvaluationContext.MtlsCertificate.get -> System.Security.Cryptography.X509Certificates.X509Certificate2 +static Microsoft.Identity.Client.Extensibility.AcquireTokenParameterBuilderExtensions.WithCachePartitionKey(this Microsoft.Identity.Client.BaseAbstractAcquireTokenParameterBuilder builder, string key, string value) -> T +static Microsoft.Identity.Client.Extensibility.AcquireTokenParameterBuilderExtensions.WithReservedScopes(this Microsoft.Identity.Client.AcquireTokenByAuthorizationCodeParameterBuilder builder, bool offlineAccessScope) -> Microsoft.Identity.Client.AcquireTokenByAuthorizationCodeParameterBuilder 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 1ace583cda..99e725f55b 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 @@ -3,3 +3,5 @@ Microsoft.Identity.Client.AuthScheme.IAuthenticationOperation3.AfterCredentialEv Microsoft.Identity.Client.AuthScheme.CredentialEvaluationContext Microsoft.Identity.Client.AuthScheme.CredentialEvaluationContext.CredentialEvaluationContext(System.Security.Cryptography.X509Certificates.X509Certificate2 mtlsCertificate) -> void Microsoft.Identity.Client.AuthScheme.CredentialEvaluationContext.MtlsCertificate.get -> System.Security.Cryptography.X509Certificates.X509Certificate2 +static Microsoft.Identity.Client.Extensibility.AcquireTokenParameterBuilderExtensions.WithCachePartitionKey(this Microsoft.Identity.Client.BaseAbstractAcquireTokenParameterBuilder builder, string key, string value) -> T +static Microsoft.Identity.Client.Extensibility.AcquireTokenParameterBuilderExtensions.WithReservedScopes(this Microsoft.Identity.Client.AcquireTokenByAuthorizationCodeParameterBuilder builder, bool offlineAccessScope) -> Microsoft.Identity.Client.AcquireTokenByAuthorizationCodeParameterBuilder 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 1ace583cda..99e725f55b 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 @@ -3,3 +3,5 @@ Microsoft.Identity.Client.AuthScheme.IAuthenticationOperation3.AfterCredentialEv Microsoft.Identity.Client.AuthScheme.CredentialEvaluationContext Microsoft.Identity.Client.AuthScheme.CredentialEvaluationContext.CredentialEvaluationContext(System.Security.Cryptography.X509Certificates.X509Certificate2 mtlsCertificate) -> void Microsoft.Identity.Client.AuthScheme.CredentialEvaluationContext.MtlsCertificate.get -> System.Security.Cryptography.X509Certificates.X509Certificate2 +static Microsoft.Identity.Client.Extensibility.AcquireTokenParameterBuilderExtensions.WithCachePartitionKey(this Microsoft.Identity.Client.BaseAbstractAcquireTokenParameterBuilder builder, string key, string value) -> T +static Microsoft.Identity.Client.Extensibility.AcquireTokenParameterBuilderExtensions.WithReservedScopes(this Microsoft.Identity.Client.AcquireTokenByAuthorizationCodeParameterBuilder builder, bool offlineAccessScope) -> Microsoft.Identity.Client.AcquireTokenByAuthorizationCodeParameterBuilder 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 1ace583cda..99e725f55b 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 @@ -3,3 +3,5 @@ Microsoft.Identity.Client.AuthScheme.IAuthenticationOperation3.AfterCredentialEv Microsoft.Identity.Client.AuthScheme.CredentialEvaluationContext Microsoft.Identity.Client.AuthScheme.CredentialEvaluationContext.CredentialEvaluationContext(System.Security.Cryptography.X509Certificates.X509Certificate2 mtlsCertificate) -> void Microsoft.Identity.Client.AuthScheme.CredentialEvaluationContext.MtlsCertificate.get -> System.Security.Cryptography.X509Certificates.X509Certificate2 +static Microsoft.Identity.Client.Extensibility.AcquireTokenParameterBuilderExtensions.WithCachePartitionKey(this Microsoft.Identity.Client.BaseAbstractAcquireTokenParameterBuilder builder, string key, string value) -> T +static Microsoft.Identity.Client.Extensibility.AcquireTokenParameterBuilderExtensions.WithReservedScopes(this Microsoft.Identity.Client.AcquireTokenByAuthorizationCodeParameterBuilder builder, bool offlineAccessScope) -> Microsoft.Identity.Client.AcquireTokenByAuthorizationCodeParameterBuilder 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 1ace583cda..99e725f55b 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 @@ -3,3 +3,5 @@ Microsoft.Identity.Client.AuthScheme.IAuthenticationOperation3.AfterCredentialEv Microsoft.Identity.Client.AuthScheme.CredentialEvaluationContext Microsoft.Identity.Client.AuthScheme.CredentialEvaluationContext.CredentialEvaluationContext(System.Security.Cryptography.X509Certificates.X509Certificate2 mtlsCertificate) -> void Microsoft.Identity.Client.AuthScheme.CredentialEvaluationContext.MtlsCertificate.get -> System.Security.Cryptography.X509Certificates.X509Certificate2 +static Microsoft.Identity.Client.Extensibility.AcquireTokenParameterBuilderExtensions.WithCachePartitionKey(this Microsoft.Identity.Client.BaseAbstractAcquireTokenParameterBuilder builder, string key, string value) -> T +static Microsoft.Identity.Client.Extensibility.AcquireTokenParameterBuilderExtensions.WithReservedScopes(this Microsoft.Identity.Client.AcquireTokenByAuthorizationCodeParameterBuilder builder, bool offlineAccessScope) -> Microsoft.Identity.Client.AcquireTokenByAuthorizationCodeParameterBuilder diff --git a/tests/Microsoft.Identity.Test.Unit/PublicApiTests/WithCachePartitionKeyTests.cs b/tests/Microsoft.Identity.Test.Unit/PublicApiTests/WithCachePartitionKeyTests.cs new file mode 100644 index 0000000000..90b878881f --- /dev/null +++ b/tests/Microsoft.Identity.Test.Unit/PublicApiTests/WithCachePartitionKeyTests.cs @@ -0,0 +1,145 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Identity.Client; +using Microsoft.Identity.Client.ApiConfig.Parameters; +using Microsoft.Identity.Client.Extensibility; +using Microsoft.Identity.Client.OAuth2; +using Microsoft.Identity.Client.Utils; +using Microsoft.Identity.Test.Common.Core.Helpers; +using Microsoft.Identity.Test.Common.Core.Mocks; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Microsoft.Identity.Test.Unit.PublicApiTests +{ + [TestClass] + public class WithCachePartitionKeyTests + { + [TestMethod] + public async Task WithCachePartitionKey_PopulatesCacheKeyComponents_Async() + { + // Arrange + const string cachePartitionKey = "partition_key"; + const string cachePartitionValue = "partition_value"; + + var app = ConfidentialClientApplicationBuilder.Create(TestConstants.ClientId) + .WithAuthority(new Uri(ClientApplicationBase.DefaultAuthority), true) + .WithClientSecret(TestConstants.ClientSecret) + .BuildConcrete(); + + var builder = app.AcquireTokenForClient(TestConstants.s_scope) + .WithCachePartitionKey(cachePartitionKey, cachePartitionValue); + + // Act + var commonParameters = GetCommonParameters(builder); + + // Assert + Assert.IsNotNull(commonParameters.CacheKeyComponents); + Assert.HasCount(1, commonParameters.CacheKeyComponents); + Assert.IsTrue(commonParameters.CacheKeyComponents.ContainsKey(cachePartitionKey)); + var cachedValue = await commonParameters.CacheKeyComponents[cachePartitionKey](CancellationToken.None) + .ConfigureAwait(false); + Assert.AreEqual(cachePartitionValue, cachedValue); + Assert.IsNull(commonParameters.ExtraQueryParameters); + } + + [TestMethod] + public async Task WithCachePartitionKey_DoesNotAddToExtraQueryParameters_Async() + { + using (var httpManager = new MockHttpManager()) + { + // Arrange + const string cachePartitionKey = "partition_key"; + const string cachePartitionValue = "partition_value"; + + var app = ConfidentialClientApplicationBuilder.Create(TestConstants.ClientId) + .WithAuthority(new Uri(ClientApplicationBase.DefaultAuthority), true) + .WithClientSecret(TestConstants.ClientSecret) + .WithHttpManager(httpManager) + .WithInstanceDiscovery(false) + .BuildConcrete(); + + var handler = httpManager.AddSuccessTokenResponseMockHandlerForPost(); + handler.UnExpectedPostData = new Dictionary + { + { cachePartitionKey, null } + }; + + // Act + var result = await app.AcquireTokenByAuthorizationCode(TestConstants.s_scope, TestConstants.DefaultAuthorizationCode) + .WithCachePartitionKey(cachePartitionKey, cachePartitionValue) + .ExecuteAsync() + .ConfigureAwait(false); + + // Assert + Assert.IsNotNull(result); + Assert.AreEqual(TokenSource.IdentityProvider, result.AuthenticationResultMetadata.TokenSource); + } + } + + [TestMethod] + public void WithCachePartitionKey_ThrowsOnNullOrEmptyKey() + { + // Arrange + var app = ConfidentialClientApplicationBuilder.Create(TestConstants.ClientId) + .WithAuthority(new Uri(ClientApplicationBase.DefaultAuthority), true) + .WithClientSecret(TestConstants.ClientSecret) + .BuildConcrete(); + + var builder = app.AcquireTokenForClient(TestConstants.s_scope); + + // Act + ArgumentNullException nullKeyException = AssertException.Throws(() => builder.WithCachePartitionKey(null, "value")); + ArgumentException emptyKeyException = AssertException.Throws(() => builder.WithCachePartitionKey(string.Empty, "value")); + + // Assert + Assert.AreEqual("key", nullKeyException.ParamName); + Assert.AreEqual("key", emptyKeyException.ParamName); + } + + [TestMethod] + public void WithCachePartitionKey_ThrowsOnNullValue() + { + // Arrange + var app = ConfidentialClientApplicationBuilder.Create(TestConstants.ClientId) + .WithAuthority(new Uri(ClientApplicationBase.DefaultAuthority), true) + .WithClientSecret(TestConstants.ClientSecret) + .BuildConcrete(); + + var builder = app.AcquireTokenForClient(TestConstants.s_scope); + + // Act + ArgumentNullException exception = AssertException.Throws(() => builder.WithCachePartitionKey("key", null)); + + // Assert + Assert.AreEqual("value", exception.ParamName); + } + + private static AcquireTokenCommonParameters GetCommonParameters(object builder) + { + Type currentType = builder.GetType(); + + while (currentType != null) + { + var commonParametersProperty = currentType.GetProperty( + "CommonParameters", + BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); + + if (commonParametersProperty != null) + { + return (AcquireTokenCommonParameters)commonParametersProperty.GetValue(builder); + } + + currentType = currentType.BaseType; + } + + Assert.Fail("CommonParameters property was not found on the builder."); + return null; + } + } +} diff --git a/tests/Microsoft.Identity.Test.Unit/PublicApiTests/WithReservedScopesTests.cs b/tests/Microsoft.Identity.Test.Unit/PublicApiTests/WithReservedScopesTests.cs new file mode 100644 index 0000000000..968fc6da8c --- /dev/null +++ b/tests/Microsoft.Identity.Test.Unit/PublicApiTests/WithReservedScopesTests.cs @@ -0,0 +1,135 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Identity.Client; +using Microsoft.Identity.Client.Extensibility; +using Microsoft.Identity.Client.OAuth2; +using Microsoft.Identity.Client.Utils; +using Microsoft.Identity.Test.Common.Core.Mocks; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Microsoft.Identity.Test.Unit.PublicApiTests +{ + [TestClass] + public class WithReservedScopesTests + { + [TestMethod] + public async Task WithReservedScopes_ByAuthorizationCode_OmitsOfflineAccess_Async() + { + using (var httpManager = new MockHttpManager()) + { + // Arrange + var app = ConfidentialClientApplicationBuilder.Create(TestConstants.ClientId) + .WithAuthority(new Uri(ClientApplicationBase.DefaultAuthority), true) + .WithClientSecret(TestConstants.ClientSecret) + .WithHttpManager(httpManager) + .WithInstanceDiscovery(false) + .BuildConcrete(); + + var handler = httpManager.AddSuccessTokenResponseMockHandlerForPost(); + handler.ExpectedPostData = new Dictionary + { + { OAuth2Parameter.Scope, GetScopesWithoutOfflineAccess() } + }; + + // Act + var result = await app.AcquireTokenByAuthorizationCode(TestConstants.s_scope, TestConstants.DefaultAuthorizationCode) + .WithReservedScopes(offlineAccessScope: false) + .ExecuteAsync() + .ConfigureAwait(false); + + // Assert + Assert.IsNotNull(result); + Assert.AreEqual(TokenSource.IdentityProvider, result.AuthenticationResultMetadata.TokenSource); + } + } + + [TestMethod] + public async Task WithoutWithReservedScopes_ByAuthorizationCode_AddsReservedScopes_Async() + { + using (var httpManager = new MockHttpManager()) + { + // Arrange + var app = ConfidentialClientApplicationBuilder.Create(TestConstants.ClientId) + .WithAuthority(new Uri(ClientApplicationBase.DefaultAuthority), true) + .WithClientSecret(TestConstants.ClientSecret) + .WithHttpManager(httpManager) + .WithInstanceDiscovery(false) + .BuildConcrete(); + + var handler = httpManager.AddSuccessTokenResponseMockHandlerForPost(); + handler.ExpectedPostData = new Dictionary + { + { OAuth2Parameter.Scope, GetScopesWithReservedScopes() } + }; + + // Act + var result = await app.AcquireTokenByAuthorizationCode(TestConstants.s_scope, TestConstants.DefaultAuthorizationCode) + .ExecuteAsync() + .ConfigureAwait(false); + + // Assert + Assert.IsNotNull(result); + Assert.AreEqual(TokenSource.IdentityProvider, result.AuthenticationResultMetadata.TokenSource); + } + } + + [TestMethod] + public async Task WithReservedScopesTrue_ByAuthorizationCode_PreservesDefaultBehavior_Async() + { + using (var httpManager = new MockHttpManager()) + { + // Arrange + var app = ConfidentialClientApplicationBuilder.Create(TestConstants.ClientId) + .WithAuthority(new Uri(ClientApplicationBase.DefaultAuthority), true) + .WithClientSecret(TestConstants.ClientSecret) + .WithHttpManager(httpManager) + .WithInstanceDiscovery(false) + .BuildConcrete(); + + var handler = httpManager.AddSuccessTokenResponseMockHandlerForPost(); + handler.ExpectedPostData = new Dictionary + { + { OAuth2Parameter.Scope, GetScopesWithReservedScopes() } + }; + + // Act + var result = await app.AcquireTokenByAuthorizationCode(TestConstants.s_scope, TestConstants.DefaultAuthorizationCode) + .WithReservedScopes(offlineAccessScope: true) + .ExecuteAsync() + .ConfigureAwait(false); + + // Assert + Assert.IsNotNull(result); + Assert.AreEqual(TokenSource.IdentityProvider, result.AuthenticationResultMetadata.TokenSource); + } + } + + private static string GetScopesWithoutOfflineAccess() + { + return TestConstants.s_scope + .Concat(new[] + { + OAuth2Value.ScopeOpenId, + OAuth2Value.ScopeProfile + }) + .AsSingleString(); + } + + private static string GetScopesWithReservedScopes() + { + return TestConstants.s_scope + .Concat(new[] + { + OAuth2Value.ScopeOpenId, + OAuth2Value.ScopeProfile, + OAuth2Value.ScopeOfflineAccess + }) + .AsSingleString(); + } + } +}