-
-
Notifications
You must be signed in to change notification settings - Fork 32
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add bearer authentication concern (#506)
* First draft for bearer auth * Additional coverage and documentation * Fix test
- Loading branch information
1 parent
938e559
commit 0c9a8f9
Showing
6 changed files
with
439 additions
and
0 deletions.
There are no files selected for viewing
167 changes: 167 additions & 0 deletions
167
Modules/Authentication/Bearer/BearerAuthenticationConcern.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,167 @@ | ||
using System; | ||
using System.Collections.Generic; | ||
using System.IdentityModel.Tokens.Jwt; | ||
using System.Net.Http; | ||
using System.Text.Json; | ||
using System.Text.Json.Serialization; | ||
using System.Threading.Tasks; | ||
|
||
using GenHTTP.Api.Content; | ||
using GenHTTP.Api.Protocol; | ||
|
||
using Microsoft.IdentityModel.Logging; | ||
using Microsoft.IdentityModel.Tokens; | ||
|
||
namespace GenHTTP.Modules.Authentication.Bearer | ||
{ | ||
|
||
#region Supporting data structures | ||
|
||
internal class OpenIDConfiguration | ||
{ | ||
|
||
[JsonPropertyName("jwks_uri")] | ||
public string? KeySetUrl { get; set; } | ||
|
||
} | ||
|
||
#endregion | ||
|
||
internal sealed class BearerAuthenticationConcern : IConcern | ||
{ | ||
private ICollection<SecurityKey>? _IssuerKeys = null; | ||
|
||
#region Get-/Setters | ||
|
||
public IHandler Content { get; } | ||
|
||
public IHandler Parent { get; } | ||
|
||
private TokenValidationOptions ValidationOptions { get; } | ||
|
||
#endregion | ||
|
||
#region Initialization | ||
|
||
internal BearerAuthenticationConcern(IHandler parent, Func<IHandler, IHandler> contentFactory, TokenValidationOptions validationOptions) | ||
{ | ||
Parent = parent; | ||
Content = contentFactory(this); | ||
|
||
ValidationOptions = validationOptions; | ||
} | ||
|
||
#endregion | ||
|
||
#region Functionality | ||
|
||
public ValueTask PrepareAsync() => Content.PrepareAsync(); | ||
|
||
public IAsyncEnumerable<ContentElement> GetContentAsync(IRequest request) => Content.GetContentAsync(request); | ||
|
||
public async ValueTask<IResponse?> HandleAsync(IRequest request) | ||
{ | ||
IdentityModelEventSource.LogCompleteSecurityArtifact = true; | ||
|
||
if (!request.Headers.TryGetValue("Authorization", out var authHeader)) | ||
{ | ||
throw new ProviderException(ResponseStatus.Unauthorized, "This endpoint requires authorization"); | ||
} | ||
|
||
if (!authHeader.StartsWith("Bearer ")) | ||
{ | ||
throw new ProviderException(ResponseStatus.Unauthorized, "This endpoint requires bearer token authentication"); | ||
} | ||
|
||
var tokenString = authHeader[7..]; | ||
|
||
var tokenHandler = new JwtSecurityTokenHandler(); | ||
|
||
if (!tokenHandler.CanReadToken(tokenString)) | ||
{ | ||
throw new ProviderException(ResponseStatus.BadRequest, "Malformed authentication token"); | ||
} | ||
|
||
var issuer = ValidationOptions.Issuer; | ||
|
||
var audience = ValidationOptions.Audience; | ||
|
||
if ((issuer != null) && (_IssuerKeys == null)) | ||
{ | ||
_IssuerKeys = await FetchSigningKeys(issuer); | ||
} | ||
|
||
var validationParameters = new TokenValidationParameters() | ||
{ | ||
ValidIssuer = issuer, | ||
ValidateIssuer = (issuer != null), | ||
IssuerSigningKeys = _IssuerKeys, | ||
ValidAudience = audience, | ||
ValidateAudience = (audience != null), | ||
ValidateLifetime = ValidationOptions.Lifetime | ||
}; | ||
|
||
if (issuer == null) | ||
{ | ||
validationParameters.SignatureValidator = (string token, TokenValidationParameters p) => new JwtSecurityToken(tokenString); | ||
} | ||
|
||
try | ||
{ | ||
tokenHandler.ValidateToken(tokenString, validationParameters, out var validated); | ||
|
||
var jwt = (JwtSecurityToken)validated; | ||
|
||
if (ValidationOptions.CustomValidator != null) | ||
{ | ||
await ValidationOptions.CustomValidator.Invoke(jwt); | ||
} | ||
|
||
if (ValidationOptions.UserMapping != null) | ||
{ | ||
var user = await ValidationOptions.UserMapping.Invoke(request, jwt); | ||
|
||
if (user != null) | ||
{ | ||
request.SetUser(user); | ||
} | ||
} | ||
|
||
return await Content.HandleAsync(request); | ||
} | ||
catch (SecurityTokenValidationException ex) | ||
{ | ||
throw new ProviderException(ResponseStatus.Unauthorized, $"Authorization failed: {ex.Message}", ex); | ||
} | ||
} | ||
|
||
private static async Task<ICollection<SecurityKey>> FetchSigningKeys(string issuer) | ||
{ | ||
try | ||
{ | ||
var configUrl = $"{issuer}/.well-known/openid-configuration"; | ||
|
||
using var httpClient = new HttpClient(); | ||
|
||
var configResponse = await httpClient.GetStringAsync(configUrl); | ||
|
||
var config = JsonSerializer.Deserialize<OpenIDConfiguration>(configResponse) | ||
?? throw new InvalidOperationException($"Unable to discover configuration via '{configUrl}'"); | ||
|
||
var keyResponse = await httpClient.GetStringAsync(config.KeySetUrl); | ||
|
||
var keySet = new JsonWebKeySet(keyResponse); | ||
|
||
return keySet.GetSigningKeys(); | ||
} | ||
catch (Exception ex) | ||
{ | ||
throw new ProviderException(ResponseStatus.InternalServerError, "Unable to fetch signing issuer signing keys", ex); | ||
} | ||
} | ||
|
||
} | ||
|
||
#endregion | ||
|
||
} |
98 changes: 98 additions & 0 deletions
98
Modules/Authentication/Bearer/BearerAuthenticationConcernBuilder.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,98 @@ | ||
using System; | ||
using System.IdentityModel.Tokens.Jwt; | ||
using System.Threading.Tasks; | ||
|
||
using GenHTTP.Api.Content; | ||
using GenHTTP.Api.Content.Authentication; | ||
using GenHTTP.Api.Protocol; | ||
|
||
namespace GenHTTP.Modules.Authentication.Bearer | ||
{ | ||
|
||
public sealed class BearerAuthenticationConcernBuilder : IConcernBuilder | ||
{ | ||
private readonly TokenValidationOptions _Options = new(); | ||
|
||
#region Functionality | ||
|
||
/// <summary> | ||
/// Sets the expected issuer. Tokens that are not issued by this | ||
/// party will be declined. | ||
/// </summary> | ||
/// <param name="issuer">The URL of the exepcted issuer</param> | ||
/// <remarks> | ||
/// Setting the issuer will cause the concern to download and cache | ||
/// the signing keys that are used to ensure that the party actually | ||
/// issued a token. | ||
/// </remarks> | ||
public BearerAuthenticationConcernBuilder Issuer(string issuer) | ||
{ | ||
_Options.Issuer = issuer; | ||
return this; | ||
} | ||
|
||
/// <summary> | ||
/// Sets the expected audience that should be accepted. | ||
/// </summary> | ||
/// <param name="audience">The audience to check for</param> | ||
public BearerAuthenticationConcernBuilder Audience(string audience) | ||
{ | ||
_Options.Audience = audience; | ||
return this; | ||
} | ||
|
||
/// <summary> | ||
/// Adds a custom validator that can analyze the token read from the | ||
/// request and can perform additional checks. | ||
/// </summary> | ||
/// <param name="validator">The custom validator to be used</param> | ||
/// <remarks> | ||
/// This validator will be invoked after the regular checks (such as the | ||
/// issuer) have been performed. | ||
/// | ||
/// If you would like to deny user access within a custom validator, | ||
/// you can throw a <see cref="ProviderException" />. | ||
/// </remarks> | ||
public BearerAuthenticationConcernBuilder Validation(Func<JwtSecurityToken, Task> validator) | ||
{ | ||
_Options.CustomValidator = validator; | ||
return this; | ||
} | ||
|
||
/// <summary> | ||
/// Optionally register a function that will compute the user that | ||
/// should be set for the request. | ||
/// </summary> | ||
/// <param name="userMapping">The user mapping to be used</param> | ||
/// <remarks> | ||
/// The usage of this mechanism allows to inject the user into | ||
/// service methods via the user injector class. Returning null | ||
/// within the delegate will not deny user access - if you would | ||
/// like to prevent such user, you can throw a <see cref="ProviderException" />. | ||
/// </remarks> | ||
public BearerAuthenticationConcernBuilder UserMapping(Func<IRequest, JwtSecurityToken, ValueTask<IUser?>> userMapping) | ||
{ | ||
_Options.UserMapping = userMapping; | ||
return this; | ||
} | ||
|
||
/// <summary> | ||
/// If enabled, tokens that have expired or are not valid yet are | ||
/// still accepted. This should be used for testing purposes only. | ||
/// </summary> | ||
public BearerAuthenticationConcernBuilder AllowExpired() | ||
{ | ||
_Options.Lifetime = false; | ||
return this; | ||
} | ||
|
||
public IConcern Build(IHandler parent, Func<IHandler, IHandler> contentFactory) | ||
{ | ||
return new BearerAuthenticationConcern(parent, contentFactory, _Options); | ||
} | ||
|
||
#endregion | ||
|
||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
using System; | ||
using System.IdentityModel.Tokens.Jwt; | ||
using System.Threading.Tasks; | ||
|
||
using GenHTTP.Api.Content.Authentication; | ||
using GenHTTP.Api.Protocol; | ||
|
||
namespace GenHTTP.Modules.Authentication.Bearer | ||
{ | ||
|
||
internal sealed class TokenValidationOptions | ||
{ | ||
|
||
internal string? Audience { get; set; } | ||
|
||
internal string? Issuer { get; set; } | ||
|
||
internal bool Lifetime { get; set; } = true; | ||
|
||
internal Func<JwtSecurityToken, Task>? CustomValidator { get; set; } | ||
|
||
internal Func<IRequest, JwtSecurityToken, ValueTask<IUser?>>? UserMapping { get; set; } | ||
|
||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
using GenHTTP.Modules.Authentication.Bearer; | ||
|
||
namespace GenHTTP.Modules.Authentication | ||
{ | ||
|
||
public static class BearerAuthentication | ||
{ | ||
|
||
/// <summary> | ||
/// Creates a concern that will read an access token from | ||
/// the authorization headers and validate it according to | ||
/// its configuration. | ||
/// </summary> | ||
/// <returns>The newly created concern</returns> | ||
public static BearerAuthenticationConcernBuilder Create() => new(); | ||
|
||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.