Simplify Authentication and Authorization configuration when using WebApplicationBuilder
See original GitHub issueToday, configuring authentication/authorization for an ASP.NET Core application requires adding services and middleware at different stages of the app startup process. We’ve seen feedback that users find configuring authnz one of the hardest things about building APIs with ASP.NET Core.
Given authnz is regularly a cross-cutting, top-level concern of configuring an application, and very often the first thing someone wants to do after getting an API working, we should consider making it simpler to discover and configure.
Adding authentication to an app
Here’s a minimally functional app at the point where it is ready to have authentication configured:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/hello", () => "Hello!");
// Configure the following API to require the client be authenticated
app.MapGet("/hello-protected", () => "Hello, you are authorized to see this!");
app.Run();
Adding auth today
To protect the second API today, services must be added, along with two middleware, and finally an authorization requirement defined on the API endpoint itself:
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authorization;
var builder = WebApplication.CreateBuilder(args);
// Add the authentication and authorization services for the desired authentication scheme
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(jwtConfig =>
{
jwtConfig.Authority = "https://example.com";
jwtConfig.TokenValidationParameters = new()
{
ValidAudience = "MyAudience",
ValidIssuer = "https://example.com"
};
});
builder.Services.AddAuthorization();
var app = builder.Build();
// Add the authentication and authorization middleware
app.UseAuthentication();
app.UseAuthoriziation();
app.MapGet("/hello", () => "Hello!");
// Add authorization configuration to the API
app.MapGet("/hello-protected", () => "Hello, you are authorized to see this!")
.RequireAuthorization();
app.Run();
Every one of these changes must be applied in the correct phase of application startup (i.e. called on the right type and put on the right line) in order for the second API to be successfully protected so that only authenticated users can call it. This involved introducing the following concepts:
- Importing namespaces
- Adding services via the builder
- Adding and configuring an authentication scheme using an options configuration delegate
- Adding middleware that are order-dependent
- Adding endpoint metadata
If access to the protected endpoint is to require more than simply the fact the client is authenticated, then a “policy” must be defined as part of the authorization services being registered in the DI container, and then referred to when adding the endpoint metadata:
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authorization;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(jwtConfig =>
{
jwtConfig.Authority = "https://example.com";
jwtConfig.TokenValidationParameters = new()
{
ValidAudience = "MyAudience",
ValidIssuer = "https://example.com"
};
});
builder.Services.AddAuthorization(authzOptions =>
{
// Define the policy here
authzOptions.AddPolicy("HasProtectedAccess", policyConfig =>
{
// Add requirements to satisfy this policy
policyConfig.RequireClaim("scope", "myapi:protected-access");
});
});
var app = builder.Build();
app.UseAuthentication();
app.UseAuthoriziation();
app.MapGet("/hello", () => "Hello!");
// Update the authorization configuration to the API to require the added policy
app.MapGet("/hello-protected", () => "Hello, you are authorized to see this!")
.RequireAuthorization("HasProtectedAccess");
app.Run();
While very flexible, this process can seem overly complex for something that many folks consider a simple scenario.
Adding auth via new simplified process
The general idea is to explore promoting authentication and authorization to be more of a first-class concept of WebApplicationBuilder
, as is already the case for logging and configuration, building atop of the existing authentication and authorization primitives in ASP.NET Core.
Some proposals to explore:
- Adding top level members to
WebApplicationBuilder
to enable configuration of authentication and authorization - Automatically adding the authentication and authorization middleware to the application request pipeline when authentication is configured via
WebApplicationBuilder
- Using the changes in #39840 to support defining requirements for an API directly on the endpoint definition as metadata
Given our original example app that’s ready for configuring authnz in, consider the following:
var builder = WebApplication.CreateBuilder(args);
// This top level property is of type WebApplicationAuthenticationBuilder which derives from AuthenticationBuilder so all
// existing authentication configuration methods are available here. It also ensures that the services for authorization are
// added if any authentication scheme is added. This property also registers an IConfigureOptions<AuthenticationOptions> along
// with a new mechanism to allow individual authentication schemes to have their options set from configuration too
// (similar to the way logging does today).
builder.Authentication.AddJwtBearer();
var app = builder.Build();
// The authentication and authorization middleware are automatically added after the routing middleware by the host if any
// authentication scheme is configured via builder.Authentication
app.MapGet("/hello", () => "Hello!");
// Add authorization requirements to the API definition
app.MapGet("/hello-protected", () => "Hello, you are authorized to see this!")
.RequireAuthorization(p => p.RequireClaim("scope", "myapi:protected-access"));
app.Run();
This time the following was different:
- No new namespaces were required
- No services were explicitly added
- Authentication was configured via a top-level property on the
builder
that is easy to discover - No options needed to be configured in code via callbacks as they’re automatically read from app configuration, e.g.
appsettings.json
(which will be populated by the tool used to create a test JWT) - No middleware was explicitly added
- The authorization requirements were defined directly on the endpoint definition as metadata
Issue Analytics
- State:
- Created 2 years ago
- Reactions:8
- Comments:15 (15 by maintainers)
Top GitHub Comments
Example matching
Authorization
changes to consider, allowing sharing of policies, etc.:The
WebApplicationBuilder.Authorization
property is typed asAuthorizationOptions
allowing simple creation of policies and configuration of the default and fallback policies:The
WebApplicationBuilder
would register anIConfigureOptions<AuthorizationOptions>
in the services collection with a delegate that applies the settings.Note this suggestion has a fundamental issue in that the
AuthorizationOptions
isn’t designed to be mutated in this way, rather it should be configured via a callback registered in DI so that it runs at the appropriate time during app startup and composes with other code that wishes to configure it.Perhaps instead the
Authentication
property should also read from configuration for authorization settings, and theAuthorization
property would be a new type that simply provides easy access to adding a configuration delegate, e.g.:Some other potential example policies as defined via configuration:
API Review:
IAuthenticationConfigurationProvider
doesn’t need aConfiguration
property.GetSection
name can be more descriptive.GetAuthenticationSchemeConfiguration
is clearer. This won’t be called frequently or by most apps, so we can live with a longer name.