Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
daaa43d
Add new api to gather all wwwAuthenticateParameters
Jun 30, 2022
7108c00
refactor
Jun 30, 2022
4ef405b
Request uri fix
Jul 15, 2022
51f71f1
Add additional tests.
Jul 20, 2022
ff29f70
Rename scheme to authScheme
Jul 20, 2022
e790052
Refactoring api
Aug 2, 2022
bd59b3d
Refactoring.
Aug 3, 2022
cab97ae
Refactoring to use Dictionary return type
Aug 5, 2022
6677c1f
Merge remote-tracking branch 'origin/main' into trwalke/wwwAuthentica…
Sep 26, 2022
37d9c43
Adding Authentication header parser and support for authentication info
Sep 27, 2022
bae9643
Refactoring
Sep 27, 2022
af2f19e
Merge remote-tracking branch 'origin/main' into trwalke/wwwAuthentica…
Sep 30, 2022
eb8032f
Apply suggestions from code review
trwalke Oct 13, 2022
8949f55
Addressing PR Feedback.
Oct 18, 2022
8c46a01
Refactoring, more tests
Oct 18, 2022
e87abff
Build fix
Oct 18, 2022
4f4cc68
Adding additional tests
Oct 19, 2022
264921f
Adding NTLM support
Oct 20, 2022
8fbd0e4
Apply suggestions from code review
trwalke Nov 1, 2022
6a58dac
Update src/client/Microsoft.Identity.Client/WwwAuthenticateParameters.cs
trwalke Nov 1, 2022
9b31775
Update src/client/Microsoft.Identity.Client/WwwAuthenticateParameters.cs
trwalke Nov 1, 2022
b068acc
Recfactoring
Nov 2, 2022
384cd11
Merge remote-tracking branch 'origin/main' into trwalke/wwwAuthentica…
Nov 2, 2022
4b56fb2
resolving build errors
Nov 2, 2022
ddbadf8
resolving tests
Nov 2, 2022
0867daa
Updating parameter parsing
Nov 16, 2022
b55f3c2
Updating parsing logic
Nov 30, 2022
1a08dda
Clean up
Nov 30, 2022
fd6733a
Pr Feedback
Dec 1, 2022
9626ec3
test update
Dec 1, 2022
a1cdedf
Update src/client/Microsoft.Identity.Client/WwwAuthenticateParameters.cs
trwalke Dec 1, 2022
37b1ab7
Adding additional tests
Dec 7, 2022
d6a1e1b
Refactoring tests.
Dec 13, 2022
1e9ce55
Merge remote-tracking branch 'origin/pmaytak/net6-only' into trwalke/…
Dec 13, 2022
c3addbe
Refactoring
Dec 13, 2022
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
148 changes: 148 additions & 0 deletions src/client/Microsoft.Identity.Client/AuthenticationHeaderParser.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Identity.Client.Internal;
using Microsoft.Identity.Client.PlatformsCommon.Factories;
using Microsoft.Identity.Client.Utils;

