2

I have a weird situation in my asp.net core api project. I am tryin to access the ActionContext via the IActionContextAccessor is an AuthenticationHandler. Now I found out, that when I have only 1 AuthenticationScheme registered, the returned ActionContext is null, but once I add a second AuthenticationScheme, the ActionContext is properly returned.

The code below is a minimized reproduction scenario. I have a program.cs that sets up the authentication schemes. When I call a random api route in the project, and have a breakpoint in the HandleAuthenticateAsync method, I see the ActionContext is filled. However, once I comment out the line that registers the Schema2 authenticationhandler, the ActionContext becomes null. When hitting the same breakpoint.

Can anyone explain what is happening here?

my program.cs:

using AuthHandlerTest;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc.Authorization;
using Microsoft.AspNetCore.Mvc.Infrastructure;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddSingleton<IActionContextAccessor, ActionContextAccessor>();
builder.Services.AddControllers(options =>
{
    options.Filters.Add(new AuthorizeFilter());
});
builder.Services.AddAuthentication()
    .AddScheme<Schema1Options, Schema1>("schema1", null);
    .AddScheme<Schema2Options, Schema2>("schema2", null);
builder.Services.AddAuthorization(options =>
{
    options.DefaultPolicy = new AuthorizationPolicyBuilder("schema1").RequireAuthenticatedUser().Build();
});

builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseAuthentication();
app.UseRouting();

app.MapControllers();

app.Run();

my Schema1.cs

using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.Extensions.Options;
using System.Security.Claims;
using System.Text.Encodings.Web;

namespace AuthHandlerTest;

public class Schema1Options : AuthenticationSchemeOptions
{
}

public class Schema1 : AuthenticationHandler<Schema1Options>
{
    private readonly IActionContextAccessor _actionContextAccessor;

    public Schema1(IOptionsMonitor<Schema1Options> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock, IActionContextAccessor actionContextAccessor) : base(options, logger, encoder, clock)
    {
        _actionContextAccessor = actionContextAccessor;
    }

    protected override Task<AuthenticateResult> HandleAuthenticateAsync()
    {
        //this variable is null when only having 1 scheme registered
        var actionContext = _actionContextAccessor.ActionContext;

        var claims = new[]
        {
                new Claim(ClaimTypes.NameIdentifier, "aa"),
            };

        var claimsIdentity = new ClaimsIdentity(claims, "schema1");
        var ticket = new AuthenticationTicket(new ClaimsPrincipal(claimsIdentity), "schema1");
        return Task.FromResult(AuthenticateResult.Success(ticket));
    }
}

schema2.cs is identical to schema1.cs, only the 1's are replaced with 2's.

I also see that the stacktrace is significantly different between the working and non-working scenario.

Working: Stacktrace of working situation

Non working: Stacktrace of non-working situation

Now I understand why these different stacks are causing the ActionContext to be available or not, because in the working situation the ResourceInvoker is present in the callstack, and this component makes sure the IActionContextAccessor gets the ActionContext property filled. The million dollar question is, what is causing the difference in callstack

Update: I've added a full reproduction scenario here

PaulVrugt
  • 1,682
  • 2
  • 17
  • 40
  • 1
    `app.UseAuthentication(); app.UseRouting();` is the wrong order for middleware. You want `app.UseRouting(); app.UseAuthentication(); app.UseAuthorization(); app.UseEndpoints(...);` It's `.UseRouting` that resolves the endpoint and attaches that information to the `HttpContext`, which `UseAuthorization / UseAuthentication` then use to enforce the requested auth policy. Otherwise you can only enforce the fallback policy. – Jeremy Lakeman Jun 19 '23 at 03:33

2 Answers2

1

Here's a related issue and

IActionContextAccessor.ActionContext is null outside the scope of mvc middleware

Part of source codes of Authentication middleware

var defaultAuthenticate = await Schemes.GetDefaultAuthenticateSchemeAsync();
        if (defaultAuthenticate != null)
        {
            var result = await context.AuthenticateAsync(defaultAuthenticate.Name);
            if (result?.Principal != null)
            {
                context.User = result.Principal;
            }
            if (result?.Succeeded ?? false)
            {
                var authFeatures = new AuthenticationFeatures(result);
                context.Features.Set<IHttpAuthenticationFeature>(authFeatures);
                context.Features.Set<IAuthenticateResultFeature>(authFeatures);
            }
        }

        await _next(context);

