From daaa43d84f031a17ae9865d3c01a0e2b4d626c02 Mon Sep 17 00:00:00 2001 From: trwalke Date: Wed, 29 Jun 2022 22:15:41 -0700 Subject: [PATCH 01/31] Add new api to gather all wwwAuthenticateParameters --- .../Internal/Constants.cs | 3 +- .../WwwAuthenticateParameters.cs | 94 +++++++++++++++++-- .../WwwAuthenticateParametersTests.cs | 37 ++++++++ 3 files changed, 125 insertions(+), 9 deletions(-) diff --git a/src/client/Microsoft.Identity.Client/Internal/Constants.cs b/src/client/Microsoft.Identity.Client/Internal/Constants.cs index dbe0bebdb1..284cfff2d9 100644 --- a/src/client/Microsoft.Identity.Client/Internal/Constants.cs +++ b/src/client/Microsoft.Identity.Client/Internal/Constants.cs @@ -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) { diff --git a/src/client/Microsoft.Identity.Client/WwwAuthenticateParameters.cs b/src/client/Microsoft.Identity.Client/WwwAuthenticateParameters.cs index 7b6012a10d..1fe8fd0e61 100644 --- a/src/client/Microsoft.Identity.Client/WwwAuthenticateParameters.cs +++ b/src/client/Microsoft.Identity.Client/WwwAuthenticateParameters.cs @@ -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; @@ -21,6 +22,14 @@ namespace Microsoft.Identity.Client /// public class WwwAuthenticateParameters { + private static readonly ISet s_knownAuthenticationSchemes = new HashSet(StringComparer.OrdinalIgnoreCase); + + static WwwAuthenticateParameters() + { + s_knownAuthenticationSchemes.Add(Constants.BearerAuthHeaderPrefix); + s_knownAuthenticationSchemes.Add(Constants.PoPAuthHeaderPrefix); + } + /// /// Resource for which to request scopes. /// This is the App ID URI of the API that returned the WWW-Authenticate header. @@ -50,6 +59,16 @@ public class WwwAuthenticateParameters /// public string Error { get; set; } + /// + /// Scheme. + /// + public string Scheme { get; set; } + + /// + /// Server Nonce. + /// + public string ServerNonce { get; set; } + /// /// Return the of key . /// @@ -89,16 +108,47 @@ public string GetTenantId() => Instance.Authority public static WwwAuthenticateParameters CreateFromResponseHeaders( HttpResponseHeaders httpResponseHeaders, string scheme = "Bearer") + { + if (scheme == Constants.BearerAuthHeaderPrefix) + { + return ParseBearerParameters(httpResponseHeaders); + } + + if (scheme == Constants.PoPAuthHeaderPrefix) + { + return ParsePopParameters(httpResponseHeaders); + } + + return CreateWwwAuthenticateParameters(new Dictionary()); + } + + private static WwwAuthenticateParameters ParseBearerParameters(HttpResponseHeaders httpResponseHeaders) { 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)); string wwwAuthenticateValue = bearer.Parameter; - return CreateFromWwwAuthenticateHeaderValue(wwwAuthenticateValue); + var parameters = CreateFromWwwAuthenticateHeaderValue(wwwAuthenticateValue); + parameters.Scheme = Constants.BearerAuthHeaderPrefix; + return parameters; } - return CreateWwwAuthenticateParameters(new Dictionary()); + return null; + } + + private static WwwAuthenticateParameters ParsePopParameters(HttpResponseHeaders httpResponseHeaders) + { + if (httpResponseHeaders.WwwAuthenticate != null) + { + string wwwAuthenticateValue = httpResponseHeaders.WwwAuthenticate.ToString(); + var parameters = CreateFromWwwAuthenticateHeaderValue(wwwAuthenticateValue); + + parameters.Scheme = Constants.PoPAuthHeaderPrefix; + + return parameters; + } + + return null; } /// @@ -130,17 +180,39 @@ public static Task CreateFromResourceResponseAsync(st return CreateFromResourceResponseAsync(resourceUri, default); } + /// + /// Create a list of the known authenticate parameters by attempting to call the resource unauthenticated, and analyzing the response. + /// + /// URI of the resource. + /// The cancellation token to cancel operation. + /// a list of WWW-Authenticate Parameters extracted from a response to the unauthenticated call. + public static async Task> CreateKnownParametersFromResourceResponseAsync(string resourceUri, CancellationToken cancellationToken = default) + { + List parameterList = new List(); + + foreach (string scheme in s_knownAuthenticationSchemes) + { + var parameter = await CreateFromResourceResponseAsync(resourceUri, cancellationToken, scheme).ConfigureAwait(false); + + if (parameter != null && string.IsNullOrEmpty(parameter.Scheme)) + parameterList.Add(parameter); + } + + return parameterList; + } + /// /// Create the authenticate parameters by attempting to call the resource unauthenticated, and analyzing the response. /// /// URI of the resource. /// The cancellation token to cancel operation. + /// Authentication scheme. Default is Bearer. /// WWW-Authenticate Parameters extracted from response to the unauthenticated call. - public static Task CreateFromResourceResponseAsync(string resourceUri, CancellationToken cancellationToken = default) + public static Task CreateFromResourceResponseAsync(string resourceUri, CancellationToken cancellationToken = default, string scheme = "Bearer") { var httpClientFactory = PlatformProxyFactory.CreatePlatformProxy(null).CreateDefaultHttpClientFactory(); var httpClient = httpClientFactory.GetHttpClient(); - return CreateFromResourceResponseAsync(httpClient, resourceUri, cancellationToken); + return CreateFromResourceResponseAsync(httpClient, resourceUri, cancellationToken, scheme); } /// @@ -149,8 +221,9 @@ public static Task CreateFromResourceResponseAsync(st /// Instance of to make the request with. /// URI of the resource. /// The cancellation token to cancel operation. + /// Authentication scheme. Default is Bearer. /// WWW-Authenticate Parameters extracted from response to the unauthenticated call. - public static async Task CreateFromResourceResponseAsync(HttpClient httpClient, string resourceUri, CancellationToken cancellationToken = default) + public static async Task CreateFromResourceResponseAsync(HttpClient httpClient, string resourceUri, CancellationToken cancellationToken = default, string scheme = "Bearer") { if (httpClient is null) { @@ -163,7 +236,7 @@ public static async Task CreateFromResourceResponseAs // call this endpoint and see what the header says and return that HttpResponseMessage httpResponseMessage = await httpClient.GetAsync(resourceUri, cancellationToken).ConfigureAwait(false); - var wwwAuthParam = CreateFromResponseHeaders(httpResponseMessage.Headers); + var wwwAuthParam = CreateFromResponseHeaders(httpResponseMessage.Headers, scheme); return wwwAuthParam; } @@ -231,6 +304,11 @@ internal static WwwAuthenticateParameters CreateWwwAuthenticateParameters(IDicti wwwAuthenticateParameters.Error = value; } + if (values.TryGetValue("WWWAuthenticate: PoP nonce", out value)) + { + wwwAuthenticateParameters.ServerNonce = value.Replace("\"", string.Empty); + } + return wwwAuthenticateParameters; } diff --git a/tests/Microsoft.Identity.Test.Unit/WwwAuthenticateParametersTests.cs b/tests/Microsoft.Identity.Test.Unit/WwwAuthenticateParametersTests.cs index 006d6b8c98..2383c30580 100644 --- a/tests/Microsoft.Identity.Test.Unit/WwwAuthenticateParametersTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/WwwAuthenticateParametersTests.cs @@ -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; @@ -253,6 +254,30 @@ public void ExtractClaimChallengeFromHeader_IncorrectError_ReturnNull() Assert.IsNull(WwwAuthenticateParameters.GetClaimChallengeFromResponseHeaders(httpResponse.Headers)); } + [TestMethod] + public async Task ExtractNonceFromHeaderAsync() + { + //Arrange & Act + WwwAuthenticateParameters parameters = await WwwAuthenticateParameters.CreateFromResourceResponseAsync( + "https://testingsts.azurewebsites.net/servernonce/expired", + default, + Constants.PoPAuthHeaderPrefix).ConfigureAwait(false); + + //Assert + Assert.IsTrue(parameters.Scheme == Constants.PoPAuthHeaderPrefix); + Assert.IsNotNull(parameters.ServerNonce); + } + + //[TestMethod] + //public void ExtractAllParametersFromResponse() + //{ + // // Arrange + // HttpResponseMessage httpResponse = CreateBearerAndPopHttpResponse(); + + // // Act & Assert + // Assert.IsNull(WwwAuthenticateParameters.GetClaimChallengeFromResponseHeaders(httpResponse.Headers)); + //} + private static HttpResponseMessage CreateClaimsHttpResponse(string claims) { HttpResponseMessage httpResponse = new HttpResponseMessage(HttpStatusCode.Unauthorized); @@ -292,5 +317,17 @@ private static HttpResponseMessage CreateInvalidTokenHttpErrorResponse(string te } }; } + + //private static HttpResponseMessage CreateBearerAndPopHttpResponse() + //{ + // return new HttpResponseMessage(HttpStatusCode.Unauthorized) + // { + // Headers = + // { + // { WwwAuthenticateHeaderName, $"Bearer realm=\"\", client_id=\"00000003-0000-0000-c000-000000000000\", authorization_uri=\"https://login.microsoftonline.com/common/oauth2/authorize\", error=\"some_error\", claims=\"{DecodedClaimsHeader}\"" }, + // { WwwAuthenticateHeaderName, $"WWWAuthenticate: PoP nonce=\"\", someNonce"} + // } + // }; + //} } } From 7108c00fd034376fd5932e42bff9d2ced85bf230 Mon Sep 17 00:00:00 2001 From: trwalke Date: Wed, 29 Jun 2022 22:18:04 -0700 Subject: [PATCH 02/31] refactor --- .../Microsoft.Identity.Client/WwwAuthenticateParameters.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/client/Microsoft.Identity.Client/WwwAuthenticateParameters.cs b/src/client/Microsoft.Identity.Client/WwwAuthenticateParameters.cs index 1fe8fd0e61..398578c663 100644 --- a/src/client/Microsoft.Identity.Client/WwwAuthenticateParameters.cs +++ b/src/client/Microsoft.Identity.Client/WwwAuthenticateParameters.cs @@ -195,7 +195,9 @@ public static async Task> CreateKnownPara var parameter = await CreateFromResourceResponseAsync(resourceUri, cancellationToken, scheme).ConfigureAwait(false); if (parameter != null && string.IsNullOrEmpty(parameter.Scheme)) - parameterList.Add(parameter); + { + parameterList.Add(parameter); + } } return parameterList; From 4ef405b22a52b4ad21e1ac026c1277ccc41d207b Mon Sep 17 00:00:00 2001 From: trwalke Date: Thu, 14 Jul 2022 21:10:55 -0700 Subject: [PATCH 03/31] Request uri fix refactoring www auth api --- .../WwwAuthenticateParameters.cs | 187 +++++++++++++----- .../WwwAuthenticateParametersTests.cs | 62 +++--- 2 files changed, 173 insertions(+), 76 deletions(-) diff --git a/src/client/Microsoft.Identity.Client/WwwAuthenticateParameters.cs b/src/client/Microsoft.Identity.Client/WwwAuthenticateParameters.cs index 398578c663..028cfbdfb8 100644 --- a/src/client/Microsoft.Identity.Client/WwwAuthenticateParameters.cs +++ b/src/client/Microsoft.Identity.Client/WwwAuthenticateParameters.cs @@ -22,13 +22,12 @@ namespace Microsoft.Identity.Client /// public class WwwAuthenticateParameters { - private static readonly ISet s_knownAuthenticationSchemes = new HashSet(StringComparer.OrdinalIgnoreCase); - - static WwwAuthenticateParameters() - { - s_knownAuthenticationSchemes.Add(Constants.BearerAuthHeaderPrefix); - s_knownAuthenticationSchemes.Add(Constants.PoPAuthHeaderPrefix); - } + private static readonly ISet s_knownAuthenticationSchemes = new HashSet( + new[] { + Constants.BearerAuthHeaderPrefix, + Constants.PoPAuthHeaderPrefix + }, + StringComparer.OrdinalIgnoreCase); /// /// Resource for which to request scopes. @@ -98,6 +97,28 @@ public string GetTenantId() => Instance.Authority .CreateAuthority(Authority, validateAuthority: true) .TenantId; + ///// + ///// Create list of WWW-Authenticate parameters from known HttpResponseHeaders. + ///// + ///// HttpResponseHeaders. + ///// The parameters requested by the web API. + ///// Currently it only supports the Bearer scheme + //public static IEnumerable CreateFromKnownResponseHeaders(HttpResponseHeaders httpResponseHeaders) + //{ + // List parameterList = new List(); + + // foreach (string scheme in s_knownAuthenticationSchemes) + // { + // WwwAuthenticateParameters parameters = CreateFromResponseHeaders(httpResponseHeaders, scheme); + // if (string.IsNullOrEmpty(parameters.Scheme)) + // { + // parameterList.Add(parameters); + // } + // } + + // return parameterList; + //} + /// /// Create WWW-Authenticate parameters from the HttpResponseHeaders. /// @@ -108,21 +129,6 @@ public string GetTenantId() => Instance.Authority public static WwwAuthenticateParameters CreateFromResponseHeaders( HttpResponseHeaders httpResponseHeaders, string scheme = "Bearer") - { - if (scheme == Constants.BearerAuthHeaderPrefix) - { - return ParseBearerParameters(httpResponseHeaders); - } - - if (scheme == Constants.PoPAuthHeaderPrefix) - { - return ParsePopParameters(httpResponseHeaders); - } - - return CreateWwwAuthenticateParameters(new Dictionary()); - } - - private static WwwAuthenticateParameters ParseBearerParameters(HttpResponseHeaders httpResponseHeaders) { if (httpResponseHeaders.WwwAuthenticate.Any()) { @@ -133,22 +139,32 @@ private static WwwAuthenticateParameters ParseBearerParameters(HttpResponseHeade return parameters; } - return null; + return CreateWwwAuthenticateParameters(new Dictionary()); } - private static WwwAuthenticateParameters ParsePopParameters(HttpResponseHeaders httpResponseHeaders) + /// + /// Create WWW-Authenticate parameters from the HttpResponseHeaders for each auth scheme. + /// + /// HttpResponseHeaders. + /// The parameters requested by the web API. + /// Currently it only supports the Bearer scheme + public static IEnumerable CreateAllFromResponseHeaders( + HttpResponseHeaders httpResponseHeaders) { - if (httpResponseHeaders.WwwAuthenticate != null) - { - string wwwAuthenticateValue = httpResponseHeaders.WwwAuthenticate.ToString(); - var parameters = CreateFromWwwAuthenticateHeaderValue(wwwAuthenticateValue); + List parameterList = new List(); - parameters.Scheme = Constants.PoPAuthHeaderPrefix; + foreach (var wwwAuthenticateHeaderValue in httpResponseHeaders.WwwAuthenticate) + { + if (s_knownAuthenticationSchemes.Contains(wwwAuthenticateHeaderValue.Scheme)) + { + var parameters = CreateFromWwwAuthenticateHeaderValue(wwwAuthenticateHeaderValue.Parameter); + parameters.Scheme = Constants.BearerAuthHeaderPrefix; - return parameters; + parameterList.Add(parameters); + } } - return null; + return parameterList; } /// @@ -162,10 +178,22 @@ public static WwwAuthenticateParameters CreateFromWwwAuthenticateHeaderValue(str { throw new ArgumentNullException(nameof(wwwAuthenticateValue)); } + IDictionary parameters; - IDictionary parameters = CoreHelpers.SplitWithQuotes(wwwAuthenticateValue, ',') - .Select(v => ExtractKeyValuePair(v.Trim())) - .ToDictionary(pair => pair.Key, pair => pair.Value, StringComparer.OrdinalIgnoreCase); + var AuthValuesSplit = wwwAuthenticateValue.Split(new char[] { ' ' }, 2); + + if (s_knownAuthenticationSchemes.Contains(AuthValuesSplit[0])) + { + parameters = CoreHelpers.SplitWithQuotes(AuthValuesSplit[1], ',') + .Select(v => ExtractKeyValuePair(v.Trim())) + .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); } @@ -175,32 +203,55 @@ public static WwwAuthenticateParameters CreateFromWwwAuthenticateHeaderValue(str /// /// URI of the resource. /// WWW-Authenticate Parameters extracted from response to the unauthenticated call. + [Obsolete] public static Task CreateFromResourceResponseAsync(string resourceUri) { return CreateFromResourceResponseAsync(resourceUri, default); } + /// + /// Create the authenticate parameters by attempting to call the resource unauthenticated, and analyzing the response. + /// + /// URI of the resource. + /// WWW-Authenticate Parameters extracted from response to the unauthenticated call. + //public static Task> CreateAllFromResourceResponseAsync(string resourceUri) + //{ + // return CreateFromResourceResponseAsync(resourceUri, default); + //} + /// /// Create a list of the known authenticate parameters by attempting to call the resource unauthenticated, and analyzing the response. /// /// URI of the resource. /// The cancellation token to cancel operation. /// a list of WWW-Authenticate Parameters extracted from a response to the unauthenticated call. - public static async Task> CreateKnownParametersFromResourceResponseAsync(string resourceUri, CancellationToken cancellationToken = default) - { - List parameterList = new List(); + //public static async Task> CreateAllParametersFromResourceResponseAsync(string resourceUri, CancellationToken cancellationToken = default) + //{ + // List parameterList = new List(); - foreach (string scheme in s_knownAuthenticationSchemes) - { - var parameter = await CreateFromResourceResponseAsync(resourceUri, cancellationToken, scheme).ConfigureAwait(false); + // foreach (string scheme in s_knownAuthenticationSchemes) + // { + // var parameter = await CreateFromResourceResponseAsync(resourceUri, cancellationToken, scheme).ConfigureAwait(false); - if (parameter != null && string.IsNullOrEmpty(parameter.Scheme)) - { - parameterList.Add(parameter); - } - } + // if (parameter != null && string.IsNullOrEmpty(parameter.Scheme)) + // { + // parameterList.Add(parameter); + // } + // } - return parameterList; + // return parameterList; + //} + + /// + /// Create the authenticate parameters by attempting to call the resource unauthenticated, and analyzing the response. + /// + /// URI of the resource. + /// The cancellation token to cancel operation. + /// WWW-Authenticate Parameters extracted from response to the unauthenticated call. + [Obsolete] + public static Task CreateFromResourceResponseAsync(string resourceUri, CancellationToken cancellationToken = default) + { + return CreateFromResourceResponseAsync(GetHttpClient(), resourceUri, cancellationToken); } /// @@ -208,13 +259,16 @@ public static async Task> CreateKnownPara /// /// URI of the resource. /// The cancellation token to cancel operation. - /// Authentication scheme. Default is Bearer. /// WWW-Authenticate Parameters extracted from response to the unauthenticated call. - public static Task CreateFromResourceResponseAsync(string resourceUri, CancellationToken cancellationToken = default, string scheme = "Bearer") + public static Task> 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, scheme); + return httpClientFactory.GetHttpClient(); } /// @@ -223,9 +277,9 @@ public static Task CreateFromResourceResponseAsync(st /// Instance of to make the request with. /// URI of the resource. /// The cancellation token to cancel operation. - /// Authentication scheme. Default is Bearer. /// WWW-Authenticate Parameters extracted from response to the unauthenticated call. - public static async Task CreateFromResourceResponseAsync(HttpClient httpClient, string resourceUri, CancellationToken cancellationToken = default, string scheme = "Bearer") + [Obsolete] + public static async Task CreateFromResourceResponseAsync(HttpClient httpClient, string resourceUri, CancellationToken cancellationToken = default) { if (httpClient is null) { @@ -238,10 +292,35 @@ public static async Task CreateFromResourceResponseAs // call this endpoint and see what the header says and return that HttpResponseMessage httpResponseMessage = await httpClient.GetAsync(resourceUri, cancellationToken).ConfigureAwait(false); - var wwwAuthParam = CreateFromResponseHeaders(httpResponseMessage.Headers, scheme); + var wwwAuthParam = CreateFromResponseHeaders(httpResponseMessage.Headers); return wwwAuthParam; } + /// + /// Create the authenticate parameters by attempting to call the resource unauthenticated, and analyzing the response. + /// + /// Instance of to make the request with. + /// URI of the resource. + /// The cancellation token to cancel operation. + /// Authentication scheme. Default is Bearer. + /// WWW-Authenticate Parameters extracted from response to the unauthenticated call. + public static async Task> CreateAllFromResourceResponseAsync(HttpClient httpClient, string resourceUri, CancellationToken cancellationToken = default, string scheme = "Bearer") + { + 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; + } + /// /// Gets the claim challenge from HTTP header. /// Used, for example, for CA auth context. @@ -306,7 +385,7 @@ internal static WwwAuthenticateParameters CreateWwwAuthenticateParameters(IDicti wwwAuthenticateParameters.Error = value; } - if (values.TryGetValue("WWWAuthenticate: PoP nonce", out value)) + if (values.TryGetValue("nonce", out value)) { wwwAuthenticateParameters.ServerNonce = value.Replace("\"", string.Empty); } diff --git a/tests/Microsoft.Identity.Test.Unit/WwwAuthenticateParametersTests.cs b/tests/Microsoft.Identity.Test.Unit/WwwAuthenticateParametersTests.cs index 2383c30580..247e08baa4 100644 --- a/tests/Microsoft.Identity.Test.Unit/WwwAuthenticateParametersTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/WwwAuthenticateParametersTests.cs @@ -213,6 +213,9 @@ public async Task CreateFromResourceResponseAsync_HttpClient_Null_Async(HttpClie [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}&startTime={0}&endTime={1}").ConfigureAwait(false); + Func action = () => WwwAuthenticateParameters.CreateFromResourceResponseAsync(resourceUri); await Assert.ThrowsExceptionAsync(action).ConfigureAwait(false); @@ -259,24 +262,39 @@ public async Task ExtractNonceFromHeaderAsync() { //Arrange & Act WwwAuthenticateParameters parameters = await WwwAuthenticateParameters.CreateFromResourceResponseAsync( - "https://testingsts.azurewebsites.net/servernonce/expired", - default, - Constants.PoPAuthHeaderPrefix).ConfigureAwait(false); + "https://testingsts.azurewebsites.net/servernonce/invalidsignature", + //"https://manage.office.com/api/v1.0/fbb86d84-7975-4300-a5cb-87b448d6f13d/activity/feed/subscriptions/content?contentType={ContentType}&startTime={0}&endTime={1}", + //"https://management.core.windows.net//services/hostedservices/", + //"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(parameters.Scheme == Constants.PoPAuthHeaderPrefix); Assert.IsNotNull(parameters.ServerNonce); } - //[TestMethod] - //public void ExtractAllParametersFromResponse() - //{ - // // Arrange - // HttpResponseMessage httpResponse = CreateBearerAndPopHttpResponse(); + [TestMethod] + public void ExtractAllParametersFromResponse() + { + // Arrange + HttpResponseMessage httpResponse = CreateBearerAndPopHttpResponse(); + + // Act & Assert + Assert.IsNull(WwwAuthenticateParameters.GetClaimChallengeFromResponseHeaders(httpResponse.Headers)); + } + + [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.IsNull(WwwAuthenticateParameters.GetClaimChallengeFromResponseHeaders(httpResponse.Headers)); - //} + // Act & Assert + Assert.IsNotNull(header); + Assert.AreEqual("https://login.microsoftonline.com/TenantId", header.Authority); + } private static HttpResponseMessage CreateClaimsHttpResponse(string claims) { @@ -318,16 +336,16 @@ private static HttpResponseMessage CreateInvalidTokenHttpErrorResponse(string te }; } - //private static HttpResponseMessage CreateBearerAndPopHttpResponse() - //{ - // return new HttpResponseMessage(HttpStatusCode.Unauthorized) - // { - // Headers = - // { - // { WwwAuthenticateHeaderName, $"Bearer realm=\"\", client_id=\"00000003-0000-0000-c000-000000000000\", authorization_uri=\"https://login.microsoftonline.com/common/oauth2/authorize\", error=\"some_error\", claims=\"{DecodedClaimsHeader}\"" }, - // { WwwAuthenticateHeaderName, $"WWWAuthenticate: PoP nonce=\"\", someNonce"} - // } - // }; - //} + 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/\"" }, + { WwwAuthenticateHeaderName, $"PoP nonce=\"someNonce\""} + } + }; + } } } From 51f71f1cadeeb97ca43367da39a9cadb17c2b45e Mon Sep 17 00:00:00 2001 From: trwalke Date: Wed, 20 Jul 2022 09:27:58 -0700 Subject: [PATCH 04/31] Add additional tests. Clean Up --- .../WwwAuthenticateParameters.cs | 55 ++----------------- .../WwwAuthenticateParametersTests.cs | 55 +++++++++++++++++-- 2 files changed, 56 insertions(+), 54 deletions(-) diff --git a/src/client/Microsoft.Identity.Client/WwwAuthenticateParameters.cs b/src/client/Microsoft.Identity.Client/WwwAuthenticateParameters.cs index 028cfbdfb8..0f2335e912 100644 --- a/src/client/Microsoft.Identity.Client/WwwAuthenticateParameters.cs +++ b/src/client/Microsoft.Identity.Client/WwwAuthenticateParameters.cs @@ -97,28 +97,6 @@ public string GetTenantId() => Instance.Authority .CreateAuthority(Authority, validateAuthority: true) .TenantId; - ///// - ///// Create list of WWW-Authenticate parameters from known HttpResponseHeaders. - ///// - ///// HttpResponseHeaders. - ///// The parameters requested by the web API. - ///// Currently it only supports the Bearer scheme - //public static IEnumerable CreateFromKnownResponseHeaders(HttpResponseHeaders httpResponseHeaders) - //{ - // List parameterList = new List(); - - // foreach (string scheme in s_knownAuthenticationSchemes) - // { - // WwwAuthenticateParameters parameters = CreateFromResponseHeaders(httpResponseHeaders, scheme); - // if (string.IsNullOrEmpty(parameters.Scheme)) - // { - // parameterList.Add(parameters); - // } - // } - - // return parameterList; - //} - /// /// Create WWW-Authenticate parameters from the HttpResponseHeaders. /// @@ -158,7 +136,7 @@ public static IEnumerable CreateAllFromResponseHeader if (s_knownAuthenticationSchemes.Contains(wwwAuthenticateHeaderValue.Scheme)) { var parameters = CreateFromWwwAuthenticateHeaderValue(wwwAuthenticateHeaderValue.Parameter); - parameters.Scheme = Constants.BearerAuthHeaderPrefix; + parameters.Scheme = wwwAuthenticateHeaderValue.Scheme; parameterList.Add(parameters); } @@ -214,33 +192,10 @@ public static Task CreateFromResourceResponseAsync(st /// /// URI of the resource. /// WWW-Authenticate Parameters extracted from response to the unauthenticated call. - //public static Task> CreateAllFromResourceResponseAsync(string resourceUri) - //{ - // return CreateFromResourceResponseAsync(resourceUri, default); - //} - - /// - /// Create a list of the known authenticate parameters by attempting to call the resource unauthenticated, and analyzing the response. - /// - /// URI of the resource. - /// The cancellation token to cancel operation. - /// a list of WWW-Authenticate Parameters extracted from a response to the unauthenticated call. - //public static async Task> CreateAllParametersFromResourceResponseAsync(string resourceUri, CancellationToken cancellationToken = default) - //{ - // List parameterList = new List(); - - // foreach (string scheme in s_knownAuthenticationSchemes) - // { - // var parameter = await CreateFromResourceResponseAsync(resourceUri, cancellationToken, scheme).ConfigureAwait(false); - - // if (parameter != null && string.IsNullOrEmpty(parameter.Scheme)) - // { - // parameterList.Add(parameter); - // } - // } - - // return parameterList; - //} + public static Task> CreateAllFromResourceResponseAsync(string resourceUri) + { + return CreateAllFromResourceResponseAsync(resourceUri, default); + } /// /// Create the authenticate parameters by attempting to call the resource unauthenticated, and analyzing the response. diff --git a/tests/Microsoft.Identity.Test.Unit/WwwAuthenticateParametersTests.cs b/tests/Microsoft.Identity.Test.Unit/WwwAuthenticateParametersTests.cs index 247e08baa4..3aa371d9ce 100644 --- a/tests/Microsoft.Identity.Test.Unit/WwwAuthenticateParametersTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/WwwAuthenticateParametersTests.cs @@ -157,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); + + var authParamList = await WwwAuthenticateParameters.CreateAllFromResourceResponseAsync(httpClient, resourceUri).ConfigureAwait(false); + + Assert.AreEqual(authParamList.FirstOrDefault().GetTenantId(), tenantId); } [TestMethod] @@ -175,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] @@ -195,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)] @@ -206,6 +218,10 @@ public async Task CreateFromResourceResponseAsync_HttpClient_Null_Async(HttpClie Func action = () => WwwAuthenticateParameters.CreateFromResourceResponseAsync(httpClient, resourceUri); await Assert.ThrowsExceptionAsync(action).ConfigureAwait(false); + + Func action2 = () => WwwAuthenticateParameters.CreateAllFromResourceResponseAsync(httpClient, resourceUri); + + await Assert.ThrowsExceptionAsync(action2).ConfigureAwait(false); } [TestMethod] @@ -261,7 +277,7 @@ public void ExtractClaimChallengeFromHeader_IncorrectError_ReturnNull() public async Task ExtractNonceFromHeaderAsync() { //Arrange & Act - WwwAuthenticateParameters parameters = await WwwAuthenticateParameters.CreateFromResourceResponseAsync( + var parameterList = await WwwAuthenticateParameters.CreateAllFromResourceResponseAsync( "https://testingsts.azurewebsites.net/servernonce/invalidsignature", //"https://manage.office.com/api/v1.0/fbb86d84-7975-4300-a5cb-87b448d6f13d/activity/feed/subscriptions/content?contentType={ContentType}&startTime={0}&endTime={1}", //"https://management.core.windows.net//services/hostedservices/", @@ -270,8 +286,32 @@ public async Task ExtractNonceFromHeaderAsync() default).ConfigureAwait(false); //Assert - Assert.IsTrue(parameters.Scheme == Constants.PoPAuthHeaderPrefix); - Assert.IsNotNull(parameters.ServerNonce); + Assert.IsTrue(parameterList.FirstOrDefault().Scheme == Constants.PoPAuthHeaderPrefix); + Assert.IsNotNull(parameterList.FirstOrDefault().ServerNonce); + } + + [TestMethod] + public async Task CreateAllFromResourceResponseAsync_HttpClient_Bearer_Pop_Async() + { + 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.Scheme == "Bearer").FirstOrDefault(); + var popHeader = headers.Where(header => header.Scheme == "PoP").FirstOrDefault(); + + Assert.IsNotNull(bearerHeader); + Assert.AreEqual("https://login.microsoftonline.com/TenantId", bearerHeader.Authority); + Assert.IsNotNull(popHeader); + Assert.AreEqual("someNonce", popHeader.ServerNonce); } [TestMethod] @@ -281,7 +321,14 @@ public void ExtractAllParametersFromResponse() HttpResponseMessage httpResponse = CreateBearerAndPopHttpResponse(); // Act & Assert - Assert.IsNull(WwwAuthenticateParameters.GetClaimChallengeFromResponseHeaders(httpResponse.Headers)); + var headers = WwwAuthenticateParameters.CreateAllFromResponseHeaders(httpResponse.Headers); + var bearerHeader = headers.Where(header => header.Scheme == "Bearer").FirstOrDefault(); + var popHeader = headers.Where(header => header.Scheme == "PoP").FirstOrDefault(); + + Assert.IsNotNull(bearerHeader); + Assert.AreEqual("https://login.microsoftonline.com/TenantId", bearerHeader.Authority); + Assert.IsNotNull(popHeader); + Assert.AreEqual("someNonce", popHeader.ServerNonce); } [TestMethod] From ff29f709c089afbb1a1a226a425761e180b210d1 Mon Sep 17 00:00:00 2001 From: trwalke Date: Wed, 20 Jul 2022 09:32:17 -0700 Subject: [PATCH 05/31] Rename scheme to authScheme --- .../WwwAuthenticateParameters.cs | 9 +++++---- .../WwwAuthenticateParametersTests.cs | 10 +++++----- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/client/Microsoft.Identity.Client/WwwAuthenticateParameters.cs b/src/client/Microsoft.Identity.Client/WwwAuthenticateParameters.cs index 0f2335e912..54e5f0c8f8 100644 --- a/src/client/Microsoft.Identity.Client/WwwAuthenticateParameters.cs +++ b/src/client/Microsoft.Identity.Client/WwwAuthenticateParameters.cs @@ -59,9 +59,10 @@ public class WwwAuthenticateParameters public string Error { get; set; } /// - /// Scheme. + /// AuthScheme. + /// See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/WWW-Authenticate#syntax for more details /// - public string Scheme { get; set; } + public string AuthScheme { get; set; } /// /// Server Nonce. @@ -113,7 +114,7 @@ public static WwwAuthenticateParameters CreateFromResponseHeaders( AuthenticationHeaderValue bearer = httpResponseHeaders.WwwAuthenticate.First(v => string.Equals(v.Scheme, Constants.BearerAuthHeaderPrefix, StringComparison.OrdinalIgnoreCase)); string wwwAuthenticateValue = bearer.Parameter; var parameters = CreateFromWwwAuthenticateHeaderValue(wwwAuthenticateValue); - parameters.Scheme = Constants.BearerAuthHeaderPrefix; + parameters.AuthScheme = Constants.BearerAuthHeaderPrefix; return parameters; } @@ -136,7 +137,7 @@ public static IEnumerable CreateAllFromResponseHeader if (s_knownAuthenticationSchemes.Contains(wwwAuthenticateHeaderValue.Scheme)) { var parameters = CreateFromWwwAuthenticateHeaderValue(wwwAuthenticateHeaderValue.Parameter); - parameters.Scheme = wwwAuthenticateHeaderValue.Scheme; + parameters.AuthScheme = wwwAuthenticateHeaderValue.Scheme; parameterList.Add(parameters); } diff --git a/tests/Microsoft.Identity.Test.Unit/WwwAuthenticateParametersTests.cs b/tests/Microsoft.Identity.Test.Unit/WwwAuthenticateParametersTests.cs index 3aa371d9ce..8b01badd0d 100644 --- a/tests/Microsoft.Identity.Test.Unit/WwwAuthenticateParametersTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/WwwAuthenticateParametersTests.cs @@ -286,7 +286,7 @@ public async Task ExtractNonceFromHeaderAsync() default).ConfigureAwait(false); //Assert - Assert.IsTrue(parameterList.FirstOrDefault().Scheme == Constants.PoPAuthHeaderPrefix); + Assert.IsTrue(parameterList.FirstOrDefault().AuthScheme == Constants.PoPAuthHeaderPrefix); Assert.IsNotNull(parameterList.FirstOrDefault().ServerNonce); } @@ -305,8 +305,8 @@ public async Task CreateAllFromResourceResponseAsync_HttpClient_Bearer_Pop_Async var httpClient = new HttpClient(handler); var headers = await WwwAuthenticateParameters.CreateAllFromResourceResponseAsync(httpClient, resourceUri).ConfigureAwait(false); - var bearerHeader = headers.Where(header => header.Scheme == "Bearer").FirstOrDefault(); - var popHeader = headers.Where(header => header.Scheme == "PoP").FirstOrDefault(); + 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); @@ -322,8 +322,8 @@ public void ExtractAllParametersFromResponse() // Act & Assert var headers = WwwAuthenticateParameters.CreateAllFromResponseHeaders(httpResponse.Headers); - var bearerHeader = headers.Where(header => header.Scheme == "Bearer").FirstOrDefault(); - var popHeader = headers.Where(header => header.Scheme == "PoP").FirstOrDefault(); + 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); From e790052dbb274316d02cd19434fb7933eba34cd1 Mon Sep 17 00:00:00 2001 From: trwalke Date: Tue, 2 Aug 2022 15:57:18 -0700 Subject: [PATCH 06/31] Refactoring api --- .../WwwAuthenticateParameters.cs | 281 +++++++++++------- .../WwwAuthenticateParametersTests.cs | 42 ++- 2 files changed, 208 insertions(+), 115 deletions(-) diff --git a/src/client/Microsoft.Identity.Client/WwwAuthenticateParameters.cs b/src/client/Microsoft.Identity.Client/WwwAuthenticateParameters.cs index 54e5f0c8f8..e1d34187f8 100644 --- a/src/client/Microsoft.Identity.Client/WwwAuthenticateParameters.cs +++ b/src/client/Microsoft.Identity.Client/WwwAuthenticateParameters.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.ComponentModel; using System.Linq; using System.Net.Http; using System.Net.Http.Headers; @@ -97,6 +98,54 @@ public string this[string key] public string GetTenantId() => Instance.Authority .CreateAuthority(Authority, validateAuthority: true) .TenantId; + #region Obsolete Api + /// + /// Create the authenticate parameters by attempting to call the resource unauthenticated, and analyzing the response. + /// + /// URI of the resource. + /// WWW-Authenticate Parameters extracted from response to the unauthenticated call. + [System.ComponentModel.EditorBrowsable(EditorBrowsableState.Never)] + public static Task CreateFromResourceResponseAsync(string resourceUri) + { + return CreateFromResourceResponseAsync(resourceUri, default); + } + + /// + /// Create the authenticate parameters by attempting to call the resource unauthenticated, and analyzing the response. + /// + /// URI of the resource. + /// The cancellation token to cancel operation. + /// WWW-Authenticate Parameters extracted from response to the unauthenticated call. + [System.ComponentModel.EditorBrowsable(EditorBrowsableState.Never)] + public static Task CreateFromResourceResponseAsync(string resourceUri, CancellationToken cancellationToken = default) + { + return CreateFromResourceResponseAsync(GetHttpClient(), resourceUri, cancellationToken); + } + + /// + /// Create the authenticate parameters by attempting to call the resource unauthenticated, and analyzing the response. + /// + /// Instance of to make the request with. + /// URI of the resource. + /// The cancellation token to cancel operation. + /// WWW-Authenticate Parameters extracted from response to the unauthenticated call. + [System.ComponentModel.EditorBrowsable(EditorBrowsableState.Never)] + public static async Task CreateFromResourceResponseAsync(HttpClient httpClient, string resourceUri, 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); + var wwwAuthParam = CreateFromResponseHeaders(httpResponseMessage.Headers); + return wwwAuthParam; + } /// /// Create WWW-Authenticate parameters from the HttpResponseHeaders. @@ -105,6 +154,7 @@ public string GetTenantId() => Instance.Authority /// Authentication scheme. Default is "Bearer". /// The parameters requested by the web API. /// Currently it only supports the Bearer scheme + [System.ComponentModel.EditorBrowsable(EditorBrowsableState.Never)] // hide confidential client on mobile public static WwwAuthenticateParameters CreateFromResponseHeaders( HttpResponseHeaders httpResponseHeaders, string scheme = "Bearer") @@ -120,94 +170,118 @@ public static WwwAuthenticateParameters CreateFromResponseHeaders( return CreateWwwAuthenticateParameters(new Dictionary()); } + #endregion Obsolete Api + #region Single Scheme Api /// - /// Create WWW-Authenticate parameters from the HttpResponseHeaders for each auth scheme. + /// Create the authenticate parameters by attempting to call the resource unauthenticated, and analyzing the response. /// - /// HttpResponseHeaders. - /// The parameters requested by the web API. - /// Currently it only supports the Bearer scheme - public static IEnumerable CreateAllFromResponseHeaders( - HttpResponseHeaders httpResponseHeaders) + /// URI of the resource. + /// Authentication scheme. + /// WWW-Authenticate Parameters extracted from response to the unauthenticated call. + public static Task CreateFromAuthenticationResponseAsync(string resourceUri, string scheme) { - List parameterList = new List(); - - 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 CreateFromAuthenticationResponseAsync(resourceUri, scheme, default); + } - return parameterList; + /// + /// Create the authenticate parameters by attempting to call the resource unauthenticated, and analyzing the response. + /// + /// URI of the resource. + /// Authentication scheme. + /// The cancellation token to cancel operation. + /// WWW-Authenticate Parameters extracted from response to the unauthenticated call. + public static Task CreateFromAuthenticationResponseAsync(string resourceUri, string scheme, CancellationToken cancellationToken = default) + { + return CreateFromAuthenticationResponseAsync(GetHttpClient(), resourceUri, scheme, cancellationToken); } /// - /// Creates parameters from the WWW-Authenticate string. + /// Create the authenticate parameters by attempting to call the resource unauthenticated, and analyzing the response. /// - /// String contained in a WWW-Authenticate header. - /// The parameters requested by the web API. - public static WwwAuthenticateParameters CreateFromWwwAuthenticateHeaderValue(string wwwAuthenticateValue) + /// Instance of to make the request with. + /// URI of the resource. + /// The cancellation token to cancel operation. + /// Authentication scheme. + /// WWW-Authenticate Parameters extracted from response to the unauthenticated call. + public static async Task CreateFromAuthenticationResponseAsync(HttpClient httpClient, string resourceUri, string scheme, CancellationToken cancellationToken = default) { - if (string.IsNullOrWhiteSpace(wwwAuthenticateValue)) + if (httpClient is null) { - throw new ArgumentNullException(nameof(wwwAuthenticateValue)); + throw new ArgumentNullException(nameof(httpClient)); } - IDictionary parameters; - - var AuthValuesSplit = wwwAuthenticateValue.Split(new char[] { ' ' }, 2); - - if (s_knownAuthenticationSchemes.Contains(AuthValuesSplit[0])) - { - parameters = CoreHelpers.SplitWithQuotes(AuthValuesSplit[1], ',') - .Select(v => ExtractKeyValuePair(v.Trim())) - .ToDictionary(pair => pair.Key, pair => pair.Value, StringComparer.OrdinalIgnoreCase); - - } else + if (string.IsNullOrWhiteSpace(resourceUri)) { - parameters = CoreHelpers.SplitWithQuotes(wwwAuthenticateValue, ',') - .Select(v => ExtractKeyValuePair(v.Trim())) - .ToDictionary(pair => pair.Key, pair => pair.Value, StringComparer.OrdinalIgnoreCase); + throw new ArgumentNullException(nameof(resourceUri)); } - return CreateWwwAuthenticateParameters(parameters); + // call this endpoint and see what the header says and return that + HttpResponseMessage httpResponseMessage = await httpClient.GetAsync(resourceUri, cancellationToken).ConfigureAwait(false); + var wwwAuthParams = CreateFromAuthenticateHeaders(httpResponseMessage.Headers, scheme); + return wwwAuthParams; } /// - /// Create the authenticate parameters by attempting to call the resource unauthenticated, and analyzing the response. + /// Create WWW-Authenticate parameters from the HttpResponseHeaders. /// - /// URI of the resource. - /// WWW-Authenticate Parameters extracted from response to the unauthenticated call. - [Obsolete] - public static Task CreateFromResourceResponseAsync(string resourceUri) + /// HttpResponseHeaders. + /// Authentication scheme. + /// The parameters requested by the web API. + /// Currently it only supports the Bearer scheme + public static WwwAuthenticateParameters CreateFromAuthenticateHeaders( + HttpResponseHeaders httpResponseHeaders, + string scheme) { - return CreateFromResourceResponseAsync(resourceUri, default); + if (httpResponseHeaders.WwwAuthenticate.Any(v => string.Equals(v.Scheme, scheme, StringComparison.OrdinalIgnoreCase))) + { + AuthenticationHeaderValue header = httpResponseHeaders.WwwAuthenticate.First(v => string.Equals(v.Scheme, Constants.BearerAuthHeaderPrefix, StringComparison.OrdinalIgnoreCase)); + + string wwwAuthenticateValue = header.Parameter; + var parameters = CreateFromWwwAuthenticateHeaderValue(wwwAuthenticateValue); + parameters.AuthScheme = scheme; + + return parameters; + } + + return CreateWwwAuthenticateParameters(new Dictionary()); } /// - /// Create the authenticate parameters by attempting to call the resource unauthenticated, and analyzing the response. + /// Gets the claim challenge from HTTP header. + /// Used, for example, for CA auth context. /// - /// URI of the resource. - /// WWW-Authenticate Parameters extracted from response to the unauthenticated call. - public static Task> CreateAllFromResourceResponseAsync(string resourceUri) + /// The HTTP response headers. + /// Authentication scheme. Default is Bearer. + /// + public static string GetClaimChallengeFromResponseHeaders( + HttpResponseHeaders httpResponseHeaders, + string scheme = "Bearer") { - return CreateAllFromResourceResponseAsync(resourceUri, default); + WwwAuthenticateParameters parameters = CreateFromResponseHeaders( + httpResponseHeaders, + scheme); + + // read the header and checks if it contains an error with insufficient_claims value. + if (parameters.Claims != null && + string.Equals(parameters.Error, "insufficient_claims", StringComparison.OrdinalIgnoreCase)) + { + return parameters.Claims; + } + + return null; } + #endregion Single Scheme Api + + #region Multi Scheme Api /// /// Create the authenticate parameters by attempting to call the resource unauthenticated, and analyzing the response. /// /// URI of the resource. - /// The cancellation token to cancel operation. /// WWW-Authenticate Parameters extracted from response to the unauthenticated call. - [Obsolete] - public static Task CreateFromResourceResponseAsync(string resourceUri, CancellationToken cancellationToken = default) + public static Task> CreateFromAuthenticationResponseAsync(string resourceUri) { - return CreateFromResourceResponseAsync(GetHttpClient(), resourceUri, cancellationToken); + return CreateFromAuthenticationResponseAsync(resourceUri, new CancellationToken()); } /// @@ -216,15 +290,9 @@ public static Task CreateFromResourceResponseAsync(st /// URI of the resource. /// The cancellation token to cancel operation. /// WWW-Authenticate Parameters extracted from response to the unauthenticated call. - public static Task> CreateAllFromResourceResponseAsync(string resourceUri, CancellationToken cancellationToken = default) - { - return CreateAllFromResourceResponseAsync(GetHttpClient(), resourceUri, cancellationToken); - } - - private static HttpClient GetHttpClient() + public static Task> CreateFromAuthenticationResponseAsync(string resourceUri, CancellationToken cancellationToken = default) { - var httpClientFactory = PlatformProxyFactory.CreatePlatformProxy(null).CreateDefaultHttpClientFactory(); - return httpClientFactory.GetHttpClient(); + return CreateFromAuthenticationResponseAsync(GetHttpClient(), resourceUri, cancellationToken); } /// @@ -234,8 +302,7 @@ private static HttpClient GetHttpClient() /// URI of the resource. /// The cancellation token to cancel operation. /// WWW-Authenticate Parameters extracted from response to the unauthenticated call. - [Obsolete] - public static async Task CreateFromResourceResponseAsync(HttpClient httpClient, string resourceUri, CancellationToken cancellationToken = default) + public static async Task> CreateFromAuthenticationResponseAsync(HttpClient httpClient, string resourceUri, CancellationToken cancellationToken = default) { if (httpClient is null) { @@ -248,58 +315,72 @@ public static async Task CreateFromResourceResponseAs // call this endpoint and see what the header says and return that HttpResponseMessage httpResponseMessage = await httpClient.GetAsync(resourceUri, cancellationToken).ConfigureAwait(false); - var wwwAuthParam = CreateFromResponseHeaders(httpResponseMessage.Headers); - return wwwAuthParam; + var wwwAuthParams = CreateFromAuthenticateHeaders(httpResponseMessage.Headers); + return wwwAuthParams; } /// - /// Create the authenticate parameters by attempting to call the resource unauthenticated, and analyzing the response. + /// Create WWW-Authenticate parameters from the HttpResponseHeaders for each auth scheme. /// - /// Instance of to make the request with. - /// URI of the resource. - /// The cancellation token to cancel operation. - /// Authentication scheme. Default is Bearer. - /// WWW-Authenticate Parameters extracted from response to the unauthenticated call. - public static async Task> CreateAllFromResourceResponseAsync(HttpClient httpClient, string resourceUri, CancellationToken cancellationToken = default, string scheme = "Bearer") + /// HttpResponseHeaders. + /// The parameters requested by the web API. + /// Currently it only supports the Bearer scheme + public static IEnumerable CreateFromAuthenticateHeaders( + HttpResponseHeaders httpResponseHeaders) { - if (httpClient is null) - { - throw new ArgumentNullException(nameof(httpClient)); - } - if (string.IsNullOrWhiteSpace(resourceUri)) + List parameterList = new List(); + + foreach (AuthenticationHeaderValue wwwAuthenticateHeaderValue in httpResponseHeaders.WwwAuthenticate) { - throw new ArgumentNullException(nameof(resourceUri)); + var parameters = CreateFromWwwAuthenticateHeaderValue(wwwAuthenticateHeaderValue.Parameter); + parameters.AuthScheme = wwwAuthenticateHeaderValue.Scheme; + parameterList.Add(parameters); } - // 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; + return parameterList; } + #endregion Multi Scheme Api /// - /// Gets the claim challenge from HTTP header. - /// Used, for example, for CA auth context. + /// Creates parameters from the WWW-Authenticate string. /// - /// The HTTP response headers. - /// Authentication scheme. Default is Bearer. - /// - public static string GetClaimChallengeFromResponseHeaders( - HttpResponseHeaders httpResponseHeaders, - string scheme = "Bearer") + /// String contained in a WWW-Authenticate header. + /// The parameters requested by the web API. + public static WwwAuthenticateParameters CreateFromWwwAuthenticateHeaderValue(string wwwAuthenticateValue) { - WwwAuthenticateParameters parameters = CreateFromResponseHeaders( - httpResponseHeaders, - scheme); + if (string.IsNullOrWhiteSpace(wwwAuthenticateValue)) + { + throw new ArgumentNullException(nameof(wwwAuthenticateValue)); + } + IDictionary parameters; - // read the header and checks if it contains an error with insufficient_claims value. - if (parameters.Claims != null && - string.Equals(parameters.Error, "insufficient_claims", StringComparison.OrdinalIgnoreCase)) + var AuthValuesSplit = wwwAuthenticateValue.Split(new char[] { ' ' }, 2); + + if (s_knownAuthenticationSchemes.Contains(AuthValuesSplit[0])) { - return parameters.Claims; + parameters = CoreHelpers.SplitWithQuotes(AuthValuesSplit[1], ',') + .Select(v => ExtractKeyValuePair(v.Trim())) + .ToDictionary(pair => pair.Key, pair => pair.Value, StringComparer.OrdinalIgnoreCase); + + } + //else if (s_knownAuthenticationSchemes.Contains(AuthValuesSplit[1])) + //{ + + //} + else + { + parameters = CoreHelpers.SplitWithQuotes(wwwAuthenticateValue, ',') + .Select(v => ExtractKeyValuePair(v.Trim())) + .ToDictionary(pair => pair.Key, pair => pair.Value, StringComparer.OrdinalIgnoreCase); } - return null; + return CreateWwwAuthenticateParameters(parameters); + } + + private static HttpClient GetHttpClient() + { + var httpClientFactory = PlatformProxyFactory.CreatePlatformProxy(null).CreateDefaultHttpClientFactory(); + return httpClientFactory.GetHttpClient(); } internal static WwwAuthenticateParameters CreateWwwAuthenticateParameters(IDictionary values) diff --git a/tests/Microsoft.Identity.Test.Unit/WwwAuthenticateParametersTests.cs b/tests/Microsoft.Identity.Test.Unit/WwwAuthenticateParametersTests.cs index 8b01badd0d..5e15ee6c01 100644 --- a/tests/Microsoft.Identity.Test.Unit/WwwAuthenticateParametersTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/WwwAuthenticateParametersTests.cs @@ -127,18 +127,34 @@ public void CreateWwwAuthenticateParamsFromWwwAuthenticateHeader(string clientId { // Arrange HttpResponseMessage httpResponse = new HttpResponseMessage(HttpStatusCode.Unauthorized); + httpResponse.Headers.Add(WwwAuthenticateHeaderName, $"Bearer {clientId}, {authorizationUri}"); httpResponse.Headers.Add(WwwAuthenticateHeaderName, $"Bearer realm=\"\", {clientId}, {authorizationUri}"); + httpResponse.Headers.Add(WwwAuthenticateHeaderName, $"Bearer realm=\"\" token68 {clientId}, {authorizationUri}"); - var wwwAuthenticateResponse = httpResponse.Headers.WwwAuthenticate.First().Parameter; + var wwwAuthenticateResponse1 = httpResponse.Headers.WwwAuthenticate.First().Parameter; + var wwwAuthenticateResponse2 = httpResponse.Headers.WwwAuthenticate.ToList()[1].Parameter; + var wwwAuthenticateResponse3 = httpResponse.Headers.WwwAuthenticate.Last().Parameter; // Act - var authParams = WwwAuthenticateParameters.CreateFromWwwAuthenticateHeaderValue(wwwAuthenticateResponse); + var authParams = WwwAuthenticateParameters.CreateFromWwwAuthenticateHeaderValue(wwwAuthenticateResponse1); + var authParams2 = WwwAuthenticateParameters.CreateFromWwwAuthenticateHeaderValue(wwwAuthenticateResponse2); + var authParams3 = WwwAuthenticateParameters.CreateFromWwwAuthenticateHeaderValue(wwwAuthenticateResponse3); // Assert Assert.AreEqual(TestConstants.AuthorityCommonTenant.TrimEnd('/'), authParams.Authority); Assert.AreEqual(3, authParams.RawParameters.Count); Assert.IsNull(authParams.Claims); Assert.IsNull(authParams.Error); + + Assert.AreEqual(TestConstants.AuthorityCommonTenant.TrimEnd('/'), authParams2.Authority); + Assert.AreEqual(3, authParams2.RawParameters.Count); + Assert.IsNull(authParams2.Claims); + Assert.IsNull(authParams2.Error); + + Assert.AreEqual(TestConstants.AuthorityCommonTenant.TrimEnd('/'), authParams3.Authority); + Assert.AreEqual(3, authParams3.RawParameters.Count); + Assert.IsNull(authParams3.Claims); + Assert.IsNull(authParams3.Error); } [TestMethod] @@ -158,7 +174,7 @@ public async Task CreateFromResourceResponseAsync_HttpClient_Arm_GetTenantId_Asy Assert.AreEqual(authParams.GetTenantId(), tenantId); - var authParamList = await WwwAuthenticateParameters.CreateAllFromResourceResponseAsync(httpClient, resourceUri).ConfigureAwait(false); + var authParamList = await WwwAuthenticateParameters.CreateFromAuthenticationResponseAsync(httpClient, resourceUri).ConfigureAwait(false); Assert.AreEqual(authParamList.FirstOrDefault().GetTenantId(), tenantId); } @@ -180,7 +196,7 @@ public async Task CreateFromResourceResponseAsync_HttpClient_B2C_GetTenantId_Asy Assert.AreEqual(authParams.GetTenantId(), tenantId); - var authParamList = await WwwAuthenticateParameters.CreateAllFromResourceResponseAsync(httpClient, resourceUri).ConfigureAwait(false); + var authParamList = await WwwAuthenticateParameters.CreateFromAuthenticationResponseAsync(httpClient, resourceUri).ConfigureAwait(false); Assert.AreEqual(authParamList.FirstOrDefault().GetTenantId(), tenantId); } @@ -204,7 +220,7 @@ public async Task CreateFromResourceResponseAsync_HttpClient_ADFS_GetTenantId_Nu Assert.IsNull(authParams.GetTenantId()); - var authParamList = await WwwAuthenticateParameters.CreateAllFromResourceResponseAsync(httpClient, resourceUri).ConfigureAwait(false); + var authParamList = await WwwAuthenticateParameters.CreateFromAuthenticationResponseAsync(httpClient, resourceUri).ConfigureAwait(false); Assert.IsNull(authParamList.FirstOrDefault().GetTenantId()); } @@ -219,7 +235,7 @@ public async Task CreateFromResourceResponseAsync_HttpClient_Null_Async(HttpClie await Assert.ThrowsExceptionAsync(action).ConfigureAwait(false); - Func action2 = () => WwwAuthenticateParameters.CreateAllFromResourceResponseAsync(httpClient, resourceUri); + Func action2 = () => WwwAuthenticateParameters.CreateFromAuthenticationResponseAsync(httpClient, resourceUri); await Assert.ThrowsExceptionAsync(action2).ConfigureAwait(false); } @@ -277,13 +293,8 @@ public void ExtractClaimChallengeFromHeader_IncorrectError_ReturnNull() public async Task ExtractNonceFromHeaderAsync() { //Arrange & Act - var parameterList = await WwwAuthenticateParameters.CreateAllFromResourceResponseAsync( - "https://testingsts.azurewebsites.net/servernonce/invalidsignature", - //"https://manage.office.com/api/v1.0/fbb86d84-7975-4300-a5cb-87b448d6f13d/activity/feed/subscriptions/content?contentType={ContentType}&startTime={0}&endTime={1}", - //"https://management.core.windows.net//services/hostedservices/", - //"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); + var parameterList = await WwwAuthenticateParameters.CreateFromAuthenticationResponseAsync( + "https://testingsts.azurewebsites.net/servernonce/invalidsignature").ConfigureAwait(false); //Assert Assert.IsTrue(parameterList.FirstOrDefault().AuthScheme == Constants.PoPAuthHeaderPrefix); @@ -302,8 +313,9 @@ public async Task CreateAllFromResourceResponseAsync_HttpClient_Bearer_Pop_Async ExpectedUrl = resourceUri, ResponseMessage = CreateBearerAndPopHttpResponse() }; + var httpClient = new HttpClient(handler); - var headers = await WwwAuthenticateParameters.CreateAllFromResourceResponseAsync(httpClient, resourceUri).ConfigureAwait(false); + var headers = await WwwAuthenticateParameters.CreateFromAuthenticationResponseAsync(httpClient, resourceUri).ConfigureAwait(false); var bearerHeader = headers.Where(header => header.AuthScheme == "Bearer").FirstOrDefault(); var popHeader = headers.Where(header => header.AuthScheme == "PoP").FirstOrDefault(); @@ -321,7 +333,7 @@ public void ExtractAllParametersFromResponse() HttpResponseMessage httpResponse = CreateBearerAndPopHttpResponse(); // Act & Assert - var headers = WwwAuthenticateParameters.CreateAllFromResponseHeaders(httpResponse.Headers); + var headers = WwwAuthenticateParameters.CreateFromAuthenticateHeaders(httpResponse.Headers); var bearerHeader = headers.Where(header => header.AuthScheme == "Bearer").FirstOrDefault(); var popHeader = headers.Where(header => header.AuthScheme == "PoP").FirstOrDefault(); From bd59b3d1ee2530de32f0f1d5e3c4a04028891bbf Mon Sep 17 00:00:00 2001 From: trwalke Date: Tue, 2 Aug 2022 22:46:50 -0700 Subject: [PATCH 07/31] Refactoring. Adding tests --- .../WwwAuthenticateParameters.cs | 68 +++++++++---------- ...wAuthenticateParametersIntegrationTests.cs | 13 ++++ .../WwwAuthenticateParametersTests.cs | 53 ++++++--------- 3 files changed, 65 insertions(+), 69 deletions(-) diff --git a/src/client/Microsoft.Identity.Client/WwwAuthenticateParameters.cs b/src/client/Microsoft.Identity.Client/WwwAuthenticateParameters.cs index e1d34187f8..6cb35ae2de 100644 --- a/src/client/Microsoft.Identity.Client/WwwAuthenticateParameters.cs +++ b/src/client/Microsoft.Identity.Client/WwwAuthenticateParameters.cs @@ -170,6 +170,38 @@ public static WwwAuthenticateParameters CreateFromResponseHeaders( return CreateWwwAuthenticateParameters(new Dictionary()); } + + /// + /// Creates parameters from the WWW-Authenticate string. + /// + /// String contained in a WWW-Authenticate header. + /// The parameters requested by the web API. + public static WwwAuthenticateParameters CreateFromWwwAuthenticateHeaderValue(string wwwAuthenticateValue) + { + if (string.IsNullOrWhiteSpace(wwwAuthenticateValue)) + { + throw new ArgumentNullException(nameof(wwwAuthenticateValue)); + } + IDictionary parameters; + + var AuthValuesSplit = wwwAuthenticateValue.Split(new char[] { ' ' }, 2); + + if (s_knownAuthenticationSchemes.Contains(AuthValuesSplit[0])) + { + parameters = CoreHelpers.SplitWithQuotes(AuthValuesSplit[1], ',') + .Select(v => ExtractKeyValuePair(v.Trim())) + .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); + } #endregion Obsolete Api #region Single Scheme Api @@ -341,42 +373,6 @@ public static IEnumerable CreateFromAuthenticateHeade } #endregion Multi Scheme Api - /// - /// Creates parameters from the WWW-Authenticate string. - /// - /// String contained in a WWW-Authenticate header. - /// The parameters requested by the web API. - public static WwwAuthenticateParameters CreateFromWwwAuthenticateHeaderValue(string wwwAuthenticateValue) - { - if (string.IsNullOrWhiteSpace(wwwAuthenticateValue)) - { - throw new ArgumentNullException(nameof(wwwAuthenticateValue)); - } - IDictionary parameters; - - var AuthValuesSplit = wwwAuthenticateValue.Split(new char[] { ' ' }, 2); - - if (s_knownAuthenticationSchemes.Contains(AuthValuesSplit[0])) - { - parameters = CoreHelpers.SplitWithQuotes(AuthValuesSplit[1], ',') - .Select(v => ExtractKeyValuePair(v.Trim())) - .ToDictionary(pair => pair.Key, pair => pair.Value, StringComparer.OrdinalIgnoreCase); - - } - //else if (s_knownAuthenticationSchemes.Contains(AuthValuesSplit[1])) - //{ - - //} - else - { - parameters = CoreHelpers.SplitWithQuotes(wwwAuthenticateValue, ',') - .Select(v => ExtractKeyValuePair(v.Trim())) - .ToDictionary(pair => pair.Key, pair => pair.Value, StringComparer.OrdinalIgnoreCase); - } - - return CreateWwwAuthenticateParameters(parameters); - } - private static HttpClient GetHttpClient() { var httpClientFactory = PlatformProxyFactory.CreatePlatformProxy(null).CreateDefaultHttpClientFactory(); diff --git a/tests/Microsoft.Identity.Test.Integration.netfx/HeadlessTests/WwwAuthenticateParametersIntegrationTests.cs b/tests/Microsoft.Identity.Test.Integration.netfx/HeadlessTests/WwwAuthenticateParametersIntegrationTests.cs index 98b1f17a2d..033575f6d0 100644 --- a/tests/Microsoft.Identity.Test.Integration.netfx/HeadlessTests/WwwAuthenticateParametersIntegrationTests.cs +++ b/tests/Microsoft.Identity.Test.Integration.netfx/HeadlessTests/WwwAuthenticateParametersIntegrationTests.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Threading.Tasks; using Microsoft.Identity.Client; +using Microsoft.Identity.Client.Internal; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace Microsoft.Identity.Test.Integration.HeadlessTests @@ -60,5 +61,17 @@ public async Task CreateWwwAuthenticateResponseFromAzureResourceManagerUrlAsync( Assert.IsNull(authParams.Claims); Assert.AreEqual("invalid_token", authParams.Error); } + + [TestMethod] + public async Task ExtractNonceFromHeaderAsync() + { + //Arrange & Act + var parameterList = await WwwAuthenticateParameters.CreateFromAuthenticationResponseAsync( + "https://testingsts.azurewebsites.net/servernonce/invalidsignature").ConfigureAwait(false); + + //Assert + Assert.IsTrue(parameterList.FirstOrDefault().AuthScheme == Constants.PoPAuthHeaderPrefix); + Assert.IsNotNull(parameterList.FirstOrDefault().ServerNonce); + } } } diff --git a/tests/Microsoft.Identity.Test.Unit/WwwAuthenticateParametersTests.cs b/tests/Microsoft.Identity.Test.Unit/WwwAuthenticateParametersTests.cs index 5e15ee6c01..ddd47fcc95 100644 --- a/tests/Microsoft.Identity.Test.Unit/WwwAuthenticateParametersTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/WwwAuthenticateParametersTests.cs @@ -127,34 +127,18 @@ public void CreateWwwAuthenticateParamsFromWwwAuthenticateHeader(string clientId { // Arrange HttpResponseMessage httpResponse = new HttpResponseMessage(HttpStatusCode.Unauthorized); - httpResponse.Headers.Add(WwwAuthenticateHeaderName, $"Bearer {clientId}, {authorizationUri}"); httpResponse.Headers.Add(WwwAuthenticateHeaderName, $"Bearer realm=\"\", {clientId}, {authorizationUri}"); - httpResponse.Headers.Add(WwwAuthenticateHeaderName, $"Bearer realm=\"\" token68 {clientId}, {authorizationUri}"); - var wwwAuthenticateResponse1 = httpResponse.Headers.WwwAuthenticate.First().Parameter; - var wwwAuthenticateResponse2 = httpResponse.Headers.WwwAuthenticate.ToList()[1].Parameter; - var wwwAuthenticateResponse3 = httpResponse.Headers.WwwAuthenticate.Last().Parameter; + var wwwAuthenticateResponse = httpResponse.Headers.WwwAuthenticate.First().Parameter; // Act - var authParams = WwwAuthenticateParameters.CreateFromWwwAuthenticateHeaderValue(wwwAuthenticateResponse1); - var authParams2 = WwwAuthenticateParameters.CreateFromWwwAuthenticateHeaderValue(wwwAuthenticateResponse2); - var authParams3 = WwwAuthenticateParameters.CreateFromWwwAuthenticateHeaderValue(wwwAuthenticateResponse3); + var authParams = WwwAuthenticateParameters.CreateFromWwwAuthenticateHeaderValue(wwwAuthenticateResponse); // Assert Assert.AreEqual(TestConstants.AuthorityCommonTenant.TrimEnd('/'), authParams.Authority); Assert.AreEqual(3, authParams.RawParameters.Count); Assert.IsNull(authParams.Claims); Assert.IsNull(authParams.Error); - - Assert.AreEqual(TestConstants.AuthorityCommonTenant.TrimEnd('/'), authParams2.Authority); - Assert.AreEqual(3, authParams2.RawParameters.Count); - Assert.IsNull(authParams2.Claims); - Assert.IsNull(authParams2.Error); - - Assert.AreEqual(TestConstants.AuthorityCommonTenant.TrimEnd('/'), authParams3.Authority); - Assert.AreEqual(3, authParams3.RawParameters.Count); - Assert.IsNull(authParams3.Claims); - Assert.IsNull(authParams3.Error); } [TestMethod] @@ -174,6 +158,10 @@ public async Task CreateFromResourceResponseAsync_HttpClient_Arm_GetTenantId_Asy Assert.AreEqual(authParams.GetTenantId(), tenantId); + authParams = await WwwAuthenticateParameters.CreateFromAuthenticationResponseAsync(httpClient, resourceUri, "Bearer").ConfigureAwait(false); + + Assert.AreEqual(authParams.GetTenantId(), tenantId); + var authParamList = await WwwAuthenticateParameters.CreateFromAuthenticationResponseAsync(httpClient, resourceUri).ConfigureAwait(false); Assert.AreEqual(authParamList.FirstOrDefault().GetTenantId(), tenantId); @@ -196,6 +184,10 @@ public async Task CreateFromResourceResponseAsync_HttpClient_B2C_GetTenantId_Asy Assert.AreEqual(authParams.GetTenantId(), tenantId); + authParams = await WwwAuthenticateParameters.CreateFromAuthenticationResponseAsync(httpClient, resourceUri, "Bearer").ConfigureAwait(false); + + Assert.AreEqual(authParams.GetTenantId(), tenantId); + var authParamList = await WwwAuthenticateParameters.CreateFromAuthenticationResponseAsync(httpClient, resourceUri).ConfigureAwait(false); Assert.AreEqual(authParamList.FirstOrDefault().GetTenantId(), tenantId); @@ -220,6 +212,10 @@ public async Task CreateFromResourceResponseAsync_HttpClient_ADFS_GetTenantId_Nu Assert.IsNull(authParams.GetTenantId()); + authParams = await WwwAuthenticateParameters.CreateFromAuthenticationResponseAsync(httpClient, resourceUri, "Bearer").ConfigureAwait(false); + + Assert.IsNull(authParams.GetTenantId()); + var authParamList = await WwwAuthenticateParameters.CreateFromAuthenticationResponseAsync(httpClient, resourceUri).ConfigureAwait(false); Assert.IsNull(authParamList.FirstOrDefault().GetTenantId()); @@ -235,9 +231,13 @@ public async Task CreateFromResourceResponseAsync_HttpClient_Null_Async(HttpClie await Assert.ThrowsExceptionAsync(action).ConfigureAwait(false); - Func action2 = () => WwwAuthenticateParameters.CreateFromAuthenticationResponseAsync(httpClient, resourceUri); + action = () => WwwAuthenticateParameters.CreateFromAuthenticationResponseAsync(httpClient, resourceUri, "Bearer"); - await Assert.ThrowsExceptionAsync(action2).ConfigureAwait(false); + await Assert.ThrowsExceptionAsync(action).ConfigureAwait(false); + + action = () => WwwAuthenticateParameters.CreateFromAuthenticationResponseAsync(httpClient, resourceUri); + + await Assert.ThrowsExceptionAsync(action).ConfigureAwait(false); } [TestMethod] @@ -245,7 +245,6 @@ public async Task CreateFromResourceResponseAsync_HttpClient_Null_Async(HttpClie [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}&startTime={0}&endTime={1}").ConfigureAwait(false); Func action = () => WwwAuthenticateParameters.CreateFromResourceResponseAsync(resourceUri); @@ -289,18 +288,6 @@ public void ExtractClaimChallengeFromHeader_IncorrectError_ReturnNull() Assert.IsNull(WwwAuthenticateParameters.GetClaimChallengeFromResponseHeaders(httpResponse.Headers)); } - [TestMethod] - public async Task ExtractNonceFromHeaderAsync() - { - //Arrange & Act - var parameterList = await WwwAuthenticateParameters.CreateFromAuthenticationResponseAsync( - "https://testingsts.azurewebsites.net/servernonce/invalidsignature").ConfigureAwait(false); - - //Assert - Assert.IsTrue(parameterList.FirstOrDefault().AuthScheme == Constants.PoPAuthHeaderPrefix); - Assert.IsNotNull(parameterList.FirstOrDefault().ServerNonce); - } - [TestMethod] public async Task CreateAllFromResourceResponseAsync_HttpClient_Bearer_Pop_Async() { From cab97ae8d7ebefb9bd21b6b610909453da5eae7a Mon Sep 17 00:00:00 2001 From: trwalke Date: Thu, 4 Aug 2022 21:05:41 -0700 Subject: [PATCH 08/31] Refactoring to use Dictionary return type --- .../WwwAuthenticateParameters.cs | 12 ++++++------ .../WwwAuthenticateParametersIntegrationTests.cs | 4 ++-- .../WwwAuthenticateParametersTests.cs | 16 +++++++--------- 3 files changed, 15 insertions(+), 17 deletions(-) diff --git a/src/client/Microsoft.Identity.Client/WwwAuthenticateParameters.cs b/src/client/Microsoft.Identity.Client/WwwAuthenticateParameters.cs index 6cb35ae2de..1526487d76 100644 --- a/src/client/Microsoft.Identity.Client/WwwAuthenticateParameters.cs +++ b/src/client/Microsoft.Identity.Client/WwwAuthenticateParameters.cs @@ -311,7 +311,7 @@ public static string GetClaimChallengeFromResponseHeaders( /// /// URI of the resource. /// WWW-Authenticate Parameters extracted from response to the unauthenticated call. - public static Task> CreateFromAuthenticationResponseAsync(string resourceUri) + public static Task> CreateFromAuthenticationResponseAsync(string resourceUri) { return CreateFromAuthenticationResponseAsync(resourceUri, new CancellationToken()); } @@ -322,7 +322,7 @@ public static Task> CreateFromAuthenticat /// URI of the resource. /// The cancellation token to cancel operation. /// WWW-Authenticate Parameters extracted from response to the unauthenticated call. - public static Task> CreateFromAuthenticationResponseAsync(string resourceUri, CancellationToken cancellationToken = default) + public static Task> CreateFromAuthenticationResponseAsync(string resourceUri, CancellationToken cancellationToken = default) { return CreateFromAuthenticationResponseAsync(GetHttpClient(), resourceUri, cancellationToken); } @@ -334,7 +334,7 @@ public static Task> CreateFromAuthenticat /// URI of the resource. /// The cancellation token to cancel operation. /// WWW-Authenticate Parameters extracted from response to the unauthenticated call. - public static async Task> CreateFromAuthenticationResponseAsync(HttpClient httpClient, string resourceUri, CancellationToken cancellationToken = default) + public static async Task> CreateFromAuthenticationResponseAsync(HttpClient httpClient, string resourceUri, CancellationToken cancellationToken = default) { if (httpClient is null) { @@ -357,16 +357,16 @@ public static async Task> CreateFromAuthe /// HttpResponseHeaders. /// The parameters requested by the web API. /// Currently it only supports the Bearer scheme - public static IEnumerable CreateFromAuthenticateHeaders( + public static Dictionary CreateFromAuthenticateHeaders( HttpResponseHeaders httpResponseHeaders) { - List parameterList = new List(); + Dictionary parameterList = new Dictionary(); foreach (AuthenticationHeaderValue wwwAuthenticateHeaderValue in httpResponseHeaders.WwwAuthenticate) { var parameters = CreateFromWwwAuthenticateHeaderValue(wwwAuthenticateHeaderValue.Parameter); parameters.AuthScheme = wwwAuthenticateHeaderValue.Scheme; - parameterList.Add(parameters); + parameterList.Add(wwwAuthenticateHeaderValue.Scheme, parameters); } return parameterList; diff --git a/tests/Microsoft.Identity.Test.Integration.netfx/HeadlessTests/WwwAuthenticateParametersIntegrationTests.cs b/tests/Microsoft.Identity.Test.Integration.netfx/HeadlessTests/WwwAuthenticateParametersIntegrationTests.cs index 033575f6d0..4f90a0b730 100644 --- a/tests/Microsoft.Identity.Test.Integration.netfx/HeadlessTests/WwwAuthenticateParametersIntegrationTests.cs +++ b/tests/Microsoft.Identity.Test.Integration.netfx/HeadlessTests/WwwAuthenticateParametersIntegrationTests.cs @@ -70,8 +70,8 @@ public async Task ExtractNonceFromHeaderAsync() "https://testingsts.azurewebsites.net/servernonce/invalidsignature").ConfigureAwait(false); //Assert - Assert.IsTrue(parameterList.FirstOrDefault().AuthScheme == Constants.PoPAuthHeaderPrefix); - Assert.IsNotNull(parameterList.FirstOrDefault().ServerNonce); + Assert.IsTrue(parameterList[Constants.PoPAuthHeaderPrefix].AuthScheme == Constants.PoPAuthHeaderPrefix); + Assert.IsNotNull(parameterList[Constants.PoPAuthHeaderPrefix].ServerNonce); } } } diff --git a/tests/Microsoft.Identity.Test.Unit/WwwAuthenticateParametersTests.cs b/tests/Microsoft.Identity.Test.Unit/WwwAuthenticateParametersTests.cs index ddd47fcc95..2635a5c708 100644 --- a/tests/Microsoft.Identity.Test.Unit/WwwAuthenticateParametersTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/WwwAuthenticateParametersTests.cs @@ -164,7 +164,7 @@ public async Task CreateFromResourceResponseAsync_HttpClient_Arm_GetTenantId_Asy var authParamList = await WwwAuthenticateParameters.CreateFromAuthenticationResponseAsync(httpClient, resourceUri).ConfigureAwait(false); - Assert.AreEqual(authParamList.FirstOrDefault().GetTenantId(), tenantId); + Assert.AreEqual(authParamList.FirstOrDefault().Value.GetTenantId(), tenantId); } [TestMethod] @@ -190,7 +190,7 @@ public async Task CreateFromResourceResponseAsync_HttpClient_B2C_GetTenantId_Asy var authParamList = await WwwAuthenticateParameters.CreateFromAuthenticationResponseAsync(httpClient, resourceUri).ConfigureAwait(false); - Assert.AreEqual(authParamList.FirstOrDefault().GetTenantId(), tenantId); + Assert.AreEqual(authParamList.FirstOrDefault().Value.GetTenantId(), tenantId); } [TestMethod] @@ -218,7 +218,7 @@ public async Task CreateFromResourceResponseAsync_HttpClient_ADFS_GetTenantId_Nu var authParamList = await WwwAuthenticateParameters.CreateFromAuthenticationResponseAsync(httpClient, resourceUri).ConfigureAwait(false); - Assert.IsNull(authParamList.FirstOrDefault().GetTenantId()); + Assert.IsNull(authParamList.FirstOrDefault().Value.GetTenantId()); } [DataRow(null)] @@ -245,8 +245,6 @@ public async Task CreateFromResourceResponseAsync_HttpClient_Null_Async(HttpClie [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}&startTime={0}&endTime={1}").ConfigureAwait(false); - Func action = () => WwwAuthenticateParameters.CreateFromResourceResponseAsync(resourceUri); await Assert.ThrowsExceptionAsync(action).ConfigureAwait(false); @@ -304,8 +302,8 @@ public async Task CreateAllFromResourceResponseAsync_HttpClient_Bearer_Pop_Async var httpClient = new HttpClient(handler); var headers = await WwwAuthenticateParameters.CreateFromAuthenticationResponseAsync(httpClient, resourceUri).ConfigureAwait(false); - var bearerHeader = headers.Where(header => header.AuthScheme == "Bearer").FirstOrDefault(); - var popHeader = headers.Where(header => header.AuthScheme == "PoP").FirstOrDefault(); + var bearerHeader = headers["Bearer"]; + var popHeader = headers["PoP"]; Assert.IsNotNull(bearerHeader); Assert.AreEqual("https://login.microsoftonline.com/TenantId", bearerHeader.Authority); @@ -321,8 +319,8 @@ public void ExtractAllParametersFromResponse() // Act & Assert var headers = WwwAuthenticateParameters.CreateFromAuthenticateHeaders(httpResponse.Headers); - var bearerHeader = headers.Where(header => header.AuthScheme == "Bearer").FirstOrDefault(); - var popHeader = headers.Where(header => header.AuthScheme == "PoP").FirstOrDefault(); + var bearerHeader = headers["Bearer"]; + var popHeader = headers["PoP"]; Assert.IsNotNull(bearerHeader); Assert.AreEqual("https://login.microsoftonline.com/TenantId", bearerHeader.Authority); From 37d9c43ea78785e0ef6063d3521bca83fcdf62bd Mon Sep 17 00:00:00 2001 From: trwalke Date: Tue, 27 Sep 2022 00:20:08 -0700 Subject: [PATCH 09/31] Adding Authentication header parser and support for authentication info --- .../Headers/AuthenticationHeaderParser.cs | 154 ++++++++++++++++++ .../Headers/AuthenticationInfoParameters.cs | 57 +++++++ .../Headers}/WwwAuthenticateParameters.cs | 117 ++++++------- ...wAuthenticateParametersIntegrationTests.cs | 50 +++++- .../WwwAuthenticateParametersTests.cs | 14 +- 5 files changed, 317 insertions(+), 75 deletions(-) create mode 100644 src/client/Microsoft.Identity.Client/Http/Headers/AuthenticationHeaderParser.cs create mode 100644 src/client/Microsoft.Identity.Client/Http/Headers/AuthenticationInfoParameters.cs rename src/client/Microsoft.Identity.Client/{ => Http/Headers}/WwwAuthenticateParameters.cs (89%) diff --git a/src/client/Microsoft.Identity.Client/Http/Headers/AuthenticationHeaderParser.cs b/src/client/Microsoft.Identity.Client/Http/Headers/AuthenticationHeaderParser.cs new file mode 100644 index 0000000000..02526ee922 --- /dev/null +++ b/src/client/Microsoft.Identity.Client/Http/Headers/AuthenticationHeaderParser.cs @@ -0,0 +1,154 @@ +// 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.PlatformsCommon.Factories; +using Microsoft.Identity.Client.Utils; + +namespace Microsoft.Identity.Client +{ + /// + /// Parsed authentication headers to retreve header values from HttpResponseHeaders. + /// + public class AuthenticationHeaderParser + { + /// + /// 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. + /// + public IReadOnlyList ParsedWwwAuthenticateParameters { get; private set; } + + /// + /// Parameters returned by the Authenticatin-Info header. + /// This allows for authentication scenarios such as Proof-Of-Posession. + /// + public AuthenticationInfoParameters ParsedAuthenticationInfoParameters { get; private set; } + + /// + /// Nonce parsed from HttpResponseHeaders + /// + public string Nonce { get; private set; } + + /// + /// Creates the authenticate parameters by attempting to call the resource unauthenticated, and analyzing the response. + /// + /// URI of the resource. + /// + public static async Task ParseAuthenticationHeadersAsync(string resourceUri) + { + return await ParseAuthenticationHeadersAsync(resourceUri, new CancellationToken()).ConfigureAwait(false); + } + + /// + /// Creates the authenticate parameters by attempting to call the resource unauthenticated, and analyzing the response. + /// + /// URI of the resource. + /// The cancellation token to cancel operation. + /// + public static async Task ParseAuthenticationHeadersAsync(string resourceUri, CancellationToken cancellationToken) + { + return await ParseAuthenticationHeadersAsync(resourceUri, cancellationToken, GetHttpClient()).ConfigureAwait(false); + } + + /// + /// Creates the authenticate parameters by attempting to call the resource unauthenticated, and analyzing the response. + /// + /// URI of the resource. + /// The cancellation token to cancel operation. + /// Instance of to make the request with. + /// + /// + public static async Task ParseAuthenticationHeadersAsync(string resourceUri, CancellationToken cancellationToken, HttpClient httpClient) + { + 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); + } + + /// + /// Creates a parsed set of parameters from the provided HttpResponseHeaders. + /// + /// + /// For known values, such as the nonce used for Proof-of-Possession, the parser will first chack for in the WWW-Authenticate headers + /// If it cannot find it, it will then check the Authentication-Info parameters for the value. + /// + public static AuthenticationHeaderParser ParseAuthenticationHeaders(HttpResponseHeaders httpResponseHeaders) + { + AuthenticationHeaderParser authenticationHeaderParser = new AuthenticationHeaderParser(); + string serverNonce = null; + + //Check for WWW-AuthenticateHeaders + if (httpResponseHeaders.WwwAuthenticate.Count != 0) + { + var WwwParameters = WwwAuthenticateParameters.CreateFromAuthenticateHeaders(httpResponseHeaders); + + if (WwwParameters.Any(parameter => parameter.AuthScheme == "PoP")) + { + serverNonce = WwwParameters.Where(parameter => parameter.AuthScheme == "PoP").Single().ServerNonce; + } + + authenticationHeaderParser.ParsedWwwAuthenticateParameters = WwwParameters; + } + else + { + authenticationHeaderParser.ParsedWwwAuthenticateParameters = new List(); + } + + //If no WWW-AuthenticateHeaders exist, attempt to parse AuthenticationInfo headers instead + AuthenticationInfoParameters authenticationInfoParameters = AuthenticationInfoParameters.CreateFromHeaders(httpResponseHeaders); + authenticationHeaderParser.ParsedAuthenticationInfoParameters = authenticationInfoParameters; + + //If server nonce is not acquired from WWW-Authenticate headers, use next nonce from Authentication-Info parameters. + authenticationHeaderParser.Nonce = serverNonce?? authenticationInfoParameters.NextNonce; + + return authenticationHeaderParser; + } + + /// + /// Created an HttpClient + /// + internal static HttpClient GetHttpClient() + { + var httpClientFactory = PlatformProxyFactory.CreatePlatformProxy(null).CreateDefaultHttpClientFactory(); + return httpClientFactory.GetHttpClient(); + } + + /// + /// Extracts a key value pair from an expression of the form a=b + /// + /// assignment + /// Key Value pair + internal static KeyValuePair ExtractKeyValuePair(string assignment) + { + string[] segments = CoreHelpers.SplitWithQuotes(assignment, '=') + .Select(s => s.Trim().Trim('"')) + .ToArray(); + + if (segments.Length != 2) + { + throw new ArgumentException(nameof(assignment), $"{assignment} isn't of the form a=b"); + } + + return new KeyValuePair(segments[0], segments[1]); + } + } + +} diff --git a/src/client/Microsoft.Identity.Client/Http/Headers/AuthenticationInfoParameters.cs b/src/client/Microsoft.Identity.Client/Http/Headers/AuthenticationInfoParameters.cs new file mode 100644 index 0000000000..64d2ab8e7f --- /dev/null +++ b/src/client/Microsoft.Identity.Client/Http/Headers/AuthenticationInfoParameters.cs @@ -0,0 +1,57 @@ +// 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 System.Text; +using System.Threading.Tasks; +using Microsoft.Identity.Client.Utils; +using Microsoft.Identity.Json.Linq; + +namespace Microsoft.Identity.Client +{ + /// + /// + /// + public class AuthenticationInfoParameters + { + private const string _authenticationInfoKey = "Authentication-Info"; + /// + /// + /// + public string NextNonce { get; private set; } + + /// + /// + /// + /// + /// + public static AuthenticationInfoParameters CreateFromHeaders(HttpResponseHeaders respnseHeaders) + { + AuthenticationInfoParameters parameters = new AuthenticationInfoParameters(); + + if (respnseHeaders.Any(header => header.Key == _authenticationInfoKey)) + { + var authInfoValue = respnseHeaders.Where(header => header.Key == _authenticationInfoKey).Single().Value.FirstOrDefault(); + + var AuthValuesSplit = authInfoValue.Split(new char[] { ' ' }, 2); + + var paramValues = CoreHelpers.SplitWithQuotes(AuthValuesSplit[1], ',') + .Select(v => AuthenticationHeaderParser.ExtractKeyValuePair(v.Trim())) + .ToDictionary(pair => pair.Key, pair => pair.Value, StringComparer.OrdinalIgnoreCase); + + string value; + + if (paramValues.TryGetValue("nextnonce", out value)) + { + parameters.NextNonce = value; + } + + } + + return parameters; + } + } +} diff --git a/src/client/Microsoft.Identity.Client/WwwAuthenticateParameters.cs b/src/client/Microsoft.Identity.Client/Http/Headers/WwwAuthenticateParameters.cs similarity index 89% rename from src/client/Microsoft.Identity.Client/WwwAuthenticateParameters.cs rename to src/client/Microsoft.Identity.Client/Http/Headers/WwwAuthenticateParameters.cs index 1526487d76..c61a99f04b 100644 --- a/src/client/Microsoft.Identity.Client/WwwAuthenticateParameters.cs +++ b/src/client/Microsoft.Identity.Client/Http/Headers/WwwAuthenticateParameters.cs @@ -13,6 +13,7 @@ using Microsoft.Identity.Client.Internal; using Microsoft.Identity.Client.PlatformsCommon.Factories; using Microsoft.Identity.Client.Utils; +using Microsoft.Identity.Json.Linq; namespace Microsoft.Identity.Client { @@ -59,6 +60,11 @@ public class WwwAuthenticateParameters /// public string Error { get; set; } + /// + /// Error indicating that parsing failed. + /// + public bool ParsingError { get; private set; } + /// /// AuthScheme. /// See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/WWW-Authenticate#syntax for more details @@ -119,7 +125,7 @@ public static Task CreateFromResourceResponseAsync(st [System.ComponentModel.EditorBrowsable(EditorBrowsableState.Never)] public static Task CreateFromResourceResponseAsync(string resourceUri, CancellationToken cancellationToken = default) { - return CreateFromResourceResponseAsync(GetHttpClient(), resourceUri, cancellationToken); + return CreateFromResourceResponseAsync(AuthenticationHeaderParser.GetHttpClient(), resourceUri, cancellationToken); } /// @@ -170,38 +176,6 @@ public static WwwAuthenticateParameters CreateFromResponseHeaders( return CreateWwwAuthenticateParameters(new Dictionary()); } - - /// - /// Creates parameters from the WWW-Authenticate string. - /// - /// String contained in a WWW-Authenticate header. - /// The parameters requested by the web API. - public static WwwAuthenticateParameters CreateFromWwwAuthenticateHeaderValue(string wwwAuthenticateValue) - { - if (string.IsNullOrWhiteSpace(wwwAuthenticateValue)) - { - throw new ArgumentNullException(nameof(wwwAuthenticateValue)); - } - IDictionary parameters; - - var AuthValuesSplit = wwwAuthenticateValue.Split(new char[] { ' ' }, 2); - - if (s_knownAuthenticationSchemes.Contains(AuthValuesSplit[0])) - { - parameters = CoreHelpers.SplitWithQuotes(AuthValuesSplit[1], ',') - .Select(v => ExtractKeyValuePair(v.Trim())) - .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); - } #endregion Obsolete Api #region Single Scheme Api @@ -225,7 +199,7 @@ public static Task CreateFromAuthenticationResponseAs /// WWW-Authenticate Parameters extracted from response to the unauthenticated call. public static Task CreateFromAuthenticationResponseAsync(string resourceUri, string scheme, CancellationToken cancellationToken = default) { - return CreateFromAuthenticationResponseAsync(GetHttpClient(), resourceUri, scheme, cancellationToken); + return CreateFromAuthenticationResponseAsync(AuthenticationHeaderParser.GetHttpClient(), resourceUri, scheme, cancellationToken); } /// @@ -259,7 +233,6 @@ public static async Task CreateFromAuthenticationResp /// HttpResponseHeaders. /// Authentication scheme. /// The parameters requested by the web API. - /// Currently it only supports the Bearer scheme public static WwwAuthenticateParameters CreateFromAuthenticateHeaders( HttpResponseHeaders httpResponseHeaders, string scheme) @@ -284,7 +257,7 @@ public static WwwAuthenticateParameters CreateFromAuthenticateHeaders( /// /// The HTTP response headers. /// Authentication scheme. Default is Bearer. - /// + /// The claims challenge public static string GetClaimChallengeFromResponseHeaders( HttpResponseHeaders httpResponseHeaders, string scheme = "Bearer") @@ -311,7 +284,7 @@ public static string GetClaimChallengeFromResponseHeaders( /// /// URI of the resource. /// WWW-Authenticate Parameters extracted from response to the unauthenticated call. - public static Task> CreateFromAuthenticationResponseAsync(string resourceUri) + public static Task> CreateFromAuthenticationResponseAsync(string resourceUri) { return CreateFromAuthenticationResponseAsync(resourceUri, new CancellationToken()); } @@ -322,9 +295,9 @@ public static Task> CreateFromAuth /// URI of the resource. /// The cancellation token to cancel operation. /// WWW-Authenticate Parameters extracted from response to the unauthenticated call. - public static Task> CreateFromAuthenticationResponseAsync(string resourceUri, CancellationToken cancellationToken = default) + public static Task> CreateFromAuthenticationResponseAsync(string resourceUri, CancellationToken cancellationToken = default) { - return CreateFromAuthenticationResponseAsync(GetHttpClient(), resourceUri, cancellationToken); + return CreateFromAuthenticationResponseAsync(AuthenticationHeaderParser.GetHttpClient(), resourceUri, cancellationToken); } /// @@ -334,7 +307,7 @@ public static Task> CreateFromAuth /// URI of the resource. /// The cancellation token to cancel operation. /// WWW-Authenticate Parameters extracted from response to the unauthenticated call. - public static async Task> CreateFromAuthenticationResponseAsync(HttpClient httpClient, string resourceUri, CancellationToken cancellationToken = default) + public static async Task> CreateFromAuthenticationResponseAsync(HttpClient httpClient, string resourceUri, CancellationToken cancellationToken = default) { if (httpClient is null) { @@ -357,26 +330,52 @@ public static async Task> CreateFr /// HttpResponseHeaders. /// The parameters requested by the web API. /// Currently it only supports the Bearer scheme - public static Dictionary CreateFromAuthenticateHeaders( + public static IReadOnlyList CreateFromAuthenticateHeaders( HttpResponseHeaders httpResponseHeaders) { - Dictionary parameterList = new Dictionary(); + List parameterList = new List(); foreach (AuthenticationHeaderValue wwwAuthenticateHeaderValue in httpResponseHeaders.WwwAuthenticate) { var parameters = CreateFromWwwAuthenticateHeaderValue(wwwAuthenticateHeaderValue.Parameter); parameters.AuthScheme = wwwAuthenticateHeaderValue.Scheme; - parameterList.Add(wwwAuthenticateHeaderValue.Scheme, parameters); + parameterList.Add(parameters); } return parameterList; } #endregion Multi Scheme Api - private static HttpClient GetHttpClient() + /// + /// Creates parameters from the WWW-Authenticate string. + /// + /// String contained in a WWW-Authenticate header. + /// The parameters requested by the web API. + public static WwwAuthenticateParameters CreateFromWwwAuthenticateHeaderValue(string wwwAuthenticateValue) { - var httpClientFactory = PlatformProxyFactory.CreatePlatformProxy(null).CreateDefaultHttpClientFactory(); - return httpClientFactory.GetHttpClient(); + if (string.IsNullOrWhiteSpace(wwwAuthenticateValue)) + { + throw new ArgumentNullException(nameof(wwwAuthenticateValue)); + } + IDictionary parameters; + + var AuthValuesSplit = wwwAuthenticateValue.Split(new char[] { ' ' }, 2); + + if (s_knownAuthenticationSchemes.Contains(AuthValuesSplit[0])) + { + parameters = CoreHelpers.SplitWithQuotes(AuthValuesSplit[1], ',') + .Select(v => AuthenticationHeaderParser.ExtractKeyValuePair(v.Trim())) + .ToDictionary(pair => pair.Key, pair => pair.Value, StringComparer.OrdinalIgnoreCase); + + } + else + { + parameters = CoreHelpers.SplitWithQuotes(wwwAuthenticateValue, ',') + .Select(v => AuthenticationHeaderParser.ExtractKeyValuePair(v.Trim())) + .ToDictionary(pair => pair.Key, pair => pair.Value, StringComparer.OrdinalIgnoreCase); + } + + return CreateWwwAuthenticateParameters(parameters); } internal static WwwAuthenticateParameters CreateWwwAuthenticateParameters(IDictionary values) @@ -386,7 +385,14 @@ internal static WwwAuthenticateParameters CreateWwwAuthenticateParameters(IDicti RawParameters = values }; + if (values.Count == 0) + { + //unable to parse auth header values + wwwAuthenticateParameters.ParsingError = true; + } + string value; + if (values.TryGetValue("authorization_uri", out value)) { wwwAuthenticateParameters.Authority = value.Replace("/oauth2/authorize", string.Empty); @@ -426,25 +432,6 @@ internal static WwwAuthenticateParameters CreateWwwAuthenticateParameters(IDicti return wwwAuthenticateParameters; } - /// - /// Extracts a key value pair from an expression of the form a=b - /// - /// assignment - /// Key Value pair - private static KeyValuePair ExtractKeyValuePair(string assignment) - { - string[] segments = CoreHelpers.SplitWithQuotes(assignment, '=') - .Select(s => s.Trim().Trim('"')) - .ToArray(); - - if (segments.Length != 2) - { - throw new ArgumentException(nameof(assignment), $"{assignment} isn't of the form a=b"); - } - - return new KeyValuePair(segments[0], segments[1]); - } - /// /// Checks if input is a base-64 encoded string. /// If it is one, decodes it to get a json fragment. diff --git a/tests/Microsoft.Identity.Test.Integration.netfx/HeadlessTests/WwwAuthenticateParametersIntegrationTests.cs b/tests/Microsoft.Identity.Test.Integration.netfx/HeadlessTests/WwwAuthenticateParametersIntegrationTests.cs index 4f90a0b730..f8ff5f607c 100644 --- a/tests/Microsoft.Identity.Test.Integration.netfx/HeadlessTests/WwwAuthenticateParametersIntegrationTests.cs +++ b/tests/Microsoft.Identity.Test.Integration.netfx/HeadlessTests/WwwAuthenticateParametersIntegrationTests.cs @@ -3,9 +3,12 @@ using System; using System.Linq; +using System.Net.Http; +using System.Threading; using System.Threading.Tasks; using Microsoft.Identity.Client; using Microsoft.Identity.Client.Internal; +using Microsoft.Identity.Client.PlatformsCommon.Factories; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace Microsoft.Identity.Test.Integration.HeadlessTests @@ -63,15 +66,56 @@ public async Task CreateWwwAuthenticateResponseFromAzureResourceManagerUrlAsync( } [TestMethod] - public async Task ExtractNonceFromHeaderAsync() + public async Task ExtractNonceFromWwwAuthHeadersAsync() { //Arrange & Act + //Test for nonce in WWW-Authenticate header var parameterList = await WwwAuthenticateParameters.CreateFromAuthenticationResponseAsync( "https://testingsts.azurewebsites.net/servernonce/invalidsignature").ConfigureAwait(false); //Assert - Assert.IsTrue(parameterList[Constants.PoPAuthHeaderPrefix].AuthScheme == Constants.PoPAuthHeaderPrefix); - Assert.IsNotNull(parameterList[Constants.PoPAuthHeaderPrefix].ServerNonce); + Assert.IsTrue(parameterList.Any(param => param.AuthScheme == Constants.PoPAuthHeaderPrefix)); + Assert.IsNotNull(parameterList.Where(param => param.AuthScheme == Constants.PoPAuthHeaderPrefix).Single().ServerNonce); + } + + [TestMethod] + public async Task ExtractNonceFromAuthInfoHeadersAsync() + { + //Arrange & Act + var httpClientFactory = PlatformProxyFactory.CreatePlatformProxy(null).CreateDefaultHttpClientFactory(); + var httpClient = httpClientFactory.GetHttpClient(); + + HttpResponseMessage httpResponseMessage = await httpClient.GetAsync("https://testingsts.azurewebsites.net/servernonce/authinfo", new CancellationToken()).ConfigureAwait(false); + + //Assert + var authInfoParameters = AuthenticationInfoParameters.CreateFromHeaders(httpResponseMessage.Headers); + Assert.IsNotNull(authInfoParameters); + Assert.IsNotNull(authInfoParameters.NextNonce); + } + + [TestMethod] + public async Task ExtractNonceWithAuthParserAsync() + { + //Arrange & Act + //Test for nonce in WWW-Authenticate header + var parsedHeaders = await AuthenticationHeaderParser.ParseAuthenticationHeadersAsync("https://testingsts.azurewebsites.net/servernonce/invalidsignature").ConfigureAwait(false); + + //Assert + Assert.IsTrue(parsedHeaders.ParsedWwwAuthenticateParameters.Any(param => param.AuthScheme == Constants.PoPAuthHeaderPrefix)); + var serverNonce = parsedHeaders.ParsedWwwAuthenticateParameters.Where(param => param.AuthScheme == Constants.PoPAuthHeaderPrefix).Single().ServerNonce; + Assert.IsNotNull(serverNonce); + Assert.AreEqual(parsedHeaders.Nonce, serverNonce); + Assert.IsNull(parsedHeaders.ParsedAuthenticationInfoParameters.NextNonce); + + //Arrange & Act + //Test for nonce in Authentication-Info header + parsedHeaders = await AuthenticationHeaderParser.ParseAuthenticationHeadersAsync("https://testingsts.azurewebsites.net/servernonce/authinfo").ConfigureAwait(false); + + //Assert + Assert.IsNotNull(parsedHeaders.ParsedAuthenticationInfoParameters.NextNonce); + Assert.AreEqual(parsedHeaders.Nonce, parsedHeaders.ParsedAuthenticationInfoParameters.NextNonce); + + Assert.IsFalse(parsedHeaders.ParsedWwwAuthenticateParameters.Any(param => param.AuthScheme == Constants.PoPAuthHeaderPrefix)); } } } diff --git a/tests/Microsoft.Identity.Test.Unit/WwwAuthenticateParametersTests.cs b/tests/Microsoft.Identity.Test.Unit/WwwAuthenticateParametersTests.cs index 2635a5c708..2e249f8dbd 100644 --- a/tests/Microsoft.Identity.Test.Unit/WwwAuthenticateParametersTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/WwwAuthenticateParametersTests.cs @@ -164,7 +164,7 @@ public async Task CreateFromResourceResponseAsync_HttpClient_Arm_GetTenantId_Asy var authParamList = await WwwAuthenticateParameters.CreateFromAuthenticationResponseAsync(httpClient, resourceUri).ConfigureAwait(false); - Assert.AreEqual(authParamList.FirstOrDefault().Value.GetTenantId(), tenantId); + Assert.AreEqual(authParamList.FirstOrDefault().GetTenantId(), tenantId); } [TestMethod] @@ -190,7 +190,7 @@ public async Task CreateFromResourceResponseAsync_HttpClient_B2C_GetTenantId_Asy var authParamList = await WwwAuthenticateParameters.CreateFromAuthenticationResponseAsync(httpClient, resourceUri).ConfigureAwait(false); - Assert.AreEqual(authParamList.FirstOrDefault().Value.GetTenantId(), tenantId); + Assert.AreEqual(authParamList.FirstOrDefault().GetTenantId(), tenantId); } [TestMethod] @@ -218,7 +218,7 @@ public async Task CreateFromResourceResponseAsync_HttpClient_ADFS_GetTenantId_Nu var authParamList = await WwwAuthenticateParameters.CreateFromAuthenticationResponseAsync(httpClient, resourceUri).ConfigureAwait(false); - Assert.IsNull(authParamList.FirstOrDefault().Value.GetTenantId()); + Assert.IsNull(authParamList.FirstOrDefault().GetTenantId()); } [DataRow(null)] @@ -302,8 +302,8 @@ public async Task CreateAllFromResourceResponseAsync_HttpClient_Bearer_Pop_Async var httpClient = new HttpClient(handler); var headers = await WwwAuthenticateParameters.CreateFromAuthenticationResponseAsync(httpClient, resourceUri).ConfigureAwait(false); - var bearerHeader = headers["Bearer"]; - var popHeader = headers["PoP"]; + var bearerHeader = headers.Where(header => header.AuthScheme == "Bearer").Single(); + var popHeader = headers.Where(header => header.AuthScheme == "PoP").Single(); Assert.IsNotNull(bearerHeader); Assert.AreEqual("https://login.microsoftonline.com/TenantId", bearerHeader.Authority); @@ -319,8 +319,8 @@ public void ExtractAllParametersFromResponse() // Act & Assert var headers = WwwAuthenticateParameters.CreateFromAuthenticateHeaders(httpResponse.Headers); - var bearerHeader = headers["Bearer"]; - var popHeader = headers["PoP"]; + var bearerHeader = headers.Where(header => header.AuthScheme == "Bearer").Single(); + var popHeader = headers.Where(header => header.AuthScheme == "PoP").Single(); Assert.IsNotNull(bearerHeader); Assert.AreEqual("https://login.microsoftonline.com/TenantId", bearerHeader.Authority); From bae9643d8f8665826d1f2e42d1d4bf63faf6925e Mon Sep 17 00:00:00 2001 From: trwalke Date: Tue, 27 Sep 2022 00:32:18 -0700 Subject: [PATCH 10/31] Refactoring --- .../Http/Headers/WwwAuthenticateParameters.cs | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/src/client/Microsoft.Identity.Client/Http/Headers/WwwAuthenticateParameters.cs b/src/client/Microsoft.Identity.Client/Http/Headers/WwwAuthenticateParameters.cs index c61a99f04b..0d9819dbf1 100644 --- a/src/client/Microsoft.Identity.Client/Http/Headers/WwwAuthenticateParameters.cs +++ b/src/client/Microsoft.Identity.Client/Http/Headers/WwwAuthenticateParameters.cs @@ -167,11 +167,15 @@ public static WwwAuthenticateParameters CreateFromResponseHeaders( { if (httpResponseHeaders.WwwAuthenticate.Any()) { - AuthenticationHeaderValue bearer = httpResponseHeaders.WwwAuthenticate.First(v => string.Equals(v.Scheme, Constants.BearerAuthHeaderPrefix, StringComparison.OrdinalIgnoreCase)); - string wwwAuthenticateValue = bearer.Parameter; - var parameters = CreateFromWwwAuthenticateHeaderValue(wwwAuthenticateValue); - parameters.AuthScheme = Constants.BearerAuthHeaderPrefix; - return parameters; + AuthenticationHeaderValue headerValue = httpResponseHeaders.WwwAuthenticate.FirstOrDefault(v => string.Equals(v.Scheme, Constants.BearerAuthHeaderPrefix, StringComparison.OrdinalIgnoreCase)); + + if (headerValue != null) + { + string wwwAuthenticateValue = headerValue.Parameter; + var parameters = CreateFromWwwAuthenticateHeaderValue(wwwAuthenticateValue); + parameters.AuthScheme = scheme; + return parameters; + } } return CreateWwwAuthenticateParameters(new Dictionary()); @@ -359,11 +363,11 @@ public static WwwAuthenticateParameters CreateFromWwwAuthenticateHeaderValue(str } IDictionary parameters; - var AuthValuesSplit = wwwAuthenticateValue.Split(new char[] { ' ' }, 2); + var authValuesSplit = wwwAuthenticateValue.Split(new char[] { ' ' }, 2); - if (s_knownAuthenticationSchemes.Contains(AuthValuesSplit[0])) + if (s_knownAuthenticationSchemes.Contains(authValuesSplit[0])) { - parameters = CoreHelpers.SplitWithQuotes(AuthValuesSplit[1], ',') + parameters = CoreHelpers.SplitWithQuotes(authValuesSplit[1], ',') .Select(v => AuthenticationHeaderParser.ExtractKeyValuePair(v.Trim())) .ToDictionary(pair => pair.Key, pair => pair.Value, StringComparer.OrdinalIgnoreCase); From eb8032fc9c7da41b75f677b80890f1eeea86d0e8 Mon Sep 17 00:00:00 2001 From: Travis Walker Date: Thu, 13 Oct 2022 15:36:44 -0700 Subject: [PATCH 11/31] Apply suggestions from code review Co-authored-by: Neha Bhargava <61847233+neha-bhargava@users.noreply.github.com> Co-authored-by: Peter M <34331512+pmaytak@users.noreply.github.com> --- .../Http/Headers/AuthenticationHeaderParser.cs | 8 ++++---- .../Http/Headers/AuthenticationInfoParameters.cs | 6 ++---- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/src/client/Microsoft.Identity.Client/Http/Headers/AuthenticationHeaderParser.cs b/src/client/Microsoft.Identity.Client/Http/Headers/AuthenticationHeaderParser.cs index 02526ee922..5ab7299711 100644 --- a/src/client/Microsoft.Identity.Client/Http/Headers/AuthenticationHeaderParser.cs +++ b/src/client/Microsoft.Identity.Client/Http/Headers/AuthenticationHeaderParser.cs @@ -27,7 +27,7 @@ public class AuthenticationHeaderParser public IReadOnlyList ParsedWwwAuthenticateParameters { get; private set; } /// - /// Parameters returned by the Authenticatin-Info header. + /// Parameters returned by the Authentication-Info header. /// This allows for authentication scenarios such as Proof-Of-Posession. /// public AuthenticationInfoParameters ParsedAuthenticationInfoParameters { get; private set; } @@ -44,7 +44,7 @@ public class AuthenticationHeaderParser /// public static async Task ParseAuthenticationHeadersAsync(string resourceUri) { - return await ParseAuthenticationHeadersAsync(resourceUri, new CancellationToken()).ConfigureAwait(false); + return await ParseAuthenticationHeadersAsync(resourceUri, default).ConfigureAwait(false); } /// @@ -66,7 +66,7 @@ public static async Task ParseAuthenticationHeadersA /// Instance of to make the request with. /// /// - public static async Task ParseAuthenticationHeadersAsync(string resourceUri, CancellationToken cancellationToken, HttpClient httpClient) + public static async Task ParseAuthenticationHeadersAsync(string resourceUri, HttpClient httpClient, CancellationToken cancellationToken) { if (httpClient is null) { @@ -87,7 +87,7 @@ public static async Task ParseAuthenticationHeadersA /// Creates a parsed set of parameters from the provided HttpResponseHeaders. /// /// - /// For known values, such as the nonce used for Proof-of-Possession, the parser will first chack for in the WWW-Authenticate headers + /// 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. /// public static AuthenticationHeaderParser ParseAuthenticationHeaders(HttpResponseHeaders httpResponseHeaders) diff --git a/src/client/Microsoft.Identity.Client/Http/Headers/AuthenticationInfoParameters.cs b/src/client/Microsoft.Identity.Client/Http/Headers/AuthenticationInfoParameters.cs index 64d2ab8e7f..293ade36e0 100644 --- a/src/client/Microsoft.Identity.Client/Http/Headers/AuthenticationInfoParameters.cs +++ b/src/client/Microsoft.Identity.Client/Http/Headers/AuthenticationInfoParameters.cs @@ -32,7 +32,7 @@ public static AuthenticationInfoParameters CreateFromHeaders(HttpResponseHeaders { AuthenticationInfoParameters parameters = new AuthenticationInfoParameters(); - if (respnseHeaders.Any(header => header.Key == _authenticationInfoKey)) + if (respnseHeaders.Contains(_authenticationInfoKey)) { var authInfoValue = respnseHeaders.Where(header => header.Key == _authenticationInfoKey).Single().Value.FirstOrDefault(); @@ -42,9 +42,7 @@ public static AuthenticationInfoParameters CreateFromHeaders(HttpResponseHeaders .Select(v => AuthenticationHeaderParser.ExtractKeyValuePair(v.Trim())) .ToDictionary(pair => pair.Key, pair => pair.Value, StringComparer.OrdinalIgnoreCase); - string value; - - if (paramValues.TryGetValue("nextnonce", out value)) + if (paramValues.TryGetValue("nextnonce", out string value)) { parameters.NextNonce = value; } From 8949f555f60791253b9ce3621d63fb42cbcbfba2 Mon Sep 17 00:00:00 2001 From: trwalke Date: Tue, 18 Oct 2022 00:59:35 -0700 Subject: [PATCH 12/31] Addressing PR Feedback. Adding additional test coverage --- .../AuthenticationHeaderParser.cs | 23 +-- .../AuthenticationInfoParameters.cs | 63 +++++++ .../Microsoft.Identity.Client.csproj | 4 + .../Microsoft.Identity.Client/MsalError.cs | 5 + .../MsalErrorMessage.cs | 2 + .../Headers => }/WwwAuthenticateParameters.cs | 92 ++++++---- ...wAuthenticateParametersIntegrationTests.cs | 12 +- .../WwwAuthenticateParametersTests.cs | 158 ++++++++++++++++-- 8 files changed, 297 insertions(+), 62 deletions(-) rename src/client/Microsoft.Identity.Client/{Http/Headers => }/AuthenticationHeaderParser.cs (85%) create mode 100644 src/client/Microsoft.Identity.Client/AuthenticationInfoParameters.cs rename src/client/Microsoft.Identity.Client/{Http/Headers => }/WwwAuthenticateParameters.cs (86%) diff --git a/src/client/Microsoft.Identity.Client/Http/Headers/AuthenticationHeaderParser.cs b/src/client/Microsoft.Identity.Client/AuthenticationHeaderParser.cs similarity index 85% rename from src/client/Microsoft.Identity.Client/Http/Headers/AuthenticationHeaderParser.cs rename to src/client/Microsoft.Identity.Client/AuthenticationHeaderParser.cs index 5ab7299711..04193b6f66 100644 --- a/src/client/Microsoft.Identity.Client/Http/Headers/AuthenticationHeaderParser.cs +++ b/src/client/Microsoft.Identity.Client/AuthenticationHeaderParser.cs @@ -24,13 +24,13 @@ public class AuthenticationHeaderParser /// scenarios such as claim challenge, CAE, CA auth context. /// See https://aka.ms/msal-net/wwwAuthenticate. /// - public IReadOnlyList ParsedWwwAuthenticateParameters { get; private set; } + public IReadOnlyList WwwAuthenticateParameters { get; private set; } /// /// Parameters returned by the Authentication-Info header. /// This allows for authentication scenarios such as Proof-Of-Posession. /// - public AuthenticationInfoParameters ParsedAuthenticationInfoParameters { get; private set; } + public AuthenticationInfoParameters AuthenticationInfoParameters { get; private set; } /// /// Nonce parsed from HttpResponseHeaders @@ -55,7 +55,7 @@ public static async Task ParseAuthenticationHeadersA /// public static async Task ParseAuthenticationHeadersAsync(string resourceUri, CancellationToken cancellationToken) { - return await ParseAuthenticationHeadersAsync(resourceUri, cancellationToken, GetHttpClient()).ConfigureAwait(false); + return await ParseAuthenticationHeadersAsync(resourceUri, GetHttpClient(), cancellationToken).ConfigureAwait(false); } /// @@ -93,31 +93,32 @@ public static async Task ParseAuthenticationHeadersA 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 = WwwAuthenticateParameters.CreateFromAuthenticateHeaders(httpResponseHeaders); + var WwwParameters = Client.WwwAuthenticateParameters.CreateFromAuthenticationHeaders(httpResponseHeaders); if (WwwParameters.Any(parameter => parameter.AuthScheme == "PoP")) { serverNonce = WwwParameters.Where(parameter => parameter.AuthScheme == "PoP").Single().ServerNonce; } - authenticationHeaderParser.ParsedWwwAuthenticateParameters = WwwParameters; + authenticationHeaderParser.WwwAuthenticateParameters = WwwParameters; } else { - authenticationHeaderParser.ParsedWwwAuthenticateParameters = new List(); + authenticationHeaderParser.WwwAuthenticateParameters = new List(); + + //If no WWW-AuthenticateHeaders exist, attempt to parse AuthenticationInfo headers instead + authenticationInfoParameters = AuthenticationInfoParameters.CreateFromHeaders(httpResponseHeaders); + authenticationHeaderParser.AuthenticationInfoParameters = authenticationInfoParameters; } - //If no WWW-AuthenticateHeaders exist, attempt to parse AuthenticationInfo headers instead - AuthenticationInfoParameters authenticationInfoParameters = AuthenticationInfoParameters.CreateFromHeaders(httpResponseHeaders); - authenticationHeaderParser.ParsedAuthenticationInfoParameters = authenticationInfoParameters; - //If server nonce is not acquired from WWW-Authenticate headers, use next nonce from Authentication-Info parameters. - authenticationHeaderParser.Nonce = serverNonce?? authenticationInfoParameters.NextNonce; + authenticationHeaderParser.Nonce = serverNonce ?? authenticationInfoParameters.NextNonce; return authenticationHeaderParser; } diff --git a/src/client/Microsoft.Identity.Client/AuthenticationInfoParameters.cs b/src/client/Microsoft.Identity.Client/AuthenticationInfoParameters.cs new file mode 100644 index 0000000000..e70dc11f20 --- /dev/null +++ b/src/client/Microsoft.Identity.Client/AuthenticationInfoParameters.cs @@ -0,0 +1,63 @@ +// 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 System.Text; +using System.Threading.Tasks; +using Microsoft.Identity.Client.Utils; +using Microsoft.Identity.Json.Linq; + +namespace Microsoft.Identity.Client +{ + /// + /// Parameters returned by the Authentication-Info header. This allows for + /// scenarios such as proof-of-possession, etc. + /// See https://learn.microsoft.com/en-us/openspecs/office_protocols/ms-sipae/b3ac8451-ee93-43a8-a51a-baedfdd3bed5. + /// + public class AuthenticationInfoParameters + { + private const string AuthenticationInfoKey = "Authentication-Info"; + /// + /// The next nonce to be used in the preceding authentication request. + /// + public string NextNonce { get; private set; } + + /// + /// Create Authentication-Info parameters from the HttpResponseHeaders for each auth scheme. + /// + /// HttpResponseHeaders. + /// Authentication-Info provided by the endpoint + public static AuthenticationInfoParameters CreateFromHeaders(HttpResponseHeaders httpResponseHeaders) + { + AuthenticationInfoParameters parameters = new AuthenticationInfoParameters(); + + try + { + if (httpResponseHeaders.Contains(AuthenticationInfoKey)) + { + var authInfoValue = httpResponseHeaders.Where(header => header.Key == AuthenticationInfoKey).Single().Value.FirstOrDefault(); + + var AuthValuesSplit = authInfoValue.Split(new char[] { ' ' }, 2); + + var paramValues = CoreHelpers.SplitWithQuotes(AuthValuesSplit[1], ',') + .Select(v => AuthenticationHeaderParser.ExtractKeyValuePair(v.Trim())) + .ToDictionary(pair => pair.Key, pair => pair.Value, StringComparer.OrdinalIgnoreCase); + + if (paramValues.TryGetValue("nextnonce", out string value)) + { + parameters.NextNonce = value; + } + } + } + catch(Exception ex) + { + throw new MsalClientException(MsalError.UnableToParseAuthenticationHeader, MsalErrorMessage.UnableToParseAuthenticationHeader, ex); + } + + return parameters; + } + } +} diff --git a/src/client/Microsoft.Identity.Client/Microsoft.Identity.Client.csproj b/src/client/Microsoft.Identity.Client/Microsoft.Identity.Client.csproj index ae28839acf..02834bb3fe 100644 --- a/src/client/Microsoft.Identity.Client/Microsoft.Identity.Client.csproj +++ b/src/client/Microsoft.Identity.Client/Microsoft.Identity.Client.csproj @@ -328,4 +328,8 @@ + + + + diff --git a/src/client/Microsoft.Identity.Client/MsalError.cs b/src/client/Microsoft.Identity.Client/MsalError.cs index ea6eefe810..fc42eae698 100644 --- a/src/client/Microsoft.Identity.Client/MsalError.cs +++ b/src/client/Microsoft.Identity.Client/MsalError.cs @@ -1096,5 +1096,10 @@ public static class MsalError /// A required value is missing from the token providerresponse /// public const string InvalidTokenProviderResponseValue = "invalid_token_provider_response_value"; + + /// + /// Msal is unable to parse the authentication reader returned from the endpoint + /// + public const string UnableToParseAuthenticationHeader = "unable_to_parse_authenticationh_header"; } } diff --git a/src/client/Microsoft.Identity.Client/MsalErrorMessage.cs b/src/client/Microsoft.Identity.Client/MsalErrorMessage.cs index 24bdd82639..2ed288da48 100644 --- a/src/client/Microsoft.Identity.Client/MsalErrorMessage.cs +++ b/src/client/Microsoft.Identity.Client/MsalErrorMessage.cs @@ -432,6 +432,8 @@ 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 reader returned from the endpoint"; + public static string InvalidTokenProviderResponseValue(string invalidValueName) { return string.Format( diff --git a/src/client/Microsoft.Identity.Client/Http/Headers/WwwAuthenticateParameters.cs b/src/client/Microsoft.Identity.Client/WwwAuthenticateParameters.cs similarity index 86% rename from src/client/Microsoft.Identity.Client/Http/Headers/WwwAuthenticateParameters.cs rename to src/client/Microsoft.Identity.Client/WwwAuthenticateParameters.cs index 0d9819dbf1..4a93b0c6f6 100644 --- a/src/client/Microsoft.Identity.Client/Http/Headers/WwwAuthenticateParameters.cs +++ b/src/client/Microsoft.Identity.Client/WwwAuthenticateParameters.cs @@ -60,21 +60,16 @@ public class WwwAuthenticateParameters /// public string Error { get; set; } - /// - /// Error indicating that parsing failed. - /// - public bool ParsingError { get; private set; } - /// /// AuthScheme. /// See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/WWW-Authenticate#syntax for more details /// - public string AuthScheme { get; set; } + public string AuthScheme { get; private set; } /// /// Server Nonce. /// - public string ServerNonce { get; set; } + public string ServerNonce { get; private set; } /// /// Return the of key . @@ -180,6 +175,17 @@ public static WwwAuthenticateParameters CreateFromResponseHeaders( return CreateWwwAuthenticateParameters(new Dictionary()); } + + /// + /// Creates parameters from the WWW-Authenticate string. + /// + /// String contained in a WWW-Authenticate header. + /// The parameters requested by the web API. + [System.ComponentModel.EditorBrowsable(EditorBrowsableState.Never)] + public static WwwAuthenticateParameters CreateFromWwwAuthenticateHeaderValue(string wwwAuthenticateValue) + { + return CreateFromWwwAuthenticationHeaderValue(wwwAuthenticateValue); + } #endregion Obsolete Api #region Single Scheme Api @@ -201,9 +207,9 @@ public static Task CreateFromAuthenticationResponseAs /// Authentication scheme. /// The cancellation token to cancel operation. /// WWW-Authenticate Parameters extracted from response to the unauthenticated call. - public static Task CreateFromAuthenticationResponseAsync(string resourceUri, string scheme, CancellationToken cancellationToken = default) + public static Task CreateFromAuthenticationResponseAsync(string resourceUri, string scheme, CancellationToken cancellationToken) { - return CreateFromAuthenticationResponseAsync(AuthenticationHeaderParser.GetHttpClient(), resourceUri, scheme, cancellationToken); + return CreateFromAuthenticationResponseAsync(resourceUri, scheme, AuthenticationHeaderParser.GetHttpClient(), cancellationToken); } /// @@ -214,7 +220,7 @@ public static Task CreateFromAuthenticationResponseAs /// The cancellation token to cancel operation. /// Authentication scheme. /// WWW-Authenticate Parameters extracted from response to the unauthenticated call. - public static async Task CreateFromAuthenticationResponseAsync(HttpClient httpClient, string resourceUri, string scheme, CancellationToken cancellationToken = default) + public static async Task CreateFromAuthenticationResponseAsync(string resourceUri, string scheme, HttpClient httpClient, CancellationToken cancellationToken) { if (httpClient is null) { @@ -227,7 +233,7 @@ public static async Task CreateFromAuthenticationResp // call this endpoint and see what the header says and return that HttpResponseMessage httpResponseMessage = await httpClient.GetAsync(resourceUri, cancellationToken).ConfigureAwait(false); - var wwwAuthParams = CreateFromAuthenticateHeaders(httpResponseMessage.Headers, scheme); + var wwwAuthParams = CreateFromAuthenticationHeaders(httpResponseMessage.Headers, scheme); return wwwAuthParams; } @@ -237,19 +243,32 @@ public static async Task CreateFromAuthenticationResp /// HttpResponseHeaders. /// Authentication scheme. /// The parameters requested by the web API. - public static WwwAuthenticateParameters CreateFromAuthenticateHeaders( + public static WwwAuthenticateParameters CreateFromAuthenticationHeaders( HttpResponseHeaders httpResponseHeaders, string scheme) { - if (httpResponseHeaders.WwwAuthenticate.Any(v => string.Equals(v.Scheme, scheme, StringComparison.OrdinalIgnoreCase))) + AuthenticationHeaderValue header = httpResponseHeaders.WwwAuthenticate.FirstOrDefault(v => string.Equals(v.Scheme, scheme, StringComparison.OrdinalIgnoreCase)); + + if (header != null) { - AuthenticationHeaderValue header = httpResponseHeaders.WwwAuthenticate.First(v => string.Equals(v.Scheme, Constants.BearerAuthHeaderPrefix, StringComparison.OrdinalIgnoreCase)); - string wwwAuthenticateValue = header.Parameter; - var parameters = CreateFromWwwAuthenticateHeaderValue(wwwAuthenticateValue); - parameters.AuthScheme = scheme; + WwwAuthenticateParameters parameters; + try + { + parameters = CreateFromWwwAuthenticationHeaderValue(wwwAuthenticateValue); + parameters.AuthScheme = scheme; - return parameters; + return parameters; + } + catch(Exception ex) + { + if (ex is MsalException) + { + throw; + } + + throw new MsalClientException(MsalError.UnableToParseAuthenticationHeader, MsalErrorMessage.UnableToParseAuthenticationHeader, ex); + } } return CreateWwwAuthenticateParameters(new Dictionary()); @@ -264,9 +283,9 @@ public static WwwAuthenticateParameters CreateFromAuthenticateHeaders( /// The claims challenge public static string GetClaimChallengeFromResponseHeaders( HttpResponseHeaders httpResponseHeaders, - string scheme = "Bearer") + string scheme = Constants.BearerAuthHeaderPrefix) { - WwwAuthenticateParameters parameters = CreateFromResponseHeaders( + WwwAuthenticateParameters parameters = CreateFromAuthenticationHeaders( httpResponseHeaders, scheme); @@ -299,9 +318,9 @@ public static Task> CreateFromAuthentic /// URI of the resource. /// The cancellation token to cancel operation. /// WWW-Authenticate Parameters extracted from response to the unauthenticated call. - public static Task> CreateFromAuthenticationResponseAsync(string resourceUri, CancellationToken cancellationToken = default) + public static Task> CreateFromAuthenticationResponseAsync(string resourceUri, CancellationToken cancellationToken) { - return CreateFromAuthenticationResponseAsync(AuthenticationHeaderParser.GetHttpClient(), resourceUri, cancellationToken); + return CreateFromAuthenticationResponseAsync(resourceUri, AuthenticationHeaderParser.GetHttpClient(), cancellationToken); } /// @@ -311,7 +330,7 @@ public static Task> CreateFromAuthentic /// URI of the resource. /// The cancellation token to cancel operation. /// WWW-Authenticate Parameters extracted from response to the unauthenticated call. - public static async Task> CreateFromAuthenticationResponseAsync(HttpClient httpClient, string resourceUri, CancellationToken cancellationToken = default) + public static async Task> CreateFromAuthenticationResponseAsync(string resourceUri, HttpClient httpClient, CancellationToken cancellationToken) { if (httpClient is null) { @@ -324,7 +343,7 @@ public static async Task> CreateFromAut // call this endpoint and see what the header says and return that HttpResponseMessage httpResponseMessage = await httpClient.GetAsync(resourceUri, cancellationToken).ConfigureAwait(false); - var wwwAuthParams = CreateFromAuthenticateHeaders(httpResponseMessage.Headers); + var wwwAuthParams = CreateFromAuthenticationHeaders(httpResponseMessage.Headers); return wwwAuthParams; } @@ -334,16 +353,29 @@ public static async Task> CreateFromAut /// HttpResponseHeaders. /// The parameters requested by the web API. /// Currently it only supports the Bearer scheme - public static IReadOnlyList CreateFromAuthenticateHeaders( + public static IReadOnlyList CreateFromAuthenticationHeaders( HttpResponseHeaders httpResponseHeaders) { List parameterList = new List(); foreach (AuthenticationHeaderValue wwwAuthenticateHeaderValue in httpResponseHeaders.WwwAuthenticate) { - var parameters = CreateFromWwwAuthenticateHeaderValue(wwwAuthenticateHeaderValue.Parameter); - parameters.AuthScheme = wwwAuthenticateHeaderValue.Scheme; - parameterList.Add(parameters); + try + { + var parameters = CreateFromWwwAuthenticationHeaderValue(wwwAuthenticateHeaderValue.Parameter); + parameters.AuthScheme = wwwAuthenticateHeaderValue.Scheme; + + parameterList.Add(parameters); + } + catch (Exception ex) + { + if (ex is MsalException) + { + throw; + } + + throw new MsalClientException(MsalError.UnableToParseAuthenticationHeader, MsalErrorMessage.UnableToParseAuthenticationHeader, ex); + } } return parameterList; @@ -355,7 +387,7 @@ public static IReadOnlyList CreateFromAuthenticateHea /// /// String contained in a WWW-Authenticate header. /// The parameters requested by the web API. - public static WwwAuthenticateParameters CreateFromWwwAuthenticateHeaderValue(string wwwAuthenticateValue) + private static WwwAuthenticateParameters CreateFromWwwAuthenticationHeaderValue(string wwwAuthenticateValue) { if (string.IsNullOrWhiteSpace(wwwAuthenticateValue)) { @@ -392,7 +424,7 @@ internal static WwwAuthenticateParameters CreateWwwAuthenticateParameters(IDicti if (values.Count == 0) { //unable to parse auth header values - wwwAuthenticateParameters.ParsingError = true; + throw new MsalClientException(MsalError.UnableToParseAuthenticationHeader, MsalErrorMessage.UnableToParseAuthenticationHeader); } string value; diff --git a/tests/Microsoft.Identity.Test.Integration.netfx/HeadlessTests/WwwAuthenticateParametersIntegrationTests.cs b/tests/Microsoft.Identity.Test.Integration.netfx/HeadlessTests/WwwAuthenticateParametersIntegrationTests.cs index f8ff5f607c..a03b1d6f4c 100644 --- a/tests/Microsoft.Identity.Test.Integration.netfx/HeadlessTests/WwwAuthenticateParametersIntegrationTests.cs +++ b/tests/Microsoft.Identity.Test.Integration.netfx/HeadlessTests/WwwAuthenticateParametersIntegrationTests.cs @@ -101,21 +101,21 @@ public async Task ExtractNonceWithAuthParserAsync() var parsedHeaders = await AuthenticationHeaderParser.ParseAuthenticationHeadersAsync("https://testingsts.azurewebsites.net/servernonce/invalidsignature").ConfigureAwait(false); //Assert - Assert.IsTrue(parsedHeaders.ParsedWwwAuthenticateParameters.Any(param => param.AuthScheme == Constants.PoPAuthHeaderPrefix)); - var serverNonce = parsedHeaders.ParsedWwwAuthenticateParameters.Where(param => param.AuthScheme == Constants.PoPAuthHeaderPrefix).Single().ServerNonce; + Assert.IsTrue(parsedHeaders.WwwAuthenticateParameters.Any(param => param.AuthScheme == Constants.PoPAuthHeaderPrefix)); + var serverNonce = parsedHeaders.WwwAuthenticateParameters.Where(param => param.AuthScheme == Constants.PoPAuthHeaderPrefix).Single().ServerNonce; Assert.IsNotNull(serverNonce); Assert.AreEqual(parsedHeaders.Nonce, serverNonce); - Assert.IsNull(parsedHeaders.ParsedAuthenticationInfoParameters.NextNonce); + Assert.IsNull(parsedHeaders.AuthenticationInfoParameters); //Arrange & Act //Test for nonce in Authentication-Info header parsedHeaders = await AuthenticationHeaderParser.ParseAuthenticationHeadersAsync("https://testingsts.azurewebsites.net/servernonce/authinfo").ConfigureAwait(false); //Assert - Assert.IsNotNull(parsedHeaders.ParsedAuthenticationInfoParameters.NextNonce); - Assert.AreEqual(parsedHeaders.Nonce, parsedHeaders.ParsedAuthenticationInfoParameters.NextNonce); + Assert.IsNotNull(parsedHeaders.AuthenticationInfoParameters.NextNonce); + Assert.AreEqual(parsedHeaders.Nonce, parsedHeaders.AuthenticationInfoParameters.NextNonce); - Assert.IsFalse(parsedHeaders.ParsedWwwAuthenticateParameters.Any(param => param.AuthScheme == Constants.PoPAuthHeaderPrefix)); + Assert.IsFalse(parsedHeaders.WwwAuthenticateParameters.Any(param => param.AuthScheme == Constants.PoPAuthHeaderPrefix)); } } } diff --git a/tests/Microsoft.Identity.Test.Unit/WwwAuthenticateParametersTests.cs b/tests/Microsoft.Identity.Test.Unit/WwwAuthenticateParametersTests.cs index 2e249f8dbd..4b4d001c1d 100644 --- a/tests/Microsoft.Identity.Test.Unit/WwwAuthenticateParametersTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/WwwAuthenticateParametersTests.cs @@ -18,6 +18,7 @@ namespace Microsoft.Identity.Test.Unit public class WwwAuthenticateParametersTests { private const string WwwAuthenticateHeaderName = "WWW-Authenticate"; + private const string AuthenticationInfoName = "Authentication-Info"; private const string ClientIdKey = "client_id"; private const string ResourceIdKey = "resource_id"; private const string ResourceKey = "resource"; @@ -48,7 +49,7 @@ public void CreateWwwAuthenticateResponse(string resource, string authorizationU httpResponse.Headers.Add(WwwAuthenticateHeaderName, $"Bearer realm=\"\", {resource}, {authorizationUri}"); // Act - var authParams = WwwAuthenticateParameters.CreateFromResponseHeaders(httpResponse.Headers); + var authParams = WwwAuthenticateParameters.CreateFromAuthenticationHeaders(httpResponse.Headers, "Bearer"); // Assert Assert.AreEqual(TestConstants.AuthorityCommonTenant.TrimEnd('/'), authParams.Authority); @@ -57,6 +58,57 @@ public void CreateWwwAuthenticateResponse(string resource, string authorizationU Assert.IsNull(authParams.Error); } + [TestMethod] + [DataRow("Bearer","")] + [DataRow("Bearer", "Bearer Malformed String Malformed String\", \"Malformed String, Malformed String")] + [DataRow("Bearer", "Malformed String Malformed StringMalformed String, Malformed String")] + [DataRow("Pop", "Malformed String Malformed StringMalformed String, Malformed String")] + [DataRow("", "Malformed String Malformed StringMalformed String, Malformed String")] + [DataRow("SomeAuthScheme", "Malformed String Malformed StringMalformed String, Malformed String")] + [DataRow("\'SomeAuthScheme\'", "\'Malformed String Malformed StringMalformed String, Malformed String\'")] + [DataRow("&SomeAuthScheme&", "Malformed String Malformed StringMalformed String, Malformed String")] + public void CreateFromMalformedWwwAuthenticateResponse(string scheme, string value) + { + // Arrange + HttpResponseMessage httpResponse = new HttpResponseMessage(HttpStatusCode.Unauthorized); + httpResponse.Headers.Add(WwwAuthenticateHeaderName, $"{scheme} {value}"); + + // Act + var ex = Assert.ThrowsException(() => + WwwAuthenticateParameters.CreateFromAuthenticationHeaders(httpResponse.Headers, scheme)); + + //Assert + Assert.AreEqual(ex.ErrorCode, MsalError.UnableToParseAuthenticationHeader); + Assert.AreEqual(ex.Message, MsalErrorMessage.UnableToParseAuthenticationHeader); + + if (ex.InnerException != null) + { + //Expected inner exceptions from parsing errors + string innerExceptionType = ex.InnerException.GetType().ToString(); + Assert.IsTrue(innerExceptionType == "System.ArgumentException" || innerExceptionType == "System.ArgumentNullException"); + } + } + + [TestMethod] + [DataRow("nextnonce", "")] + [DataRow("nextnonce", "Some, Malformed, Nonce")] + [DataRow("", "SomeNonce")] + [DataRow("", "Some, Malformed, Nonce")] + public void CreateFromMalformedAuthInfoResponse(string paramName, string value) + { + // Arrange + HttpResponseMessage httpResponse = new HttpResponseMessage(HttpStatusCode.Unauthorized); + httpResponse.Headers.Add(AuthenticationInfoName, $"{paramName}={value}"); + + // Act + var ex = Assert.ThrowsException(() => + AuthenticationInfoParameters.CreateFromHeaders(httpResponse.Headers)); + + //Assert + Assert.AreEqual(ex.ErrorCode, MsalError.UnableToParseAuthenticationHeader); + Assert.AreEqual(ex.Message, MsalErrorMessage.UnableToParseAuthenticationHeader); + } + [TestMethod] [DataRow(ClientIdKey, AuthorizationUriKey)] [DataRow(ClientIdKey, AuthorizationKey)] @@ -158,11 +210,11 @@ public async Task CreateFromResourceResponseAsync_HttpClient_Arm_GetTenantId_Asy Assert.AreEqual(authParams.GetTenantId(), tenantId); - authParams = await WwwAuthenticateParameters.CreateFromAuthenticationResponseAsync(httpClient, resourceUri, "Bearer").ConfigureAwait(false); + authParams = await WwwAuthenticateParameters.CreateFromAuthenticationResponseAsync(resourceUri, "Bearer", httpClient, default).ConfigureAwait(false); Assert.AreEqual(authParams.GetTenantId(), tenantId); - var authParamList = await WwwAuthenticateParameters.CreateFromAuthenticationResponseAsync(httpClient, resourceUri).ConfigureAwait(false); + var authParamList = await WwwAuthenticateParameters.CreateFromAuthenticationResponseAsync(resourceUri, httpClient, default).ConfigureAwait(false); Assert.AreEqual(authParamList.FirstOrDefault().GetTenantId(), tenantId); } @@ -184,11 +236,11 @@ public async Task CreateFromResourceResponseAsync_HttpClient_B2C_GetTenantId_Asy Assert.AreEqual(authParams.GetTenantId(), tenantId); - authParams = await WwwAuthenticateParameters.CreateFromAuthenticationResponseAsync(httpClient, resourceUri, "Bearer").ConfigureAwait(false); + authParams = await WwwAuthenticateParameters.CreateFromAuthenticationResponseAsync(resourceUri, "Bearer", httpClient, default).ConfigureAwait(false); Assert.AreEqual(authParams.GetTenantId(), tenantId); - var authParamList = await WwwAuthenticateParameters.CreateFromAuthenticationResponseAsync(httpClient, resourceUri).ConfigureAwait(false); + var authParamList = await WwwAuthenticateParameters.CreateFromAuthenticationResponseAsync(resourceUri, httpClient, default).ConfigureAwait(false); Assert.AreEqual(authParamList.FirstOrDefault().GetTenantId(), tenantId); } @@ -212,11 +264,11 @@ public async Task CreateFromResourceResponseAsync_HttpClient_ADFS_GetTenantId_Nu Assert.IsNull(authParams.GetTenantId()); - authParams = await WwwAuthenticateParameters.CreateFromAuthenticationResponseAsync(httpClient, resourceUri, "Bearer").ConfigureAwait(false); + authParams = await WwwAuthenticateParameters.CreateFromAuthenticationResponseAsync(resourceUri, "Bearer", httpClient, default).ConfigureAwait(false); Assert.IsNull(authParams.GetTenantId()); - var authParamList = await WwwAuthenticateParameters.CreateFromAuthenticationResponseAsync(httpClient, resourceUri).ConfigureAwait(false); + var authParamList = await WwwAuthenticateParameters.CreateFromAuthenticationResponseAsync(resourceUri, httpClient, default).ConfigureAwait(false); Assert.IsNull(authParamList.FirstOrDefault().GetTenantId()); } @@ -231,11 +283,11 @@ public async Task CreateFromResourceResponseAsync_HttpClient_Null_Async(HttpClie await Assert.ThrowsExceptionAsync(action).ConfigureAwait(false); - action = () => WwwAuthenticateParameters.CreateFromAuthenticationResponseAsync(httpClient, resourceUri, "Bearer"); + action = () => WwwAuthenticateParameters.CreateFromAuthenticationResponseAsync(resourceUri, "Bearer", httpClient, default); await Assert.ThrowsExceptionAsync(action).ConfigureAwait(false); - action = () => WwwAuthenticateParameters.CreateFromAuthenticationResponseAsync(httpClient, resourceUri); + action = () => WwwAuthenticateParameters.CreateFromAuthenticationResponseAsync(resourceUri, httpClient, default); await Assert.ThrowsExceptionAsync(action).ConfigureAwait(false); } @@ -300,7 +352,7 @@ public async Task CreateAllFromResourceResponseAsync_HttpClient_Bearer_Pop_Async }; var httpClient = new HttpClient(handler); - var headers = await WwwAuthenticateParameters.CreateFromAuthenticationResponseAsync(httpClient, resourceUri).ConfigureAwait(false); + var headers = await WwwAuthenticateParameters.CreateFromAuthenticationResponseAsync(resourceUri, httpClient, default).ConfigureAwait(false); var bearerHeader = headers.Where(header => header.AuthScheme == "Bearer").Single(); var popHeader = headers.Where(header => header.AuthScheme == "PoP").Single(); @@ -312,22 +364,76 @@ public async Task CreateAllFromResourceResponseAsync_HttpClient_Bearer_Pop_Async } [TestMethod] - public void ExtractAllParametersFromResponse() + [DataRow(false)] + [DataRow(true)] + public void ExtractAllWWWAuthenticateParametersFromResponse(bool combineHeaders) { // Arrange - HttpResponseMessage httpResponse = CreateBearerAndPopHttpResponse(); + HttpResponseMessage httpResponse = CreateBearerAndPopHttpResponse(combineHeaders); - // Act & Assert - var headers = WwwAuthenticateParameters.CreateFromAuthenticateHeaders(httpResponse.Headers); + // Act + var headers = WwwAuthenticateParameters.CreateFromAuthenticationHeaders(httpResponse.Headers); var bearerHeader = headers.Where(header => header.AuthScheme == "Bearer").Single(); var popHeader = headers.Where(header => header.AuthScheme == "PoP").Single(); + // Assert + Assert.IsNotNull(bearerHeader); + Assert.AreEqual("https://login.microsoftonline.com/TenantId", bearerHeader.Authority); + Assert.IsNotNull(popHeader); + Assert.AreEqual("someNonce", popHeader.ServerNonce); + } + + [TestMethod] + [DataRow(false)] + [DataRow(true)] + public void ExtractAllParametersFromResponseWithAuthParser(bool combineHeaders) + { + // Arrange + HttpResponseMessage httpResponse = CreateBearerAndPopHttpResponse(combineHeaders); + + // Act + var headers = AuthenticationHeaderParser.ParseAuthenticationHeaders(httpResponse.Headers); + var bearerHeader = headers.WwwAuthenticateParameters.Where(header => header.AuthScheme == "Bearer").Single(); + var popHeader = headers.WwwAuthenticateParameters.Where(header => header.AuthScheme == "PoP").Single(); + + // Assert Assert.IsNotNull(bearerHeader); Assert.AreEqual("https://login.microsoftonline.com/TenantId", bearerHeader.Authority); Assert.IsNotNull(popHeader); Assert.AreEqual("someNonce", popHeader.ServerNonce); } + [TestMethod] + public void ExtractAllAuthenticationInfoParametersFromResponse() + { + // Arrange + HttpResponseMessage httpResponse = CreateAuthInfoHttpResponse(); + + // Act + var header = AuthenticationInfoParameters.CreateFromHeaders(httpResponse.Headers); + + // Assert + Assert.IsNotNull(header); + Assert.AreEqual("someNonce", header.NextNonce); + } + + [TestMethod] + [DataRow(false)] + [DataRow(true)] + public void ExtractAllAuthinfoParametersFromResponseWithAuthParser(bool combineHeaders) + { + // Arrange + HttpResponseMessage httpResponse = CreateAuthInfoHttpResponse(); + + // Act + var headers = AuthenticationHeaderParser.ParseAuthenticationHeaders(httpResponse.Headers); + + // Assert + Assert.IsNotNull(headers.AuthenticationInfoParameters); + Assert.AreEqual("someNonce", headers.AuthenticationInfoParameters.NextNonce); + Assert.AreEqual(0, headers.WwwAuthenticateParameters.Count); + } + [TestMethod] //Test for fix https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/issues/3026 public void ExtractParametersFromResponseWithScheme() @@ -380,8 +486,19 @@ private static HttpResponseMessage CreateInvalidTokenHttpErrorResponse(string te }; } - private static HttpResponseMessage CreateBearerAndPopHttpResponse() + private static HttpResponseMessage CreateBearerAndPopHttpResponse(bool combinedChallenge = false) { + if (combinedChallenge) + { + return new HttpResponseMessage(HttpStatusCode.Unauthorized) + { + Headers = + { + { WwwAuthenticateHeaderName, $"Bearer authorization_uri=\"https://login.microsoftonline.com/TenantId/oauth2/authorize\", resource_id=\"https://endpoint/\", PoP nonce=\"someNonce\"" } + } + }; + } + return new HttpResponseMessage(HttpStatusCode.Unauthorized) { Headers = @@ -391,5 +508,16 @@ private static HttpResponseMessage CreateBearerAndPopHttpResponse() } }; } + + private static HttpResponseMessage CreateAuthInfoHttpResponse(bool combinedChallenge = false) + { + return new HttpResponseMessage(HttpStatusCode.Unauthorized) + { + Headers = + { + { AuthenticationInfoName, $"Authentication-Info nextnonce=\"someNonce\"" } + } + }; + } } } From 8c46a01965c03de619d9c398256a85e3ebd21064 Mon Sep 17 00:00:00 2001 From: trwalke Date: Tue, 18 Oct 2022 10:01:48 -0700 Subject: [PATCH 13/31] Refactoring, more tests --- .../AuthenticationHeaderParser.cs | 8 +-- .../AuthenticationInfoParameters.cs | 56 ++++++++++++++---- .../Headers/AuthenticationInfoParameters.cs | 55 ----------------- .../WwwAuthenticateParameters.cs | 4 +- .../TestConstants.cs | 1 + ...wAuthenticateParametersIntegrationTests.cs | 26 +++++++- .../WwwAuthenticateParametersTests.cs | 59 ++++++++++++++----- 7 files changed, 121 insertions(+), 88 deletions(-) delete mode 100644 src/client/Microsoft.Identity.Client/Http/Headers/AuthenticationInfoParameters.cs diff --git a/src/client/Microsoft.Identity.Client/AuthenticationHeaderParser.cs b/src/client/Microsoft.Identity.Client/AuthenticationHeaderParser.cs index 04193b6f66..00708cc002 100644 --- a/src/client/Microsoft.Identity.Client/AuthenticationHeaderParser.cs +++ b/src/client/Microsoft.Identity.Client/AuthenticationHeaderParser.cs @@ -99,14 +99,14 @@ public static AuthenticationHeaderParser ParseAuthenticationHeaders(HttpResponse //Check for WWW-AuthenticateHeaders if (httpResponseHeaders.WwwAuthenticate.Count != 0) { - var WwwParameters = Client.WwwAuthenticateParameters.CreateFromAuthenticationHeaders(httpResponseHeaders); + var wwwParameters = Client.WwwAuthenticateParameters.CreateFromAuthenticationHeaders(httpResponseHeaders); - if (WwwParameters.Any(parameter => parameter.AuthScheme == "PoP")) + if (wwwParameters.Any(parameter => parameter.AuthScheme == "PoP")) { - serverNonce = WwwParameters.Where(parameter => parameter.AuthScheme == "PoP").Single().ServerNonce; + serverNonce = wwwParameters.Where(parameter => parameter.AuthScheme == "PoP").Single().Nonce; } - authenticationHeaderParser.WwwAuthenticateParameters = WwwParameters; + authenticationHeaderParser.WwwAuthenticateParameters = wwwParameters; } else { diff --git a/src/client/Microsoft.Identity.Client/AuthenticationInfoParameters.cs b/src/client/Microsoft.Identity.Client/AuthenticationInfoParameters.cs index e70dc11f20..053df9013a 100644 --- a/src/client/Microsoft.Identity.Client/AuthenticationInfoParameters.cs +++ b/src/client/Microsoft.Identity.Client/AuthenticationInfoParameters.cs @@ -15,7 +15,7 @@ namespace Microsoft.Identity.Client /// /// Parameters returned by the Authentication-Info header. This allows for /// scenarios such as proof-of-possession, etc. - /// See https://learn.microsoft.com/en-us/openspecs/office_protocols/ms-sipae/b3ac8451-ee93-43a8-a51a-baedfdd3bed5. + /// See https://www.rfc-editor.org/rfc/rfc7615 /// public class AuthenticationInfoParameters { @@ -25,6 +25,28 @@ public class AuthenticationInfoParameters /// public string NextNonce { get; private set; } + /// + /// Return the of key . + /// + /// Name of the raw parameter to retrieve. + /// The raw parameter if it exists, + /// or throws a otherwise. + /// + public string this[string key] + { + get + { + return RawParameters[key]; + } + } + + /// + /// 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). + /// + internal IDictionary RawParameters { get; private set; } + /// /// Create Authentication-Info parameters from the HttpResponseHeaders for each auth scheme. /// @@ -40,24 +62,38 @@ public static AuthenticationInfoParameters CreateFromHeaders(HttpResponseHeaders { var authInfoValue = httpResponseHeaders.Where(header => header.Key == AuthenticationInfoKey).Single().Value.FirstOrDefault(); - var AuthValuesSplit = authInfoValue.Split(new char[] { ' ' }, 2); + if (authInfoValue != null) + { + var AuthValuesSplit = authInfoValue.Split(new char[] { ' ' }, 2); - var paramValues = CoreHelpers.SplitWithQuotes(AuthValuesSplit[1], ',') - .Select(v => AuthenticationHeaderParser.ExtractKeyValuePair(v.Trim())) - .ToDictionary(pair => pair.Key, pair => pair.Value, StringComparer.OrdinalIgnoreCase); + var paramValues = CoreHelpers.SplitWithQuotes(AuthValuesSplit[1], ',') + .Select(v => AuthenticationHeaderParser.ExtractKeyValuePair(v.Trim())) + .ToDictionary(pair => pair.Key, pair => pair.Value, StringComparer.OrdinalIgnoreCase); - if (paramValues.TryGetValue("nextnonce", out string value)) - { - parameters.NextNonce = value; + parameters.RawParameters = paramValues; + + if (paramValues.TryGetValue("nextnonce", out string value)) + { + parameters.NextNonce = value; + } + + return parameters; } + + //Could not get Auth info parameters + throw new MsalClientException(MsalError.UnableToParseAuthenticationHeader, MsalErrorMessage.UnableToParseAuthenticationHeader); } + + return parameters; } catch(Exception ex) { + if (ex is MsalClientException) + { + throw; + } throw new MsalClientException(MsalError.UnableToParseAuthenticationHeader, MsalErrorMessage.UnableToParseAuthenticationHeader, ex); } - - return parameters; } } } diff --git a/src/client/Microsoft.Identity.Client/Http/Headers/AuthenticationInfoParameters.cs b/src/client/Microsoft.Identity.Client/Http/Headers/AuthenticationInfoParameters.cs deleted file mode 100644 index 293ade36e0..0000000000 --- a/src/client/Microsoft.Identity.Client/Http/Headers/AuthenticationInfoParameters.cs +++ /dev/null @@ -1,55 +0,0 @@ -// 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 System.Text; -using System.Threading.Tasks; -using Microsoft.Identity.Client.Utils; -using Microsoft.Identity.Json.Linq; - -namespace Microsoft.Identity.Client -{ - /// - /// - /// - public class AuthenticationInfoParameters - { - private const string _authenticationInfoKey = "Authentication-Info"; - /// - /// - /// - public string NextNonce { get; private set; } - - /// - /// - /// - /// - /// - public static AuthenticationInfoParameters CreateFromHeaders(HttpResponseHeaders respnseHeaders) - { - AuthenticationInfoParameters parameters = new AuthenticationInfoParameters(); - - if (respnseHeaders.Contains(_authenticationInfoKey)) - { - var authInfoValue = respnseHeaders.Where(header => header.Key == _authenticationInfoKey).Single().Value.FirstOrDefault(); - - var AuthValuesSplit = authInfoValue.Split(new char[] { ' ' }, 2); - - var paramValues = CoreHelpers.SplitWithQuotes(AuthValuesSplit[1], ',') - .Select(v => AuthenticationHeaderParser.ExtractKeyValuePair(v.Trim())) - .ToDictionary(pair => pair.Key, pair => pair.Value, StringComparer.OrdinalIgnoreCase); - - if (paramValues.TryGetValue("nextnonce", out string value)) - { - parameters.NextNonce = value; - } - - } - - return parameters; - } - } -} diff --git a/src/client/Microsoft.Identity.Client/WwwAuthenticateParameters.cs b/src/client/Microsoft.Identity.Client/WwwAuthenticateParameters.cs index 4a93b0c6f6..f9620e5ad0 100644 --- a/src/client/Microsoft.Identity.Client/WwwAuthenticateParameters.cs +++ b/src/client/Microsoft.Identity.Client/WwwAuthenticateParameters.cs @@ -69,7 +69,7 @@ public class WwwAuthenticateParameters /// /// Server Nonce. /// - public string ServerNonce { get; private set; } + public string Nonce { get; private set; } /// /// Return the of key . @@ -462,7 +462,7 @@ internal static WwwAuthenticateParameters CreateWwwAuthenticateParameters(IDicti if (values.TryGetValue("nonce", out value)) { - wwwAuthenticateParameters.ServerNonce = value.Replace("\"", string.Empty); + wwwAuthenticateParameters.Nonce = value.Replace("\"", string.Empty); } return wwwAuthenticateParameters; diff --git a/tests/Microsoft.Identity.Test.Common/TestConstants.cs b/tests/Microsoft.Identity.Test.Common/TestConstants.cs index af5fecdc70..90c7174125 100644 --- a/tests/Microsoft.Identity.Test.Common/TestConstants.cs +++ b/tests/Microsoft.Identity.Test.Common/TestConstants.cs @@ -175,6 +175,7 @@ public static HashSet s_scope public const string CodeVerifier = "someCodeVerifier"; public const string Nonce = "someNonce"; + public const string Realm = "someRealm"; public const string TestErrCode = "TestErrCode"; public const string iOSBrokerSuberrCode = "TestSuberrCode"; diff --git a/tests/Microsoft.Identity.Test.Integration.netfx/HeadlessTests/WwwAuthenticateParametersIntegrationTests.cs b/tests/Microsoft.Identity.Test.Integration.netfx/HeadlessTests/WwwAuthenticateParametersIntegrationTests.cs index a03b1d6f4c..3d946fc781 100644 --- a/tests/Microsoft.Identity.Test.Integration.netfx/HeadlessTests/WwwAuthenticateParametersIntegrationTests.cs +++ b/tests/Microsoft.Identity.Test.Integration.netfx/HeadlessTests/WwwAuthenticateParametersIntegrationTests.cs @@ -4,11 +4,13 @@ using System; using System.Linq; using System.Net.Http; +using System.Reflection.Metadata; using System.Threading; using System.Threading.Tasks; using Microsoft.Identity.Client; using Microsoft.Identity.Client.Internal; using Microsoft.Identity.Client.PlatformsCommon.Factories; +using Microsoft.Identity.Client.Utils; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace Microsoft.Identity.Test.Integration.HeadlessTests @@ -75,7 +77,27 @@ public async Task ExtractNonceFromWwwAuthHeadersAsync() //Assert Assert.IsTrue(parameterList.Any(param => param.AuthScheme == Constants.PoPAuthHeaderPrefix)); - Assert.IsNotNull(parameterList.Where(param => param.AuthScheme == Constants.PoPAuthHeaderPrefix).Single().ServerNonce); + Assert.IsNotNull(parameterList.Where(param => param.AuthScheme == Constants.PoPAuthHeaderPrefix).Single().Nonce); + } + + [TestMethod] + public async Task ExtractNonceFromWwwAuthHeadersRawPamamsAsync() + { + //Arrange & Act + //Test for nonce in WWW-Authenticate header + string popNonce = string.Empty; + var parameterList = await WwwAuthenticateParameters.CreateFromAuthenticationResponseAsync( + "https://testingsts.azurewebsites.net/servernonce/invalidsignature").ConfigureAwait(false); + + var parameters = parameterList.FirstOrDefault(); + + if (parameters.AuthScheme == "Pop" && parameters.RawParameters.Keys.Contains("nonce")) //Check if next nonce for POP is available + { + popNonce = parameters.RawParameters["nonce"]; + } + + //Assert + Assert.IsTrue(!popNonce.IsNullOrEmpty()); } [TestMethod] @@ -102,7 +124,7 @@ public async Task ExtractNonceWithAuthParserAsync() //Assert Assert.IsTrue(parsedHeaders.WwwAuthenticateParameters.Any(param => param.AuthScheme == Constants.PoPAuthHeaderPrefix)); - var serverNonce = parsedHeaders.WwwAuthenticateParameters.Where(param => param.AuthScheme == Constants.PoPAuthHeaderPrefix).Single().ServerNonce; + var serverNonce = parsedHeaders.WwwAuthenticateParameters.Where(param => param.AuthScheme == Constants.PoPAuthHeaderPrefix).Single().Nonce; Assert.IsNotNull(serverNonce); Assert.AreEqual(parsedHeaders.Nonce, serverNonce); Assert.IsNull(parsedHeaders.AuthenticationInfoParameters); diff --git a/tests/Microsoft.Identity.Test.Unit/WwwAuthenticateParametersTests.cs b/tests/Microsoft.Identity.Test.Unit/WwwAuthenticateParametersTests.cs index 4b4d001c1d..5c697ffa85 100644 --- a/tests/Microsoft.Identity.Test.Unit/WwwAuthenticateParametersTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/WwwAuthenticateParametersTests.cs @@ -58,6 +58,25 @@ public void CreateWwwAuthenticateResponse(string resource, string authorizationU Assert.IsNull(authParams.Error); } + [TestMethod] + [DataRow("WLID1.0", "realm=WindowsLive, policy=MBI_SSL, siteId=\"ssl.live-tst.net\"")] + //[DataRow("NTLM", "dG9rZW42OA==")] TODO: Investigate NTLM support. Since the parameter value is not in the format of a=b, an exception is thrown. + public void CreateWwwAuthenticateResponseForUnknownChallenges(string scheme, string values) + { + // Arrange + HttpResponseMessage httpResponse = new HttpResponseMessage(HttpStatusCode.Unauthorized); + httpResponse.Headers.Add(WwwAuthenticateHeaderName, $"{scheme} {values}"); + + // Act + var authParams = WwwAuthenticateParameters.CreateFromAuthenticationHeaders(httpResponse.Headers, scheme); + + // Assert + Assert.AreEqual(3, authParams.RawParameters.Count); + Assert.AreEqual("WindowsLive", authParams.RawParameters["realm"]); + Assert.AreEqual("MBI_SSL", authParams.RawParameters["policy"]); + Assert.AreEqual("ssl.live-tst.net", authParams.RawParameters["siteId"]); + } + [TestMethod] [DataRow("Bearer","")] [DataRow("Bearer", "Bearer Malformed String Malformed String\", \"Malformed String, Malformed String")] @@ -80,19 +99,12 @@ public void CreateFromMalformedWwwAuthenticateResponse(string scheme, string val //Assert Assert.AreEqual(ex.ErrorCode, MsalError.UnableToParseAuthenticationHeader); Assert.AreEqual(ex.Message, MsalErrorMessage.UnableToParseAuthenticationHeader); - - if (ex.InnerException != null) - { - //Expected inner exceptions from parsing errors - string innerExceptionType = ex.InnerException.GetType().ToString(); - Assert.IsTrue(innerExceptionType == "System.ArgumentException" || innerExceptionType == "System.ArgumentNullException"); - } } [TestMethod] [DataRow("nextnonce", "")] [DataRow("nextnonce", "Some, Malformed, Nonce")] - [DataRow("", "SomeNonce")] + [DataRow("", TestConstants.Nonce)] [DataRow("", "Some, Malformed, Nonce")] public void CreateFromMalformedAuthInfoResponse(string paramName, string value) { @@ -360,7 +372,7 @@ public async Task CreateAllFromResourceResponseAsync_HttpClient_Bearer_Pop_Async Assert.IsNotNull(bearerHeader); Assert.AreEqual("https://login.microsoftonline.com/TenantId", bearerHeader.Authority); Assert.IsNotNull(popHeader); - Assert.AreEqual("someNonce", popHeader.ServerNonce); + Assert.AreEqual(TestConstants.Nonce, popHeader.Nonce); } [TestMethod] @@ -380,7 +392,7 @@ public void ExtractAllWWWAuthenticateParametersFromResponse(bool combineHeaders) Assert.IsNotNull(bearerHeader); Assert.AreEqual("https://login.microsoftonline.com/TenantId", bearerHeader.Authority); Assert.IsNotNull(popHeader); - Assert.AreEqual("someNonce", popHeader.ServerNonce); + Assert.AreEqual(TestConstants.Nonce, popHeader.Nonce); } [TestMethod] @@ -400,7 +412,21 @@ public void ExtractAllParametersFromResponseWithAuthParser(bool combineHeaders) Assert.IsNotNull(bearerHeader); Assert.AreEqual("https://login.microsoftonline.com/TenantId", bearerHeader.Authority); Assert.IsNotNull(popHeader); - Assert.AreEqual("someNonce", popHeader.ServerNonce); + Assert.AreEqual(TestConstants.Nonce, popHeader.Nonce); + } + + [TestMethod] + public void ExtractAuthenticationInfoParametersFromResponse() + { + // Arrange + HttpResponseMessage httpResponse = CreateAuthInfoHttpResponse(); + + // Act + var header = AuthenticationInfoParameters.CreateFromHeaders(httpResponse.Headers); + + // Assert + Assert.IsNotNull(header); + Assert.AreEqual(TestConstants.Nonce, header.NextNonce); } [TestMethod] @@ -411,10 +437,13 @@ public void ExtractAllAuthenticationInfoParametersFromResponse() // Act var header = AuthenticationInfoParameters.CreateFromHeaders(httpResponse.Headers); + var nextNonce = header.RawParameters["nextnonce"]; + var realm = header.RawParameters["realm"]; // Assert Assert.IsNotNull(header); - Assert.AreEqual("someNonce", header.NextNonce); + Assert.AreEqual(TestConstants.Nonce, nextNonce); + Assert.AreEqual(TestConstants.Realm, realm); } [TestMethod] @@ -430,7 +459,7 @@ public void ExtractAllAuthinfoParametersFromResponseWithAuthParser(bool combineH // Assert Assert.IsNotNull(headers.AuthenticationInfoParameters); - Assert.AreEqual("someNonce", headers.AuthenticationInfoParameters.NextNonce); + Assert.AreEqual(TestConstants.Nonce, headers.AuthenticationInfoParameters.NextNonce); Assert.AreEqual(0, headers.WwwAuthenticateParameters.Count); } @@ -509,13 +538,13 @@ private static HttpResponseMessage CreateBearerAndPopHttpResponse(bool combinedC }; } - private static HttpResponseMessage CreateAuthInfoHttpResponse(bool combinedChallenge = false) + private static HttpResponseMessage CreateAuthInfoHttpResponse() { return new HttpResponseMessage(HttpStatusCode.Unauthorized) { Headers = { - { AuthenticationInfoName, $"Authentication-Info nextnonce=\"someNonce\"" } + { AuthenticationInfoName, $"PoP nextnonce=\"{TestConstants.Nonce}\", realm = \"{TestConstants.Realm}\"" } } }; } From e87abff3fc2b2fd7e147cae1f2329ccc64c907e7 Mon Sep 17 00:00:00 2001 From: trwalke Date: Tue, 18 Oct 2022 10:54:20 -0700 Subject: [PATCH 14/31] Build fix --- .../HeadlessTests/WwwAuthenticateParametersIntegrationTests.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/Microsoft.Identity.Test.Integration.netfx/HeadlessTests/WwwAuthenticateParametersIntegrationTests.cs b/tests/Microsoft.Identity.Test.Integration.netfx/HeadlessTests/WwwAuthenticateParametersIntegrationTests.cs index 3d946fc781..c6c495cb9b 100644 --- a/tests/Microsoft.Identity.Test.Integration.netfx/HeadlessTests/WwwAuthenticateParametersIntegrationTests.cs +++ b/tests/Microsoft.Identity.Test.Integration.netfx/HeadlessTests/WwwAuthenticateParametersIntegrationTests.cs @@ -4,7 +4,6 @@ using System; using System.Linq; using System.Net.Http; -using System.Reflection.Metadata; using System.Threading; using System.Threading.Tasks; using Microsoft.Identity.Client; From 4f4cc6850a9f29f3a674ddf97fcc3cdcb498f9dd Mon Sep 17 00:00:00 2001 From: trwalke Date: Tue, 18 Oct 2022 22:17:54 -0700 Subject: [PATCH 15/31] Adding additional tests --- .../AuthenticationHeaderParser.cs | 2 +- .../TestCommon.cs | 23 ++++++++- ...wAuthenticateParametersIntegrationTests.cs | 48 ++++++++++++++++++- 3 files changed, 70 insertions(+), 3 deletions(-) diff --git a/src/client/Microsoft.Identity.Client/AuthenticationHeaderParser.cs b/src/client/Microsoft.Identity.Client/AuthenticationHeaderParser.cs index 00708cc002..3e4294f203 100644 --- a/src/client/Microsoft.Identity.Client/AuthenticationHeaderParser.cs +++ b/src/client/Microsoft.Identity.Client/AuthenticationHeaderParser.cs @@ -124,7 +124,7 @@ public static AuthenticationHeaderParser ParseAuthenticationHeaders(HttpResponse } /// - /// Created an HttpClient + /// Creates a HttpClient /// internal static HttpClient GetHttpClient() { diff --git a/tests/Microsoft.Identity.Test.Common/TestCommon.cs b/tests/Microsoft.Identity.Test.Common/TestCommon.cs index 581510156d..0c6d49ec1c 100644 --- a/tests/Microsoft.Identity.Test.Common/TestCommon.cs +++ b/tests/Microsoft.Identity.Test.Common/TestCommon.cs @@ -6,8 +6,9 @@ using System.Globalization; using System.Linq; using System.Net; +using System.Net.Http; using System.Text; - +using System.Threading.Tasks; using Microsoft.Identity.Client; using Microsoft.Identity.Client.ApiConfig.Parameters; using Microsoft.Identity.Client.Http; @@ -241,5 +242,25 @@ public static void ValidateNoKerberosTicketFromToken(string token) KerberosSupplementalTicket ticket = KerberosSupplementalTicketManager.FromIdToken(token); Assert.IsNull(ticket, "Kerberos Ticket exists."); } + + + public static async Task ValidatePopNonceAsync(string nonce) + { + var httpClientFactory = PlatformProxyFactory.CreatePlatformProxy(null).CreateDefaultHttpClientFactory(); + var HttpClient = httpClientFactory.GetHttpClient(); + var response = await HttpClient.GetAsync($"https://testingsts.azurewebsites.net/servernonce/validate?serverNonce={nonce}").ConfigureAwait(false); + + Assert.AreEqual(response.StatusCode, System.Net.HttpStatusCode.OK); + } + + //Pop SHR validation endpoint is currently not functioning + //public static async Task ValidatePopShrAsync(string popShr) + //{ + // var httpClientFactory = PlatformProxyFactory.CreatePlatformProxy(null).CreateDefaultHttpClientFactory(); + // var HttpClient = httpClientFactory.GetHttpClient(); + // var response = await HttpClient.GetAsync($"https://testingsts.azurewebsites.net/servernonce/validate?shr={popShr}").ConfigureAwait(false); + + // Assert.AreEqual(response.StatusCode, System.Net.HttpStatusCode.OK); + //} } } diff --git a/tests/Microsoft.Identity.Test.Integration.netfx/HeadlessTests/WwwAuthenticateParametersIntegrationTests.cs b/tests/Microsoft.Identity.Test.Integration.netfx/HeadlessTests/WwwAuthenticateParametersIntegrationTests.cs index c6c495cb9b..1bba37e9e2 100644 --- a/tests/Microsoft.Identity.Test.Integration.netfx/HeadlessTests/WwwAuthenticateParametersIntegrationTests.cs +++ b/tests/Microsoft.Identity.Test.Integration.netfx/HeadlessTests/WwwAuthenticateParametersIntegrationTests.cs @@ -7,9 +7,15 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Identity.Client; +#if NET_CORE +using Microsoft.Identity.Client.Broker; +#endif using Microsoft.Identity.Client.Internal; using Microsoft.Identity.Client.PlatformsCommon.Factories; using Microsoft.Identity.Client.Utils; +using Microsoft.Identity.Test.Common; +using Microsoft.Identity.Test.Integration.Infrastructure; +using Microsoft.Identity.Test.LabInfrastructure; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace Microsoft.Identity.Test.Integration.HeadlessTests @@ -17,6 +23,8 @@ namespace Microsoft.Identity.Test.Integration.HeadlessTests [TestClass] public class WwwAuthenticateParametersIntegrationTests { + private const string ProtectedUrl = "https://www.contoso.com/path1/path2?queryParam1=a&queryParam2=b"; + [TestMethod] public async Task CreateWwwAuthenticateResponseFromKeyVaultUrlAsync() { @@ -90,13 +98,14 @@ public async Task ExtractNonceFromWwwAuthHeadersRawPamamsAsync() var parameters = parameterList.FirstOrDefault(); - if (parameters.AuthScheme == "Pop" && parameters.RawParameters.Keys.Contains("nonce")) //Check if next nonce for POP is available + if (parameters.AuthScheme == "PoP" && parameters.RawParameters.Keys.Contains("nonce")) //Check if next nonce for POP is available { popNonce = parameters.RawParameters["nonce"]; } //Assert Assert.IsTrue(!popNonce.IsNullOrEmpty()); + await TestCommon.ValidatePopNonceAsync(popNonce).ConfigureAwait(false); } [TestMethod] @@ -112,6 +121,7 @@ public async Task ExtractNonceFromAuthInfoHeadersAsync() var authInfoParameters = AuthenticationInfoParameters.CreateFromHeaders(httpResponseMessage.Headers); Assert.IsNotNull(authInfoParameters); Assert.IsNotNull(authInfoParameters.NextNonce); + await TestCommon.ValidatePopNonceAsync(authInfoParameters.NextNonce).ConfigureAwait(false); } [TestMethod] @@ -137,6 +147,42 @@ public async Task ExtractNonceWithAuthParserAsync() Assert.AreEqual(parsedHeaders.Nonce, parsedHeaders.AuthenticationInfoParameters.NextNonce); Assert.IsFalse(parsedHeaders.WwwAuthenticateParameters.Any(param => param.AuthScheme == Constants.PoPAuthHeaderPrefix)); + await TestCommon.ValidatePopNonceAsync(parsedHeaders.Nonce).ConfigureAwait(false); + } + +#if NET_CORE + [TestMethod] + [Ignore("Pop SHR validation endpoint is currently not functioning")] + public async Task WamUsernamePasswordRequestWithPOPAsync() + { + var labResponse = await LabUserHelper.GetDefaultUserAsync().ConfigureAwait(false); + string[] scopes = { "User.Read" }; + string[] expectedScopes = { "email", "offline_access", "openid", "profile", "User.Read" }; + + //Arrange & Act + //Test for nonce in WWW-Authenticate header + var parsedHeaders = await AuthenticationHeaderParser.ParseAuthenticationHeadersAsync("https://testingsts.azurewebsites.net/servernonce/invalidsignature").ConfigureAwait(false); + + IPublicClientApplication pca = PublicClientApplicationBuilder + .Create(labResponse.App.AppId) + .WithAuthority(labResponse.Lab.Authority, "organizations") + .WithExperimentalFeatures() + .WithBrokerPreview().Build(); + + Assert.IsTrue(pca.IsProofOfPossessionSupportedByClient(), "Either the broker is not configured or it does not support POP."); + + var result = await pca + .AcquireTokenByUsernamePassword( + scopes, + labResponse.User.Upn, + labResponse.User.GetOrFetchPassword()) + .WithProofOfPossession(parsedHeaders.Nonce, HttpMethod.Get, new Uri(ProtectedUrl)) + .ExecuteAsync().ConfigureAwait(false); + + MsalAssert.AssertAuthResult(result, TokenSource.Broker, labResponse.Lab.TenantId, expectedScopes, true); + + //await TestCommon.ValidatePopShrAsync(result.AccessToken).ConfigureAwait(false); } +#endif } } From 264921f780fabb3ed77e26310149041e8d4a39dc Mon Sep 17 00:00:00 2001 From: trwalke Date: Thu, 20 Oct 2022 00:20:54 -0700 Subject: [PATCH 16/31] Adding NTLM support --- .../WwwAuthenticateParameters.cs | 42 +++++++++++++------ ...wAuthenticateParametersIntegrationTests.cs | 2 +- .../WwwAuthenticateParametersTests.cs | 18 +++++--- 3 files changed, 44 insertions(+), 18 deletions(-) diff --git a/src/client/Microsoft.Identity.Client/WwwAuthenticateParameters.cs b/src/client/Microsoft.Identity.Client/WwwAuthenticateParameters.cs index f9620e5ad0..9678daa890 100644 --- a/src/client/Microsoft.Identity.Client/WwwAuthenticateParameters.cs +++ b/src/client/Microsoft.Identity.Client/WwwAuthenticateParameters.cs @@ -173,7 +173,7 @@ public static WwwAuthenticateParameters CreateFromResponseHeaders( } } - return CreateWwwAuthenticateParameters(new Dictionary()); + return CreateWwwAuthenticateParameters(new Dictionary(), string.Empty); } /// @@ -184,7 +184,7 @@ public static WwwAuthenticateParameters CreateFromResponseHeaders( [System.ComponentModel.EditorBrowsable(EditorBrowsableState.Never)] public static WwwAuthenticateParameters CreateFromWwwAuthenticateHeaderValue(string wwwAuthenticateValue) { - return CreateFromWwwAuthenticationHeaderValue(wwwAuthenticateValue); + return CreateFromWwwAuthenticationHeaderValue(wwwAuthenticateValue, string.Empty); } #endregion Obsolete Api @@ -255,8 +255,7 @@ public static WwwAuthenticateParameters CreateFromAuthenticationHeaders( WwwAuthenticateParameters parameters; try { - parameters = CreateFromWwwAuthenticationHeaderValue(wwwAuthenticateValue); - parameters.AuthScheme = scheme; + parameters = CreateFromWwwAuthenticationHeaderValue(wwwAuthenticateValue, scheme); return parameters; } @@ -271,7 +270,7 @@ public static WwwAuthenticateParameters CreateFromAuthenticationHeaders( } } - return CreateWwwAuthenticateParameters(new Dictionary()); + return CreateWwwAuthenticateParameters(new Dictionary(), string.Empty); } /// @@ -362,9 +361,7 @@ public static IReadOnlyList CreateFromAuthenticationH { try { - var parameters = CreateFromWwwAuthenticationHeaderValue(wwwAuthenticateHeaderValue.Parameter); - parameters.AuthScheme = wwwAuthenticateHeaderValue.Scheme; - + var parameters = CreateFromWwwAuthenticationHeaderValue(wwwAuthenticateHeaderValue.Parameter, wwwAuthenticateHeaderValue.Scheme); parameterList.Add(parameters); } catch (Exception ex) @@ -386,13 +383,33 @@ public static IReadOnlyList CreateFromAuthenticationH /// Creates parameters from the WWW-Authenticate string. /// /// String contained in a WWW-Authenticate header. + /// Auth scheme of the result. /// The parameters requested by the web API. - private static WwwAuthenticateParameters CreateFromWwwAuthenticationHeaderValue(string wwwAuthenticateValue) + private static WwwAuthenticateParameters CreateFromWwwAuthenticationHeaderValue(string wwwAuthenticateValue, string scheme) { if (string.IsNullOrWhiteSpace(wwwAuthenticateValue)) { throw new ArgumentNullException(nameof(wwwAuthenticateValue)); } + + if (string.IsNullOrWhiteSpace(scheme)) + { + throw new ArgumentNullException(nameof(scheme)); + } + + //Special NTLM case does not have an a=b format + if (scheme == "NTLM") + { + return new WwwAuthenticateParameters + { + RawParameters = new Dictionary() + { + { scheme, wwwAuthenticateValue } + }, + AuthScheme = scheme + }; + } + IDictionary parameters; var authValuesSplit = wwwAuthenticateValue.Split(new char[] { ' ' }, 2); @@ -402,7 +419,6 @@ private static WwwAuthenticateParameters CreateFromWwwAuthenticationHeaderValue( parameters = CoreHelpers.SplitWithQuotes(authValuesSplit[1], ',') .Select(v => AuthenticationHeaderParser.ExtractKeyValuePair(v.Trim())) .ToDictionary(pair => pair.Key, pair => pair.Value, StringComparer.OrdinalIgnoreCase); - } else { @@ -411,10 +427,10 @@ private static WwwAuthenticateParameters CreateFromWwwAuthenticationHeaderValue( .ToDictionary(pair => pair.Key, pair => pair.Value, StringComparer.OrdinalIgnoreCase); } - return CreateWwwAuthenticateParameters(parameters); + return CreateWwwAuthenticateParameters(parameters, scheme); } - internal static WwwAuthenticateParameters CreateWwwAuthenticateParameters(IDictionary values) + internal static WwwAuthenticateParameters CreateWwwAuthenticateParameters(IDictionary values, string scheme) { WwwAuthenticateParameters wwwAuthenticateParameters = new WwwAuthenticateParameters { @@ -465,6 +481,8 @@ internal static WwwAuthenticateParameters CreateWwwAuthenticateParameters(IDicti wwwAuthenticateParameters.Nonce = value.Replace("\"", string.Empty); } + wwwAuthenticateParameters.AuthScheme = scheme; + return wwwAuthenticateParameters; } diff --git a/tests/Microsoft.Identity.Test.Integration.netfx/HeadlessTests/WwwAuthenticateParametersIntegrationTests.cs b/tests/Microsoft.Identity.Test.Integration.netfx/HeadlessTests/WwwAuthenticateParametersIntegrationTests.cs index 1bba37e9e2..e9adcdc313 100644 --- a/tests/Microsoft.Identity.Test.Integration.netfx/HeadlessTests/WwwAuthenticateParametersIntegrationTests.cs +++ b/tests/Microsoft.Identity.Test.Integration.netfx/HeadlessTests/WwwAuthenticateParametersIntegrationTests.cs @@ -153,7 +153,7 @@ public async Task ExtractNonceWithAuthParserAsync() #if NET_CORE [TestMethod] [Ignore("Pop SHR validation endpoint is currently not functioning")] - public async Task WamUsernamePasswordRequestWithPOPAsync() + public async Task ExtractNonceWithAuthParserAndValidateShrAsync() { var labResponse = await LabUserHelper.GetDefaultUserAsync().ConfigureAwait(false); string[] scopes = { "User.Read" }; diff --git a/tests/Microsoft.Identity.Test.Unit/WwwAuthenticateParametersTests.cs b/tests/Microsoft.Identity.Test.Unit/WwwAuthenticateParametersTests.cs index 5c697ffa85..ee16c917e2 100644 --- a/tests/Microsoft.Identity.Test.Unit/WwwAuthenticateParametersTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/WwwAuthenticateParametersTests.cs @@ -60,7 +60,7 @@ public void CreateWwwAuthenticateResponse(string resource, string authorizationU [TestMethod] [DataRow("WLID1.0", "realm=WindowsLive, policy=MBI_SSL, siteId=\"ssl.live-tst.net\"")] - //[DataRow("NTLM", "dG9rZW42OA==")] TODO: Investigate NTLM support. Since the parameter value is not in the format of a=b, an exception is thrown. + [DataRow("NTLM", "dG9rZW42OA==")] public void CreateWwwAuthenticateResponseForUnknownChallenges(string scheme, string values) { // Arrange @@ -71,10 +71,18 @@ public void CreateWwwAuthenticateResponseForUnknownChallenges(string scheme, str var authParams = WwwAuthenticateParameters.CreateFromAuthenticationHeaders(httpResponse.Headers, scheme); // Assert - Assert.AreEqual(3, authParams.RawParameters.Count); - Assert.AreEqual("WindowsLive", authParams.RawParameters["realm"]); - Assert.AreEqual("MBI_SSL", authParams.RawParameters["policy"]); - Assert.AreEqual("ssl.live-tst.net", authParams.RawParameters["siteId"]); + if (scheme == "NTLM") + { + Assert.AreEqual(scheme, authParams.AuthScheme); + Assert.AreEqual(values, authParams.RawParameters[scheme]); + } + else + { + Assert.AreEqual(3, authParams.RawParameters.Count); + Assert.AreEqual("WindowsLive", authParams.RawParameters["realm"]); + Assert.AreEqual("MBI_SSL", authParams.RawParameters["policy"]); + Assert.AreEqual("ssl.live-tst.net", authParams.RawParameters["siteId"]); + } } [TestMethod] From 8fbd0e452234f325744cfc49a9ee906c83a7953c Mon Sep 17 00:00:00 2001 From: Travis Walker Date: Mon, 31 Oct 2022 22:24:56 -0700 Subject: [PATCH 17/31] Apply suggestions from code review Co-authored-by: Gladwin Johnson <90415114+gladjohn@users.noreply.github.com> Co-authored-by: Peter M <34331512+pmaytak@users.noreply.github.com> --- .../Microsoft.Identity.Client/AuthenticationHeaderParser.cs | 6 +++--- .../AuthenticationInfoParameters.cs | 2 +- src/client/Microsoft.Identity.Client/MsalError.cs | 4 ++-- src/client/Microsoft.Identity.Client/MsalErrorMessage.cs | 3 ++- 4 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/client/Microsoft.Identity.Client/AuthenticationHeaderParser.cs b/src/client/Microsoft.Identity.Client/AuthenticationHeaderParser.cs index 3e4294f203..ba10b6a326 100644 --- a/src/client/Microsoft.Identity.Client/AuthenticationHeaderParser.cs +++ b/src/client/Microsoft.Identity.Client/AuthenticationHeaderParser.cs @@ -28,7 +28,7 @@ public class AuthenticationHeaderParser /// /// Parameters returned by the Authentication-Info header. - /// This allows for authentication scenarios such as Proof-Of-Posession. + /// This allows for authentication scenarios such as Proof-Of-Possession. /// public AuthenticationInfoParameters AuthenticationInfoParameters { get; private set; } @@ -51,7 +51,7 @@ public static async Task ParseAuthenticationHeadersA /// Creates the authenticate parameters by attempting to call the resource unauthenticated, and analyzing the response. /// /// URI of the resource. - /// The cancellation token to cancel operation. + /// The cancellation token to cancel the operation. /// public static async Task ParseAuthenticationHeadersAsync(string resourceUri, CancellationToken cancellationToken) { @@ -103,7 +103,7 @@ public static AuthenticationHeaderParser ParseAuthenticationHeaders(HttpResponse if (wwwParameters.Any(parameter => parameter.AuthScheme == "PoP")) { - serverNonce = wwwParameters.Where(parameter => parameter.AuthScheme == "PoP").Single().Nonce; + serverNonce = wwwParameters.Single(parameter => parameter.AuthScheme == "PoP").Nonce; } authenticationHeaderParser.WwwAuthenticateParameters = wwwParameters; diff --git a/src/client/Microsoft.Identity.Client/AuthenticationInfoParameters.cs b/src/client/Microsoft.Identity.Client/AuthenticationInfoParameters.cs index 053df9013a..c71de770dd 100644 --- a/src/client/Microsoft.Identity.Client/AuthenticationInfoParameters.cs +++ b/src/client/Microsoft.Identity.Client/AuthenticationInfoParameters.cs @@ -60,7 +60,7 @@ public static AuthenticationInfoParameters CreateFromHeaders(HttpResponseHeaders { if (httpResponseHeaders.Contains(AuthenticationInfoKey)) { - var authInfoValue = httpResponseHeaders.Where(header => header.Key == AuthenticationInfoKey).Single().Value.FirstOrDefault(); + var authInfoValue = httpResponseHeaders.Single(header => header.Key == AuthenticationInfoKey).Value.FirstOrDefault(); if (authInfoValue != null) { diff --git a/src/client/Microsoft.Identity.Client/MsalError.cs b/src/client/Microsoft.Identity.Client/MsalError.cs index fc42eae698..6bb62134b1 100644 --- a/src/client/Microsoft.Identity.Client/MsalError.cs +++ b/src/client/Microsoft.Identity.Client/MsalError.cs @@ -1098,8 +1098,8 @@ public static class MsalError public const string InvalidTokenProviderResponseValue = "invalid_token_provider_response_value"; /// - /// Msal is unable to parse the authentication reader returned from the endpoint + /// Unable to parse the authentication header returned from the resource endpoint /// - public const string UnableToParseAuthenticationHeader = "unable_to_parse_authenticationh_header"; + public const string UnableToParseAuthenticationHeader = "unable_to_parse_authentication_header"; } } diff --git a/src/client/Microsoft.Identity.Client/MsalErrorMessage.cs b/src/client/Microsoft.Identity.Client/MsalErrorMessage.cs index 2ed288da48..449184319e 100644 --- a/src/client/Microsoft.Identity.Client/MsalErrorMessage.cs +++ b/src/client/Microsoft.Identity.Client/MsalErrorMessage.cs @@ -432,7 +432,8 @@ 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 reader returned from the endpoint"; + public const string UnableToParseAuthenticationHeader = "MSAL is unable to parse the authentication header returned from the resource endpoint."; + public static string InvalidTokenProviderResponseValue(string invalidValueName) { From 6a58dacbdf8b6b324b6d4bdcd2a56cd7b14f298c Mon Sep 17 00:00:00 2001 From: Travis Walker Date: Tue, 1 Nov 2022 16:56:42 -0700 Subject: [PATCH 18/31] Update src/client/Microsoft.Identity.Client/WwwAuthenticateParameters.cs Co-authored-by: Peter M <34331512+pmaytak@users.noreply.github.com> --- .../Microsoft.Identity.Client/WwwAuthenticateParameters.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/Microsoft.Identity.Client/WwwAuthenticateParameters.cs b/src/client/Microsoft.Identity.Client/WwwAuthenticateParameters.cs index 9678daa890..4561a0cce2 100644 --- a/src/client/Microsoft.Identity.Client/WwwAuthenticateParameters.cs +++ b/src/client/Microsoft.Identity.Client/WwwAuthenticateParameters.cs @@ -162,7 +162,7 @@ public static WwwAuthenticateParameters CreateFromResponseHeaders( { if (httpResponseHeaders.WwwAuthenticate.Any()) { - AuthenticationHeaderValue headerValue = httpResponseHeaders.WwwAuthenticate.FirstOrDefault(v => string.Equals(v.Scheme, Constants.BearerAuthHeaderPrefix, StringComparison.OrdinalIgnoreCase)); + AuthenticationHeaderValue headerValue = httpResponseHeaders.WwwAuthenticate.FirstOrDefault(v => string.Equals(v.Scheme, scheme, StringComparison.OrdinalIgnoreCase)); if (headerValue != null) { From 9b31775e3f3c43aeb720b6db4bf4f601e41d8aeb Mon Sep 17 00:00:00 2001 From: Travis Walker Date: Tue, 1 Nov 2022 16:56:52 -0700 Subject: [PATCH 19/31] Update src/client/Microsoft.Identity.Client/WwwAuthenticateParameters.cs Co-authored-by: Peter M <34331512+pmaytak@users.noreply.github.com> --- .../Microsoft.Identity.Client/WwwAuthenticateParameters.cs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/client/Microsoft.Identity.Client/WwwAuthenticateParameters.cs b/src/client/Microsoft.Identity.Client/WwwAuthenticateParameters.cs index 4561a0cce2..28fe54d257 100644 --- a/src/client/Microsoft.Identity.Client/WwwAuthenticateParameters.cs +++ b/src/client/Microsoft.Identity.Client/WwwAuthenticateParameters.cs @@ -364,13 +364,8 @@ public static IReadOnlyList CreateFromAuthenticationH var parameters = CreateFromWwwAuthenticationHeaderValue(wwwAuthenticateHeaderValue.Parameter, wwwAuthenticateHeaderValue.Scheme); parameterList.Add(parameters); } - catch (Exception ex) + catch (Exception ex) when (ex is not MsalException) { - if (ex is MsalException) - { - throw; - } - throw new MsalClientException(MsalError.UnableToParseAuthenticationHeader, MsalErrorMessage.UnableToParseAuthenticationHeader, ex); } } From b068accc02c39043b1998fac161bf39462f10885 Mon Sep 17 00:00:00 2001 From: trwalke Date: Tue, 1 Nov 2022 22:43:08 -0700 Subject: [PATCH 20/31] Recfactoring --- .../AuthenticationHeaderParser.cs | 15 ++++----- .../AuthenticationInfoParameters.cs | 32 ++++++++----------- .../Microsoft.Identity.Client.csproj | 4 --- .../Microsoft.Identity.Client/MsalError.cs | 2 +- .../MsalErrorMessage.cs | 2 +- .../WwwAuthenticateParametersTests.cs | 2 +- 6 files changed, 23 insertions(+), 34 deletions(-) diff --git a/src/client/Microsoft.Identity.Client/AuthenticationHeaderParser.cs b/src/client/Microsoft.Identity.Client/AuthenticationHeaderParser.cs index ba10b6a326..261713abf0 100644 --- a/src/client/Microsoft.Identity.Client/AuthenticationHeaderParser.cs +++ b/src/client/Microsoft.Identity.Client/AuthenticationHeaderParser.cs @@ -15,10 +15,13 @@ namespace Microsoft.Identity.Client { /// - /// Parsed authentication headers to retreve header values from HttpResponseHeaders. + /// Parsed authentication headers to retrieve header values from HttpResponseHeaders. /// public class AuthenticationHeaderParser { + private static readonly Lazy _httpClientFactory = new Lazy( + () => PlatformProxyFactory.CreatePlatformProxy(null).CreateDefaultHttpClientFactory()); + /// /// Parameters returned by the WWW-Authenticate header. This allows for dynamic /// scenarios such as claim challenge, CAE, CA auth context. @@ -33,7 +36,7 @@ public class AuthenticationHeaderParser public AuthenticationInfoParameters AuthenticationInfoParameters { get; private set; } /// - /// Nonce parsed from HttpResponseHeaders + /// Nonce parsed from HttpResponseHeaders. This is acquired from with the POP WWW-Authenticate header or the Authetnciation-Info header /// public string Nonce { get; private set; } @@ -101,10 +104,7 @@ public static AuthenticationHeaderParser ParseAuthenticationHeaders(HttpResponse { var wwwParameters = Client.WwwAuthenticateParameters.CreateFromAuthenticationHeaders(httpResponseHeaders); - if (wwwParameters.Any(parameter => parameter.AuthScheme == "PoP")) - { - serverNonce = wwwParameters.Single(parameter => parameter.AuthScheme == "PoP").Nonce; - } + serverNonce = wwwParameters.SingleOrDefault(parameter => parameter.AuthScheme == "PoP")?.Nonce; authenticationHeaderParser.WwwAuthenticateParameters = wwwParameters; } @@ -128,8 +128,7 @@ public static AuthenticationHeaderParser ParseAuthenticationHeaders(HttpResponse /// internal static HttpClient GetHttpClient() { - var httpClientFactory = PlatformProxyFactory.CreatePlatformProxy(null).CreateDefaultHttpClientFactory(); - return httpClientFactory.GetHttpClient(); + return _httpClientFactory.Value.GetHttpClient(); } /// diff --git a/src/client/Microsoft.Identity.Client/AuthenticationInfoParameters.cs b/src/client/Microsoft.Identity.Client/AuthenticationInfoParameters.cs index c71de770dd..a0eb143e3e 100644 --- a/src/client/Microsoft.Identity.Client/AuthenticationInfoParameters.cs +++ b/src/client/Microsoft.Identity.Client/AuthenticationInfoParameters.cs @@ -60,38 +60,32 @@ public static AuthenticationInfoParameters CreateFromHeaders(HttpResponseHeaders { if (httpResponseHeaders.Contains(AuthenticationInfoKey)) { - var authInfoValue = httpResponseHeaders.Single(header => header.Key == AuthenticationInfoKey).Value.FirstOrDefault(); + var authInfoValue = httpResponseHeaders.Where(header => header.Key == AuthenticationInfoKey).Single().Value.FirstOrDefault(); - if (authInfoValue != null) - { - var AuthValuesSplit = authInfoValue.Split(new char[] { ' ' }, 2); - var paramValues = CoreHelpers.SplitWithQuotes(AuthValuesSplit[1], ',') - .Select(v => AuthenticationHeaderParser.ExtractKeyValuePair(v.Trim())) - .ToDictionary(pair => pair.Key, pair => pair.Value, StringComparer.OrdinalIgnoreCase); + var AuthValuesSplit = authInfoValue.Split(new char[] { ' ' }, 2); - parameters.RawParameters = paramValues; + var paramValues = CoreHelpers.SplitWithQuotes(AuthValuesSplit[1], ',') + .Select(v => AuthenticationHeaderParser.ExtractKeyValuePair(v.Trim())) + .ToDictionary(pair => pair.Key, pair => pair.Value, StringComparer.OrdinalIgnoreCase); - if (paramValues.TryGetValue("nextnonce", out string value)) - { - parameters.NextNonce = value; - } + parameters.RawParameters = paramValues; - return parameters; + if (paramValues.TryGetValue("nextnonce", out string value)) + { + parameters.NextNonce = value; } + return parameters; + //Could not get Auth info parameters throw new MsalClientException(MsalError.UnableToParseAuthenticationHeader, MsalErrorMessage.UnableToParseAuthenticationHeader); } - return parameters; + return null; } - catch(Exception ex) + catch (Exception ex) when (ex is not MsalClientException) { - if (ex is MsalClientException) - { - throw; - } throw new MsalClientException(MsalError.UnableToParseAuthenticationHeader, MsalErrorMessage.UnableToParseAuthenticationHeader, ex); } } diff --git a/src/client/Microsoft.Identity.Client/Microsoft.Identity.Client.csproj b/src/client/Microsoft.Identity.Client/Microsoft.Identity.Client.csproj index 02834bb3fe..ae28839acf 100644 --- a/src/client/Microsoft.Identity.Client/Microsoft.Identity.Client.csproj +++ b/src/client/Microsoft.Identity.Client/Microsoft.Identity.Client.csproj @@ -328,8 +328,4 @@ - - - - diff --git a/src/client/Microsoft.Identity.Client/MsalError.cs b/src/client/Microsoft.Identity.Client/MsalError.cs index 6bb62134b1..a90cf27be6 100644 --- a/src/client/Microsoft.Identity.Client/MsalError.cs +++ b/src/client/Microsoft.Identity.Client/MsalError.cs @@ -1098,7 +1098,7 @@ public static class MsalError public const string InvalidTokenProviderResponseValue = "invalid_token_provider_response_value"; /// - /// Unable to parse the authentication header returned from the resource endpoint + /// 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. /// public const string UnableToParseAuthenticationHeader = "unable_to_parse_authentication_header"; } diff --git a/src/client/Microsoft.Identity.Client/MsalErrorMessage.cs b/src/client/Microsoft.Identity.Client/MsalErrorMessage.cs index 449184319e..34844fd9cd 100644 --- a/src/client/Microsoft.Identity.Client/MsalErrorMessage.cs +++ b/src/client/Microsoft.Identity.Client/MsalErrorMessage.cs @@ -432,7 +432,7 @@ 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."; + 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. See inner exception for details"; public static string InvalidTokenProviderResponseValue(string invalidValueName) diff --git a/tests/Microsoft.Identity.Test.Unit/WwwAuthenticateParametersTests.cs b/tests/Microsoft.Identity.Test.Unit/WwwAuthenticateParametersTests.cs index ee16c917e2..a22599872c 100644 --- a/tests/Microsoft.Identity.Test.Unit/WwwAuthenticateParametersTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/WwwAuthenticateParametersTests.cs @@ -457,7 +457,7 @@ public void ExtractAllAuthenticationInfoParametersFromResponse() [TestMethod] [DataRow(false)] [DataRow(true)] - public void ExtractAllAuthinfoParametersFromResponseWithAuthParser(bool combineHeaders) + public void ExtractAllAuthinfoParametersFromResponseWithAuthParser() { // Arrange HttpResponseMessage httpResponse = CreateAuthInfoHttpResponse(); From 4b56fb21596000f5943aa4d5c44454915872b1da Mon Sep 17 00:00:00 2001 From: trwalke Date: Tue, 1 Nov 2022 23:57:37 -0700 Subject: [PATCH 21/31] resolving build errors --- .../Microsoft.Identity.Client/AuthenticationInfoParameters.cs | 3 --- .../Microsoft.Identity.Client/WwwAuthenticateParameters.cs | 2 -- 2 files changed, 5 deletions(-) diff --git a/src/client/Microsoft.Identity.Client/AuthenticationInfoParameters.cs b/src/client/Microsoft.Identity.Client/AuthenticationInfoParameters.cs index a0eb143e3e..dc97e5a6a9 100644 --- a/src/client/Microsoft.Identity.Client/AuthenticationInfoParameters.cs +++ b/src/client/Microsoft.Identity.Client/AuthenticationInfoParameters.cs @@ -5,10 +5,7 @@ using System.Collections.Generic; using System.Linq; using System.Net.Http.Headers; -using System.Text; -using System.Threading.Tasks; using Microsoft.Identity.Client.Utils; -using Microsoft.Identity.Json.Linq; namespace Microsoft.Identity.Client { diff --git a/src/client/Microsoft.Identity.Client/WwwAuthenticateParameters.cs b/src/client/Microsoft.Identity.Client/WwwAuthenticateParameters.cs index 28fe54d257..8a0c218167 100644 --- a/src/client/Microsoft.Identity.Client/WwwAuthenticateParameters.cs +++ b/src/client/Microsoft.Identity.Client/WwwAuthenticateParameters.cs @@ -11,9 +11,7 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Identity.Client.Internal; -using Microsoft.Identity.Client.PlatformsCommon.Factories; using Microsoft.Identity.Client.Utils; -using Microsoft.Identity.Json.Linq; namespace Microsoft.Identity.Client { From ddbadf8cbae5df86796d866fc452d475b33e7cb3 Mon Sep 17 00:00:00 2001 From: trwalke Date: Wed, 2 Nov 2022 00:20:32 -0700 Subject: [PATCH 22/31] resolving tests --- .../Microsoft.Identity.Client/WwwAuthenticateParameters.cs | 5 ----- .../WwwAuthenticateParametersTests.cs | 2 -- 2 files changed, 7 deletions(-) diff --git a/src/client/Microsoft.Identity.Client/WwwAuthenticateParameters.cs b/src/client/Microsoft.Identity.Client/WwwAuthenticateParameters.cs index 8a0c218167..62e834fba3 100644 --- a/src/client/Microsoft.Identity.Client/WwwAuthenticateParameters.cs +++ b/src/client/Microsoft.Identity.Client/WwwAuthenticateParameters.cs @@ -385,11 +385,6 @@ private static WwwAuthenticateParameters CreateFromWwwAuthenticationHeaderValue( throw new ArgumentNullException(nameof(wwwAuthenticateValue)); } - if (string.IsNullOrWhiteSpace(scheme)) - { - throw new ArgumentNullException(nameof(scheme)); - } - //Special NTLM case does not have an a=b format if (scheme == "NTLM") { diff --git a/tests/Microsoft.Identity.Test.Unit/WwwAuthenticateParametersTests.cs b/tests/Microsoft.Identity.Test.Unit/WwwAuthenticateParametersTests.cs index a22599872c..b2193ae085 100644 --- a/tests/Microsoft.Identity.Test.Unit/WwwAuthenticateParametersTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/WwwAuthenticateParametersTests.cs @@ -455,8 +455,6 @@ public void ExtractAllAuthenticationInfoParametersFromResponse() } [TestMethod] - [DataRow(false)] - [DataRow(true)] public void ExtractAllAuthinfoParametersFromResponseWithAuthParser() { // Arrange From 0867daadb67211f1129df411c53f2ff3c081f742 Mon Sep 17 00:00:00 2001 From: trwalke Date: Tue, 15 Nov 2022 23:59:12 -0800 Subject: [PATCH 23/31] Updating parameter parsing --- .../AuthenticationHeaderParser.cs | 22 +++--- .../AuthenticationInfoParameters.cs | 2 +- .../MsalErrorMessage.cs | 2 +- .../WwwAuthenticateParameters.cs | 78 ++++++++----------- ...wAuthenticateParametersIntegrationTests.cs | 16 ++-- .../WwwAuthenticateParametersTests.cs | 66 +++++++++------- 6 files changed, 96 insertions(+), 90 deletions(-) diff --git a/src/client/Microsoft.Identity.Client/AuthenticationHeaderParser.cs b/src/client/Microsoft.Identity.Client/AuthenticationHeaderParser.cs index 261713abf0..2677075ee0 100644 --- a/src/client/Microsoft.Identity.Client/AuthenticationHeaderParser.cs +++ b/src/client/Microsoft.Identity.Client/AuthenticationHeaderParser.cs @@ -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; @@ -38,7 +39,7 @@ public class AuthenticationHeaderParser /// /// Nonce parsed from HttpResponseHeaders. This is acquired from with the POP WWW-Authenticate header or the Authetnciation-Info header /// - public string Nonce { get; private set; } + public string PopNonce { get; private set; } /// /// Creates the authenticate parameters by attempting to call the resource unauthenticated, and analyzing the response. @@ -104,7 +105,7 @@ public static AuthenticationHeaderParser ParseAuthenticationHeaders(HttpResponse { var wwwParameters = Client.WwwAuthenticateParameters.CreateFromAuthenticationHeaders(httpResponseHeaders); - serverNonce = wwwParameters.SingleOrDefault(parameter => parameter.AuthScheme == "PoP")?.Nonce; + serverNonce = wwwParameters.SingleOrDefault(parameter => string.Equals(parameter.AuthScheme, Constants.PoPAuthHeaderPrefix, StringComparison.Ordinal))?.PopNonce; authenticationHeaderParser.WwwAuthenticateParameters = wwwParameters; } @@ -118,7 +119,7 @@ public static AuthenticationHeaderParser ParseAuthenticationHeaders(HttpResponse } //If server nonce is not acquired from WWW-Authenticate headers, use next nonce from Authentication-Info parameters. - authenticationHeaderParser.Nonce = serverNonce ?? authenticationInfoParameters.NextNonce; + authenticationHeaderParser.PopNonce = serverNonce ?? authenticationInfoParameters.NextNonce; return authenticationHeaderParser; } @@ -132,19 +133,22 @@ internal static HttpClient GetHttpClient() } /// - /// Extracts a key value pair from an expression of the form a=b + /// 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:, value:) /// - /// assignment + /// assignment + /// authScheme /// Key Value pair - internal static KeyValuePair ExtractKeyValuePair(string assignment) + internal static KeyValuePair CreateKeyValuePair(string paramValue, string authScheme) { - string[] segments = CoreHelpers.SplitWithQuotes(assignment, '=') + string[] segments = CoreHelpers.SplitWithQuotes(paramValue, '=') .Select(s => s.Trim().Trim('"')) .ToArray(); - if (segments.Length != 2) + if (segments.Length < 2) { - throw new ArgumentException(nameof(assignment), $"{assignment} isn't of the form a=b"); + // 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(authScheme, paramValue); } return new KeyValuePair(segments[0], segments[1]); diff --git a/src/client/Microsoft.Identity.Client/AuthenticationInfoParameters.cs b/src/client/Microsoft.Identity.Client/AuthenticationInfoParameters.cs index dc97e5a6a9..2b49dba0a8 100644 --- a/src/client/Microsoft.Identity.Client/AuthenticationInfoParameters.cs +++ b/src/client/Microsoft.Identity.Client/AuthenticationInfoParameters.cs @@ -63,7 +63,7 @@ public static AuthenticationInfoParameters CreateFromHeaders(HttpResponseHeaders var AuthValuesSplit = authInfoValue.Split(new char[] { ' ' }, 2); var paramValues = CoreHelpers.SplitWithQuotes(AuthValuesSplit[1], ',') - .Select(v => AuthenticationHeaderParser.ExtractKeyValuePair(v.Trim())) + .Select(v => AuthenticationHeaderParser.CreateKeyValuePair(v.Trim(), AuthenticationInfoKey)) .ToDictionary(pair => pair.Key, pair => pair.Value, StringComparer.OrdinalIgnoreCase); parameters.RawParameters = paramValues; diff --git a/src/client/Microsoft.Identity.Client/MsalErrorMessage.cs b/src/client/Microsoft.Identity.Client/MsalErrorMessage.cs index 34844fd9cd..9a863ad194 100644 --- a/src/client/Microsoft.Identity.Client/MsalErrorMessage.cs +++ b/src/client/Microsoft.Identity.Client/MsalErrorMessage.cs @@ -432,7 +432,7 @@ 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. See inner exception for details"; + 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. See inner exception for details. For more information on Proof-of-Possession, please see https://aka.ms/msal-net-pop"; public static string InvalidTokenProviderResponseValue(string invalidValueName) diff --git a/src/client/Microsoft.Identity.Client/WwwAuthenticateParameters.cs b/src/client/Microsoft.Identity.Client/WwwAuthenticateParameters.cs index 62e834fba3..ee8115daa8 100644 --- a/src/client/Microsoft.Identity.Client/WwwAuthenticateParameters.cs +++ b/src/client/Microsoft.Identity.Client/WwwAuthenticateParameters.cs @@ -62,12 +62,12 @@ public class WwwAuthenticateParameters /// AuthScheme. /// See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/WWW-Authenticate#syntax for more details /// - public string AuthScheme { get; private set; } + public string AuthScheme { get; private set; } /// - /// Server Nonce. + /// Pop Nonce. This is acquired from with the POP WWW-Authenticate header. /// - public string Nonce { get; private set; } + public string PopNonce { get; private set; } /// /// Return the of key . @@ -95,7 +95,7 @@ public string this[string key] /// Gets Azure AD tenant ID. /// public string GetTenantId() => Instance.Authority - .CreateAuthority(Authority, validateAuthority: true) + .CreateAuthority(Authority, validateAuthority: true)? .TenantId; #region Obsolete Api /// @@ -104,6 +104,7 @@ public string GetTenantId() => Instance.Authority /// URI of the resource. /// WWW-Authenticate Parameters extracted from response to the unauthenticated call. [System.ComponentModel.EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("This api is now obsolete and has been replaced with CreateFromAuthenticationResponseAsync(...)")] public static Task CreateFromResourceResponseAsync(string resourceUri) { return CreateFromResourceResponseAsync(resourceUri, default); @@ -116,6 +117,7 @@ public static Task CreateFromResourceResponseAsync(st /// The cancellation token to cancel operation. /// WWW-Authenticate Parameters extracted from response to the unauthenticated call. [System.ComponentModel.EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("This api is now obsolete and has been replaced with CreateFromAuthenticationResponseAsync(...)")] public static Task CreateFromResourceResponseAsync(string resourceUri, CancellationToken cancellationToken = default) { return CreateFromResourceResponseAsync(AuthenticationHeaderParser.GetHttpClient(), resourceUri, cancellationToken); @@ -129,6 +131,7 @@ public static Task CreateFromResourceResponseAsync(st /// The cancellation token to cancel operation. /// WWW-Authenticate Parameters extracted from response to the unauthenticated call. [System.ComponentModel.EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("This api is now obsolete and has been replaced with CreateFromAuthenticationResponseAsync(...)")] public static async Task CreateFromResourceResponseAsync(HttpClient httpClient, string resourceUri, CancellationToken cancellationToken = default) { if (httpClient is null) @@ -153,7 +156,8 @@ public static async Task CreateFromResourceResponseAs /// Authentication scheme. Default is "Bearer". /// The parameters requested by the web API. /// Currently it only supports the Bearer scheme - [System.ComponentModel.EditorBrowsable(EditorBrowsableState.Never)] // hide confidential client on mobile + [System.ComponentModel.EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("This api is now obsolete and has been replaced with CreateFromAuthenticationHeaders(...)")] public static WwwAuthenticateParameters CreateFromResponseHeaders( HttpResponseHeaders httpResponseHeaders, string scheme = "Bearer") @@ -180,6 +184,7 @@ public static WwwAuthenticateParameters CreateFromResponseHeaders( /// String contained in a WWW-Authenticate header. /// The parameters requested by the web API. [System.ComponentModel.EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("This api is now obsolete and should not be used.")] public static WwwAuthenticateParameters CreateFromWwwAuthenticateHeaderValue(string wwwAuthenticateValue) { return CreateFromWwwAuthenticationHeaderValue(wwwAuthenticateValue, string.Empty); @@ -380,39 +385,24 @@ public static IReadOnlyList CreateFromAuthenticationH /// The parameters requested by the web API. private static WwwAuthenticateParameters CreateFromWwwAuthenticationHeaderValue(string wwwAuthenticateValue, string scheme) { - if (string.IsNullOrWhiteSpace(wwwAuthenticateValue)) - { - throw new ArgumentNullException(nameof(wwwAuthenticateValue)); - } + IDictionary parameters = null; - //Special NTLM case does not have an a=b format - if (scheme == "NTLM") + if (!string.IsNullOrWhiteSpace(wwwAuthenticateValue)) { - return new WwwAuthenticateParameters - { - RawParameters = new Dictionary() - { - { scheme, wwwAuthenticateValue } - }, - AuthScheme = scheme - }; - } + var authValuesSplit = wwwAuthenticateValue.Split(new char[] { ' ' }, 2); - IDictionary parameters; - - var authValuesSplit = wwwAuthenticateValue.Split(new char[] { ' ' }, 2); - - if (s_knownAuthenticationSchemes.Contains(authValuesSplit[0])) - { - parameters = CoreHelpers.SplitWithQuotes(authValuesSplit[1], ',') - .Select(v => AuthenticationHeaderParser.ExtractKeyValuePair(v.Trim())) - .ToDictionary(pair => pair.Key, pair => pair.Value, StringComparer.OrdinalIgnoreCase); - } - else - { - parameters = CoreHelpers.SplitWithQuotes(wwwAuthenticateValue, ',') - .Select(v => AuthenticationHeaderParser.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 => AuthenticationHeaderParser.CreateKeyValuePair(v.Trim(), scheme)) + .ToDictionary(pair => pair.Key, pair => pair.Value, StringComparer.OrdinalIgnoreCase); + } + else + { + parameters = CoreHelpers.SplitWithQuotes(wwwAuthenticateValue, ',') + .Select(v => AuthenticationHeaderParser.CreateKeyValuePair(v.Trim(), scheme)) + .ToDictionary(pair => pair.Key, pair => pair.Value, StringComparer.OrdinalIgnoreCase); + } } return CreateWwwAuthenticateParameters(parameters, scheme); @@ -420,17 +410,17 @@ private static WwwAuthenticateParameters CreateFromWwwAuthenticationHeaderValue( internal static WwwAuthenticateParameters CreateWwwAuthenticateParameters(IDictionary values, string scheme) { - WwwAuthenticateParameters wwwAuthenticateParameters = new WwwAuthenticateParameters - { - RawParameters = values - }; + WwwAuthenticateParameters wwwAuthenticateParameters = new WwwAuthenticateParameters(); - if (values.Count == 0) + wwwAuthenticateParameters.AuthScheme = scheme; + + if (values == null) { - //unable to parse auth header values - throw new MsalClientException(MsalError.UnableToParseAuthenticationHeader, MsalErrorMessage.UnableToParseAuthenticationHeader); + return wwwAuthenticateParameters; } + wwwAuthenticateParameters.RawParameters = values; + string value; if (values.TryGetValue("authorization_uri", out value)) @@ -466,11 +456,9 @@ internal static WwwAuthenticateParameters CreateWwwAuthenticateParameters(IDicti if (values.TryGetValue("nonce", out value)) { - wwwAuthenticateParameters.Nonce = value.Replace("\"", string.Empty); + wwwAuthenticateParameters.PopNonce = value.Replace("\"", string.Empty); } - wwwAuthenticateParameters.AuthScheme = scheme; - return wwwAuthenticateParameters; } diff --git a/tests/Microsoft.Identity.Test.Integration.netfx/HeadlessTests/WwwAuthenticateParametersIntegrationTests.cs b/tests/Microsoft.Identity.Test.Integration.netfx/HeadlessTests/WwwAuthenticateParametersIntegrationTests.cs index e9adcdc313..bbf6b8fe91 100644 --- a/tests/Microsoft.Identity.Test.Integration.netfx/HeadlessTests/WwwAuthenticateParametersIntegrationTests.cs +++ b/tests/Microsoft.Identity.Test.Integration.netfx/HeadlessTests/WwwAuthenticateParametersIntegrationTests.cs @@ -18,6 +18,7 @@ using Microsoft.Identity.Test.LabInfrastructure; using Microsoft.VisualStudio.TestTools.UnitTesting; +#pragma warning disable CS0618 // Type or member is obsolete namespace Microsoft.Identity.Test.Integration.HeadlessTests { [TestClass] @@ -28,6 +29,7 @@ public class WwwAuthenticateParametersIntegrationTests [TestMethod] public async Task CreateWwwAuthenticateResponseFromKeyVaultUrlAsync() { + var authParams = await WwwAuthenticateParameters.CreateFromResourceResponseAsync("https://buildautomation.vault.azure.net/secrets/CertName/CertVersion").ConfigureAwait(false); Assert.AreEqual("login.windows.net", new Uri(authParams.Authority).Host); @@ -84,7 +86,7 @@ public async Task ExtractNonceFromWwwAuthHeadersAsync() //Assert Assert.IsTrue(parameterList.Any(param => param.AuthScheme == Constants.PoPAuthHeaderPrefix)); - Assert.IsNotNull(parameterList.Where(param => param.AuthScheme == Constants.PoPAuthHeaderPrefix).Single().Nonce); + Assert.IsNotNull(parameterList.Where(param => param.AuthScheme == Constants.PoPAuthHeaderPrefix).Single().PopNonce); } [TestMethod] @@ -133,9 +135,9 @@ public async Task ExtractNonceWithAuthParserAsync() //Assert Assert.IsTrue(parsedHeaders.WwwAuthenticateParameters.Any(param => param.AuthScheme == Constants.PoPAuthHeaderPrefix)); - var serverNonce = parsedHeaders.WwwAuthenticateParameters.Where(param => param.AuthScheme == Constants.PoPAuthHeaderPrefix).Single().Nonce; + var serverNonce = parsedHeaders.WwwAuthenticateParameters.Where(param => param.AuthScheme == Constants.PoPAuthHeaderPrefix).Single().PopNonce; Assert.IsNotNull(serverNonce); - Assert.AreEqual(parsedHeaders.Nonce, serverNonce); + Assert.AreEqual(parsedHeaders.PopNonce, serverNonce); Assert.IsNull(parsedHeaders.AuthenticationInfoParameters); //Arrange & Act @@ -144,10 +146,10 @@ public async Task ExtractNonceWithAuthParserAsync() //Assert Assert.IsNotNull(parsedHeaders.AuthenticationInfoParameters.NextNonce); - Assert.AreEqual(parsedHeaders.Nonce, parsedHeaders.AuthenticationInfoParameters.NextNonce); + Assert.AreEqual(parsedHeaders.PopNonce, parsedHeaders.AuthenticationInfoParameters.NextNonce); Assert.IsFalse(parsedHeaders.WwwAuthenticateParameters.Any(param => param.AuthScheme == Constants.PoPAuthHeaderPrefix)); - await TestCommon.ValidatePopNonceAsync(parsedHeaders.Nonce).ConfigureAwait(false); + await TestCommon.ValidatePopNonceAsync(parsedHeaders.PopNonce).ConfigureAwait(false); } #if NET_CORE @@ -166,7 +168,6 @@ public async Task ExtractNonceWithAuthParserAndValidateShrAsync() IPublicClientApplication pca = PublicClientApplicationBuilder .Create(labResponse.App.AppId) .WithAuthority(labResponse.Lab.Authority, "organizations") - .WithExperimentalFeatures() .WithBrokerPreview().Build(); Assert.IsTrue(pca.IsProofOfPossessionSupportedByClient(), "Either the broker is not configured or it does not support POP."); @@ -176,7 +177,7 @@ public async Task ExtractNonceWithAuthParserAndValidateShrAsync() scopes, labResponse.User.Upn, labResponse.User.GetOrFetchPassword()) - .WithProofOfPossession(parsedHeaders.Nonce, HttpMethod.Get, new Uri(ProtectedUrl)) + .WithProofOfPossession(parsedHeaders.PopNonce, HttpMethod.Get, new Uri(ProtectedUrl)) .ExecuteAsync().ConfigureAwait(false); MsalAssert.AssertAuthResult(result, TokenSource.Broker, labResponse.Lab.TenantId, expectedScopes, true); @@ -185,4 +186,5 @@ public async Task ExtractNonceWithAuthParserAndValidateShrAsync() } #endif } +#pragma warning restore CS0618 // Type or member is obsolete } diff --git a/tests/Microsoft.Identity.Test.Unit/WwwAuthenticateParametersTests.cs b/tests/Microsoft.Identity.Test.Unit/WwwAuthenticateParametersTests.cs index b2193ae085..470ed2af9d 100644 --- a/tests/Microsoft.Identity.Test.Unit/WwwAuthenticateParametersTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/WwwAuthenticateParametersTests.cs @@ -12,6 +12,7 @@ using Microsoft.Identity.Test.Common.Core.Mocks; using Microsoft.VisualStudio.TestTools.UnitTesting; +#pragma warning disable CS0618 // Type or member is obsolete namespace Microsoft.Identity.Test.Unit { [TestClass] @@ -58,6 +59,39 @@ public void CreateWwwAuthenticateResponse(string resource, string authorizationU Assert.IsNull(authParams.Error); } + [TestMethod] + [DataRow("AuthScheme", "" )] + [DataRow("AuthScheme", "realm = someRealm")] + [DataRow("AuthScheme", "token68")] + [DataRow("AuthScheme", "realm = someRealm token68")] + [DataRow("AuthScheme", "auth-param1=token1, auth-param2=token2, auth-param3=token3")] + [DataRow("AuthScheme", "realm = someRealm auth-param1=token1, auth-param2=token2, auth-param3=token3")] + [DataRow("AuthScheme", "token68 auth-param1=token1, auth-param2=token2, auth-param3=token3")] + [DataRow("AuthScheme", "realm = someRealm token68 auth-param1=token1, auth-param2=token2, auth-param3=token3")] + public void EnsureProperlyFormattedHeadersDoNotFail(string scheme, string values) + { + // Arrange + HttpResponseMessage httpResponse = new HttpResponseMessage(HttpStatusCode.Unauthorized); + httpResponse.Headers.Add(WwwAuthenticateHeaderName, $"{scheme} {values}"); + + // Act + var authParams = WwwAuthenticateParameters.CreateFromAuthenticationHeaders(httpResponse.Headers, scheme); + + // Assert + //if (scheme == "NTLM") + //{ + // Assert.AreEqual(scheme, authParams.AuthScheme); + // Assert.AreEqual(values, authParams.RawParameters[scheme]); + //} + //else + //{ + // Assert.AreEqual(3, authParams.RawParameters.Count); + // Assert.AreEqual("WindowsLive", authParams.RawParameters["realm"]); + // Assert.AreEqual("MBI_SSL", authParams.RawParameters["policy"]); + // Assert.AreEqual("ssl.live-tst.net", authParams.RawParameters["siteId"]); + //} + } + [TestMethod] [DataRow("WLID1.0", "realm=WindowsLive, policy=MBI_SSL, siteId=\"ssl.live-tst.net\"")] [DataRow("NTLM", "dG9rZW42OA==")] @@ -85,30 +119,6 @@ public void CreateWwwAuthenticateResponseForUnknownChallenges(string scheme, str } } - [TestMethod] - [DataRow("Bearer","")] - [DataRow("Bearer", "Bearer Malformed String Malformed String\", \"Malformed String, Malformed String")] - [DataRow("Bearer", "Malformed String Malformed StringMalformed String, Malformed String")] - [DataRow("Pop", "Malformed String Malformed StringMalformed String, Malformed String")] - [DataRow("", "Malformed String Malformed StringMalformed String, Malformed String")] - [DataRow("SomeAuthScheme", "Malformed String Malformed StringMalformed String, Malformed String")] - [DataRow("\'SomeAuthScheme\'", "\'Malformed String Malformed StringMalformed String, Malformed String\'")] - [DataRow("&SomeAuthScheme&", "Malformed String Malformed StringMalformed String, Malformed String")] - public void CreateFromMalformedWwwAuthenticateResponse(string scheme, string value) - { - // Arrange - HttpResponseMessage httpResponse = new HttpResponseMessage(HttpStatusCode.Unauthorized); - httpResponse.Headers.Add(WwwAuthenticateHeaderName, $"{scheme} {value}"); - - // Act - var ex = Assert.ThrowsException(() => - WwwAuthenticateParameters.CreateFromAuthenticationHeaders(httpResponse.Headers, scheme)); - - //Assert - Assert.AreEqual(ex.ErrorCode, MsalError.UnableToParseAuthenticationHeader); - Assert.AreEqual(ex.Message, MsalErrorMessage.UnableToParseAuthenticationHeader); - } - [TestMethod] [DataRow("nextnonce", "")] [DataRow("nextnonce", "Some, Malformed, Nonce")] @@ -147,6 +157,7 @@ public void CreateRawParameters(string resourceHeaderKey, string authorizationUr // Act var authParams = WwwAuthenticateParameters.CreateFromResponseHeaders(httpResponse.Headers); + // Assert Assert.IsTrue(authParams.RawParameters.ContainsKey(resourceHeaderKey)); Assert.IsTrue(authParams.RawParameters.ContainsKey(authorizationUriHeaderKey)); @@ -380,7 +391,7 @@ public async Task CreateAllFromResourceResponseAsync_HttpClient_Bearer_Pop_Async Assert.IsNotNull(bearerHeader); Assert.AreEqual("https://login.microsoftonline.com/TenantId", bearerHeader.Authority); Assert.IsNotNull(popHeader); - Assert.AreEqual(TestConstants.Nonce, popHeader.Nonce); + Assert.AreEqual(TestConstants.Nonce, popHeader.PopNonce); } [TestMethod] @@ -400,7 +411,7 @@ public void ExtractAllWWWAuthenticateParametersFromResponse(bool combineHeaders) Assert.IsNotNull(bearerHeader); Assert.AreEqual("https://login.microsoftonline.com/TenantId", bearerHeader.Authority); Assert.IsNotNull(popHeader); - Assert.AreEqual(TestConstants.Nonce, popHeader.Nonce); + Assert.AreEqual(TestConstants.Nonce, popHeader.PopNonce); } [TestMethod] @@ -420,7 +431,7 @@ public void ExtractAllParametersFromResponseWithAuthParser(bool combineHeaders) Assert.IsNotNull(bearerHeader); Assert.AreEqual("https://login.microsoftonline.com/TenantId", bearerHeader.Authority); Assert.IsNotNull(popHeader); - Assert.AreEqual(TestConstants.Nonce, popHeader.Nonce); + Assert.AreEqual(TestConstants.Nonce, popHeader.PopNonce); } [TestMethod] @@ -556,3 +567,4 @@ private static HttpResponseMessage CreateAuthInfoHttpResponse() } } } +#pragma warning restore CS0618 // Type or member is obsolete From b55f3c21ee5475813dba73ec0cf3a6d251689776 Mon Sep 17 00:00:00 2001 From: trwalke Date: Wed, 30 Nov 2022 09:12:38 -0800 Subject: [PATCH 24/31] Updating parsing logic --- .../WwwAuthenticateParameters.cs | 57 +++++++++++++----- .../WwwAuthenticateParametersTests.cs | 60 +++++++++++++------ 2 files changed, 84 insertions(+), 33 deletions(-) diff --git a/src/client/Microsoft.Identity.Client/WwwAuthenticateParameters.cs b/src/client/Microsoft.Identity.Client/WwwAuthenticateParameters.cs index ee8115daa8..a3cd24913b 100644 --- a/src/client/Microsoft.Identity.Client/WwwAuthenticateParameters.cs +++ b/src/client/Microsoft.Identity.Client/WwwAuthenticateParameters.cs @@ -389,25 +389,51 @@ private static WwwAuthenticateParameters CreateFromWwwAuthenticationHeaderValue( if (!string.IsNullOrWhiteSpace(wwwAuthenticateValue)) { - var authValuesSplit = wwwAuthenticateValue.Split(new char[] { ' ' }, 2); - - if (s_knownAuthenticationSchemes.Contains(authValuesSplit[0])) - { - parameters = CoreHelpers.SplitWithQuotes(authValuesSplit[1], ',') - .Select(v => AuthenticationHeaderParser.CreateKeyValuePair(v.Trim(), scheme)) - .ToDictionary(pair => pair.Key, pair => pair.Value, StringComparer.OrdinalIgnoreCase); - } - else - { - parameters = CoreHelpers.SplitWithQuotes(wwwAuthenticateValue, ',') - .Select(v => AuthenticationHeaderParser.CreateKeyValuePair(v.Trim(), scheme)) - .ToDictionary(pair => pair.Key, pair => pair.Value, StringComparer.OrdinalIgnoreCase); - } + var parametersList = GetParsedAuthValueElements(wwwAuthenticateValue); + + parameters = parametersList.Select(v => AuthenticationHeaderParser.CreateKeyValuePair(v.Trim(), scheme)) + .ToDictionary(pair => pair.Key, pair => pair.Value, StringComparer.OrdinalIgnoreCase); + + //var authValuesSplit = wwwAuthenticateValue.Split(new char[] { ' ' }); + + //if (s_knownAuthenticationSchemes.Contains(authValuesSplit[0])) + //{ + // parameters = CoreHelpers.SplitWithQuotes(authValuesSplit[1], ',') + // .Select(v => AuthenticationHeaderParser.CreateKeyValuePair(v.Trim(), scheme)) + // .ToDictionary(pair => pair.Key, pair => pair.Value, StringComparer.OrdinalIgnoreCase); + //} + //else + //{ + // parameters = CoreHelpers.SplitWithQuotes(wwwAuthenticateValue, ',') + // .Select(v => AuthenticationHeaderParser.CreateKeyValuePair(v.Trim(), scheme)) + // .ToDictionary(pair => pair.Key, pair => pair.Value, StringComparer.OrdinalIgnoreCase); + //} } return CreateWwwAuthenticateParameters(parameters, scheme); } + private static string[] GetParsedAuthValueElements(string wwwAuthenticateValue) + { + string[] result; + var authValuesSplit = wwwAuthenticateValue.Split(new char[] { ' ' }); + + //Ensure that known headers are not being parsed. + if (s_knownAuthenticationSchemes.Contains(authValuesSplit[0])) + { + authValuesSplit = authValuesSplit.Skip(1).ToArray(); + } + + //foreach (var authValue in authValuesSplit) + //{ + // authValue = authValue.Replace(",", string.Empty); + //} + + result = authValuesSplit.Select(authValue => authValue.Replace(",", string.Empty)).ToArray(); + + return result ?? new string[0]; + } + internal static WwwAuthenticateParameters CreateWwwAuthenticateParameters(IDictionary values, string scheme) { WwwAuthenticateParameters wwwAuthenticateParameters = new WwwAuthenticateParameters(); @@ -416,6 +442,7 @@ internal static WwwAuthenticateParameters CreateWwwAuthenticateParameters(IDicti if (values == null) { + wwwAuthenticateParameters.RawParameters = new Dictionary(); return wwwAuthenticateParameters; } @@ -456,7 +483,7 @@ internal static WwwAuthenticateParameters CreateWwwAuthenticateParameters(IDicti if (values.TryGetValue("nonce", out value)) { - wwwAuthenticateParameters.PopNonce = value.Replace("\"", string.Empty); + wwwAuthenticateParameters.PopNonce = value; } return wwwAuthenticateParameters; diff --git a/tests/Microsoft.Identity.Test.Unit/WwwAuthenticateParametersTests.cs b/tests/Microsoft.Identity.Test.Unit/WwwAuthenticateParametersTests.cs index 470ed2af9d..57c40827bb 100644 --- a/tests/Microsoft.Identity.Test.Unit/WwwAuthenticateParametersTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/WwwAuthenticateParametersTests.cs @@ -60,15 +60,11 @@ public void CreateWwwAuthenticateResponse(string resource, string authorizationU } [TestMethod] - [DataRow("AuthScheme", "" )] - [DataRow("AuthScheme", "realm = someRealm")] + [DataRow("AuthScheme", "")] [DataRow("AuthScheme", "token68")] - [DataRow("AuthScheme", "realm = someRealm token68")] [DataRow("AuthScheme", "auth-param1=token1, auth-param2=token2, auth-param3=token3")] - [DataRow("AuthScheme", "realm = someRealm auth-param1=token1, auth-param2=token2, auth-param3=token3")] [DataRow("AuthScheme", "token68 auth-param1=token1, auth-param2=token2, auth-param3=token3")] - [DataRow("AuthScheme", "realm = someRealm token68 auth-param1=token1, auth-param2=token2, auth-param3=token3")] - public void EnsureProperlyFormattedHeadersDoNotFail(string scheme, string values) + public void EnsureProperlyFormattedHeadersWithToken86DoNotFail(string scheme, string values) { // Arrange HttpResponseMessage httpResponse = new HttpResponseMessage(HttpStatusCode.Unauthorized); @@ -78,18 +74,46 @@ public void EnsureProperlyFormattedHeadersDoNotFail(string scheme, string values var authParams = WwwAuthenticateParameters.CreateFromAuthenticationHeaders(httpResponse.Headers, scheme); // Assert - //if (scheme == "NTLM") - //{ - // Assert.AreEqual(scheme, authParams.AuthScheme); - // Assert.AreEqual(values, authParams.RawParameters[scheme]); - //} - //else - //{ - // Assert.AreEqual(3, authParams.RawParameters.Count); - // Assert.AreEqual("WindowsLive", authParams.RawParameters["realm"]); - // Assert.AreEqual("MBI_SSL", authParams.RawParameters["policy"]); - // Assert.AreEqual("ssl.live-tst.net", authParams.RawParameters["siteId"]); - //} + Assert.AreEqual(scheme, authParams.AuthScheme); + + if (string.IsNullOrEmpty(values)) + { + Assert.AreEqual(0, authParams.RawParameters.Count); + } + else if (values == "token68") + { + Assert.AreEqual("token68", authParams.RawParameters[scheme]); + } + else + { + Assert.AreEqual("token1", authParams.RawParameters["auth-param1"]); + Assert.AreEqual("token2", authParams.RawParameters["auth-param2"]); + Assert.AreEqual("token3", authParams.RawParameters["auth-param3"]); + } + } + + [TestMethod] + [DataRow("AuthScheme", "realm=someRealm")] + [DataRow("AuthScheme", "realm=someRealm token68")] + [DataRow("AuthScheme", "realm=someRealm auth-param1=token1, auth-param2=token2, auth-param3=token3")] + [DataRow("AuthScheme", "realm=someRealm token68 auth-param1=token1, auth-param2=token2, auth-param3=token3")] + public void EnsureProperlyFormattedHeadersWithRealmDoNotFail(string scheme, string values) + { + // Arrange + HttpResponseMessage httpResponse = new HttpResponseMessage(HttpStatusCode.Unauthorized); + httpResponse.Headers.Add(WwwAuthenticateHeaderName, $"{scheme} {values}"); + + // Act + var authParams = WwwAuthenticateParameters.CreateFromAuthenticationHeaders(httpResponse.Headers, scheme); + + // Assert + Assert.AreEqual(scheme, authParams.AuthScheme); + Assert.AreEqual("someRealm", authParams.RawParameters["realm"]); + + if (values == "token68") + { + Assert.AreEqual("token68", authParams.RawParameters[scheme]); + } } [TestMethod] From 1a08dda302fa9f9cbbb1ac20bfcac29a5593dd1e Mon Sep 17 00:00:00 2001 From: trwalke Date: Wed, 30 Nov 2022 09:16:25 -0800 Subject: [PATCH 25/31] Clean up --- .../WwwAuthenticateParameters.cs | 20 ------------------- 1 file changed, 20 deletions(-) diff --git a/src/client/Microsoft.Identity.Client/WwwAuthenticateParameters.cs b/src/client/Microsoft.Identity.Client/WwwAuthenticateParameters.cs index a3cd24913b..0a8c7cf3f5 100644 --- a/src/client/Microsoft.Identity.Client/WwwAuthenticateParameters.cs +++ b/src/client/Microsoft.Identity.Client/WwwAuthenticateParameters.cs @@ -393,21 +393,6 @@ private static WwwAuthenticateParameters CreateFromWwwAuthenticationHeaderValue( parameters = parametersList.Select(v => AuthenticationHeaderParser.CreateKeyValuePair(v.Trim(), scheme)) .ToDictionary(pair => pair.Key, pair => pair.Value, StringComparer.OrdinalIgnoreCase); - - //var authValuesSplit = wwwAuthenticateValue.Split(new char[] { ' ' }); - - //if (s_knownAuthenticationSchemes.Contains(authValuesSplit[0])) - //{ - // parameters = CoreHelpers.SplitWithQuotes(authValuesSplit[1], ',') - // .Select(v => AuthenticationHeaderParser.CreateKeyValuePair(v.Trim(), scheme)) - // .ToDictionary(pair => pair.Key, pair => pair.Value, StringComparer.OrdinalIgnoreCase); - //} - //else - //{ - // parameters = CoreHelpers.SplitWithQuotes(wwwAuthenticateValue, ',') - // .Select(v => AuthenticationHeaderParser.CreateKeyValuePair(v.Trim(), scheme)) - // .ToDictionary(pair => pair.Key, pair => pair.Value, StringComparer.OrdinalIgnoreCase); - //} } return CreateWwwAuthenticateParameters(parameters, scheme); @@ -424,11 +409,6 @@ private static string[] GetParsedAuthValueElements(string wwwAuthenticateValue) authValuesSplit = authValuesSplit.Skip(1).ToArray(); } - //foreach (var authValue in authValuesSplit) - //{ - // authValue = authValue.Replace(",", string.Empty); - //} - result = authValuesSplit.Select(authValue => authValue.Replace(",", string.Empty)).ToArray(); return result ?? new string[0]; From fd6733ae517c324ca52bb0b38f098c523254b349 Mon Sep 17 00:00:00 2001 From: trwalke Date: Thu, 1 Dec 2022 01:03:43 -0800 Subject: [PATCH 26/31] Pr Feedback --- .../AuthenticationHeaderParser.cs | 2 +- .../AuthenticationInfoParameters.cs | 39 +++++++++++-------- .../MsalErrorMessage.cs | 2 +- .../WwwAuthenticateParameters.cs | 17 ++++---- ...wAuthenticateParametersIntegrationTests.cs | 18 ++------- .../WwwAuthenticateParametersTests.cs | 29 ++++++++++---- 6 files changed, 58 insertions(+), 49 deletions(-) diff --git a/src/client/Microsoft.Identity.Client/AuthenticationHeaderParser.cs b/src/client/Microsoft.Identity.Client/AuthenticationHeaderParser.cs index 2677075ee0..1cc33d9caf 100644 --- a/src/client/Microsoft.Identity.Client/AuthenticationHeaderParser.cs +++ b/src/client/Microsoft.Identity.Client/AuthenticationHeaderParser.cs @@ -105,7 +105,7 @@ public static AuthenticationHeaderParser ParseAuthenticationHeaders(HttpResponse { var wwwParameters = Client.WwwAuthenticateParameters.CreateFromAuthenticationHeaders(httpResponseHeaders); - serverNonce = wwwParameters.SingleOrDefault(parameter => string.Equals(parameter.AuthScheme, Constants.PoPAuthHeaderPrefix, StringComparison.Ordinal))?.PopNonce; + serverNonce = wwwParameters.SingleOrDefault(parameter => string.Equals(parameter.AuthScheme, Constants.PoPAuthHeaderPrefix, StringComparison.Ordinal))?.Nonce; authenticationHeaderParser.WwwAuthenticateParameters = wwwParameters; } diff --git a/src/client/Microsoft.Identity.Client/AuthenticationInfoParameters.cs b/src/client/Microsoft.Identity.Client/AuthenticationInfoParameters.cs index 2b49dba0a8..21ef60bcd6 100644 --- a/src/client/Microsoft.Identity.Client/AuthenticationInfoParameters.cs +++ b/src/client/Microsoft.Identity.Client/AuthenticationInfoParameters.cs @@ -55,35 +55,40 @@ public static AuthenticationInfoParameters CreateFromHeaders(HttpResponseHeaders try { - if (httpResponseHeaders.Contains(AuthenticationInfoKey)) - { - var authInfoValue = httpResponseHeaders.Where(header => header.Key == AuthenticationInfoKey).Single().Value.FirstOrDefault(); - + var authInfoValueList = httpResponseHeaders.SingleOrDefault(header => header.Key == AuthenticationInfoKey).Value; + if (authInfoValueList != null) + { + var authInfoValue = authInfoValueList.FirstOrDefault(); var AuthValuesSplit = authInfoValue.Split(new char[] { ' ' }, 2); + IDictionary paramValues; - var paramValues = CoreHelpers.SplitWithQuotes(AuthValuesSplit[1], ',') - .Select(v => AuthenticationHeaderParser.CreateKeyValuePair(v.Trim(), AuthenticationInfoKey)) - .ToDictionary(pair => pair.Key, pair => pair.Value, StringComparer.OrdinalIgnoreCase); - - parameters.RawParameters = paramValues; - - if (paramValues.TryGetValue("nextnonce", out string value)) + if (AuthValuesSplit.Count() != 2) { - parameters.NextNonce = value; + //Header is not in the form of a=b. + paramValues = new Dictionary(); + paramValues.Add(new KeyValuePair(AuthenticationInfoKey, authInfoValue)); } + else + { + paramValues = CoreHelpers.SplitWithQuotes(AuthValuesSplit[1], ',') + .Select(v => AuthenticationHeaderParser.CreateKeyValuePair(v.Trim(), AuthenticationInfoKey)) + .ToDictionary(pair => pair.Key, pair => pair.Value, StringComparer.OrdinalIgnoreCase); - return parameters; + if (paramValues.TryGetValue("nextnonce", out string value)) + { + parameters.NextNonce = value; + } + } - //Could not get Auth info parameters - throw new MsalClientException(MsalError.UnableToParseAuthenticationHeader, MsalErrorMessage.UnableToParseAuthenticationHeader); + parameters.RawParameters = paramValues; } - return null; + return parameters; } catch (Exception ex) when (ex is not MsalClientException) { - throw new MsalClientException(MsalError.UnableToParseAuthenticationHeader, MsalErrorMessage.UnableToParseAuthenticationHeader, ex); + throw new MsalClientException(MsalError.UnableToParseAuthenticationHeader, MsalErrorMessage.UnableToParseAuthenticationHeader + $" See inner exception for details.", ex); } } } diff --git a/src/client/Microsoft.Identity.Client/MsalErrorMessage.cs b/src/client/Microsoft.Identity.Client/MsalErrorMessage.cs index 9a863ad194..c8c81dc3ec 100644 --- a/src/client/Microsoft.Identity.Client/MsalErrorMessage.cs +++ b/src/client/Microsoft.Identity.Client/MsalErrorMessage.cs @@ -432,7 +432,7 @@ 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. See inner exception for details. For more information on Proof-of-Possession, please see https://aka.ms/msal-net-pop"; + 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) diff --git a/src/client/Microsoft.Identity.Client/WwwAuthenticateParameters.cs b/src/client/Microsoft.Identity.Client/WwwAuthenticateParameters.cs index 0a8c7cf3f5..2eed89e66e 100644 --- a/src/client/Microsoft.Identity.Client/WwwAuthenticateParameters.cs +++ b/src/client/Microsoft.Identity.Client/WwwAuthenticateParameters.cs @@ -65,9 +65,9 @@ public class WwwAuthenticateParameters public string AuthScheme { get; private set; } /// - /// Pop Nonce. This is acquired from with the POP WWW-Authenticate header. + /// The nonce acquired from the WWW-Authenticate header. /// - public string PopNonce { get; private set; } + public string Nonce { get; private set; } /// /// Return the of key . @@ -269,7 +269,7 @@ public static WwwAuthenticateParameters CreateFromAuthenticationHeaders( throw; } - throw new MsalClientException(MsalError.UnableToParseAuthenticationHeader, MsalErrorMessage.UnableToParseAuthenticationHeader, ex); + throw new MsalClientException(MsalError.UnableToParseAuthenticationHeader, MsalErrorMessage.UnableToParseAuthenticationHeader + " See inner exception for details.", ex); } } @@ -369,7 +369,7 @@ public static IReadOnlyList CreateFromAuthenticationH } catch (Exception ex) when (ex is not MsalException) { - throw new MsalClientException(MsalError.UnableToParseAuthenticationHeader, MsalErrorMessage.UnableToParseAuthenticationHeader, ex); + throw new MsalClientException(MsalError.UnableToParseAuthenticationHeader, MsalErrorMessage.UnableToParseAuthenticationHeader + " See inner exception for details.", ex); } } @@ -401,15 +401,16 @@ private static WwwAuthenticateParameters CreateFromWwwAuthenticationHeaderValue( private static string[] GetParsedAuthValueElements(string wwwAuthenticateValue) { string[] result; - var authValuesSplit = wwwAuthenticateValue.Split(new char[] { ' ' }); + char[] charsToTrim = { ',', ' ' }; + var authValuesSplit = CoreHelpers.SplitWithQuotes(wwwAuthenticateValue, ' ' ); //Ensure that known headers are not being parsed. if (s_knownAuthenticationSchemes.Contains(authValuesSplit[0])) { - authValuesSplit = authValuesSplit.Skip(1).ToArray(); + authValuesSplit = authValuesSplit.Skip(1).ToList(); } - result = authValuesSplit.Select(authValue => authValue.Replace(",", string.Empty)).ToArray(); + result = authValuesSplit.Select(authValue => authValue.TrimEnd(charsToTrim)).ToArray(); return result ?? new string[0]; } @@ -463,7 +464,7 @@ internal static WwwAuthenticateParameters CreateWwwAuthenticateParameters(IDicti if (values.TryGetValue("nonce", out value)) { - wwwAuthenticateParameters.PopNonce = value; + wwwAuthenticateParameters.Nonce = value; } return wwwAuthenticateParameters; diff --git a/tests/Microsoft.Identity.Test.Integration.netfx/HeadlessTests/WwwAuthenticateParametersIntegrationTests.cs b/tests/Microsoft.Identity.Test.Integration.netfx/HeadlessTests/WwwAuthenticateParametersIntegrationTests.cs index bbf6b8fe91..5d589b4412 100644 --- a/tests/Microsoft.Identity.Test.Integration.netfx/HeadlessTests/WwwAuthenticateParametersIntegrationTests.cs +++ b/tests/Microsoft.Identity.Test.Integration.netfx/HeadlessTests/WwwAuthenticateParametersIntegrationTests.cs @@ -74,23 +74,11 @@ public async Task CreateWwwAuthenticateResponseFromAzureResourceManagerUrlAsync( Assert.AreEqual(3, authParams.RawParameters.Count); Assert.IsNull(authParams.Claims); Assert.AreEqual("invalid_token", authParams.Error); + Assert.AreEqual($"https://{authority}/{tenantId}", authParams.RawParameters["authorization_uri"]); } [TestMethod] public async Task ExtractNonceFromWwwAuthHeadersAsync() - { - //Arrange & Act - //Test for nonce in WWW-Authenticate header - var parameterList = await WwwAuthenticateParameters.CreateFromAuthenticationResponseAsync( - "https://testingsts.azurewebsites.net/servernonce/invalidsignature").ConfigureAwait(false); - - //Assert - Assert.IsTrue(parameterList.Any(param => param.AuthScheme == Constants.PoPAuthHeaderPrefix)); - Assert.IsNotNull(parameterList.Where(param => param.AuthScheme == Constants.PoPAuthHeaderPrefix).Single().PopNonce); - } - - [TestMethod] - public async Task ExtractNonceFromWwwAuthHeadersRawPamamsAsync() { //Arrange & Act //Test for nonce in WWW-Authenticate header @@ -106,6 +94,8 @@ public async Task ExtractNonceFromWwwAuthHeadersRawPamamsAsync() } //Assert + Assert.IsTrue(parameterList.Any(param => param.AuthScheme == Constants.PoPAuthHeaderPrefix)); + Assert.IsNotNull(parameterList.Single(param => param.AuthScheme == Constants.PoPAuthHeaderPrefix).Nonce); Assert.IsTrue(!popNonce.IsNullOrEmpty()); await TestCommon.ValidatePopNonceAsync(popNonce).ConfigureAwait(false); } @@ -135,7 +125,7 @@ public async Task ExtractNonceWithAuthParserAsync() //Assert Assert.IsTrue(parsedHeaders.WwwAuthenticateParameters.Any(param => param.AuthScheme == Constants.PoPAuthHeaderPrefix)); - var serverNonce = parsedHeaders.WwwAuthenticateParameters.Where(param => param.AuthScheme == Constants.PoPAuthHeaderPrefix).Single().PopNonce; + var serverNonce = parsedHeaders.WwwAuthenticateParameters.Where(param => param.AuthScheme == Constants.PoPAuthHeaderPrefix).Single().Nonce; Assert.IsNotNull(serverNonce); Assert.AreEqual(parsedHeaders.PopNonce, serverNonce); Assert.IsNull(parsedHeaders.AuthenticationInfoParameters); diff --git a/tests/Microsoft.Identity.Test.Unit/WwwAuthenticateParametersTests.cs b/tests/Microsoft.Identity.Test.Unit/WwwAuthenticateParametersTests.cs index 57c40827bb..d0d619f542 100644 --- a/tests/Microsoft.Identity.Test.Unit/WwwAuthenticateParametersTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/WwwAuthenticateParametersTests.cs @@ -131,8 +131,8 @@ public void CreateWwwAuthenticateResponseForUnknownChallenges(string scheme, str // Assert if (scheme == "NTLM") { - Assert.AreEqual(scheme, authParams.AuthScheme); - Assert.AreEqual(values, authParams.RawParameters[scheme]); + Assert.AreEqual("NTLM", authParams.AuthScheme); + Assert.AreEqual("dG9rZW42OA==", authParams.RawParameters[scheme]); } else { @@ -144,9 +144,22 @@ public void CreateWwwAuthenticateResponseForUnknownChallenges(string scheme, str } [TestMethod] - [DataRow("nextnonce", "")] + public void CreateFromtoken68AuthInfoResponse() + { + // Arrange + HttpResponseMessage httpResponse = new HttpResponseMessage(HttpStatusCode.Unauthorized); + httpResponse.Headers.Add(AuthenticationInfoName, $"token68"); + + // Act + var parameters = AuthenticationInfoParameters.CreateFromHeaders(httpResponse.Headers); + + //Assert + Assert.IsNotNull(parameters); + Assert.AreEqual("token68", parameters.RawParameters[AuthenticationInfoName]); + } + + [TestMethod] [DataRow("nextnonce", "Some, Malformed, Nonce")] - [DataRow("", TestConstants.Nonce)] [DataRow("", "Some, Malformed, Nonce")] public void CreateFromMalformedAuthInfoResponse(string paramName, string value) { @@ -160,7 +173,7 @@ public void CreateFromMalformedAuthInfoResponse(string paramName, string value) //Assert Assert.AreEqual(ex.ErrorCode, MsalError.UnableToParseAuthenticationHeader); - Assert.AreEqual(ex.Message, MsalErrorMessage.UnableToParseAuthenticationHeader); + Assert.AreEqual(ex.Message, MsalErrorMessage.UnableToParseAuthenticationHeader + " See inner exception for details."); } [TestMethod] @@ -415,7 +428,7 @@ public async Task CreateAllFromResourceResponseAsync_HttpClient_Bearer_Pop_Async Assert.IsNotNull(bearerHeader); Assert.AreEqual("https://login.microsoftonline.com/TenantId", bearerHeader.Authority); Assert.IsNotNull(popHeader); - Assert.AreEqual(TestConstants.Nonce, popHeader.PopNonce); + Assert.AreEqual(TestConstants.Nonce, popHeader.Nonce); } [TestMethod] @@ -435,7 +448,7 @@ public void ExtractAllWWWAuthenticateParametersFromResponse(bool combineHeaders) Assert.IsNotNull(bearerHeader); Assert.AreEqual("https://login.microsoftonline.com/TenantId", bearerHeader.Authority); Assert.IsNotNull(popHeader); - Assert.AreEqual(TestConstants.Nonce, popHeader.PopNonce); + Assert.AreEqual(TestConstants.Nonce, popHeader.Nonce); } [TestMethod] @@ -455,7 +468,7 @@ public void ExtractAllParametersFromResponseWithAuthParser(bool combineHeaders) Assert.IsNotNull(bearerHeader); Assert.AreEqual("https://login.microsoftonline.com/TenantId", bearerHeader.Authority); Assert.IsNotNull(popHeader); - Assert.AreEqual(TestConstants.Nonce, popHeader.PopNonce); + Assert.AreEqual(TestConstants.Nonce, popHeader.Nonce); } [TestMethod] From 9626ec3a9b63ff4fd42b36d7f799f0a35e8e38fe Mon Sep 17 00:00:00 2001 From: trwalke Date: Thu, 1 Dec 2022 01:12:00 -0800 Subject: [PATCH 27/31] test update --- .../WwwAuthenticateParametersTests.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tests/Microsoft.Identity.Test.Unit/WwwAuthenticateParametersTests.cs b/tests/Microsoft.Identity.Test.Unit/WwwAuthenticateParametersTests.cs index d0d619f542..d40d749ac6 100644 --- a/tests/Microsoft.Identity.Test.Unit/WwwAuthenticateParametersTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/WwwAuthenticateParametersTests.cs @@ -110,10 +110,16 @@ public void EnsureProperlyFormattedHeadersWithRealmDoNotFail(string scheme, stri Assert.AreEqual(scheme, authParams.AuthScheme); Assert.AreEqual("someRealm", authParams.RawParameters["realm"]); - if (values == "token68") + if (values.Contains("token68")) { Assert.AreEqual("token68", authParams.RawParameters[scheme]); } + else if (values.Contains("auth-param1")) + { + Assert.AreEqual("token1", authParams.RawParameters["auth-param1"]); + Assert.AreEqual("token2", authParams.RawParameters["auth-param2"]); + Assert.AreEqual("token3", authParams.RawParameters["auth-param3"]); + } } [TestMethod] From a1cdedf3bf4c3009f271796ad4b38b61db0653db Mon Sep 17 00:00:00 2001 From: Travis Walker Date: Thu, 1 Dec 2022 01:12:23 -0800 Subject: [PATCH 28/31] Update src/client/Microsoft.Identity.Client/WwwAuthenticateParameters.cs Co-authored-by: Bogdan Gavril --- .../Microsoft.Identity.Client/WwwAuthenticateParameters.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/Microsoft.Identity.Client/WwwAuthenticateParameters.cs b/src/client/Microsoft.Identity.Client/WwwAuthenticateParameters.cs index 2eed89e66e..cb07732988 100644 --- a/src/client/Microsoft.Identity.Client/WwwAuthenticateParameters.cs +++ b/src/client/Microsoft.Identity.Client/WwwAuthenticateParameters.cs @@ -131,7 +131,7 @@ public static Task CreateFromResourceResponseAsync(st /// The cancellation token to cancel operation. /// WWW-Authenticate Parameters extracted from response to the unauthenticated call. [System.ComponentModel.EditorBrowsable(EditorBrowsableState.Never)] - [Obsolete("This api is now obsolete and has been replaced with CreateFromAuthenticationResponseAsync(...)")] + [Obsolete("This api is now obsolete and has been replaced with replaced with CreateFromAuthenticationResponseAsync(HttpResponseHeaders, string)")] public static async Task CreateFromResourceResponseAsync(HttpClient httpClient, string resourceUri, CancellationToken cancellationToken = default) { if (httpClient is null) From 37b1ab70a8de6dbd83634008f354a76150a5636e Mon Sep 17 00:00:00 2001 From: trwalke Date: Wed, 7 Dec 2022 08:48:12 -0800 Subject: [PATCH 29/31] Adding additional tests Refactoring. --- .../WwwAuthenticateParameters.cs | 50 ++++++------ .../WwwAuthenticateParametersTests.cs | 80 ++++++++++++++++++- 2 files changed, 104 insertions(+), 26 deletions(-) diff --git a/src/client/Microsoft.Identity.Client/WwwAuthenticateParameters.cs b/src/client/Microsoft.Identity.Client/WwwAuthenticateParameters.cs index cb07732988..cd96043931 100644 --- a/src/client/Microsoft.Identity.Client/WwwAuthenticateParameters.cs +++ b/src/client/Microsoft.Identity.Client/WwwAuthenticateParameters.cs @@ -275,31 +275,6 @@ public static WwwAuthenticateParameters CreateFromAuthenticationHeaders( return CreateWwwAuthenticateParameters(new Dictionary(), string.Empty); } - - /// - /// Gets the claim challenge from HTTP header. - /// Used, for example, for CA auth context. - /// - /// The HTTP response headers. - /// Authentication scheme. Default is Bearer. - /// The claims challenge - public static string GetClaimChallengeFromResponseHeaders( - HttpResponseHeaders httpResponseHeaders, - string scheme = Constants.BearerAuthHeaderPrefix) - { - WwwAuthenticateParameters parameters = CreateFromAuthenticationHeaders( - httpResponseHeaders, - scheme); - - // read the header and checks if it contains an error with insufficient_claims value. - if (parameters.Claims != null && - string.Equals(parameters.Error, "insufficient_claims", StringComparison.OrdinalIgnoreCase)) - { - return parameters.Claims; - } - - return null; - } #endregion Single Scheme Api #region Multi Scheme Api @@ -377,6 +352,31 @@ public static IReadOnlyList CreateFromAuthenticationH } #endregion Multi Scheme Api + /// + /// Gets the claim challenge from HTTP header. + /// Used, for example, for CA auth context. + /// + /// The HTTP response headers. + /// Authentication scheme. Default is Bearer. + /// The claims challenge + public static string GetClaimChallengeFromResponseHeaders( + HttpResponseHeaders httpResponseHeaders, + string scheme = Constants.BearerAuthHeaderPrefix) + { + WwwAuthenticateParameters parameters = CreateFromAuthenticationHeaders( + httpResponseHeaders, + scheme); + + // read the header and checks if it contains an error with insufficient_claims value. + if (parameters.Claims != null && + string.Equals(parameters.Error, "insufficient_claims", StringComparison.OrdinalIgnoreCase)) + { + return parameters.Claims; + } + + return null; + } + /// /// Creates parameters from the WWW-Authenticate string. /// diff --git a/tests/Microsoft.Identity.Test.Unit/WwwAuthenticateParametersTests.cs b/tests/Microsoft.Identity.Test.Unit/WwwAuthenticateParametersTests.cs index d40d749ac6..3a5851cdc2 100644 --- a/tests/Microsoft.Identity.Test.Unit/WwwAuthenticateParametersTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/WwwAuthenticateParametersTests.cs @@ -124,6 +124,7 @@ public void EnsureProperlyFormattedHeadersWithRealmDoNotFail(string scheme, stri [TestMethod] [DataRow("WLID1.0", "realm=WindowsLive, policy=MBI_SSL, siteId=\"ssl.live-tst.net\"")] + [DataRow("WLID2.0", "realm=WindowsLive, policy=MBI_SSL, siteId=\"ssl.live-tst.net - ssl2.live-tst2.net\"")]//Adding spaces in between quotes to ensure parsing is not broken [DataRow("NTLM", "dG9rZW42OA==")] public void CreateWwwAuthenticateResponseForUnknownChallenges(string scheme, string values) { @@ -140,13 +141,20 @@ public void CreateWwwAuthenticateResponseForUnknownChallenges(string scheme, str Assert.AreEqual("NTLM", authParams.AuthScheme); Assert.AreEqual("dG9rZW42OA==", authParams.RawParameters[scheme]); } - else + else if (scheme == "WLID1.0") { Assert.AreEqual(3, authParams.RawParameters.Count); Assert.AreEqual("WindowsLive", authParams.RawParameters["realm"]); Assert.AreEqual("MBI_SSL", authParams.RawParameters["policy"]); Assert.AreEqual("ssl.live-tst.net", authParams.RawParameters["siteId"]); } + else + { + Assert.AreEqual(3, authParams.RawParameters.Count); + Assert.AreEqual("WindowsLive", authParams.RawParameters["realm"]); + Assert.AreEqual("MBI_SSL", authParams.RawParameters["policy"]); + Assert.AreEqual("ssl.live-tst.net - ssl2.live-tst2.net", authParams.RawParameters["siteId"]); + } } [TestMethod] @@ -457,6 +465,50 @@ public void ExtractAllWWWAuthenticateParametersFromResponse(bool combineHeaders) Assert.AreEqual(TestConstants.Nonce, popHeader.Nonce); } + [TestMethod] + public void ExtractDuplicateWWWAuthenticateParametersFromResponse() + { + // Arrange + HttpResponseMessage httpResponse = CreateMultipleHttpHeaderResponse(); + + // Act + var headers = WwwAuthenticateParameters.CreateFromAuthenticationHeaders(httpResponse.Headers); + var bearerHeader1 = headers.Where(header => header.AuthScheme == "Bearer").First(); + var popHeader1 = headers.Where(header => header.AuthScheme == "PoP").First(); + var bearerHeader2 = headers.Where(header => header.AuthScheme == "Bearer").Last(); + var popHeader2 = headers.Where(header => header.AuthScheme == "PoP").Last(); + + // Assert + Assert.IsNotNull(bearerHeader1); + Assert.AreEqual("https://login.microsoftonline.com/TenantId", bearerHeader1.Authority); + Assert.IsNotNull(popHeader1); + Assert.AreEqual(TestConstants.Nonce, popHeader1.Nonce); + + Assert.IsNotNull(bearerHeader2); + Assert.AreEqual("https://login.microsoftonline.com/TenantId2", bearerHeader2.Authority); + + Assert.IsNotNull(popHeader2); + Assert.AreEqual("someNonce2", popHeader2.Nonce); + } + + [TestMethod] + public void ExtractAllUppercaseParametersFromResponse() + { + // Arrange + HttpResponseMessage httpResponse = CreateUppercaseHttpHeaderResponse(); + + // Act + var headers = WwwAuthenticateParameters.CreateFromAuthenticationHeaders(httpResponse.Headers); + var bearerHeader = headers.Where(header => header.AuthScheme == "Bearer").Single(); + var popHeader = headers.Where(header => header.AuthScheme == "PoP").Single(); + + // Assert + Assert.IsNotNull(bearerHeader); + Assert.AreEqual("https://login.microsoftonline.com/TenantId", bearerHeader.Authority); + Assert.IsNotNull(popHeader); + Assert.AreEqual(TestConstants.Nonce, popHeader.Nonce); + } + [TestMethod] [DataRow(false)] [DataRow(true)] @@ -598,6 +650,32 @@ private static HttpResponseMessage CreateBearerAndPopHttpResponse(bool combinedC }; } + private static HttpResponseMessage CreateMultipleHttpHeaderResponse() + { + return new HttpResponseMessage(HttpStatusCode.Unauthorized) + { + Headers = + { + { WwwAuthenticateHeaderName, $"Bearer authorization_uri=\"https://login.microsoftonline.com/TenantId/oauth2/authorize\"" }, + { WwwAuthenticateHeaderName, $"Bearer authorization_uri=\"https://login.microsoftonline.com/TenantId/oauth2/authorize2\"" }, + { WwwAuthenticateHeaderName, $"PoP nonce=\"someNonce\"" }, + { WwwAuthenticateHeaderName, $"PoP nonce=\"someNonce2\"" } + } + }; + } + + private static HttpResponseMessage CreateUppercaseHttpHeaderResponse() + { + return new HttpResponseMessage(HttpStatusCode.Unauthorized) + { + Headers = + { + { WwwAuthenticateHeaderName, $"Bearer AUTHORIZATION_URI=\"https://login.microsoftonline.com/TenantId/oauth2/authorize\"" }, + { WwwAuthenticateHeaderName, $"PoP NONCE=\"someNonce\"" } + } + }; + } + private static HttpResponseMessage CreateAuthInfoHttpResponse() { return new HttpResponseMessage(HttpStatusCode.Unauthorized) From d6a1e1bba59169b009ceda245dc0189fcccb80f8 Mon Sep 17 00:00:00 2001 From: trwalke Date: Mon, 12 Dec 2022 23:57:38 -0800 Subject: [PATCH 30/31] Refactoring tests. Updating error message --- .../AuthenticationHeaderParser.cs | 14 +------ .../AuthenticationInfoParameters.cs | 2 +- .../WwwAuthenticateParameters.cs | 30 ++------------- .../WwwAuthenticateParametersTests.cs | 38 ++++++++++--------- 4 files changed, 27 insertions(+), 57 deletions(-) diff --git a/src/client/Microsoft.Identity.Client/AuthenticationHeaderParser.cs b/src/client/Microsoft.Identity.Client/AuthenticationHeaderParser.cs index 1cc33d9caf..21dca750a1 100644 --- a/src/client/Microsoft.Identity.Client/AuthenticationHeaderParser.cs +++ b/src/client/Microsoft.Identity.Client/AuthenticationHeaderParser.cs @@ -41,23 +41,13 @@ public class AuthenticationHeaderParser /// public string PopNonce { get; private set; } - /// - /// Creates the authenticate parameters by attempting to call the resource unauthenticated, and analyzing the response. - /// - /// URI of the resource. - /// - public static async Task ParseAuthenticationHeadersAsync(string resourceUri) - { - return await ParseAuthenticationHeadersAsync(resourceUri, default).ConfigureAwait(false); - } - /// /// Creates the authenticate parameters by attempting to call the resource unauthenticated, and analyzing the response. /// /// URI of the resource. /// The cancellation token to cancel the operation. /// - public static async Task ParseAuthenticationHeadersAsync(string resourceUri, CancellationToken cancellationToken) + public static async Task ParseAuthenticationHeadersAsync(string resourceUri, CancellationToken cancellationToken = default) { return await ParseAuthenticationHeadersAsync(resourceUri, GetHttpClient(), cancellationToken).ConfigureAwait(false); } @@ -70,7 +60,7 @@ public static async Task ParseAuthenticationHeadersA /// Instance of to make the request with. /// /// - public static async Task ParseAuthenticationHeadersAsync(string resourceUri, HttpClient httpClient, CancellationToken cancellationToken) + public static async Task ParseAuthenticationHeadersAsync(string resourceUri, HttpClient httpClient, CancellationToken cancellationToken = default) { if (httpClient is null) { diff --git a/src/client/Microsoft.Identity.Client/AuthenticationInfoParameters.cs b/src/client/Microsoft.Identity.Client/AuthenticationInfoParameters.cs index 21ef60bcd6..4bf80932e8 100644 --- a/src/client/Microsoft.Identity.Client/AuthenticationInfoParameters.cs +++ b/src/client/Microsoft.Identity.Client/AuthenticationInfoParameters.cs @@ -88,7 +88,7 @@ public static AuthenticationInfoParameters CreateFromHeaders(HttpResponseHeaders } catch (Exception ex) when (ex is not MsalClientException) { - throw new MsalClientException(MsalError.UnableToParseAuthenticationHeader, MsalErrorMessage.UnableToParseAuthenticationHeader + $" See inner exception for details.", ex); + throw new MsalClientException(MsalError.UnableToParseAuthenticationHeader, MsalErrorMessage.UnableToParseAuthenticationHeader + $"Response Headers: {httpResponseHeaders.ToString()} See inner exception for details.", ex); } } } diff --git a/src/client/Microsoft.Identity.Client/WwwAuthenticateParameters.cs b/src/client/Microsoft.Identity.Client/WwwAuthenticateParameters.cs index cd96043931..10e160de98 100644 --- a/src/client/Microsoft.Identity.Client/WwwAuthenticateParameters.cs +++ b/src/client/Microsoft.Identity.Client/WwwAuthenticateParameters.cs @@ -192,17 +192,6 @@ public static WwwAuthenticateParameters CreateFromWwwAuthenticateHeaderValue(str #endregion Obsolete Api #region Single Scheme Api - /// - /// Create the authenticate parameters by attempting to call the resource unauthenticated, and analyzing the response. - /// - /// URI of the resource. - /// Authentication scheme. - /// WWW-Authenticate Parameters extracted from response to the unauthenticated call. - public static Task CreateFromAuthenticationResponseAsync(string resourceUri, string scheme) - { - return CreateFromAuthenticationResponseAsync(resourceUri, scheme, default); - } - /// /// Create the authenticate parameters by attempting to call the resource unauthenticated, and analyzing the response. /// @@ -210,7 +199,7 @@ public static Task CreateFromAuthenticationResponseAs /// Authentication scheme. /// The cancellation token to cancel operation. /// WWW-Authenticate Parameters extracted from response to the unauthenticated call. - public static Task CreateFromAuthenticationResponseAsync(string resourceUri, string scheme, CancellationToken cancellationToken) + public static Task CreateFromAuthenticationResponseAsync(string resourceUri, string scheme, CancellationToken cancellationToken = default) { return CreateFromAuthenticationResponseAsync(resourceUri, scheme, AuthenticationHeaderParser.GetHttpClient(), cancellationToken); } @@ -223,7 +212,7 @@ public static Task CreateFromAuthenticationResponseAs /// The cancellation token to cancel operation. /// Authentication scheme. /// WWW-Authenticate Parameters extracted from response to the unauthenticated call. - public static async Task CreateFromAuthenticationResponseAsync(string resourceUri, string scheme, HttpClient httpClient, CancellationToken cancellationToken) + public static async Task CreateFromAuthenticationResponseAsync(string resourceUri, string scheme, HttpClient httpClient, CancellationToken cancellationToken = default) { if (httpClient is null) { @@ -278,24 +267,13 @@ public static WwwAuthenticateParameters CreateFromAuthenticationHeaders( #endregion Single Scheme Api #region Multi Scheme Api - - /// - /// Create the authenticate parameters by attempting to call the resource unauthenticated, and analyzing the response. - /// - /// URI of the resource. - /// WWW-Authenticate Parameters extracted from response to the unauthenticated call. - public static Task> CreateFromAuthenticationResponseAsync(string resourceUri) - { - return CreateFromAuthenticationResponseAsync(resourceUri, new CancellationToken()); - } - /// /// Create the authenticate parameters by attempting to call the resource unauthenticated, and analyzing the response. /// /// URI of the resource. /// The cancellation token to cancel operation. /// WWW-Authenticate Parameters extracted from response to the unauthenticated call. - public static Task> CreateFromAuthenticationResponseAsync(string resourceUri, CancellationToken cancellationToken) + public static Task> CreateFromAuthenticationResponseAsync(string resourceUri, CancellationToken cancellationToken = default) { return CreateFromAuthenticationResponseAsync(resourceUri, AuthenticationHeaderParser.GetHttpClient(), cancellationToken); } @@ -307,7 +285,7 @@ public static Task> CreateFromAuthentic /// URI of the resource. /// The cancellation token to cancel operation. /// WWW-Authenticate Parameters extracted from response to the unauthenticated call. - public static async Task> CreateFromAuthenticationResponseAsync(string resourceUri, HttpClient httpClient, CancellationToken cancellationToken) + public static async Task> CreateFromAuthenticationResponseAsync(string resourceUri, HttpClient httpClient, CancellationToken cancellationToken = default) { if (httpClient is null) { diff --git a/tests/Microsoft.Identity.Test.Unit/WwwAuthenticateParametersTests.cs b/tests/Microsoft.Identity.Test.Unit/WwwAuthenticateParametersTests.cs index 3a5851cdc2..d29010a2d8 100644 --- a/tests/Microsoft.Identity.Test.Unit/WwwAuthenticateParametersTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/WwwAuthenticateParametersTests.cs @@ -6,6 +6,7 @@ using System.Linq; using System.Net; using System.Net.Http; +using System.Net.Http.Headers; using System.Threading.Tasks; using Microsoft.Identity.Client; using Microsoft.Identity.Client.Internal; @@ -60,10 +61,10 @@ public void CreateWwwAuthenticateResponse(string resource, string authorizationU } [TestMethod] - [DataRow("AuthScheme", "")] - [DataRow("AuthScheme", "token68")] - [DataRow("AuthScheme", "auth-param1=token1, auth-param2=token2, auth-param3=token3")] - [DataRow("AuthScheme", "token68 auth-param1=token1, auth-param2=token2, auth-param3=token3")] + [DataRow("AuthScheme1", "")] + [DataRow("AuthScheme2", "token68")] + [DataRow("AuthScheme3", "auth-param1=token1, auth-param2=token2, auth-param3=token3")] + [DataRow("AuthScheme4", "token68 auth-param1=token1, auth-param2=token2, auth-param3=token3")] public void EnsureProperlyFormattedHeadersWithToken86DoNotFail(string scheme, string values) { // Arrange @@ -76,11 +77,11 @@ public void EnsureProperlyFormattedHeadersWithToken86DoNotFail(string scheme, st // Assert Assert.AreEqual(scheme, authParams.AuthScheme); - if (string.IsNullOrEmpty(values)) + if (scheme == "AuthScheme1") { Assert.AreEqual(0, authParams.RawParameters.Count); } - else if (values == "token68") + else if (scheme == "AuthScheme2") { Assert.AreEqual("token68", authParams.RawParameters[scheme]); } @@ -93,10 +94,10 @@ public void EnsureProperlyFormattedHeadersWithToken86DoNotFail(string scheme, st } [TestMethod] - [DataRow("AuthScheme", "realm=someRealm")] - [DataRow("AuthScheme", "realm=someRealm token68")] - [DataRow("AuthScheme", "realm=someRealm auth-param1=token1, auth-param2=token2, auth-param3=token3")] - [DataRow("AuthScheme", "realm=someRealm token68 auth-param1=token1, auth-param2=token2, auth-param3=token3")] + [DataRow("AuthScheme1", "realm=someRealm")] + [DataRow("AuthScheme2", "realm=someRealm token68")] + [DataRow("AuthScheme3", "realm=someRealm auth-param1=token1, auth-param2=token2, auth-param3=token3")] + [DataRow("AuthScheme4", "realm=someRealm token68 auth-param1=token1, auth-param2=token2, auth-param3=token3")] public void EnsureProperlyFormattedHeadersWithRealmDoNotFail(string scheme, string values) { // Arrange @@ -110,11 +111,12 @@ public void EnsureProperlyFormattedHeadersWithRealmDoNotFail(string scheme, stri Assert.AreEqual(scheme, authParams.AuthScheme); Assert.AreEqual("someRealm", authParams.RawParameters["realm"]); - if (values.Contains("token68")) + if (scheme == "AuthScheme2" || scheme == "AuthScheme4") { Assert.AreEqual("token68", authParams.RawParameters[scheme]); } - else if (values.Contains("auth-param1")) + + if (scheme == "AuthScheme3" || scheme == "AuthScheme4") { Assert.AreEqual("token1", authParams.RawParameters["auth-param1"]); Assert.AreEqual("token2", authParams.RawParameters["auth-param2"]); @@ -187,7 +189,7 @@ public void CreateFromMalformedAuthInfoResponse(string paramName, string value) //Assert Assert.AreEqual(ex.ErrorCode, MsalError.UnableToParseAuthenticationHeader); - Assert.AreEqual(ex.Message, MsalErrorMessage.UnableToParseAuthenticationHeader + " See inner exception for details."); + Assert.AreEqual(ex.Message, MsalErrorMessage.UnableToParseAuthenticationHeader + $"Response Headers: {httpResponse.Headers.ToString()} See inner exception for details."); } [TestMethod] @@ -355,21 +357,21 @@ public async Task CreateFromResourceResponseAsync_HttpClient_ADFS_GetTenantId_Nu Assert.IsNull(authParamList.FirstOrDefault().GetTenantId()); } - [DataRow(null)] [TestMethod] - public async Task CreateFromResourceResponseAsync_HttpClient_Null_Async(HttpClient httpClient) + public async Task CreateFromResourceResponseAsync_HttpClient_Null_Async() { const string resourceUri = "https://example.com/"; + HttpClient client = null; - Func action = () => WwwAuthenticateParameters.CreateFromResourceResponseAsync(httpClient, resourceUri); + Func action = () => WwwAuthenticateParameters.CreateFromResourceResponseAsync(null, resourceUri); await Assert.ThrowsExceptionAsync(action).ConfigureAwait(false); - action = () => WwwAuthenticateParameters.CreateFromAuthenticationResponseAsync(resourceUri, "Bearer", httpClient, default); + action = () => WwwAuthenticateParameters.CreateFromAuthenticationResponseAsync(resourceUri, "Bearer", null, default); await Assert.ThrowsExceptionAsync(action).ConfigureAwait(false); - action = () => WwwAuthenticateParameters.CreateFromAuthenticationResponseAsync(resourceUri, httpClient, default); + action = () => WwwAuthenticateParameters.CreateFromAuthenticationResponseAsync(resourceUri, client, default); await Assert.ThrowsExceptionAsync(action).ConfigureAwait(false); } From c3addbe579223929c67ff97658d3b0c54ca4aa52 Mon Sep 17 00:00:00 2001 From: trwalke Date: Tue, 13 Dec 2022 00:25:56 -0800 Subject: [PATCH 31/31] Refactoring --- .../AuthenticationHeaderParser.cs | 4 +- .../AuthenticationInfoParameters.cs | 2 +- .../WwwAuthenticateParameters.cs | 9 +++-- ...wAuthenticateParametersIntegrationTests.cs | 14 +++---- .../WwwAuthenticateParametersTests.cs | 38 +++++++++---------- 5 files changed, 34 insertions(+), 33 deletions(-) diff --git a/src/client/Microsoft.Identity.Client/AuthenticationHeaderParser.cs b/src/client/Microsoft.Identity.Client/AuthenticationHeaderParser.cs index 21dca750a1..55771df47a 100644 --- a/src/client/Microsoft.Identity.Client/AuthenticationHeaderParser.cs +++ b/src/client/Microsoft.Identity.Client/AuthenticationHeaderParser.cs @@ -95,7 +95,7 @@ public static AuthenticationHeaderParser ParseAuthenticationHeaders(HttpResponse { var wwwParameters = Client.WwwAuthenticateParameters.CreateFromAuthenticationHeaders(httpResponseHeaders); - serverNonce = wwwParameters.SingleOrDefault(parameter => string.Equals(parameter.AuthScheme, Constants.PoPAuthHeaderPrefix, StringComparison.Ordinal))?.Nonce; + serverNonce = wwwParameters.SingleOrDefault(parameter => string.Equals(parameter.AuthenticationScheme, Constants.PoPAuthHeaderPrefix, StringComparison.Ordinal))?.Nonce; authenticationHeaderParser.WwwAuthenticateParameters = wwwParameters; } @@ -104,7 +104,7 @@ public static AuthenticationHeaderParser ParseAuthenticationHeaders(HttpResponse authenticationHeaderParser.WwwAuthenticateParameters = new List(); //If no WWW-AuthenticateHeaders exist, attempt to parse AuthenticationInfo headers instead - authenticationInfoParameters = AuthenticationInfoParameters.CreateFromHeaders(httpResponseHeaders); + authenticationInfoParameters = AuthenticationInfoParameters.CreateFromResponseHeaders(httpResponseHeaders); authenticationHeaderParser.AuthenticationInfoParameters = authenticationInfoParameters; } diff --git a/src/client/Microsoft.Identity.Client/AuthenticationInfoParameters.cs b/src/client/Microsoft.Identity.Client/AuthenticationInfoParameters.cs index 4bf80932e8..697c8f7328 100644 --- a/src/client/Microsoft.Identity.Client/AuthenticationInfoParameters.cs +++ b/src/client/Microsoft.Identity.Client/AuthenticationInfoParameters.cs @@ -49,7 +49,7 @@ public string this[string key] /// /// HttpResponseHeaders. /// Authentication-Info provided by the endpoint - public static AuthenticationInfoParameters CreateFromHeaders(HttpResponseHeaders httpResponseHeaders) + public static AuthenticationInfoParameters CreateFromResponseHeaders(HttpResponseHeaders httpResponseHeaders) { AuthenticationInfoParameters parameters = new AuthenticationInfoParameters(); diff --git a/src/client/Microsoft.Identity.Client/WwwAuthenticateParameters.cs b/src/client/Microsoft.Identity.Client/WwwAuthenticateParameters.cs index 10e160de98..c29b89f033 100644 --- a/src/client/Microsoft.Identity.Client/WwwAuthenticateParameters.cs +++ b/src/client/Microsoft.Identity.Client/WwwAuthenticateParameters.cs @@ -10,6 +10,7 @@ using System.Text; using System.Threading; using System.Threading.Tasks; +using Microsoft.Identity.Client.Http; using Microsoft.Identity.Client.Internal; using Microsoft.Identity.Client.Utils; @@ -62,7 +63,7 @@ public class WwwAuthenticateParameters /// AuthScheme. /// See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/WWW-Authenticate#syntax for more details /// - public string AuthScheme { get; private set; } + public string AuthenticationScheme { get; private set; } /// /// The nonce acquired from the WWW-Authenticate header. @@ -170,7 +171,7 @@ public static WwwAuthenticateParameters CreateFromResponseHeaders( { string wwwAuthenticateValue = headerValue.Parameter; var parameters = CreateFromWwwAuthenticateHeaderValue(wwwAuthenticateValue); - parameters.AuthScheme = scheme; + parameters.AuthenticationScheme = scheme; return parameters; } } @@ -258,7 +259,7 @@ public static WwwAuthenticateParameters CreateFromAuthenticationHeaders( throw; } - throw new MsalClientException(MsalError.UnableToParseAuthenticationHeader, MsalErrorMessage.UnableToParseAuthenticationHeader + " See inner exception for details.", ex); + throw new MsalClientException(MsalError.UnableToParseAuthenticationHeader, MsalErrorMessage.UnableToParseAuthenticationHeader + $"Response Headers: {httpResponseHeaders.ToString()} See inner exception for details.", ex); } } @@ -397,7 +398,7 @@ internal static WwwAuthenticateParameters CreateWwwAuthenticateParameters(IDicti { WwwAuthenticateParameters wwwAuthenticateParameters = new WwwAuthenticateParameters(); - wwwAuthenticateParameters.AuthScheme = scheme; + wwwAuthenticateParameters.AuthenticationScheme = scheme; if (values == null) { diff --git a/tests/Microsoft.Identity.Test.Integration.netfx/HeadlessTests/WwwAuthenticateParametersIntegrationTests.cs b/tests/Microsoft.Identity.Test.Integration.netfx/HeadlessTests/WwwAuthenticateParametersIntegrationTests.cs index 5d589b4412..05d670a871 100644 --- a/tests/Microsoft.Identity.Test.Integration.netfx/HeadlessTests/WwwAuthenticateParametersIntegrationTests.cs +++ b/tests/Microsoft.Identity.Test.Integration.netfx/HeadlessTests/WwwAuthenticateParametersIntegrationTests.cs @@ -88,14 +88,14 @@ public async Task ExtractNonceFromWwwAuthHeadersAsync() var parameters = parameterList.FirstOrDefault(); - if (parameters.AuthScheme == "PoP" && parameters.RawParameters.Keys.Contains("nonce")) //Check if next nonce for POP is available + if (parameters.AuthenticationScheme == "PoP" && parameters.RawParameters.Keys.Contains("nonce")) //Check if next nonce for POP is available { popNonce = parameters.RawParameters["nonce"]; } //Assert - Assert.IsTrue(parameterList.Any(param => param.AuthScheme == Constants.PoPAuthHeaderPrefix)); - Assert.IsNotNull(parameterList.Single(param => param.AuthScheme == Constants.PoPAuthHeaderPrefix).Nonce); + Assert.IsTrue(parameterList.Any(param => param.AuthenticationScheme == Constants.PoPAuthHeaderPrefix)); + Assert.IsNotNull(parameterList.Single(param => param.AuthenticationScheme == Constants.PoPAuthHeaderPrefix).Nonce); Assert.IsTrue(!popNonce.IsNullOrEmpty()); await TestCommon.ValidatePopNonceAsync(popNonce).ConfigureAwait(false); } @@ -110,7 +110,7 @@ public async Task ExtractNonceFromAuthInfoHeadersAsync() HttpResponseMessage httpResponseMessage = await httpClient.GetAsync("https://testingsts.azurewebsites.net/servernonce/authinfo", new CancellationToken()).ConfigureAwait(false); //Assert - var authInfoParameters = AuthenticationInfoParameters.CreateFromHeaders(httpResponseMessage.Headers); + var authInfoParameters = AuthenticationInfoParameters.CreateFromResponseHeaders(httpResponseMessage.Headers); Assert.IsNotNull(authInfoParameters); Assert.IsNotNull(authInfoParameters.NextNonce); await TestCommon.ValidatePopNonceAsync(authInfoParameters.NextNonce).ConfigureAwait(false); @@ -124,8 +124,8 @@ public async Task ExtractNonceWithAuthParserAsync() var parsedHeaders = await AuthenticationHeaderParser.ParseAuthenticationHeadersAsync("https://testingsts.azurewebsites.net/servernonce/invalidsignature").ConfigureAwait(false); //Assert - Assert.IsTrue(parsedHeaders.WwwAuthenticateParameters.Any(param => param.AuthScheme == Constants.PoPAuthHeaderPrefix)); - var serverNonce = parsedHeaders.WwwAuthenticateParameters.Where(param => param.AuthScheme == Constants.PoPAuthHeaderPrefix).Single().Nonce; + Assert.IsTrue(parsedHeaders.WwwAuthenticateParameters.Any(param => param.AuthenticationScheme == Constants.PoPAuthHeaderPrefix)); + var serverNonce = parsedHeaders.WwwAuthenticateParameters.Where(param => param.AuthenticationScheme == Constants.PoPAuthHeaderPrefix).Single().Nonce; Assert.IsNotNull(serverNonce); Assert.AreEqual(parsedHeaders.PopNonce, serverNonce); Assert.IsNull(parsedHeaders.AuthenticationInfoParameters); @@ -138,7 +138,7 @@ public async Task ExtractNonceWithAuthParserAsync() Assert.IsNotNull(parsedHeaders.AuthenticationInfoParameters.NextNonce); Assert.AreEqual(parsedHeaders.PopNonce, parsedHeaders.AuthenticationInfoParameters.NextNonce); - Assert.IsFalse(parsedHeaders.WwwAuthenticateParameters.Any(param => param.AuthScheme == Constants.PoPAuthHeaderPrefix)); + Assert.IsFalse(parsedHeaders.WwwAuthenticateParameters.Any(param => param.AuthenticationScheme == Constants.PoPAuthHeaderPrefix)); await TestCommon.ValidatePopNonceAsync(parsedHeaders.PopNonce).ConfigureAwait(false); } diff --git a/tests/Microsoft.Identity.Test.Unit/WwwAuthenticateParametersTests.cs b/tests/Microsoft.Identity.Test.Unit/WwwAuthenticateParametersTests.cs index d29010a2d8..0c19d8ea56 100644 --- a/tests/Microsoft.Identity.Test.Unit/WwwAuthenticateParametersTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/WwwAuthenticateParametersTests.cs @@ -75,7 +75,7 @@ public void EnsureProperlyFormattedHeadersWithToken86DoNotFail(string scheme, st var authParams = WwwAuthenticateParameters.CreateFromAuthenticationHeaders(httpResponse.Headers, scheme); // Assert - Assert.AreEqual(scheme, authParams.AuthScheme); + Assert.AreEqual(scheme, authParams.AuthenticationScheme); if (scheme == "AuthScheme1") { @@ -108,7 +108,7 @@ public void EnsureProperlyFormattedHeadersWithRealmDoNotFail(string scheme, stri var authParams = WwwAuthenticateParameters.CreateFromAuthenticationHeaders(httpResponse.Headers, scheme); // Assert - Assert.AreEqual(scheme, authParams.AuthScheme); + Assert.AreEqual(scheme, authParams.AuthenticationScheme); Assert.AreEqual("someRealm", authParams.RawParameters["realm"]); if (scheme == "AuthScheme2" || scheme == "AuthScheme4") @@ -140,7 +140,7 @@ public void CreateWwwAuthenticateResponseForUnknownChallenges(string scheme, str // Assert if (scheme == "NTLM") { - Assert.AreEqual("NTLM", authParams.AuthScheme); + Assert.AreEqual("NTLM", authParams.AuthenticationScheme); Assert.AreEqual("dG9rZW42OA==", authParams.RawParameters[scheme]); } else if (scheme == "WLID1.0") @@ -167,7 +167,7 @@ public void CreateFromtoken68AuthInfoResponse() httpResponse.Headers.Add(AuthenticationInfoName, $"token68"); // Act - var parameters = AuthenticationInfoParameters.CreateFromHeaders(httpResponse.Headers); + var parameters = AuthenticationInfoParameters.CreateFromResponseHeaders(httpResponse.Headers); //Assert Assert.IsNotNull(parameters); @@ -185,7 +185,7 @@ public void CreateFromMalformedAuthInfoResponse(string paramName, string value) // Act var ex = Assert.ThrowsException(() => - AuthenticationInfoParameters.CreateFromHeaders(httpResponse.Headers)); + AuthenticationInfoParameters.CreateFromResponseHeaders(httpResponse.Headers)); //Assert Assert.AreEqual(ex.ErrorCode, MsalError.UnableToParseAuthenticationHeader); @@ -438,8 +438,8 @@ public async Task CreateAllFromResourceResponseAsync_HttpClient_Bearer_Pop_Async var httpClient = new HttpClient(handler); var headers = await WwwAuthenticateParameters.CreateFromAuthenticationResponseAsync(resourceUri, httpClient, default).ConfigureAwait(false); - var bearerHeader = headers.Where(header => header.AuthScheme == "Bearer").Single(); - var popHeader = headers.Where(header => header.AuthScheme == "PoP").Single(); + var bearerHeader = headers.Where(header => header.AuthenticationScheme == "Bearer").Single(); + var popHeader = headers.Where(header => header.AuthenticationScheme == "PoP").Single(); Assert.IsNotNull(bearerHeader); Assert.AreEqual("https://login.microsoftonline.com/TenantId", bearerHeader.Authority); @@ -457,8 +457,8 @@ public void ExtractAllWWWAuthenticateParametersFromResponse(bool combineHeaders) // Act var headers = WwwAuthenticateParameters.CreateFromAuthenticationHeaders(httpResponse.Headers); - var bearerHeader = headers.Where(header => header.AuthScheme == "Bearer").Single(); - var popHeader = headers.Where(header => header.AuthScheme == "PoP").Single(); + var bearerHeader = headers.Where(header => header.AuthenticationScheme == "Bearer").Single(); + var popHeader = headers.Where(header => header.AuthenticationScheme == "PoP").Single(); // Assert Assert.IsNotNull(bearerHeader); @@ -475,10 +475,10 @@ public void ExtractDuplicateWWWAuthenticateParametersFromResponse() // Act var headers = WwwAuthenticateParameters.CreateFromAuthenticationHeaders(httpResponse.Headers); - var bearerHeader1 = headers.Where(header => header.AuthScheme == "Bearer").First(); - var popHeader1 = headers.Where(header => header.AuthScheme == "PoP").First(); - var bearerHeader2 = headers.Where(header => header.AuthScheme == "Bearer").Last(); - var popHeader2 = headers.Where(header => header.AuthScheme == "PoP").Last(); + var bearerHeader1 = headers.Where(header => header.AuthenticationScheme == "Bearer").First(); + var popHeader1 = headers.Where(header => header.AuthenticationScheme == "PoP").First(); + var bearerHeader2 = headers.Where(header => header.AuthenticationScheme == "Bearer").Last(); + var popHeader2 = headers.Where(header => header.AuthenticationScheme == "PoP").Last(); // Assert Assert.IsNotNull(bearerHeader1); @@ -501,8 +501,8 @@ public void ExtractAllUppercaseParametersFromResponse() // Act var headers = WwwAuthenticateParameters.CreateFromAuthenticationHeaders(httpResponse.Headers); - var bearerHeader = headers.Where(header => header.AuthScheme == "Bearer").Single(); - var popHeader = headers.Where(header => header.AuthScheme == "PoP").Single(); + var bearerHeader = headers.Where(header => header.AuthenticationScheme == "Bearer").Single(); + var popHeader = headers.Where(header => header.AuthenticationScheme == "PoP").Single(); // Assert Assert.IsNotNull(bearerHeader); @@ -521,8 +521,8 @@ public void ExtractAllParametersFromResponseWithAuthParser(bool combineHeaders) // Act var headers = AuthenticationHeaderParser.ParseAuthenticationHeaders(httpResponse.Headers); - var bearerHeader = headers.WwwAuthenticateParameters.Where(header => header.AuthScheme == "Bearer").Single(); - var popHeader = headers.WwwAuthenticateParameters.Where(header => header.AuthScheme == "PoP").Single(); + var bearerHeader = headers.WwwAuthenticateParameters.Where(header => header.AuthenticationScheme == "Bearer").Single(); + var popHeader = headers.WwwAuthenticateParameters.Where(header => header.AuthenticationScheme == "PoP").Single(); // Assert Assert.IsNotNull(bearerHeader); @@ -538,7 +538,7 @@ public void ExtractAuthenticationInfoParametersFromResponse() HttpResponseMessage httpResponse = CreateAuthInfoHttpResponse(); // Act - var header = AuthenticationInfoParameters.CreateFromHeaders(httpResponse.Headers); + var header = AuthenticationInfoParameters.CreateFromResponseHeaders(httpResponse.Headers); // Assert Assert.IsNotNull(header); @@ -552,7 +552,7 @@ public void ExtractAllAuthenticationInfoParametersFromResponse() HttpResponseMessage httpResponse = CreateAuthInfoHttpResponse(); // Act - var header = AuthenticationInfoParameters.CreateFromHeaders(httpResponse.Headers); + var header = AuthenticationInfoParameters.CreateFromResponseHeaders(httpResponse.Headers); var nextNonce = header.RawParameters["nextnonce"]; var realm = header.RawParameters["realm"];