How to ConfigureServices Authentication based on routes in ASP.NET Core 2.0

The Microsoft docs say what to do if you want to use multiple authentication schemes in ASP.NET Core 2+:

The following example enables dynamic selection of schemes on a per request basis. That is, how to mix cookies and API authentication:

public void ConfigureServices(IServiceCollection services)
{
    services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
        .AddCookie(options =>
        {
            // For example, can foward any requests that start with /api 
            // to the api scheme.
            options.ForwardDefaultSelector = ctx => 
               ctx.Request.Path.StartsWithSegments("/api") ? "Api" : null;
        })
        .AddYourApiAuth("Api");
}

Example:

I had to implement a mixed-authentication solution in which I needed Cookie authentication for some requests and Token authentication for other requests. Here is what it looks like for me:

services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
    .AddCookie(options =>
    {
        // if URL path starts with "/api" then use Bearer authentication instead
        options.ForwardDefaultSelector = httpContext => httpContext.Request.Path.StartsWithSegments("/api") ? JwtBearerDefaults.AuthenticationScheme : null;
    })
    .AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, o =>
        {
            o.TokenValidationParameters.ValidateIssuerSigningKey = true;
            o.TokenValidationParameters.IssuerSigningKey = symmetricKey;
            o.TokenValidationParameters.ValidAudience = JwtSignInHandler.TokenAudience;
            o.TokenValidationParameters.ValidIssuer = JwtSignInHandler.TokenIssuer;
        });

where the JWT Bearer authentication is implemented as described in this answer.

Tips:

One of the biggest 'gotchas' for me was this: Even though the Cookies Policy forwards requests with URLs that start with "/api" to the Bearer policy, the cookie-authenticated users can still access those URLs if you're using the [Authorize] annotation. If you want those URLs to only be accessed through Bearer authentication, you must use the [Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)] annotation on the API Controllers/Actions.


In 2.0, the best option to do per-route authentication is to use a custom IAuthenticationSchemeProvider:

public class CustomAuthenticationSchemeProvider : AuthenticationSchemeProvider
{
    private readonly IHttpContextAccessor httpContextAccessor;

    public CustomAuthenticationSchemeProvider(
        IHttpContextAccessor httpContextAccessor,
        IOptions<AuthenticationOptions> options)
        : base(options)
    {
        this.httpContextAccessor = httpContextAccessor;
    }

    private async Task<AuthenticationScheme> GetRequestSchemeAsync()
    {
        var request = httpContextAccessor.HttpContext?.Request;
        if (request == null)
        {
            throw new ArgumentNullException("The HTTP request cannot be retrieved.");
        }

        // For API requests, use authentication tokens.
        if (request.Path.StartsWithSegments("/api"))
        {
            return await GetSchemeAsync(OAuthValidationDefaults.AuthenticationScheme);
        }

        // For the other requests, return null to let the base methods
        // decide what's the best scheme based on the default schemes
        // configured in the global authentication options.
        return null;
    }

    public override async Task<AuthenticationScheme> GetDefaultAuthenticateSchemeAsync() =>
        await GetRequestSchemeAsync() ??
        await base.GetDefaultAuthenticateSchemeAsync();

    public override async Task<AuthenticationScheme> GetDefaultChallengeSchemeAsync() =>
        await GetRequestSchemeAsync() ??
        await base.GetDefaultChallengeSchemeAsync();

    public override async Task<AuthenticationScheme> GetDefaultForbidSchemeAsync() =>
        await GetRequestSchemeAsync() ??
        await base.GetDefaultForbidSchemeAsync();

    public override async Task<AuthenticationScheme> GetDefaultSignInSchemeAsync() =>
        await GetRequestSchemeAsync() ??
        await base.GetDefaultSignInSchemeAsync();

    public override async Task<AuthenticationScheme> GetDefaultSignOutSchemeAsync() =>
        await GetRequestSchemeAsync() ??
        await base.GetDefaultSignOutSchemeAsync();
}

Don't forget to register it in the DI container (ideally, as a singleton):

// IHttpContextAccessor is not registered by default
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
services.AddSingleton<IAuthenticationSchemeProvider, CustomAuthenticationSchemeProvider>();