From 9f1e9891eccace4453288be8ee1dbcace6c8135f Mon Sep 17 00:00:00 2001 From: Shane Krueger Date: Wed, 7 May 2025 15:03:20 -0400 Subject: [PATCH 01/13] Add capability to enable JwtBearer events --- .../AspNetCore3JwtBearerExtensions.cs | 9 + .../JwtBearerAuthenticationOptions.cs | 17 ++ .../JwtWebSocketAuthenticationService.cs | 185 +++++++++++++++++- src/Tests.ApiApprovals/ApiApprovalTests.cs | 2 + ...GraphQL.AspNetCore3.JwtBearer.approved.txt | 27 +++ src/Tests.ApiApprovals/Tests.ApiTests.csproj | 1 + .../AspNetCore3JwtBearerExtensionsTests.cs | 5 + 7 files changed, 241 insertions(+), 5 deletions(-) create mode 100644 src/GraphQL.AspNetCore3.JwtBearer/JwtBearerAuthenticationOptions.cs create mode 100644 src/Tests.ApiApprovals/GraphQL.AspNetCore3.JwtBearer.approved.txt 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..89e1ad9 --- /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 implement + /// , , + /// 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..bd6693a 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,23 @@ 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 +116,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 +157,35 @@ 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 +225,105 @@ 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..c7c653c 100644 --- a/src/Tests/JwtBearer/AspNetCore3JwtBearerExtensionsTests.cs +++ b/src/Tests/JwtBearer/AspNetCore3JwtBearerExtensionsTests.cs @@ -25,6 +25,11 @@ public void AddJwtBearerAuthentication_ShouldAddJwtWebSocketAuthenticationServic ServiceLifetime.Singleton, 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(); From 8bad37b504779aba31bea25dd927c4cfbb46e304 Mon Sep 17 00:00:00 2001 From: Shane Krueger Date: Wed, 7 May 2025 15:13:01 -0400 Subject: [PATCH 02/13] fix comment --- .../JwtBearerAuthenticationOptions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/GraphQL.AspNetCore3.JwtBearer/JwtBearerAuthenticationOptions.cs b/src/GraphQL.AspNetCore3.JwtBearer/JwtBearerAuthenticationOptions.cs index 89e1ad9..05e953f 100644 --- a/src/GraphQL.AspNetCore3.JwtBearer/JwtBearerAuthenticationOptions.cs +++ b/src/GraphQL.AspNetCore3.JwtBearer/JwtBearerAuthenticationOptions.cs @@ -9,7 +9,7 @@ public class JwtBearerAuthenticationOptions { /// /// Gets or sets a value indicating whether JWT events should be enabled. - /// When enabled, the will implement + /// When enabled, the will raise the /// , , /// and events as appropriate. /// From 94c07fbac9b4354d67794bc845926206e8c2483a Mon Sep 17 00:00:00 2001 From: Shane Krueger Date: Wed, 7 May 2025 15:13:39 -0400 Subject: [PATCH 03/13] update formatting --- .../JwtWebSocketAuthenticationService.cs | 82 ++++++++----------- 1 file changed, 32 insertions(+), 50 deletions(-) diff --git a/src/GraphQL.AspNetCore3.JwtBearer/JwtWebSocketAuthenticationService.cs b/src/GraphQL.AspNetCore3.JwtBearer/JwtWebSocketAuthenticationService.cs index bd6693a..89f5130 100644 --- a/src/GraphQL.AspNetCore3.JwtBearer/JwtWebSocketAuthenticationService.cs +++ b/src/GraphQL.AspNetCore3.JwtBearer/JwtWebSocketAuthenticationService.cs @@ -91,19 +91,16 @@ public async Task AuthenticateAsync(AuthenticationRequest authenticationRequest) var options = _jwtBearerOptionsMonitor.Get(scheme); // If JWT events are enabled, trigger the MessageReceived event - if (_jwtBearerAuthenticationOptions.EnableJwtEvents) - { + if (_jwtBearerAuthenticationOptions.EnableJwtEvents) { var messageResult = await TriggerMessageReceivedEventAsync(connection.HttpContext, options, token, scheme).ConfigureAwait(false); - if (messageResult.Handled) - { - if (messageResult.Success) - { + if (messageResult.Handled) { + if (messageResult.Success) { connection.HttpContext.User = messageResult.Principal!; return; } continue; } - + token = messageResult.Token; } @@ -158,34 +155,30 @@ public async Task AuthenticateAsync(AuthenticationRequest authenticationRequest) if (validator.CanReadToken(token)) { try { var principal = validator.ValidateToken(token, tokenValidationParameters, out var securityToken); - + // If JWT events are enabled, trigger the TokenValidated event - if (_jwtBearerAuthenticationOptions.EnableJwtEvents) - { + if (_jwtBearerAuthenticationOptions.EnableJwtEvents) { var validatedResult = await TriggerTokenValidatedEventAsync(connection.HttpContext, options, principal, securityToken, scheme).ConfigureAwait(false); - if (validatedResult.Handled && !validatedResult.Success) - { + 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 (Exception ex) { // If JWT events are enabled, trigger the AuthenticationFailed event - if (_jwtBearerAuthenticationOptions.EnableJwtEvents) - { + if (_jwtBearerAuthenticationOptions.EnableJwtEvents) { var failedResult = await TriggerAuthenticationFailedEventAsync(connection.HttpContext, options, ex, scheme).ConfigureAwait(false); - if (failedResult.Handled && failedResult.Success) - { + 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 } @@ -229,22 +222,19 @@ private async Task TriggerMessageReceivedEventAsync(HttpContext htt { var scheme = await _schemeProvider.GetSchemeAsync(schemeName) ?? throw new InvalidOperationException($"Authentication scheme '{schemeName}' not found."); - - var messageReceivedContext = new MessageReceivedContext(httpContext, scheme, options) - { + + var messageReceivedContext = new MessageReceivedContext(httpContext, scheme, options) { Token = token }; - if (options.Events != null && options.Events.MessageReceived != null) - { + 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) - { + if (messageReceivedContext.Result?.Succeeded == true) { result.Handled = true; result.Success = true; result.Principal = messageReceivedContext.Principal; @@ -257,27 +247,23 @@ private async Task TriggerTokenValidatedEventAsync(HttpContext http { var scheme = await _schemeProvider.GetSchemeAsync(schemeName) ?? throw new InvalidOperationException($"Authentication scheme '{schemeName}' not found."); - - var tokenValidatedContext = new TokenValidatedContext(httpContext, scheme, options) - { + + var tokenValidatedContext = new TokenValidatedContext(httpContext, scheme, options) { Principal = principal, SecurityToken = securityToken }; - if (options.Events != null && options.Events.TokenValidated != null) - { + 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) - { + if (tokenValidatedContext.Result != null) { result.Handled = true; result.Success = tokenValidatedContext.Result.Succeeded; - if (tokenValidatedContext.Result.Succeeded) - { + if (tokenValidatedContext.Result.Succeeded) { result.Principal = tokenValidatedContext.Principal; } } @@ -289,26 +275,22 @@ private async Task TriggerAuthenticationFailedEventAsync(HttpContex { var scheme = await _schemeProvider.GetSchemeAsync(schemeName) ?? throw new InvalidOperationException($"Authentication scheme '{schemeName}' not found."); - - var authenticationFailedContext = new AuthenticationFailedContext(httpContext, scheme, options) - { + + var authenticationFailedContext = new AuthenticationFailedContext(httpContext, scheme, options) { Exception = exception }; - if (options.Events != null && options.Events.AuthenticationFailed != null) - { + 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) - { + if (authenticationFailedContext.Result != null) { result.Handled = true; result.Success = authenticationFailedContext.Result.Succeeded; - if (authenticationFailedContext.Result.Succeeded) - { + if (authenticationFailedContext.Result.Succeeded) { result.Principal = authenticationFailedContext.Principal; } } From b3ecb394d06ebd32da8ed634fd98038a887f3e58 Mon Sep 17 00:00:00 2001 From: Shane Krueger Date: Wed, 7 May 2025 16:24:44 -0400 Subject: [PATCH 04/13] update --- src/Tests/JwtBearer/AspNetCore3JwtBearerExtensionsTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Tests/JwtBearer/AspNetCore3JwtBearerExtensionsTests.cs b/src/Tests/JwtBearer/AspNetCore3JwtBearerExtensionsTests.cs index c7c653c..0015b93 100644 --- a/src/Tests/JwtBearer/AspNetCore3JwtBearerExtensionsTests.cs +++ b/src/Tests/JwtBearer/AspNetCore3JwtBearerExtensionsTests.cs @@ -25,7 +25,7 @@ public void AddJwtBearerAuthentication_ShouldAddJwtWebSocketAuthenticationServic ServiceLifetime.Singleton, false)) .Returns(serviceRegisterMock.Object); - + // Setup the Configure method to accept any Action serviceRegisterMock .Setup(x => x.Configure(It.IsAny>())) From 1a640b56429f126e3dac76205263b9550e1aa49f Mon Sep 17 00:00:00 2001 From: Shane Krueger Date: Wed, 7 May 2025 16:28:51 -0400 Subject: [PATCH 05/13] delete net core 3.1 / 5.0 testing --- src/Tests/JwtBearer/JwtWebSocketAuthenticationServiceTests.cs | 2 +- src/Tests/Tests.csproj | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) 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..912602c 100644 --- a/src/Tests/Tests.csproj +++ b/src/Tests/Tests.csproj @@ -1,10 +1,10 @@ - netcoreapp2.1;netcoreapp3.1;net5.0;net6.0;net8.0 + netcoreapp2.1;net6.0;net8.0 - net48;netcoreapp2.1;netcoreapp3.1;net5.0;net6.0;net8.0 + net48;netcoreapp2.1;net6.0;net8.0 From 4f88d25df2e9b37dd736144f2fa8ce24592797d9 Mon Sep 17 00:00:00 2001 From: Shane Krueger Date: Wed, 7 May 2025 16:29:19 -0400 Subject: [PATCH 06/13] update --- src/Tests/Tests.csproj | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/src/Tests/Tests.csproj b/src/Tests/Tests.csproj index 912602c..e2f2106 100644 --- a/src/Tests/Tests.csproj +++ b/src/Tests/Tests.csproj @@ -75,22 +75,6 @@ 6.0.* - - - 5.0.* - - - 5.0.* - - - - - 3.1.* - - - 3.1.* - - 2.1.* From 033f8dfbe201fe8db144fd1f1cf9ae3de9851ff1 Mon Sep 17 00:00:00 2001 From: Shane Krueger Date: Wed, 7 May 2025 16:34:55 -0400 Subject: [PATCH 07/13] update --- .github/workflows/test.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f094b51..d2fdbff 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -26,8 +26,6 @@ jobs: with: dotnet-version: | 2.1.x - 3.1.x - 5.0.x 6.0.x 8.0.x source-url: https://nuget.pkg.github.com/Shane32/index.json From 92a648673d45caf59c42aab5d8da08f288144b7b Mon Sep 17 00:00:00 2001 From: Shane Krueger Date: Wed, 7 May 2025 16:40:52 -0400 Subject: [PATCH 08/13] update --- .github/workflows/test.yml | 2 ++ src/Tests/Tests.csproj | 20 ++++++++++++++++++-- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d2fdbff..f094b51 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -26,6 +26,8 @@ jobs: with: dotnet-version: | 2.1.x + 3.1.x + 5.0.x 6.0.x 8.0.x source-url: https://nuget.pkg.github.com/Shane32/index.json diff --git a/src/Tests/Tests.csproj b/src/Tests/Tests.csproj index e2f2106..fbd5ab5 100644 --- a/src/Tests/Tests.csproj +++ b/src/Tests/Tests.csproj @@ -1,10 +1,10 @@ - netcoreapp2.1;net6.0;net8.0 + net6.0;net8.0 - net48;netcoreapp2.1;net6.0;net8.0 + net48;netcoreapp2.1;netcoreapp3.1;net5.0;net6.0;net8.0 @@ -75,6 +75,22 @@ 6.0.* + + + 5.0.* + + + 5.0.* + + + + + 3.1.* + + + 3.1.* + + 2.1.* From aaeea368b62661e667d9b2e3d58c053e24347182 Mon Sep 17 00:00:00 2001 From: Shane Krueger Date: Wed, 7 May 2025 18:06:52 -0400 Subject: [PATCH 09/13] Update --- .../JwtWebSocketAuthenticationServiceTests.cs | 194 +++++++++++++++--- 1 file changed, 162 insertions(+), 32 deletions(-) diff --git a/src/Tests/JwtBearer/JwtWebSocketAuthenticationServiceTests.cs b/src/Tests/JwtBearer/JwtWebSocketAuthenticationServiceTests.cs index 670de68..0ab5339 100644 --- a/src/Tests/JwtBearer/JwtWebSocketAuthenticationServiceTests.cs +++ b/src/Tests/JwtBearer/JwtWebSocketAuthenticationServiceTests.cs @@ -21,146 +21,252 @@ public class JwtWebSocketAuthenticationServiceTests private string? _jwtAccessToken; private readonly MockHttpMessageHandler _oidcHttpMessageHandler = new(); private readonly ISchema _schema; + + // Event tracking flags + private bool _messageReceived; + private bool _tokenValidated; + private bool _authenticationFailed; + private bool _enableJwtEvents; + + private readonly JwtBearerEvents _jwtBearerEvents; + private Action? _testFieldAction; public JwtWebSocketAuthenticationServiceTests() { var query = new ObjectGraphType() { Name = "Query" }; - query.Field("test").Resolve(ctx => ctx.User?.FindFirst(ClaimTypes.NameIdentifier)?.Value); + query.Field("test").Resolve(ctx => + { + _testFieldAction?.Invoke(ctx); + return ctx.User?.FindFirst(ClaimTypes.NameIdentifier)?.Value; + }); _schema = new Schema { Query = query }; + + // Initialize JwtBearerEvents + _jwtBearerEvents = new JwtBearerEvents + { + OnMessageReceived = context => + { + _messageReceived = true; + return Task.CompletedTask; + }, + OnTokenValidated = context => + { + _tokenValidated = true; + return Task.CompletedTask; + }, + OnAuthenticationFailed = context => + { + _authenticationFailed = true; + return Task.CompletedTask; + } + }; + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task SuccessfulAuthentication(bool enableJwtEvents) + { + CreateSignedToken(); + SetupOidcDiscovery(); + _enableJwtEvents = enableJwtEvents; + using var testServer = CreateTestServer(); + await TestGetAsync(testServer, isAuthenticated: true); + await TestWebSocketAsync(testServer, isAuthenticated: true); } + [Fact] - public async Task SuccessfulAuthentication() + public async Task SuccessfulAuthenticationWithCustomClaim() { + // Configure JwtBearerEvents to add the custom claim during token validation + _jwtBearerEvents.OnTokenValidated = context => { + // Add the custom claim to the user's identity + var identity = context.Principal?.Identity as ClaimsIdentity; + identity?.AddClaim(new Claim("custom:role", "admin")); + + _tokenValidated = true; + return Task.CompletedTask; + }; + + // Set up the test field action to verify the custom claim + _testFieldAction = context => { + var claim = context.User?.FindFirst("custom:role"); + claim.ShouldNotBeNull(); + claim.Value.ShouldBe("admin"); + }; + + // Create the token and set up the test server CreateSignedToken(); SetupOidcDiscovery(); + _enableJwtEvents = true; using var testServer = CreateTestServer(); await TestGetAsync(testServer, isAuthenticated: true); await TestWebSocketAsync(testServer, isAuthenticated: true); } - [Fact] - public async Task WrongKeys() + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task WrongKeys(bool enableJwtEvents) { CreateSignedToken(); SetupOidcDiscovery(); + _enableJwtEvents = enableJwtEvents; using var testServer = CreateTestServer(); CreateSignedToken(); // create new token with different keys await TestGetAsync(testServer, isAuthenticated: false); await TestWebSocketAsync(testServer, isAuthenticated: false); } - [Fact] - public async Task WrongIssuer() + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task WrongIssuer(bool enableJwtEvents) { CreateSignedToken(); _issuer = "https://wrong.issuer"; SetupOidcDiscovery(); + _enableJwtEvents = enableJwtEvents; using var testServer = CreateTestServer(); await TestGetAsync(testServer, isAuthenticated: false); await TestWebSocketAsync(testServer, isAuthenticated: false); } - [Fact] - public async Task WrongAudience() + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task WrongAudience(bool enableJwtEvents) { CreateSignedToken(); _audience = "wrongAudience"; SetupOidcDiscovery(); + _enableJwtEvents = enableJwtEvents; using var testServer = CreateTestServer(); await TestGetAsync(testServer, isAuthenticated: false); await TestWebSocketAsync(testServer, isAuthenticated: false); } - [Fact] - public async Task Expired() + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task Expired(bool enableJwtEvents) { CreateSignedToken(expired: true); SetupOidcDiscovery(); + _enableJwtEvents = enableJwtEvents; using var testServer = CreateTestServer(); await TestGetAsync(testServer, isAuthenticated: false); await TestWebSocketAsync(testServer, isAuthenticated: false); } - [Fact] - public async Task NoDefaultScheme() + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task NoDefaultScheme(bool enableJwtEvents) { CreateSignedToken(); SetupOidcDiscovery(); + _enableJwtEvents = enableJwtEvents; using var testServer = CreateTestServer(defaultScheme: false); await TestGetAsync(testServer, isAuthenticated: false); - await TestWebSocketAsync(testServer, isAuthenticated: false); + await TestWebSocketAsync(testServer, isAuthenticated: false, + expectMessageReceived: false, expectAuthenticationFailed: false); } - [Fact] - public async Task NoDefaultSchemeSpecified() + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task NoDefaultSchemeSpecified(bool enableJwtEvents) { CreateSignedToken(); SetupOidcDiscovery(); + _enableJwtEvents = enableJwtEvents; using var testServer = CreateTestServer(defaultScheme: false, specifyScheme: true); await TestGetAsync(testServer, isAuthenticated: true); await TestWebSocketAsync(testServer, isAuthenticated: true); } - [Fact] - public async Task CustomScheme() + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task CustomScheme(bool enableJwtEvents) { CreateSignedToken(); SetupOidcDiscovery(); + _enableJwtEvents = enableJwtEvents; using var testServer = CreateTestServer(customScheme: true); await TestGetAsync(testServer, isAuthenticated: true); await TestWebSocketAsync(testServer, isAuthenticated: true); } - [Fact] - public async Task CustomNoDefaultScheme() + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task CustomNoDefaultScheme(bool enableJwtEvents) { CreateSignedToken(); SetupOidcDiscovery(); + _enableJwtEvents = enableJwtEvents; using var testServer = CreateTestServer(customScheme: true, defaultScheme: false); await TestGetAsync(testServer, isAuthenticated: false); - await TestWebSocketAsync(testServer, isAuthenticated: false); + await TestWebSocketAsync(testServer, isAuthenticated: false, + expectMessageReceived: false, expectAuthenticationFailed: false); } - [Fact] - public async Task CustomNoDefaultSchemeSpecified() + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task CustomNoDefaultSchemeSpecified(bool enableJwtEvents) { CreateSignedToken(); SetupOidcDiscovery(); + _enableJwtEvents = enableJwtEvents; using var testServer = CreateTestServer(customScheme: true, defaultScheme: false, specifyScheme: true); await TestGetAsync(testServer, isAuthenticated: true); await TestWebSocketAsync(testServer, isAuthenticated: true); } - [Fact] - public async Task WrongScheme() + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task WrongScheme(bool enableJwtEvents) { CreateSignedToken(); SetupOidcDiscovery(); + _enableJwtEvents = enableJwtEvents; using var testServer = CreateTestServer(specifyInvalidScheme: true); await TestGetAsync(testServer, isAuthenticated: false); - await TestWebSocketAsync(testServer, isAuthenticated: false); + await TestWebSocketAsync(testServer, isAuthenticated: false, + expectMessageReceived: true, expectAuthenticationFailed: false, expectTokenValidated: true); } - [Fact] - public async Task MultipleSchemes() + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task MultipleSchemes(bool enableJwtEvents) { CreateSignedToken(); SetupOidcDiscovery(); + _enableJwtEvents = enableJwtEvents; using var testServer = CreateTestServer(specifyInvalidScheme: true, specifyScheme: true, defaultScheme: false); await TestGetAsync(testServer, isAuthenticated: true); await TestWebSocketAsync(testServer, isAuthenticated: true); } - [Fact] - public async Task NoToken() + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task NoToken(bool enableJwtEvents) { CreateSignedToken(); SetupOidcDiscovery(); + _enableJwtEvents = enableJwtEvents; using var testServer = CreateTestServer(); _jwtAccessToken = null; await TestGetAsync(testServer, isAuthenticated: false); - await TestWebSocketAsync(testServer, isAuthenticated: false); + await TestWebSocketAsync(testServer, isAuthenticated: false, + expectMessageReceived: true, expectAuthenticationFailed: false); } private async Task TestGetAsync(TestServer testServer, bool isAuthenticated) @@ -182,7 +288,8 @@ private async Task TestGetAsync(TestServer testServer, bool isAuthenticated) } } - private async Task TestWebSocketAsync(TestServer testServer, bool isAuthenticated) + private async Task TestWebSocketAsync(TestServer testServer, bool isAuthenticated, + bool expectMessageReceived = true, bool expectAuthenticationFailed = true, bool expectTokenValidated = false) { // test an authenticated request var webSocketClient = testServer.CreateWebSocketClient(); @@ -208,6 +315,15 @@ await webSocket.SendMessageAsync(new OperationMessage { // wait for websocket closure (await webSocket.ReceiveCloseAsync()).ShouldBe((WebSocketCloseStatus)4401); + + // Verify events were triggered if _enableJwtEvents is true + if (_enableJwtEvents) + { + _messageReceived.ShouldBe(expectMessageReceived); + _authenticationFailed.ShouldBe(expectAuthenticationFailed); + _tokenValidated.ShouldBe(expectTokenValidated); + } + return; } @@ -231,6 +347,14 @@ await webSocket.SendMessageAsync(new OperationMessage { message.Payload.ShouldBe($$$""" {"data":{"test":"{{{_subject}}}"}} """); + + // Verify events were triggered if _enableJwtEvents is true + if (_enableJwtEvents) + { + _messageReceived.ShouldBeTrue(); + _tokenValidated.ShouldBeTrue(); + _authenticationFailed.ShouldBeFalse(); + } } /// @@ -249,11 +373,17 @@ private TestServer CreateTestServer(bool defaultScheme = true, bool customScheme o.Authority = _issuer; o.Audience = _audience; o.BackchannelHttpHandler = _oidcHttpMessageHandler; + + // Configure JWT events if enabled + if (_enableJwtEvents) + { + o.Events = _jwtBearerEvents; + } }); services.AddGraphQL(b => b .AddSchema(_schema) .AddSystemTextJson() - .AddJwtBearerAuthentication(true) + .AddJwtBearerAuthentication(_enableJwtEvents) ); }) .Configure(app => { From c2db1719fa349824a828b91881fe4e1b2fccf9e3 Mon Sep 17 00:00:00 2001 From: Shane Krueger Date: Wed, 7 May 2025 18:06:59 -0400 Subject: [PATCH 10/13] update --- .../JwtWebSocketAuthenticationServiceTests.cs | 40 ++++++++----------- 1 file changed, 16 insertions(+), 24 deletions(-) diff --git a/src/Tests/JwtBearer/JwtWebSocketAuthenticationServiceTests.cs b/src/Tests/JwtBearer/JwtWebSocketAuthenticationServiceTests.cs index 0ab5339..66d2959 100644 --- a/src/Tests/JwtBearer/JwtWebSocketAuthenticationServiceTests.cs +++ b/src/Tests/JwtBearer/JwtWebSocketAuthenticationServiceTests.cs @@ -21,47 +21,42 @@ public class JwtWebSocketAuthenticationServiceTests private string? _jwtAccessToken; private readonly MockHttpMessageHandler _oidcHttpMessageHandler = new(); private readonly ISchema _schema; - + // Event tracking flags private bool _messageReceived; private bool _tokenValidated; private bool _authenticationFailed; private bool _enableJwtEvents; - + private readonly JwtBearerEvents _jwtBearerEvents; private Action? _testFieldAction; public JwtWebSocketAuthenticationServiceTests() { var query = new ObjectGraphType() { Name = "Query" }; - query.Field("test").Resolve(ctx => - { + query.Field("test").Resolve(ctx => { _testFieldAction?.Invoke(ctx); return ctx.User?.FindFirst(ClaimTypes.NameIdentifier)?.Value; }); _schema = new Schema { Query = query }; - + // Initialize JwtBearerEvents - _jwtBearerEvents = new JwtBearerEvents - { - OnMessageReceived = context => - { + _jwtBearerEvents = new JwtBearerEvents { + OnMessageReceived = context => { _messageReceived = true; return Task.CompletedTask; }, - OnTokenValidated = context => - { + OnTokenValidated = context => { _tokenValidated = true; return Task.CompletedTask; }, - OnAuthenticationFailed = context => - { + OnAuthenticationFailed = context => { _authenticationFailed = true; return Task.CompletedTask; } }; } - + [Theory] [InlineData(true)] [InlineData(false)] @@ -315,15 +310,14 @@ await webSocket.SendMessageAsync(new OperationMessage { // wait for websocket closure (await webSocket.ReceiveCloseAsync()).ShouldBe((WebSocketCloseStatus)4401); - + // Verify events were triggered if _enableJwtEvents is true - if (_enableJwtEvents) - { + if (_enableJwtEvents) { _messageReceived.ShouldBe(expectMessageReceived); _authenticationFailed.ShouldBe(expectAuthenticationFailed); _tokenValidated.ShouldBe(expectTokenValidated); } - + return; } @@ -347,10 +341,9 @@ await webSocket.SendMessageAsync(new OperationMessage { message.Payload.ShouldBe($$$""" {"data":{"test":"{{{_subject}}}"}} """); - + // Verify events were triggered if _enableJwtEvents is true - if (_enableJwtEvents) - { + if (_enableJwtEvents) { _messageReceived.ShouldBeTrue(); _tokenValidated.ShouldBeTrue(); _authenticationFailed.ShouldBeFalse(); @@ -373,10 +366,9 @@ private TestServer CreateTestServer(bool defaultScheme = true, bool customScheme o.Authority = _issuer; o.Audience = _audience; o.BackchannelHttpHandler = _oidcHttpMessageHandler; - + // Configure JWT events if enabled - if (_enableJwtEvents) - { + if (_enableJwtEvents) { o.Events = _jwtBearerEvents; } }); From a79237b72aa941bd3ae6143e3d116eb4ab57808a Mon Sep 17 00:00:00 2001 From: Shane Krueger Date: Wed, 7 May 2025 18:10:10 -0400 Subject: [PATCH 11/13] update --- src/Tests/JwtBearer/JwtWebSocketAuthenticationServiceTests.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Tests/JwtBearer/JwtWebSocketAuthenticationServiceTests.cs b/src/Tests/JwtBearer/JwtWebSocketAuthenticationServiceTests.cs index 66d2959..bdc67cf 100644 --- a/src/Tests/JwtBearer/JwtWebSocketAuthenticationServiceTests.cs +++ b/src/Tests/JwtBearer/JwtWebSocketAuthenticationServiceTests.cs @@ -70,7 +70,6 @@ public async Task SuccessfulAuthentication(bool enableJwtEvents) await TestWebSocketAsync(testServer, isAuthenticated: true); } - [Fact] public async Task SuccessfulAuthenticationWithCustomClaim() { From c3241a80d6b0e30fc6cf3d1c3df7269bad1f8b05 Mon Sep 17 00:00:00 2001 From: Shane Krueger Date: Wed, 7 May 2025 18:34:39 -0400 Subject: [PATCH 12/13] update --- .../JwtWebSocketAuthenticationService.cs | 8 +++++++ .../JwtWebSocketAuthenticationServiceTests.cs | 23 +++++++++++++------ 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/src/GraphQL.AspNetCore3.JwtBearer/JwtWebSocketAuthenticationService.cs b/src/GraphQL.AspNetCore3.JwtBearer/JwtWebSocketAuthenticationService.cs index 89f5130..869bce6 100644 --- a/src/GraphQL.AspNetCore3.JwtBearer/JwtWebSocketAuthenticationService.cs +++ b/src/GraphQL.AspNetCore3.JwtBearer/JwtWebSocketAuthenticationService.cs @@ -129,6 +129,14 @@ public async Task AuthenticateAsync(AuthenticationRequest authenticationRequest) // set the ClaimsPrincipal for the HttpContext; authentication will take place against this object connection.HttpContext.User = principal; return; + } else if (_jwtBearerAuthenticationOptions.EnableJwtEvents) { + // If JWT events are enabled, trigger the AuthenticationFailed event + var exception = tokenValidationResult.Exception ?? new SecurityTokenValidationException($"The TokenHandler: '{tokenHandler}', was unable to validate the Token."); + var failedResult = await TriggerAuthenticationFailedEventAsync(connection.HttpContext, options, exception, scheme).ConfigureAwait(false); + if (failedResult.Handled && failedResult.Success) { + connection.HttpContext.User = failedResult.Principal!; + return; + } } } catch (Exception ex) { // If JWT events are enabled, trigger the AuthenticationFailed event diff --git a/src/Tests/JwtBearer/JwtWebSocketAuthenticationServiceTests.cs b/src/Tests/JwtBearer/JwtWebSocketAuthenticationServiceTests.cs index bdc67cf..9d1d126 100644 --- a/src/Tests/JwtBearer/JwtWebSocketAuthenticationServiceTests.cs +++ b/src/Tests/JwtBearer/JwtWebSocketAuthenticationServiceTests.cs @@ -232,7 +232,7 @@ public async Task WrongScheme(bool enableJwtEvents) using var testServer = CreateTestServer(specifyInvalidScheme: true); await TestGetAsync(testServer, isAuthenticated: false); await TestWebSocketAsync(testServer, isAuthenticated: false, - expectMessageReceived: true, expectAuthenticationFailed: false, expectTokenValidated: true); + expectMessageReceived: false, expectAuthenticationFailed: false); } [Theory] @@ -260,7 +260,7 @@ public async Task NoToken(bool enableJwtEvents) _jwtAccessToken = null; await TestGetAsync(testServer, isAuthenticated: false); await TestWebSocketAsync(testServer, isAuthenticated: false, - expectMessageReceived: true, expectAuthenticationFailed: false); + expectMessageReceived: false, expectAuthenticationFailed: false); } private async Task TestGetAsync(TestServer testServer, bool isAuthenticated) @@ -293,6 +293,11 @@ private async Task TestWebSocketAsync(TestServer testServer, bool isAuthenticate webSocketClient.SubProtocols.Add("graphql-ws"); using var webSocket = await webSocketClient.ConnectAsync(new Uri(testServer.BaseAddress, "/graphql"), default); + // reset event tracking flags after initial connection has been made (as messageReceived is called on connection by the ASP.NET Core pipeline) + _messageReceived = false; + _authenticationFailed = false; + _tokenValidated = false; + // send CONNECTION_INIT await webSocket.SendMessageAsync(new OperationMessage { Type = "connection_init", @@ -315,6 +320,10 @@ await webSocket.SendMessageAsync(new OperationMessage { _messageReceived.ShouldBe(expectMessageReceived); _authenticationFailed.ShouldBe(expectAuthenticationFailed); _tokenValidated.ShouldBe(expectTokenValidated); + } else { + _messageReceived.ShouldBeFalse(); + _authenticationFailed.ShouldBeFalse(); + _tokenValidated.ShouldBeFalse(); } return; @@ -346,6 +355,10 @@ await webSocket.SendMessageAsync(new OperationMessage { _messageReceived.ShouldBeTrue(); _tokenValidated.ShouldBeTrue(); _authenticationFailed.ShouldBeFalse(); + } else { + _messageReceived.ShouldBeFalse(); + _tokenValidated.ShouldBeFalse(); + _authenticationFailed.ShouldBeFalse(); } } @@ -365,11 +378,7 @@ private TestServer CreateTestServer(bool defaultScheme = true, bool customScheme o.Authority = _issuer; o.Audience = _audience; o.BackchannelHttpHandler = _oidcHttpMessageHandler; - - // Configure JWT events if enabled - if (_enableJwtEvents) { - o.Events = _jwtBearerEvents; - } + o.Events = _jwtBearerEvents; }); services.AddGraphQL(b => b .AddSchema(_schema) From a5d250318bf385b52ce3d0468cef57bddc22db56 Mon Sep 17 00:00:00 2001 From: Shane Krueger Date: Wed, 7 May 2025 19:44:18 -0400 Subject: [PATCH 13/13] try creating rsa parameters once --- .../JwtWebSocketAuthenticationServiceTests.cs | 28 ++++++++++++++----- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/src/Tests/JwtBearer/JwtWebSocketAuthenticationServiceTests.cs b/src/Tests/JwtBearer/JwtWebSocketAuthenticationServiceTests.cs index 9d1d126..d61e35f 100644 --- a/src/Tests/JwtBearer/JwtWebSocketAuthenticationServiceTests.cs +++ b/src/Tests/JwtBearer/JwtWebSocketAuthenticationServiceTests.cs @@ -17,7 +17,7 @@ public class JwtWebSocketAuthenticationServiceTests private string _issuer = "https://demo.identityserver.io"; private string _audience = "testAudience"; private readonly string _subject = "user123"; - private RSAParameters _rsaParameters; + private static readonly RSAParameters _rsaParameters; private string? _jwtAccessToken; private readonly MockHttpMessageHandler _oidcHttpMessageHandler = new(); private readonly ISchema _schema; @@ -108,7 +108,7 @@ public async Task WrongKeys(bool enableJwtEvents) SetupOidcDiscovery(); _enableJwtEvents = enableJwtEvents; using var testServer = CreateTestServer(); - CreateSignedToken(); // create new token with different keys + CreateSignedToken(differentKeys: true); // create new token with different keys await TestGetAsync(testServer, isAuthenticated: false); await TestWebSocketAsync(testServer, isAuthenticated: false); } @@ -454,14 +454,29 @@ private void SetupOidcDiscovery() } /// - /// Creates a new RSA key pair and a signed JWT token. + /// Creates a new RSA key pair. + /// + /// + /// .NET Framework can only handle around 50 RSA keys at a time. + /// + static JwtWebSocketAuthenticationServiceTests() + { + using var rsa = RSA.Create(2048); + _rsaParameters = rsa.ExportParameters(true); + } + + /// + /// Creates a signed JWT token. /// Uses the currently configured , , and . /// Overwrites the and fields. /// - private void CreateSignedToken(bool expired = false) + private void CreateSignedToken(bool expired = false, bool differentKeys = false) { - using var rsa = RSA.Create(2048); - var rsaParameters = rsa.ExportParameters(true); + RSAParameters rsaParameters = _rsaParameters; + if (differentKeys) { + using var rsa = RSA.Create(2048); + rsaParameters = rsa.ExportParameters(true); + } var key = new RsaSecurityKey(rsaParameters); var signingCredentials = new SigningCredentials(key, SecurityAlgorithms.RsaSha256); @@ -481,7 +496,6 @@ private void CreateSignedToken(bool expired = false) var tokenHandler = new JwtSecurityTokenHandler(); var token = tokenHandler.CreateToken(tokenDescriptor); var tokenStr = tokenHandler.WriteToken(token); - _rsaParameters = rsaParameters; _jwtAccessToken = tokenStr; } }