Skip to content

Commit

Permalink
Enabling ROPC on CCA (#4799)
Browse files Browse the repository at this point in the history
* Enabling ROPC on CCA

* Clean up.
Adding tests.

* Refactoring.
Adding explicit interface for ROPC with CCA

* Update src/client/Microsoft.Identity.Client/IByUsernameAndPassword.cs

Co-authored-by: Neha Bhargava <[email protected]>

* Clean up.
Refactoring

* clean up

---------

Co-authored-by: trwalke <[email protected]>
Co-authored-by: Gladwin Johnson <[email protected]>
Co-authored-by: Neha Bhargava <[email protected]>
  • Loading branch information
4 people authored Aug 7, 2024
1 parent 89dba0a commit adf5dab
Show file tree
Hide file tree
Showing 9 changed files with 241 additions and 12 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// Parameter builder for the <see cref="IConfidentialClientApplication.AcquireTokenByUsernamePassword(IEnumerable{string}, string, string)"/>

Check warning on line 21 in src/client/Microsoft.Identity.Client/ApiConfig/AcquireTokenByUsernameAndPasswordConfidentialParameterBuilder.cs

View workflow job for this annotation

GitHub Actions / Run performance benchmarks

XML comment has cref attribute 'AcquireTokenByUsernamePassword(IEnumerable{string}, string, string)' that could not be resolved

Check warning on line 21 in src/client/Microsoft.Identity.Client/ApiConfig/AcquireTokenByUsernameAndPasswordConfidentialParameterBuilder.cs

View workflow job for this annotation

GitHub Actions / Run performance benchmarks

XML comment has cref attribute 'AcquireTokenByUsernamePassword(IEnumerable{string}, string, string)' that could not be resolved
/// operation. See https://aka.ms/msal-net-up
/// </summary>
public sealed class AcquireTokenByUsernameAndPasswordConfidentialParameterBuilder :
AbstractConfidentialClientAcquireTokenParameterBuilder<AcquireTokenByUsernameAndPasswordConfidentialParameterBuilder>
{
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<string> 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);
}

/// <inheritdoc/>
internal override Task<AuthenticationResult> ExecuteInternalAsync(CancellationToken cancellationToken)
{
return ConfidentialClientApplicationExecutor.ExecuteAsync(CommonParameters, Parameters, cancellationToken);
}

/// <inheritdoc/>
internal override ApiEvent.ApiIds CalculateApiEventId()
{
return ApiEvent.ApiIds.AcquireTokenByUsernamePassword;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -130,5 +130,25 @@ public async Task<Uri> ExecuteAsync(
return await handler.GetAuthorizationUriWithoutPkceAsync(cancellationToken).ConfigureAwait(false);
}
}

public async Task<AuthenticationResult> 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);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,5 +32,10 @@ Task<Uri> ExecuteAsync(
AcquireTokenCommonParameters commonParameters,
GetAuthorizationRequestUrlParameters authorizationRequestUrlParameters,
CancellationToken cancellationToken);

Task<AuthenticationResult> ExecuteAsync(
AcquireTokenCommonParameters commonParameters,
AcquireTokenByUsernamePasswordParameters usernamePasswordParameters,
CancellationToken cancellationToken);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ public sealed partial class ConfidentialClientApplication
IConfidentialClientApplication,
IConfidentialClientApplicationWithCertificate,
IByRefreshToken,
ILongRunningWebApi
ILongRunningWebApi,
IByUsernameAndPassword
{
/// <summary>
/// Instructs MSAL to try to auto discover the Azure region.
Expand Down Expand Up @@ -170,6 +171,19 @@ public GetAuthorizationRequestUrlParameterBuilder GetAuthorizationRequestUrl(
scopes);
}

/// <inheritdoc/>
AcquireTokenByUsernameAndPasswordConfidentialParameterBuilder IByUsernameAndPassword.AcquireTokenByUsernamePassword(
IEnumerable<string> scopes,
string username,
string password)
{
return AcquireTokenByUsernameAndPasswordConfidentialParameterBuilder.Create(
ClientExecutorFactory.CreateConfidentialClientExecutor(this),
scopes,
username,
password);
}

AcquireTokenByRefreshTokenParameterBuilder IByRefreshToken.AcquireTokenByRefreshToken(
IEnumerable<string> scopes,
string refreshToken)
Expand Down
33 changes: 33 additions & 0 deletions src/client/Microsoft.Identity.Client/IByUsernameAndPassword.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// Provides an explicit interface for using Resource Owner Password Grant on Confidential Client.
/// </summary>
public interface IByUsernameAndPassword
{
/// <summary>
/// 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 <see cref="IClientApplicationBase.AcquireTokenSilent(IEnumerable{string}, IAccount)"/> to check the token cache.
/// </summary>
/// <param name="scopes">Scopes requested to access a protected API.</param>
/// <param name="username">Identifier of the user, application requests token on behalf of.
/// Generally in UserPrincipalName (UPN) format, e.g. <c>[email protected]</c></param>
/// <param name="password">User password as a string.</param>
/// <returns>A builder enabling you to add optional parameters before executing the token request.</returns>
/// <remarks>
/// Available only for .NET Framework and .NET Core applications. See <see href="https://aka.ms/msal-net-up">our documentation</see> for details.
/// </remarks>
AcquireTokenByUsernameAndPasswordConfidentialParameterBuilder AcquireTokenByUsernamePassword(IEnumerable<string> scopes, string username, string password);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,13 @@ private async Task<UserAssertion> 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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -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()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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()
Expand All @@ -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()
Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -315,16 +347,30 @@ private async Task RunB2CHappyPathTestAsync(LabResponse labResponse, string fede
private async Task<AuthenticationResult> 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);
Expand Down

0 comments on commit adf5dab

Please sign in to comment.