-
Notifications
You must be signed in to change notification settings - Fork 10.3k
Authentication Handler as an Endpoint #17615
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
Comments
It can't, as authentication needs to happen before the endpoint is executed. Endpoints are... endpoints, not middleware pipelines 😊 |
I think I should have explain my point more. I'm not talking about the authentication process itself. I'm talking about the endpoints which I'm redirecting to from Identity Providers like |
No there's not. Those aren't really endpoints, but ephemeral locations authentication intercepts, which, as Kristian points out needs to happen before normal endpoint code would execute. What do you think would be gained by making them endpoints? |
This is something I've briefly discussed with @rynowak. No, you couldn't convert all of an auth handler to endpoints, but moving some of the callbacks like This part might scale better if you had a lot of auth handlers, routing is much more optimized for this scenario. The current code allocates every auth handler on every request (the schemes are singletons but the handlers are scoped). |
+1 -- we discussed this somewhat already, but auth handlers are middleware for most requests, but endpoints for some specific requests (signouts and challenge callbacks). The signouts and challenge callbacks would benefit from being endpoints, I think -- mainly for the scale issue once you need 100+ OIDC/Ws-Fed/SAML handlers in your pipeline. |
The transition might even be able to be done in a non-breaking way.
|
This also sounds semi-related to #6993? |
On a related perf note, this code even allocates handlers that don't implement IAuthenticationRequestHandler such as JwtBearer. We could probably add a scheme option to suppress that too. |
Yes this was my intention. I should have specify better. |
Could you start by outlining a detailed design proposal? This has several moving parts that we need to make sure are accounted for before starting on a PR. |
Triage: Seems reasonable, but definitely needs a bunch of design. |
Here's what I thought. We can add a public class AuthenticationOptions
{
public bool UseEndpoints { get; set; }
} Adding and removing endpoints could happen in public interface IAuthenticationSchemeProvider
{
EndpointDataSource GetEndpointDataSource();
//OR
EndpointDataSource EndpointDataSource { get; }
} I have two thought on how to get endpoints for one scheme
public class AuthenticationScheme
{
public IEnumerable<Endpoint> Endpoints { get; set; }
}
public interface IAuthenticationRequestHandler
{
void ConfigureEndpoints(IEndpointRouteBuilder endpointRouteBuilder);
[Obsolete]
Task<bool> HandleRequestAsync();
} Since we have to cover adding and removing scheme in runtime a dynamic EndpointDataSource is needed. Well I found one in unit tests and with some change it fits our needs. public class DynamicEndpointDataSource : EndpointDataSource
{
private readonly ConcurrentDictionary<string, IEnumerable<Endpoint>> _endpoints;
private CancellationTokenSource _cts;
private CancellationChangeToken _changeToken;
public DynamicEndpointDataSource()
{
_endpoints = new ConcurrentDictionary<string, IEnumerable<Endpoint>>();
CreateChangeToken();
}
public override IChangeToken GetChangeToken() => _changeToken;
public override IReadOnlyList<Endpoint> Endpoints => _endpoints.SelectMany(pair => pair.Value).ToArray();
public void AddEndpoint(string schema, IEnumerable<Endpoint> endpoint)
{
_endpoints.TryAdd(schema, endpoint);
TriggerChange();
}
public void RemoveEndpoint(string schema)
{
_endpoints.TryRemove(schema, out _);
TriggerChange();
}
private void TriggerChange()
{
// Capture the old tokens so that we can raise the callbacks on them. This is important so that
// consumers do not register callbacks on an inflight event causing a stackoverflow.
var oldTokenSource = _cts;
var oldToken = _changeToken;
CreateChangeToken();
// Raise consumer callbacks. Any new callback registration would happen on the new token
// created in earlier step.
oldTokenSource.Cancel();
}
private void CreateChangeToken()
{
_cts = new CancellationTokenSource();
_changeToken = new CancellationChangeToken(_cts.Token);
}
} And add an extension method for adding the public static class EndpointRouteBuilderExtensions
{
public static void MapAuthenticationEndpoints(this IEndpointRouteBuilder endpoints)
{
var authenticationSchemeProvider = endpoints.ServiceProvider.GetService<IAuthenticationSchemeProvider>();
var endpointDataSource = authenticationSchemeProvider.GetEndpointDataSource();
endpoints.DataSources.Add(endpointDataSource);
}
}
public class AuthenticationMiddleware
{
public async Task Invoke(HttpContext context)
{
context.Features.Set<IAuthenticationFeature>(new AuthenticationFeature
{
...
if (!options.UseEndpoints)
{
// Give any IAuthenticationRequestHandler schemes a chance to handle the request
var handlers = context.RequestServices.GetRequiredService<IAuthenticationHandlerProvider>();
foreach (var scheme in await Schemes.GetRequestHandlerSchemesAsync())
{
var handler = await handlers.GetHandlerAsync(context, scheme.Name) as IAuthenticationRequestHandler;
if (handler != null && await handler.HandleRequestAsync())
{
return;
}
}
}
...
}
} Let me know what do you think? |
One breaking behavior in switching to endpoint is that Also I'm not quite familiar with Negotiate Handler, does it also need to break into an endpoint? I feel like its business should move to |
No, Negotaite, CertAuth, and JwtBearer do not have any dedicated endpoints. NegotiateAuth's HandleRequestAsync need to stay due to mutli-stage handshakes that could happen on any url path. |
True, you'd have to opt-out of endpoints if you wanted to use dynamic fallback logic. Most scenarios don't require this by default. |
I was thinking of deprecating and eventually removing Also I would like to know your thoughts on my comment on how to move to Endpoint. Should I add more details? Is there something that I missed? |
For compat reasons HandleRequestAsync is going to have to stay. Besides NegotiateAuth, there are also WsFed scenarios that require it. Not all WsFed providers use dedicated endpoints, many of them redirect you back to the site root. Thus you need to be able to have multiple components on a path and sometimes skip one in favor of the other. That scenario's disabled by default, but still needs to be possible. I'm also less concerned about NegotiateAuth because that's not a component that makes sense to have multiple of (unlike OAuth or OIDC). If we can get the OAuth and OIDC providers moved to endpoints by default then that should be a good start here. |
cc @rynowak |
I hope |
@PinpointTownes where do you use it? The idea is that callback paths like |
@Tratcher are you thinking about opting out for all schemes or per scheme? |
In OpenIddict, pretty much everywhere, as it's what we use to enable pass-through. Once this mode is enabled, OpenIddict validates requests for you but eventually calls |
Per scheme. E.g. someone might need to opt out for WsFed but not for OIDC. |
Is it used on callback paths? Or only on normally pass through components like JWT validation? |
Does |
I think we should make it smart enough to automatically register any authentication endpoints if any were registered without any additional calls. The extension methods should do that magic underneath the covers. |
@HaoK that seems overkill, it forks the ecosystem. Moving people over to endpoints by default should be good for the vast majority of cases. It's only some corner cases that need to opt-out. |
I don't know, we've already had lots of behavior changes with endpoint routing to auth already. So I can't imagine we won't run into more. Forking the handlers would better guarantee there's at least a workaround in case/when we accidentally break something. Opting into the new endpoint enabled handlers would only require them changing their implementation to derive from the new Endpoint aware base class. Seems like this is something auth handlers should be aware of and opt into. We can still make endpoints the default for our auth handlers since we control the extension methods. |
Adding a base class is maximally disruptive since we already have a class hierarchy (AuthenticationHandler, RemoteAuthenticationHandler, OAuthHandler), you'd have to fork the entire hierarchy. Maintaining two copies of every auth handler and base class is not sustainable. Yes there's risk with this change, but we should be able to manage that with opt-out options and avoid mass duplication. |
Yeah if there wasn't an easy way to refactor things to reuse most of the logic in both hierarchies, that would be no good. My main point is the handler implementation code has been mostly unchanged since katana, we know how hard it is to extend/customize, as we've never spent the time to make that easy. Seems like this is an opportunity to see if we can finally make things nicer since endpoint support is a big change |
I guess lets just see how the PR goes, given that there's already stuff like HandleEndpointPost/GetAsync showing up, seems like we are going to have resolve some of these questions anyways |
I'd rather try and think it through design wise rather than go code first. This is a major enough change that we need a consistent approach. |
Yeah I agree, but the prototype PR might be good to flush out some of the design issues we need to discuss. |
I'm catching up to this discussion a little bit late, but I'm wondering what kinds of concrete answers have cropped out to this question (emphasis mine). In general endpoints are useful because....
Are any of these things true for this case? |
@rynowak the one scenario I've identified above is a perf optimization to avoid allocating and looping through every auth handler on every request. This would be covered more by routing than endpoints. #17615 (comment) |
Another benefit is simply having the callback endpoints be part of the endpoint collection for other middleware to discover, like OpenAPI can read its metadata and include it in the document. |
It also could be useful for throttling or logging callback endpoints. |
Not that anything should be calling these endpoints directly, an initial challenge is required in almost all cases. |
I went over this again with @rynowak and came to the conclusion that what we want for these callback paths is routing, not endpoints. We haven't identified any other concrete scenarios that need endpoints. The theory is that the auth middleware or service would build their own internal route table mapping callback paths to auth schemes & handlers (not Endpoints). This would re-use existing routing code internally rather than participate in the larger route table. The middleware would then run that handler's HandleRequestAsync method. Schemes would opt-in/out via options, and be given some way to register and unregister their callbacks. (@PinpointTownes) Skip would still mostly work by allowing you to keep executing after the new routing component and still run the rest of the auth middleware logic (AuthenticateAsync for the default scheme, etc.) and the request pipeline. The loop through schemes to run HandleRequestAsync would still happen if no route was found, but schemes that had registered callbacks would opt out of that part. |
@Tratcher perfect, thanks! 👏 |
I don't know if these would count as good scenarios:
|
@Kahbazi I don't see why you'd do any of those things specifically for these auth endpoints and not for the reset of your app. |
I'm thinking all my app has these things but maybe I wan't different settings for these endpoints. For example I have Also I could write a analysis middleware based on Endpoint which generate report for all endpoints and the callbacks would be included in the report automatically. |
@Tratcher Is this decision final? I think by having just routing for authentication callbacks, there would be some limitations. I'm just expanding my previous example here. Let's say I have a app.UseRouting();
app.ConcurrencyMiddleware();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(); Now I can't assign a specific queue for callbacks. The only option for them could be a default fallback queue which I might only wanna use it for when there's no Endpoint found at routing. I promise this is my last attempt to apply Endpoints for authentication callbacks 😅 |
Nothing's final, but the case for endpoints has not been compelling. The use of endpoints also has tradeoffs with fallback patterns currently supported by several of the auth handlers. How would you add metadata/attributes to the callback endpoints when they're created and registered by the auth service? |
As you said, there could be an opt out option.
By adding a Metadata on |
Needs some more thought. There's a strong preference for not having API breaks required for this change, only additions. |
We could add the new class (let's call it |
Now that almost everything (Controller, RazorPage, SignalR Hub, HealthCheck, Grpc) in ASP.NET Core is becoming an Endpoint, I'm curious is there a plan to make authentication handlers into endpoints?
The text was updated successfully, but these errors were encountered: