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);