-
Notifications
You must be signed in to change notification settings - Fork 597
Redesign automatic authentication choice mechanism #1061
Comments
Tagging interested parties: @PinpointTownes @leastprivilege @brockallen |
After a bit of experience and reflection on how all of the authentication middleware works today, my sense is that the design of the authentication middleware pipeline that was done for katana is not generally appropriate here in ASP.NET Core. IOW, the whole linked list wireup per-request seems unnecessary given that we have DI and that we could inject the main logic (the handlers). There would still be the need for middleware to trigger the handler, but that's about it. API calls like Authenticate/Challenge/Signin/Signout could simply be to a service that was obtained via DI (and not via the handler chain wired up during the request). Perhaps then more specialized interfaces (ICookeAuthentication, IExternalAuthentication, etc?) would be necessary for this. The design of a one-size-fits all authentication manager API seems like an ill suited abstraction. Of course the DI system is not a panacea and has its own issues with ambiguity when registering multiple instances of the same type. Not sure how earth shattering this suggestion would be in practice, but again the linked list wireup seems brittle and the source of many problems. |
I wasn't involved in the original design but would be fine re-thinking it for 2.0 now that we have a DI enabled system. The decorator pattern we use in the authentication manager wouldn't work in DI either so we'd need an alternate pattern for chaining auth handlers.
Not sure, we'd need to look at both the consumers and implementors of the interface to see who would need to implement it and how it would get triggered. I'd like to keep the Challenge/Forbid calling pattern as I think it makes sense. Just some initial thoughts... |
The reason I suggest explicit interfaces for certain types of functionality is because sometimes the generic gestures (Challenge for example) don't really work. Should it mean show the access denied page, or should it mean go login to google. Having the framework guess hasn't worked in some cases, so an explicit API might just be easier. |
Disclaimer: I'm a bit behind here as I wasn't involved here so bare with me as I get up to speed. @brockallen Easier for who? What are the actors in the system that would have to know about each interface?
I don't think the framework was guessing anything. Challenge targeted a specific middleware or thing that looked like middleware. The biggest complication besides the auth handler being a property on the http context (that's kinda weird), is the active/passive behavior. That was all to keep I might make sense to spell out all of the interfaces we have now and all the frameworks and touch points to make sure we understand where the pains are. |
DI is a non-starter when common apps have 2+ instances of cookie auth. Many of the other auth middleware can be duplicated too. I don't see how coming up with a custom interface per scenario helps when you have to somehow associate those with specific middleware instances. |
Don't dismiss DI it just yet. There might be more DI friendly ways of composing the system. Let's just draw out the components and interactions we currently have. |
Yeah I think we can maybe end up with something more similar to authZ. Which has a similar problem of configuring multiple named policies. The only real difference between Authentication is that the handlers aren't global and are per scheme. But I think something maybe?
ConfigureAuthentication(options => {
options.AddCookies("cookie1", o => o.Handler.OnRedirect = someMethod)(
options.AddCookies("cookie2");
options.AddGoogle("Google", o => o.ClientId = "secret"):
};
Where basically you configure the handler instance in a new single AuthenticationOptions which would contain a named instance for each scheme which would have its own configuration and handlers (similar to authZ policy configuration today)
And then we could have a single UseAuthentication middleware in the app that builds the authentication infrastructure using the fully configured options instead of relying on middleware wire up. This would let us validate everything as well much easier than what we have today.
… On Dec 8, 2016, at 10:42 AM, David Fowler ***@***.***> wrote:
Don't dismiss DI it just yet. There might be more DI friendly ways of composing the system. Let's just draw out the components and interactions we currently have.
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub, or mute the thread.
|
@HaoK Ohhh, nice. What do you think the auth service would look like? I think we would nuke the IAuthenticationManager from |
@davidfowl Yeah we can finally get rid of that weirdness with half of Auth living in HttpAbstractions, and have |
Also solving the active problem should be fairly straightforward with a central single AuthenticationOptions:
With the only magic being, if no default is specified, the default is only implied when there's a single auth scheme configured. |
Yeah we definitely should nuke the weird We should also revisit the weird AuthenticationProperties/Description legacy stuff we carried over as that seems way more complicated than it should be...
|
Who implements the interface? Today it's possible for non middleware components to implement an auth handler (IIS Integration does it as well as WebListener). How would those change as a result? Some implementations outside of the security middleware:
Call sites that use the
The status code needs to be changed as a result of calling Forbid or Challenge. If we have a service we'd need to inject the Security/src/Microsoft.AspNetCore.Authentication/AuthenticationMiddleware.cs Lines 65 to 86 in 219617f
It's funny, after looking at this a bit, the middleware basically has no logic... I think we're on the right track. |
/cc @PinpointTownes |
Today the manager is an abstract class, so there probably would just be our default implementation. I'm not super familiar with the host auth handlers, @Tratcher will have to chime in with how these might work... I'm guessing maybe these would look like this?
Also since we want to get away from the singleton lifetime that middleware forces on us, we can
Note: for our Security based auth handlers, we'd keep a singleton handler per auth scheme type, and use an EventsType service/instance which would expose the extensibility points (something like: #1055) |
Hao, we should have a meeting where we can show you how we use this stuff in a real app today. |
That seems reasonable, @blowdart can you set something up? |
@danroth27 is already on it. |
cc @Eilon |
@HaoK forcing the use of the IHttpContextAccessor makes me sad, especially because it's not in by default. We'll need to revisit that. |
Sorry for the late feedback (I'm sick and stuck in my bed... 😢) To be honest, I'm not a huge fan of the "full DI" approach suggested by @HaoK and @brockallen as it will likely introduce a whole new set of issues without really solving the initial one. First issue: how do you control when the authentication handler is invoked if it's automatically registered for you by the security stack? In practice, it's super common to register the security middleware after things like the static files middleware, the URL rewriting module or the exception page handler to avoid wasting CPU cycles validating a cookie that won't be used at all. Second issue: how do you support path-specific authentication scenarios with this new design? // Add a middleware used to validate access tokens and protect the API endpoints.
app.UseWhen(context => context.Request.Path.StartsWithSegments("/api"), branch => {
branch.UseOAuthValidation();
});
// Warning: starting with ASP.NET Core 1.1.0, having multiple authentication middleware
// with AutomaticChallenge = true is no longer supported. To ensure the validation and
// cookies middleware don't collide, app.UseIdentity() is only invoked for non-API calls.
app.UseWhen(context => !context.Request.Path.StartsWithSegments("/api"), branch => {
branch.UseIdentity();
}); Personally, I'm quite happy with the existing "middleware-like"/Russian doll design, tho' there are things that can definitely be improved:
Note that it will require revamping the cookies middleware, as it uses fields to store intermediate state objects (e.g to determine if a cookie should be renewed) 😄 |
@PinpointTownes we still have a single instance of UseAuthentication that would be required, so you'd still be able to place that middlewhere in the proper location (after static files, etc) so I don't think this affects the ability to skip auth for some requests. Also if there an equivalent way of splitting your configure services similar to the app.useWhen, you could just configure the services differently based on what branch of the app the request is in... @davidfowl Actually maybe we wouldn't need the handlers to use IHttpContextAccessor, the IAuthenticationHandler methods today already take context objects, so we could start stuffing the httpContext into each context when the new single middleware invokes the handler?
|
That's pretty vague. The current approach was inherited from katana with tweaks that happened along the way to make it ASP.NET Core ready. I absolutely think we should consider the alternative design and look at the 2 things side by side and decide if we should do anything for the next major release or not. There would still be an authentication middleware in the pipeline that would use these services.
You never needed to if you challenged by scheme. |
@PinpointTownes if all you want to do is change the default behavior of |
And that should work even with AutomaticChallenge disabled once a scheme is specified. |
Well, it's almost a "must" in practice when dealing with stoopid JS clients, as we can't control the request headers. It's not really SignalR-specific, tho'. The good thing is that it's completely standard, so it's not really a problem. |
@PinpointTownes except AFAIK, our middleware doesn't read from the query string (does it?). Also query string limits with JWT tokens..... |
@HaoK sure, but 1. you can't set the scheme per-path. 2. it's tied to the authorization stack, so you can't implement "optional authentication" actions like you could with Web API 2 and its OWIN/Katana
@davidfowl it does not... by default, but it's super easy to change thanks to the events model. Here's how I do it with the aspnet-contrib validation middleware: https://github.com/aspnet-contrib/AspNet.Security.OpenIdConnect.Samples/blob/master/samples/SignalR/HelloSignalR/Startup.cs#L28 (the JWT bearer has a similar event, but the name differs). |
See #1062 for updated guidance regarding the current APIs. With a little more sugar around AddAuthorization/DefaultPolicy/AddPolicy I think we could completely remove |
In fact, we could even update the VS templates for 1.0.x to use AuthPolicies rather than AutomaticChallenge. |
One thing to recognize here is that really there's only the need one for cookie handler, and one google handler, and one oidc handler ever in DI. What we need multiple of is settings. IOW, we just need one instance of the code, and then possibly multiple named settings/config. Decoupling the code from the config would be useful, especially for OIDC. I had a customer that used Katana that needed hundreds of the ws-fed middleware, and it wasn't pretty to make that happen. For this redesign if it's possible to decouple those that'd be great. And then taking that one step further, the ability (at least for the external config like OIDC and eventually ws-fed) if those could be loaded from a "store" style interface. Perhaps by default it's just an in-memory implementation for the common scenarios where there are only one or two, but it allows for the possibility of a database to back the OIDC config. |
@brockallen yup that's where I was at too, with a single instance of each type of handler, and a named configuration/settings for each "scheme" in DI. Once we have that, it would be fairly easy to load the config/settings from another source, since the settings are just normal options, so this would already more or less work with options/config binding.
But we might need to spend some cycles making it easier to read options/configuration from a database for your second step (cc @divega) |
@HaoK @brockallen That's where we were all at but @PinpointTownes isn't on the same page. At this point I think we need to prototype the other system and show key scenarios in both of them, including the ones @PinpointTownes feels strongly about. Then we can weigh the pros and cons of each. |
Yep... who knows -- this all might just prove that what's already here is good enough (with some adjustments). But it's still worth the exercise. |
Ok I will start a PR next week with some real working code.
|
@davidfowl I'm always skeptical when hearing that DI is involved in multi-instances scenarios. Last time you did that, it was to bring back the |
Take 3, well try it again and all party on the PR. |
@HaoK, this thread has wandered quiet a bit, care to summarize the issues you plan to address in your PR? |
I was going to start from the public service area first.
Starting with something like:
ConfigureAuthentication() => AuthenticationOptions + AuthenticationScheme[Builder] to represent how we would register the name scheme/handler which would be the replacement for today's events + AuthenticationDescription.
IAuthenticationSchemeProvider which would be responsible for resolving named schemes and providing the actual handler and configuration. This would be the extensibility point for plugging in stuff like database driven schemes. This would just be the master AuthenticationOptions by default.
Also the single UseAuthentication() master middleware that would consume the uber options and dispatch/create the appropriate handlers for a request.
And finally removing auth from httpabstractions so moving everything we need into the base auth package.
|
Sneak preview of what I've distilled from this thread so far, in case there's any early feedback: https://gist.github.com/HaoK/0197141b932bcf45d99dcb5e6eb70374 |
Started the PR here: #1065 |
Tracked via #1179 |
The current mechanism for choosing which authentication middleware runs by default has been fragile and caused pain when more than one middleware has been set to automatically run, leading to what is, at best, undefined behaviour. The undefined behaviour changed in 1.1, but some folks were relying on it.
The basic idea behind automatic challenging middleware was that only one middleware should be configured to automatically challenge, however historically this was configured by a property on the individual middleware, and, as middlewares cannot inspect each other, it was too easy to set multiple middlewares to automatically challenge.
The new behaviour should take the decision of what middleware to use to issue a challenge when no middleware is specifically selected out of the hands of the middleware, and into the realm of authorization policy. It's envisaged that the default policy would contain information on the elected middleware to use to issue a challenge, and syntactic sugar added so you can set it as a property on authorization, rather than having to write a complete policy.
This leaves an open question of what happens when Challenge is called manually, without specifying which middleware to use - do we use the one from the default policy, or remove automatic challenge at that point and force a developer to specify the middleware to be used?
The text was updated successfully, but these errors were encountered: