Skip to content
This repository was archived by the owner on Dec 13, 2018. It is now read-only.

Authentication 2.0 Iteration 3 (with Names/OptionFactory) #1151

Closed
wants to merge 64 commits into from
Closed

Conversation

HaoK
Copy link
Member

@HaoK HaoK commented Mar 15, 2017

  • New IOptionsFactory which provides named options in addition to validation, both of which are triggered via IOptionsFactory.Get("name"). Allows normal options configuration for schemes, Validation is just a different set of Actions that run after the Configures

IOptionsFactory Usage:

  services.ConfigureAll<GoogleOptions>(o => o.SignInScheme = "GoogleCookie");
  services.Configure<GoogleOptions>("Google", o => ClientId = "avsdlkj");
  services.Configure<GoogleOptions>("Google2" p => ClientId = "two");
  services.ValidateAll<GoogleOptions>(o => if (o.ClientId == null) throw new ArgumentException());

  optionsFactory.Get<GoogleOptions>("Google"); // OK
  optionsFactory.Get<GoogleOptions>("Default"); // throws ArgumentException
  • split HandleRequest into its own IAuthenticationRequestHandler interface instead, CallbackPaths routing is disabled for now (all IAuthenticationRequestHandler schemes are given a chance to handle the request)

TL:DR Auth 2.0 summary

  • New IAuthenticationService with the same Authenticate/Challenge/SignIn/Out/ForbidAsync API.
  • Made up of AuthenticationScheme instances which have: Name, Type, and Settings dictionary (Options instance + old description bag)
  • app.UseXyzAuth(new XyzOptions()) => services.AddXyzAuth(o => o.Foo = bar) adds an AuthenticationScheme instance for Xyz
  • Single UseAuthentication is required, which takes care of the old 'automatic' authenticate, in addition to giving each scheme a chance to handle the request like the old middlewares did.
  • Auth provider extensibility model is mostly unchanged, just tweaked (no more middleware, handler base class is cleaned up and activated from DI (see DbContext example below)
  • 'Automatic' authenticate and challenge are now a single Default[Authenticate/SignIn]Scheme in AuthenticationOptions, rather than a flag on each middleware. No ambiguity, altho there's one bit of magic where if none are specified and there's only a single scheme registered, we use that scheme as the default.

Pros of the new design

  • AuthN is now a service and 'feels' like AuthZ.
  • Auth handlers can now consume services from DI easily (e.g. see DbContext example).
  • ClaimsTransformation simplified (sane now, rather than wrapped AuthHandler + xform middleware fun), example below.
  • Dynamic schemes supported via replacing the IAuthenticationSchemeProvider service.
  • Eliminated a lot of design/complexity issues that we have consistently run into when trying to fix bugs (no more handler chaining, automatic gone)

Example of using scoped DbContext

This was not really easily accomplished in the old stack since handlers were created by the middleware via protected abstract AuthenticationHandler<TOptions> CreateHandler();

   // ConfigureServices:
   services.AddSchemeHandler<CookieAuthenticationOptions, MyCookieAuthenticationHandler>("Cookie1", o => { o.CookieName = "Cookie1" });
   services.AddSchemeHandler<CookieAuthenticationOptions, MyCookieAuthenticationHandler>("Cookie2", o => { o.CookieName = "Cookie2" });

public class MyCookieAuthenticationHandler : CookieAuthenticationSchemeHandler {
    public MyCookieAuthenticationHandler(DbContext context, ILoggerFactory logger, UrlEncoder encoder) : base(logger, encoder) {
       _dbContext = context;
    }
    
        protected async override Task<CookieAuthenticationOptions> CreateOptionsAsync()
        {
            var options = await base.CreateOptionsAsync();
            UseYourDbContextOnThese(options);
        }
}

Claims Transformation

A lot simpler, new IClaimsTransformation service which has a single method:
Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal)

