Skip to content
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

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

Closed
DamianEdwards opened this issue Jan 29, 2022 · 10 comments
Assignees
Labels
area-auth Includes: Authn, Authz, OAuth, OIDC, Bearer area-commandlinetools Includes: Command line tools, dotnet-dev-certs, dotnet-user-jwts, and OpenAPI Epic Groups multiple user stories. Can be grouped under a theme. Needs: Design This issue requires design work before implementating.
Milestone

Comments

@DamianEdwards
Copy link
Member

DamianEdwards commented Jan 29, 2022

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 DamianEdwards added Epic Groups multiple user stories. Can be grouped under a theme. area-commandlinetools Includes: Command line tools, dotnet-dev-certs, dotnet-user-jwts, and OpenAPI area-auth Includes: Authn, Authz, OAuth, OIDC, Bearer Needs: Design This issue requires design work before implementating. labels Jan 29, 2022
@DamianEdwards
Copy link
Member Author

DamianEdwards commented Jan 31, 2022

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

@adityamandaleeka adityamandaleeka added this to the .NET 7 Planning milestone Jan 31, 2022
@ghost
Copy link

ghost commented Jan 31, 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

@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
Copy link
Member

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
Copy link
Member Author

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

@CamiloTerevinto
Copy link

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

  • 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.
  • 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...).
  • 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.

More related to this effort:

  • 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.
  • 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.
  • As @martincostello said, it should be straightforward to use these JWTs under integration testing. This would simplify tests when running on a CI system.

@DamianEdwards
Copy link
Member Author

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
Copy link

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:

  • Environment: "Development" (localhost debugging). JWTs accepted: dev-jwt (used by the dev) and STS (used to validate the integration with other services).
  • Environment: "anything else". JWTs accepted: STS.

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
Copy link
Member Author

@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
Copy link
Member

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

@ghost ghost locked as resolved and limited conversation to collaborators Jul 6, 2022
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
area-auth Includes: Authn, Authz, OAuth, OIDC, Bearer area-commandlinetools Includes: Command line tools, dotnet-dev-certs, dotnet-user-jwts, and OpenAPI Epic Groups multiple user stories. Can be grouped under a theme. Needs: Design This issue requires design work before implementating.
Projects
None yet
Development

No branches or pull requests

6 participants