Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
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 static string FormatEnterpriseRegistrationOnPremiseUri(string domain)
{
Expand Down
131 changes: 123 additions & 8 deletions src/client/Microsoft.Identity.Client/WwwAuthenticateParameters.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
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;

Expand All @@ -21,6 +22,13 @@ namespace Microsoft.Identity.Client
/// </summary>
public class WwwAuthenticateParameters
{
private static readonly ISet<string> s_knownAuthenticationSchemes = new HashSet<string>(
new[] {
Constants.BearerAuthHeaderPrefix,
Constants.PoPAuthHeaderPrefix
},
StringComparer.OrdinalIgnoreCase);

/// <summary>
/// Resource for which to request scopes.
/// This is the App ID URI of the API that returned the WWW-Authenticate header.
Expand Down Expand Up @@ -50,6 +58,17 @@ public class WwwAuthenticateParameters
/// </summary>
public string Error { get; set; }

/// <summary>
/// AuthScheme.
/// See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/WWW-Authenticate#syntax for more details
/// </summary>
public string AuthScheme { get; set; }
Comment thread
trwalke marked this conversation as resolved.
Outdated

/// <summary>
/// Server Nonce.
Comment thread
trwalke marked this conversation as resolved.
Outdated
/// </summary>
public string ServerNonce { get; set; }
Comment thread
trwalke marked this conversation as resolved.
Outdated

/// <summary>
/// Return the <see cref="RawParameters"/> of key <paramref name="key"/>.
Comment thread
trwalke marked this conversation as resolved.
/// </summary>
Expand Down Expand Up @@ -92,15 +111,41 @@ public static WwwAuthenticateParameters CreateFromResponseHeaders(
{
if (httpResponseHeaders.WwwAuthenticate.Any())
{
// TODO: add POP support
AuthenticationHeaderValue bearer = httpResponseHeaders.WwwAuthenticate.First(v => string.Equals(v.Scheme, scheme, StringComparison.OrdinalIgnoreCase));
AuthenticationHeaderValue bearer = httpResponseHeaders.WwwAuthenticate.First(v => string.Equals(v.Scheme, Constants.BearerAuthHeaderPrefix, StringComparison.OrdinalIgnoreCase));
Comment thread
trwalke marked this conversation as resolved.
Outdated
Comment thread
trwalke marked this conversation as resolved.
Outdated
Comment thread
trwalke marked this conversation as resolved.
Outdated
string wwwAuthenticateValue = bearer.Parameter;
return CreateFromWwwAuthenticateHeaderValue(wwwAuthenticateValue);
var parameters = CreateFromWwwAuthenticateHeaderValue(wwwAuthenticateValue);
parameters.AuthScheme = Constants.BearerAuthHeaderPrefix;
Comment thread
trwalke marked this conversation as resolved.
Outdated
return parameters;
}

return CreateWwwAuthenticateParameters(new Dictionary<string, string>());
}

/// <summary>
/// Create WWW-Authenticate parameters from the HttpResponseHeaders for each auth scheme.
/// </summary>
/// <param name="httpResponseHeaders">HttpResponseHeaders.</param>
/// <returns>The parameters requested by the web API.</returns>
/// <remarks>Currently it only supports the Bearer scheme</remarks>
Comment thread
trwalke marked this conversation as resolved.
public static IEnumerable<WwwAuthenticateParameters> CreateAllFromResponseHeaders(
Comment thread
trwalke marked this conversation as resolved.
Outdated
Comment thread
trwalke marked this conversation as resolved.
Outdated
HttpResponseHeaders httpResponseHeaders)
{
List<WwwAuthenticateParameters> parameterList = new List<WwwAuthenticateParameters>();

foreach (var wwwAuthenticateHeaderValue in httpResponseHeaders.WwwAuthenticate)
{
if (s_knownAuthenticationSchemes.Contains(wwwAuthenticateHeaderValue.Scheme))
{
var parameters = CreateFromWwwAuthenticateHeaderValue(wwwAuthenticateHeaderValue.Parameter);
parameters.AuthScheme = wwwAuthenticateHeaderValue.Scheme;

parameterList.Add(parameters);
}
}

return parameterList;
}

/// <summary>
/// Creates parameters from the WWW-Authenticate string.
/// </summary>
Expand All @@ -112,10 +157,22 @@ public static WwwAuthenticateParameters CreateFromWwwAuthenticateHeaderValue(str
{
throw new ArgumentNullException(nameof(wwwAuthenticateValue));
Comment thread
trwalke marked this conversation as resolved.
Outdated
}
IDictionary<string, string> parameters;

var AuthValuesSplit = wwwAuthenticateValue.Split(new char[] { ' ' }, 2);
Comment thread
trwalke marked this conversation as resolved.
Outdated

IDictionary<string, string> parameters = CoreHelpers.SplitWithQuotes(wwwAuthenticateValue, ',')
.Select(v => ExtractKeyValuePair(v.Trim()))
.ToDictionary(pair => pair.Key, pair => pair.Value, StringComparer.OrdinalIgnoreCase);
if (s_knownAuthenticationSchemes.Contains(AuthValuesSplit[0]))
{
parameters = CoreHelpers.SplitWithQuotes(AuthValuesSplit[1], ',')
.Select(v => ExtractKeyValuePair(v.Trim()))
Comment thread
trwalke marked this conversation as resolved.
Outdated
.ToDictionary(pair => pair.Key, pair => pair.Value, StringComparer.OrdinalIgnoreCase);

} else
{
parameters = CoreHelpers.SplitWithQuotes(wwwAuthenticateValue, ',')
.Select(v => ExtractKeyValuePair(v.Trim()))
.ToDictionary(pair => pair.Key, pair => pair.Value, StringComparer.OrdinalIgnoreCase);
}

return CreateWwwAuthenticateParameters(parameters);
}
Expand All @@ -125,22 +182,49 @@ public static WwwAuthenticateParameters CreateFromWwwAuthenticateHeaderValue(str
/// </summary>
/// <param name="resourceUri">URI of the resource.</param>
/// <returns>WWW-Authenticate Parameters extracted from response to the unauthenticated call.</returns>
[Obsolete]
Comment thread
trwalke marked this conversation as resolved.
Outdated
public static Task<WwwAuthenticateParameters> CreateFromResourceResponseAsync(string resourceUri)
{
return CreateFromResourceResponseAsync(resourceUri, default);
}

/// <summary>
/// Create the authenticate parameters by attempting to call the resource unauthenticated, and analyzing the response.
/// </summary>
/// <param name="resourceUri">URI of the resource.</param>
/// <returns>WWW-Authenticate Parameters extracted from response to the unauthenticated call.</returns>
public static Task<IEnumerable<WwwAuthenticateParameters>> CreateAllFromResourceResponseAsync(string resourceUri)
{
return CreateAllFromResourceResponseAsync(resourceUri, default);
}

/// <summary>
/// Create 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>
/// <returns>WWW-Authenticate Parameters extracted from response to the unauthenticated call.</returns>
[Obsolete]
public static Task<WwwAuthenticateParameters> CreateFromResourceResponseAsync(string resourceUri, CancellationToken cancellationToken = default)
{
return CreateFromResourceResponseAsync(GetHttpClient(), resourceUri, cancellationToken);
}

/// <summary>
/// Create 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>
/// <returns>WWW-Authenticate Parameters extracted from response to the unauthenticated call.</returns>
public static Task<IEnumerable<WwwAuthenticateParameters>> CreateAllFromResourceResponseAsync(string resourceUri, CancellationToken cancellationToken = default)
{
return CreateAllFromResourceResponseAsync(GetHttpClient(), resourceUri, cancellationToken);
}

private static HttpClient GetHttpClient()
{
var httpClientFactory = PlatformProxyFactory.CreatePlatformProxy(null).CreateDefaultHttpClientFactory();
var httpClient = httpClientFactory.GetHttpClient();
return CreateFromResourceResponseAsync(httpClient, resourceUri, cancellationToken);
return httpClientFactory.GetHttpClient();
}

/// <summary>
Expand All @@ -150,6 +234,7 @@ public static Task<WwwAuthenticateParameters> CreateFromResourceResponseAsync(st
/// <param name="resourceUri">URI of the resource.</param>
/// <param name="cancellationToken">The cancellation token to cancel operation.</param>
/// <returns>WWW-Authenticate Parameters extracted from response to the unauthenticated call.</returns>
[Obsolete]
public static async Task<WwwAuthenticateParameters> CreateFromResourceResponseAsync(HttpClient httpClient, string resourceUri, CancellationToken cancellationToken = default)
{
if (httpClient is null)
Expand All @@ -167,6 +252,31 @@ public static async Task<WwwAuthenticateParameters> CreateFromResourceResponseAs
return wwwAuthParam;
}

/// <summary>
/// Create the authenticate parameters by attempting to call the resource unauthenticated, and analyzing the response.
/// </summary>
/// <param name="httpClient">Instance of <see cref="HttpClient"/> to make the request with.</param>
/// <param name="resourceUri">URI of the resource.</param>
/// <param name="cancellationToken">The cancellation token to cancel operation.</param>
/// <param name="scheme">Authentication scheme. Default is Bearer.</param>
/// <returns>WWW-Authenticate Parameters extracted from response to the unauthenticated call.</returns>
public static async Task<IEnumerable<WwwAuthenticateParameters>> CreateAllFromResourceResponseAsync(HttpClient httpClient, string resourceUri, CancellationToken cancellationToken = default, string scheme = "Bearer")
Comment thread
trwalke marked this conversation as resolved.
Outdated
{
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);
var wwwAuthParams = CreateAllFromResponseHeaders(httpResponseMessage.Headers);
return wwwAuthParams;
}

/// <summary>
/// Gets the claim challenge from HTTP header.
/// Used, for example, for CA auth context.
Expand Down Expand Up @@ -231,6 +341,11 @@ internal static WwwAuthenticateParameters CreateWwwAuthenticateParameters(IDicti
wwwAuthenticateParameters.Error = value;
}

if (values.TryGetValue("nonce", out value))
Comment thread
trwalke marked this conversation as resolved.
{
wwwAuthenticateParameters.ServerNonce = value.Replace("\"", string.Empty);
}

return wwwAuthenticateParameters;
}

Expand Down
102 changes: 102 additions & 0 deletions tests/Microsoft.Identity.Test.Unit/WwwAuthenticateParametersTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.Identity.Client;
using Microsoft.Identity.Client.Internal;
using Microsoft.Identity.Test.Common.Core.Mocks;
using Microsoft.VisualStudio.TestTools.UnitTesting;

Expand Down Expand Up @@ -156,6 +157,10 @@ public async Task CreateFromResourceResponseAsync_HttpClient_Arm_GetTenantId_Asy
var authParams = await WwwAuthenticateParameters.CreateFromResourceResponseAsync(httpClient, resourceUri).ConfigureAwait(false);

Assert.AreEqual(authParams.GetTenantId(), tenantId);

Comment thread
trwalke marked this conversation as resolved.
Comment thread
trwalke marked this conversation as resolved.
var authParamList = await WwwAuthenticateParameters.CreateAllFromResourceResponseAsync(httpClient, resourceUri).ConfigureAwait(false);

Assert.AreEqual(authParamList.FirstOrDefault().GetTenantId(), tenantId);
}

[TestMethod]
Expand All @@ -174,6 +179,10 @@ public async Task CreateFromResourceResponseAsync_HttpClient_B2C_GetTenantId_Asy
var authParams = await WwwAuthenticateParameters.CreateFromResourceResponseAsync(httpClient, resourceUri).ConfigureAwait(false);

Assert.AreEqual(authParams.GetTenantId(), tenantId);

var authParamList = await WwwAuthenticateParameters.CreateAllFromResourceResponseAsync(httpClient, resourceUri).ConfigureAwait(false);

Assert.AreEqual(authParamList.FirstOrDefault().GetTenantId(), tenantId);
}

[TestMethod]
Expand All @@ -194,6 +203,10 @@ public async Task CreateFromResourceResponseAsync_HttpClient_ADFS_GetTenantId_Nu
var authParams = await WwwAuthenticateParameters.CreateFromResourceResponseAsync(httpClient, resourceUri).ConfigureAwait(false);

Assert.IsNull(authParams.GetTenantId());

var authParamList = await WwwAuthenticateParameters.CreateAllFromResourceResponseAsync(httpClient, resourceUri).ConfigureAwait(false);

Assert.IsNull(authParamList.FirstOrDefault().GetTenantId());
}