We call this on any successful AuthenticateAsync call.

   services.AddAuthentication(o => o.ClaimsTransform = p => {
     p.AddIdentity(new ClaimsIdentity());
     return Task.FromResult(p);
  }

To use a more advanced scoped claims transform that uses DbContext's, they would just need to add their own IClaimsTransformation before any AddAuthentication calls.

@dnfclas
Copy link

dnfclas commented Mar 15, 2017

@HaoK,
Thanks for having already signed the Contribution License Agreement. Your agreement was validated by .NET Foundation. We will now review your pull request.
Thanks,
.NET Foundation Pull Request Bot

@HaoK
Copy link
Member Author

HaoK commented Mar 16, 2017

@brockallen @PinpointTownes we've decided today that we are going ahead with this change, the implementation details are still subject to change, but the high level switch from middleware to services/DI/NamedOptions is a go. Definitely let us know if any of your important scenarios are affected, or if you have any ideas for quality of life improvements in the base auth handler classes, since the entire auth code base is opened up for surgery right now...

@HaoK
Copy link
Member Author

HaoK commented Mar 16, 2017

Note to self: need to investigate adding schemes and configuring them outside of ConfigureServices. We can probably cheat and use the AuthenticationScheme.Settings bag to stuff options instances for this scenario

@kevinchalet
Copy link
Contributor

@brockallen @PinpointTownes we've decided today that we are going ahead with this change, the implementation details are still subject to change, but the high level switch from middleware to services/DI/NamedOptions is a go.

Thanks for the info, I'll take a look ASAP. When do you plan to merge this PR?

@HaoK
Copy link
Member Author

HaoK commented Mar 16, 2017

No firm ETA on merge, need to start working on the changes in all the other repos (Identity/servers), which likely will require some more changes here. But we can start settling on implementation choices now. Also the options changes will go in first, so I'd start there if you are interested on how that lands: aspnet/Options#176

SignOutScheme = SignInScheme;
}

if (Options.StateDataFormat == null)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@HaoK correct me if I'm wrong, but Options is a shared instance, right? (at least, when using the default options factory). So this means that the options could be modified concurrently, as InitializeAsync is invoked per request. Isn't that risky as hell?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah so that's why I'm trying to move all the Options manipulations into 'validations' that occur as part of OptionsFactory.Get("schemeName")

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can also switch to using the form of 'scoped' options for the schemes, IOptionsSnapshot for example which wouldn't be shared.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense. Thanks for the clarification.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now that I think about it, that's probably the easiest fix to this issue since the scheme options don't want to share

/// 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 virtual object Events { get; set; }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How bad would it be if AuthenticationHandler had a TEvents generic argument?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤢

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But yeah the events stuff needs some love, I guess every handler does have an events class today.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The events is a bit weird to live on options, maybe we can move it into the AuthenticationScheme instead, as something that gets fed into Intialize(IOptionsFactory<TOptions>, TEvent events, HttpContext)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've always felt configuring the events as part of the options makes it look ginormous. Can we do better if we tease it apart/have a new pattern for hooking events that's not tied to options configuration?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you sure every event isn't overridable? Isn't that the only purpose for the IXyzEvents to exist? basically its the list of virtual methods that would exist if you could plug in your own handler.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you sure every event isn't overridable?

The events are all overridable, but not the infrastructure code that call them: the OAuthHandler.CreateTicketAsync method you used in your sample is quite an exception, as it's the only handler that was really designed to be subclassed.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So moving to a virtual method would allow us to nuke all of the context classes, and just pass them as parameters (or let them access handler directly since its now in the handler, which they couldn't even do before).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I personally like the fact the public hooks are separated from the rest of the infrastructure code, so I'd really love this events pattern to be present in 2.0.0, but heh, your PR, your choice 😄

