Skip to content

Commit

Permalink
Merge pull request #89 from akunzai/send-access-token-in-request
Browse files Browse the repository at this point in the history
Supports sending access token in form-encoded body or query parameter
  • Loading branch information
akunzai authored Aug 29, 2022
2 parents 75ce1cc + da03272 commit 3760fcf
Show file tree
Hide file tree
Showing 23 changed files with 191 additions and 134 deletions.
10 changes: 7 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,21 @@

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

## 2.6.0 (2022-08-29)

- [Supports sending access token in form-encoded body or query parameter](https://www.rfc-editor.org/rfc/rfc6750#section-2.2)

## 2.5.0 (2022-04-20)

- [Use the ASP.NET Core shared framework](https://docs.microsoft.com/aspnet/core/fundamentals/target-aspnetcore#use-the-aspnet-core-shared-framework)

## 2.4.1 (2022-04-03)

- [Avoid to override the Authorization header](https://tools.ietf.org/html/rfc6749#section-5.2)
- [Avoid to override the Authorization header](https://www.rfc-editor.org/rfc/rfc6749#section-5.2)

## 2.4.0 (2022-04-02)

- [Prefer to send the client credentials in Authorization header](https://tools.ietf.org/html/rfc6749#section-2.3.1)
- [Prefer to send the client credentials in Authorization header](https://www.rfc-editor.org/rfc/rfc6749#section-2.3.1)

## 2.3.1 (2021-11-14)

Expand Down Expand Up @@ -55,7 +59,7 @@ All notable changes to this project will be documented in this file.

### GSS.Authorization.OAuth2.HttpClient 2.1.0

- Handle WWW-Authenticate response error (https://tools.ietf.org/html/rfc6750#section-3)
- Handle WWW-Authenticate response error (https://www.rfc-editor.org/rfc/rfc6750#section-3)

## 2020-07-27

Expand Down
13 changes: 10 additions & 3 deletions samples/OAuth2HttpClientSample/Program.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System.Net;
using System.Net.Http.Headers;
using System.Net.Mime;
using System.Reflection;
using GSS.Authorization.OAuth2;
using Microsoft.Extensions.Configuration;
Expand All @@ -17,26 +18,32 @@ static void ConfigureAuthorizerOptions(IServiceProvider resolver, AuthorizerOpti
options.Credentials = new NetworkCredential(
configuration["OAuth2:Credentials:UserName"],
configuration["OAuth2:Credentials:Password"]);
options.Scopes = configuration.GetSection("OAuth2:Scopes").Get<IEnumerable<string>>();
options.Scopes = configuration["OAuth2:Scope"]?.Split(" ");
options.OnError = (code, message) => Console.Error.Write($"ERROR: [${code}]: {message}");
}

static void ConfigureHttpClient(HttpClient client)
{
var assembly = Assembly.GetEntryAssembly();
var productName = assembly?.GetCustomAttribute<AssemblyProductAttribute>()?.Product;
var productVersion = assembly?.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion ?? assembly?.GetName().Version?.ToString();
var productVersion = assembly?.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion ??
assembly?.GetName().Version?.ToString();
if (!string.IsNullOrEmpty(productName) && !string.IsNullOrEmpty(productVersion))
{
client.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue(productName, productVersion));
}

client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue(MediaTypeNames.Application.Json));
}

var host = Host.CreateDefaultBuilder(args)
.ConfigureServices((hostContext, services) =>
{
services.AddOptions<OAuth2HttpHandlerOptions>().Configure(options =>
{
options.SendAccessTokenInBody = hostContext.Configuration.GetValue("OAuth2:SendAccessTokenInBody", false);
options.SendAccessTokenInQuery = hostContext.Configuration.GetValue("OAuth2:SendAccessTokenInQuery", false);
});
var clientBuilder =
hostContext.Configuration.GetValue("OAuth2:GrantFlow", "ClientCredentials")
.Equals("ClientCredentials", StringComparison.OrdinalIgnoreCase)
Expand Down
2 changes: 1 addition & 1 deletion src/Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
<EmbedUntrackedSources>true</EmbedUntrackedSources>
<EnableNETAnalyzers>true</EnableNETAnalyzers>
<Nullable>enable</Nullable>
<Version>2.5.0</Version>
<Version>2.6.0</Version>
</PropertyGroup>

<PropertyGroup Condition="'$(Configuration)' == 'Release' ">
Expand Down
2 changes: 1 addition & 1 deletion src/GSS.Authorization.OAuth.HttpClient/OAuthHttpHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage
}
}

// The form-encoded httpContent, see https://tools.ietf.org/html/rfc5849#section-3.5.2
// The form-encoded httpContent, see https://www.rfc-editor.org/rfc/rfc5849#section-3.5.2
request.Content = new FormUrlEncodedContent(values);
}
else if (_options.SignedAsQuery)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,13 @@ public Func<HttpRequestMessage, ValueTask<OAuthCredential>> TokenCredentialProvi

/// <summary>
/// sign request as query parameter ? (default: Authorization header)
/// , see https://tools.ietf.org/html/rfc5849#section-3.5.3
/// , see https://www.rfc-editor.org/rfc/rfc5849#section-3.5.3
/// </summary>
public bool SignedAsQuery { get; set; }

/// <summary>
/// sign request as form-encoded body ? (default: Authorization header)
/// , see https://tools.ietf.org/html/rfc5849#section-3.5.2
/// , see https://www.rfc-editor.org/rfc/rfc5849#section-3.5.2
/// </summary>
public bool SignedAsBody { get; set; }
}
Expand Down
2 changes: 1 addition & 1 deletion src/GSS.Authorization.OAuth/AuthorizerBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ public async Task<OAuthCredential> GrantAccessAsync(CancellationToken cancellati
var verificationCode =
await GetVerificationCodeAsync(authorizationUri, cancellationToken).ConfigureAwait(false);

// Step 3: Token Credentials, see https://tools.ietf.org/html/rfc5849#section-2.3
// Step 3: Token Credentials, see https://www.rfc-editor.org/rfc/rfc5849#section-2.3
return await GetTokenCredentialAsync(temporaryCredentials, verificationCode, cancellationToken)
.ConfigureAwait(false);
}
Expand Down
6 changes: 3 additions & 3 deletions src/GSS.Authorization.OAuth/OAuthOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,17 +24,17 @@ public class OAuthOptions
public Func<string> TimestampProvider { get; set; } = () => DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString(CultureInfo.InvariantCulture);

/// <summary>
/// Provides the version of the authentication process as defined in this specification. see https://tools.ietf.org/html/rfc5849#section-3.1
/// Provides the version of the authentication process as defined in this specification. see https://www.rfc-editor.org/rfc/rfc5849#section-3.1
/// </summary>
public bool ProvideVersion { get; set; }

/// <summary>
/// The realm parameter defines a protection realm per (https://tools.ietf.org/html/rfc2617). see https://tools.ietf.org/html/rfc5849#section-3.5.1
/// The realm parameter defines a protection realm per (https://www.rfc-editor.org/rfc/rfc2617). see https://www.rfc-editor.org/rfc/rfc5849#section-3.5.1
/// </summary>
public string? Realm { get; set; }

/// <summary>
/// The Percent-Encoder, see https://tools.ietf.org/html/rfc3986#section-2.1
/// The Percent-Encoder, see https://www.rfc-editor.org/rfc/rfc3986#section-2.1
/// by default, the <see cref="Uri.EscapeDataString(string)"/> is RFC3986 compliant.
/// </summary>
public Func<string, string> PercentEncoder { get; set; } = Uri.EscapeDataString;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
namespace GSS.Authorization.OAuth
{
/// <summary>
/// HMAC-SHA1 signature algorithm, see https://tools.ietf.org/html/rfc5849#section-3.4.2
/// HMAC-SHA1 signature algorithm, see https://www.rfc-editor.org/rfc/rfc5849#section-3.4.2
/// </summary>
public class HmacSha1RequestSigner : RequestSignerBase
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
namespace GSS.Authorization.OAuth
{
/// <summary>
/// PLAINTEXT signature algorithm, see https://tools.ietf.org/html/rfc5849#section-3.4.4
/// PLAINTEXT signature algorithm, see https://www.rfc-editor.org/rfc/rfc5849#section-3.4.4
/// </summary>
public class PlainTextRequestSigner : RequestSignerBase
{
Expand Down
4 changes: 2 additions & 2 deletions src/GSS.Authorization.OAuth/Signers/RequestSignerBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ protected internal string GetBaseString(HttpMethod method, Uri uri, IEnumerable<
throw new ArgumentNullException(nameof(parameters));
}
var baseUri = GetBaseStringUri(uri);
// Parameters Normalization, see https://tools.ietf.org/html/rfc5849#section-3.4.1.3.2
// Parameters Normalization, see https://www.rfc-editor.org/rfc/rfc5849#section-3.4.1.3.2
var normalizationParameters = new List<KeyValuePair<string, string>>();
foreach (var parameter in parameters
// the `oauth_signature`,`realm` parameter MUST be excluded
Expand All @@ -70,7 +70,7 @@ protected internal string GetBaseString(HttpMethod method, Uri uri, IEnumerable<
}

/// <summary>
/// Base String URI, see https://tools.ietf.org/html/rfc5849#section-3.4.1.2
/// Base String URI, see https://www.rfc-editor.org/rfc/rfc5849#section-3.4.1.2
/// </summary>
/// <param name="uri"></param>
/// <returns></returns>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,11 @@
</ItemGroup>

<ItemGroup Condition=" '$(TargetFramework)' == 'netstandard2.0' ">
<PackageReference Include="System.Threading.Tasks.Extensions" Version="4.5.4" />
<PackageReference Include="Microsoft.AspNetCore.WebUtilities" Version="2.2.0" />
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="2.2.0" />
<PackageReference Include="Microsoft.Extensions.Http" Version="2.2.0" />
<PackageReference Include="System.Text.Encodings.Web" Version="4.7.2" />
<PackageReference Include="System.Threading.Tasks.Extensions" Version="4.5.4" />
</ItemGroup>

</Project>
51 changes: 42 additions & 9 deletions src/GSS.Authorization.OAuth2.HttpClient/OAuth2HttpHandler.cs
Original file line number Diff line number Diff line change
@@ -1,23 +1,32 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.WebUtilities;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Options;

namespace GSS.Authorization.OAuth2
{
public class OAuth2HttpHandler : DelegatingHandler
{
private const string ApplicationFormUrlEncoded = "application/x-www-form-urlencoded";
private readonly OAuth2HttpHandlerOptions _options;
private readonly SemaphoreSlim _semaphore = new(1, 1);
private readonly IAuthorizer _authorizer;
private readonly IMemoryCache _memoryCache;
private readonly string _cacheKey;

public OAuth2HttpHandler(IAuthorizer authorizer, IMemoryCache memoryCache)
public OAuth2HttpHandler(
IOptions<OAuth2HttpHandlerOptions> options,
IAuthorizer authorizer,
IMemoryCache memoryCache)
{
_options = options.Value;
_authorizer = authorizer;
_memoryCache = memoryCache;
_cacheKey = Guid.NewGuid().ToString();
Expand All @@ -31,16 +40,16 @@ protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage
if (request.Headers.Authorization != null)
return await base.SendAsync(request, cancellationToken).ConfigureAwait(false);

TrySetAuthorizationHeaderToRequest(await GetAccessTokenAsync(cancellationToken).ConfigureAwait(false),
request);
var accessToken = await GetAccessTokenAsync(cancellationToken).ConfigureAwait(false);
await SendAccessTokenInRequestAsync(accessToken, request).ConfigureAwait(false);
var response = await base.SendAsync(request, cancellationToken).ConfigureAwait(false);
// https://tools.ietf.org/html/rfc6749#section-5.2
// https://www.rfc-editor.org/rfc/rfc6750#section-3
var challenges = response.Headers.WwwAuthenticate;
if (response.StatusCode != HttpStatusCode.Unauthorized ||
challenges.Any() && !challenges.Any(c => c.Scheme.Equals(AuthorizerDefaults.Bearer)))
return response;
TrySetAuthorizationHeaderToRequest(
await GetAccessTokenAsync(cancellationToken, forceRenew: true).ConfigureAwait(false), request);
accessToken = await GetAccessTokenAsync(cancellationToken, forceRenew: true).ConfigureAwait(false);
await SendAccessTokenInRequestAsync(accessToken, request).ConfigureAwait(false);
return await base.SendAsync(request, cancellationToken).ConfigureAwait(false);
}

Expand Down Expand Up @@ -73,11 +82,35 @@ private async ValueTask<AccessToken> GetAccessTokenAsync(CancellationToken cance
}
}

private static void TrySetAuthorizationHeaderToRequest(AccessToken accessToken, HttpRequestMessage request)
private async Task SendAccessTokenInRequestAsync(AccessToken accessToken, HttpRequestMessage request)
{
if (string.IsNullOrWhiteSpace(accessToken.Token)) return;
request.Headers.Authorization =
new AuthenticationHeaderValue(AuthorizerDefaults.Bearer, accessToken.Token);
if (_options.SendAccessTokenInBody && request.Content != null && string.Equals(
request.Content.Headers?.ContentType?.MediaType,
ApplicationFormUrlEncoded, StringComparison.OrdinalIgnoreCase))
{
var parameters =
QueryHelpers.ParseQuery(await request.Content.ReadAsStringAsync().ConfigureAwait(false));
parameters[AuthorizerDefaults.AccessToken] = accessToken.Token;
var values = new List<KeyValuePair<string?, string?>>();
foreach (var parameter in parameters)
{
values.AddRange(parameter.Value.Select(value =>
new KeyValuePair<string?, string?>(parameter.Key, value)));
}

request.Content = new FormUrlEncodedContent(values);
}
else if (_options.SendAccessTokenInQuery)
{
request.RequestUri = new Uri(QueryHelpers.AddQueryString(request.RequestUri.OriginalString,
AuthorizerDefaults.AccessToken, accessToken.Token));
}
else
{
request.Headers.Authorization =
new AuthenticationHeaderValue(AuthorizerDefaults.Bearer, accessToken.Token);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
namespace GSS.Authorization.OAuth2
{
public class OAuth2HttpHandlerOptions
{
/// <summary>
/// sending access token in query parameter ? (default: Authorization header)
/// , see https://www.rfc-editor.org/rfc/rfc6750#section-2.3
/// </summary>
public bool SendAccessTokenInQuery { get; set; }

/// <summary>
/// sending access token in form-encoded body ? (default: Authorization header)
/// , see https://www.rfc-editor.org/rfc/rfc6750#section-2.2
/// </summary>
public bool SendAccessTokenInBody { get; set; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using System.ComponentModel.DataAnnotations;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Options;

namespace GSS.Authorization.OAuth2
{
Expand Down Expand Up @@ -39,6 +40,7 @@ public static IHttpClientBuilder AddOAuth2HttpClient<TClient, TAuthorizer>(this
.AddMemoryCache()
.AddHttpClient<TClient>()
.AddHttpMessageHandler(resolver => new OAuth2HttpHandler(
resolver.GetRequiredService<IOptions<OAuth2HttpHandlerOptions>>(),
resolver.GetRequiredService<TAuthorizer>(),
resolver.GetRequiredService<IMemoryCache>()));
}
Expand All @@ -47,7 +49,7 @@ public static IHttpClientBuilder AddOAuth2HttpClient<TClient, TAuthorizer>(this
/// Add named HttpClient with <see cref="OAuth2HttpHandler"/> and related services
/// </summary>
/// <param name="services">The <see cref="IServiceCollection"/>.</param>
/// <param name="name">The logical name of the <see cref="HttpClient"/> to configure.</param>
/// <param name="name">The logical name of the <see cref="System.Net.Http.HttpClient"/> to configure.</param>
/// <typeparam name="TAuthorizer">The type of the authorizer.</typeparam>
/// <param name="configureOptions">A delegate that is used to configure an <see cref="AuthorizerOptions"/>.</param>
/// <param name="configureAuthorizer">A delegate that is used to configure an <see cref="T:Microsoft.Extensions.DependencyInjection.IHttpClientBuilder" /> for the <see cref="Authorizer"/>.</param>
Expand All @@ -73,6 +75,7 @@ public static IHttpClientBuilder AddOAuth2HttpClient<TAuthorizer>(this IServiceC
.AddMemoryCache()
.AddHttpClient(name)
.AddHttpMessageHandler(resolver => new OAuth2HttpHandler(
resolver.GetRequiredService<IOptions<OAuth2HttpHandlerOptions>>(),
resolver.GetRequiredService<TAuthorizer>(),
resolver.GetRequiredService<IMemoryCache>()));
}
Expand All @@ -83,8 +86,9 @@ public static IHttpClientBuilder AddOAuth2HttpClient<TAuthorizer>(this IServiceC
/// <typeparam name="TAuthorizer">The type of the authorizer.</typeparam>
/// <param name="services">The <see cref="IServiceCollection"/>.</param>
/// <param name="configureOptions">A delegate that is used to configure an <see cref="AuthorizerOptions"/>.</param>
/// <returns>An <see cref="T:Microsoft.Extensions.DependencyInjection.IHttpClientBuilder" /> that can be used to configure the <see cref="AuthorizerHttpClient"/>.</returns>
internal static void TryAddOAuth2Authorizer<TAuthorizer>(this IServiceCollection services,
/// <param name="configureAuthorizer">A delegate that is used to configure an <see cref="T:Microsoft.Extensions.DependencyInjection.IHttpClientBuilder" /> for the <see cref="Authorizer"/>.</param>
/// <returns>An <see cref="T:Microsoft.Extensions.DependencyInjection.IHttpClientBuilder" /> that can be used to configure the <see cref="Authorizer"/>.</returns>
private static void TryAddOAuth2Authorizer<TAuthorizer>(this IServiceCollection services,
Action<IServiceProvider, AuthorizerOptions> configureOptions,
Action<IHttpClientBuilder>? configureAuthorizer = null)
where TAuthorizer : Authorizer
Expand Down
2 changes: 2 additions & 0 deletions src/GSS.Authorization.OAuth2/AuthorizerDefaults.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ namespace GSS.Authorization.OAuth2
{
public static class AuthorizerDefaults
{
public const string AccessToken = "access_token";

public const string Basic = "Basic";
public const string Bearer = "Bearer";

Expand Down
4 changes: 2 additions & 2 deletions src/GSS.Authorization.OAuth2/AuthorizerOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,11 @@ public class AuthorizerOptions

[Required]
public string ClientSecret { get; set; } = default!;

/*
* send the client credentials in the request-body? (default: Authorization header)
*
* see https://tools.ietf.org/html/rfc6749#section-2.3.1
* see https://www.rfc-editor.org/rfc/rfc6749#section-2.3.1
*/
public bool SendClientCredentialsInRequestBody { get; set; }

Expand Down
Loading

0 comments on commit 3760fcf

Please sign in to comment.