Configuring ASP.NET Core 2.0 Authentication
Global Authentication Filters for Projects for both OpenID Connect Users and JWT Bearer Token Daemons
Problem Statement
I'm building a ASP.NET Core 2.0 Web Application with MVC. I want the following:
- MVC Controllers
- Secured with Azure ActiveDirectory Authentication
- Authentication Challenges should redirect user to the login page
- WebApi controllers
- Secured by JWT Bearer Tokens
- Authentication Challenges should return 401 (Unauthorized) responses
- A global Authentication filter so that all controllers (UI and WebAPI) are locked down by default
In short, I want an MVC application with some API endpoints in the same project
Getting Started
I'm using the following:
- ASP.NET Core 2.0
- Visual Studio 2017.15.3+
- A new ASP.NET Core 2.0 WebApplication (Model-View-Controller) using
Work or School Accounts
for Authentication
By default, the template should generate a Startup class with something like this for the configure method:
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication(sharedOptions =>
{
sharedOptions.DefaultScheme =
CookieAuthenticationDefaults.AuthenticationScheme;
sharedOptions.DefaultChallengeScheme =
OpenIdConnectDefaults.AuthenticationScheme;
})
.AddAzureAd(options => Configuration.Bind("AzureAd", options))
.AddCookie();
// other registrations...
}
Step 1: Configuring Authentication
We'll be configuring three things here:
- Configuring JWT Bearer Authentication
- Configuring the Cookie Authentication behavior
- Adjusting the Default scheme configuration
First, we need to add JwtBearer Authentication to the mix. This will allow api calls in code to authenticate via a JSON Web Token as a Bearer token. If you haven't used these, you should read up on them, because they are awesome.
I'm using Azure for my JWT Tokens. If you aren't, your configuration might be a touch different. The Azure App ID URI can be found in the Azure control panel under: Azure Active Directory => App Registrations => {Your Application} => Settings => Properties => App ID URI
.
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication(sharedOptions => ... )
.AddAzureAd(options => Configuration.Bind("AzureAd", options))
// Configure JWT Bearer Authentication
.AddJwtBearer(options =>
{
options.Authority = "https://login.microsoftonline.com/{TENANT NAME}";
options.Audience = "{Azure App ID URI}"
}
.AddCookie();
}
Next, we want configure the Cookie behavior. What we're going to do here is short circuit the redirect if the request path starts with /api
. Your setup may be different, but I like all my api url paths to start with /api
.
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication(...)
.AddAzureAd(...)
.AddJwtBearer(...)
.AddCookie(options =>
{
options.LoginPath = "/Account/SignIn";
options.LogoutPath = "/Account/SignOut";
options.Events = new CookieAuthenticationEvents
{
OnRedirectToLogin = OnRedirectToLogin
}
});
}
private static Task OnRedirectToLogin(RedirectContext<CookieAuthenticationOptions> context)
{
// your logic may vary, this logic should reflect
// whichever pattern distinguishes this as an api request
if(context.Request.Path.StartsWithSegments("/api"))
{
// override the status code
context.Response.StatusCode = (int) HttpStatusCode.Unauthorized;
return Task.CompletedTask;
}
// all other requests we'll assume should redirect to a login page
context.Response.Redirect(context.RedirectUri);
return Task.CompletedTask;
}
The final Authentication configuration is push everything though the cookie authentication flow, ensuring we catch non-authenticated api requests and interrupt the login redirect. This is done by adjusting the default scheme in the authentication service registration.
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddAzureAd(...)
.AddJwtBearer(...)
.AddCookie(...);
Step 2: Adding an Authorization Policy
Here we're going to use the Authorization Policy features in dotnet core to create a policy that requires the request to be authenticated from either the JwtBearer or Cookie authentication schemes. Without the policy, the default authorization policy seems to let all API request through, regardless of bearer token. (If I'm wrong about this, I'd love to know).
public IServiceProvider ConfigureServices(IServiceCollection services)
{
services.AddAuthentication(...);
services.AddAuthorization(options =>
{
options.AddPolicy("Authenticated",
policy => policy
.AddAuthenticationSchemes(
JwtBearerDefaults.AuthenticationScheme,
CookieAuthenticationDefaults.AuthenticationScheme)
.RequireAuthenticatedUser());
});
}
Step 3: Adding a Global Authorization Policy filter
Finally, we want to add a global filter so that requests are Authorized by default.
public IServiceProvider ConfigureServices(IServiceCollection services)
{
services.AddAuthentication(...);
services.AddAuthorization(...);
services.Configure<MvcOptions>(options =>
{
options.Filters.Add(new AuthorizeFilter("Authenticated"));
});
}
Conclusion
There is still more work to cover scenarios in which an authenticated user should be prevented from accessing API calls they shouldn't be able to as well as preventing daemons with API access from accessing UI pages, but this seems a descent start to, in the very least ensure Authentication on every controller and prevent WebAPI controllers from returning a login page.
Software Development Nerd