dotnet / aspnetcore

ASP.NET Core is a cross-platform .NET framework for building modern cloud-based web applications on Windows, Mac, or Linux.

Home Page:https://asp.net

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Allow specifying exact authentication schemes in AuthenticationMiddleware.

ShadedBlink opened this issue · comments

Background and Motivation

I believe there are many different web applications that support more than one authentication mode as default like "if there is an API-key specified, use it, otherwise fallback to cookies". I had such services in development and had an urge to know current user for logging purposes before authorization takes places and cancells requests with 403. It instantly comes to mind that best place for such middleware is between app.UseAuthentication() and app.UseAuthorization() steps, but the main problem is that you can specify multiple default authentication schemes only for authorization scheme, i.e. authentication step can handle only single authentication scheme(cookies in our case), while API-key is considered only on authorization step.

Proposed API

The idea is to allow specifying exact list of authentication schemes in middleware so they are handled in same way as they are handled in authorization.

namespace Microsoft.AspNetCore.Builder;

public static class AuthAppBuilderExtensions
{
+    public static IApplicationBuilder UseAuthentication(this IApplicationBuilder app, params string[] schemes);
}

Usage Examples

     app.UseAuthentication("api-key", "cookies");

In given case this middleware should first challenge "api-key" scheme. If it succeeds, then step is finished, otherwise fallback to "cookies". This overload is not supposed to consider default authentication scheme since all schemes are specified explicitly, i.e. if default authentication scheme is "jwt", then only "api-key" and "cookies" are used, "jwt" is ignored since it is not specified. If default scheme is among the specified schemes, then it should be applied exactly in order as it is specified in schemes list. I.e. being a default scheme doesn't interfere in any way with this middleware.

Also this approach should work better with branched middleware chains since you can specify exact auth scheme to reach them. Like in main branch you use scheme "cookies", for health or prometheus branch you use scheme "api-key". Current approach doesn't support any configuration for middleware.

Risks

Since this overload is optional and new, it is not supposed to break anything.

If we were to do this, I think the natural place for it would be in AuthenticationOptions alongside DefaultScheme, DefaultAuthenticateScheme, DefaultChallengeScheme, etc... In theory, we could try to make plural versions of all of those. I'm not sure it's a good idea though.

the main problem is that you can specify multiple default authentication schemes only for authorization scheme, i.e. authentication step can handle only single authentication scheme(cookies in our case), while API-key is considered only on authorization step.

Proposed API

The idea is to allow specifying exact list of authentication schemes in middleware so they are handled in same way as they are handled in authorization.

If it's handled the same way we handle multiple schemes specified as part of the authorization policy, we'd merge together ClaimsPrincipals rather than take the first successful result.

if (policy.AuthenticationSchemes != null && policy.AuthenticationSchemes.Count > 0)
{
ClaimsPrincipal? newPrincipal = null;
DateTimeOffset? minExpiresUtc = null;
foreach (var scheme in policy.AuthenticationSchemes)
{
var result = await context.AuthenticateAsync(scheme);
if (result != null && result.Succeeded)
{
newPrincipal = SecurityHelper.MergeUserPrincipal(newPrincipal, result.Principal);
if (minExpiresUtc is null || result.Properties?.ExpiresUtc < minExpiresUtc)
{
minExpiresUtc = result.Properties?.ExpiresUtc;
}
}
}

I don't think merging ClaimsPrincipals is necessarily wrong, but most of the time it probably is probably unessary. I'm not sure it's logic we want to copy to more places. At least authorization polices are usually isolated to particular endpoints where they can specify a subset of globally supported authentication schemes. And usually, it's just one.

If you set both a cookie and API key as global default authentication handlers, the former will attempt to redirect while the latter will attempt to set a 401 given a challenge. What happens would depend on the order they run in which wouldn't be very intuitive.

An application that needs to support both cookie and API keys for auth would probably be best served registering or configuring a policy scheme to indicate what scheme should be used what purpose.

This requires a little bit more code than just providing an array of scheme names, but it gives far more precise control over how to combine the authentication schemes. For example:

.AddPolicyScheme("B2C_OR_AAD", "B2C_OR_AAD", options =>
{
    options.ForwardDefaultSelector = context =>
    {
        string authorization = context.Request.Headers[HeaderNames.Authorization];
        if (!string.IsNullOrEmpty(authorization) && authorization.StartsWith("Bearer "))
        {
            var token = authorization.Substring("Bearer ".Length).Trim();
            var jwtHandler = new JwtSecurityTokenHandler();

            return (jwtHandler.CanReadToken(token) && jwtHandler.ReadJwtToken(token).Issuer.Equals("B2C-Authority"))
                ? "B2C" : "AAD";
        }
        return "AAD";
    };
});

https://learn.microsoft.com/en-us/aspnet/core/security/authorization/limitingidentitybyscheme?view=aspnetcore-8.0#use-multiple-authentication-schemes

@halter73 , actually this doesn't have to go along with any existing code since this middleware is supposed to be new. Existing one is expected to use only default scheme and I don't want to interrupt it in any way.
For the challenge case we can just provide another parameter to configure challenge order/options.
Also I want to note that this middleware has to be configured in middleware chain or atleast we need a way to specify policy there. Thus we can write something like this:

app.UseCompanyHangfire("/hangfire");
app.UseCompanyHealth("/health");

Both these methods are branched with personal app.UseAuthentication("company-sms-token");. In this example both middleware endpoints can use their own authentication schemes without any conflicts between each other and outer logic. For example, company may have both these methods protected under SMS-authentication available internally right out-of-the-box as nuget package. Thus these middlewares will definitely use such authentication, while rest of the application is covered by default cookie authentication.