84

I've been on quite an adventure to get JWT working on DotNet core 2.0 (now reaching final release today). There is a ton of documentation, but all the sample code seems to be using deprecated APIs and coming in fresh to Core, It's positively dizzying to figure out how exactly it's supposed to be implemented. I tried using Jose, but app. UseJwtBearerAuthentication has been deprecated, and there is no documentation on what to do next.

Does anyone have an open source project that uses dotnet core 2.0 that can simply parse a JWT from the authorization header and allow me to authorize requests for a HS256 encoded JWT token?

The class below doesn't throw any exceptions, but no requests are authorized, and I get no indication why they are unauthorized. The responses are empty 401's, so to me that indicates there was no exception, but that the secret isn't matching.

One odd thing is that my tokens are encrypted with the HS256 algorithm, but I see no indicator to tell it to force it to use that algorithm anywhere.

Here is the class I have so far:

using System;
using System.Collections.Generic;
using System.IO;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Net.Http.Headers;
using Newtonsoft.Json.Linq;
using Microsoft.IdentityModel.Tokens;
using System.Text;

namespace Site.Authorization
{
    public static class SiteAuthorizationExtensions
    {
        public static IServiceCollection AddSiteAuthorization(this IServiceCollection services)
        {
            var signingKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes("SECRET_KEY"));

            var tokenValidationParameters = new TokenValidationParameters
            {
                // The signing key must match!
                ValidateIssuerSigningKey = true,
                ValidateAudience = false,
                ValidateIssuer = false,
                IssuerSigningKeys = new List<SecurityKey>{ signingKey },


                // Validate the token expiry
                ValidateLifetime = true,
            };

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


            })

            .AddJwtBearer(o =>
            {
                o.IncludeErrorDetails = true;
                o.TokenValidationParameters  = tokenValidationParameters;
                o.Events = new JwtBearerEvents()
                {
                    OnAuthenticationFailed = c =>
                    {
                        c.NoResult();

                        c.Response.StatusCode = 401;
                        c.Response.ContentType = "text/plain";

                        return c.Response.WriteAsync(c.Exception.ToString());
                    }

                };
            });

            return services;
        }
    }
}
thehennyy
  • 3,507
  • 1
  • 19
  • 27
Michael Draper
  • 1,778
  • 3
  • 16
  • 21

6 Answers6

89

Here is a full working minimal sample with a controller. I hope you can check it using Postman or JavaScript call.

  1. appsettings.json, appsettings.Development.json. Add a section. Note, Key should be rather long and Issuer is an address of the service:

    ...
    ,"Tokens": {
        "Key": "Rather_very_long_key",
        "Issuer": "http://localhost:56268/"
    }
    ...
    

    !!! In real project, don't keep Key in appsettings.json file. It should be kept in Environment variable and take it like this:

    Environment.GetEnvironmentVariable("JWT_KEY");
    

