Search code examples
.netauthenticationasp.net-core-mvcasp.net-core-mvc-2.0

Path based authentication in ASP.NET Core MVC 2.0


In ASP.NET Core MVC 1.1 we had path based authentication like this:

public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
    // /api/* path
    app.UseWhen(ctx => IsApiRequest(ctx), subBranch =>
    {
        subBranch.UseApiAuth(GetApiAuthOptions());
    });
    // else
    app.UseWhen(ctx => !IsApiRequest(ctx), subBranch =>
    {
        subBranch.UseOpenIdConnectAuthentication(GetOpenIdOptions());
    });
}

Now we want to migrate it to ASP.NET Core MVC 2.0. In new version authentication was completely redesigned, and in docs I didn't find any clue how to do that. Any ideas how to migrate the code above?


Solution

  • After 2 days of testing and trying, I've came up with working solution.

    The main problem is, that in ASP.NET Core MVC 2.0, authentication methods are registred as services rather than middleware. This implies that they must be registered in ConfigureServices method rather than in Configure, so there is no way to branch at registration time to create branches. Moreover, auth system uses AuthenticationOptions to detemine which authentication method will be used. From my testing I discovered, that AuthenticationOptions instance is shared across requests, so it cannot be modified to adjust DefaultScheme. After some digging, I've found IAuthenticationSchemeProvider, which can be overriden to overcome those issues.

    Here's the code:

    // Startup.cs
    public IServiceProvider ConfigureServices(IServiceCollection services)
    {
        [...]
    
        // Override default IAuthenticationSchemeProvider implementation
        services.AddSingleton<IAuthenticationSchemeProvider, CustomAuthenticationSchemeProvider>();
    
        // Register OpenId Authentication services
        services.AddAuthentication().AddCookie(this.GetCookieOptions);
        services.AddAuthentication().AddOpenIdConnect(this.GetOpenIdOptions);
    
        // Register HMac Authentication services (for API)
        services.AddAuthentication().AddHMac(this.GetHMacOptions);
    
        [...]
    }
    
    public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
    {
        [...]
    
        // /api/* path
        app.UseWhen(ctx => IsApiRequest(ctx), subBranch =>
        {
            // Register middleware which will override DefaultScheme; must be called before UseAuthentication()
            subBranch.UseAuthenticationOverride(HMacAuthenticationDefaults.AuthenticationScheme);
            subBranch.UseAuthentication();
        });
        // else
        app.UseWhen(ctx => !IsApiRequest(ctx), subBranch =>
        {
            // Register middleware which will override DefaultScheme and DefaultChallengeScheme; must be called before UseAuthentication()
            subBranch.UseAuthenticationOverride(new AuthenticationOptions
            {
                DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme,
                DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme
            });
            subBranch.UseAuthentication();
        });
    
        [...]
    }
    

    Middleware:

    // AuthenticationOverrideMiddleware.cs
    // Adds overriden AuthenticationOptions to HttpContext to be used by CustomAuthenticationSchemeProvider
    public class AuthenticationOverrideMiddleware
    {
        private readonly RequestDelegate _next;
        private readonly AuthenticationOptions _authenticationOptionsOverride;
    
        public AuthenticationOverrideMiddleware(RequestDelegate next, AuthenticationOptions authenticationOptionsOverride)
        {
            this._next = next;
            this._authenticationOptionsOverride = authenticationOptionsOverride;
        }
        public async Task Invoke(HttpContext context)
        {
            // Add overriden options to HttpContext
            context.Features.Set(this._authenticationOptionsOverride);
            await this._next(context);
        }
    }
    public static class AuthenticationOverrideMiddlewareExtensions
    {
        public static IApplicationBuilder UseAuthenticationOverride(this IApplicationBuilder app, string defaultScheme)
        {
            return app.UseMiddleware<AuthenticationOverrideMiddleware>(new AuthenticationOptions { DefaultScheme = defaultScheme });
        }
        public static IApplicationBuilder UseAuthenticationOverride(this IApplicationBuilder app, AuthenticationOptions authenticationOptionsOverride)
        {
            return app.UseMiddleware<AuthenticationOverrideMiddleware>(authenticationOptionsOverride);
        }
    }
    

    CustomAuthenticationSchemeProvider:

    // CustomAuthenticationSchemeProvider.cs
    // When asked for Default*Scheme, will check in HttpContext for overriden options, and return appropriate schema name
    public class CustomAuthenticationSchemeProvider : AuthenticationSchemeProvider
    {
        private readonly IHttpContextAccessor _contextAccessor;
    
        public CustomAuthenticationSchemeProvider(IOptions<AuthenticationOptions> options, IHttpContextAccessor contextAccessor) : base(options)
        {
            this._contextAccessor = contextAccessor;
        }
    
        // Retrieves overridden options from HttpContext
        private AuthenticationOptions GetOverrideOptions()
        {
            HttpContext context = this._contextAccessor.HttpContext;
            return context?.Features.Get<AuthenticationOptions>();
        }
        public override Task<AuthenticationScheme> GetDefaultAuthenticateSchemeAsync()
        {
            AuthenticationOptions overrideOptions = this.GetOverrideOptions();
            string overridenScheme = overrideOptions?.DefaultAuthenticateScheme ?? overrideOptions?.DefaultScheme;
            if (overridenScheme != null)
                return this.GetSchemeAsync(overridenScheme);
            return base.GetDefaultAuthenticateSchemeAsync();
        }
        public override Task<AuthenticationScheme> GetDefaultChallengeSchemeAsync()
        {
            AuthenticationOptions overrideOptions = this.GetOverrideOptions();
            string overridenScheme = overrideOptions?.DefaultChallengeScheme ?? overrideOptions?.DefaultScheme;
            if (overridenScheme != null)
                return this.GetSchemeAsync(overridenScheme);
            return base.GetDefaultChallengeSchemeAsync();
        }
        public override Task<AuthenticationScheme> GetDefaultForbidSchemeAsync()
        {
            AuthenticationOptions overrideOptions = this.GetOverrideOptions();
            string overridenScheme = overrideOptions?.DefaultForbidScheme ?? overrideOptions?.DefaultScheme;
            if (overridenScheme != null)
                return this.GetSchemeAsync(overridenScheme);
            return base.GetDefaultForbidSchemeAsync();
        }
        public override Task<AuthenticationScheme> GetDefaultSignInSchemeAsync()
        {
            AuthenticationOptions overrideOptions = this.GetOverrideOptions();
            string overridenScheme = overrideOptions?.DefaultSignInScheme ?? overrideOptions?.DefaultScheme;
            if (overridenScheme != null)
                return this.GetSchemeAsync(overridenScheme);
            return base.GetDefaultSignInSchemeAsync();
        }
        public override Task<AuthenticationScheme> GetDefaultSignOutSchemeAsync()
        {
            AuthenticationOptions overrideOptions = this.GetOverrideOptions();
            string overridenScheme = overrideOptions?.DefaultSignOutScheme ?? overrideOptions?.DefaultScheme;
            if (overridenScheme != null)
                return this.GetSchemeAsync(overridenScheme);
            return base.GetDefaultSignOutSchemeAsync();
        }
    }
    

    If somebody knows better solution, I would love to see it.