.Net Core 2.0 Web API using JWT - Adding Identity breaks the JWT authentication
Asked Answered
P

2

23

(Edit - Found proper fix! see below)

OK - this is my first attempt at .Net Core 2.0 and authentication, though I've done things with Web API 2.0 in the past, and have worked fairly extensively on various MVC and Webforms ASP projects over the last couple of years.

I'm trying to create a Web API ONLY project using .Net Core. This will form the back end of a multi-tenant application for producing some reports, so I need to be able to authenticate users. It seems the usual approach is to use JWT - first authenticate the user to generate a token, then pass that to the client to use on every API request. Data will be stored and retrieved using EF Core.

I followed this post for a basic way to get this set up, and I managed to get this to work ok - I have a controller which accepts a username/password and returns a token if valid, and some Authorization policies set up based on the claims.

The next thing I need is to actually manage the users/passwords/etc. I thought I'd just use .Net Core Identity for this as that way I would have lots of ready-made code for worry about users/roles, passwords etc. I was using custom User class and UserRole classes which derived from the standard IdentityUser and IdentityRole classes, but I've since reverted to the standard ones now.

The problem I have is that I can't quite figure out how to add identity & register all the various Services (rolemanager, usermanager, etc) without also breaking the authentication - basically as soon as I add this line to my Startup.ConfigureServices class:

services.AddIdentity<IdentityUser, IdentityRole>()
    .AddEntityFrameworkStores<MyContext>();

It all goes wrong, and I can no longer see any claims when I receive a request, so all the policies just lock down and you can't get to anything.

If I don't have those lines, then I end up with errors related to UserManager, RoleManager, UserStore etc. all not being registered for DI.

So... how (if it's possible) can I register Identity and hook it up to the Context correctly, but avoid/Remove any changes to the actual Authorisation mechanism?

I've looked around a fair bit online, but a lot of this has changed since .Net Core 1.x so a lot of the tutorials etc. aren't really valid any more.

I'm not intending this API application to have any front-end code, so I don't need any cookie authentication for forms or anything for now.

Edit
ok, I've now found that in this code setting up the JWT authentication in the Startup.ConfigureServices() method:

 services.AddAuthentication(
            JwtBearerDefaults.AuthenticationScheme)
                .AddJwtBearer(options =>
                {
                 >>breakpoint>>>   options.TokenValidationParameters =
                        new TokenValidationParameters
                        {
                            ValidateIssuer = true,
                            ValidateAudience = true,
                            ValidateLifetime = true,
                            ValidateIssuerSigningKey = true,

                            ValidIssuer = "Blah.Blah.Bearer",
                            ValidAudience = "Blah.Blah.Bearer",
                            IssuerSigningKey =
                            JwtSecurityKey.Create("verylongsecretkey")

                        };
                });

If I put a breakpoint at the line indicated (via ">>breakpoint>>>") then it gets hit when I don't add the lines to add identity services, but if I do add those lines then it never gets hit. This is true no matter where in the method I put the services.AddIdentity() call. I get that this is simply a lambda so it's getting executed at a later point, but is there any way I can get the AddIdentity stuff to NOT set up Authentication, or make the code immediately remove it? I assume at some point there's some code which elects to not run the Lambda for config I've set there as the Identity stuff has already set it...

Thanks for reading all that if you have :)

EDIT - Found an answer
ok, I eventually found this GH issue which is basically exactly this problem: https://github.com/aspnet/Identity/issues/1376

Basically what I had to do was twofold:

Ensure that the call to services.AddIdentity<IdentityUser, IdentityContext() was made first

Change the call to add auth from:

