Skip to content

Simplify Authentication and Authorization configuration when using WebApplicationBuilder #39855

Closed
@DamianEdwards

Description

@DamianEdwards

Today, 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:

  1. Importing namespaces
  2. Adding services via the builder
  3. Adding and configuring an authentication scheme using an options configuration delegate
  4. Adding middleware that are order-dependent
  5. 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 Allow direct configuration of authorization policies via endpoint metadata #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:

  1. No new namespaces were required
  2. No services were explicitly added
  3. Authentication was configured via a top-level property on the builder that is easy to discover
  4. 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)
  5. No middleware was explicitly added
  6. The authorization requirements were defined directly on the endpoint definition as metadata

Metadata

Metadata

Labels

api-approvedAPI was approved in API review, it can be implementedarea-authIncludes: Authn, Authz, OAuth, OIDC, Bearerarea-minimalIncludes minimal APIs, endpoint filters, parameter binding, request delegate generator etcfeature-minimal-hostingold-area-web-frameworks-do-not-use*DEPRECATED* This label is deprecated in favor of the area-mvc and area-minimal labels

Type

No type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions