aspnetcore icon indicating copy to clipboard operation
aspnetcore copied to clipboard

Microsoft.AspNetCore.Authentication --> Microsoft.Extensions.Authentication

Open dazinator opened this issue 4 months ago • 2 comments

Background and Motivation

I'd like to be able to sign in as a user, from a console app, similar to how I can from an aspnet core web app.

aspnet core offers a rich and extensible capability for configuring various authentication schemes, and handlers. However it currently couples itself in its implementation to HttpContext - where this coupling is not unnecessary.

For example the journey begins when, to authenticate, we must get an IAuthenticationService - there are some convenience extension methods for HttpContext that resolve this for us from HttpContext.RequestServices and this is fine:

 public static class AuthenticationHttpContextExtensions
 {
          public static Task<AuthenticateResult> AuthenticateAsync(this HttpContext context, string? scheme) =>
            context.RequestServices.GetRequiredService<IAuthenticationService>().AuthenticateAsync(context, scheme);

This is not a problem because I can do the same from a console app with an IServiceProvider so no issue there.

However looking at IAuthenticationService.AuthenticateAsync() (and similar api's) we can see that they pretty much all want HttpContext as an argument. This seems unnecessary coupling because any implementation of AuthenticationService including the default, can inject IHttpContextAccessor and access HttpContext if it needs it that way. This is also true for any IAuthenticationHandlerProvider or IAuthenticationHandler that is "web" specific.

In other words, anything that needs http context has a way to get it, and there is no need to insist on this being passed all the way around the stack via the api's.

Conceptually, I propose the following, to allow Microsoft.AspNetCore.Authentication to be pushed into the runtime libraries as Microsoft.Extensions.Authentication with no dependency on HttpContext, and the the current HttpContext convenience authentication methods can remain in an aspnet core package, which is now a thin layer.

  1. Remove the HttpContext arguments from IAuthenticationService
  2. Change the base implementations of IAuthenticationService and IAuthenticationHandlerProvider (both are "scoped" services) to inject IHttpContextAccessor and use HttpContext where needed.
  3. Do the same as 2 for anything else that needs http context such as the implemenations of IAuthenticationHandler themselves like cookies based handlers etc or find away to adapt these to keep compatibility, whilst also allowing the larger goal of having handlers not dependent on HttpContext.

Usage Examples

The end goal, is that once Microsoft.Extensions.Authentication exists you'd be able to share an authentication story for non web apps that looks a little like this:

public class ConsoleSignInAuthenticationOptions : AuthenticationSchemeOptions
{
    // Custom options can be added here
}

public class ConsoleSignInAuthenticationHandler : AuthenticationHandler<ConsoleSignInAuthenticationOptions>
{
    private static AsyncLocal<ClaimsPrincipal> _currentPrincipal = new AsyncLocal<ClaimsPrincipal>();

    public ConsoleSignInAuthenticationHandler(
        IOptionsMonitor<ConsoleSignInAuthenticationOptions> options,
        ILoggerFactory logger,
        UrlEncoder encoder,
        ISystemClock clock)
        : base(options, logger, encoder, clock)
    {
    }

    protected override Task<AuthenticateResult> HandleAuthenticateAsync()
    {
        var principal = _currentPrincipal.Value;
        if (principal != null)
        {
            var ticket = new AuthenticationTicket(principal, Scheme.Name);
            return Task.FromResult(AuthenticateResult.Success(ticket));
        }

        return Task.FromResult(AuthenticateResult.Fail("No ClaimsPrincipal is available."));
    }

    public static async Task SignInAsync(ClaimsPrincipal principal)
    {
        _currentPrincipal.Value = principal;
        // Perform any additional sign-in logic here if necessary
    }

    public static async Task SignOutAsync()
    {
        _currentPrincipal.Value = null;
        // Perform any sign-out cleanup logic here if necessary
    }
}

services.AddAuthentication(options =>
{
    options.DefaultAuthenticateScheme = "ConsoleScheme";
    options.DefaultChallengeScheme = "ConsoleScheme";
})
.AddScheme<ConsoleSignInAuthenticationOptions, ConsoleSignInAuthenticationHandler>("ConsoleScheme", options => { });


Alternative Designs

Risks

There would be breaking changes, but these might be minimised by adjusting base implementations to adapt current api's to conform to newer api's in some scenarios.

dazinator avatar Feb 20 '24 13:02 dazinator

This is a huge breaking change that's just not going to happen. At best you'd get an incompatible fork with no interop between them. Since you wouldn't be able to use any the HTTP based implementations in any other kind of app, what's the value in a shared abstraction? I'd expect there to be hardly any code left once you removed the HTTP specific parts, just a hollow shell.

2. Change the base implementations of IAuthenticationService and IAuthenticationHandlerProvider (both are "scoped" services) to inject IHttpContextAccessor and use HttpContext where needed.

IHttpContextAccessor should be avoided whenever possible, there are too many ways to break it. Passing direct references is always preferred.

Tratcher avatar Feb 20 '24 22:02 Tratcher

what's the value in a shared abstraction?

Having "one way" to do authentication irrespective of hosting model / paradigm. Sure the providers you'd pull in look different for a web app vs a non web app, but I see this as similar in nature to how the IServer / IHostLifetime (from memory!) that is used for web app versus non web app is different but there is still the concept of a generic host - because I guess you want to standardise concepts more broadly across dotnet, so its less cognitive load for developers working across solutions.

Perhaps it comes down to whether you think Authentication and Authorization are first class concepts, worthy of addressing as such for dotnet more broadly, so there is a consistent story irrespective of the type of app. Or whether you think various eco systems in dotnet, should roll their own solutions.

dazinator avatar Feb 22 '24 14:02 dazinator