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