loading...
IT Minds

Getting started with JWT authorization - .NET Core edition

bmi profile image Benjamin Mikkelsen ・5 min read

This blog post will teach you how to issue JSON Web Tokens (JWT) from a .NET Core 3.1 Web API – the guide should also be somewhat applicable to .NET Core 2.2.

JWTs makes it possible to securely transmit data between parties – such as a client and a server. A JWT consists of three things:

  1. A header that usually defines the signing algorithm and the token type,
  2. A payload containing claims such as expiration time and custom data such as user UID and user type,
  3. A signature consisting of Base 64 Url encoded header and Base 64 Url encoded payload and a secret key.

A JWT will look something like this:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

Which basically translates to the following:

base64urlEncoding(header) + '.' + base64urlEncoding(payload) + '.' + base64urlEncoding(HMAC-SHA256(secret,base64urlEncoding(header) + '.' +base64urlEncoding(payload)))

This JWT is taken from https://jwt.io/, where you can decode, verify and generate tokens.

For this guide I assume you have already created a basic .NET Core Web API. If you haven't, then use dotnet new webapi in an empty folder.

The code used in the example can be found on GitHub

Installing required NuGet Packages

For this demo you have to add the following NuGet packages to your solution:

  • Microsoft.AspNetCore.Authentication.JwtBearer
  • Microsoft.EntityFrameworkCore.SqlServer

Basic setup of models and Authorize Attribute

I’ll be using EF Core to save information to a database for this demonstration. The relevant information regarding the User model can be seen here:

    public class User
    {
        [Key]
        public Guid Id { get; set; }
        public string UserName { get; set; }
        public string Password { get; set; }
        public UserType Type { get; set; }
    }

    public enum UserType
    {
        Regular = 1,
        Admin = 2,
        SuperUser = 3
    }

In this instance a user can have one of three types. These types will be used to restrict some endpoints - and therefore implement authorization.

To implement these roles in your controllers, you must create a custom AuthorizeAttribute as such:

    public class AuthorizeRolesAttribute : AuthorizeAttribute
    {
        public AuthorizeRolesAttribute(params UserType[] roles) : base()
        {
            Roles = string.Join(",", roles);
        }
    }

Doing this allows you to restrict endpoint to specific user types, simply by using a AuthorizeRoles-tag such as [AuthorizeRoles(UserType.Regular)]

Using EF Core

To use EF Core add the following to appsettings.development.json.

  "ConnectionStrings": {
    "DefaultConnection": "Server=(localdb)\\MSSQLLocalDB;Integrated Security=true;Database=JwtExample"
  }

This defines which database to connect to. Afterwards, implement a DbContext class as such:

    public class JwtExampleContext : DbContext
    {
        public DbSet<User> Users { get; set; }

        public JwtExampleContext(DbContextOptions options) : base(options)
        {
        }

        protected override void OnModelCreating(ModelBuilder builder)
        {
            builder.Entity<User>()
                .HasIndex(x => x.UserName)
                .IsUnique();

            var regularUser = new User { Id = Guid.Parse("54D39F1A-EF2D-4816-B2E8-90991F548BF0"), UserName = "Regular", Password = "RegularPassword", Type = UserType.Regular };
            var adminUser = new User { Id = Guid.Parse("36F72591-1D95-4E4F-877C-261D3386DF94"), UserName = "Admin", Password = "AdminPassword", Type = UserType.Admin };
            var superUser = new User { Id = Guid.Parse("FB270C9A-9479-4BD0-9C86-589F3FA84527"), UserName = "Super", Password = "SuperPassword", Type = UserType.SuperUser };
            builder.Entity<User>().HasData(regularUser, adminUser, superUser);
        }
    }

To connect to the database write the following lines in Startup.cs.

var connectionString = Configuration.GetConnectionString("DefaultConnection");
services.AddDbContext<JwtExampleContext>(options => options.UseSqlServer(connectionString));

Running dotnet ef migrations add Initial will create a migration file. To create and seed the database use dotnet ef database update which will run the migration script. Please note, passwords should never be stored as plain text in a database. This is only for demonstration purposes.

To see how the context is being used, look at UserRepository.cs

Token related setup

First, you must configure your IServiceCollection to add Authentication and JwtBearer. This can be done by adding the following lines to ConfigureServices in Startup.cs file in your .NET Core project:

            var key = Configuration.GetValue<string>("jwt-signing-key");
            services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(options =>
            {
                options.TokenValidationParameters = new TokenValidationParameters
                {
                    ValidateIssuer = true,
                    ValidateAudience = true,
                    ValidateLifetime = true,
                    ValidateIssuerSigningKey = true,
                    ValidIssuer = "http://localhost:5000",
                    ValidAudience = "http://localhost:5000",
                    IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(key))
                };
            });

