Description
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 theHttpContext
extension methods to expose the 5 main operationsAuthenticate/Challenge/Forbid/SignIn/SignOut
IAuthenticationHandler
: Defines the required operations for all handlers: Authenticate/Challenge/ForbidIAuthenticationSignIn/OutHandler
: Implemented to add the SignIn/SignOut methods respectivelyIAuthenticationRequestHandler
: 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 givenIAuthenticationService
method, it binds the scheme name (and optional display name) to anIAuthenticationHandler
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 theAuthenticationOptions
)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 anapp.Map()
Types that are mostly unchanged, just with new homes:
AuthenticationProperties
: metadata for authentication operations.AuthenticationTicket
: used to store a claims principal (user) + authentication propertiesAuthenticateResult
: return value forAuthenticateAsync
, 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 usingAuthenticationOptions.DefaultAuthenticateScheme
to sethttpContext.User
if specified. It also will giveIAuthenticationRequestHandler's
a chance to handle the request.AuthenticationSchemeOptions
: Base class for options used with theAuthenticationHandler
base class, it defines two common properties in Events and ClaimsIssuer, as well as a virtualValidate()
method that will be called on every request by the handler.AuthenticationHandler<TOptions>
: Abstract base class for handlers who must implementHandleAuthenticateAsync
. The rest ofIAuthenticationHandler
is implemented with some reasonable defaults, Challenge(401)/Forbid(403), and logic to handle per request initialization usingIOptionsMonitor<TOptions>.Get(authenticationScheme.Name)
to resolve the handler's options.RemoteAuthenticationHandler<TOptions>
: Adds the abstractHandleRemoteAuthenticateAsync
and implementsHandleAuthenticateAsync
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 beRemoteAuthenticationOptions
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 byservices.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:
-
BaseContext
events which are the simplest and just expose properties with no real control flow. -
ResultContext
events which revolve around producingAuthenticateResult
s 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.
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 valueDefaultAuthenticateScheme
: if specified,AuthenticateAsync()
will use this scheme, and also the
AuthenticationMiddleware
added byUseAuthentication()
will use this scheme to setcontext.User
automatically. (Corresponds to AutomaticAuthentication)DefaultChallengeScheme
if specified,ChallengeAsync()
will use this scheme,[Authorize]
with policies that don't specify schemes will also use thisDefaultSignInScheme
is used bySignInAsync()
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 bySignOutAsync()
falls back toDefaultSignInScheme
DefaultForbidScheme
is used byForbidAsync()
, falls back toDefaultChallengeScheme
"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
- An effective policy is computed by combining all of the requested policies/requirements from all relevant
[Authorize]
attributes on the controller/method/globally. IPolicyEvaluator.AuthenticateAsync(policy, httpContext)
is called, by default, if the has specified anypolicy.AuthenticationSchemes
,AuthenticateAsync
will be called on each scheme, and each resultingClaimsPrincipal
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, asDefaultScheme/DefaultAuthenticateScheme
will be set to the main application cookie, and theAuthenticationMiddleware
will have already set context.User using this scheme'sAuthenticateAsync()
Authenticate logic- If
AllowAnoynmous
was specified, authorization is skipped, and the filter logic short circuits and is done. - Finally,
IPolicyEvaluator.AuthenticateAsync(policy, authenticationResult, httpContext)
is called with the result from step 2. This just basically turns into a call toIAuthorizationService.AuthorizeAsync
, and the result is used to determine the appropriateChallenge/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 customIConfigureOptions
. These will need to be updated to implementIConfigureNamedOptions
instead, which just means adding an additional public get/set Name property.