Skip to content

Commit 4c23733

Browse files
authored
Merge pull request #76 from akunzai/avoid-override-authorization-header
Avoid to override the Authorization header
2 parents cff2329 + 1102e92 commit 4c23733

File tree

7 files changed

+124
-86
lines changed

7 files changed

+124
-86
lines changed

CHANGELOG.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,13 @@
22

33
All notable changes to this project will be documented in this file.
44

5+
## 2.4.1 (2022-04-03)
6+
7+
- [Avoid to override the Authorization header](https://tools.ietf.org/html/rfc6749#section-5.2)
8+
59
## 2.4.0 (2022-04-02)
610

7-
- [Prefer to send the client credentials in Authorization header](https://datatracker.ietf.org/doc/html/rfc6749#section-2.3.1)
11+
- [Prefer to send the client credentials in Authorization header](https://tools.ietf.org/html/rfc6749#section-2.3.1)
812

913
## 2.3.1 (2021-11-14)
1014

src/Directory.Build.props

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
<EmbedUntrackedSources>true</EmbedUntrackedSources>
1111
<EnableNETAnalyzers>true</EnableNETAnalyzers>
1212
<Nullable>enable</Nullable>
13-
<Version>2.4.0</Version>
13+
<Version>2.4.1</Version>
1414
</PropertyGroup>
1515

1616
<PropertyGroup Condition="'$(Configuration)' == 'Release' ">

src/GSS.Authorization.OAuth.HttpClient/OAuthHttpHandler.cs

Lines changed: 9 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -30,17 +30,15 @@ protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage
3030
throw new ArgumentNullException(nameof(request));
3131
var tokenCredentials = await _options.TokenCredentialProvider(request).ConfigureAwait(false);
3232
var queryString = QueryHelpers.ParseQuery(request.RequestUri?.Query);
33-
if (_options.SignedAsBody && request.Content != null && string.Equals(request.Content.Headers?.ContentType?.MediaType,
34-
ApplicationFormUrlEncoded, StringComparison.OrdinalIgnoreCase))
33+
if (_options.SignedAsBody && request.Content != null && string.Equals(
34+
request.Content.Headers?.ContentType?.MediaType,
35+
ApplicationFormUrlEncoded, StringComparison.OrdinalIgnoreCase))
3536
{
3637
var urlEncoded = await request.Content.ReadAsStringAsync().ConfigureAwait(false);
3738
var formData = QueryHelpers.ParseQuery(urlEncoded);
38-
foreach (var query in queryString)
39+
foreach (var query in queryString.Where(query => !formData.ContainsKey(query.Key)))
3940
{
40-
if (!formData.ContainsKey(query.Key))
41-
{
42-
formData.Add(query.Key, query.Value);
43-
}
41+
formData.Add(query.Key, query.Value);
4442
}
4543

4644
var parameters = _signer.AppendAuthorizationParameters(request.Method, request.RequestUri!, _options,
@@ -62,18 +60,12 @@ protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage
6260
{
6361
var parameters = _signer.AppendAuthorizationParameters(request.Method, request.RequestUri!, _options,
6462
queryString, tokenCredentials);
65-
var values = new List<string>();
66-
foreach (var parameter in parameters)
67-
{
68-
foreach (var value in parameter.Value)
69-
{
70-
values.Add($"{Uri.EscapeDataString(parameter.Key)}={Uri.EscapeDataString(value)}");
71-
}
72-
}
73-
63+
var values = (from parameter in parameters
64+
from value in parameter.Value
65+
select $"{Uri.EscapeDataString(parameter.Key)}={Uri.EscapeDataString(value)}").ToList();
7466
request.RequestUri = new UriBuilder(request.RequestUri!) { Query = "?" + string.Join("&", values) }.Uri;
7567
}
76-
else
68+
else if (request.Headers.Authorization == null)
7769
{
7870
request.Headers.Authorization = _signer.GetAuthorizationHeader(
7971
request.Method,

src/GSS.Authorization.OAuth2.HttpClient/OAuth2HttpHandler.cs

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -28,16 +28,19 @@ protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage
2828
{
2929
if (request == null)
3030
throw new ArgumentNullException(nameof(request));
31-
if (request.Headers.Authorization == null)
32-
{
33-
TrySetAuthorizationHeaderToRequest(await GetAccessTokenAsync(cancellationToken).ConfigureAwait(false), request);
34-
}
31+
if (request.Headers.Authorization != null)
32+
return await base.SendAsync(request, cancellationToken).ConfigureAwait(false);
33+
34+
TrySetAuthorizationHeaderToRequest(await GetAccessTokenAsync(cancellationToken).ConfigureAwait(false),
35+
request);
3536
var response = await base.SendAsync(request, cancellationToken).ConfigureAwait(false);
36-
// https://tools.ietf.org/html/rfc6750#section-3
37-
var authenticateError = response.Headers.WwwAuthenticate.FirstOrDefault()?.Parameter;
38-
if (string.IsNullOrWhiteSpace(authenticateError) && response.StatusCode != HttpStatusCode.Unauthorized)
37+
// https://tools.ietf.org/html/rfc6749#section-5.2
38+
var challenges = response.Headers.WwwAuthenticate;
39+
if (response.StatusCode != HttpStatusCode.Unauthorized ||
40+
challenges.Any() && !challenges.Any(c => c.Scheme.Equals(AuthorizerDefaults.Bearer)))
3941
return response;
40-
TrySetAuthorizationHeaderToRequest(await GetAccessTokenAsync(cancellationToken, forceRenew: true).ConfigureAwait(false), request);
42+
TrySetAuthorizationHeaderToRequest(
43+
await GetAccessTokenAsync(cancellationToken, forceRenew: true).ConfigureAwait(false), request);
4144
return await base.SendAsync(request, cancellationToken).ConfigureAwait(false);
4245
}
4346

@@ -61,6 +64,7 @@ private async ValueTask<AccessToken> GetAccessTokenAsync(CancellationToken cance
6164
{
6265
_memoryCache.Set(_cacheKey, accessToken);
6366
}
67+
6468
return accessToken;
6569
}
6670
finally
@@ -71,11 +75,9 @@ private async ValueTask<AccessToken> GetAccessTokenAsync(CancellationToken cance
7175

7276
private static void TrySetAuthorizationHeaderToRequest(AccessToken accessToken, HttpRequestMessage request)
7377
{
74-
if (!string.IsNullOrWhiteSpace(accessToken.Token))
75-
{
76-
request.Headers.Authorization =
77-
new AuthenticationHeaderValue(AuthorizerDefaults.Bearer, accessToken.Token);
78-
}
78+
if (string.IsNullOrWhiteSpace(accessToken.Token)) return;
79+
request.Headers.Authorization =
80+
new AuthenticationHeaderValue(AuthorizerDefaults.Bearer, accessToken.Token);
7981
}
8082
}
8183
}

src/GSS.Authorization.OAuth2/AuthorizerOptions.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ public class AuthorizerOptions
1919
/*
2020
* send the client credentials in the request-body? (default: Authorization header)
2121
*
22-
* see https://datatracker.ietf.org/doc/html/rfc6749#section-2.3.1
22+
* see https://tools.ietf.org/html/rfc6749#section-2.3.1
2323
*/
2424
public bool SendClientCredentialsInRequestBody { get; set; }
2525

test/GSS.Authorization.OAuth.HttpClient.Tests/OAuthHttpClientTests.cs

Lines changed: 62 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
using System.Globalization;
22
using System.Net;
3+
using System.Net.Http.Headers;
34
using System.Security.Cryptography;
5+
using System.Text;
46
using Microsoft.AspNetCore.WebUtilities;
57
using Microsoft.Extensions.Configuration;
68
using Microsoft.Extensions.DependencyInjection;
@@ -18,19 +20,21 @@ public class OAuthHttpClientTests : IClassFixture<OAuthFixture>
1820
private readonly MockHttpMessageHandler? _mockHttp;
1921
private readonly IRequestSigner _signer = new HmacSha1RequestSigner();
2022
private readonly IConfiguration _configuration;
23+
private readonly OAuthCredential _clientCredentials;
2124
private readonly OAuthCredential _tokenCredentials;
2225

2326
public OAuthHttpClientTests(OAuthFixture fixture)
2427
{
2528
_configuration = fixture.Configuration;
29+
_clientCredentials = new OAuthCredential(
30+
_configuration["OAuth:ClientId"],
31+
_configuration["OAuth:ClientSecret"]);
2632
_tokenCredentials = new OAuthCredential(
2733
_configuration["OAuth:TokenId"],
2834
_configuration["OAuth:TokenSecret"]);
29-
if (_configuration.GetValue("HttpClient:Mock", true))
30-
{
31-
_mockHttp = new MockHttpMessageHandler();
32-
_mockHttp.Fallback.Respond(HttpStatusCode.Unauthorized);
33-
}
35+
if (!_configuration.GetValue("HttpClient:Mock", true)) return;
36+
_mockHttp = new MockHttpMessageHandler();
37+
_mockHttp.Fallback.Respond(HttpStatusCode.Unauthorized);
3438
}
3539

3640
[Fact]
@@ -40,18 +44,14 @@ public async Task HttpClient_AccessProtectedResourceWithAuthorizationHeader_Shou
4044
var services = new ServiceCollection()
4145
.AddOAuthHttpClient<OAuthHttpClient>((_, handlerOptions) =>
4246
{
43-
handlerOptions.ClientCredentials = new OAuthCredential(
44-
_configuration["OAuth:ClientId"],
45-
_configuration["OAuth:ClientSecret"]);
47+
handlerOptions.ClientCredentials = _clientCredentials;
4648
handlerOptions.TokenCredentials = _tokenCredentials;
47-
if (_mockHttp != null)
48-
{
49-
var timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds()
50-
.ToString(CultureInfo.InvariantCulture);
51-
var nonce = generateNonce();
52-
handlerOptions.TimestampProvider = () => timestamp;
53-
handlerOptions.NonceProvider = () => nonce;
54-
}
49+
if (_mockHttp == null) return;
50+
var timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds()
51+
.ToString(CultureInfo.InvariantCulture);
52+
var nonce = GenerateNonce();
53+
handlerOptions.TimestampProvider = () => timestamp;
54+
handlerOptions.NonceProvider = () => nonce;
5555
})
5656
.ConfigurePrimaryHttpMessageHandler(_ => _mockHttp ?? new HttpClientHandler() as HttpMessageHandler)
5757
.Services.BuildServiceProvider();
@@ -72,6 +72,36 @@ public async Task HttpClient_AccessProtectedResourceWithAuthorizationHeader_Shou
7272
_mockHttp?.VerifyNoOutstandingExpectation();
7373
_mockHttp?.VerifyNoOutstandingRequest();
7474
}
75+
76+
[Fact]
77+
public async Task HttpClient_AccessProtectedResourceWithPredefinedAuthorizationHeader_ShouldPassThrough()
78+
{
79+
// Arrange
80+
var services = new ServiceCollection()
81+
.AddOAuthHttpClient<OAuthHttpClient>((_, handlerOptions) =>
82+
{
83+
handlerOptions.ClientCredentials = _clientCredentials;
84+
handlerOptions.TokenCredentials = _tokenCredentials;
85+
})
86+
.ConfigurePrimaryHttpMessageHandler(_ => _mockHttp ?? new HttpClientHandler() as HttpMessageHandler)
87+
.Services.BuildServiceProvider();
88+
var client = services.GetRequiredService<OAuthHttpClient>();
89+
var resourceUri = _configuration.GetValue<Uri>("Request:Uri");
90+
var basicAuth = Convert.ToBase64String(Encoding.ASCII.GetBytes($"{_clientCredentials.Key}:{_clientCredentials.Secret}"));
91+
_mockHttp?.Expect(HttpMethod.Get, resourceUri.AbsoluteUri)
92+
.WithHeaders("Authorization", $"Basic {basicAuth}")
93+
.Respond(HttpStatusCode.Forbidden);
94+
95+
// Act
96+
using var request = new HttpRequestMessage(HttpMethod.Get, resourceUri);
97+
request.Headers.Authorization = new AuthenticationHeaderValue("Basic", basicAuth);
98+
var response = await client.HttpClient.SendAsync(request).ConfigureAwait(false);
99+
100+
// Assert
101+
Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode);
102+
_mockHttp?.VerifyNoOutstandingExpectation();
103+
_mockHttp?.VerifyNoOutstandingRequest();
104+
}
75105

76106
[Fact]
77107
public async Task HttpClient_AccessProtectedResourceWithQueryString_ShouldAuthorized()
@@ -80,19 +110,15 @@ public async Task HttpClient_AccessProtectedResourceWithQueryString_ShouldAuthor
80110
var services = new ServiceCollection()
81111
.AddOAuthHttpClient<OAuthHttpClient>((_, handlerOptions) =>
82112
{
83-
handlerOptions.ClientCredentials = new OAuthCredential(
84-
_configuration["OAuth:ClientId"],
85-
_configuration["OAuth:ClientSecret"]);
113+
handlerOptions.ClientCredentials = _clientCredentials;
86114
handlerOptions.TokenCredentials = _tokenCredentials;
87115
handlerOptions.SignedAsQuery = true;
88-
if (_mockHttp != null)
89-
{
90-
var timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds()
91-
.ToString(CultureInfo.InvariantCulture);
92-
var nonce = generateNonce();
93-
handlerOptions.TimestampProvider = () => timestamp;
94-
handlerOptions.NonceProvider = () => nonce;
95-
}
116+
if (_mockHttp == null) return;
117+
var timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds()
118+
.ToString(CultureInfo.InvariantCulture);
119+
var nonce = GenerateNonce();
120+
handlerOptions.TimestampProvider = () => timestamp;
121+
handlerOptions.NonceProvider = () => nonce;
96122
})
97123
.ConfigurePrimaryHttpMessageHandler(_ => _mockHttp ?? new HttpClientHandler() as HttpMessageHandler)
98124
.Services.BuildServiceProvider();
@@ -104,14 +130,7 @@ public async Task HttpClient_AccessProtectedResourceWithQueryString_ShouldAuthor
104130
: "?foo=v1&foo=v2";
105131
var parameters = _signer.AppendAuthorizationParameters(HttpMethod.Get, resourceUri.Uri,
106132
options.Value, QueryHelpers.ParseQuery(resourceUri.Uri.Query), _tokenCredentials);
107-
var values = new List<string>();
108-
foreach (var parameter in parameters)
109-
{
110-
foreach (var value in parameter.Value)
111-
{
112-
values.Add($"{Uri.EscapeDataString(parameter.Key)}={Uri.EscapeDataString(value)}");
113-
}
114-
}
133+
var values = (from parameter in parameters from value in parameter.Value select $"{Uri.EscapeDataString(parameter.Key)}={Uri.EscapeDataString(value)}").ToList();
115134

116135
_mockHttp?.Expect(HttpMethod.Get, resourceUri.Uri.AbsoluteUri)
117136
.WithQueryString("?" + string.Join("&", values))
@@ -133,19 +152,15 @@ public async Task HttpClient_AccessProtectedResourceWithFormBody_ShouldAuthorize
133152
var services = new ServiceCollection()
134153
.AddOAuthHttpClient<OAuthHttpClient>((_, handlerOptions) =>
135154
{
136-
handlerOptions.ClientCredentials = new OAuthCredential(
137-
_configuration["OAuth:ClientId"],
138-
_configuration["OAuth:ClientSecret"]);
155+
handlerOptions.ClientCredentials = _clientCredentials;
139156
handlerOptions.TokenCredentials = _tokenCredentials;
140157
handlerOptions.SignedAsBody = true;
141-
if (_mockHttp != null)
142-
{
143-
var timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds()
144-
.ToString(CultureInfo.InvariantCulture);
145-
var nonce = generateNonce();
146-
handlerOptions.TimestampProvider = () => timestamp;
147-
handlerOptions.NonceProvider = () => nonce;
148-
}
158+
if (_mockHttp == null) return;
159+
var timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds()
160+
.ToString(CultureInfo.InvariantCulture);
161+
var nonce = GenerateNonce();
162+
handlerOptions.TimestampProvider = () => timestamp;
163+
handlerOptions.NonceProvider = () => nonce;
149164
})
150165
.ConfigurePrimaryHttpMessageHandler(_ => _mockHttp ?? new HttpClientHandler() as HttpMessageHandler)
151166
.Services.BuildServiceProvider();
@@ -193,7 +208,7 @@ public async Task HttpClient_AccessProtectedResourceWithFormBody_ShouldAuthorize
193208
_mockHttp?.VerifyNoOutstandingRequest();
194209
}
195210

196-
private static string generateNonce()
211+
private static string GenerateNonce()
197212
{
198213
var bytes = new byte[16];
199214
_randomNumberGenerator.GetNonZeroBytes(bytes);

0 commit comments

Comments
 (0)