Authentication and Authorization in ASP.NET 6.0 API With JWT using Identity Framework

Views: 497
Comments: 3
Like/Unlike: 3
Posted On: 04-Apr-2023 04:52 

Share:   fb twitter linkedin
Rahul M...
Teacher
256 Points
22 Posts


Authentication is the process of identifying a user or client making a request to our application. Authorization is the process of determining whether a user or client has access to a specific resource or functionality within our application. In this article we will see how to authenticate and authorize ASP.NET API endpoint by using JWT token. And we will use ASP.NET Identity framework to store user credentials in an SQL server database, and we will use Entity framework and Identity framework for database operations. Before we will start let's see few terms:

ASP.NET Core Identity

ASP.NET Core Identity is a membership system that provides authentication and authorization functionality out-of-the-box. It supports a variety of authentication methods such as cookies, OAuth, OpenID Connect, and more. It also provides a user management system with features like password hashing, two-factor authentication, and account lockout.

JWT (JSON Web Tokens) Authentication

JSON Web Tokens (JWT) is a popular authentication mechanism that uses JSON-based tokens to authenticate users. It can be used in combination with ASP.NET Core Identity or with a custom authentication scheme. JSON Web Token (JWT) is an open standard (RFC 7519) that defines a compact and self-contained way for securely transmitting information between parties as a JSON object. This information can be verified and trusted because it is digitally signed. JWTs can be signed using a secret (with the HMAC algorithm) or a public/private key pair using RSA or ECDSA. 

In its compact form, JSON Web Tokens consist of three parts separated by dots (.), which are: 

  • Header 
  • Payload 
  • Signature 

Therefore, a JWT typically looks like the following. 

xxxx.yyyy.zzzz 

 

Create an ASP.NET Core Web API using Visual Studio 2022 

We require Visual Studio 2022 to create .NET 6.0 applications. We can choose ASP.NET Core Web API template from Visual Studio 2022:

We may give a suitable name for our project and choose the .NET 6.0 framework as it has long term support.

Packages to Install

Install the following libraries below into the new project. We can use NuGet package manger to install these packages:

  1. Microsoft.EntityFrameworkCore.SqlServer 
  2. Microsoft.EntityFrameworkCore.Tools 
  3. Microsoft.AspNetCore.Identity.EntityFrameworkCore 
  4. Microsoft.AspNetCore.Authentication.JwtBearer 
  5. Swashbuckle.AspNetCore

 

Add DB connection string and JWT tocket setting in appsettings.json

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "ConnectionStrings": {
    "DefaultConnection": "Data Source=ServerName;Initial Catalog=MyDataBase;Integrated Security=SSPI;TrustServerCertificate=True"
  },
  "JWTAppSettings": {
    "ValidAudience": "http://localhost:",
    "ValidIssuer": "http://localhost:",
    "Secret": "JWTAuthenticationSecuredPasswordVVVp1OH7Yzyr"
  }
}

Add Application DbContext

 Create ApiDbContext by extending IdentityDbContext:

using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;

namespace Noc.Api.Models
{
    public class ApiDbContext : IdentityDbContext<IdentityUser>
    {
        public ApiDbContext(DbContextOptions<ApiDbContext> options)
            : base(options)
        {
        }

        protected override void OnModelCreating(ModelBuilder builder)
        {
            base.OnModelCreating(builder);
        }
    }
}

Create enum say "UserRoles"

namespace Noc.Api.Models.Enum
{
    public enum UserRoles
    {
        Admin,
        User
    }
}

Create different classes for new user registration and login

  1. Registration models
    using System;
    using System.Collections.Generic;
    using System.ComponentModel.DataAnnotations;
    using System.Linq;
    using System.Text;
    using System.Threading.Tasks;

    namespace Noc.Api.Models
    {
        public class RegisterViewModel
        {
            [Required(ErrorMessage = "User Name is required")]
            public string? Username { get; set; }

            [EmailAddress]
            [Required(ErrorMessage = "Email is required")]
            public string? Email { get; set; }

            [Required(ErrorMessage = "Password is required")]
            public string? Password { get; set; }
        }
    }
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Threading.Tasks;

    namespace Noc.Api.Models
    {
        public class ResponseViewModel
        {
            public string? Status { get; set; }
            public string? Message { get; set; }
        }
    }
  2. Login Models
    using System;
    using System.Collections.Generic;
    using System.ComponentModel.DataAnnotations;
    using System.Linq;
    using System.Text;
    using System.Threading.Tasks;

    namespace Noc.Api.Models
    {
        public class LoginViewModel
        {
            [Required(ErrorMessage = "User Name is required")]
            public string? Username { get; set; }

            [Required(ErrorMessage = "Password is required")]
            public string? Password { get; set; }
        }
    }
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Threading.Tasks;

    namespace Noc.Api.Models
    {
        public class LoginResponseViewModel
        {
            public string Token { get; set; }
            public DateTime Expiration { get; set; }
        }
    }