(also, I'm not a huge fan of having to override a class to just to do something that can be done in a 2-line event delegate)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair enough, @Tratcher had the same opinion, there's so many other things on my list right now, so its most likely not going to change soon


// Holds things like the configured options instances for the handler
// Also replacement for AuthenticationDescription
public IReadOnlyDictionary<string, object> Settings { get; }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still a weird property bag

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah this can go away now, since we can add/remove options dynamically I don't think this is needed at all. Fleshing out the add/remove auth scheme using custom options in the next update

throw new ArgumentException("handlerType must implement IAuthenticationSchemeHandler.");
}
HandlerType = handlerType ?? throw new ArgumentNullException(nameof(handlerType));
Settings = settings ?? throw new ArgumentNullException(nameof(settings));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

syntax

public Type HandlerType { get; set; }

// Holds things like the configured options instances for the handler
public Dictionary<string, object> Settings { get; set; } = new Dictionary<string, object>(); // casing?
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ordinal

AuthenticationScheme = authenticationScheme;
Principal = principal;
Principal = principal ?? throw new ArgumentNullException(nameof(principal));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

syntax

Task SignOutAsync(SignOutContext context);
}

public interface IAuthenticationRequestHandler : IAuthenticationHandler
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Split file


namespace Microsoft.AspNetCore.Authentication
{
public interface IAuthenticationSchemeProvider
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where did the implementation go?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I nuked the old Authentication package and moved Impl into there

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not in this PR anywhere.

@HaoK
Copy link
Member Author

HaoK commented Mar 20, 2017

Updated with some cleanup, nuked settings bag on AuthenticationScheme, RIP ?? throw

@HaoK
Copy link
Member Author

HaoK commented Mar 20, 2017

So in terms of final homes:

Authentication.Abstractions => Http Repo (keeps its own package)
Should we split Authentication into Authentication.Core (lives in Http), and Authentication(.Security)? Basically the core interfaces/implementations for AddAuthentication live in Core, and the base class stuff we use for the Security auth packages would stay in this repo...

So...
Authentication.Abstractions (HttpAbstractions)
Authentication.Core (HttpAbstractions)
Authentication (Security)

@Tratcher
Copy link
Member

Which repos need to depend on Authentication.Core?

loggerfactory.AddConsole(LogLevel.Information);

// Simple error page to avoid a repo dependency.
app.Use(async (context, next) =>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we just have Security depend on Diagnostics so we can stop doing this stuff? I don't think it creates any cycles, it just bumps security up the chart one level.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems fine to me, I'll add it


app.UseAuthentication();

if (string.IsNullOrEmpty(Configuration["facebook:appid"]))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Move this check to ConfigureServices


namespace Microsoft.AspNetCore.Authentication
{
public interface IAuthenticationSchemeProvider
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not in this PR anywhere.

/// <param name="app">The <see cref="IApplicationBuilder"/> to add the middleware to.</param>
/// <param name="options">A <see cref="CookieAuthenticationOptions"/> that specifies options for the middleware.</param>
/// <returns>A reference to this instance after the operation has completed.</returns>
public static IApplicationBuilder UseCookieAuthentication(this IApplicationBuilder app, CookieAuthenticationOptions options)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bring these back as Obsolete Error with a fwlink.

public SignInContext(HttpContext context, string authenticationScheme, ClaimsPrincipal principal, AuthenticationProperties properties)
: base(context, authenticationScheme, properties)
{
Principal = principal ?? throw new ArgumentNullException(nameof(principal));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

syntax

@@ -38,25 +56,20 @@ public class RemoteAuthenticationOptions : AuthenticationOptions
/// Gets or sets the authentication scheme corresponding to the middleware
/// responsible of persisting user's identity after a successful authentication.
/// This value typically corresponds to a cookie middleware registered in the Startup class.
/// When omitted, <see cref="SharedAuthenticationOptions.SignInScheme"/> is used as a fallback value.
/// When omitted, <see cref="SignInScheme"/> is used as a fallback value.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment is now referencing itself?

using System.Reflection;
using Microsoft.AspNetCore.Http;

Copy link
Member Author

@HaoK HaoK Mar 21, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Tratcher they are here, but perhaps hard to find in the 178 file diff... maybe easier to just view via commits

@@ -71,8 +71,17 @@ protected AuthenticationHandler(IOptionsService<TOptions> optionsFactory, ILogge
/// <returns></returns>
public virtual Task InitializeAsync(AuthenticationScheme scheme, HttpContext context)
{
Scheme = scheme ?? throw new ArgumentNullException(nameof(scheme));
Context = context ?? throw new ArgumentNullException(nameof(context));
if (scheme == null)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Tratcher See AuthenticationHandler here

@HaoK
Copy link
Member Author

HaoK commented Mar 21, 2017

@Tratcher I'll break this PR up into 2 new PRs to make things more managable, I'll start the PR for Abstractions/Core in HttpAbstractions and open a new one for only the Security changes

@HaoK
Copy link
Member Author

HaoK commented Mar 23, 2017

Updated removing the stuff that is being moved to HttpAbstractions via: aspnet/HttpAbstractions#798

Hopefully easier to review

@kevinchalet
Copy link
Contributor

@HaoK hum, is there a particular reason to move so many types to HttpAbstractions? Of course, IAuthenticationHandler and the types used to register the handler in the DI need to live there for layering reasons, but is it really useful to move things like AuthenticationMiddleware there?

@Tratcher
Copy link
Member

@PinpointTownes I think they're needed to write tests for HttpSysServer and IISIntegration

@kevinchalet
Copy link
Contributor

AuthenticationMiddleware's role is extremely limited: it simply adds a special feature to track the original paths, calls IAuthenticationSchemeProvider to get the default sign-in scheme (which is kinda the equivalent of "active authentication") and calls handler.HandleRequestAsync() for "promiscuous handlers". AFAICT, none of the HttpSysServer/IISIntegration tests couldn't live without this type. Do you have any specific example in mind?

@HaoK
Copy link
Member Author

HaoK commented Mar 23, 2017

The middleware isn't critical, I can move that back along with UseAuthentication to Security. What's critical is the core services/AddAuthenticationCore as you can't even use Auth without those. I'll move the Middleware/Use out now

@HaoK
Copy link
Member Author

HaoK commented Mar 23, 2017

Updated, UseXyz's are back as obsolete/throw not Supported. Also brought back the AuthMiddleware from HttpAbstractions.

@@ -12,26 +12,26 @@ namespace Microsoft.AspNetCore.Builder
public static class CookieAppBuilderExtensions
{
/// <summary>
/// Obsolete, see TODO: fwdlink
/// Obsolete, see https://go.microsoft.com/fwlink/?linkid=845470
/// </summary>
/// <param name="app">The <see cref="IApplicationBuilder"/> to add the middleware to.</param>
/// <returns>A reference to this instance after the operation has completed.</returns>
[Obsolete]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Put the link in the Obsolete comment

@@ -16,7 +16,7 @@ public static class CookieAppBuilderExtensions
/// </summary>
/// <param name="app">The <see cref="IApplicationBuilder"/> to add the middleware to.</param>
/// <returns>A reference to this instance after the operation has completed.</returns>
[Obsolete]
[Obsolete("See https://go.microsoft.com/fwlink/?linkid=845470")]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, it's only a warning by default, you have to set IsError true
https://msdn.microsoft.com/en-us/library/system.obsoleteattribute.iserror(v=vs.110).aspx

@HaoK
Copy link
Member Author

HaoK commented Mar 31, 2017

Replaced with #1170

@HaoK HaoK closed this Mar 31, 2017
@HaoK HaoK mentioned this pull request Apr 22, 2017
13 tasks
@HaoK HaoK deleted the haok/named branch August 7, 2017 16:59
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants