Skip to content

Commit

Permalink
Switched to events model to DI of event type (#495)
Browse files Browse the repository at this point in the history
* Switch to Events Model to Allow IoC of Event Type
* Added, updated tests
* Updated changelog
* Bumped JWT.Extensions.AspNetCore version to beta3
---------

Co-authored-by: Alexander Batishchev <[email protected]>
Co-authored-by: Alex Batishchev <[email protected]>
  • Loading branch information
3 people authored Apr 29, 2024
1 parent 31a9366 commit c2434a1
Show file tree
Hide file tree
Showing 9 changed files with 365 additions and 90 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Unreleased

# JWT.Extensions.AspNetCore 11.0.0-beta3

- Converted to use the event model to allow dependency injection with custom event classes.

# JWT 11.0.0-beta2, JWT.Extensions.AspNetCore 11.0.0-beta2, JWT.Extensions.DependencyInjection 3.0.0-beta2

- Replaced .NET 7 with .NET 8 whenever applicable
Expand Down
25 changes: 25 additions & 0 deletions src/JWT.Extensions.AspNetCore/FailedTicketContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using System;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;

namespace JWT.Extensions.AspNetCore
{
public class FailedTicketContext
{
public FailedTicketContext(ILogger logger, Exception exception, HttpContext context, JwtAuthenticationOptions options)
{
this.Logger = logger;
this.Exception = exception;
this.Context = context;
this.Options = options;
}

public ILogger Logger { get; }

public Exception Exception { get; }

public HttpContext Context { get; }

public JwtAuthenticationOptions Options { get; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
<Authors>Alexander Batishchev</Authors>
<PackageTags>jwt;json;asp.net;asp.net core;.net core;authorization</PackageTags>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<Version>11.0.0-beta2</Version>
<Version>11.0.0-beta3</Version>
<FileVersion>11.0.0.0</FileVersion>
<AssemblyVersion>11.0.0.0</AssemblyVersion>
<RootNamespace>JWT.Extensions.AspNetCore</RootNamespace>
Expand Down
87 changes: 87 additions & 0 deletions src/JWT.Extensions.AspNetCore/JwtAuthenticationEvents.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
using System;
using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.Logging;
using Microsoft.Net.Http.Headers;

namespace JWT.Extensions.AspNetCore
{
public class JwtAuthenticationEvents
{
private static readonly Action<ILogger, Exception> _logMissingHeader;
private static readonly Action<ILogger, string, string, Exception> _logIncorrectScheme;
private static readonly Action<ILogger, Exception> _logEmptyHeader;
private static readonly Action<ILogger, Exception> _logSuccessfulTicket;
private static readonly Action<ILogger, string, Exception> _logFailedTicket;

static JwtAuthenticationEvents()
{
_logMissingHeader = LoggerMessage.Define(
LogLevel.Information,
10,
$"Header {nameof(HeaderNames.Authorization)} is empty, returning none");

_logIncorrectScheme = LoggerMessage.Define<string, string>(
LogLevel.Information,
11,
$"Header {nameof(HeaderNames.Authorization)} scheme is {{ActualScheme}}, expected {{ExpectedScheme}}, returning none");

_logEmptyHeader = LoggerMessage.Define(
LogLevel.Information,
12,
$"Token in header {nameof(HeaderNames.Authorization)} is empty, returning none");

_logSuccessfulTicket = LoggerMessage.Define(
LogLevel.Information,
1,
"Successfully decoded JWT, returning success");

_logFailedTicket = LoggerMessage.Define<string>(
LogLevel.Information,
2,
"Error decoding JWT: {0}, returning failure");
}

public Func<ILogger, AuthenticateResult> OnMissingHeader { get; set; } =
logger =>
{
_logMissingHeader(logger, null);
return AuthenticateResult.NoResult();
};

public Func<ILogger, string, string, AuthenticateResult> OnIncorrectScheme { get; set; } =
(logger, actualScheme, expectedScheme) =>
{
_logIncorrectScheme(logger, actualScheme, expectedScheme, null);
return AuthenticateResult.NoResult();
};

public Func<ILogger, string, AuthenticateResult> OnEmptyHeader { get; set; } = (logger, header) =>
{
_logEmptyHeader(logger, null);
return AuthenticateResult.NoResult();
};

public Func<SuccessfulTicketContext, AuthenticateResult> OnSuccessfulTicket { get; set; } = context =>
{
_logSuccessfulTicket(context.Logger, null);
return AuthenticateResult.Success(context.Ticket);
};

public Func<FailedTicketContext, AuthenticateResult> OnFailedTicket { get; set; } = context =>
{
_logFailedTicket(context.Logger, context.Exception.Message, context.Exception);
return AuthenticateResult.Fail(context.Exception);
};

public virtual AuthenticateResult SuccessfulTicket(SuccessfulTicketContext context) => OnSuccessfulTicket(context);

public virtual AuthenticateResult FailedTicket(FailedTicketContext context) => OnFailedTicket(context);

public virtual AuthenticateResult EmptyHeader(ILogger logger, string header) => OnEmptyHeader(logger, header);

public virtual AuthenticateResult IncorrectScheme(ILogger logger, string actualScheme, string expectedScheme) => OnIncorrectScheme(logger, actualScheme, expectedScheme);

public virtual AuthenticateResult MissingHeader(ILogger logger) => OnMissingHeader(logger);

}
}
88 changes: 18 additions & 70 deletions src/JWT.Extensions.AspNetCore/JwtAuthenticationHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,44 +12,10 @@ namespace JWT.Extensions.AspNetCore
{
public sealed class JwtAuthenticationHandler : AuthenticationHandler<JwtAuthenticationOptions>
{
private static readonly Action<ILogger, Exception> _logMissingHeader;
private static readonly Action<ILogger, string, string, Exception> _logIncorrectScheme;
private static readonly Action<ILogger, Exception> _logEmptyHeader;
private static readonly Action<ILogger, Exception> _logSuccessfulTicket;
private static readonly Action<ILogger, string, Exception> _logFailedTicket;

private readonly IJwtDecoder _jwtDecoder;
private readonly IIdentityFactory _identityFactory;
private readonly ITicketFactory _ticketFactory;

static JwtAuthenticationHandler()
{
_logMissingHeader = LoggerMessage.Define(
LogLevel.Information,
10,
$"Header {nameof(HeaderNames.Authorization)} is empty, returning none");

_logIncorrectScheme = LoggerMessage.Define<string, string>(
LogLevel.Information,
11,
$"Header {nameof(HeaderNames.Authorization)} scheme is {{0}}, expected {{1}}, returning none");

_logEmptyHeader = LoggerMessage.Define(
LogLevel.Information,
12,
$"Token in header {nameof(HeaderNames.Authorization)} is empty, returning none");

_logSuccessfulTicket = LoggerMessage.Define(
LogLevel.Information,
1,
"Successfully decoded JWT, returning success");

_logFailedTicket = LoggerMessage.Define<string>(
LogLevel.Information,
2,
"Error decoding JWT: {0}, returning failure");
}

public JwtAuthenticationHandler(
IJwtDecoder jwtDecoder,
IIdentityFactory identityFactory,
Expand All @@ -65,36 +31,6 @@ public JwtAuthenticationHandler(
_ticketFactory = ticketFactory;
}

public static AuthenticateResult OnMissingHeader(ILogger logger)
{
_logMissingHeader(logger, null);
return AuthenticateResult.NoResult();
}

public static AuthenticateResult OnIncorrectScheme(ILogger logger, string actualScheme, string expectedScheme)
{
_logIncorrectScheme(logger, actualScheme, expectedScheme, null);
return AuthenticateResult.NoResult();
}

public static AuthenticateResult OnEmptyHeader(ILogger logger, string header)
{
_logEmptyHeader(logger, null);
return AuthenticateResult.NoResult();
}

public static AuthenticateResult OnSuccessfulTicket(ILogger logger, AuthenticationTicket ticket)
{
_logSuccessfulTicket(logger, null);
return AuthenticateResult.Success(ticket);
}

public static AuthenticateResult OnFailedTicket(ILogger logger, Exception ex)
{
_logFailedTicket(logger, ex.Message, ex);
return AuthenticateResult.Fail(ex);
}

protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{
var header = this.Context.Request.Headers[HeaderNames.Authorization];
Expand All @@ -105,27 +41,39 @@ protected override Task<AuthenticateResult> HandleAuthenticateAsync()
private AuthenticateResult GetAuthenticationResult(string header)
{
if (String.IsNullOrEmpty(header))
return this.Options.OnMissingHeader(this.Logger);
return this.Events.MissingHeader(this.Logger);

if (!header.StartsWith(this.Scheme.Name, StringComparison.OrdinalIgnoreCase))
return this.Options.OnIncorrectScheme(this.Logger, header.Split(' ').FirstOrDefault(), this.Scheme.Name);
return this.Events.IncorrectScheme(this.Logger, header.Split(' ').FirstOrDefault(), this.Scheme.Name);

var token = header.Substring(this.Scheme.Name.Length).Trim();
if (String.IsNullOrEmpty(token))
return this.Options.OnEmptyHeader(this.Logger, header);
return this.Events.EmptyHeader(this.Logger, header);

try
{
object payload = _jwtDecoder.DecodeToObject(this.Options.PayloadType, token, this.Options.Keys, this.Options.VerifySignature);
var identity = _identityFactory.CreateIdentity(this.Options.PayloadType, payload);
var ticket = _ticketFactory.CreateTicket(identity, this.Scheme);

return this.Options.OnSuccessfulTicket(this.Logger, ticket);
var successContext = new SuccessfulTicketContext(this.Logger, ticket, this.Context, this.Options);
return this.Events.SuccessfulTicket(successContext);
}
catch (Exception ex)
{
return this.Options.OnFailedTicket(this.Logger, ex);
var failedContext = new FailedTicketContext(this.Logger, ex, this.Context, this.Options);
return this.Events.FailedTicket(failedContext);
}
}

/// <summary>
/// The handler calls methods on the events which give the application control at certain points where processing is occurring.
/// If it is not provided, a default instance is supplied which does nothing when the methods are called.
/// </summary>
protected new JwtAuthenticationEvents Events
{
get => (JwtAuthenticationEvents)base.Events!;
set => base.Events = value;
}
}
}
}
60 changes: 49 additions & 11 deletions src/JWT.Extensions.AspNetCore/JwtAuthenticationOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ namespace JWT.Extensions.AspNetCore
{
public class JwtAuthenticationOptions : AuthenticationSchemeOptions
{
public JwtAuthenticationOptions()
{
Events = new JwtAuthenticationEvents();
}

/// <summary>
/// The keys used to sign the JWT.
/// </summary>
Expand All @@ -26,41 +31,64 @@ public class JwtAuthenticationOptions : AuthenticationSchemeOptions
/// Handles missing authentication header.
/// </summary>
/// <remarks>
/// For the default behavior <see cref="JwtAuthenticationHandler.OnMissingHeader" />.
/// For the default behavior <see cref="JwtAuthenticationEvents.OnMissingHeader" />.
/// </remarks>
public Func<ILogger, AuthenticateResult> OnMissingHeader { get; set; } = JwtAuthenticationHandler.OnMissingHeader;
[Obsolete("Use Events.OnMissingHeader")]
public Func<ILogger, AuthenticateResult> OnMissingHeader
{
get => Events.OnMissingHeader;
set => Events.OnMissingHeader = value;
}

/// <summary>
/// Handles incorrect authentication scheme.
/// </summary>
/// <remarks>
/// For the default behavior <see cref="JwtAuthenticationHandler.OnIncorrectScheme" />.
/// For the default behavior <see cref="JwtAuthenticationEvents.OnIncorrectScheme" />.
/// </remarks>
public Func<ILogger, string, string, AuthenticateResult> OnIncorrectScheme { get; set; } = JwtAuthenticationHandler.OnIncorrectScheme;
[Obsolete("Use Events.OnIncorrectScheme")]
public Func<ILogger, string, string, AuthenticateResult> OnIncorrectScheme
{
get => Events.OnIncorrectScheme;
set => Events.OnIncorrectScheme = value;
}

/// <summary>
/// Handles empty authentication header.
/// </summary>
/// <remarks>
/// For the default behavior <see cref="JwtAuthenticationHandler.OnEmptyHeader" />.
/// For the default behavior <see cref="JwtAuthenticationEvents.OnEmptyHeader" />.
/// </remarks>
public Func<ILogger, string, AuthenticateResult> OnEmptyHeader { get; set; } = JwtAuthenticationHandler.OnEmptyHeader;
[Obsolete("Use Events.OnEmptyHeader")]
public Func<ILogger, string, AuthenticateResult> OnEmptyHeader
{
get => Events.OnEmptyHeader;
set => Events.OnEmptyHeader = value;
}

/// <summary>
/// Handles successful authentication header.
/// </summary>
/// <remarks>
/// For the default behavior <see cref="JwtAuthenticationHandler.OnSuccessfulTicket" />.
/// For the default behavior <see cref="JwtAuthenticationEvents.OnSuccessfulTicket" />.
/// </remarks>
public Func<ILogger, AuthenticationTicket, AuthenticateResult> OnSuccessfulTicket { get; set; } = JwtAuthenticationHandler.OnSuccessfulTicket;
[Obsolete("Use Events.OnSuccessfulTicket")]
public Func<ILogger, AuthenticationTicket, AuthenticateResult> OnSuccessfulTicket
{
set => Events.OnSuccessfulTicket = (context) => value(context.Logger, context.Ticket);
}

/// <summary>
/// Handles failed authentication header.
/// </summary>
/// <remarks>
/// For the default behavior <see cref="JwtAuthenticationHandler.OnFailedTicket" />.
/// For the default behavior <see cref="JwtAuthenticationEvents.OnFailedTicket" />.
/// </remarks>
public Func<ILogger, Exception, AuthenticateResult> OnFailedTicket { get; set; } = JwtAuthenticationHandler.OnFailedTicket;
[Obsolete("Use Events.OnFailedTicket")]
public Func<ILogger, Exception, AuthenticateResult> OnFailedTicket
{
set => Events.OnFailedTicket = context => value(context.Logger, context.Exception);
}

/// <summary>
/// Whether to include by default AuthenticationScheme into the resulting <see cref="ClaimsIdentity" />.
Expand All @@ -77,5 +105,15 @@ public class JwtAuthenticationOptions : AuthenticationSchemeOptions
/// The default value is <see cref="Dictionary{String, String}" />.
/// </remarks>
public Type PayloadType { get; set; } = typeof(Dictionary<string, object>);


/// <summary>
/// Custom Event Overrides
/// </summary>
public new JwtAuthenticationEvents Events
{
get => (JwtAuthenticationEvents)base.Events!;
set => base.Events = value;
}
}
}
}
25 changes: 25 additions & 0 deletions src/JWT.Extensions.AspNetCore/SuccessfulTicketContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;

namespace JWT.Extensions.AspNetCore
{
public class SuccessfulTicketContext
{
public SuccessfulTicketContext(ILogger logger, AuthenticationTicket ticket, HttpContext context, JwtAuthenticationOptions options)
{
this.Logger = logger;
this.Ticket = ticket;
this.Context = context;
this.Options = options;
}

public ILogger Logger { get; }

public AuthenticationTicket Ticket { get; }

public HttpContext Context { get; }

public JwtAuthenticationOptions Options { get; }
}
}
Loading

0 comments on commit c2434a1

Please sign in to comment.