[DataRow(null)]
Expand All @@ -205,13 +218,20 @@ public async Task CreateFromResourceResponseAsync_HttpClient_Null_Async(HttpClie
Func<Task> action = () => WwwAuthenticateParameters.CreateFromResourceResponseAsync(httpClient, resourceUri);

await Assert.ThrowsExceptionAsync<ArgumentNullException>(action).ConfigureAwait(false);

Comment thread
trwalke marked this conversation as resolved.
Func<Task> action2 = () => WwwAuthenticateParameters.CreateAllFromResourceResponseAsync(httpClient, resourceUri);

await Assert.ThrowsExceptionAsync<ArgumentNullException>(action2).ConfigureAwait(false);
}

[TestMethod]
[DataRow(null)]
[DataRow("")]
public async Task CreateFromResourceResponseAsync_Incorrect_ResourceUri_Async(string resourceUri)
{

await WwwAuthenticateParameters.CreateFromResourceResponseAsync("https://manage.office.com/api/v1.0/fbb86d84-7975-4300-a5cb-87b448d6f13d/activity/feed/subscriptions/content?contentType={ContentType}&amp;startTime={0}&amp;endTime={1}").ConfigureAwait(false);

Func<Task> action = () => WwwAuthenticateParameters.CreateFromResourceResponseAsync(resourceUri);

await Assert.ThrowsExceptionAsync<ArgumentNullException>(action).ConfigureAwait(false);
Expand Down Expand Up @@ -253,6 +273,76 @@ public void ExtractClaimChallengeFromHeader_IncorrectError_ReturnNull()
Assert.IsNull(WwwAuthenticateParameters.GetClaimChallengeFromResponseHeaders(httpResponse.Headers));
}

