Skip to content

Commit

Permalink
Update JWT sample code for subscriptions (#1171)
Browse files Browse the repository at this point in the history
  • Loading branch information
Shane32 authored Nov 21, 2024
1 parent e01e46b commit a530f4b
Showing 1 changed file with 97 additions and 15 deletions.
112 changes: 97 additions & 15 deletions samples/Samples.Jwt/JwtWebSocketAuthenticationService.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -15,59 +22,134 @@ namespace GraphQL.Server.Samples.Jwt;
/// <list type="bullet">
/// <item>This class is not used when authenticating over GET/POST.</item>
/// <item>
/// This class pulls the <see cref="TokenValidationParameters"/> instance from the instance of
/// <see cref="IOptionsMonitor{TOptions}">IOptionsMonitor</see>&lt;<see cref="JwtBearerOptions"/>&gt; registered
/// by ASP.NET Core during the call to <see cref="JwtBearerExtensions.AddJwtBearer(Microsoft.AspNetCore.Authentication.AuthenticationBuilder, Action{JwtBearerOptions})">AddJwtBearer</see>.
/// This class pulls the <see cref="JwtBearerOptions"/> instance registered by ASP.NET Core during the call to
/// <see cref="JwtBearerExtensions.AddJwtBearer(Microsoft.AspNetCore.Authentication.AuthenticationBuilder, Action{JwtBearerOptions})">AddJwtBearer</see>
/// for the <see cref="JwtBearerDefaults.AuthenticationScheme">Bearer</see> 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>
/// This implementation only supports the "Bearer" scheme configured in ASP.NET Core. Any scheme configured via
/// <see cref="Transports.AspNetCore.GraphQLHttpMiddlewareOptions.AuthenticationSchemes"/> property is
/// ignored by this implementation.
/// </item>
/// <item>
/// Events configured in <see cref="JwtBearerOptions.Events"/> are not raised by this implementation.
/// </item>
/// </list>
/// </summary>
public class JwtWebSocketAuthenticationService : IWebSocketAuthenticationService
{
private readonly IGraphQLSerializer _graphQLSerializer;
private readonly IOptionsMonitor<JwtBearerOptions> _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<JwtBearerOptions> 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<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);
// 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
{
// 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<TokenValidationParameters> 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
Expand Down

0 comments on commit a530f4b

Please sign in to comment.