Create Identity Service

It will contain different method for login and registration:

using Noc.Api.Models;

namespace Noc.Api.Service
{
    public interface IIdentityService
    {
        Task<LoginResponseViewModel> LoginAsync(LoginViewModel loginViewModel);
        Task<ResponseViewModel> RegisterAsync(RegisterViewModel registerViewModel);
        Task<ResponseViewModel> RegisterAdminAsync(RegisterViewModel registerViewModel);
    }
}
using Azure;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
using Noc.Api.Models;
using Noc.Api.Models.AppSettings;
using Noc.Api.Models.Enum;
using System.IdentityModel.Tokens.Jwt;
using System.Linq;
using System.Security.Claims;
using System.Text;
using System.Threading.Tasks;

namespace Noc.Api.Service
{
    public class IdentityService: IIdentityService
    {
        private readonly UserManager<IdentityUser> _userManager;
        private readonly RoleManager<IdentityRole> _roleManager;
        private readonly JWTAppSettings _jWTAppSettings;

        public IdentityService(UserManager<IdentityUser> userManager,
            RoleManager<IdentityRole> roleManager,
            IOptions<JWTAppSettings> jWTAppSettings)
        {
            _userManager = userManager;
            _roleManager = roleManager;
            _jWTAppSettings=jWTAppSettings.Value;
        }

        public async Task<LoginResponseViewModel> LoginAsync(LoginViewModel loginViewModel)
        {
            var user = await _userManager.FindByNameAsync(loginViewModel.Username);
            if (user != null && await _userManager.CheckPasswordAsync(user, loginViewModel.Password))
            {
                var userRoles = await _userManager.GetRolesAsync(user);

                var authClaims = new List<Claim>
                {
                    new Claim(ClaimTypes.Name, user.UserName),
                    new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
                };

                foreach (var userRole in userRoles)
                {
                    authClaims.Add(new Claim(ClaimTypes.Role, userRole));
                }

                var token = GetToken(authClaims);

                return new LoginResponseViewModel
                {
                    Token = new JwtSecurityTokenHandler().WriteToken(token),
                    Expiration = token.ValidTo
                };
            }
            return new LoginResponseViewModel { };
        }

        public async Task<ResponseViewModel> RegisterAsync(RegisterViewModel registerViewModel)
        {
            var userExists = await _userManager.FindByNameAsync(registerViewModel.Username);
            if (userExists != null)
                return new ResponseViewModel { Status = "Error", Message = "User already exists!" };

            IdentityUser user = new()
            {
                Email = registerViewModel.Email,
                SecurityStamp = Guid.NewGuid().ToString(),
                UserName = registerViewModel.Username
            };
            var result = await _userManager.CreateAsync(user, registerViewModel.Password);
            if (!result.Succeeded)
                return new ResponseViewModel { Status = "Error", Message = "User creation failed! Please check user details and try again." };

            return new ResponseViewModel { Status = "Success", Message = "User created successfully!" };
        }

        public async Task<ResponseViewModel> RegisterAdminAsync(RegisterViewModel registerViewModel)
        {
            var userExists = await _userManager.FindByNameAsync(registerViewModel.Username);
            if (userExists != null)
                return new ResponseViewModel { Status = "Error", Message = "User already exists!" };

            IdentityUser user = new()
            {
                Email = registerViewModel.Email,
                SecurityStamp = Guid.NewGuid().ToString(),
                UserName = registerViewModel.Username
            };
            var result = await _userManager.CreateAsync(user, registerViewModel.Password);
            if (!result.Succeeded)
                return new ResponseViewModel { Status = "Error", Message = "User creation failed! Please check user details and try again." };

            if (!await _roleManager.RoleExistsAsync(UserRoles.Admin.ToString()))
                await _roleManager.CreateAsync(new IdentityRole(UserRoles.Admin.ToString()));
            if (!await _roleManager.RoleExistsAsync(UserRoles.User.ToString()))
                await _roleManager.CreateAsync(new IdentityRole(UserRoles.User.ToString()));

            if (await _roleManager.RoleExistsAsync(UserRoles.Admin.ToString()))
            {
                await _userManager.AddToRoleAsync(user, UserRoles.Admin.ToString());
            }
            if (await _roleManager.RoleExistsAsync(UserRoles.Admin.ToString()))
            {
                await _userManager.AddToRoleAsync(user, UserRoles.User.ToString());
            }
            return new ResponseViewModel { Status = "Success", Message = "User created successfully!" };
        }

        private JwtSecurityToken GetToken(List<Claim> authClaims)
        {
            var authSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_jWTAppSettings.Secret));

            var token = new JwtSecurityToken(
                issuer: _jWTAppSettings.ValidIssuer,
                audience: _jWTAppSettings.ValidAudience,
                expires: DateTime.Now.AddHours(3),
                claims: authClaims,
                signingCredentials: new SigningCredentials(authSigningKey, SecurityAlgorithms.HmacSha256)
                );

