-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge branch 'master' into dispose_once
- Loading branch information
Showing
20 changed files
with
756 additions
and
16 deletions.
There are no files selected for viewing
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
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
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
20 changes: 20 additions & 0 deletions
20
src/GraphQL.AspNetCore3.JwtBearer/AspNetCore3JwtBearerExtensions.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,20 @@ | ||
using GraphQL.AspNetCore3; | ||
using GraphQL.AspNetCore3.JwtBearer; | ||
using GraphQL.DI; | ||
|
||
namespace GraphQL; | ||
|
||
/// <summary> | ||
/// Extension methods for adding JWT bearer authentication to a GraphQL server for WebSocket communications. | ||
/// </summary> | ||
public static class AspNetCore3JwtBearerExtensions | ||
{ | ||
/// <summary> | ||
/// Adds JWT bearer authentication to a GraphQL server for WebSocket communications. | ||
/// </summary> | ||
public static IGraphQLBuilder AddJwtBearerAuthentication(this IGraphQLBuilder builder) | ||
{ | ||
builder.AddWebSocketAuthentication<JwtWebSocketAuthenticationService>(); | ||
return builder; | ||
} | ||
} |
27 changes: 27 additions & 0 deletions
27
src/GraphQL.AspNetCore3.JwtBearer/GraphQL.AspNetCore3.JwtBearer.csproj
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,27 @@ | ||
<Project Sdk="Microsoft.NET.Sdk"> | ||
|
||
<PropertyGroup> | ||
<TargetFrameworks>netstandard2.0;netcoreapp2.1;netcoreapp3.1;net6.0;net8.0</TargetFrameworks> | ||
<Description>JWT Bearer authentication for GraphQL projects</Description> | ||
<IsPackable>true</IsPackable> | ||
</PropertyGroup> | ||
|
||
<ItemGroup> | ||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.*" Condition="'$(TargetFramework)' == 'net8.0'" /> | ||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="6.0.*" Condition="'$(TargetFramework)' == 'net6.0'" /> | ||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="3.1.*" Condition="'$(TargetFramework)' == 'netcoreapp3.1'" /> | ||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="2.1.*" Condition="'$(TargetFramework)' == 'netcoreapp2.1'" /> | ||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="2.1.*" Condition="'$(TargetFramework)' == 'netstandard2.0'" /> | ||
</ItemGroup> | ||
|
||
<ItemGroup> | ||
<ProjectReference Include="..\GraphQL.AspNetCore3\GraphQL.AspNetCore3.csproj" /> | ||
</ItemGroup> | ||
|
||
<ItemGroup> | ||
<Using Include="System.Text" /> | ||
<Using Include="System.Threading" /> | ||
<Using Include="System.Threading.Tasks" /> | ||
</ItemGroup> | ||
|
||
</Project> |
158 changes: 158 additions & 0 deletions
158
src/GraphQL.AspNetCore3.JwtBearer/JwtWebSocketAuthenticationService.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,158 @@ | ||
// Parts of this code file are based on the JwtBearerHandler class in the Microsoft.AspNetCore.Authentication.JwtBearer package found at: | ||
// https://github.com/dotnet/aspnetcore/blob/5493b413d1df3aaf00651bdf1cbd8135fa63f517/src/Security/Authentication/JwtBearer/src/JwtBearerHandler.cs | ||
// | ||
// Those sections of code may be subject to the MIT license found at: | ||
// https://github.com/dotnet/aspnetcore/blob/5493b413d1df3aaf00651bdf1cbd8135fa63f517/LICENSE.txt | ||
|
||
using System.Security.Claims; | ||
using GraphQL.AspNetCore3.WebSockets; | ||
using Microsoft.AspNetCore.Authentication; | ||
using Microsoft.AspNetCore.Authentication.JwtBearer; | ||
using Microsoft.AspNetCore.Http; | ||
using Microsoft.Extensions.DependencyInjection; | ||
using Microsoft.Extensions.Options; | ||
using Microsoft.IdentityModel.Tokens; | ||
|
||
namespace GraphQL.AspNetCore3.JwtBearer; | ||
|
||
/// <summary> | ||
/// Authenticates WebSocket connections via the 'payload' of the initialization packet. | ||
/// This is necessary because WebSocket connections initiated from the browser cannot | ||
/// authenticate via HTTP headers. | ||
/// <br/><br/> | ||
/// Notes: | ||
/// <list type="bullet"> | ||
/// <item>This class is not used when authenticating over GET/POST.</item> | ||
/// <item> | ||
/// This class pulls the <see cref="JwtBearerOptions"/> instance registered by ASP.NET Core during the call to | ||
/// <see cref="JwtBearerExtensions.AddJwtBearer(AuthenticationBuilder, Action{JwtBearerOptions})">AddJwtBearer</see> | ||
/// for the default or configured authentication scheme and authenticates the token | ||
/// based on simplified logic used by <see cref="JwtBearerHandler"/>. | ||
/// </item> | ||
/// <item> | ||
/// The expected format of the payload is <c>{"Authorization":"Bearer TOKEN"}</c> where TOKEN is the JSON Web Token (JWT), | ||
/// mirroring the format of the 'Authorization' HTTP header. | ||
/// </item> | ||
/// <item> | ||
/// Events configured in <see cref="JwtBearerOptions.Events"/> are not raised by this implementation. | ||
/// </item> | ||
/// <item> | ||
/// Implementation does not call <see cref="Microsoft.Extensions.Logging.ILogger"/> to log authentication events. | ||
/// </item> | ||
/// </list> | ||
/// </summary> | ||
public class JwtWebSocketAuthenticationService : IWebSocketAuthenticationService | ||
{ | ||
private readonly IGraphQLSerializer _graphQLSerializer; | ||
private readonly IOptionsMonitor<JwtBearerOptions> _jwtBearerOptionsMonitor; | ||
private readonly string[] _defaultAuthenticationSchemes; | ||
|
||
/// <summary> | ||
/// Initializes a new instance of the <see cref="JwtWebSocketAuthenticationService"/> class. | ||
/// </summary> | ||
public JwtWebSocketAuthenticationService(IGraphQLSerializer graphQLSerializer, IOptionsMonitor<JwtBearerOptions> jwtBearerOptionsMonitor, IOptions<AuthenticationOptions> authenticationOptions) | ||
{ | ||
_graphQLSerializer = graphQLSerializer; | ||
_jwtBearerOptionsMonitor = jwtBearerOptionsMonitor; | ||
var defaultAuthenticationScheme = authenticationOptions.Value.DefaultScheme; | ||
_defaultAuthenticationSchemes = defaultAuthenticationScheme != null ? [defaultAuthenticationScheme] : []; | ||
} | ||
|
||
/// <inheritdoc/> | ||
public async Task AuthenticateAsync(AuthenticationRequest authenticationRequest) | ||
{ | ||
var connection = authenticationRequest.Connection; | ||
var operationMessage = authenticationRequest.OperationMessage; | ||
var schemes = authenticationRequest.AuthenticationSchemes.Any() ? authenticationRequest.AuthenticationSchemes : _defaultAuthenticationSchemes; | ||
try { | ||
// for connections authenticated via HTTP headers, no need to reauthenticate | ||
if (connection.HttpContext.User.Identity?.IsAuthenticated ?? false) | ||
return; | ||
|
||
// attempt to read the 'Authorization' key from the payload object and verify it contains "Bearer XXXXXXXX" | ||
var authPayload = _graphQLSerializer.ReadNode<AuthPayload>(operationMessage.Payload); | ||
if (authPayload != null && authPayload.Authorization != null && authPayload.Authorization.StartsWith("Bearer ", StringComparison.Ordinal)) { | ||
// pull the token from the value | ||
var token = authPayload.Authorization.Substring(7); | ||
|
||
// try to authenticate with each of the configured authentication schemes | ||
foreach (var scheme in schemes) { | ||
var options = _jwtBearerOptionsMonitor.Get(scheme); | ||
|
||
// follow logic simplified from JwtBearerHandler.HandleAuthenticateAsync, as follows: | ||
var tokenValidationParameters = await SetupTokenValidationParametersAsync(options, connection.HttpContext).ConfigureAwait(false); | ||
#if NET8_0_OR_GREATER | ||
if (!options.UseSecurityTokenValidators) { | ||
foreach (var tokenHandler in options.TokenHandlers) { | ||
try { | ||
var tokenValidationResult = await tokenHandler.ValidateTokenAsync(token, tokenValidationParameters).ConfigureAwait(false); | ||
if (tokenValidationResult.IsValid) { | ||
var principal = new ClaimsPrincipal(tokenValidationResult.ClaimsIdentity); | ||
// set the ClaimsPrincipal for the HttpContext; authentication will take place against this object | ||
connection.HttpContext.User = principal; | ||
return; | ||
} | ||
} catch { | ||
// no errors during authentication should throw an exception | ||
// specifically, attempting to validate an invalid JWT token may result in an exception | ||
} | ||
} | ||
} else { | ||
#else | ||
{ | ||
#endif | ||
#pragma warning disable CS0618 // Type or member is obsolete | ||
foreach (var validator in options.SecurityTokenValidators) { | ||
if (validator.CanReadToken(token)) { | ||
try { | ||
var principal = validator.ValidateToken(token, tokenValidationParameters, out _); | ||
// set the ClaimsPrincipal for the HttpContext; authentication will take place against this object | ||
connection.HttpContext.User = principal; | ||
return; | ||
} catch { | ||
// no errors during authentication should throw an exception | ||
// specifically, attempting to validate an invalid JWT token will result in an exception | ||
} | ||
} | ||
} | ||
#pragma warning restore CS0618 // Type or member is obsolete | ||
} | ||
} | ||
} | ||
} catch { | ||
// no errors during authentication should throw an exception | ||
// specifically, parsing invalid JSON will result in an exception | ||
} | ||
} | ||
|
||
private static async ValueTask<TokenValidationParameters> SetupTokenValidationParametersAsync(JwtBearerOptions options, HttpContext httpContext) | ||
{ | ||
// Clone to avoid cross request race conditions for updated configurations. | ||
var tokenValidationParameters = options.TokenValidationParameters.Clone(); | ||
|
||
#if NET8_0_OR_GREATER | ||
if (options.ConfigurationManager is BaseConfigurationManager baseConfigurationManager) { | ||
tokenValidationParameters.ConfigurationManager = baseConfigurationManager; | ||
} else { | ||
#else | ||
{ | ||
#endif | ||
if (options.ConfigurationManager != null) { | ||
// GetConfigurationAsync has a time interval that must pass before new http request will be issued. | ||
var configuration = await options.ConfigurationManager.GetConfigurationAsync(httpContext.RequestAborted).ConfigureAwait(false); | ||
var issuers = new[] { configuration.Issuer }; | ||
tokenValidationParameters.ValidIssuers = (tokenValidationParameters.ValidIssuers == null ? issuers : tokenValidationParameters.ValidIssuers.Concat(issuers)); | ||
tokenValidationParameters.IssuerSigningKeys = (tokenValidationParameters.IssuerSigningKeys == null ? configuration.SigningKeys : tokenValidationParameters.IssuerSigningKeys.Concat(configuration.SigningKeys)); | ||
} | ||
} | ||
|
||
return tokenValidationParameters; | ||
} | ||
|
||
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member | ||
public sealed class AuthPayload | ||
{ | ||
public string? Authorization { get; set; } | ||
} | ||
#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member | ||
} |
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
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
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
48 changes: 48 additions & 0 deletions
48
src/GraphQL.AspNetCore3/WebSockets/AuthenticationRequest.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,48 @@ | ||
namespace GraphQL.AspNetCore3.WebSockets; | ||
|
||
/// <summary> | ||
/// Represents an authentication request within the GraphQL ASP.NET Core WebSocket context. | ||
/// </summary> | ||
public class AuthenticationRequest | ||
{ | ||
/// <summary> | ||
/// Gets the WebSocket connection associated with the authentication request. | ||
/// </summary> | ||
/// <value> | ||
/// An instance of <see cref="IWebSocketConnection"/> representing the active WebSocket connection. | ||
/// </value> | ||
public IWebSocketConnection Connection { get; } | ||
|
||
/// <summary> | ||
/// Gets the subprotocol used for the WebSocket communication. | ||
/// </summary> | ||
/// <value> | ||
/// A <see cref="string"/> specifying the subprotocol negotiated for the WebSocket connection. | ||
/// </value> | ||
public string SubProtocol { get; } | ||
|
||
/// <summary> | ||
/// Gets the operation message containing details of the authentication operation. | ||
/// </summary> | ||
/// <value> | ||
/// An instance of <see cref="OperationMessage"/> that encapsulates the specifics of the authentication request. | ||
/// </value> | ||
public OperationMessage OperationMessage { get; } | ||
|
||
/// <summary> | ||
/// Gets a list of the authentication schemes the authentication requirements are evaluated against. | ||
/// When no schemes are specified, the default authentication scheme is used. | ||
/// </summary> | ||
public IEnumerable<string> AuthenticationSchemes { get; } | ||
|
||
/// <summary> | ||
/// Initializes a new instance of the <see cref="AuthenticationRequest"/> class. | ||
/// </summary> | ||
public AuthenticationRequest(IWebSocketConnection connection, string subProtocol, OperationMessage operationMessage, IEnumerable<string> authenticationSchemes) | ||
{ | ||
Connection = connection; | ||
SubProtocol = subProtocol; | ||
OperationMessage = operationMessage; | ||
AuthenticationSchemes = authenticationSchemes; | ||
} | ||
} |
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.