dotnet / aspnetcore

ASP.NET Core is a cross-platform .NET framework for building modern cloud-based web applications on Windows, Mac, or Linux.
https://asp.net
MIT License
35.34k stars 9.98k forks source link

Developers can easily work with JWT bearer authentication for API apps during development #39857

Closed DamianEdwards closed 2 years ago

DamianEdwards commented 2 years ago

Basic idea is to do for JWT bearer authentication what we did for HTTPS in development, i.e. make it extremely easy to configure apps to use JWT bearer authentication in development, without the need for a discrete token issuing server.

Example Minimal APIs using dev JWTs

> dotnet new webapi -minimal -o MyApi
> cd MyApi
MyApi> dotnet dev-jwts list
Could not find the global property 'UserSecretsId' in MSBuild project 'MyApi/MyApi.csproj'. Ensure this property
is set in the project or use the 'dotnet user-secrets init' command to initialize this project.
MyApi> dotnet user-secrets init
Set UserSecretsId to '4105052b-5b99-4fff-8fc1-9d6c59887d0a' for MSBuild project 'MyApi/MyApi.csproj'.
MyApi> dotnet dev-jwts list
No tokens configured for this application.
MyApi> dotnet dev-jwts create
Token created for user "damian":
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4iLCJpYXQiOjE1MTYyMzkwMjJ9.
MyApi> dotnet dev-jwts create --name privileged --claim scope="myapi:protected-access"
Token created for user "privileged":
jHy8bGciOiJIUzIR5cCI61NiIsInIkpXVCIxMjM0NTweiuI6IkpvakwIiwiJ9.eyJzdWIiOibmFtZSG4iLCJpYMTYyMzkwMjJ9XQiOjE1.
MyApi> dotnet dev-jwts list
User        Issued               Expires    
------      -------------------  -------------------
damian      2022-01-28 17:37:34  2022-07-28 17:37:34
privileged  2022-01-28 17:37:48  2022-07-28 17:37:48
var builder = WebApplication.CreateBuilder(args);

builder.Authentication.AddJwtBearer();

var app = builder.Build();

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

app.MapGet("/hello-protected", () => "Hello, you are authorized to see this!")
    .RequireAuthorization(p => p.RequireClaim("scope", "myapi:protected-access"));

app.Run();
DamianEdwards commented 2 years ago

Suggested by @pranavkm, this might be better incorporated into the the existing dotnet user-secrets tools, which we should alias to dotnet dev-secrets in .NET 7, giving us a nice symmetry of dotnet dev-certs jwt and dotnet dev-secrets jwt, e.g.:

> dotnet new webapi -minimal -o MyApi
> cd MyApi
MyApi> dotnet dev-secrets jwt list
Could not find the global property 'UserSecretsId' in MSBuild project 'MyApi/MyApi.csproj'. Ensure this property
is set in the project or use the 'dotnet dev-secrets init' command to initialize this project.
MyApi> dotnet dev-secrets init
Set UserSecretsId to '4105052b-5b99-4fff-8fc1-9d6c59887d0a' for MSBuild project 'MyApi/MyApi.csproj'.
MyApi> dotnet dev-secrets jwt list
No tokens configured for this application.
MyApi> dotnet dev-secrets jwt create
Token created for user "damian":
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4iLCJpYXQiOjE1MTYyMzkwMjJ9.
MyApi> dotnet dev-secrets jwt create --name privileged --claim scope="myapi:protected-access"
Token created for user "privileged":
jHy8bGciOiJIUzIR5cCI61NiIsInIkpXVCIxMjM0NTweiuI6IkpvakwIiwiJ9.eyJzdWIiOibmFtZSG4iLCJpYMTYyMzkwMjJ9XQiOjE1.
MyApi> dotnet dev-secrets jwt list
User        Issued               Expires    
------      -------------------  -------------------
damian      2022-01-28 17:37:34  2022-07-28 17:37:34
privileged  2022-01-28 17:37:48  2022-07-28 17:37:48
ghost commented 2 years ago

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 commented 2 years ago

@davidfowl convinced me that it's likely not worth using a cert for the dev JWT tokens, rather we can simply generate a symmetric key and store it in user/dev secrets. Certificates have a habit of causing issues, especially on Linux, and don't provide any additional benefit in this scenario.

martincostello commented 2 years ago

Making a variant of this work for automated integration test scenarios too, like with Mvc.Testing, would be most welcome.

A very very very off-the-top-of-my-head idea of what I'm getting at is something like this:

WebApplicationFactory<Program> webApplicationFactory = ...;

