Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[draft] sni + mtls #4965

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ protected override void Validate()
/// <item><description>This is an experimental API. The method signature may change in the future without involving a major version upgrade.</description></item>
/// </list>
/// </remarks>
[EditorBrowsable(EditorBrowsableState.Never)] // Soft deprecate
public T WithProofOfPossession(PoPAuthenticationConfiguration popAuthenticationConfiguration)
{
ValidateUseOfExperimentalFeature();
Expand All @@ -89,5 +90,29 @@ public T WithProofOfPossession(PoPAuthenticationConfiguration popAuthenticationC

return this as T;
}

/// <summary>
/// Modifies the token acquisition request so that the acquired token is a Proof-of-Possession token (PoP), rather than a Bearer token.
/// PoP tokens are similar to Bearer tokens, but are bound to the HTTP request and to a cryptographic key, which MSAL can manage on Windows.
/// See https://aka.ms/msal-net-pop
/// </summary>
/// <param name="popAuthenticationConfiguration">Configuration properties used to construct a Proof-of-Possession request.</param>
/// <returns>The builder.</returns>
/// <remarks>
/// <list type="bullet">
/// <item><description>An Authentication header is automatically added to the request.</description></item>
/// <item><description>The PoP token is bound to the HTTP request, more specifically to the HTTP method (GET, POST, etc.) and to the Uri (path and query, but not query parameters).</description></item>
/// <item><description>MSAL creates, reads and stores a key in memory that will be cycled every 8 hours.</description></item>
/// <item><description>This is an experimental API. The method signature may change in the future without involving a major version upgrade.</description></item>
/// </list>
/// </remarks>
public T WithSignedHttpRequestProofOfPossession(PoPAuthenticationConfiguration popAuthenticationConfiguration)
{
CommonParameters.PopAuthenticationConfiguration = popAuthenticationConfiguration ?? throw new ArgumentNullException(nameof(popAuthenticationConfiguration));

CommonParameters.AuthenticationOperation = new PopAuthenticationOperation(CommonParameters.PopAuthenticationConfiguration, ServiceBundle);

return this as T;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,18 @@ public AcquireTokenForClientParameterBuilder WithSendX5C(bool withSendX5C)
return this;
}

/// <summary>
/// Specifies that the certificate provided will be used for PoP tokens with MTLS (Mutual TLS) authentication.
gladjohn marked this conversation as resolved.
Show resolved Hide resolved
/// For more information, refer to the documentation at: /// https://aka.ms/mstls-pop
/// </summary>
/// <returns>The current instance of <see cref="AcquireTokenForClientParameterBuilder"/> to enable method chaining.</returns>
public AcquireTokenForClientParameterBuilder WithMtlsProofOfPossession()
{
ValidateUseOfExperimentalFeature();
Parameters.UseMtlsPop = true;
gladjohn marked this conversation as resolved.
Show resolved Hide resolved
return this; // Return the builder to allow method chaining
}

/// <summary>
/// Please use WithAzureRegion on the ConfidentialClientApplicationBuilder object
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Identity.Client.ApiConfig.Parameters;
using Microsoft.Identity.Client.AuthScheme.PoP;
using Microsoft.Identity.Client.Instance.Discovery;
using Microsoft.Identity.Client.Internal;
using Microsoft.Identity.Client.Internal.Requests;
Expand Down Expand Up @@ -63,6 +64,16 @@ public async Task<AuthenticationResult> ExecuteAsync(

requestParams.SendX5C = clientParameters.SendX5C ?? false;

requestParams.UseMtlsPop = clientParameters.UseMtlsPop;

if (requestParams.UseMtlsPop)
{
commonParameters.MtlsCertificate = _confidentialClientApplication.Certificate;
commonParameters.AuthenticationOperation = new MtlsPopAuthenticationOperation(_confidentialClientApplication.Certificate);
ServiceBundle.Config.ClientCredential = null;
ServiceBundle.Config.UseMtlsPop = true;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Avoid having this as the request level and at the app level.

}

var handler = new ClientCredentialRequest(
ServiceBundle,
requestParams,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ internal class AcquireTokenForClientParameters : AbstractAcquireTokenConfidentia
/// </summary>
public bool ForceRefresh { get; set; }

/// <summary>
/// Whether to use MTLS Proof of Possession (PoP)
/// </summary>
public bool UseMtlsPop { get; set; } = false;

/// <inheritdoc/>
public void LogParameters(ILoggerAdapter logger)
{
Expand All @@ -20,6 +25,7 @@ public void LogParameters(ILoggerAdapter logger)
var builder = new StringBuilder();
builder.AppendLine("=== AcquireTokenForClientParameters ===");
builder.AppendLine("SendX5C: " + SendX5C);
builder.AppendLine("UseMtlsPop: " + UseMtlsPop);
builder.AppendLine("ForceRefresh: " + ForceRefresh);
logger.Info(builder.ToString());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -123,8 +123,9 @@ public string ClientVersion
public bool IsPublicClient => !IsConfidentialClient && !IsManagedIdentity;

public Func<AppTokenProviderParameters, Task<AppTokenProviderResult>> AppTokenProvider;
public bool UseMtlsPop { get; internal set; } = false;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should not be needed.


#region ClientCredentials
#region ClientCredentials

public IClientCredential ClientCredential { get; internal set; }

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System;
using System.Collections.Generic;
using System.Globalization;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using Microsoft.Identity.Client.AppConfig;
using Microsoft.Identity.Client.Cache.Items;
using Microsoft.Identity.Client.Internal;
using Microsoft.Identity.Client.OAuth2;
using Microsoft.Identity.Client.Utils;
#if SUPPORTS_SYSTEM_TEXT_JSON
using JObject = System.Text.Json.Nodes.JsonObject;
using JToken = System.Text.Json.Nodes.JsonNode;
#else
using Microsoft.Identity.Json;
using Microsoft.Identity.Json.Linq;
#endif

namespace Microsoft.Identity.Client.AuthScheme.PoP
{
internal class MtlsPopAuthenticationOperation : IAuthenticationOperation
{
private readonly X509Certificate2 _mtlsCert;

public MtlsPopAuthenticationOperation(X509Certificate2 mtlsCert)
{
_mtlsCert = mtlsCert;
KeyId = mtlsCert.Thumbprint;
}

public int TelemetryTokenType => (int)TokenType.Mtls;

public string AuthorizationHeaderPrefix => Constants.MtlsPoPAuthHeaderPrefix;

public string AccessTokenType => Constants.MtlsPoPTokenType;

/// <summary>
/// For MTLS PoP, we use x5t
/// </summary>
public string KeyId { get; }

public IReadOnlyDictionary<string, string> GetTokenRequestParams()
{
return CollectionHelpers.GetEmptyDictionary<string, string>();
}

public void FormatResult(AuthenticationResult authenticationResult)
{
var header = new JObject();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is this for?

header[JsonWebTokenConstants.KeyId] = KeyId;
header[JsonWebTokenConstants.Type] = Constants.MtlsPoPTokenType;

authenticationResult.MtlsCertificate = _mtlsCert;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,12 @@ internal enum TokenType
/// Extension token type.
/// Extension = 5
/// </summary>
Extension = 5
Extension = 5,

/// <summary>
/// MTLS token type.
/// MTLS = 5
/// </summary>
Mtls = 5
}
}
11 changes: 10 additions & 1 deletion src/client/Microsoft.Identity.Client/AuthenticationResult.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using System.ComponentModel;
using System.Globalization;
using System.Security.Claims;
using System.Security.Cryptography.X509Certificates;
using System.Threading.Tasks;
using Microsoft.Identity.Client.AuthScheme;
using Microsoft.Identity.Client.Cache;
Expand Down Expand Up @@ -42,6 +43,7 @@ public partial class AuthenticationResult
/// <param name="claimsPrincipal">Claims from the ID token</param>
/// <param name="spaAuthCode">Auth Code returned by the Microsoft identity platform when you use AcquireTokenByAuthorizationCode.WithSpaAuthorizationCode(). This auth code is meant to be redeemed by the frontend code. See https://aka.ms/msal-net/spa-auth-code</param>
/// <param name="additionalResponseParameters">Other properties from the token response.</param>
/// <param name="mtlsCertificate">MTLS certificate used in the token acquisition, if any. For MTLS Pop you may need to use this while calling the target resource</param>
public AuthenticationResult( // for backwards compat with 4.16-
string accessToken,
bool isExtendedLifeTimeToken,
Expand All @@ -57,7 +59,8 @@ public partial class AuthenticationResult
AuthenticationResultMetadata authenticationResultMetadata = null,
ClaimsPrincipal claimsPrincipal = null,
string spaAuthCode = null,
IReadOnlyDictionary<string, string> additionalResponseParameters = null)
IReadOnlyDictionary<string, string> additionalResponseParameters = null,
X509Certificate2 mtlsCertificate = null)
{
AccessToken = accessToken;
#pragma warning disable CS0618 // Type or member is obsolete
Expand All @@ -76,6 +79,7 @@ public partial class AuthenticationResult
ClaimsPrincipal = claimsPrincipal;
SpaAuthCode = spaAuthCode;
AdditionalResponseParameters = additionalResponseParameters;
MtlsCertificate = mtlsCertificate;
}

/// <summary>
Expand Down Expand Up @@ -315,6 +319,11 @@ internal AuthenticationResult() { }
/// </summary>
public AuthenticationResultMetadata AuthenticationResultMetadata { get; }

/// <summary>
/// Exposes the MTLS certificate used for authentication, if applicable.
/// </summary
public X509Certificate2 MtlsCertificate { get; internal set; }

/// <summary>
/// Creates the content for an HTTP authorization header from this authentication result, so
/// that you can call a protected API
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ internal class RegionDiscoveryProvider : IRegionDiscoveryProvider
{
private readonly IRegionManager _regionManager;
public const string PublicEnvForRegional = "login.microsoft.com";
public const string PublicEnvForMtls = "mtlsauth.microsoft.com";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I recommend you rename this to RegionAndMtlsProvider

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@bgavrilMS but the region is not included in the const?


public RegionDiscoveryProvider(IHttpManager httpManager, bool clearCache)
{
Expand Down Expand Up @@ -56,9 +57,14 @@ private static InstanceDiscoveryMetadataEntry CreateEntry(string originalEnv, st

private static string GetRegionalizedEnvironment(Uri authority, string region, RequestContext requestContext)
{

string host = authority.Host;

if (requestContext.ServiceBundle.Config.UseMtlsPop)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about the case of non-regional + mtls POP config? Do we want to support this or just error out?

{
requestContext.Logger.Info(() => $"[Region discovery] Using MTLS public endpoint: {PublicEnvForMtls}");
return $"{region}.{PublicEnvForMtls}";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is wrong for non public cloud.

}

if (KnownMetadataProvider.IsPublicEnvironment(host))
{
requestContext.Logger.Info(() => $"[Region discovery] Regionalized Environment is : {region}.{PublicEnvForRegional}. ");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,9 @@ internal static class Constants
public static readonly TimeSpan AccessTokenExpirationBuffer = TimeSpan.FromMinutes(5);
public const string EnableSpaAuthCode = "1";
public const string PoPTokenType = "pop";
public const string PoPAuthHeaderPrefix = "PoP";
public const string MtlsPoPTokenType = "mtls_pop";
public const string PoPAuthHeaderPrefix = "PoP";
public const string MtlsPoPAuthHeaderPrefix = "mtls_pop";
public const string RequestConfirmation = "req_cnf";
public const string BearerAuthHeaderPrefix = "Bearer";

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ public AuthenticationRequestParameters(

public AuthorityInfo AuthorityInfo => AuthorityManager.Authority.AuthorityInfo;

public AuthorityInfo AuthorityOverride => _commonParameters.AuthorityOverride;
public AuthorityInfo AuthorityOverride => _commonParameters.AuthorityOverride;

#endregion

Expand Down Expand Up @@ -163,6 +163,12 @@ public string LoginHint
public string LongRunningOboCacheKey { get; set; }

public KeyValuePair<string, string>? CcsRoutingHint { get; set; }

/// <summary>
/// Indicates if MTLS Proof of Possession should be used.
/// </summary>
public bool UseMtlsPop { get; set; } = false;

#endregion

public void LogParameters()
Expand All @@ -185,6 +191,7 @@ public void LogParameters()
builder.AppendLine("ApiId - " + ApiId);
builder.AppendLine("IsConfidentialClient - " + AppConfig.IsConfidentialClient);
builder.AppendLine("SendX5C - " + SendX5C);
builder.AppendLine("UseMtlsPop - " + UseMtlsPop);
builder.AppendLine("LoginHint - " + LoginHint);
builder.AppendLine("IsBrokerConfigured - " + AppConfig.IsBrokerEnabled);
builder.AppendLine("HomeAccountId - " + HomeAccountId);
Expand All @@ -205,6 +212,7 @@ public void LogParameters()
builder.AppendLine("ApiId - " + ApiId);
builder.AppendLine("IsConfidentialClient - " + AppConfig.IsConfidentialClient);
builder.AppendLine("SendX5C - " + SendX5C);
builder.AppendLine("UseMtlsPop - " + UseMtlsPop);
builder.AppendLine("LoginHint ? " + !string.IsNullOrEmpty(LoginHint));
builder.AppendLine("IsBrokerConfigured - " + AppConfig.IsBrokerEnabled);
builder.AppendLine("HomeAccountId - " + !string.IsNullOrEmpty(HomeAccountId));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ private async Task<AuthenticationResult> GetAccessTokenAsync(
// Get a token from AAD
if (ServiceBundle.Config.AppTokenProvider == null)
{
MsalTokenResponse msalTokenResponse = await SendTokenRequestAsync(GetBodyParameters(), cancellationToken).ConfigureAwait(false);
MsalTokenResponse msalTokenResponse = await SendTokenRequestAsync(GetBodyParameters(_clientParameters.UseMtlsPop), cancellationToken).ConfigureAwait(false);
return await CacheTokenResponseAndCreateAuthenticationResultAsync(msalTokenResponse).ConfigureAwait(false);
}

Expand Down Expand Up @@ -224,13 +224,20 @@ protected override SortedSet<string> GetOverriddenScopes(ISet<string> inputScope
return new SortedSet<string>(inputScopes);
}

private Dictionary<string, string> GetBodyParameters()
private Dictionary<string, string> GetBodyParameters(bool useMtlsPop)
{
var dict = new Dictionary<string, string>
{
[OAuth2Parameter.GrantType] = OAuth2GrantType.ClientCredentials,
[OAuth2Parameter.Scope] = AuthenticationRequestParameters.Scope.AsSingleString()
};

// Only add TokenType if useMtlsPop is true
if (useMtlsPop)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's better to do this in MtlsPopAuthenticationOperation.cs

{
dict[OAuth2Parameter.TokenType] = RequestTokenType.MTLSPop;
}

return dict;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,12 @@ internal static class OAuth2Error
public const string AuthorizationPending = "authorization_pending";
}

internal static class RequestTokenType
{
public const string Bearer = "bearer";
public const string MTLSPop = "mtls_pop";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pls consolidate with SHR POP.

}

internal static class OAuth2Value
{
public const string CodeChallengeMethodValue = "S256";
Expand Down
Loading
Loading