3

UPDATE: Unfortunately, a Windows reboot solved this issue -.-


In our ASP.NET Core (1.0 RC2) application, we have the following requirement: only users from the internal network should be able to access some "Debug" pages (hosted by MVC Core). It's a public website and we don't have user logins, instead we managed it until now with a custom IP-address based authorization (note: this is not a security risk in our case, because we have a proxy in between, so the IP address cannot be spoofed from outside).

We want to implement such an IP-address based authorization in ASP.NET Core, as well. We use a custom policy "DebugPages" for this and corresponding [Authorize(Policy="DebugPages")] definitions on the MVC controller. Then we noticed, that we must have an authenticated user to get the AuthorizeAttribute to jump in and we create one in the request pipeline, which yields to the following code in Startup.cs (shortened for brevity):

public void ConfigureServices(IServiceCollection services)
{
    ...

    services.AddAuthorization(options =>
    {
        options.AddPolicy(
            "DebugPages",
            policy => policy.RequireAssertion(
                async context => await MyIPAuthorization.IsAuthorizedAsync()));
    });
}

public void Configure(IApplicationBuilder app)
{
    ...

    app.Use(async (context, next) =>
    {
        context.User = new ClaimsPrincipal(new GenericIdentity("anonymous"));
        await next.Invoke();
    });

    ...
}

Now this works fine when run in Debug by Visual Studio 2015 (with IIS Express). But unfortunately it doesn't work when run directly by dotnet run (with Kestrel) from the command line. In this case we get the following exception:

InvalidOperationException: No authentication handler is configured to handle the scheme: Automatic

The same error occurs when we provide the current Windows principal instead of the principal with a custom anonymous identity -- so everytime when the user is automatic-ally authenticated...

So, why is there a difference between hosting in IIS Express and Kestrel? Any suggestions how to solve the issue?

Matthias
  • 3,403
  • 8
  • 37
  • 50
  • This part of code works fine with kestrel(dotnet run) for me. It seems the reason of exception is not related with this code. – adem caglin Jun 22 '16 at 08:00
  • Damn... I just rebooted and now everything is working fine. I don't know what happened... good old Windows, I fear (_if it's not good - reboot!_). – Matthias Jun 23 '16 at 08:59
  • Hello. I have the same issue. Not in Windows but in docker too. The version is Core 1.0. I've investigated the problem and have seen that the context.Authentication.HttpAuthhenticationFeature.Handler is null when you use Kestrel and not null when you use IIS. I asume that the trick is in the .UseIISIntegration() in Program.cs – aligin Jul 19 '16 at 18:29

1 Answers1

2

So, after some research, as I mentioned in the comments, I have found that httpContext.Authentication.HttpAuthhenticationFeature.Handler is null, when you starts the application under the "selfhosted" kestrel. But when you use IIS the Handler has instantiated by Microsoft.AspNetCore.Server.IISIntegration.AuthenticationHandler. This specific handler implementation is part of the .UseIISIntegration() in Program.cs.

So, I've decided to use a part of this implementation in my App and handle nonauthenticated requests.

For my WebAPI (without any Views) service I use IdentityServer4.AccessTokenValidation that uses behind the scenes OAuth2IntrospectionAuthentication and JwtBearerAuthentication.

Create files

KestrelAuthenticationMiddleware.cs

public class KestrelAuthenticationMiddleware
{
    private readonly RequestDelegate _next;
    public KestrelAuthenticationMiddleware(RequestDelegate next)
    {
        _next = next;
    }
    public async Task Invoke(HttpContext context)
    {
        var existingPrincipal = context.Features.Get<IHttpAuthenticationFeature>()?.User;
        var handler = new KestrelAuthHandler(context, existingPrincipal);
        AttachAuthenticationHandler(handler);
        try
        {
            await _next(context);
        }
        finally
        {
            DetachAuthenticationhandler(handler);
        }
    }

    private void AttachAuthenticationHandler(KestrelAuthHandler handler)
    {
        var auth = handler.HttpContext.Features.Get<IHttpAuthenticationFeature>();
        if (auth == null)
        {
            auth = new HttpAuthenticationFeature();
            handler.HttpContext.Features.Set(auth);
        }
        handler.PriorHandler = auth.Handler;
        auth.Handler = handler;
    }

    private void DetachAuthenticationhandler(KestrelAuthHandler handler)
    {
        var auth = handler.HttpContext.Features.Get<IHttpAuthenticationFeature>();
        if (auth != null)
        {
            auth.Handler = handler.PriorHandler;
        }
    }
}

KestrelAuthHandler.cs

internal class KestrelAuthHandler : IAuthenticationHandler
{
    internal KestrelAuthHandler(HttpContext httpContext, ClaimsPrincipal user)
    {
        HttpContext = httpContext;
        User = user;
    }

    internal HttpContext HttpContext { get; }

    internal ClaimsPrincipal User { get; }

    internal IAuthenticationHandler PriorHandler { get; set; }

    public Task AuthenticateAsync(AuthenticateContext context)
    {
        if (User != null)
        {
            context.Authenticated(User, properties: null, description: null);
        }
        else
        {
            context.NotAuthenticated();
        }


        if (PriorHandler != null)
        {
            return PriorHandler.AuthenticateAsync(context);
        }

        return Task.FromResult(0);
    }

    public Task ChallengeAsync(ChallengeContext context)
    {
        bool handled = false;
        switch (context.Behavior)
        {
            case ChallengeBehavior.Automatic:
                // If there is a principal already, invoke the forbidden code path
                if (User == null)
                {
                    goto case ChallengeBehavior.Unauthorized;
                }
                else
                {
                    goto case ChallengeBehavior.Forbidden;
                }
            case ChallengeBehavior.Unauthorized:
                HttpContext.Response.StatusCode = 401;
                // We would normally set the www-authenticate header here, but IIS does that for us.
                break;
            case ChallengeBehavior.Forbidden:
                HttpContext.Response.StatusCode = 403;
                handled = true; // No other handlers need to consider this challenge.
                break;
        }
        context.Accept();


        if (!handled && PriorHandler != null)
        {
            return PriorHandler.ChallengeAsync(context);
        }

        return Task.FromResult(0);
    }

    public void GetDescriptions(DescribeSchemesContext context)
    {
        if (PriorHandler != null)
        {
            PriorHandler.GetDescriptions(context);
        }
    }

    public Task SignInAsync(SignInContext context)
    {
        // Not supported, fall through
        if (PriorHandler != null)
        {
            return PriorHandler.SignInAsync(context);
        }

        return Task.FromResult(0);
    }

    public Task SignOutAsync(SignOutContext context)
    {
        // Not supported, fall through
        if (PriorHandler != null)
        {
            return PriorHandler.SignOutAsync(context);
        }

        return Task.FromResult(0);
    }
}

And in the Startup.cs

public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
    {
        app.UseMiddleware<KestrelAuthenticationMiddleware>();

        app.UseIdentityServerAuthentication(new IdentityServerAuthenticationOptions
        {
            Authority = Configuration[AppConstants.Authority],
            RequireHttpsMetadata = false,
            AutomaticChallenge = true,
            ScopeName = Configuration[AppConstants.ScopeName],
            ScopeSecret = Configuration[AppConstants.ScopeSecret],
            AutomaticAuthenticate = true
        });
        app.UseMvc();
    }
aligin
  • 1,370
  • 1
  • 13
  • 18