UPDATE: Seeing how .net core settings work, you don't need to take it exactly from Environment. You may use setting. However,instead we may write this variable to environment variables in production, then our code will prefer environment variables instead of configuration.

  1. AuthRequest.cs : Dto keeping values for passing login and password:

    public class AuthRequest
    {
        public string UserName { get; set; }
        public string Password { get; set; }
    }
    
  2. Startup.cs in Configure() method BEFORE app.UseMvc() :

    app.UseAuthentication();
    
  3. Startup.cs in ConfigureServices() :

    services.AddAuthentication()
        .AddJwtBearer(cfg =>
        {
            cfg.RequireHttpsMetadata = false;
            cfg.SaveToken = true;
    
            cfg.TokenValidationParameters = new TokenValidationParameters()
            {
                ValidIssuer = Configuration["Tokens:Issuer"],
                ValidAudience = Configuration["Tokens:Issuer"],
                IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["Tokens:Key"]))
            };
    
        });
    
  4. Add a controller:

        [Route("api/[controller]")]
        public class TokenController : Controller
        {
            private readonly IConfiguration _config;
            private readonly IUserManager _userManager;
    
            public TokenController(IConfiguration configuration, IUserManager userManager)
            {
                _config = configuration;
                _userManager = userManager;
            }
    
            [HttpPost("")]
            [AllowAnonymous]
            public IActionResult Login([FromBody] AuthRequest authUserRequest)
            {
                var user = _userManager.FindByEmail(model.UserName);
    
                if (user != null)
                {
                    var checkPwd = _signInManager.CheckPasswordSignIn(user, model.authUserRequest);
                    if (checkPwd)
                    {
                        var claims = new[]
                        {
                            new Claim(JwtRegisteredClaimNames.Sub, user.UserName),
                            new Claim(JwtRegisteredClaimNames.Jti, user.Id.ToString()),
                        };
    
                        var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_config["Tokens:Key"]));
                        var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
    
                        var token = new JwtSecurityToken(_config["Tokens:Issuer"],
                        _config["Tokens:Issuer"],
                        claims,
                        expires: DateTime.Now.AddMinutes(30),
                        signingCredentials: creds);
    
                        return Ok(new { token = new JwtSecurityTokenHandler().WriteToken(token) });
                    }
                }
    
                return BadRequest("Could not create token");
            }}
    

That's all folks! Cheers!

UPDATE: People ask how get Current User. Todo:

  1. In Startup.cs in ConfigureServices() add

    services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
    
  2. In a controller add to constructor:

    private readonly int _currentUser;
    public MyController(IHttpContextAccessor httpContextAccessor)
    {
       _currentUser = httpContextAccessor.CurrentUser();
    }
    
  3. Add somewhere an extension and use it in your Controller (using ....)

    public static class IHttpContextAccessorExtension
    {
        public static int CurrentUser(this IHttpContextAccessor httpContextAccessor)
        {
            var stringId = httpContextAccessor?.HttpContext?.User?.FindFirst(JwtRegisteredClaimNames.Jti)?.Value;
            int.TryParse(stringId ?? "0", out int userId);
    
            return userId;
        }
    }
    
alerya
  • 2,856
  • 3
  • 24
  • 44
  • 1
    This was very helpful for me. The only thing I'm still unclear on is how to check the token for subsequent calls, or how to determine who the currently logged in user is. – Travesty3 Oct 03 '17 at 14:39
  • It's really easy. In the controller's method call: var currentUser = HttpContext.User.Identity.Name; Cheers! – alerya Oct 03 '17 at 18:48
  • 1
    Dude this was such a huge help, thank you so much for the detailed answer! – Ryanman Nov 30 '17 at 19:20
  • 1
    Thanks for the following information. It saved me from 401 Unauthorized error. 3.Startup.cs in Configure() method BEFORE app.UseMvc() : app.UseAuthentication(); – Sumia Sep 17 '18 at 16:29
  • 1
    This is a contradiction, we shouldn't store the key in the appsettings file yet we're retrieving it here Configuration["Tokens:Key"]). If we have a different service that retrieves the key securely, how would we incorporate that in ConfigureServices? – War Gravy Apr 23 '19 at 16:21
  • @alerya I did more research and was missing an important piece. You can store secrets or credentials on a machine as environment variables or in a secrets directory and the IConfiguration service is smart enough to pull in secrets and environment variables so that you can access them via config["MyKeyHere"]. There's a microsoft blog here: https://blogs.msdn.microsoft.com/mihansen/2017/09/10/managing-secrets-in-net-core-2-0-apps/ – War Gravy Apr 25 '19 at 18:21
  • Thanks a lot the "Startup.cs in Configure() method BEFORE app.UseMvc()" saved my day or rather I wasted a day but solved it thanks to this post. – Michael Aug 15 '19 at 06:46
18

