@@ -34,7 +34,9 @@ namespace GraphQL.AspNetCore3.JwtBearer;
3434/// mirroring the format of the 'Authorization' HTTP header.
3535/// </item>
3636/// <item>
37- /// Events configured in <see cref="JwtBearerOptions.Events"/> are not raised by this implementation.
37+ /// When JWT events are enabled via <see cref="JwtBearerAuthenticationOptions.EnableJwtEvents"/>, this implementation
38+ /// will raise the <see cref="JwtBearerEvents.MessageReceived"/>, <see cref="JwtBearerEvents.TokenValidated"/>,
39+ /// and <see cref="JwtBearerEvents.AuthenticationFailed"/> events as appropriate.
3840/// </item>
3941/// <item>
4042/// Implementation does not call <see cref="Microsoft.Extensions.Logging.ILogger"/> to log authentication events.
@@ -46,16 +48,25 @@ public class JwtWebSocketAuthenticationService : IWebSocketAuthenticationService
4648 private readonly IGraphQLSerializer _graphQLSerializer ;
4749 private readonly IOptionsMonitor < JwtBearerOptions > _jwtBearerOptionsMonitor ;
4850 private readonly string [ ] _defaultAuthenticationSchemes ;
51+ private readonly JwtBearerAuthenticationOptions _jwtBearerAuthenticationOptions ;
52+ private readonly IAuthenticationSchemeProvider _schemeProvider ;
4953
5054 /// <summary>
5155 /// Initializes a new instance of the <see cref="JwtWebSocketAuthenticationService"/> class.
5256 /// </summary>
53- public JwtWebSocketAuthenticationService ( IGraphQLSerializer graphQLSerializer , IOptionsMonitor < JwtBearerOptions > jwtBearerOptionsMonitor , IOptions < AuthenticationOptions > authenticationOptions )
57+ public JwtWebSocketAuthenticationService (
58+ IGraphQLSerializer graphQLSerializer ,
59+ IOptionsMonitor < JwtBearerOptions > jwtBearerOptionsMonitor ,
60+ IOptions < AuthenticationOptions > authenticationOptions ,
61+ IOptions < JwtBearerAuthenticationOptions > jwtBearerAuthenticationOptions ,
62+ IAuthenticationSchemeProvider schemeProvider )
5463 {
5564 _graphQLSerializer = graphQLSerializer ;
5665 _jwtBearerOptionsMonitor = jwtBearerOptionsMonitor ;
5766 var defaultAuthenticationScheme = authenticationOptions . Value . DefaultScheme ;
5867 _defaultAuthenticationSchemes = defaultAuthenticationScheme != null ? [ defaultAuthenticationScheme ] : [ ] ;
68+ _jwtBearerAuthenticationOptions = jwtBearerAuthenticationOptions . Value ;
69+ _schemeProvider = schemeProvider ;
5970 }
6071
6172 /// <inheritdoc/>
@@ -79,6 +90,20 @@ public async Task AuthenticateAsync(AuthenticationRequest authenticationRequest)
7990 foreach ( var scheme in schemes ) {
8091 var options = _jwtBearerOptionsMonitor . Get ( scheme ) ;
8192
93+ // If JWT events are enabled, trigger the MessageReceived event
94+ if ( _jwtBearerAuthenticationOptions . EnableJwtEvents ) {
95+ var messageResult = await TriggerMessageReceivedEventAsync ( connection . HttpContext , options , token , scheme ) . ConfigureAwait ( false ) ;
96+ if ( messageResult . Handled ) {
97+ if ( messageResult . Success ) {
98+ connection . HttpContext . User = messageResult . Principal ! ;
99+ return ;
100+ }
101+ continue ;
102+ }
103+
104+ token = messageResult . Token ;
105+ }
106+
82107 // follow logic simplified from JwtBearerHandler.HandleAuthenticateAsync, as follows:
83108 var tokenValidationParameters = await SetupTokenValidationParametersAsync ( options , connection . HttpContext ) . ConfigureAwait ( false ) ;
84109#if NET8_0_OR_GREATER
@@ -88,11 +113,35 @@ public async Task AuthenticateAsync(AuthenticationRequest authenticationRequest)
88113 var tokenValidationResult = await tokenHandler . ValidateTokenAsync ( token , tokenValidationParameters ) . ConfigureAwait ( false ) ;
89114 if ( tokenValidationResult . IsValid ) {
90115 var principal = new ClaimsPrincipal ( tokenValidationResult . ClaimsIdentity ) ;
116+
117+ // If JWT events are enabled, trigger the TokenValidated event
118+ if ( _jwtBearerAuthenticationOptions . EnableJwtEvents )
119+ {
120+ var validatedResult = await TriggerTokenValidatedEventAsync ( connection . HttpContext , options , principal , tokenValidationResult . SecurityToken , scheme ) . ConfigureAwait ( false ) ;
121+ if ( validatedResult . Handled && ! validatedResult . Success )
122+ {
123+ continue ;
124+ }
125+
126+ principal = validatedResult . Principal ?? principal ;
127+ }
128+
91129 // set the ClaimsPrincipal for the HttpContext; authentication will take place against this object
92130 connection . HttpContext . User = principal ;
93131 return ;
94132 }
95- } catch {
133+ } catch ( Exception ex ) {
134+ // If JWT events are enabled, trigger the AuthenticationFailed event
135+ if ( _jwtBearerAuthenticationOptions . EnableJwtEvents )
136+ {
137+ var failedResult = await TriggerAuthenticationFailedEventAsync ( connection . HttpContext , options , ex , scheme ) . ConfigureAwait ( false ) ;
138+ if ( failedResult . Handled && failedResult . Success )
139+ {
140+ connection . HttpContext . User = failedResult . Principal ! ;
141+ return ;
142+ }
143+ }
144+
96145 // no errors during authentication should throw an exception
97146 // specifically, attempting to validate an invalid JWT token may result in an exception
98147 }
@@ -105,11 +154,31 @@ public async Task AuthenticateAsync(AuthenticationRequest authenticationRequest)
105154 foreach ( var validator in options . SecurityTokenValidators ) {
106155 if ( validator . CanReadToken ( token ) ) {
107156 try {
108- var principal = validator . ValidateToken ( token , tokenValidationParameters , out _ ) ;
157+ var principal = validator . ValidateToken ( token , tokenValidationParameters , out var securityToken ) ;
158+
159+ // If JWT events are enabled, trigger the TokenValidated event
160+ if ( _jwtBearerAuthenticationOptions . EnableJwtEvents ) {
161+ var validatedResult = await TriggerTokenValidatedEventAsync ( connection . HttpContext , options , principal , securityToken , scheme ) . ConfigureAwait ( false ) ;
162+ if ( validatedResult . Handled && ! validatedResult . Success ) {
163+ continue ;
164+ }
165+
166+ principal = validatedResult . Principal ?? principal ;
167+ }
168+
109169 // set the ClaimsPrincipal for the HttpContext; authentication will take place against this object
110170 connection . HttpContext . User = principal ;
111171 return ;
112- } catch {
172+ } catch ( Exception ex ) {
173+ // If JWT events are enabled, trigger the AuthenticationFailed event
174+ if ( _jwtBearerAuthenticationOptions . EnableJwtEvents ) {
175+ var failedResult = await TriggerAuthenticationFailedEventAsync ( connection . HttpContext , options , ex , scheme ) . ConfigureAwait ( false ) ;
176+ if ( failedResult . Handled && failedResult . Success ) {
177+ connection . HttpContext . User = failedResult . Principal ! ;
178+ return ;
179+ }
180+ }
181+
113182 // no errors during authentication should throw an exception
114183 // specifically, attempting to validate an invalid JWT token will result in an exception
115184 }
@@ -149,6 +218,94 @@ private static async ValueTask<TokenValidationParameters> SetupTokenValidationPa
149218 return tokenValidationParameters ;
150219 }
151220
221+ private async Task < EventResult > TriggerMessageReceivedEventAsync ( HttpContext httpContext , JwtBearerOptions options , string token , string schemeName )
222+ {
223+ var scheme = await _schemeProvider . GetSchemeAsync ( schemeName )
224+ ?? throw new InvalidOperationException ( $ "Authentication scheme '{ schemeName } ' not found.") ;
225+
226+ var messageReceivedContext = new MessageReceivedContext ( httpContext , scheme , options ) {
227+ Token = token
228+ } ;
229+
230+ if ( options . Events != null && options . Events . MessageReceived != null ) {
231+ await options . Events . MessageReceived ( messageReceivedContext ) . ConfigureAwait ( false ) ;
232+ }
233+
234+ var result = new EventResult { Token = messageReceivedContext . Token } ;
235+
236+ // If the event provided a principal, use it directly
237+ if ( messageReceivedContext . Result ? . Succeeded == true ) {
238+ result . Handled = true ;
239+ result . Success = true ;
240+ result . Principal = messageReceivedContext . Principal ;
241+ }
242+
243+ return result ;
244+ }
245+
246+ private async Task < EventResult > TriggerTokenValidatedEventAsync ( HttpContext httpContext , JwtBearerOptions options , ClaimsPrincipal principal , SecurityToken securityToken , string schemeName )
247+ {
248+ var scheme = await _schemeProvider . GetSchemeAsync ( schemeName )
249+ ?? throw new InvalidOperationException ( $ "Authentication scheme '{ schemeName } ' not found.") ;
250+
251+ var tokenValidatedContext = new TokenValidatedContext ( httpContext , scheme , options ) {
252+ Principal = principal ,
253+ SecurityToken = securityToken
254+ } ;
255+
256+ if ( options . Events != null && options . Events . TokenValidated != null ) {
257+ await options . Events . TokenValidated ( tokenValidatedContext ) . ConfigureAwait ( false ) ;
258+ }
259+
260+ var result = new EventResult ( ) ;
261+
262+ // If the event failed or replaced the principal
263+ if ( tokenValidatedContext . Result != null ) {
264+ result . Handled = true ;
265+ result . Success = tokenValidatedContext . Result . Succeeded ;
266+ if ( tokenValidatedContext . Result . Succeeded ) {
267+ result . Principal = tokenValidatedContext . Principal ;
268+ }
269+ }
270+
271+ return result ;
272+ }
273+
274+ private async Task < EventResult > TriggerAuthenticationFailedEventAsync ( HttpContext httpContext , JwtBearerOptions options , Exception exception , string schemeName )
275+ {
276+ var scheme = await _schemeProvider . GetSchemeAsync ( schemeName )
277+ ?? throw new InvalidOperationException ( $ "Authentication scheme '{ schemeName } ' not found.") ;
278+
279+ var authenticationFailedContext = new AuthenticationFailedContext ( httpContext , scheme , options ) {
280+ Exception = exception
281+ } ;
282+
283+ if ( options . Events != null && options . Events . AuthenticationFailed != null ) {
284+ await options . Events . AuthenticationFailed ( authenticationFailedContext ) . ConfigureAwait ( false ) ;
285+ }
286+
287+ var result = new EventResult ( ) ;
288+
289+ // If the event handled the exception and succeeded
290+ if ( authenticationFailedContext . Result != null ) {
291+ result . Handled = true ;
292+ result . Success = authenticationFailedContext . Result . Succeeded ;
293+ if ( authenticationFailedContext . Result . Succeeded ) {
294+ result . Principal = authenticationFailedContext . Principal ;
295+ }
296+ }
297+
298+ return result ;
299+ }
300+
301+ private sealed class EventResult
302+ {
303+ public bool Handled { get ; set ; }
304+ public bool Success { get ; set ; }
305+ public ClaimsPrincipal ? Principal { get ; set ; }
306+ public string Token { get ; set ; } = string . Empty ;
307+ }
308+
152309#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
153310 public sealed class AuthPayload
154311 {
0 commit comments