[TestMethod]
public async Task ExtractNonceFromHeaderAsync()
{
//Arrange & Act
var parameterList = await WwwAuthenticateParameters.CreateAllFromResourceResponseAsync(
"https://testingsts.azurewebsites.net/servernonce/invalidsignature",
Comment thread
trwalke marked this conversation as resolved.
Outdated
//"https://manage.office.com/api/v1.0/fbb86d84-7975-4300-a5cb-87b448d6f13d/activity/feed/subscriptions/content?contentType={ContentType}&amp;startTime={0}&amp;endTime={1}",
Comment thread
trwalke marked this conversation as resolved.
Outdated
//"https://management.core.windows.net/<subscription-id>/services/hostedservices/<cloudservice-name>",
//"https://management.azure.com/subscriptions/c1686c51-b717-4fe0-9af3-24a20a41fb0c/resourceGroups/iddp/providers/Microsoft.KeyVault/vaults/iddp?api-version=2021-10-01",
//"https://mkttest0127tipsg908org3.crm10.dynamics.com/api/data/v9.1/",
default).ConfigureAwait(false);

//Assert
Assert.IsTrue(parameterList.FirstOrDefault().AuthScheme == Constants.PoPAuthHeaderPrefix);
Comment thread
trwalke marked this conversation as resolved.
Outdated
Assert.IsNotNull(parameterList.FirstOrDefault().ServerNonce);
}

