Skip to content

Simplify Authentication and Authorization configuration when using WebApplicationBuilder #39855

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Tracked by #34545
DamianEdwards opened this issue Jan 29, 2022 · 15 comments
Assignees
Labels
api-approved API was approved in API review, it can be implemented area-auth Includes: Authn, Authz, OAuth, OIDC, Bearer area-minimal Includes minimal APIs, endpoint filters, parameter binding, request delegate generator etc feature-minimal-hosting old-area-web-frameworks-do-not-use *DEPRECATED* This label is deprecated in favor of the area-mvc and area-minimal labels
Milestone

Comments

@DamianEdwards
Copy link
Member

DamianEdwards commented Jan 29, 2022

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
@DamianEdwards DamianEdwards added area-auth Includes: Authn, Authz, OAuth, OIDC, Bearer feature-minimal-hosting old-area-web-frameworks-do-not-use *DEPRECATED* This label is deprecated in favor of the area-mvc and area-minimal labels labels Jan 29, 2022
@DamianEdwards DamianEdwards added this to the .NET 7 Planning milestone Jan 29, 2022
@ghost
Copy link

ghost commented Jan 29, 2022

Thanks for contacting us.

We're moving this issue to the .NET 7 Planning milestone for future evaluation / consideration. We would like to keep this around to collect more feedback, which can help us with prioritizing this work. We will re-evaluate this issue, during our next planning meeting(s).
If we later determine, that the issue has no community involvement, or it's very rare and low-impact issue, we will close it - so that the team can focus on more important and high impact issues.
To learn more about what to expect next and how this issue will be handled you can read more about our triage process here.

@kevinchalet
Copy link
Contributor

kevinchalet commented Jan 29, 2022

// The authentication and authorization middleware are automatically added after the routing middleware by the host if any
// authentication scheme is configured via builder.Authentication

Please be very careful when designing this part as this will likely impact authentication handlers that implement IAuthenticationRequestHandler: they are invoked by the authentication middleware (registered by app.UseAuthentication()) and what comes before that middleware might be important (as a concrete example, OpenIddict will typically require that the CORS middleware be invoked before it starts handling things like discovery requests).

Edit: also, in your last example, I don't see where the default authentication scheme is defined. I'd add these 2 questions to your list of things to take into account 😃

  • What happens if multiple authentication handlers are registered?
  • Who/what decides what becomes the default scheme handler?

@DamianEdwards
Copy link
Member Author

The intent is that the ordering of the middleware can still be controlled for scenarios where the order matters, similar to what happens today with the implicitly registered routing middleware. But indeed we'll need to consider the interactions you mention.

Regarding the default handler, one of my goals with this is to simplify the setup of simpler authentication scenarios, like those where there is only one handler registered. I would very much like it to behave such that if there is only one handler, it becomes the default. For scenarios with multiple handlers, I imagine the default would need to be set by the developer on the AuthenticationBuilder.

@rafikiassumani-msft rafikiassumani-msft added the Needs: Design This issue requires design work before implementating. label Feb 15, 2022
@kevinchalet
Copy link
Contributor

But indeed we'll need to consider the interactions you mention.

FYI, here's a concrete case involving the CORS middleware, gRPC-Web and authentication handlers: openiddict/openiddict-core#1385 😃

@DamianEdwards
Copy link
Member Author

@kevinchalet thanks, that's helpful. @JamesNK FYI.

@ghost
Copy link

ghost commented Mar 17, 2022

Thanks for contacting us.

We're moving this issue to the .NET 7 Planning milestone for future evaluation / consideration. We would like to keep this around to collect more feedback, which can help us with prioritizing this work. We will re-evaluate this issue, during our next planning meeting(s).
If we later determine, that the issue has no community involvement, or it's very rare and low-impact issue, we will close it - so that the team can focus on more important and high impact issues.
To learn more about what to expect next and how this issue will be handled you can read more about our triage process here.

@DamianEdwards
Copy link
Member Author

Starting to explore this over at https://github.com/DamianEdwards/AspNetCoreDevJwts

@Tratcher
Copy link
Member

Tratcher commented May 6, 2022

