Skip to content

Commit a530f4b

Browse files
authored
Update JWT sample code for subscriptions (#1171)
1 parent e01e46b commit a530f4b

File tree

1 file changed

+97
-15
lines changed

1 file changed

+97
-15
lines changed

samples/Samples.Jwt/JwtWebSocketAuthenticationService.cs

Lines changed: 97 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
// Parts of this code file are based on the JwtBearerHandler class in the Microsoft.AspNetCore.Authentication.JwtBearer package found at:
2+
// https://github.com/dotnet/aspnetcore/blob/5493b413d1df3aaf00651bdf1cbd8135fa63f517/src/Security/Authentication/JwtBearer/src/JwtBearerHandler.cs
3+
//
4+
// Those sections of code may be subject to the MIT license found at:
5+
// https://github.com/dotnet/aspnetcore/blob/5493b413d1df3aaf00651bdf1cbd8135fa63f517/LICENSE.txt
6+
7+
using System.Security.Claims;
18
using GraphQL.Server.Transports.AspNetCore.WebSockets;
29
using GraphQL.Transport;
310
using Microsoft.AspNetCore.Authentication.JwtBearer;
@@ -15,59 +22,134 @@ namespace GraphQL.Server.Samples.Jwt;
1522
/// <list type="bullet">
1623
/// <item>This class is not used when authenticating over GET/POST.</item>
1724
/// <item>
18-
/// This class pulls the <see cref="TokenValidationParameters"/> instance from the instance of
19-
/// <see cref="IOptionsMonitor{TOptions}">IOptionsMonitor</see>&lt;<see cref="JwtBearerOptions"/>&gt; registered
20-
/// by ASP.NET Core during the call to <see cref="JwtBearerExtensions.AddJwtBearer(Microsoft.AspNetCore.Authentication.AuthenticationBuilder, Action{JwtBearerOptions})">AddJwtBearer</see>.
25+
/// This class pulls the <see cref="JwtBearerOptions"/> instance registered by ASP.NET Core during the call to
26+
/// <see cref="JwtBearerExtensions.AddJwtBearer(Microsoft.AspNetCore.Authentication.AuthenticationBuilder, Action{JwtBearerOptions})">AddJwtBearer</see>
27+
/// for the <see cref="JwtBearerDefaults.AuthenticationScheme">Bearer</see> scheme and authenticates the token
28+
/// based on simplified logic used by <see cref="JwtBearerHandler"/>.
2129
/// </item>
2230
/// <item>
2331
/// The expected format of the payload is <c>{"Authorization":"Bearer TOKEN"}</c> where TOKEN is the JSON Web Token (JWT),
2432
/// mirroring the format of the 'Authorization' HTTP header.
2533
/// </item>
34+
/// <item>
35+
/// This implementation only supports the "Bearer" scheme configured in ASP.NET Core. Any scheme configured via
36+
/// <see cref="Transports.AspNetCore.GraphQLHttpMiddlewareOptions.AuthenticationSchemes"/> property is
37+
/// ignored by this implementation.
38+
/// </item>
39+
/// <item>
40+
/// Events configured in <see cref="JwtBearerOptions.Events"/> are not raised by this implementation.
41+
/// </item>
2642
/// </list>
2743
/// </summary>
2844
public class JwtWebSocketAuthenticationService : IWebSocketAuthenticationService
2945
{
3046
private readonly IGraphQLSerializer _graphQLSerializer;
3147
private readonly IOptionsMonitor<JwtBearerOptions> _jwtBearerOptionsMonitor;
3248

49+
// This implementation currently only supports the "Bearer" scheme configured in ASP.NET Core
50+
private static string _scheme => JwtBearerDefaults.AuthenticationScheme;
51+
3352
public JwtWebSocketAuthenticationService(IGraphQLSerializer graphQLSerializer, IOptionsMonitor<JwtBearerOptions> jwtBearerOptionsMonitor)
3453
{
3554
_graphQLSerializer = graphQLSerializer;
3655
_jwtBearerOptionsMonitor = jwtBearerOptionsMonitor;
3756
}
3857

39-
public Task AuthenticateAsync(IWebSocketConnection connection, string subProtocol, OperationMessage operationMessage)
58+
public async Task AuthenticateAsync(IWebSocketConnection connection, string subProtocol, OperationMessage operationMessage)
4059
{
4160
try
4261
{
4362
// for connections authenticated via HTTP headers, no need to reauthenticate
4463
if (connection.HttpContext.User.Identity?.IsAuthenticated ?? false)
45-
return Task.CompletedTask;
64+
return;
4665

4766
// attempt to read the 'Authorization' key from the payload object and verify it contains "Bearer XXXXXXXX"
4867
var authPayload = _graphQLSerializer.ReadNode<AuthPayload>(operationMessage.Payload);
4968
if (authPayload != null && authPayload.Authorization != null && authPayload.Authorization.StartsWith("Bearer ", StringComparison.Ordinal))
5069
{
5170
// pull the token from the value
5271
var token = authPayload.Authorization.Substring(7);
53-
// parse the token in the same manner that the .NET AddJwtBearer() method does:
54-
// JwtSecurityTokenHandler maps the 'name' and 'role' claims to the 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name'
55-
// and 'http://schemas.microsoft.com/ws/2008/06/identity/claims/role' claims;
56-
// this mapping is not performed by Microsoft.IdentityModel.JsonWebTokens.JsonWebTokenHandler
57-
var handler = new System.IdentityModel.Tokens.Jwt.JwtSecurityTokenHandler();
58-
var tokenValidationParameters = _jwtBearerOptionsMonitor.Get(JwtBearerDefaults.AuthenticationScheme).TokenValidationParameters;
59-
var principal = handler.ValidateToken(token, tokenValidationParameters, out var securityToken);
60-
// set the ClaimsPrincipal for the HttpContext; authentication will take place against this object
61-
connection.HttpContext.User = principal;
72+
73+
var options = _jwtBearerOptionsMonitor.Get(_scheme);
74+
75+
// follow logic simplified from JwtBearerHandler.HandleAuthenticateAsync, as follows:
76+
var tokenValidationParameters = await SetupTokenValidationParametersAsync(options, connection.HttpContext).ConfigureAwait(false);
77+
if (!options.UseSecurityTokenValidators)
78+
{
79+
foreach (var tokenHandler in options.TokenHandlers)
80+
{
81+
try
82+
{
83+
var tokenValidationResult = await tokenHandler.ValidateTokenAsync(token, tokenValidationParameters);
84+
if (tokenValidationResult.IsValid)
85+
{
86+
var principal = new ClaimsPrincipal(tokenValidationResult.ClaimsIdentity);
87+
// set the ClaimsPrincipal for the HttpContext; authentication will take place against this object
88+
connection.HttpContext.User = principal;
89+
return;
90+
}
91+
}
92+
catch
93+
{
94+
// no errors during authentication should throw an exception
95+
// 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
96+
}
97+
}
98+
}
99+
else
100+
{
101+
#pragma warning disable CS0618 // Type or member is obsolete
102+
foreach (var validator in options.SecurityTokenValidators)
103+
{
104+
if (validator.CanReadToken(token))
105+
{
106+
try
107+
{
108+
var principal = validator.ValidateToken(token, tokenValidationParameters, out _);
109+
// set the ClaimsPrincipal for the HttpContext; authentication will take place against this object
110+
connection.HttpContext.User = principal;
111+
return;
112+
}
113+
catch
114+
{
115+
// no errors during authentication should throw an exception
116+
// 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
117+
}
118+
}
119+
}
120+
#pragma warning restore CS0618 // Type or member is obsolete
121+
}
62122
}
63123
}
64124
catch
65125
{
66126
// no errors during authentication should throw an exception
67127
// 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
68128
}
129+
}
130+
131+
private static async ValueTask<TokenValidationParameters> SetupTokenValidationParametersAsync(JwtBearerOptions options, HttpContext httpContext)
132+
{
133+
// Clone to avoid cross request race conditions for updated configurations.
134+
var tokenValidationParameters = options.TokenValidationParameters.Clone();
135+
136+
if (options.ConfigurationManager is BaseConfigurationManager baseConfigurationManager)
137+
{
138+
tokenValidationParameters.ConfigurationManager = baseConfigurationManager;
139+
}
140+
else
141+
{
142+
if (options.ConfigurationManager != null)
143+
{
144+
// GetConfigurationAsync has a time interval that must pass before new http request will be issued.
145+
var configuration = await options.ConfigurationManager.GetConfigurationAsync(httpContext.RequestAborted);
146+
var issuers = new[] { configuration.Issuer };
147+
tokenValidationParameters.ValidIssuers = (tokenValidationParameters.ValidIssuers == null ? issuers : tokenValidationParameters.ValidIssuers.Concat(issuers));
148+
tokenValidationParameters.IssuerSigningKeys = (tokenValidationParameters.IssuerSigningKeys == null ? configuration.SigningKeys : tokenValidationParameters.IssuerSigningKeys.Concat(configuration.SigningKeys));
149+
}
150+
}
69151

70-
return Task.CompletedTask;
152+
return tokenValidationParameters;
71153
}
72154

73155
private class AuthPayload

0 commit comments

Comments
 (0)