diff --git a/src/client/Microsoft.Identity.Client/ApiConfig/AbstractConfidentialClientAcquireTokenParameterBuilder.cs b/src/client/Microsoft.Identity.Client/ApiConfig/AbstractConfidentialClientAcquireTokenParameterBuilder.cs index 1cfcf89f55..f2fea7ee59 100644 --- a/src/client/Microsoft.Identity.Client/ApiConfig/AbstractConfidentialClientAcquireTokenParameterBuilder.cs +++ b/src/client/Microsoft.Identity.Client/ApiConfig/AbstractConfidentialClientAcquireTokenParameterBuilder.cs @@ -49,7 +49,7 @@ protected override void Validate() if (ServiceBundle?.Config.ClientCredential == null && CommonParameters.OnBeforeTokenRequestHandler == null && ServiceBundle?.Config.AppTokenProvider == null - ) + ) { throw new MsalClientException( MsalError.ClientCredentialAuthenticationTypeMustBeDefined, diff --git a/src/client/Microsoft.Identity.Client/ApiConfig/AcquireTokenByUsernameAndPasswordConfidentialParameterBuilder.cs b/src/client/Microsoft.Identity.Client/ApiConfig/AcquireTokenByUsernameAndPasswordConfidentialParameterBuilder.cs new file mode 100644 index 0000000000..801e6409cd --- /dev/null +++ b/src/client/Microsoft.Identity.Client/ApiConfig/AcquireTokenByUsernameAndPasswordConfidentialParameterBuilder.cs @@ -0,0 +1,71 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Net.Http; +using System.Security; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Identity.Client.ApiConfig.Executors; +using Microsoft.Identity.Client.ApiConfig.Parameters; +using Microsoft.Identity.Client.AppConfig; +using Microsoft.Identity.Client.AuthScheme.PoP; +using Microsoft.Identity.Client.PlatformsCommon.Shared; +using Microsoft.Identity.Client.TelemetryCore.Internal.Events; + +namespace Microsoft.Identity.Client +{ + /// + /// Parameter builder for the + /// operation. See https://aka.ms/msal-net-up + /// + public sealed class AcquireTokenByUsernameAndPasswordConfidentialParameterBuilder : + AbstractConfidentialClientAcquireTokenParameterBuilder + { + private AcquireTokenByUsernamePasswordParameters Parameters { get; } = new AcquireTokenByUsernamePasswordParameters(); + + internal AcquireTokenByUsernameAndPasswordConfidentialParameterBuilder( + IConfidentialClientApplicationExecutor confidentialClientApplicationExecutor, + string username, + string password) + : base(confidentialClientApplicationExecutor) + { + Parameters.Username = username; + Parameters.Password = password; + } + + internal static AcquireTokenByUsernameAndPasswordConfidentialParameterBuilder Create( + IConfidentialClientApplicationExecutor confidentialClientApplicationExecutor, + IEnumerable scopes, + string username, + string password) + { + if (string.IsNullOrEmpty(username)) + { + throw new ArgumentNullException(nameof(username)); + } + + if (string.IsNullOrEmpty(password)) + { + throw new ArgumentNullException(nameof(password)); + } + + return new AcquireTokenByUsernameAndPasswordConfidentialParameterBuilder(confidentialClientApplicationExecutor, username, password) + .WithScopes(scopes); + } + + /// + internal override Task ExecuteInternalAsync(CancellationToken cancellationToken) + { + return ConfidentialClientApplicationExecutor.ExecuteAsync(CommonParameters, Parameters, cancellationToken); + } + + /// + internal override ApiEvent.ApiIds CalculateApiEventId() + { + return ApiEvent.ApiIds.AcquireTokenByUsernamePassword; + } + } +} diff --git a/src/client/Microsoft.Identity.Client/ApiConfig/Executors/ConfidentialClientExecutor.cs b/src/client/Microsoft.Identity.Client/ApiConfig/Executors/ConfidentialClientExecutor.cs index 67b7540232..97bfb4a884 100644 --- a/src/client/Microsoft.Identity.Client/ApiConfig/Executors/ConfidentialClientExecutor.cs +++ b/src/client/Microsoft.Identity.Client/ApiConfig/Executors/ConfidentialClientExecutor.cs @@ -130,5 +130,25 @@ public async Task ExecuteAsync( return await handler.GetAuthorizationUriWithoutPkceAsync(cancellationToken).ConfigureAwait(false); } } + + public async Task ExecuteAsync( + AcquireTokenCommonParameters commonParameters, + AcquireTokenByUsernamePasswordParameters usernamePasswordParameters, + CancellationToken cancellationToken) + { + var requestContext = CreateRequestContextAndLogVersionInfo(commonParameters.CorrelationId, cancellationToken); + + var requestParams = await _confidentialClientApplication.CreateRequestParametersAsync( + commonParameters, + requestContext, + _confidentialClientApplication.UserTokenCacheInternal).ConfigureAwait(false); + + var handler = new UsernamePasswordRequest( + ServiceBundle, + requestParams, + usernamePasswordParameters); + + return await handler.RunAsync(cancellationToken).ConfigureAwait(false); + } } } diff --git a/src/client/Microsoft.Identity.Client/ApiConfig/Executors/IConfidentialClientApplicationExecutor.cs b/src/client/Microsoft.Identity.Client/ApiConfig/Executors/IConfidentialClientApplicationExecutor.cs index d47382f565..60bdb24189 100644 --- a/src/client/Microsoft.Identity.Client/ApiConfig/Executors/IConfidentialClientApplicationExecutor.cs +++ b/src/client/Microsoft.Identity.Client/ApiConfig/Executors/IConfidentialClientApplicationExecutor.cs @@ -32,5 +32,10 @@ Task ExecuteAsync( AcquireTokenCommonParameters commonParameters, GetAuthorizationRequestUrlParameters authorizationRequestUrlParameters, CancellationToken cancellationToken); + + Task ExecuteAsync( + AcquireTokenCommonParameters commonParameters, + AcquireTokenByUsernamePasswordParameters usernamePasswordParameters, + CancellationToken cancellationToken); } } diff --git a/src/client/Microsoft.Identity.Client/ConfidentialClientApplication.cs b/src/client/Microsoft.Identity.Client/ConfidentialClientApplication.cs index 268b60e589..1cebb456f6 100644 --- a/src/client/Microsoft.Identity.Client/ConfidentialClientApplication.cs +++ b/src/client/Microsoft.Identity.Client/ConfidentialClientApplication.cs @@ -25,7 +25,8 @@ public sealed partial class ConfidentialClientApplication IConfidentialClientApplication, IConfidentialClientApplicationWithCertificate, IByRefreshToken, - ILongRunningWebApi + ILongRunningWebApi, + IByUsernameAndPassword { /// /// Instructs MSAL to try to auto discover the Azure region. @@ -170,6 +171,19 @@ public GetAuthorizationRequestUrlParameterBuilder GetAuthorizationRequestUrl( scopes); } + /// + AcquireTokenByUsernameAndPasswordConfidentialParameterBuilder IByUsernameAndPassword.AcquireTokenByUsernamePassword( + IEnumerable scopes, + string username, + string password) + { + return AcquireTokenByUsernameAndPasswordConfidentialParameterBuilder.Create( + ClientExecutorFactory.CreateConfidentialClientExecutor(this), + scopes, + username, + password); + } + AcquireTokenByRefreshTokenParameterBuilder IByRefreshToken.AcquireTokenByRefreshToken( IEnumerable scopes, string refreshToken) diff --git a/src/client/Microsoft.Identity.Client/IByUsernameAndPassword.cs b/src/client/Microsoft.Identity.Client/IByUsernameAndPassword.cs new file mode 100644 index 0000000000..471e865f74 --- /dev/null +++ b/src/client/Microsoft.Identity.Client/IByUsernameAndPassword.cs @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.Identity.Client.ApiConfig; + +namespace Microsoft.Identity.Client +{ + /// + /// Provides an explicit interface for using Resource Owner Password Grant on Confidential Client. + /// + public interface IByUsernameAndPassword + { + /// + /// Acquires a token without user interaction using username and password authentication. + /// This method does not look in the token cache, but stores the result in it. Before calling this method, use other methods + /// such as to check the token cache. + /// + /// Scopes requested to access a protected API. + /// Identifier of the user, application requests token on behalf of. + /// Generally in UserPrincipalName (UPN) format, e.g. john.doe@contoso.com + /// User password as a string. + /// A builder enabling you to add optional parameters before executing the token request. + /// + /// Available only for .NET Framework and .NET Core applications. See our documentation for details. + /// + AcquireTokenByUsernameAndPasswordConfidentialParameterBuilder AcquireTokenByUsernamePassword(IEnumerable scopes, string username, string password); + } +} diff --git a/src/client/Microsoft.Identity.Client/Internal/Requests/UsernamePasswordRequest.cs b/src/client/Microsoft.Identity.Client/Internal/Requests/UsernamePasswordRequest.cs index 3a67968e2c..c675540cb3 100644 --- a/src/client/Microsoft.Identity.Client/Internal/Requests/UsernamePasswordRequest.cs +++ b/src/client/Microsoft.Identity.Client/Internal/Requests/UsernamePasswordRequest.cs @@ -111,6 +111,13 @@ private async Task FetchAssertionFromWsTrustAsync() return null; } + //WsTrust not supported on ROPC + if (AuthenticationRequestParameters.AppConfig.IsConfidentialClient) + { + _logger.Info("WSTrust is not supported on confidential clients. Skipping wstrust for ROPC."); + return null; + } + var userRealmResponse = await _commonNonInteractiveHandler .QueryUserRealmDataAsync(AuthenticationRequestParameters.AuthorityInfo.UserRealmUriPrefix, _usernamePasswordParameters.Username) .ConfigureAwait(false); diff --git a/tests/Microsoft.Identity.Test.Integration.netcore/HeadlessTests/PoPTests.NetFwk.cs b/tests/Microsoft.Identity.Test.Integration.netcore/HeadlessTests/PoPTests.NetFwk.cs index cb0cd3bbf5..b60c70f865 100644 --- a/tests/Microsoft.Identity.Test.Integration.netcore/HeadlessTests/PoPTests.NetFwk.cs +++ b/tests/Microsoft.Identity.Test.Integration.netcore/HeadlessTests/PoPTests.NetFwk.cs @@ -45,6 +45,7 @@ public class PoPTests { private static readonly string[] s_keyvaultScope = { "https://vault.azure.net/.default" }; + private static readonly string[] s_ropcScope = { "User.Read" }; private const string ProtectedUrl = "https://www.contoso.com/path1/path2?queryParam1=a&queryParam2=b"; @@ -289,6 +290,38 @@ public async Task PopTestWithRSAAsync() Assert.AreEqual("RS256", alg, "The algorithm in the token header should be RS256"); } + [RunOn(TargetFrameworks.NetCore)] + public async Task ROPC_PopTestWithRSAAsync() + { + var settings = ConfidentialAppSettings.GetSettings(Cloud.Public); + var labResponse = await LabUserHelper.GetDefaultUserAsync().ConfigureAwait(false); + + var confidentialApp = ConfidentialClientApplicationBuilder + .Create(settings.ClientId) + .WithExperimentalFeatures() + .WithAuthority(settings.Authority) + .WithClientSecret(settings.GetSecret()) + .Build(); + + //RSA provider + var popConfig = new PoPAuthenticationConfiguration(new Uri(ProtectedUrl)); + popConfig.PopCryptoProvider = new RSACertificatePopCryptoProvider(GetCertificate()); + popConfig.HttpMethod = HttpMethod.Get; + + var result = await (confidentialApp as IByUsernameAndPassword).AcquireTokenByUsernamePassword(s_ropcScope, labResponse.User.Upn, labResponse.User.GetOrFetchPassword()) + .WithProofOfPossession(popConfig) + .ExecuteAsync(CancellationToken.None) + .ConfigureAwait(false); + + Assert.AreEqual("pop", result.TokenType); + PoPValidator.VerifyPoPToken( + settings.ClientId, + ProtectedUrl, + HttpMethod.Get, + result); + + } + [TestMethod] public async Task PopTest_ExternalWilsonSigning_Async() { diff --git a/tests/Microsoft.Identity.Test.Integration.netcore/HeadlessTests/UsernamePasswordIntegrationTests.NetFwk.cs b/tests/Microsoft.Identity.Test.Integration.netcore/HeadlessTests/UsernamePasswordIntegrationTests.NetFwk.cs index 309bbfb164..e9238b5460 100644 --- a/tests/Microsoft.Identity.Test.Integration.netcore/HeadlessTests/UsernamePasswordIntegrationTests.NetFwk.cs +++ b/tests/Microsoft.Identity.Test.Integration.netcore/HeadlessTests/UsernamePasswordIntegrationTests.NetFwk.cs @@ -18,6 +18,7 @@ using Microsoft.Identity.Test.Common; using Microsoft.Identity.Test.Common.Core.Helpers; using Microsoft.Identity.Test.Integration.Infrastructure; +using Microsoft.Identity.Test.Integration.NetFx.Infrastructure; using Microsoft.Identity.Test.LabInfrastructure; using Microsoft.Identity.Test.Unit; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -56,6 +57,13 @@ public async Task ROPC_AAD_Async() await RunHappyPathTestAsync(labResponse).ConfigureAwait(false); } + [TestMethod] + public async Task ROPC_AAD_CCA_Async() + { + var labResponse = await LabUserHelper.GetDefaultUserAsync().ConfigureAwait(false); + await RunHappyPathTestAsync(labResponse: labResponse, isPublicClient: false).ConfigureAwait(false); + } + [RunOn(TargetFrameworks.NetCore)] [TestCategory(TestCategories.Arlington)] public async Task ARLINGTON_ROPC_AAD_Async() @@ -64,6 +72,14 @@ public async Task ARLINGTON_ROPC_AAD_Async() await RunHappyPathTestAsync(labResponse).ConfigureAwait(false); } + [RunOn(TargetFrameworks.NetCore)] + [TestCategory(TestCategories.Arlington)] + public async Task ARLINGTON_ROPC_AAD_CCA_Async() + { + var labResponse = await LabUserHelper.GetArlingtonUserAsync().ConfigureAwait(false); + await RunHappyPathTestAsync(labResponse, isPublicClient: false, cloud:Cloud.Arlington).ConfigureAwait(false); + } + [RunOn(TargetFrameworks.NetCore)] [TestCategory(TestCategories.Arlington)] public async Task ARLINGTON_ROPC_ADFS_Async() @@ -242,21 +258,37 @@ private async Task RunAcquireTokenWithUsernameIncorrectPasswordAsync( Assert.Fail("Bad exception or no exception thrown"); } - private async Task RunHappyPathTestAsync(LabResponse labResponse, string federationMetadata = "") + private async Task RunHappyPathTestAsync(LabResponse labResponse, string federationMetadata = "", bool isPublicClient = true, Cloud cloud = Cloud.Public) { var factory = new HttpSnifferClientFactory(); - var msalPublicClient = PublicClientApplicationBuilder - .Create(labResponse.App.AppId) - .WithTestLogging() - .WithHttpClientFactory(factory) - .WithAuthority(labResponse.Lab.Authority, "organizations") - .Build(); + IClientApplicationBase clientApp = null; + + if (isPublicClient) + { + clientApp = PublicClientApplicationBuilder + .Create(labResponse.App.AppId) + .WithTestLogging() + .WithHttpClientFactory(factory) + .WithAuthority(labResponse.Lab.Authority, "organizations") + .Build(); + } + else + { + IConfidentialAppSettings settings = ConfidentialAppSettings.GetSettings(cloud); + clientApp = ConfidentialClientApplicationBuilder + .Create(settings.ClientId) + .WithTestLogging() + .WithHttpClientFactory(factory) + .WithAuthority(labResponse.Lab.Authority, "organizations") + .WithClientSecret(settings.GetSecret()) + .Build(); + } AuthenticationResult authResult = await GetAuthenticationResultWithAssertAsync( labResponse, factory, - msalPublicClient, + clientApp, federationMetadata, CorrelationId).ConfigureAwait(false); @@ -315,16 +347,30 @@ private async Task RunB2CHappyPathTestAsync(LabResponse labResponse, string fede private async Task GetAuthenticationResultWithAssertAsync( LabResponse labResponse, HttpSnifferClientFactory factory, - IPublicClientApplication msalPublicClient, + IClientApplicationBase clientApp, string federationMetadata, Guid testCorrelationId) { - AuthenticationResult authResult = await msalPublicClient + AuthenticationResult authResult; + + if (clientApp is IPublicClientApplication publicClientApp) + { + authResult = await publicClientApp .AcquireTokenByUsernamePassword(s_scopes, labResponse.User.Upn, labResponse.User.GetOrFetchPassword()) .WithCorrelationId(testCorrelationId) .WithFederationMetadata(federationMetadata) .ExecuteAsync(CancellationToken.None) .ConfigureAwait(false); + } + else + { + authResult = await (((IConfidentialClientApplication)clientApp) as IByUsernameAndPassword) + .AcquireTokenByUsernamePassword(s_scopes, labResponse.User.Upn, labResponse.User.GetOrFetchPassword()) + .WithCorrelationId(testCorrelationId) + .ExecuteAsync(CancellationToken.None) + .ConfigureAwait(false); + } + Assert.IsNotNull(authResult); Assert.AreEqual(TokenSource.IdentityProvider, authResult.AuthenticationResultMetadata.TokenSource);