Dotnet core 2.0 authentication multiple schemas identity cookies and jwt

17,562

Solution 1

Asp.Net Core 2.0 definitely support multiple authentication schemes. Rather than a hacking with authenticate middleware, you can try to specify the schema in Authorize attribute:

[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]

I gave a try and it worked fine. Assuming you have added both Identity and JWT as below:

services.AddIdentity<ApplicationUser, ApplicationRole>()
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)

Since AddIdentity() already set cookie authentication as the default schema, we have to specify schema in Authorize attribute of controllers. For now, I have no idea how to overwrite the default schema set by AddIdentity(), or maybe we'd better not to do that.

A work around is to compose a new class (you can call it JwtAuthorize) that derives from Authorize and have Bearer as the default schema, so you don't have to specify it every time.

UPDATE

Found the way to override Identity default authentication scheme!

Instead of below line:

services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)

Use below overload to set default schema:

services.AddAuthentication(option =>
                {
                    option.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
                })
                .AddJwtBearer(options =>....

UPDATE 2 As mentioned in comments, you can enable both Identity and JWT auth by join them together. [Authorize(AuthenticationSchemes = "Identity.Application" + "," + JwtBearerDefaults.AuthenticationScheme)]

Solution 2

I used this question for solving my (similar) problem of combining Identity and Bearer authentication in a .Net Core 2.0 web application. Important to note is that you need to add new[] { JwtBearerDefaults.AuthenticationScheme, IdentityConstants.ApplicationScheme to the following piece of code:

services.AddMvc(config =>
    {
        var defaultPolicy = new AuthorizationPolicyBuilder(new[] { JwtBearerDefaults.AuthenticationScheme, IdentityConstants.ApplicationScheme })
                         .RequireAuthenticatedUser()
                         .Build();
        config.Filters.Add(new AuthorizeFilter(defaultPolicy));
        config.Filters.Add(new AutoValidateAntiforgeryTokenAttribute());
        config.Filters.Add(new ValidateModelAttribute());
    });

AND

Add the default authentication option:

services.AddAuthentication(option =>
                {
                    option.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
                })
                .AddJwtBearer(options =>....

In my initial solution based on this question I did not notice that both changes in my code were needed. Hopefully I can save somebody the hours that I wasted :)

Solution 3

Based on what kevin rich says here http://www.whoiskevinrich.com/configuring-asp-net-core-2-0-authentication

I was able to set jwt as the default authentication method:

        services.AddAuthentication(sharedOptions =>
        {
            sharedOptions.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
            sharedOptions.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
            sharedOptions.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
            sharedOptions.DefaultForbidScheme = JwtBearerDefaults.AuthenticationScheme;
        })

I tested this and was able to remove (AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme) from the authorize attribute mentioned in donnytian's post.

Share:
17,562
didiHamman
Author by

didiHamman

Updated on June 28, 2022

Comments

  • didiHamman
    didiHamman almost 2 years

    In dotnet core 1.1 asp, I was able to configure and use identity middleware followed by jwt middleware by doing the following:

      app.UseIdentity();
      app.UseJwtBearerAuthentication(new JwtBearerOptions() {});
    

    This has now changed in that we implement the middleware with:

       app.UseAuthentication();
    

    Configuration of the settings is done via the ConfigureServices section of Startup.cs.

    There are some references to the use of authorization schema's in the migration documentation:

    https://docs.microsoft.com/en-us/aspnet/core/migration/1x-to-2x/identity-2x#authentication-middleware-and-services

    In 2.0 projects, authentication is configured via services. Each authentication scheme is registered in the ConfigureServices method of Startup.cs. The UseIdentity method is replaced with UseAuthentication.

    Additionally there is a reference to:

    Setting Default Authentication Schemes

    In 1.x, the AutomaticAuthenticate and AutomaticChallenge properties were intended to be set on a single authentication scheme. There was no good way to enforce this.

    In 2.0, these two properties have been removed as flags on the individual AuthenticationOptions instance and have moved into the base AuthenticationOptions class. The properties can be configured in the AddAuthentication method call within the ConfigureServices method of Startup.cs:

    Alternatively, use an overloaded version of the AddAuthentication method to set more than one property. In the following overloaded method example, the default scheme is set to CookieAuthenticationDefaults.AuthenticationScheme. The authentication scheme may alternatively be specified within your individual [Authorize] attributes or authorization policies.

    Is it still possible in dotnet core 2.0 to use multiple authentication schemas? I cannot get the policy to respect the JWT configuration ("Bearer" schema), and only Identity is working at present with both configured. I can't find any samples of multiple authentication schemas.

    Edit:

    I've reread the documentation, and now understand that the:

    app.UseAuthentication()
    

    adds automatic authentication against a default schema. Identity configures the default schemas for you.

    I have gotten around the issue with what seems like a hack working against the new api's by doing the following in Startup.cs Configure:

        app.UseAuthentication();
        app.Use(async (context, next) =>
        {
            if (!context.User.Identity.IsAuthenticated)
            {
                var result = await context.AuthenticateAsync(JwtBearerDefaults.AuthenticationScheme);
                if (result?.Principal != null)
                {
                    context.User = result.Principal;
                }
            }
    
            await next.Invoke();
        });
    

    Is this the correct way to do this, or should I be utilising the framework, DI and interfaces for custom implementations of IAuthenticationSchemeProvider?

    Edit - Futher details of the implementation and where to find it.

    The JWT Config can be found here, and I am using policies to define the authorization, which include the accepted auth schema's:

    https://github.com/Arragro/ArragroCMS/blob/master/src/ArragroCMS.Management/Startup.cs

    Custom middleware is still implemented. The Auth controller is here:

    https://github.com/Arragro/ArragroCMS/blob/master/src/ArragroCMS.Web.Management/ApiControllers/AuthController.cs

    It uses API Keys generated by the app to get read only access to data. You can find the implementation of a controller utilising the policy here:

    https://github.com/Arragro/ArragroCMS/blob/master/src/ArragroCMS.Web.Management/ApiControllers/SitemapController.cs

    Change the DB Connection string to point to your SQL Server, and run the application. It migrates the DB automatically and configures an admin user ([email protected] - ArragroPassword1!). Then go to the Settings tab in the menu bar and click "Configure the JWT ReadOnly API Key Settings" to get a key. In postman, get a jwt token by configuring a new tab and setting it to POST with the following address:

    http://localhost:5000/api/auth/readonly-token

    Supply the headers: Content-Type: application/json

    Supply the body:

    {
        "apiKey": "the api token from the previous step"
    }
    

    Copy the token in the response, and then use the following in postman:

    http://localhost:5000/api/sitemap/flat

    Authorization: "bearer - The token you received in the previous request"
    

    It will work inititally because of the custom middleware. Comment out the code mentioned above and try again and you will receive a 401.

    Edit -@DonnyTian's answer below covers my solution in his comments. The problem I was having was setting a default policy on UseMvc, but not supplying the schema's:

        services.AddMvc(config =>
        {
            var defaultPolicy = new AuthorizationPolicyBuilder(new[] { JwtBearerDefaults.AuthenticationScheme, IdentityConstants.ApplicationScheme })
                             .RequireAuthenticatedUser()
                             .Build();
            config.Filters.Add(new AuthorizeFilter(defaultPolicy));
            config.Filters.Add(new AutoValidateAntiforgeryTokenAttribute());
            config.Filters.Add(new ValidateModelAttribute());
        });
    

    Following the advice, this works without custom middleware.

  • Sunil Shahi
    Sunil Shahi over 6 years
    Just want to point out something you did not mention above. AuthenticationSchemes property in Authorize attribute accepts comma delimited list of Authentication Scheme name. like [Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme + "," + CookieAuthenticationDefaults.AuthenticationScheme)]
  • DonnyTian
    DonnyTian over 6 years
    Not sure why the answer has a down vote. It's should be a valid answer and please note the question is about Identity and JWT. Identity framework automatically added cookie for you.
  • didiHamman
    didiHamman over 6 years
    I can't get this to work. Every request is returning 401. I get "Authorization failed for user: null." "Authorization failed for the request at filter 'Microsoft.AspNetCore.Mvc.Authorization.AuthorizeFilter'." "Executing ChallengeResult with authentication schemes ([])." "AuthenticationScheme: Identity.Application was challenged.". Every authorization is performed with Identity with the above example, JWT isn't even challenged. Really want this to be the answer as it makes the most sense to me.
  • didiHamman
    didiHamman over 6 years
    The only issue I have is that I wanted to UseIdentity. This would involve rolling something different. I could go down this path, but its not what I set out to do initially.
  • didiHamman
    didiHamman over 6 years
    This just gets rid of Identity, leaving you only with Jwt only.
  • DonnyTian
    DonnyTian over 6 years
    I've looked into your Startup.cs and found issues: line#129 services.AddAuthentication(), you should pass a string to tell the scheme name you added. just as my answer stated. And you remove the line ".AddCookie()" right below this line since bearer token is not using any cookie, it uses HTTP header to pass the token.
  • DonnyTian
    DonnyTian over 6 years
    Just uploaded a demo, you can run the application and use Postman to play with it. You may need to comment and uncomment some lines to switch from MongoDb to EF. Check at github.com/donnytian/Todo/blob/master/Todo.Web/…
  • didiHamman
    didiHamman over 6 years
    Will try again soon after I have looked at your example, thanks for sharing it. The code linked is the original, not the version I changed to try you advice. I did pass the jwt scheme to addauthentication and removed the addcookie line (not sure why that ended up there to be honest)
  • DonnyTian
    DonnyTian over 6 years
    @didiHamman Then you'd better to check my AuthController to see how we should generate a token (ignore lines to add cookie, that's another senario). If you still get 401, post your Postman screenshot or check this link for how to play with Postman: logcorner.com/…
  • didiHamman
    didiHamman over 6 years
    @DonnyTian I've reimplemented and tried a few things, but with no success. I pulled down your code and configured it with EF, but stopped once I realised I had to do a bit of work to create a user and migrate the DB's. I've realigned the code, removing the addcookie line, and utilising the jwt scheme. The main files are referenced on the original question. The postman logs wouldn't serve much purpose here as this is working with the custom middleware, and it just returns a 401 with this config.
  • didiHamman
    didiHamman over 6 years
    @DonnyTian If you want to run this, you just need to change the DB connections string. Then login with [email protected] and ArragroPassword1!. The api keys can be found in the Settings section "Configure the JWT ReadOnly API Key Settings"
  • DonnyTian
    DonnyTian over 6 years
    @didiHamman cant believe I successfully started your project :). I am able to reproduce 401 by removing the hacking middleware. And your issue can be fixed at line #154 in Startup.cs by passing the Bearer scheme name to the policy builder constructor. And if you will never use identity cookie authentication, another solution is to set the Bearer scheme as the default scheme (yes, I found the way to override Identity's scheme), just see my updated answer content. Any further issue please contact my skype [email protected], love to discuss technologies.
  • didiHamman
    didiHamman over 6 years
    @DonnyTian Awesome, I had no idea about the default policy on services.AddMvc. That is working perfectly now and no need for that middleware. Thanks for your help!
  • John
    John over 6 years
    If you look at donntian's update, you'd noticed he posted the same thing, except slightly more succinct. Your edit corrects both of us on what you you needed for usemvc.
  • John
    John over 6 years
    Also, this is only overriding the defaults. If you check the options, identity is still there in case someone wanted to configure jwt first and cookie second.
  • didiHamman
    didiHamman over 6 years
    Yeah I misunderstood the defaults completely, I didn't realise they were applied regardless of whether you had an authorise attribute on your controller. You are right, this way would make Jwt the default authentication option.
  • Jonesopolis
    Jonesopolis over 6 years
    fan-freaking-tastic. Thanks a ton.
  • fubaar
    fubaar over 6 years
    Really good point with this. I have IdentityServer 4 providing JWT token authentication together with regular cookie authentication. But my authorization was not working until I added IdentityConstants.ApplicationScheme to AddAuthenticationSchemes. I had previously added CookieAuthenticationDefaults.AuthenticationScheme by I guess that wasn't what was required there.
  • Toni Petrina
    Toni Petrina about 6 years
    Wow! It wasn't cookies, it was Identity...thanks a lot!
  • Ramesh
    Ramesh about 6 years
    @DonnyTian, for me the below got fixed [Authorize(AuthenticationSchemes = "Identity.Application" + "," + JwtBearerDefaults.AuthenticationScheme)]