Skip to content

Commit f08b8ae

Browse files
authored
Refactor ChallengeBasedAuthPolicy to extend BearerTokenChallengeAuthPolicy (Azure#18504)
* Refactor ChallengeBasedAuthenticationPolicy onto BearerTokenChallengeAuthenticationPolicy
1 parent 236aa0b commit f08b8ae

27 files changed

+693
-652
lines changed

sdk/core/Azure.Core/api/Azure.Core.net461.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,7 @@ public static partial class Names
269269
public static string Range { get { throw null; } }
270270
public static string Referer { get { throw null; } }
271271
public static string UserAgent { get { throw null; } }
272+
public static string WWWAuthenticate { get { throw null; } }
272273
public static string XMsDate { get { throw null; } }
273274
public static string XMsRange { get { throw null; } }
274275
public static string XMsRequestId { get { throw null; } }

sdk/core/Azure.Core/api/Azure.Core.net5.0.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,7 @@ public static partial class Names
269269
public static string Range { get { throw null; } }
270270
public static string Referer { get { throw null; } }
271271
public static string UserAgent { get { throw null; } }
272+
public static string WWWAuthenticate { get { throw null; } }
272273
public static string XMsDate { get { throw null; } }
273274
public static string XMsRange { get { throw null; } }
274275
public static string XMsRequestId { get { throw null; } }

sdk/core/Azure.Core/api/Azure.Core.netstandard2.0.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,7 @@ public static partial class Names
269269
public static string Range { get { throw null; } }
270270
public static string Referer { get { throw null; } }
271271
public static string UserAgent { get { throw null; } }
272+
public static string WWWAuthenticate { get { throw null; } }
272273
public static string XMsDate { get { throw null; } }
273274
public static string XMsRange { get { throw null; } }
274275
public static string XMsRequestId { get { throw null; } }

sdk/core/Azure.Core/src/Azure.Core.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
<ItemGroup>
2727
<Compile Remove="Shared\**\*.cs" />
2828
<Compile Include="Shared\Argument.cs" />
29+
<Compile Include="Shared\AuthorizationChallengeParser.cs" />
2930
<Compile Include="Shared\AzureKeyCredentialPolicy.cs" />
3031
<Compile Include="Shared\AzureSasCredentialSynchronousPolicy.cs" />
3132
<Compile Include="Shared\Base64Url.cs" />

sdk/core/Azure.Core/src/HttpHeader.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,9 +141,14 @@ public static class Names
141141
public static string Host => "Host";
142142

143143
/// <summary>
144-
/// Returns <code>"Content-Disposition"</code>
144+
/// Returns <code>"Content-Disposition"</code>.
145145
/// </summary>
146146
public static string ContentDisposition => "Content-Disposition";
147+
148+
/// <summary>
149+
/// Returns <code>"WWW-Authenticate"</code>.
150+
/// </summary>
151+
public static string WWWAuthenticate => "WWW-Authenticate";
147152
}
148153