namespace Microsoft.Identity.Client
{
/// <summary>
/// Parsed authentication headers to retrieve header values from HttpResponseHeaders.
/// </summary>
public class AuthenticationHeaderParser
Comment thread
trwalke marked this conversation as resolved.
{
private static readonly Lazy<IMsalHttpClientFactory> _httpClientFactory = new Lazy<IMsalHttpClientFactory>(
() => PlatformProxyFactory.CreatePlatformProxy(null).CreateDefaultHttpClientFactory());

/// <summary>
/// Parameters returned by the WWW-Authenticate header. This allows for dynamic
/// scenarios such as claim challenge, CAE, CA auth context.
/// See https://aka.ms/msal-net/wwwAuthenticate.
/// </summary>
public IReadOnlyList<WwwAuthenticateParameters> WwwAuthenticateParameters { get; private set; }
Comment thread
trwalke marked this conversation as resolved.

/// <summary>
/// Parameters returned by the Authentication-Info header.
/// This allows for authentication scenarios such as Proof-Of-Possession.
/// </summary>
public AuthenticationInfoParameters AuthenticationInfoParameters { get; private set; }

/// <summary>
/// Nonce parsed from HttpResponseHeaders. This is acquired from with the POP WWW-Authenticate header or the Authetnciation-Info header
/// </summary>
public string PopNonce { get; private set; }

/// <summary>
/// Creates the authenticate parameters by attempting to call the resource unauthenticated, and analyzing the response.
/// </summary>
/// <param name="resourceUri">URI of the resource.</param>
/// <param name="cancellationToken">The cancellation token to cancel the operation.</param>
/// <returns></returns>
public static async Task<AuthenticationHeaderParser> ParseAuthenticationHeadersAsync(string resourceUri, CancellationToken cancellationToken = default)
{
return await ParseAuthenticationHeadersAsync(resourceUri, GetHttpClient(), cancellationToken).ConfigureAwait(false);
}

/// <summary>
/// Creates the authenticate parameters by attempting to call the resource unauthenticated, and analyzing the response.
/// </summary>
/// <param name="resourceUri">URI of the resource.</param>
/// <param name="cancellationToken">The cancellation token to cancel operation.</param>
Comment thread
trwalke marked this conversation as resolved.
/// <param name="httpClient">Instance of <see cref="HttpClient"/> to make the request with.</param>
/// <returns></returns>
/// <exception cref="ArgumentNullException"></exception>
public static async Task<AuthenticationHeaderParser> ParseAuthenticationHeadersAsync(string resourceUri, HttpClient httpClient, CancellationToken cancellationToken = default)
{
if (httpClient is null)
{
throw new ArgumentNullException(nameof(httpClient));
}
if (string.IsNullOrWhiteSpace(resourceUri))
{
throw new ArgumentNullException(nameof(resourceUri));
}

// call this endpoint and see what the header says and return that
HttpResponseMessage httpResponseMessage = await httpClient.GetAsync(resourceUri, cancellationToken).ConfigureAwait(false);

return ParseAuthenticationHeaders(httpResponseMessage.Headers);
}

/// <summary>
/// Creates a parsed set of parameters from the provided HttpResponseHeaders.
/// </summary>
/// <param name="httpResponseHeaders"></param>
/// <remarks>For known values, such as the nonce used for Proof-of-Possession, the parser will first check for it in the WWW-Authenticate headers
/// If it cannot find it, it will then check the Authentication-Info parameters for the value.</remarks>
/// <returns></returns>
public static AuthenticationHeaderParser ParseAuthenticationHeaders(HttpResponseHeaders httpResponseHeaders)
{
AuthenticationHeaderParser authenticationHeaderParser = new AuthenticationHeaderParser();
AuthenticationInfoParameters authenticationInfoParameters = new AuthenticationInfoParameters();
string serverNonce = null;

//Check for WWW-AuthenticateHeaders
if (httpResponseHeaders.WwwAuthenticate.Count != 0)
{
var wwwParameters = Client.WwwAuthenticateParameters.CreateFromAuthenticationHeaders(httpResponseHeaders);

serverNonce = wwwParameters.SingleOrDefault(parameter => string.Equals(parameter.AuthenticationScheme, Constants.PoPAuthHeaderPrefix, StringComparison.Ordinal))?.Nonce;

authenticationHeaderParser.WwwAuthenticateParameters = wwwParameters;
}
else
{
authenticationHeaderParser.WwwAuthenticateParameters = new List<WwwAuthenticateParameters>();

//If no WWW-AuthenticateHeaders exist, attempt to parse AuthenticationInfo headers instead
authenticationInfoParameters = AuthenticationInfoParameters.CreateFromResponseHeaders(httpResponseHeaders);
authenticationHeaderParser.AuthenticationInfoParameters = authenticationInfoParameters;
}

//If server nonce is not acquired from WWW-Authenticate headers, use next nonce from Authentication-Info parameters.
authenticationHeaderParser.PopNonce = serverNonce ?? authenticationInfoParameters.NextNonce;

return authenticationHeaderParser;
}

/// <summary>
/// Creates a HttpClient
/// </summary>
internal static HttpClient GetHttpClient()
{
return _httpClientFactory.Value.GetHttpClient();
}

/// <summary>
/// Creates a key value pair from an expression of the form a=b is possible.
/// Otherwise, the key value pair will be returned as (key:<paramref name="authScheme"/>, value:<paramref name="paramValue"/>)
/// </summary>
/// <param name="paramValue">assignment</param>
/// <param name="authScheme">authScheme</param>
/// <returns>Key Value pair</returns>
internal static KeyValuePair<string, string> CreateKeyValuePair(string paramValue, string authScheme)
{
string[] segments = CoreHelpers.SplitWithQuotes(paramValue, '=')
.Select(s => s.Trim().Trim('"'))
.ToArray();

if (segments.Length < 2)
{
// Extracted assignment isn't of the form a=b. To enable this value to be more easily discoverable, it is set with the auth scheme as the key."
return new KeyValuePair<string, string>(authScheme, paramValue);
}

return new KeyValuePair<string, string>(segments[0], segments[1]);
}
}

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

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http.Headers;
using Microsoft.Identity.Client.Utils;

namespace Microsoft.Identity.Client
{
/// <summary>
/// Parameters returned by the Authentication-Info header. This allows for
/// scenarios such as proof-of-possession, etc.
/// See https://www.rfc-editor.org/rfc/rfc7615
/// </summary>
public class AuthenticationInfoParameters
{
private const string AuthenticationInfoKey = "Authentication-Info";
/// <summary>
/// The next nonce to be used in the preceding authentication request.
/// </summary>
public string NextNonce { get; private set; }

/// <summary>
/// Return the <see cref="RawParameters"/> of key <paramref name="key"/>.
/// </summary>
/// <param name="key">Name of the raw parameter to retrieve.</param>
/// <returns>The raw parameter if it exists,
/// or throws a <see cref="System.Collections.Generic.KeyNotFoundException"/> otherwise.
/// </returns>
public string this[string key]
{
get
{
return RawParameters[key];
}
}

/// <summary>
/// Dictionary of raw parameters in the Authentication-Info header (extracted from the Authentication-Info header
/// string value, without any processing). This allows support for APIs which are not mappable easily to the standard
/// or framework specific (Microsoft.Identity.Model, Microsoft.Identity.Web).
/// </summary>
internal IDictionary<string, string> RawParameters { get; private set; }

/// <summary>
/// Create Authentication-Info parameters from the HttpResponseHeaders for each auth scheme.
/// </summary>
/// <param name="httpResponseHeaders">HttpResponseHeaders.</param>
/// <returns>Authentication-Info provided by the endpoint</returns>
public static AuthenticationInfoParameters CreateFromResponseHeaders(HttpResponseHeaders httpResponseHeaders)
{
AuthenticationInfoParameters parameters = new AuthenticationInfoParameters();

try
{
var authInfoValueList = httpResponseHeaders.SingleOrDefault(header => header.Key == AuthenticationInfoKey).Value;

if (authInfoValueList != null)
{
var authInfoValue = authInfoValueList.FirstOrDefault();
var AuthValuesSplit = authInfoValue.Split(new char[] { ' ' }, 2);
Comment thread
trwalke marked this conversation as resolved.
IDictionary<string, string> paramValues;

if (AuthValuesSplit.Count() != 2)
{
//Header is not in the form of a=b.
paramValues = new Dictionary<string, string>();
paramValues.Add(new KeyValuePair<string, string>(AuthenticationInfoKey, authInfoValue));
}
else
{
paramValues = CoreHelpers.SplitWithQuotes(AuthValuesSplit[1], ',')
.Select(v => AuthenticationHeaderParser.CreateKeyValuePair(v.Trim(), AuthenticationInfoKey))
.ToDictionary(pair => pair.Key, pair => pair.Value, StringComparer.OrdinalIgnoreCase);

if (paramValues.TryGetValue("nextnonce", out string value))
{
parameters.NextNonce = value;
}
}

parameters.RawParameters = paramValues;
}

return parameters;
Comment thread
trwalke marked this conversation as resolved.
}
catch (Exception ex) when (ex is not MsalClientException)
{
throw new MsalClientException(MsalError.UnableToParseAuthenticationHeader, MsalErrorMessage.UnableToParseAuthenticationHeader + $"Response Headers: {httpResponseHeaders.ToString()} See inner exception for details.", ex);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,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 PoPAuthHeaderPrefix = "PoP";
public const string RequestConfirmation = "req_cnf";
public const string BearerAuthHeaderPrefix = "Bearer";

public const string ManagedIdentityClientId = "client_id";
public const string ManagedIdentityResourceId = "mi_res_id";
Expand Down
4 changes: 4 additions & 0 deletions src/client/Microsoft.Identity.Client/MsalError.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1098,6 +1098,10 @@ public static class MsalError
public const string InvalidTokenProviderResponseValue = "invalid_token_provider_response_value";

/// <summary>
/// Msal is unable to parse the authentication header returned from the endpoint. This can be a result of a malformed header returned in either the WWW-Authenticate or the Authentication-Info collections.
/// </summary>
public const string UnableToParseAuthenticationHeader = "unable_to_parse_authentication_header";

/// A required value is missing from the managed identity response.
/// </summary>
public const string InvalidManagedIdentityResponse = "invalid_managed_identity_response";
Expand Down
3 changes: 3 additions & 0 deletions src/client/Microsoft.Identity.Client/MsalErrorMessage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -432,6 +432,9 @@ public static string InitializeProcessSecurityError(string errorCode) =>

public const string RequestFailureErrorMessagePii = "=== Token Acquisition ({0}) failed:\n\tAuthority: {1}\n\tClientId: {2}.";

public const string UnableToParseAuthenticationHeader = "MSAL is unable to parse the authentication header returned from the resource endpoint. This can be a result of a malformed header returned in either the WWW-Authenticate or the Authentication-Info collections acquired from the provided endpoint.";


public static string InvalidTokenProviderResponseValue(string invalidValueName)
{
return string.Format(
Expand Down
Loading