These settings might not necessarily fit your needs and should possibly be adjusted. The key comes from appsettings.development.json. This key is used to sign the token and should always be kept secret.

You also must also call UseAuthentication()and UseAuthorization ()in Configure. These methods should be called after UseRouting() but before UseEndpoints, which should look similar to this:

        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.UseHttpsRedirection();
            app.UseRouting();

            app.UseAuthentication();
            app.UseAuthorization();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllers();
            });
        }

Issuing the tokens

I’ve made a LoginController with a single POST endpoint (api/login). This endpoint takes an UserDTO containing username and password. The LoginController calls an AuthenticationService which is responsible for generating tokens. The methods in the AuthenticationService can be seen below:

        public async Task<string> Login(UserDTO user)
        {
            var dbUser = await _repository.GetUser(user.UserName, user.Password);
            var jwt = GenerateJwt(dbUser);
            return jwt;
        }

        private string GenerateJwt(User user)
        {
            var key = _configuration.GetValue<string>("jwt-signing-key");
            var secretKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(key));
            var signinCredentials = new SigningCredentials(secretKey, SecurityAlgorithms.HmacSha256);
            var tokenOptions = new JwtSecurityToken(
                issuer: "http://localhost:5000",
                audience: "http://localhost:5000",
                claims: new List<Claim>
                {
                    new Claim(ClaimTypes.NameIdentifier,user.Id.ToString()),
                    new Claim(ClaimTypes.Role,user.Type.ToString()),
                },
                expires: DateTime.Now.AddHours(24),
                signingCredentials: signinCredentials
            );

            return new JwtSecurityTokenHandler().WriteToken(tokenOptions);
        }

It’s very important that the key, issuer and audience matches the options in Startup.cs. Otherwise your backend will return forbidden for all requests requiring authorization, since it’s validating the signature, issuer and audience.

Calling the endpoint will return something similar to this:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1laWRlbnRpZmllciI6IjM2ZjcyNTkxLTFkOTUtNGU0Zi04NzdjLTI2MWQzMzg2ZGY5NCIsImh0dHA6Ly9zY2hlbWFzLm1pY3Jvc29mdC5jb20vd3MvMjAwOC8wNi9pZGVudGl0eS9jbGFpbXMvcm9sZSI6IkFkbWluIiwiZXhwIjoxNTk4MTk2NzY2LCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjUwMDAiLCJhdWQiOiJodHRwOi8vbG9jYWxob3N0OjUwMDAifQ.x1vksHo5O-Fd8IXCUvEXfxcjToJXc7V7jMS7jqp0h5A

And that’s it! You now know how to issue tokens. However, you might ask: how do I use them?

Using the token

It’s actually fairly simple! First you make a POST call to api/login with your login credentials. This’ll return a token as seen in the previous section. This token needs to be set as a HTTP Header as ‘Authorization: Bearer token’. This header should be present for all calls to the API.
Afterwards, you simply create a new endpoint in a controller. I’ve created a new controller named DataController for this purpose. This controller has four endpoints:

  1. GET api/data - Doesn’t require authorization
  2. GET api/data/user - Available to all user types
  3. GET api/data/admin - Available to admins and superusers
  4. GET api/data/superuser - Available to superusers

All endpoints will return a small greeting and the usertype of the user. The code snippet below shows the implementation for api/data/admin.

        [AuthorizeRoles(UserType.Admin, UserType.SuperUser)]
        [HttpGet]
        [Route("admin")]
        public IActionResult GetAdminData()
        {
            try
            {
                var role = User.Claims.First(x => x.Type == ClaimTypes.Role).Value;
                var text = $"Hello from DataController. Only users above admin can access this endpoint. Your role is {role}.";
                return Ok(text);
            }
            catch (Exception)
            {
                return BadRequest();
            }
        }

Calling an endpoint that requires authorization without a valid token will result in HTTP 401 - also known as unauthorized. Calling an endpoint that doesn't match your authorization level will result in HTTP 403 - also known as forbidden.

And that's it! You know now how to implement authorization with JWTs.

But what know?

If you’re interested in the full code for this example, feel free to check out the repository here.

You might need to restore NuGet packages before you can build the project.

If you’re further interested in how JWT works, I highly recommend visiting https://jwt.io/.

Thanks for reading this blog post. I hoped you enjoyed it – feel free to ask any questions.

IT Minds

IT Minds is a development house in more than one sense. We believe in the efficient and intelligent development of IT solutions through the professional and personal development of our IT consultants' talent

Discussion

pic
Editor guide