question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Simplify Authentication and Authorization configuration when using WebApplicationBuilder

See original GitHub issue

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 #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

Issue Analytics

  • State:closed
  • Created 2 years ago
  • Reactions:8
  • Comments:15 (15 by maintainers)

github_iconTop GitHub Comments

3reactions
DamianEdwardscommented, Jun 1, 2022

Example matching Authorization changes to consider, allowing sharing of policies, etc.:

var builder = WebApplication.CreateBuilder(args);

builder.Authentication.AddJwtBearer();
builder.Authorization.AddPolicy("HasProtectedAccess", policy =>
    policy.RequireClaim("scope", "myapi:protected-access"));

var app = builder.Build();

app.MapGet("/hello", () => "Hello!");

app.MapGet("/hello-protected", () => "Hello, you are authorized to see this!")
    .RequireAuthorization("HasProtectedAccess");

app.MapGet("/hello-also-protected", () => "Hello, you authorized to see this to!")
    .RequireAuthorization("HasProtectedAccess");

app.Run();

The WebApplicationBuilder.Authorization property is typed as AuthorizationOptions allowing simple creation of policies and configuration of the default and fallback policies:

builder.Authorization.AddPolicy("HasProtectedAccess", policy => policy.RequireClaim("scope", "myapi:protected-access"));
builder.Authorization.DefaultPolicy = builder.Authorization.GetPolicy("HasProtectedAccess");

// Consider new methods to enable easily setting default/fallback policies by name
builder.Authorization.SetDefaultPolicy("HasProtectedAccess");
builder.Authorization.SetFallbackPolicy("HasProtectedAccess");

The WebApplicationBuilder would register an IConfigureOptions<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 the Authorization property would be a new type that simply provides easy access to adding a configuration delegate, e.g.:

{
  "Authorization": {
    "DefaultPolicy": "HasProtectedAccess",
    "FallbackPolicy": "",
    "InvokeHandlersAfterFailure": true,
    "Policies": {
      "HasProtectedAccess": {
        "Claims": [
          { "scope" : "myapi:protected-access" }
        ]
      }
    }
  }
}
builder.Authentication.AddJwtBearer();
builder.Authorization.Configure(authz =>
{
    // Following is the code-based equivalent of config above
    authz.AddPolicy("HasProtectedAccess", policy => policy.RequireClaim("scope", "myapi:protected-access"));
    authz.DefaultPolicy = authz.GetPolicy("HasProtectedAccess");
});

Some other potential example policies as defined via configuration:

{
  "Authorization": {
    "DefaultPolicy": "HasProtectedAccess",
    "Policies": {
      "AuthenticatedUsers": {
        "AuthenticationRequired": true
      },
      "Employees": {
        "AuthenticationRequired": true,
        "Roles": [ "Employees" ]
      },
      "OnlyHomers": {
        "AuthenticationRequired": true,
        "UserName": "Homer"
      },
      "ApiClients": {
        "AuthenticationRequired": true,
        // Any unrecognized properties are auto-mapped as claims perhaps?
        "scope": [ "myapi:read", "myapi:protected-access" ]
      }
    }
  }
}
1reaction
halter73commented, May 27, 2022

API Review:

  • IAuthenticationConfigurationProvider doesn’t need a Configuration 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.
namespace Microsoft.AspNetCore.Builder;
 
class WebApplicationBuilder
{
+  public AuthenticationBuilder Authentication { get; }
}
 
namespace Microsoft.AspNetCore.Authentication;
 
+ public class IAuthenticationConfigurationProvider
+ {
+  public IConfiguration GetAuthenticationSchemeConfiguration(string authenticationScheme)
+ }

 
namespace Microsoft.Extensions.DependencyInjection;
 
public static class JwtBearerExtensions
{
+ public static AuthenticationBuilder AddJwtBearer(this AuthenticationBuilder builder, string authenticationScheme)
}
Read more comments on GitHub >

github_iconTop Results From Across the Web

What's New in .NET 7 for Authentication and Authorization
Let's explore the new .NET 7 features for improving and simplifying authentication and authorization support in .NET applications.
Read more >
ASP.NET Core updates in .NET 7 Preview 5
Simplified authentication configuration. Authentication options can now be automatically configured directly from the app's configuration ...
Read more >
Using PathBase with .NET 6's WebApplicationBuilder
In this post I describe the difficulties of adding calls to UsePathBase with .NET 6 WebApplication programs, and describe two approaches to ...
Read more >
Comparing WebApplicationBuilder to the Generic Host
In this post I take a first look at the new WebApplication and WebApplicationBuilder types in .NET 6, and compare them to the...
Read more >
Minimal APIs in .NET 6 — A Complete Guide(Beginners ...
NET6 has simplified a lot of tedious tasks by introducing ... Minimal APIs — Adding Authentication and Authorization using JWT.
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found