diff --git a/samples/Samples.Jwt/JwtWebSocketAuthenticationService.cs b/samples/Samples.Jwt/JwtWebSocketAuthenticationService.cs
index 71dead28..7b52b8d0 100644
--- a/samples/Samples.Jwt/JwtWebSocketAuthenticationService.cs
+++ b/samples/Samples.Jwt/JwtWebSocketAuthenticationService.cs
@@ -1,3 +1,10 @@
+// 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.Server.Transports.AspNetCore.WebSockets;
using GraphQL.Transport;
using Microsoft.AspNetCore.Authentication.JwtBearer;
@@ -15,14 +22,23 @@ namespace GraphQL.Server.Samples.Jwt;
///
/// - This class is not used when authenticating over GET/POST.
/// -
-/// This class pulls the instance from the instance of
-/// IOptionsMonitor<> registered
-/// by ASP.NET Core during the call to AddJwtBearer.
+/// This class pulls the instance registered by ASP.NET Core during the call to
+/// AddJwtBearer
+/// for the Bearer scheme and authenticates the token
+/// based on simplified logic used by .
///
/// -
/// The expected format of the payload is {"Authorization":"Bearer TOKEN"} where TOKEN is the JSON Web Token (JWT),
/// mirroring the format of the 'Authorization' HTTP header.
///
+/// -
+/// This implementation only supports the "Bearer" scheme configured in ASP.NET Core. Any scheme configured via
+/// property is
+/// ignored by this implementation.
+///
+/// -
+/// Events configured in are not raised by this implementation.
+///
///
///
public class JwtWebSocketAuthenticationService : IWebSocketAuthenticationService
@@ -30,19 +46,22 @@ public class JwtWebSocketAuthenticationService : IWebSocketAuthenticationService
private readonly IGraphQLSerializer _graphQLSerializer;
private readonly IOptionsMonitor _jwtBearerOptionsMonitor;
+ // This implementation currently only supports the "Bearer" scheme configured in ASP.NET Core
+ private static string _scheme => JwtBearerDefaults.AuthenticationScheme;
+
public JwtWebSocketAuthenticationService(IGraphQLSerializer graphQLSerializer, IOptionsMonitor jwtBearerOptionsMonitor)
{
_graphQLSerializer = graphQLSerializer;
_jwtBearerOptionsMonitor = jwtBearerOptionsMonitor;
}
- public Task AuthenticateAsync(IWebSocketConnection connection, string subProtocol, OperationMessage operationMessage)
+ public async Task AuthenticateAsync(IWebSocketConnection connection, string subProtocol, OperationMessage operationMessage)
{
try
{
// for connections authenticated via HTTP headers, no need to reauthenticate
if (connection.HttpContext.User.Identity?.IsAuthenticated ?? false)
- return Task.CompletedTask;
+ return;
// attempt to read the 'Authorization' key from the payload object and verify it contains "Bearer XXXXXXXX"
var authPayload = _graphQLSerializer.ReadNode(operationMessage.Payload);
@@ -50,15 +69,56 @@ public Task AuthenticateAsync(IWebSocketConnection connection, string subProtoco
{
// pull the token from the value
var token = authPayload.Authorization.Substring(7);
- // parse the token in the same manner that the .NET AddJwtBearer() method does:
- // JwtSecurityTokenHandler maps the 'name' and 'role' claims to the 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name'
- // and 'http://schemas.microsoft.com/ws/2008/06/identity/claims/role' claims;
- // this mapping is not performed by Microsoft.IdentityModel.JsonWebTokens.JsonWebTokenHandler
- var handler = new System.IdentityModel.Tokens.Jwt.JwtSecurityTokenHandler();
- var tokenValidationParameters = _jwtBearerOptionsMonitor.Get(JwtBearerDefaults.AuthenticationScheme).TokenValidationParameters;
- var principal = handler.ValidateToken(token, tokenValidationParameters, out var securityToken);
- // set the ClaimsPrincipal for the HttpContext; authentication will take place against this object
- connection.HttpContext.User = principal;
+
+ var options = _jwtBearerOptionsMonitor.Get(_scheme);
+
+ // follow logic simplified from JwtBearerHandler.HandleAuthenticateAsync, as follows:
+ var tokenValidationParameters = await SetupTokenValidationParametersAsync(options, connection.HttpContext).ConfigureAwait(false);
+ if (!options.UseSecurityTokenValidators)
+ {
+ foreach (var tokenHandler in options.TokenHandlers)
+ {
+ try
+ {
+ var tokenValidationResult = await tokenHandler.ValidateTokenAsync(token, tokenValidationParameters);
+ 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 will result in an exception, which may be logged or simply ignored to not generate an inordinate amount of logs without purpose
+ }
+ }
+ }
+ else
+ {
+#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, which may be logged or simply ignored to not generate an inordinate amount of logs without purpose
+ }
+ }
+ }
+#pragma warning restore CS0618 // Type or member is obsolete
+ }
}
}
catch
@@ -66,8 +126,30 @@ public Task AuthenticateAsync(IWebSocketConnection connection, string subProtoco
// no errors during authentication should throw an exception
// specifically, attempting to validate an invalid JWT token will result in an exception, which may be logged or simply ignored to not generate an inordinate amount of logs without purpose
}
+ }
+
+ private static async ValueTask SetupTokenValidationParametersAsync(JwtBearerOptions options, HttpContext httpContext)
+ {
+ // Clone to avoid cross request race conditions for updated configurations.
+ var tokenValidationParameters = options.TokenValidationParameters.Clone();
+
+ if (options.ConfigurationManager is BaseConfigurationManager baseConfigurationManager)
+ {
+ tokenValidationParameters.ConfigurationManager = baseConfigurationManager;
+ }
+ else
+ {
+ 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);
+ 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 Task.CompletedTask;
+ return tokenValidationParameters;
}
private class AuthPayload