If the default scheme is not null ,context.AuthenticateAsync(defaultAuthenticate.Name); would be executed in Authentication middleware (as shown by the stacktrace),which means the handler would be called out of the scope of mvc middleware

The document related with this issue

Starting in ASP.NET Core 7.0, if (and only if) a single scheme is registered in an application, that scheme is treated as the default. In the following code, the CookieDefaults.AuthenticationScheme is treated as the default scheme.

However, in the next code snippet, no default is set because multiple schemes are registered.

To disable it :

AppContext.SetSwitch("Microsoft.AspNetCore.Authentication.SuppressAutoDefaultScheme", true);

It works onmyside now:

enter image description here

enter image description here

Ruikai Feng
  • 6,823
  • 1
  • 2
  • 11
  • Sorry, I see I posted the wrong image in the "wrong stack", I'll update it – PaulVrugt Jun 16 '23 at 08:06
  • I've also added a link to a reproduction scenario – PaulVrugt Jun 16 '23 at 08:38
  • Yesterday I tried in .net 6 and failed to fully repo, builder.Services.AddAuthentication() .AddScheme("schema1", null) would also work,but if I specifc the default scheme builder.Services.AddAuthentication("schema1") .AddScheme("schema1", null) I would partially repo – Ruikai Feng Jun 16 '23 at 09:55
  • I tried with your scenario today and reproduced the issue ,seems when you regist only one scheme,it would consider it as the default scheme. – Ruikai Feng Jun 16 '23 at 09:58
  • well, maybe, but does that explain why the ResourceInvoker is left out? Because I fail to see why having a default schema stops the need for having the ResourceInvoker involved – PaulVrugt Jun 16 '23 at 10:53
  • Please check what I've updated ,when the default scheme is not null,your handler would be called in Authentication middleware out of the scope of mvc middleware – Ruikai Feng Jun 19 '23 at 03:14
  • Your first trace indicates you've hit the endpoint and got into the filter ,but the second indicates the handler was called in Authentication middleware before you hit the endpoint that 's why the ResourceInvoker is left out – Ruikai Feng Jun 19 '23 at 05:40
  • Yeah I've checked and you seem correct. But even explicitly setting the `DefaultAuthenticationScheme` and `DefaultScheme` to null in the `AuthenticationOptions` object doesn't seem to help. This seems very unwanted behavior, because we lose the entire mvc middleware scope – PaulVrugt Jun 19 '23 at 07:24
  • I found the related document and updated my answer – Ruikai Feng Jun 19 '23 at 09:15
  • AppContext.SetSwitch("Microsoft.AspNetCore.Authentication.SuppressAutoDefaultScheme", true); would solve this issue – Ruikai Feng Jun 19 '23 at 10:00
  • Thanks for the input. However, setting that flag seems like a temporary workaround. Apparently MS thinks this new behavior should be correct. I'm not sure if they accounted for this side effect – PaulVrugt Jun 19 '23 at 11:37
  • 1
    Here's a issue related on github:https://github.com/dotnet/aspnetcore/issues/44661 ,"We have no intention of removing this switch. Indeed, if this scenario (single authn scheme but no default) is common enough we can consider elevating the property on AuthenticationOptions that wraps the switch to be public in .NET 8." – Ruikai Feng Jun 20 '23 at 02:10
0

You should check the order of your middlewares, you are registering Authentication before Routing. See the official documentation about the middleware registering order => https://learn.microsoft.com/en-us/aspnet/core/fundamentals/middleware/?view=aspnetcore-7.0#middleware-order

Max
  • 794
  • 3
  • 7
  • you are absolutely correct, however, changing the order or the `UseRouting` and `UseAuthentication` doesn't fix the issue – PaulVrugt Jun 19 '23 at 11:36
  • Did you try specifying the default scheme to use when having multiple Scheme ? Because when you have only one Scheme it is set by default as the default AUTHENTICATION scheme but when you provide multiple scheme you need to specify to the Authentication Middleware which one is the default one and it is completely different from the Authorization :) – Max Jun 19 '23 at 12:05
  • https://learn.microsoft.com/en-us/dotnet/core/compatibility/aspnet-core/7.0/default-authentication-scheme – Max Jun 19 '23 at 12:06