            return token;
        }
    }
}

 

Program.cs

In .NET 6.0, Microsoft removed the Startup class and only kept Program class. We must define all our dependency injection and other configurations inside the Program class. 

using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.IdentityModel.Tokens;
using Noc.Api.Models;
using Noc.Api.Models.AppSettings;
using Noc.Api.Service;
using System.Text;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.

builder.Services.AddControllers();

builder.Services.AddDbContext<ApiDbContext>(options =>
{
    options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection"));
});

// For Identity
builder.Services.AddIdentity<IdentityUser, IdentityRole>()
    .AddEntityFrameworkStores<ApiDbContext>()
    .AddDefaultTokenProviders();

// Adding Authentication
builder.Services.AddAuthentication(options =>
{
    options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
    options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
})
// Adding Jwt Bearer
.AddJwtBearer(options =>
{
    options.SaveToken = true;
    options.RequireHttpsMetadata = false;
    options.TokenValidationParameters = new TokenValidationParameters()
    {
        ValidateIssuer = true,
        ValidateAudience = true,
        ValidAudience = builder.Configuration.GetSection("JWTAppSettings:ValidAudience").Value,
        ValidIssuer = builder.Configuration.GetSection("JWTAppSettings:ValidIssuer").Value,
        IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(builder.Configuration.GetSection("JWTAppSettings:Secret").Value))
    };
});

builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

builder.Services.AddScoped<IIdentityService, IdentityService>();
builder.Services.Configure<JWTAppSettings>(builder.Configuration.GetSection("JWTAppSettings"));

var app = builder.Build();

// Configure the HTTP request pipeline.


app.UseSwagger();
app.UseSwaggerUI();

app.UseHttpsRedirection();

// Authentication & Authorization
app.UseAuthentication();
app.UseAuthorization();

app.MapControllers();

app.Run();

 

Create Identity Controller to expose different endpoints like login, register, etc

using Microsoft.AspNetCore.Mvc;
using Noc.Api.Models;
using Noc.Api.Service;

namespace Noc.Api.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class IdentityController : ControllerBase
    {
        private readonly IIdentityService _identityService;

        public IdentityController(IIdentityService identityService)
        {
            _identityService = identityService;
        }

        [HttpPost("login")]
        public async Task<IActionResult> Login([FromBody] LoginViewModel model)
        {
            var result = await _identityService.LoginAsync(model);
            if (!string.IsNullOrEmpty(result?.Token))
            {
                return Ok(result);
            }
            return Unauthorized();
        }

        [HttpPost("register")]
        public async Task<IActionResult> Register([FromBody] RegisterViewModel model)
        {
            var result = await _identityService.RegisterAsync(model);
            return Ok(result);
        }

        [HttpPost("register-admin")]
        public async Task<IActionResult> RegisterAdmin([FromBody] RegisterViewModel model)
        {

            var result = await _identityService.RegisterAdminAsync(model);
            return Ok(result);
        }
    }
}

Add Db migration and create database

We need to create a database and tables before running the application. As we are using entity framework, we can use below database migration command with package manger console to create a migration script. 

Use the command below to create database and tables. 

Now, we can check database and we will see following tables:

Add Authorize attribute

Let's add Authorize attribute at controller

Now all set. Let's run the application. You will see following swagger page

Testing API endpoints in postman

Now we can run the application and try to access get method in weatherforecast controller from Postman tool and you will see 401 unauthorize response

Register an user and then login to get acess tocket to access above endpoint

Now let's try with this jwt token to authorize the weather endpoint. And now we will see data with 200 status code

Now, let's change the weatherforecast controller with role-based authorization. 

Rerun application and try to access the enpoint. Now we will see 403 forbidden status code

We have received a 403 forbidden error instead of 401 now. Even though we are passing a valid token but we don’t have sufficient privilege to access the controller. To access this controller, the user must have an admin role permission. Current user is a normal user and does not have any admin role permission. 

Let's create a new user with an admin role permission. We already have a method “register-admin” in authenticate controller for the same purpose. 

Get admin access token

Now, we can use this token instead of the old token to access the weatherforecast controller. 

Now we have successfully fetched the data from weatherforecast controller.  

Conclusion

In this article, we saw how to create a JSON token in .NET 6.0 ASP.NET Core Web API application and use this token for authentication and authorization by using ASP.NET Identity Framework. We have created two users, one without any role and one with admin role. We have applied authentication and authorization at controller level and saw the different behaviors with these two users. 

 

 

3 Comments

Great work!


Brian
17-Apr-2023 at 19:11

It's very helpful. Great work!


beginer
19-Apr-2023 at 04:44

Great man! Thanks.


dumytest
10-May-2023 at 02:34
 Log In to Chat