-
Notifications
You must be signed in to change notification settings - Fork 597
Conversation
Hi @HaoK, I'm your friendly neighborhood .NET Foundation Pull Request Bot (You can call me DNFBOT). Thanks for your contribution! The agreement was validated by .NET Foundation and real humans are currently evaluating your PR. TTYL, DNFBOT; |
Other things to consider for later: (can we get rid of the accept logic in the Challenge/SignIn/SignOut) handlers with the new model do we still care about multiple handlers getting a chance at each scheme? Would drastically simplify the context's/manager logic if we could get rid of that stuff |
W00t |
{ | ||
internal static class HttpContextExtensions | ||
{ | ||
internal static IHttpAuthenticationFeature GetAuthentication(this HttpContext context) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do we still need this? Wouldn't it be a service now?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We don't, vestige that was copied over and not deleted yet
So I got the cookie sample working on top of the new stack... it became clear that the AuthenticationManager's job really is to maintain the per request cache that maps authenticationScheme to the worker instance per scheme. There's also a problem with how AddCookies is sugar ontop of AuthenticationOptions, as it needs to add a transient CookieAuthenticationHandler for itself... but it doesn't have access to the services hanging where it is right now... |
You know, we could keep the auth APIs on HttpContext as extension methods, since to get rid of the IHttpContextAccessor we probably have to start taking HttpContext for all IAuthenticationManager Authenticate/Challenge/SignIn apis... So we could go back to something like: context.AuthenticateAsync("cookies") => context.RequestServices.GetRequiredService<IAuthenticationManager>().AuthenticateAsync(context, "cookies") |
@HaoK in your example in the PR description, can you also show a simple middleware with the raw calls to |
That might be cleaner although the settable |
Yeah I don't think we need the feature, just some extension methods to hang the manager APIs directly off HttpContext. |
You need to add more code to your samples: public static class HttpAuthenticationExtensions
{
public virtual async Task<AuthenticationTicket2> AuthenticateAsync(this HttpContext context, string authenticationScheme)
{
return context.RequestServices.GetRequiredService<IAuthenticationManager>().AuthenticateAsync(context, authenticationScheme);
}
} |
Updated the sample and the example at the top of the PR with a middleware that simulates basic login flow:
|
@davidfowl how do you feel about each auth scheme being a top level thing (that internally calls AddAuthentication with its own configuration). Only thing that irritates me is that we end up calling AddAuthentication a bunch of times, but they are TryAdd's so its safe... This lets the auth handlers properly add their handlers.
|
@PinpointTownes @brockallen take a look and see how this feels, I'm working on OAuth/Remote today now that cookies is functional |
I'm traveling this week. Won't be back until next week.
…On Tue, Dec 13, 2016 at 9:36 PM Hao Kung ***@***.***> wrote:
@PinpointTownes <https://github.com/PinpointTownes> @brockallen
<https://github.com/brockallen> take a look and see how this feels, I'm
working on OAuth/Remote today now that cookies is functional
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
<#1065 (comment)>, or mute
the thread
<https://github.com/notifications/unsubscribe-auth/AAnpFK6uW_kgCqau5vkF-J0Lodx4k0iBks5rHwHjgaJpZM4LLAQP>
.
|
@davidfowl Okay so the remote middleware requires the ability to handle entire requests which cookies does not, so the next iteration will add back some kind of HandleRequest entry point where every scheme gets a chance to see if this is a request for them (otherwise let it continue)... So much for the AuthenticationMiddleware only setting httpContext.User. I'm now more open to your idea of making it a IStartupFilter, its pretty much mandatory for remote auth middleware |
@Tratcher any thoughts on any improvements we can make to the control flow logic we have today? Personally I hate the bool, so we should do something to replace:
We could use the Enum we have today I guess?
|
SchemeMap[name] = builder.Build(); | ||
} | ||
|
||
public string DefaultAuthenticationScheme { get; set; } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These defaults for auth and challenge scheme belong in Authorization, not Authentication.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How is that an authorization concern? That would be great if we could avoid making the same mistake twice 😄
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[Authorize]
is the component that calls this and it can be configured via DefaultAuthPolicy, so I think this one is redundant. Same for DefaultChallengeScheme.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The fact the authorization filters are currently responsible of determining the applicable scheme doesn't mean it's the right design nor really an authorization concern. In Web API 2 OWIN, the separation of concerns was much better/clearer, thanks to HostAuthenticationFilter
/HostAuthenticationAttribute
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Now is certainly a good time to revisit AuthZ if anything needs to move there, there's a few breaking changes I have on my todo list there, namely a similar change where bool AuthorizeAsync
needs to become AuthorizationResult AuthorizeAsync
similar to how we are beefing up HandleRequest
|
||
public Type HandlerType { get; set; } | ||
|
||
// Holds things like the configured options instances for the handler |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's weird that Options isn't first class.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Tradeoff vs generic hell, once I implement a few more auth handlers, I'll do a cleanup/rollup pass to see if we need anything at this layer but there's is an equivalent AuthenticationSchemeHandler<TOptions>
that is similar to AuthenticationHandler<TOptions>
today. The options don't need to be typed at the scheme builder layer, since these untyped options are only used by the scheme handler instance later, when it can just cast it to the type it knows. Its not related to the global singleton AuthenticationOptions which deals only with the various schemes and handler types.
} | ||
|
||
services.AddDataProtection(); | ||
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
remove IHttpContextAccessor?
public class ChallengeContext | ||
{ | ||
public ChallengeContext(string authenticationScheme) | ||
: this(authenticationScheme, properties: null, behavior: ChallengeBehavior.Automatic) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should look at moving the ChallengeBehavior.Automatic logic to Authorization.
// handler instance cache, need to initialize once per request, manager lifetime should be per request | ||
private Dictionary<string, IAuthenticationSchemeHandler> _handlerMap = new Dictionary<string, IAuthenticationSchemeHandler>(); | ||
|
||
private readonly IHttpContextAccessor _accessor; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This was going away right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah its on the todo list, possibly we modify the signature of all of the methods to explicitly pass in an HttpContext instead.
} | ||
if (String.IsNullOrEmpty(Options.CookieName)) | ||
{ | ||
Options.CookieName = CookieAuthenticationDefaults.CookiePrefix + Scheme.Name; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's weird that you resolve all of these Options on the first request rather than when the middleware is created. This may cause some weird race conditions.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The handlers are initialized per request, as enabling DI activation is a goal now. So each auth options is not really like our IOptions instances, each is actually per named scheme instance, similar to what we have today with the UseXyzAuth(new XyzOptions())
, and why I stuff it into the dictionary of each scheme... (RIP named options)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree with @Tratcher. It's not only weird, it's dangerous (and potentially a perf' disaster if you receive thousands of requests when the app warms up, as there's no locking preventing InitializeAsync
from being called multiple times).
There's also another BIG problem with this design approach: configuration errors cannot be detected at startup and won't prevent the app from loading successfully (so each time it will receive a new request, it will try to initialize the handlers... and fail miserably).
Task SignOutAsync(SignOutContext context); | ||
} | ||
|
||
public abstract class AuthenticationSchemeHandler<TOptions> : IAuthenticationSchemeHandler where TOptions : class |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perf: How can we avoid re-creating these handlers on every request? Is there some way we can make them stateless and store the relevant state in the HttpContext?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah if we end up trying to make this real, we need to spend some cycles on this part. The next iteration that's coming introduces a SchemeHandlerCache service, since both the single AuthMiddleware, and the manager need a central place to access the per request handler instances with all the state... The middleware needs the instances to do HandleRequest, while the manager needs them to handle the auth APIs. Does it make sense to split up these responsibilities at all, or do we like everything hanging off of IAuthenticationHandler?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Specifically, I'm adding back some kind of Task<AuthenticationRequestResult> HandleRequest()
back for the remote auth flows, does this make sense to live in IAuthenticationHandler still.
Honestly, this sounds like an interesting approach, but I still have no idea what concrete problem(s) this huge API change is supposed to fix (I'm currently blocked by the fact the migration path will be extremely painful, without being sure the new design solves something that we can't fix in the existing architecture). Curious: what's your plan for the host-provided authentication handlers? |
Please don't make that |
@HaoK note: I'm not 100% sure |
Migration is painful, no denying that. Maybe we deprecate the old system and make a new system side by side. That would let 3rd party auth middleware gradually migrate. The concrete things this solves:
|
@PinpointTownes don't worry the request handling path is coming back, I'm looking at unifying the request flow processing that we already have today in the handlers, with the top level HandleRequest in IAuthenticationHandler, it feels a little weird that they are inconsistent today, hopefully that will result in a single Result class rather than the 2, 3, or 4? we have today... |
@PinpointTownes I think the hosts can just follow the new pattern and add their own authentication handlers/schemes. IIS today doesn't look to be doing anything special, WebListener looks a bit different with the explicit schemes it handles like NTLM/Kerberos, but is there something that sticks out to you where the new pattern breaks down for the hosts?
|
@davidfowl AFAICT, none of the points you list are really "issues" (they are more like general architecture concerns).
Of course it is, it's just handled differently: instead of being handled at the handler level (current design), it's done in
Sure, but for a reason: it doesn't support the same features set yet (e.g it doesn't seem to support "automatic challenge" nor converts raw 401 responses into "challenges").
@HaoK no no, I'm just curious to know how the layering issue will be solved (IIRC, the host-provided handlers directly implement |
@PinpointTownes We should consider removing the automatic 401 -> Challenge and require auth to be invoked explicitly. This made sense in Katana because challenges were always processed on the way out, but now they're processed immediately except for these 401s so you have two disconnected stages. This caused the original bug you complained about in #930. |
I'm fine with that, but this will be blocking for "pure OWIN" applications/frameworks (e.g like NancyFX), where the ASP.NET Core authentication infrastructure is not directly available. Returning a 401 status code was a convenient way to trigger a challenge. |
True, but we could move that logic to the owin adapter, or provide something similar in the adapter. |
I'd prefer a clean break but I understand why it might not be a good idea to break the world. That's a lot of customer pain to deal with. Hence these ugly looking changes.
|
At least with obsolete extensions we can give the developer direct feedback about what changed. If we just remove the extensions then it fails to compile and they have no idea why. We may not even have to implement the extensions, just make them throw :-) |
I'm pretty sure that's what migration guides are for |
Cleaned things up a bit, pushed the goop into |
@@ -1100,7 +1103,7 @@ private static TestServer CreateServer(Action<GoogleOptions> configureOptions, F | |||
}; | |||
}); | |||
services.AddCookieAuthentication(TestExtensions.CookieAuthenticationScheme); | |||
services.AddGoogleAuthentication(configureOptions); | |||
//services.AddGoogleAuthentication(configureOptions); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Mixing old and new patterns of registration also don't matter since order no longer has any real meaning to the schemes...
+1 for making breaking changes in v2. People need to understand that this API works differently. [Obsolete] is not good enough IMO |
@PinpointTownes @leastprivilege thanks for keeping an eye on this PR and staying involved. Honestly, we're still at the point where we're playing with a bunch of different ideas. I mean, wouldn't it be great if we could preserve compatibility as much as possible and have the new features that this design enables? (And eliminate as many problems as possible with the old design?) Ok, yeah, that's quite optimistic, but that's part of what this PR is about! We've had some design meetings within the team to see what we can come up with. I agree it could be worse if we make everything look compatible but it behaves very differently, potentially introducing serious app bugs. But we have to balance that with breaking dozens of NuGet packages (ahem, yours!), and countless apps that use the old system. And then perhaps the hardest problem of it all: breaking peoples' understanding of how this stuff works! So, we're not quite sure how this will all end up, but we love what the new design brings, and we've been reading all your reactions to it and trying to account for as much of it as possible. I hope that once we end up on a design that we like, we'll be able to make it clear that we have an official proposal, and will of course seek a round of feedback on that (probably in a new PR, so that we can start the discussion fresh). |
Unfortunately, I have the weird feeling that's where this PR is currently heading to. I haven't played with the latest commits yet, but I'm sure there are also security concerns when using authentication middleware with branching. E.g, in this snippet, won't the cookie middleware execute even for non-API calls? (since it will register the cookie handler regardless of the branching and execute it via the authentication middleware registered by app.UseJwtBearerAuthentication(...);
app.UseWhen(context => context.Request.Path != "/api", map => {
map.UseCookieAuthentication();
});
app.UseMvc();
Existing packages targeting ASP.NET Core 1.x will most likely break due to massive changes in .NET Core 2.0 anyway (see aspnet/SignalR#155 (comment) for a concrete case), so if an important design change must be adopted, it's the perfect moment.
That would be great if you could list the concrete things "the new design brings", 'cause it's not clear to me yet. |
Map is an interesting one, and its more or less gone for Auth in the new design. But that example is another instance of bearer/cookies conflicting and a necessary workaround from the old middleware not playing nicely together. In the new stack, there is only a tiny bit of overlap, where each handler is given a chance to handle the request, but there's no automatic challenge like conflict that you need to resolve via conditional existence of schemes. Each scheme is part of the authentication service, and since we don't have support for different services per branch, every scheme will appear in every branch. This isn't that different today from AuthZ, where there's no way to limit policies to a specific branch either, and since policies can reference schemes, having schemes only exist in some branches is a bit weird anyways. |
Not really gone if you decide to keep your "legacy authentication" extensions
I'm not sure I would consider that as a workaround in this particular case, as it's super common to create subsections in your apps, that can possibly use different schemes.
The problem with the sample I mentioned is not with the automatic challenge part, but with automatic authentication: since the cookie handler will be called despite the path restriction, it will populate |
UseLegacyAuthentication as it stands doesn't really preserve the old behavior (and its not really trying to, its trying to implement the old API behavior using the new stack as best as it can), its just adding the new authentication middleware (if one hasn't been added already), and any schemes added by one branch will be available to all branches as soon as its run once since the service is shared. |
Updated first comment at the top with a cleaned up up to date summary, including a section about some of the things we like about the new design (also my main focus was on fixing things that were existing issues(claims transformation, per request events, using db contexts) and also the things that were hampering our ability to make improvements, aka RIP automatic) |
Made ISystemClock a service since this is now easy (unlike when I tried to do this before) |
Summary of next iteration plan after meeting with @davidfowl @Tratcher @Eilon:
|
How will that work in practice? Will "legacy" middleware be visible using the new If the two worlds can't work together, it would be way better to remove the old APIs.
👍 for removing the "naked 401" handling, but if you remove |
We can keep the DefaultChallengeScheme, I was just talking about killing the naked 401 logic that is in the new auth middleware. For interop basically the only thing we will support is existing 1.x packages continuing to work, so old identity +with old 1.x providers using the old APIs will continue to work. So in practice, this just means we are preserving the HttpAuthentication interfaces, so old httpContext.Authentication APIs with the old 1.x packages and providers and middleware will continue to do what they always did. The new stack won't affect that at all since the new IAuthenticationService doesn't depend on the old HttpAbstractions IAuthenticationManager at all. Any kind of interop between the two will be left as a choose your own adventure exercise :) But basically old 1.x providers will continue to work in MVC2.0 side by side with the new stack, but upgrading to 2.0 will require moving to the new stack, and all the old middleware/interfaces will be deleted/updated. I'm doing some of the incremental cleanup from the review in this PR, but I'll start a new one when I takeover the existing classes/namespaces in the next iteration. |
Regarding identity in 2.0, and everything else, it will be updated to only use the new stack (i.e. enumerating only the authentication schemes by default). The old auth will still be there if you registered them, but identity, and MVC for that matter won't pick them up. For MVC/AuthorizeFilter there was some brief discussion about how we would add something like LegacyAuthFilter that you could plug in to restore talking to the old context.Authentication.Xyz. But [Authorize] would target the new auth system only by default. |
- Removed UseLegacy - Revisit options initalization again
So it will be pretty much like using Katana authentication middleware on ASP.NET Core... totally unusable in practice? If even authorization doesn't work with the legacy middleware, there's really no point keeping them in 2.0. |
What do you mean about 'keeping them in 2.0'? The plan is to nuke all the old code in Security as part of 2.0. But you can still use old 1.1 packages in 2.0 apps, and that scenario should still work. (where they use the old AuthenticationManager on context to communicate with each other) which would still exist and behave the same.. |
Ah great. This point wasn't clear to me
Hum, are you sure about that? The major version increment means things won't be guaranteed to be binary compatible. See aspnet/SignalR#155 (comment) for a concrete case. |
Yeah I believe the plan is for most to be binary compatible, but basically the interop should be limited to preserving the old HttpAbstractions authentication functionality. But 2.0 Security/IIS/WebListener will depend on a new Authentication.Abstractions package which will be in the HttpAbstractions repo but in the Authentication namespace and contain all the new interfaces/extension methods. We can choose to keep the existing names if we want now (IAuthenticationHandler vs IAuthenticationSchemeHandler etc... I'll start the new PR tomorrow and we can revisit naming there |
This PR is effectively abandoned now, new iteration PR is here: #1113 |
TL:DR summary
IAuthenticationService
with the sameAuthenticate/Challenge/SignIn/Out/ForbidAsync
API.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 anAuthenticationScheme
instance for XyzUseAuthentication
is required, which takes care of the old 'automatic' challenge, authenticate, in addition to giving each scheme a chance to handle the request like the old middlewares did.Default[Authenticate/Challenge/SignIn]Scheme
inAuthenticationOptions
, 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
IAuthenticationSchemeProvider
service.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();
Composing configuration of a scheme's options looks like this (Identity needs this):
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.To use a more advanced scoped claims transform that uses DbContext's, they would just need to add their own
IClaimsTransformation
before anyAddAuthentication
calls.Interop options (Currently exploring Partial interop)
UseLegacyAuthentication
which the old UseXyzAuth methods would call with the XyzOptions, and this would add a newAuthenticationScheme
to the new service.Example: Updated with twitter, google social auth + cookie sign in