New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Auth 2.0 Changes / Migration #262

Open
HaoK opened this Issue Jul 17, 2017 · 0 comments

Comments

Projects
None yet
2 participants
@HaoK
Member

HaoK commented Jul 17, 2017

Summary:

The old 1.0 Authentication stack no longer will work, and is obsolete in 2.0. All authentication related functionality must be migrated to the 2.0 stack, any interop between old and new must be side by side apps, as opposed to mixing 1.0 auth code with 2.0 auth code in the same app. Cookie authentication will interop, so 1.0 Cookies and 2.0 Cookies will be valid in both apps if configured properly. The main motivation was to move to a more flexible service based IAuthenticationService and away from the old middleware/IAuthenticationManager design that came over from Microsoft.Owin.

For discussion/questions/help, please start with aspnet/Security#1338

IAuthenticationManager(aka httpContext.Authentication) is now obsolete

This was the main entry point into the old auth system. This has now been replaced with a new set of HttpContext extensions that live in the Microsoft.AspNetCore.Authentication namespace and remain very similar:

// Add using to pickup the new extension methods
using Microsoft.AspNetCore.Authentication;

// Update by just removing the .Authentication
context.Authentication.AuthenticateAsync => context.AuthenticateAsync
context.Authentication.ChallengeAsync => context.ChallengeAsync

Configure(): UseXyzAuthentication has been replaced by ConfigureService(): AddXyz()

In Auth 1.0, every auth scheme had its own middleware, and startup looked something like this:

public void ConfigureServices(IServiceCollection services)
{
     services.AddIdentity<ApplicationUser, IdentityRole>()
             .AddEntityFrameworkStores<ApplicationDbContext>();
}

public void Configure(IApplicationBuilder app, ILoggerFactory loggerfactory) {
    app.UseIdentity();
    app.UseFacebookAuthentication(new FacebookOptions
       { AppId = Configuration["facebook:appid"],  AppSecret = Configuration["facebook:appsecret"] });
} 

In Auth 2.0, there is now only a single Authentication middleware, and each authentication scheme is registered during ConfigureServices, and UseIdentity() is no longer required (since it was just calling UseCookie 4 times underneath the covers)

public void ConfigureServices(IServiceCollection services) {
    services.AddIdentity<ApplicationUser, IdentityRole>().AddEntityFrameworkStores();

    // If you want to tweak identity cookies, they no longer are part of identityOptions
    services.ConfigureApplicationCookie(o => o.LoginPath = new PathString("/login");
    services.AddAuthentication()
                .AddFacebook(o =>
                {
                    o.AppId = Configuration["facebook:appid"];
                    o.AppSecret = Configuration["facebook:appsecret"];
                });
}

public void Configure(IApplicationBuilder app, ILoggerFactory loggerfactory) {
    app.UseAuthentication();
}

New Microsoft.AspNetCore.Authentication.Core/Abstractions

All of the old Authentication namespaces in HttpAbstractions have been deprecated. The new Auth 2.0 stack lives in two new packages inside the HttpAbstractions repo: Microsoft.AspNetCore.Authentication.Core/Abstractions.

Brief overview:

  • IAuthenticationService: used by the HttpContext extension methods to expose the 5 main operations Authenticate/Challenge/Forbid/SignIn/SignOut
  • IAuthenticationHandler: Defines the required operations for all handlers: Authenticate/Challenge/Forbid
  • IAuthenticationSignIn/OutHandler: Implemented to add the SignIn/SignOut methods respectively
  • IAuthenticationRequestHandler: Implemented by handlers that need to participate in request handler, i.e. remote authentication schemes like OAuth/OIDC that need to process 3rd party auth responses.
  • AuthenticationScheme: represents a logical named authentication scheme to target for any given IAuthenticationService method, it binds the scheme name (and optional display name) to an IAuthenticationHandler which implements the scheme specific logic.
  • IAuthenticationSchemeProvider: responsible for managing which schemes are supported, and what the defaults are for each operation (the default implementation just reads from the AuthenticationOptions)
  • IAuthenticationHandlerProvider: responsible for returning the correct handler instance for a given scheme and request.
  • IAuthenticationFeature: used to capture the original request path/pathbase so redirects can be computed property after an app.Map()

Types that are mostly unchanged, just with new homes:

  • AuthenticationProperties: metadata for authentication operations.
  • AuthenticationTicket: used to store a claims principal (user) + authentication properties
  • AuthenticateResult: return value for AuthenticateAsync, contains either a ticket, or a failure

Security repo: Microsoft.AspNetCore.Authentication / AuthenticationHandler changes

All of the core abstractions and services for authentication live in HttpAbstracions, but there's an additional layer of base classes/functionality targeted towards implementation of AuthenticationHandlers. This is also where the AuthenticationMiddleware lives. The handlers themselves for the various implementations aren't drastically different, but there were a fair amount of changes

Microsoft.AspNetCore.Authentication

Overview:

  • AuthenticationMiddleware: UseAuthentication() adds this middleware which does two things. By default It will automatically Authenticate using AuthenticationOptions.DefaultAuthenticateScheme to set httpContext.User if specified. It also will give IAuthenticationRequestHandler's a chance to handle the request.
  • AuthenticationSchemeOptions: Base class for options used with the AuthenticationHandler base class, it defines two common properties in Events and ClaimsIssuer, as well as a virtual Validate() method that will be called on every request by the handler.
  • AuthenticationHandler<TOptions>: Abstract base class for handlers who must implement HandleAuthenticateAsync. The rest of IAuthenticationHandler is implemented with some reasonable defaults, Challenge(401)/Forbid(403), and logic to handle per request initialization using IOptionsMonitor<TOptions>.Get(authenticationScheme.Name) to resolve the handler's options.
  • RemoteAuthenticationHandler<TOptions>: Adds the abstract HandleRemoteAuthenticateAsync and implements HandleAuthenticateAsync to call this method. This is meant to be used for 3rd party authentication, i.e. OAuth/OIDC. It adds an additional constraint to TOptions that requires them to be RemoteAuthenticationOptions which adds a bunch of settings like CallbackPath, CorrelationCookie, Backchannel which are needed to talk to a remote authentication provider.
  • AuthenticationBuilder: is a new class which is used to group all of the AddXyz() authentication methods. This is returned by services.AddAuthentication() and is where specific authentication methods are expected to add themselves as extension methods, i.e. AddCookie(), AddGoogle(), AddFacebook().

Configuring Identity / application cookies

The cookie options that used to be configured via:

   services.AddIdentity(options => options.Cookies...)

now are configured via:

   services.Configure[Application/External]Cookie()

Event changes overview

  • Thanks mostly to @PinpointTownes determination to improve the events story, we've refactored and renamed some things to improve the events experience (caveat: if you don't like the names we picked, don't blame @PinpointTownes we overruled him on naming)

At a high level, there 3 main kinds of events:

  1. BaseContext events which are the simplest and just expose properties with no real control flow.

  2. ResultContext events which revolve around producing AuthenticateResults which expose:

  • Success(): used to indicate that authentication was successful and to use the Principal/Properties in the event to construct the result.
  • NoResult(): used to indicate no authentication result is to be returned.
  • Fail(): used to return a failure.
  1. HandleRequestContext events are used in the IAuthenticationRequestHandler/HandleRemoteAuthenticate methods and adds two more methods:
  • HandleResponse(): used to indicate that the response was generated and the AuthenticationMiddleware should not invoke the rest of the middlewares in the pipeline after it.

  • SkipHandler() used to indicate that this handler is done with the request, but subsequent handlers will be called, as well as any other middleware in the pipeline if none of those handlers handle the request.

AutomaticAuthentication/Challenge have been replaced by Default[Authenticate/Challenge]Scheme

AutomaticAuthentication/Challenge were intended to only be set on one authentication scheme, but there was no good way to enforce this in 1.0. These have been removed as flags on the individual AuthenticationOptions, and have been moved into the base AuthenticationOptions which can be configured in the call to AddAuthentication(authenticationOptions => authenticationOptions.DefaultScheme = "Cookies").

There are now overloads that use the default schemes for each method in IAuthenticationService

  • DefaultScheme: if specified, all the other defaults will fallback to this value
  • DefaultAuthenticateScheme: if specified, AuthenticateAsync() will use this scheme, and also the
    AuthenticationMiddleware added by UseAuthentication() will use this scheme to set context.User automatically. (Corresponds to AutomaticAuthentication)
  • DefaultChallengeScheme if specified, ChallengeAsync() will use this scheme, [Authorize] with policies that don't specify schemes will also use this
  • DefaultSignInScheme is used by SignInAsync() and also by all of the remote auth schemes like Google/Facebook/OIDC/OAuth, typically this would be set to a cookie.
  • DefaultSignOutScheme is used by SignOutAsync() falls back to DefaultSignInScheme
  • DefaultForbidScheme is used by ForbidAsync(), falls back to DefaultChallengeScheme

"Windows" Authentication(HttpSys/IISIntegration)

The host behavior hasn't changed too much, but now they each register a single "Windows" authentication scheme. Also IISIntegration now conditionally registers the handler only if windows auth is enabled in IIS (if you have the latest version of ANCM, otherwise it's always registered as before).

Authorization changes

IAuthorizationService.AuthorizeAsync now returns AuthorizationResult instead of bool

In order to enable scenarios around authorization failures, IAuthorizationService now returns a result object which allows access to the reasons why AuthorizeAsync failed (either context.Fail(), or a list of failed requirements)

Removal of ChallengeBehavior => new PolicyEvaluator

In Auth 1.0, there was a ChallengeBehavior enum that was used to specify either Automatic/Unauthorized/Forbid behaviors to signal to the auth middleware what behavior the caller wanted. Automatic was the default and would go down the Forbid(403) code path if the middleware already had an authentication ticket, otherwise would result in Unauthorized(401).