HttpClient httpClient = webApplicationFactory
   .CreateDefaultClient()
   .WithBearerJwtAuthorization(x => x.WithClaim(ClaimTypes.NameIdentifier, "john-smith"));

// The below call to the protected endpoint succeeds because there's a valid JWT
// for the john-smith user in the Authorization header on the HttpClient
string html = await httpClient.GetStringAsync("/admin-secrets");
DamianEdwards commented 2 years ago

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

CamiloTerevinto commented 2 years ago

Some thoughts which might not be related to this work but rather more general related to auth in ASP.NET Core:

More related to this effort:

DamianEdwards commented 2 years ago

Thanks for the feedback @CamiloTerevinto. Some thoughts:

Make it easier to discover how to enforce auth: builder.Authentication.RequireAuthentication() or similar, which could just hide the call to add the AuthorizeAttribute filter.

I like this idea. I've personally struggled with finding the right settings to "just require auth for the whole app" (FallbackPolicy et al). We'll consider this scenario.

More for Swashbuckle than here, but it needs to be made much simpler to add auth to SwaggerUI. I've seen a lot of times people turn off auth for debugging was because they didn't know how to add the security requirements using Swashbuckle's library (and magically that ends up going to production...).

Absolutely. We have another issue where we're tracking that scenario and @captainsafia is especially keen to get this scenario working by default.

It's almost embarrassing to have to look up how to configure the AddJwtBearer call because it doesn't use the sub claim for the User.Identity.Name by default. This should be the default, with maybe a fallback/flag for the old behaviour.

I found that odd too (admittedly I'm not hugely familiar with JWT "in the real world") and in my experiment I made it so the dev JWTs do indeed set a sub claim with the username. We can follow up with the identity folks as to why this isn't a default.

A sample application should make it very clear that this is intended for development only and should make it effortless to have the production configuration applied instead.

Agreed. The code in my experiment is written to work this way, I just haven't added the "not development" configuration in the app yet. The intention is it will work that way though. Of course it all comes down to how the configuration code is written in the app, i.e. whether it overwrites the JWT options or adds to them, e.g.:

builder.Authentication.AddJwtBearer(c =>
{
    if (!builder.Environment.IsDevelopment())
    {
        // Code here to add the issuers, audiences, signing credentials, etc. for non-dev environments
    }
});

It should be possible to accept these JWTs as well as from a STS - especially useful to validate integrations and when the "development" environment is not localhost.

Do you mean you'd like an easy way to standup endpoints that act as an STS that serves JWTs in the same fashion, either in the same application or as a separate application? Or just that I should be able to configure the dev JWT options manually when not in development too, e.g. builder.Authentication.AddJwtBearer(useDevDefaults: true)?

As @martincostello said, it should be straightforward to use these JWTs under integration testing. This would simplify tests when running on a CI system.

Yep that's a great scenario we'll consider too.

CamiloTerevinto commented 2 years ago

Thanks for the response @DamianEdwards.

Agreed. The code in my experiment is written to work this way, I just haven't added the "not development" configuration in the app yet. The intention is it will work that way though. Of course it all comes down to how the configuration code is written in the app, i.e. whether it overwrites the JWT options or adds to them, e.g.:

You see all this stuff here in your example lib? Ideally, when "not in development" (akin to your example), the production settings would be used. Do notice though that you would have to discard the default dev-jwt settings in that case since it could cause confusion or incorrect settings. I would say that, ideally, we should be able to handle all that through IConfiguration (appsettings and/or environment variables).

Do you mean you'd like an easy way to standup endpoints that act as an STS that serves JWTs in the same fashion, either in the same application or as a separate application?

Sorry, let me rephrase that. What I meant is that it may be desired to be able to have the dev-jwt tokens accepted as well as the tokens by some other service. Think of this case:

However, if we could have the library have endpoints to generate JWTs, it would be fantastic! As long as the default options are secure enough (short lived, strong keys, etc.), it would save a lot of problems of people re-implementing these for small apps that cannot afford a separate STS. But that's... not too far from what Identity does. Side note: it would be awesome to have Identity generate JWTs and non-UI-based endpoints.

DamianEdwards commented 2 years ago

@CamiloTerevinto the intent is exactly what you state: that the dev configuration can be used in conjunction with any custom configuration so that both kinds of JWTs are accepted. That's why the code in my example is written to add to the configured signers, etc. rather than just set them.

captainsafia commented 2 years ago

The initial version of user-jwts shipped in preview5. We are tracking some follow-ups in #41820 and #41888.