diff --git a/src/GraphQL.AspNetCore3.JwtBearer/AspNetCore3JwtBearerExtensions.cs b/src/GraphQL.AspNetCore3.JwtBearer/AspNetCore3JwtBearerExtensions.cs
index aa73d7a..442e1ef 100644
--- a/src/GraphQL.AspNetCore3.JwtBearer/AspNetCore3JwtBearerExtensions.cs
+++ b/src/GraphQL.AspNetCore3.JwtBearer/AspNetCore3JwtBearerExtensions.cs
@@ -13,7 +13,16 @@ public static class AspNetCore3JwtBearerExtensions
/// Adds JWT bearer authentication to a GraphQL server for WebSocket communications.
///
public static IGraphQLBuilder AddJwtBearerAuthentication(this IGraphQLBuilder builder)
+ => builder.AddJwtBearerAuthentication(options => { });
+
+ ///
+ public static IGraphQLBuilder AddJwtBearerAuthentication(this IGraphQLBuilder builder, bool enableJwtEvents)
+ => builder.AddJwtBearerAuthentication(options => options.EnableJwtEvents = enableJwtEvents);
+
+ ///
+ public static IGraphQLBuilder AddJwtBearerAuthentication(this IGraphQLBuilder builder, Action configureOptions)
{
+ builder.Services.Configure(configureOptions);
builder.AddWebSocketAuthentication();
return builder;
}
diff --git a/src/GraphQL.AspNetCore3.JwtBearer/JwtBearerAuthenticationOptions.cs b/src/GraphQL.AspNetCore3.JwtBearer/JwtBearerAuthenticationOptions.cs
new file mode 100644
index 0000000..05e953f
--- /dev/null
+++ b/src/GraphQL.AspNetCore3.JwtBearer/JwtBearerAuthenticationOptions.cs
@@ -0,0 +1,17 @@
+using Microsoft.AspNetCore.Authentication.JwtBearer;
+
+namespace GraphQL.AspNetCore3.JwtBearer;
+
+///
+/// Options for JWT Bearer authentication in GraphQL WebSocket connections.
+///
+public class JwtBearerAuthenticationOptions
+{
+ ///
+ /// Gets or sets a value indicating whether JWT events should be enabled.
+ /// When enabled, the will raise the
+ /// , ,
+ /// and events as appropriate.
+ ///
+ public bool EnableJwtEvents { get; set; }
+}
diff --git a/src/GraphQL.AspNetCore3.JwtBearer/JwtWebSocketAuthenticationService.cs b/src/GraphQL.AspNetCore3.JwtBearer/JwtWebSocketAuthenticationService.cs
index 6b30f71..89f5130 100644
--- a/src/GraphQL.AspNetCore3.JwtBearer/JwtWebSocketAuthenticationService.cs
+++ b/src/GraphQL.AspNetCore3.JwtBearer/JwtWebSocketAuthenticationService.cs
@@ -34,7 +34,9 @@ namespace GraphQL.AspNetCore3.JwtBearer;
/// mirroring the format of the 'Authorization' HTTP header.
///
/// -
-/// Events configured in are not raised by this implementation.
+/// When JWT events are enabled via , this implementation
+/// will raise the , ,
+/// and events as appropriate.
///
/// -
/// Implementation does not call to log authentication events.
@@ -46,16 +48,25 @@ public class JwtWebSocketAuthenticationService : IWebSocketAuthenticationService
private readonly IGraphQLSerializer _graphQLSerializer;
private readonly IOptionsMonitor _jwtBearerOptionsMonitor;
private readonly string[] _defaultAuthenticationSchemes;
+ private readonly JwtBearerAuthenticationOptions _jwtBearerAuthenticationOptions;
+ private readonly IAuthenticationSchemeProvider _schemeProvider;
///
/// Initializes a new instance of the class.
///
- public JwtWebSocketAuthenticationService(IGraphQLSerializer graphQLSerializer, IOptionsMonitor jwtBearerOptionsMonitor, IOptions authenticationOptions)
+ public JwtWebSocketAuthenticationService(
+ IGraphQLSerializer graphQLSerializer,
+ IOptionsMonitor jwtBearerOptionsMonitor,
+ IOptions authenticationOptions,
+ IOptions jwtBearerAuthenticationOptions,
+ IAuthenticationSchemeProvider schemeProvider)
{
_graphQLSerializer = graphQLSerializer;
_jwtBearerOptionsMonitor = jwtBearerOptionsMonitor;
var defaultAuthenticationScheme = authenticationOptions.Value.DefaultScheme;
_defaultAuthenticationSchemes = defaultAuthenticationScheme != null ? [defaultAuthenticationScheme] : [];
+ _jwtBearerAuthenticationOptions = jwtBearerAuthenticationOptions.Value;
+ _schemeProvider = schemeProvider;
}
///
@@ -79,6 +90,20 @@ public async Task AuthenticateAsync(AuthenticationRequest authenticationRequest)
foreach (var scheme in schemes) {
var options = _jwtBearerOptionsMonitor.Get(scheme);
+ // If JWT events are enabled, trigger the MessageReceived event
+ if (_jwtBearerAuthenticationOptions.EnableJwtEvents) {
+ var messageResult = await TriggerMessageReceivedEventAsync(connection.HttpContext, options, token, scheme).ConfigureAwait(false);
+ if (messageResult.Handled) {
+ if (messageResult.Success) {
+ connection.HttpContext.User = messageResult.Principal!;
+ return;
+ }
+ continue;
+ }
+
+ token = messageResult.Token;
+ }
+
// follow logic simplified from JwtBearerHandler.HandleAuthenticateAsync, as follows:
var tokenValidationParameters = await SetupTokenValidationParametersAsync(options, connection.HttpContext).ConfigureAwait(false);
#if NET8_0_OR_GREATER
@@ -88,11 +113,35 @@ public async Task AuthenticateAsync(AuthenticationRequest authenticationRequest)
var tokenValidationResult = await tokenHandler.ValidateTokenAsync(token, tokenValidationParameters).ConfigureAwait(false);
if (tokenValidationResult.IsValid) {
var principal = new ClaimsPrincipal(tokenValidationResult.ClaimsIdentity);
+
+ // If JWT events are enabled, trigger the TokenValidated event
+ if (_jwtBearerAuthenticationOptions.EnableJwtEvents)
+ {
+ var validatedResult = await TriggerTokenValidatedEventAsync(connection.HttpContext, options, principal, tokenValidationResult.SecurityToken, scheme).ConfigureAwait(false);
+ if (validatedResult.Handled && !validatedResult.Success)
+ {
+ continue;
+ }
+
+ principal = validatedResult.Principal ?? principal;
+ }
+
// set the ClaimsPrincipal for the HttpContext; authentication will take place against this object
connection.HttpContext.User = principal;
return;
}
- } catch {
+ } catch (Exception ex) {
+ // If JWT events are enabled, trigger the AuthenticationFailed event
+ if (_jwtBearerAuthenticationOptions.EnableJwtEvents)
+ {
+ var failedResult = await TriggerAuthenticationFailedEventAsync(connection.HttpContext, options, ex, scheme).ConfigureAwait(false);
+ if (failedResult.Handled && failedResult.Success)
+ {
+ connection.HttpContext.User = failedResult.Principal!;
+ return;
+ }
+ }
+
// no errors during authentication should throw an exception
// specifically, attempting to validate an invalid JWT token may result in an exception
}
@@ -105,11 +154,31 @@ public async Task AuthenticateAsync(AuthenticationRequest authenticationRequest)
foreach (var validator in options.SecurityTokenValidators) {
if (validator.CanReadToken(token)) {
try {
- var principal = validator.ValidateToken(token, tokenValidationParameters, out _);
+ var principal = validator.ValidateToken(token, tokenValidationParameters, out var securityToken);
+
+ // If JWT events are enabled, trigger the TokenValidated event
+ if (_jwtBearerAuthenticationOptions.EnableJwtEvents) {
+ var validatedResult = await TriggerTokenValidatedEventAsync(connection.HttpContext, options, principal, securityToken, scheme).ConfigureAwait(false);
+ if (validatedResult.Handled && !validatedResult.Success) {
+ continue;
+ }
+
+ principal = validatedResult.Principal ?? principal;
+ }
+
// set the ClaimsPrincipal for the HttpContext; authentication will take place against this object
connection.HttpContext.User = principal;
return;
- } catch {
+ } catch (Exception ex) {
+ // If JWT events are enabled, trigger the AuthenticationFailed event
+ if (_jwtBearerAuthenticationOptions.EnableJwtEvents) {
+ var failedResult = await TriggerAuthenticationFailedEventAsync(connection.HttpContext, options, ex, scheme).ConfigureAwait(false);
+ if (failedResult.Handled && failedResult.Success) {
+ connection.HttpContext.User = failedResult.Principal!;
+ return;
+ }
+ }
+
// no errors during authentication should throw an exception
// specifically, attempting to validate an invalid JWT token will result in an exception
}
@@ -149,6 +218,94 @@ private static async ValueTask SetupTokenValidationPa
return tokenValidationParameters;
}
+ private async Task TriggerMessageReceivedEventAsync(HttpContext httpContext, JwtBearerOptions options, string token, string schemeName)
+ {
+ var scheme = await _schemeProvider.GetSchemeAsync(schemeName)
+ ?? throw new InvalidOperationException($"Authentication scheme '{schemeName}' not found.");
+
+ var messageReceivedContext = new MessageReceivedContext(httpContext, scheme, options) {
+ Token = token
+ };
+
+ if (options.Events != null && options.Events.MessageReceived != null) {
+ await options.Events.MessageReceived(messageReceivedContext).ConfigureAwait(false);
+ }
+
+ var result = new EventResult { Token = messageReceivedContext.Token };
+
+ // If the event provided a principal, use it directly
+ if (messageReceivedContext.Result?.Succeeded == true) {
+ result.Handled = true;
+ result.Success = true;
+ result.Principal = messageReceivedContext.Principal;
+ }
+
+ return result;
+ }
+
+ private async Task TriggerTokenValidatedEventAsync(HttpContext httpContext, JwtBearerOptions options, ClaimsPrincipal principal, SecurityToken securityToken, string schemeName)
+ {
+ var scheme = await _schemeProvider.GetSchemeAsync(schemeName)
+ ?? throw new InvalidOperationException($"Authentication scheme '{schemeName}' not found.");
+
+ var tokenValidatedContext = new TokenValidatedContext(httpContext, scheme, options) {
+ Principal = principal,
+ SecurityToken = securityToken
+ };
+
+ if (options.Events != null && options.Events.TokenValidated != null) {
+ await options.Events.TokenValidated(tokenValidatedContext).ConfigureAwait(false);
+ }
+
+ var result = new EventResult();
+
+ // If the event failed or replaced the principal
+ if (tokenValidatedContext.Result != null) {
+ result.Handled = true;
+ result.Success = tokenValidatedContext.Result.Succeeded;
+ if (tokenValidatedContext.Result.Succeeded) {
+ result.Principal = tokenValidatedContext.Principal;
+ }
+ }
+
+ return result;
+ }
+
+ private async Task TriggerAuthenticationFailedEventAsync(HttpContext httpContext, JwtBearerOptions options, Exception exception, string schemeName)
+ {
+ var scheme = await _schemeProvider.GetSchemeAsync(schemeName)
+ ?? throw new InvalidOperationException($"Authentication scheme '{schemeName}' not found.");
+
+ var authenticationFailedContext = new AuthenticationFailedContext(httpContext, scheme, options) {
+ Exception = exception
+ };
+
+ if (options.Events != null && options.Events.AuthenticationFailed != null) {
+ await options.Events.AuthenticationFailed(authenticationFailedContext).ConfigureAwait(false);
+ }
+
+ var result = new EventResult();
+
+ // If the event handled the exception and succeeded
+ if (authenticationFailedContext.Result != null) {
+ result.Handled = true;
+ result.Success = authenticationFailedContext.Result.Succeeded;
+ if (authenticationFailedContext.Result.Succeeded) {
+ result.Principal = authenticationFailedContext.Principal;
+ }
+ }
+
+ return result;
+ }
+
+ private sealed class EventResult
+ {
+ public bool Handled { get; set; }
+ public bool Success { get; set; }
+ public ClaimsPrincipal? Principal { get; set; }
+ public string Token { get; set; } = string.Empty;
+ }
+
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
public sealed class AuthPayload
{
diff --git a/src/Tests.ApiApprovals/ApiApprovalTests.cs b/src/Tests.ApiApprovals/ApiApprovalTests.cs
index 9acb29b..2daf149 100644
--- a/src/Tests.ApiApprovals/ApiApprovalTests.cs
+++ b/src/Tests.ApiApprovals/ApiApprovalTests.cs
@@ -1,4 +1,5 @@
using GraphQL.AspNetCore3;
+using GraphQL.AspNetCore3.JwtBearer;
using PublicApiGenerator;
using Shouldly;
using Xunit;
@@ -12,6 +13,7 @@ public class ApiApprovalTests
{
[Theory]
[InlineData(typeof(GraphQLHttpMiddleware))]
+ [InlineData(typeof(JwtWebSocketAuthenticationService))]
public void PublicApi(Type type)
{
string publicApi = type.Assembly.GeneratePublicApi(new ApiGeneratorOptions {
diff --git a/src/Tests.ApiApprovals/GraphQL.AspNetCore3.JwtBearer.approved.txt b/src/Tests.ApiApprovals/GraphQL.AspNetCore3.JwtBearer.approved.txt
new file mode 100644
index 0000000..96587a5
--- /dev/null
+++ b/src/Tests.ApiApprovals/GraphQL.AspNetCore3.JwtBearer.approved.txt
@@ -0,0 +1,27 @@
+namespace GraphQL.AspNetCore3.JwtBearer
+{
+ public class JwtBearerAuthenticationOptions
+ {
+ public JwtBearerAuthenticationOptions() { }
+ public bool EnableJwtEvents { get; set; }
+ }
+ public class JwtWebSocketAuthenticationService : GraphQL.AspNetCore3.WebSockets.IWebSocketAuthenticationService
+ {
+ public JwtWebSocketAuthenticationService(GraphQL.IGraphQLSerializer graphQLSerializer, Microsoft.Extensions.Options.IOptionsMonitor jwtBearerOptionsMonitor, Microsoft.Extensions.Options.IOptions authenticationOptions, Microsoft.Extensions.Options.IOptions jwtBearerAuthenticationOptions, Microsoft.AspNetCore.Authentication.IAuthenticationSchemeProvider schemeProvider) { }
+ public System.Threading.Tasks.Task AuthenticateAsync(GraphQL.AspNetCore3.WebSockets.AuthenticationRequest authenticationRequest) { }
+ public sealed class AuthPayload
+ {
+ public AuthPayload() { }
+ public string? Authorization { get; set; }
+ }
+ }
+}
+namespace GraphQL
+{
+ public static class AspNetCore3JwtBearerExtensions
+ {
+ public static GraphQL.DI.IGraphQLBuilder AddJwtBearerAuthentication(this GraphQL.DI.IGraphQLBuilder builder) { }
+ public static GraphQL.DI.IGraphQLBuilder AddJwtBearerAuthentication(this GraphQL.DI.IGraphQLBuilder builder, System.Action configureOptions) { }
+ public static GraphQL.DI.IGraphQLBuilder AddJwtBearerAuthentication(this GraphQL.DI.IGraphQLBuilder builder, bool enableJwtEvents) { }
+ }
+}
diff --git a/src/Tests.ApiApprovals/Tests.ApiTests.csproj b/src/Tests.ApiApprovals/Tests.ApiTests.csproj
index fcbb7d5..86c37e7 100644
--- a/src/Tests.ApiApprovals/Tests.ApiTests.csproj
+++ b/src/Tests.ApiApprovals/Tests.ApiTests.csproj
@@ -16,6 +16,7 @@
+
diff --git a/src/Tests/JwtBearer/AspNetCore3JwtBearerExtensionsTests.cs b/src/Tests/JwtBearer/AspNetCore3JwtBearerExtensionsTests.cs
index 2686cc0..0015b93 100644
--- a/src/Tests/JwtBearer/AspNetCore3JwtBearerExtensionsTests.cs
+++ b/src/Tests/JwtBearer/AspNetCore3JwtBearerExtensionsTests.cs
@@ -26,6 +26,11 @@ public void AddJwtBearerAuthentication_ShouldAddJwtWebSocketAuthenticationServic
false))
.Returns(serviceRegisterMock.Object);
+ // Setup the Configure method to accept any Action
+ serviceRegisterMock
+ .Setup(x => x.Configure(It.IsAny>()))
+ .Returns(serviceRegisterMock.Object);
+
// Act
var result = graphQLBuilderMock.Object.AddJwtBearerAuthentication();
diff --git a/src/Tests/JwtBearer/JwtWebSocketAuthenticationServiceTests.cs b/src/Tests/JwtBearer/JwtWebSocketAuthenticationServiceTests.cs
index f71f1be..670de68 100644
--- a/src/Tests/JwtBearer/JwtWebSocketAuthenticationServiceTests.cs
+++ b/src/Tests/JwtBearer/JwtWebSocketAuthenticationServiceTests.cs
@@ -253,7 +253,7 @@ private TestServer CreateTestServer(bool defaultScheme = true, bool customScheme
services.AddGraphQL(b => b
.AddSchema(_schema)
.AddSystemTextJson()
- .AddJwtBearerAuthentication()
+ .AddJwtBearerAuthentication(true)
);
})
.Configure(app => {
diff --git a/src/Tests/Tests.csproj b/src/Tests/Tests.csproj
index 6da495c..fbd5ab5 100644
--- a/src/Tests/Tests.csproj
+++ b/src/Tests/Tests.csproj
@@ -1,7 +1,7 @@
- netcoreapp2.1;netcoreapp3.1;net5.0;net6.0;net8.0
+ net6.0;net8.0
net48;netcoreapp2.1;netcoreapp3.1;net5.0;net6.0;net8.0