In Auth 2.0, this behavior has been moved into a new Authorization.Policy package, which introduces the IPolicyEvaluator which uses both IAuthenticationService (when requested via policy.AuthenticationSchemes), and IAuthorizationService to decide whether to return a tri state PolicyAuthorizationResult (Succeeded/Challenged/Forbidden).

Overview of [Authorize]

The [Authorize] attribute hasn't changed much, but the there were some implementation details that have changed significantly in MVC's AuthorizeFilter, and here's an overview of how things work:
AuthorizeFilter source

  1. An effective policy is computed by combining all of the requested policies/requirements from all relevant [Authorize] attributes on the controller/method/globally.
  2. IPolicyEvaluator.AuthenticateAsync(policy, httpContext) is called, by default, if the has specified any policy.AuthenticationSchemes, AuthenticateAsync will be called on each scheme, and each resulting ClaimsPrincipal will be merged together into a single ClaimsPrincipal set on context.User. If no schemes were specified, the evaluator will attempt to use context.User if it contains an authenticated user. This is usually the normal code path, as DefaultScheme/DefaultAuthenticateScheme will be set to the main application cookie, and the AuthenticationMiddleware will have already set context.User using this scheme's AuthenticateAsync()
    Authenticate logic
  3. If AllowAnoynmous was specified, authorization is skipped, and the filter logic short circuits and is done.
  4. Finally, IPolicyEvaluator.AuthenticateAsync(policy, authenticationResult, httpContext) is called with the result from step 2. This just basically turns into a call to IAuthorizationService.AuthorizeAsync, and the result is used to determine the appropriate Challenge/ForbidResult if needed.

Claims Transformation

Simpler, new IClaimsTransformation service with a single method:
Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal)

We call this on any successful AuthenticateAsync call.

        services.AddSingleton<IClaimsTransformation, ClaimsTransformer>();

        private class ClaimsTransformer : IClaimsTransformation {
            // Can consume services from DI as needed, including scoped DbContexts
            public ClaimsTransformer(IHttpContextAccessor httpAccessor) { }
            public Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal p) {
                p.AddIdentity(new ClaimsIdentity());
                return Task.FromResult(p);
            }
        }

How to replicate branched auth middleware functionality in 2.0

It turns out a massive amount of people are not using the AuthenticationSchemes property on the AuthorizeAttribute and instead are using middleware branching to select the current scheme for a request based on some logic (most commonly a path prefix /api).

[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
[Route("/api/products")]
public class ProductsController : Controller
{
   [HttpGet]
   public List<Product> Get() => ...;
}

This will use the correct authentication scheme when doing auth for this specific controller.

As an alternative, here's a middleware that can be used to change the current user based on the authentication scheme.

public class AuthenticateSchemeMiddleware
{
    private readonly RequestDelegate _next;
    private readonly string _scheme;

    public AuthenticateSchemeMiddleware(RequestDelegate next, string scheme)
    {
        _next = next;
        _scheme = scheme ?? throw new ArgumentNullException(nameof(scheme));
    }

    public async Task Invoke(HttpContext httpContext)
    {
        var result = await httpContext.AuthenticateAsync(_scheme);

        if (result.Succeeded)
        {
            httpContext.User = result.Principal;
        }

        await _next(httpContext);
    }
}

public static class AuthenticateMiddlewareExtensions
{
    public static IApplicationBuilder UseAuthenticationScheme(this IApplicationBuilder builder, string scheme)
    {
        return builder.UseMiddleware<AuthenticateSchemeMiddleware>(scheme);
    }
}

Usage looks like this:

In Startup.ConfigureServices

void ConfigureServices(IServiceCollection services)
{
    services.AddAuthentication()
                  .AddJwtBearer(...);
}

In Startup.Configure

app.Map("/api", sub => sub.UseAuthenticationScheme(JwtBearerDefaults.AuthenticationScheme));
[Authorize]
[Route("/api/products")]
public class ProductsController : Controller
{
   [HttpGet]
   public List<Product> Get() => ...;
}

UPDATE: I should point out that if you're using this middleware and the user isn't authorized, the challenge/forbid issued will end up being the default scheme. If none is set, an exception will be thrown.

Known issues/breaking changes:

  • In 1.0, it was possible to configure different Authentication middleware with branching, this is no longer possible with a single middleware and shared services across all branches. A workaround could be to use different schemes/options for each branch.
  • Auth 2.0 now uses IOptionsMonitor and named options, this introduces a breaking change for any apps that were configuring auth using a custom IConfigureOptions. These will need to be updated to implement IConfigureNamedOptions instead, which just means adding an additional public get/set Name property.

@aspnet aspnet locked and limited conversation to collaborators Jul 17, 2017

@HaoK HaoK added this to the 2.0.0 milestone Jul 17, 2017

@danroth27 danroth27 added 2.0.0 and removed 2.0.0 labels Aug 3, 2017

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.