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 ;
1
8
using GraphQL . Server . Transports . AspNetCore . WebSockets ;
2
9
using GraphQL . Transport ;
3
10
using Microsoft . AspNetCore . Authentication . JwtBearer ;
@@ -15,59 +22,134 @@ namespace GraphQL.Server.Samples.Jwt;
15
22
/// <list type="bullet">
16
23
/// <item>This class is not used when authenticating over GET/POST.</item>
17
24
/// <item>
18
- /// This class pulls the <see cref="TokenValidationParameters"/> instance from the instance of
19
- /// <see cref="IOptionsMonitor{TOptions}">IOptionsMonitor</see><<see cref="JwtBearerOptions"/>> 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"/>.
21
29
/// </item>
22
30
/// <item>
23
31
/// The expected format of the payload is <c>{"Authorization":"Bearer TOKEN"}</c> where TOKEN is the JSON Web Token (JWT),
24
32
/// mirroring the format of the 'Authorization' HTTP header.
25
33
/// </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>
26
42
/// </list>
27
43
/// </summary>
28
44
public class JwtWebSocketAuthenticationService : IWebSocketAuthenticationService
29
45
{
30
46
private readonly IGraphQLSerializer _graphQLSerializer ;
31
47
private readonly IOptionsMonitor < JwtBearerOptions > _jwtBearerOptionsMonitor ;
32
48
49
+ // This implementation currently only supports the "Bearer" scheme configured in ASP.NET Core
50
+ private static string _scheme => JwtBearerDefaults . AuthenticationScheme ;
51
+
33
52
public JwtWebSocketAuthenticationService ( IGraphQLSerializer graphQLSerializer , IOptionsMonitor < JwtBearerOptions > jwtBearerOptionsMonitor )
34
53
{
35
54
_graphQLSerializer = graphQLSerializer ;
36
55
_jwtBearerOptionsMonitor = jwtBearerOptionsMonitor ;
37
56
}
38
57
39
- public Task AuthenticateAsync ( IWebSocketConnection connection , string subProtocol , OperationMessage operationMessage )
58
+ public async Task AuthenticateAsync ( IWebSocketConnection connection , string subProtocol , OperationMessage operationMessage )
40
59
{
41
60
try
42
61
{
43
62
// for connections authenticated via HTTP headers, no need to reauthenticate
44
63
if ( connection . HttpContext . User . Identity ? . IsAuthenticated ?? false )
45
- return Task . CompletedTask ;
64
+ return ;
46
65
47
66
// attempt to read the 'Authorization' key from the payload object and verify it contains "Bearer XXXXXXXX"
48
67
var authPayload = _graphQLSerializer . ReadNode < AuthPayload > ( operationMessage . Payload ) ;
49
68
if ( authPayload != null && authPayload . Authorization != null && authPayload . Authorization . StartsWith ( "Bearer " , StringComparison . Ordinal ) )
50
69
{
51
70
// pull the token from the value
52
71
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
+ }
62
122
}
63
123
}
64
124
catch
65
125
{
66
126
// no errors during authentication should throw an exception
67
127
// 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
68
128
}
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
+ }
69
151
70
- return Task . CompletedTask ;
152
+ return tokenValidationParameters ;
71
153
}
72
154
73
155
private class AuthPayload
0 commit comments