My tokenValidationParameters works when they look like this:

 var tokenValidationParameters = new TokenValidationParameters
  {
      ValidateIssuerSigningKey = true,
      IssuerSigningKey = GetSignInKey(),
      ValidateIssuer = true,
      ValidIssuer = GetIssuer(),
      ValidateAudience = true,
      ValidAudience = GetAudience(),
      ValidateLifetime = true,
      ClockSkew = TimeSpan.Zero
   };

and

    static private SymmetricSecurityKey GetSignInKey()
    {
        const string secretKey = "very_long_very_secret_secret";
        var signingKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secretKey));

        return signingKey;
    }

    static private string GetIssuer()
    {
        return "issuer";
    }

    static private string GetAudience()
    {
        return "audience";
    }

Moreover, add options.RequireHttpsMetadata = false like this:

         .AddJwtBearer(options =>
       {         
           options.TokenValidationParameters =tokenValidationParameters         
           options.RequireHttpsMetadata = false;
       });

EDIT:

Dont forget to call

 app.UseAuthentication();

in Startup.cs -> Configure method before app.UseMvc();

Nouman Bhatti
  • 1,119
  • 2
  • 25
  • 44
Adrian Księżarczyk
  • 763
  • 1
  • 10
  • 17
  • I bet that it's the app.UseAuthentication(); call that will do the trick, I didn't know that I needed that. Thank you! – Michael Draper Aug 16 '17 at 22:18
  • I think you need to specify ValidAudience, ValidIssuer and IssuerSigningKey too. It didn't work without it for me – Adrian Księżarczyk Aug 17 '17 at 09:17
  • Yes, that was exactly what it was. I needed to add app.UseAuthentication() and that's all it took. Thank you very much! – Michael Draper Aug 18 '17 at 16:06
  • 3
    +1 for `app.UseAuthentication();` note being called before `app.UseMvc();` if you don't you will get 401's even when the token is authorised successfully - I spent about 2 days working that one out! – pcdev Aug 23 '17 at 05:13
  • 1
    "app.UseAuthentication();", I spent a whole day to fix the 401 issue after I upgrade .net core from 1.0 to 2.0, but didn't find the solution until I see this post. Thanks Adrian. – Chan Dec 08 '17 at 03:45
8

Asp.net Core 2.0 JWT Bearer Token Authentication Implementation with Web Api Demo

Add Package "Microsoft.AspNetCore.Authentication.JwtBearer"

Startup.cs ConfigureServices()

services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
            .AddJwtBearer(cfg =>
            {
                cfg.RequireHttpsMetadata = false;
                cfg.SaveToken = true;

                cfg.TokenValidationParameters = new TokenValidationParameters()
                {
                    ValidIssuer = "me",
                    ValidAudience = "you",
                    IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("rlyaKithdrYVl6Z80ODU350md")) //Secret
                };

            });

Startup.cs Configure()

// ===== Use Authentication ======
        app.UseAuthentication();

User.cs // It is a model class just for example. It can be anything.

public class User
{
    public Int32 Id { get; set; }
    public string Username { get; set; }
    public string Country { get; set; }
    public string Password { get; set; }
}

UserContext.cs // It is just context class. It can be anything.

public class UserContext : DbContext
{
    public UserContext(DbContextOptions<UserContext> options) : base(options)
    {
        this.Database.EnsureCreated();
    }

    public DbSet<User> Users { get; set; }
}

AccountController.cs

[Route("[controller]")]
public class AccountController : Controller
{

    private readonly UserContext _context;

    public AccountController(UserContext context)
    {
        _context = context;
    }