4. Options were configured inline by constructing & initializing the options type directly (no configure callback)

What's the concern with callbacks? We use them everywhere.

What's the expected type for builder.Authentication, the existing AuthenticationBuilder or something new?

How are the default schemes supposed to be set? Especially when there are multiple combine schemes like Identity + Cookies.
Before: AddAuthentication(JwtBearerDefaults.AuthenticationScheme)

@DamianEdwards
Copy link
Member Author

What's the concern with callbacks? We use them everywhere.

In discussion yesterday we decided there was no concern with the callback and that doing anything different would juts cause more issues.

What's the expected type for builder.Authentication, the existing AuthenticationBuilder or something new?

It's a public sealed class WebApplicationAuthenticationBuilder : AuthenticationBuilder, so existing extension methods continue to work but we can intercept auth builder interactions via builder.Authentication if necessary. We'll see if that's really needed as we flesh this out more.

How are the default schemes supposed to be set? Especially when there are multiple combine schemes like Identity + Cookies.

Yep we're opening this up for discussion again 😃

The need to set a default scheme when only a single scheme is configured (which is pretty much 100% of the time when folks are starting out adding auth to their API app) is a complication we'd like to avoid.

@captainsafia
Copy link
Member

To support automatically registering middlewares and services related to authentication when a user registers an authentication handler and for providing a shorthand for invoking extension methods on the AuthenticationBuilder.

namespace Microsoft.AspNetCore.Builder;

class WebApplicationBuilder
{
+  public AuthenticationBuilder Authentication { get; }
}

We introduced a new IAuthenticationConfigurationProvider interface for resolving a configuration section through DI.

namespace Microsoft.AspNetCore.Authentication;

public class IAuthenticationConfigurationProvider
{
  public IConfigurationRoot Configuration { get; }
  public IConfiguration GetSection(string name)
}

Since we support automatically resolving authentication options from configuration, we introduce a new AddJwtBearer overload that takes an authentication scheme (with the need for an Action<JwtBearerOptions> parameter.

namespace Microsoft.Extensions.DependencyInjection;

public static class JwtBearerExtensions
{
+ public static AuthenticationBuilder AddJwtBearer(this AuthenticationBuilder builder, string authenticationScheme)
}

@DamianEdwards
Copy link
Member Author

DamianEdwards commented May 26, 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" ]
      }
    }
  }
}

@captainsafia captainsafia added api-ready-for-review API is ready for formal API review - https://github.com/dotnet/apireviews and removed Needs: Design This issue requires design work before implementating. labels May 26, 2022
@ghost
Copy link

ghost commented May 26, 2022

Thank you for submitting this for API review. This will be reviewed by @dotnet/aspnet-api-review at the next meeting of the ASP.NET Core API Review group. Please ensure you take a look at the API review process documentation and ensure that:

  • The PR contains changes to the reference-assembly that describe the API change. Or, you have included a snippet of reference-assembly-style code that illustrates the API change.
  • The PR describes the impact to users, both positive (useful new APIs) and negative (breaking changes).
  • Someone is assigned to "champion" this change in the meeting, and they understand the impact and design of the change.

@halter73
Copy link
Member

#41520 is the PR with the API change.

@halter73
Copy link
Member

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)
}

@halter73 halter73 added api-approved API was approved in API review, it can be implemented and removed api-ready-for-review API is ready for formal API review - https://github.com/dotnet/apireviews labels May 27, 2022
@DamianEdwards
Copy link
Member Author

Spun-off the authorization discussion to a new issue #42172

@ghost ghost locked as resolved and limited conversation to collaborators Jul 15, 2022
@amcasey amcasey added the area-minimal Includes minimal APIs, endpoint filters, parameter binding, request delegate generator etc label Jun 2, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
api-approved API was approved in API review, it can be implemented area-auth Includes: Authn, Authz, OAuth, OIDC, Bearer area-minimal Includes minimal APIs, endpoint filters, parameter binding, request delegate generator etc feature-minimal-hosting old-area-web-frameworks-do-not-use *DEPRECATED* This label is deprecated in favor of the area-mvc and area-minimal labels
Projects
None yet
Development

No branches or pull requests

7 participants