Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

Tags:

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

like image 425
GPW Avatar asked Sep 20 '17 13:09

GPW


People also ask

How does JWT work in .NET core?

JSON Web Tokens (commonly known as JWT) is an open standard to pass data between client and server, and enables you to transmit data back and forth between the server and the consumers in a secure manner.

How JWT token works in Web API?

JSON Web Tokens are an open, standard way for you to represent your user's identity securely during a two-party interaction. First, the user or client app sends a sign-in request. In this step, essentially, a username, password, or any other type of sign-in credentials the user provides will travel to the API.

How to implement JWT authentication in an ASP NET Core web API?

How to Implement JWT Authentication in an ASP.NET Core Web API project. Select ASP.NET Core Web Application option. Use can see the default folder structure. In this step, we need to install the following NuGet packages:

How to create a REST API with JWT token?

Under " Auth " choose type as " Bearer Token " and paste the copied token key in the " Token " field. Now by clicking on Send we will see the employee list. In this article, we have learned how to create a REST API using .Net 6.0, ASP.NET Core, perform basic CRUD operations, create a JWT token, and secure the APIs.

How to create custom authentication handler to validate JWT token?

Create Custom Authentication handler to validate JWT token Get the metadata from the custom authorization server (OAuth) Create .NET Core Project. Create "AuthConfigManager" class. It is used to get the metadata from the authorization server. Add below references to the project using the NuGet package manager.

How to check if the JWT middleware has validated the Bearer Token?

Since the breakpoint has been hit and you have been able to access the protected method in the API controller, the bearer token should have been validated by the JWT middleware. We can check the authentication header details by creating a watch on Request.Headers as shown:


2 Answers

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.

like image 173
GPW Avatar answered Oct 21 '22 20:10

GPW


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>() 
like image 38
Ricky Spanish Avatar answered Oct 21 '22 20:10

Ricky Spanish