    [AllowAnonymous]
    [Route("api/token")]
    [HttpPost]
    public async Task<IActionResult> Token([FromBody]User user)
    {
        if (!ModelState.IsValid) return BadRequest("Token failed to generate");
        var userIdentified = _context.Users.FirstOrDefault(u => u.Username == user.Username);
            if (userIdentified == null)
            {
                return Unauthorized();
            }
            user = userIdentified;

        //Add Claims
        var claims = new[]
        {
            new Claim(JwtRegisteredClaimNames.UniqueName, "data"),
            new Claim(JwtRegisteredClaimNames.Sub, "data"),
            new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
        };

        var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("rlyaKithdrYVl6Z80ODU350md")); //Secret
        var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);

        var token = new JwtSecurityToken("me",
            "you",
            claims,
            expires: DateTime.Now.AddMinutes(30),
            signingCredentials: creds);

        return Ok(new
        {
            access_token = new JwtSecurityTokenHandler().WriteToken(token),
            expires_in = DateTime.Now.AddMinutes(30),
            token_type = "bearer"
        });
    }
}

UserController.cs

[Authorize]
[Route("api/[controller]")]
public class UserController : ControllerBase
{
    private readonly UserContext _context;

    public UserController(UserContext context)
    {
        _context = context;
        if(_context.Users.Count() == 0 )
        {
            _context.Users.Add(new User { Id = 0, Username = "Abdul Hameed Abdul Sattar", Country = "Indian", Password = "123456" });
            _context.SaveChanges();
        }
    }

    [HttpGet("[action]")]
    public IEnumerable<User> GetList()
    {
        return _context.Users.ToList();
    }

    [HttpGet("[action]/{id}", Name = "GetUser")]
    public IActionResult GetById(long id)
    {
        var user = _context.Users.FirstOrDefault(u => u.Id == id);
        if(user == null)
        {
            return NotFound();
        }
        return new ObjectResult(user);
    }


    [HttpPost("[action]")]
    public IActionResult Create([FromBody] User user)
    {
        if(user == null)
        {
            return BadRequest();
        }

        _context.Users.Add(user);
        _context.SaveChanges();

        return CreatedAtRoute("GetUser", new { id = user.Id }, user);

    }

    [HttpPut("[action]/{id}")]
    public IActionResult Update(long id, [FromBody] User user)
    {
        if (user == null)
        {
            return BadRequest();
        }

        var userIdentified = _context.Users.FirstOrDefault(u => u.Id == id);
        if (userIdentified == null)
        {
            return NotFound();
        }

        userIdentified.Country = user.Country;
        userIdentified.Username = user.Username;

        _context.Users.Update(userIdentified);
        _context.SaveChanges();
        return new NoContentResult();
    }


    [HttpDelete("[action]/{id}")]
    public IActionResult Delete(long id)
    {
        var user = _context.Users.FirstOrDefault(u => u.Id == id);
        if (user == null)
        {
            return NotFound();
        }

        _context.Users.Remove(user);
        _context.SaveChanges();

        return new NoContentResult();
    }
}

Test on PostMan: You will receive token in response.

Pass TokenType and AccessToken in Header in other webservices. enter image description here

Best of Luck! I am just Beginner. I only spent one week to start learning asp.net core.

Abdul Hameed
  • 825
  • 10
  • 15
  • I get InvalidOperationException: Unable to resolve service for type 'WebApplication8.UserContext' while attempting to activate 'AccountController'. when I try the postman call to Post to account/api/token – Kirsten Greed May 27 '18 at 02:29
7

Here is a solution for you.

In your startup.cs, firstly, config it as services:

  services.AddAuthentication().AddJwtBearer(cfg =>
        {
            cfg.RequireHttpsMetadata = false;
            cfg.SaveToken = true;
            cfg.TokenValidationParameters = new TokenValidationParameters()
            {
                IssuerSigningKey = "somethong",
                ValidAudience = "something",
                :
            };
        });

second, call this services in config

          app.UseAuthentication();