[TestMethod]
public async Task CreateAllFromResourceResponseAsync_HttpClient_Bearer_Pop_Async()
Comment thread
bgavrilMS marked this conversation as resolved.
{
const string resourceUri = "https://example.com/";
string tenantId = Guid.NewGuid().ToString();

var handler = new MockHttpMessageHandler
{
ExpectedMethod = HttpMethod.Get,
ExpectedUrl = resourceUri,
ResponseMessage = CreateBearerAndPopHttpResponse()
};
var httpClient = new HttpClient(handler);
var headers = await WwwAuthenticateParameters.CreateAllFromResourceResponseAsync(httpClient, resourceUri).ConfigureAwait(false);

var bearerHeader = headers.Where(header => header.AuthScheme == "Bearer").FirstOrDefault();
var popHeader = headers.Where(header => header.AuthScheme == "PoP").FirstOrDefault();

Assert.IsNotNull(bearerHeader);
Assert.AreEqual("https://login.microsoftonline.com/TenantId", bearerHeader.Authority);
Assert.IsNotNull(popHeader);
Assert.AreEqual("someNonce", popHeader.ServerNonce);
}

[TestMethod]
public void ExtractAllParametersFromResponse()
{
// Arrange
HttpResponseMessage httpResponse = CreateBearerAndPopHttpResponse();

// Act & Assert
var headers = WwwAuthenticateParameters.CreateAllFromResponseHeaders(httpResponse.Headers);
var bearerHeader = headers.Where(header => header.AuthScheme == "Bearer").FirstOrDefault();
var popHeader = headers.Where(header => header.AuthScheme == "PoP").FirstOrDefault();

Assert.IsNotNull(bearerHeader);
Assert.AreEqual("https://login.microsoftonline.com/TenantId", bearerHeader.Authority);
Assert.IsNotNull(popHeader);
Assert.AreEqual("someNonce", popHeader.ServerNonce);
}

[TestMethod]
//Test for fix https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/issues/3026
public void ExtractParametersFromResponseWithScheme()
{
//arrange
var header = WwwAuthenticateParameters.CreateFromWwwAuthenticateHeaderValue("Bearer authorization_uri=https://login.microsoftonline.com/TenantId/oauth2/authorize, resource_id=https://endpoint/");

// Act & Assert
Assert.IsNotNull(header);
Assert.AreEqual("https://login.microsoftonline.com/TenantId", header.Authority);
}

private static HttpResponseMessage CreateClaimsHttpResponse(string claims)
{
HttpResponseMessage httpResponse = new HttpResponseMessage(HttpStatusCode.Unauthorized);
Expand Down Expand Up @@ -292,5 +382,17 @@ private static HttpResponseMessage CreateInvalidTokenHttpErrorResponse(string te
}
};
}

private static HttpResponseMessage CreateBearerAndPopHttpResponse()
{
return new HttpResponseMessage(HttpStatusCode.Unauthorized)
{
Headers =
{
{ WwwAuthenticateHeaderName, $"Bearer authorization_uri=\"https://login.microsoftonline.com/TenantId/oauth2/authorize\", resource_id=\"https://endpoint/\"" },
Comment thread
trwalke marked this conversation as resolved.
Comment thread
trwalke marked this conversation as resolved.
{ WwwAuthenticateHeaderName, $"PoP nonce=\"someNonce\""}
}
};
}
}
}