services.AddAuthentication(
            JwtBearerDefaults.AuthenticationScheme)
                .AddJwtBearer(options =>
...

To:

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

This does annoyingly result in a cookie being created, but this isn't then used for authentication as far as I can tell - it's purely using the bearer token on requests to controllers/actions which have [Authorize(Policy = "Administrator")] or similar set at least.

I need to test more, and I'll try to come back here an update this if I find it is not working in some way.

(Edited - put proper solution in as an answer now)

Pilpul answered 20/9, 2017 at 13:46 Comment(3)
you saved me. thanksSonia
you should consider breaking this into a question and then answering it yourself as a proper answer.Bambino
@Bambino not a bad idea... so I've done so.Pilpul
P
28

I eventually put together the solution, so on the suggestion of user alwayslearning I've edited my post and I'm putting this in as an actual answer.

ok, This can be done properly. First, you need to use the Authentication options I pointed out in my edit above - that's fine. Then you need to useservices.AddIdentityCore<TUser>() rather than services.AddIdentity<TUser>(). This however, doesn't add a whole bunch of things for role management, and is apparently lacking the proper constructor to give it the type of Role you want to use. This means that in my case I had to do this:

  IdentityBuilder builder = services.AddIdentityCore<IdentityUser>(opt =>
        {
            opt.Password.RequireDigit = true;
            opt.Password.RequiredLength = 8;
            opt.Password.RequireNonAlphanumeric = false;
            opt.Password.RequireUppercase = true;
            opt.Password.RequireLowercase = true;
        }
        );
        builder = new IdentityBuilder(builder.UserType, typeof(IdentityRole), builder.Services);
        builder
            .AddEntityFrameworkStores<MyContext>();
        //.AddDefaultTokenProviders();

        builder.AddRoleValidator<RoleValidator<IdentityRole>>();
        builder.AddRoleManager<RoleManager<IdentityRole>>();
        builder.AddSignInManager<SignInManager<IdentityUser>>();

Having done that, the next thing is to make sure that when validating a user login (prior to sending a token), you make sure to use the SignInManager method CheckPasswordSignInAsync and not PasswordSignInAsync:

public async Task<IdentityUser> GetUserForLogin(string userName, string password)
    {   
        //find user first...
        var user = await _userManager.FindByNameAsync(userName);

        if (user == null)
        {
            return null;
        }

        //validate password...
        var signInResult = await _signInManager.CheckPasswordSignInAsync(user, password, false);

        //if password was ok, return this user.
        if (signInResult.Succeeded)
        {
            return user;
        }

        return null;
    }

if you use the PasswordSignInAsync method then you'll get a runtime error re. No IAuthenticationSignInHandler being configured.

I hope this helps someone at some point.

Pilpul answered 21/11, 2017 at 10:9 Comment(7)
You definitely helped me. Thanks a lot. There are many tutorials outdated on the internet when you have no experience with JWT or Identity it's hard to find a solution.Ejector
I would like to ask what builder = new IdentityBuilder(builder.UserType, typeof(IdentityRole), builder.Services); does?Boyse
@Boyse That's part of the workaround for the fact that the AddIdentityCore() method doesn't have a constructor which takes the type of role in use (in my case I was just using the built-in IdentityRole class, whereas it's common to derive from this to add custom properties to roles, in which case you would need to pass it the derived class). Basically it's the workaround to enable the subsequent lines which add/register the RoleValidator, RoleManager and SignInManager to the DI system so they are then available to all the middleware.Pilpul
great answer thanks for also mentioning the checkpassworkasync call. Not sure why this information seems to be buried away, and difficult to find examples of auth in .net core.Springe
Possible candidate for a Nobel Prize !Myrtia
@Pilpul It seems to work fine without having to re-instantiate the IdentityBuilder variable although all posts I came across used re-instantiating method. var builder = services.AddIdentityCore<ApplicationUser>(o => … ) .AddRoles<IdentityRole>() .AddEntityFrameworkStores<ApplicationDbContext>()Monaco
@TandiSunarto - on looking at the code, it appears you have a point, BUT if I briefly try this (basically replace the re-instantiation with a chained call to .AddRoles<IdentityRole>() then my app simply doesn't seem to start up correctly. I don't currently have time to look into this, but it clearly isn't the same in my case (may be different in .Net Core 2.1+, this is 2.0)Pilpul
C
4

I have extracted the AddIdentity code from github and created an extension method based on it that doesn't add the default Cookie Authenticator, It's now pretty similar to the built in AddIdentityCore but can accept IdentityRole.

 /// <summary>
 /// Contains extension methods to <see cref="IServiceCollection"/> for configuring identity services.
 /// </summary>
 public static class IdentityServiceExtensions
 {
     /// <summary>
     /// Adds the default identity system configuration for the specified User and Role types. (Without Authentication Scheme)
     /// </summary>
     /// <typeparam name="TUser">The type representing a User in the system.</typeparam>
     /// <typeparam name="TRole">The type representing a Role in the system.</typeparam>
     /// <param name="services">The services available in the application.</param>
     /// <returns>An <see cref="IdentityBuilder"/> for creating and configuring the identity system.</returns>
     public static IdentityBuilder AddIdentityWithoutAuthenticator<TUser, TRole>(this IServiceCollection services)
         where TUser : class
         where TRole : class
         => services.AddIdentityWithoutAuthenticator<TUser, TRole>(setupAction: null);

     /// <summary>
     /// Adds and configures the identity system for the specified User and Role types. (Without Authentication Scheme)
     /// </summary>
     /// <typeparam name="TUser">The type representing a User in the system.</typeparam>
     /// <typeparam name="TRole">The type representing a Role in the system.</typeparam>
     /// <param name="services">The services available in the application.</param>
     /// <param name="setupAction">An action to configure the <see cref="IdentityOptions"/>.</param>
     /// <returns>An <see cref="IdentityBuilder"/> for creating and configuring the identity system.</returns>
     public static IdentityBuilder AddIdentityWithoutAuthenticator<TUser, TRole>(this IServiceCollection services, Action<IdentityOptions> setupAction)
         where TUser : class
         where TRole : class
     {
         // Hosting doesn't add IHttpContextAccessor by default
         services.AddHttpContextAccessor();
         // Identity services
         services.TryAddScoped<IUserValidator<TUser>, UserValidator<TUser>>();
         services.TryAddScoped<IPasswordValidator<TUser>, PasswordValidator<TUser>>();
         services.TryAddScoped<IPasswordHasher<TUser>, PasswordHasher<TUser>>();
         services.TryAddScoped<ILookupNormalizer, UpperInvariantLookupNormalizer>();
         services.TryAddScoped<IRoleValidator<TRole>, RoleValidator<TRole>>();
         // No interface for the error describer so we can add errors without rev'ing the interface
         services.TryAddScoped<IdentityErrorDescriber>();
         services.TryAddScoped<ISecurityStampValidator, SecurityStampValidator<TUser>>();
         services.TryAddScoped<ITwoFactorSecurityStampValidator, TwoFactorSecurityStampValidator<TUser>>();
         services.TryAddScoped<IUserClaimsPrincipalFactory<TUser>, UserClaimsPrincipalFactory<TUser, TRole>>();
         services.TryAddScoped<UserManager<TUser>>();
         services.TryAddScoped<SignInManager<TUser>>();
         services.TryAddScoped<RoleManager<TRole>>();

         if (setupAction != null)
         {
             services.Configure(setupAction);
         }

         return new IdentityBuilder(typeof(TUser), typeof(TRole), services);
     }
 }

Now you can use the above code normally from a WebApi project like so

.AddIdentityWithoutAuthenticator<User, IdentityRole>()
Coalfish answered 7/11, 2018 at 22:29 Comment(1)
+1 for the effort. I may get around to using this, but what I have for now is working fine. Definitely useful to make life easier, hopefully any newcomers to this question will now have neater code :)Pilpul

© 2022 - 2024 — McMap. All rights reserved.