now you can use it in your controller by add attribute

          [Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
          [HttpGet]
          public IActionResult GetUserInfo()
          {

For full details source code that use angular as Frond-end see here

Long Field
  • 809
  • 9
  • 16
  • This was the answer that saved my bacon! It would be nice to just be able to use [Authorize]. Imagine this can be handled withing Startup.cs – Simon Aug 22 '17 at 09:10
  • 1
    Simon, because, you can have more then one scheme in the same asp.net core mvc application, like services.AddAuthentication().AddCookie().AddJwtBearer(); – Long Field Aug 22 '17 at 12:59
  • 1
    you can also set a default Authentication Scheme by the `services.AddAuthorization` function in start up. – Neville Nazerane Sep 03 '17 at 22:33
  • To follow up on @NevilleNazerane 's statement, the code to set a default Authentication Scheme (that will be used with a plain [Authorize] Decorator) the code is in answer for this question. It's services.AddAuthentication(sharedOptions => { sharedOptions.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; sharedOptions.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; }) – Ryanman Nov 30 '17 at 19:22
  • If I try to follow this example for the IssuerSigningKey I get an error Cannot convert sourcetype 'string' to target type 'Microsoft.IdentityModel.Tokens.SecurityKey' – Kirsten Greed May 27 '18 at 02:33
4

Here is my implementation for a .Net Core 2.0 API:

    public IConfigurationRoot Configuration { get; }

    public void ConfigureServices(IServiceCollection services)
    {
        // Add framework services
        services.AddMvc(
        config =>
        {
            // This enables the AuthorizeFilter on all endpoints
            var policy = new AuthorizationPolicyBuilder()
                                .RequireAuthenticatedUser()
                                .Build();
            config.Filters.Add(new AuthorizeFilter(policy));
            
        }
        ).AddJsonOptions(opt =>
        {
            opt.SerializerSettings.NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore;
        });

        services.AddLogging();

        services.AddAuthentication(sharedOptions =>
        {
            sharedOptions.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
            sharedOptions.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
        })
        .AddJwtBearer(options =>
        {
            options.Audience = Configuration["AzureAD:Audience"];  
            options.Authority = Configuration["AzureAD:AADInstance"] + Configuration["AzureAD:TenantId"];
        });            
    }

    public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
    {
        app.UseAuthentication(); // THIS METHOD MUST COME BEFORE UseMvc...() !!
        app.UseMvcWithDefaultRoute();            
    }

appsettings.json:

{
  "AzureAD": {
    "AADInstance": "https://login.microsoftonline.com/",
    "Audience": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
    "ClientId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
    "Domain": "mydomain.com",
    "TenantId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
  },
  ...
}

The above code enables auth on all controllers. To allow anonymous access you can decorate an entire controller:

[Route("api/[controller]")]
[AllowAnonymous]
public class AnonymousController : Controller
{
    ...
}

or just decorate a method to allow a single endpoint:

    [AllowAnonymous]
    [HttpPost("anonymousmethod")]
    public async Task<IActionResult> MyAnonymousMethod()
    {
        ...
    }

Notes:

  • This is my first attempt at AD auth - if anything is wrong, please let me know!

  • Audience must match the Resource ID requested by the client. In our case our client (an Angular web app) was registered separately in Azure AD, and it used its Client Id, which we registered as the Audience in the API

  • ClientId is called Application ID in the Azure Portal (why??), the Application ID of the app registration for the API.

  • TenantId is called Directory ID in the Azure Portal (why??), found under Azure Active Directory > Properties

  • If deploying the API as an Azure hosted Web App, ensure you set the Application Settings:

    eg. AzureAD:Audience / xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx

Community
  • 1
  • 1
pcdev
  • 2,416
  • 2
  • 17
  • 37
3

Just to update on the excellent answer by @alerya I had to modify the helper class to look like this;

public static class IHttpContextAccessorExtension
    {
        public static string CurrentUser(this IHttpContextAccessor httpContextAccessor)
        {           
            var userId = httpContextAccessor?.HttpContext?.User?.FindFirst(ClaimTypes.NameIdentifier)?.Value; 
            return userId;
        }
    }

Then I could obtain the userId in my service layer. I know it's easy in the controller, but a challenge further down.

spankymac
  • 447
  • 1
  • 5
  • 13