149154
#pragma warning disable CA1034 // Nested types should not be visible
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
using System;
5+
using System.Collections.Generic;
6+
7+
#nullable enable
8+
9+
namespace Azure.Core.Pipeline
10+
{
11+
/// <summary>
12+
/// A policy that sends an <see cref="AccessToken"/> provided by a <see cref="TokenCredential"/> as an Authentication header.
13+
/// Note: This class is currently in preview and is therefore subject to possible future breaking changes.
14+
/// </summary>
15+
internal class ARMChallengeAuthenticationPolicy : BearerTokenChallengeAuthenticationPolicy
16+
{
17+
/// <summary>
18+
/// Creates a new instance of <see cref="ARMChallengeAuthenticationPolicy"/> using provided token credential and scope to authenticate for.
19+
/// </summary>
20+
/// <param name="credential">The token credential to use for authentication.</param>
21+
/// <param name="scope">The scope to authenticate for.</param>
22+
public ARMChallengeAuthenticationPolicy(TokenCredential credential, string scope) : base(credential, new[] { scope }) { }
23+
24+
/// <summary>
25+
/// Creates a new instance of <see cref="ARMChallengeAuthenticationPolicy"/> using provided token credential and scopes to authenticate for.
26+
/// </summary>
27+
/// <param name="credential">The token credential to use for authentication.</param>
28+
/// <param name="scopes">Scopes to authenticate for.</param>
29+
public ARMChallengeAuthenticationPolicy(TokenCredential credential, IEnumerable<string> scopes)
30+
: base(credential, scopes, TimeSpan.FromMinutes(5), TimeSpan.FromSeconds(30)) { }
31+
32+
/// <summary>
33+
/// Executed in the event a 401 response with a WWW-Authenticate authentication challenge header is received after the initial request.
34+
/// </summary>
35+
/// <remarks>Handles claims authentication challenges.</remarks>
36+
/// <param name="message">The <see cref="HttpMessage"/> to be authenticated.</param>
37+
/// <param name="context">If the return value is <c>true</c>, a <see cref="TokenRequestContext"/>.</param>
38+
/// <returns>A boolean indicated whether the request contained a valid challenge and a <see cref="TokenRequestContext"/> was successfully initialized with it.</returns>
39+
protected override bool TryGetTokenRequestContextFromChallenge(HttpMessage message, out TokenRequestContext context)
40+
{
41+
context = default;
42+
43+
var challenge = AuthorizationChallengeParser.GetChallengeParameterFromResponse(message.Response, "Bearer", "claims");
44+
if (challenge == null)
45+
{
46+
return false;
47+
}
48+
49+
string claimsChallenge = Base64Url.DecodeString(challenge.ToString());
50+
context = new TokenRequestContext(Scopes, message.Request.ClientRequestId, claimsChallenge);
51+
return true;
52+
}
53+
}
54+
}
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
using System;
5+
using System.Net;
6+
7+
#nullable enable
8+
9+
namespace Azure.Core
10+
{
11+
/// <summary>
12+
/// A helper class for parsing Authorization challenge headers.
13+
/// </summary>
14+
internal static class AuthorizationChallengeParser
15+
{
16+
/// <summary>
17+
/// Parses the specified parameter from a challenge hearder found in the specified <see cref="Response"/>.
18+
/// </summary>
19+
/// <param name="response">The <see cref="Response"/> to parse.</param>
20+
/// <param name="challengeScheme">The challenge scheme containing the <paramref name="challengeParameter"/>. For example: "Bearer"</param>
21+
/// <param name="challengeParameter">The parameter key name containing the value to return.</param>
22+
/// <returns>The value of the parameter name specified in <paramref name="challengeParameter"/> if it is found in the specified <paramref name="challengeScheme"/>.</returns>
23+
public static string? GetChallengeParameterFromResponse(Response response, string challengeScheme, string challengeParameter)
24+
{
25+
if (response.Status != (int)HttpStatusCode.Unauthorized || !response.Headers.TryGetValue(HttpHeader.Names.WWWAuthenticate, out string? headerValue))
26+
{
27+
return null;
28+
}
29+
30+
ReadOnlySpan<char> bearer = challengeScheme.AsSpan();
31+
ReadOnlySpan<char> claims = challengeParameter.AsSpan();
32+
ReadOnlySpan<char> headerSpan = headerValue.AsSpan();
33+
34+
// Iterate through each challenge value.
35+
while (TryGetNextChallenge(ref headerSpan, out var challengeKey))
36+
{
37+
// Enumerate each key=value parameter until we find the 'claims' key on the 'Bearer' challenge.
38+
while (TryGetNextParameter(ref headerSpan, out var key, out var value))
39+
{
40+
if (challengeKey.Equals(bearer, StringComparison.OrdinalIgnoreCase) && key.Equals(claims, StringComparison.OrdinalIgnoreCase))
41+
{
42+
return value.ToString();
43+
}
44+
}
45+
}
46+
47+
return null;
48+
}
49+
50+
/// <summary>
51+
/// Iterates through the challenge schemes present in a challenge header.
52+
/// </summary>
53+
/// <param name="headerValue">
54+
/// The header value which will be sliced to remove the first parsed <paramref name="challengeKey"/>.
55+
/// </param>
56+
/// <param name="challengeKey">The parsed challenge scheme.</param>
57+
/// <returns>
58+
/// <c>true</c> if a challenge scheme was successfully parsed.
59+
/// The value of <paramref name="headerValue"/> should be passed to <see cref="TryGetNextParameter"/> to parse the challenge parameters if <c>true</c>.
60+
/// </returns>
61+
internal static bool TryGetNextChallenge(ref ReadOnlySpan<char> headerValue, out ReadOnlySpan<char> challengeKey)
62+
{
63+
challengeKey = default;
64+
65+
headerValue = headerValue.TrimStart(' ');
66+
int endOfChallengeKey = headerValue.IndexOf(' ');
67+
68+
if (endOfChallengeKey < 0)
69+
{
70+
return false;
71+
}
72+
73+
challengeKey = headerValue.Slice(0, endOfChallengeKey);
74+
75+
// Slice the challenge key from the headerValue
76+
headerValue = headerValue.Slice(endOfChallengeKey + 1);
77+
78+
return true;
79+
}
80+
81+
/// <summary>
82+
/// Iterates through a challenge header value after being parsed by <see cref="TryGetNextChallenge"/>.
83+
/// </summary>
84+
/// <param name="headerValue">The header value after being parsed by <see cref="TryGetNextChallenge"/>.</param>
85+
/// <param name="paramKey">The parsed challenge parameter key.</param>
86+
/// <param name="paramValue">The parsed challenge parameter value.</param>
87+
/// <param name="separator">The challenge parameter key / value pair separator. The default is '='.</param>
88+
/// <returns>
89+
/// <c>true</c> if the next available challenge parameter was successfully parsed.
90+
/// <c>false</c> if there are no more parameters for the current challenge scheme or an additional challenge scheme was encountered in the <paramref name="headerValue"/>.
91+
/// The value of <paramref name="headerValue"/> should be passed again to <see cref="TryGetNextChallenge"/> to attempt to parse any additional challenge schemes if <c>false</c>.
92+
/// </returns>
93+
internal static bool TryGetNextParameter(ref ReadOnlySpan<char> headerValue, out ReadOnlySpan<char> paramKey, out ReadOnlySpan<char> paramValue, char separator = '=')
94+
{
95+
paramKey = default;
96+
paramValue = default;
97+
var spaceOrComma = " ,".AsSpan();
98+
99+
// Trim any separater prefixes.
100+
headerValue = headerValue.TrimStart(spaceOrComma);
101+
102+
int nextSpace = headerValue.IndexOf(' ');
103+
int nextSeparator = headerValue.IndexOf(separator);
104+
105+
if (nextSpace < nextSeparator && nextSpace != -1)
106+
{
107+
// we encountered another challenge value.
108+
return false;
109+
}
110+
111+
if (nextSeparator < 0)
112+
return false;
113+
114+
// Get the paramKey.
115+
paramKey = headerValue.Slice(0, nextSeparator).Trim();
116+
117+
// Slice to remove the 'paramKey=' from the parameters.
118+
headerValue = headerValue.Slice(nextSeparator + 1);
119+
120+
// The start of paramValue will usually be a quoted string. Find the first quote.
121+
int quoteIndex = headerValue.IndexOf('\"');
122+
123+
// Get the paramValue, which is delimited by the trailing quote.
124+
headerValue = headerValue.Slice(quoteIndex + 1);
125+
if (quoteIndex >= 0)
126+
{
127+
// The values are quote wrapped
128+
paramValue = headerValue.Slice(0, headerValue.IndexOf('\"'));
129+
}
130+
else
131+
{
132+
//the values are not quote wrapped (storage is one example of this)
133+
// either find the next space indicating the delimiter to the next value, or go to the end since this is the last value.
134+
int trailingDelimiterIndex = headerValue.IndexOfAny(spaceOrComma);
135+
if (trailingDelimiterIndex >= 0)
136+
{
137+
paramValue = headerValue.Slice(0, trailingDelimiterIndex);
138+
}
139+
else
140+
{
141+
paramValue = headerValue;
142+
}
143+
}
144+
145+
// Slice to remove the '"paramValue"' from the parameters.
146+
if (headerValue != paramValue)
147+
headerValue = headerValue.Slice(paramValue.Length + 1);
148+
149+
return true;
150+
}
151+
}
152+
}

0 commit comments

Comments
 (0)