Introduction
When you are designing your API, the very first thing you think will be how to secure your API.
When you think about security, there are two terms that will come into your mind Authentication and Authorization.
Authentication is a process to identify if the user is a valid member of the system.
Authorization means the member has the right to perform the following action.
JWT
JWT and API security go hand in hand. JWT means Json web token. It is used as an authentication or authorization token for your API.
It is like a Gate pass, which allows API to open a gate for the user or block him outside.
We can use the JWT token with our custom authentication and authorization mechanism, or we can use an identity that provides a built-in function to use for general use case.
We will try to secure our application through .Net Identity.
ASP .Net Identity
First, create the new Web API Project.
Add the following NuGet Package.
- Microsoft.AspNetCore.Identity.EntityFrameworkCore
- Microsoft.Extensions.Identity.Core
- Microsoft.Extensions.Identity.Stores
We will be extending the Identity User with some custom property.
public class User : IdentityUser
{
public string UserType { get; set; }
}
Now we need to create the Identities in our Database and create an Identity Context
We will be renaming the Identity table also.
public class FOAContext : IdentityDbContext<IdentityUser>
{
public FOAContext(DbContextOptions<FOAContext> options) : base(options)
{
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
// Override default AspNet Identity table names
modelBuilder.Entity<IdentityUser>(entity => { entity.ToTable(name: "Users"); });
modelBuilder.Entity<IdentityRole>(entity => { entity.ToTable(name: "Roles"); });
modelBuilder.Entity<IdentityUserRole<string>>(entity => { entity.ToTable("UserRoles"); });
modelBuilder.Entity<IdentityUserClaim<string>>(entity => { entity.ToTable("UserClaims"); });
modelBuilder.Entity<IdentityUserLogin<string>>(entity => { entity.ToTable("UserLogins"); });
modelBuilder.Entity<IdentityUserToken<string>>(entity => { entity.ToTable("UserTokens"); });
modelBuilder.Entity<IdentityRoleClaim<string>>(entity => { entity.ToTable("RoleClaims"); });
}
public DbSet<User> Users { get; set; }
}
Now Add the connection string in the appSetting.
{
"ConnectionStrings": {
"AppDb": "Data Source=databasename;Initial Catalog=FOADB;User Id=username; Password=password;Integrated Security=True;TrustServerCertificate=True"
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}
Now we need to define the identity of the program.cs.
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddControllers();
// Database connection
var connectionString = builder.Configuration.GetConnectionString("AppDb");
builder.Services.AddDbContext<FOAContext>(x => x.UseSqlServer(connectionString));
// For Identity
builder.Services.AddIdentity<User, IdentityRole>()
.AddEntityFrameworkStores<FOAContext>()
.AddDefaultTokenProviders();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.Run();
Now run the migration by following the command.
“add-migration Initial”
Now update the database
“update-database”
We now need to implement registration and login flow.
Let's create a Registration flow first. Create a Register API.
I am using CQRS architecture, but you can design your API as you want.
[HttpPost("Register")]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult> Register([FromBody] RegisterCommandRequest command)
{
try
{
var result = await _mediator.Send(command);
return Ok(result);
}
catch (Exception ex)
{
return BadRequest(ex.Message);
}
}
Now create a static class with User Roles. We will use this in the registration flow and also when creating a claim during login.
public static class UserRoles
{
public const string Admin = "Admin";
public const string User = "User";
}
Now we will create the user through Identity User Manager if it does not exist already and will assign the role to it.
private readonly UserManager<IdentityUser> _userManager;
public RegisterCommandHandler(UserManager<IdentityUser> userManager
)
{
_userManager = userManager;
}
public async Task<RegisterCommandResponse> Handle(RegisterCommandRequest request, CancellationToken cancellationToken)
{
var userExists = await _userManager.FindByNameAsync(request.Username);
if (userExists != null)
{
throw new ApplicationException("User already exist");
}
IdentityUser user = new()
{
Email = request.Email,
SecurityStamp = Guid.NewGuid().ToString(),
UserName = request.Username
};
var result = await _userManager.CreateAsync(user, request.Password);
if (!result.Succeeded)
{
throw new ApplicationException("User creation failed");
}
if (request.Userrole == UserRoles.Admin)
{
await _userManager.AddToRoleAsync(user, UserRoles.Admin);
}
else
{
await _userManager.AddToRoleAsync(user, UserRoles.User);
}
var registerResponse = FOAMapper.Mapper.Map<RegisterCommandResponse>(user);
registerResponse.UserRole = request.Userrole;
return registerResponse;
}
Now that our registration flow is complete, we need to create a login API for the login flow.
Login API will have one extra step, which will be to create a claim after the user is authenticated.
[HttpPost("Login")]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult> Login([FromBody] LoginCommandRequest command)
{
try
{
var result = await _mediator.Send(command);
var token = GetToken(result.authClaims);
return Ok(new
{
token = new JwtSecurityTokenHandler().WriteToken(token),
expiration = token.ValidTo
});
}
catch (Exception ex)
{
return Unauthorized();
}
}
private JwtSecurityToken GetToken(List<Claim> authClaims)
{
var authSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration["JWT:Secret"]));
var token = new JwtSecurityToken(
issuer: _configuration["JWT:ValidIssuer"],
audience: _configuration["JWT:ValidAudience"],
expires: DateTime.Now.AddHours(3),
claims: authClaims,
signingCredentials: new SigningCredentials(authSigningKey, SecurityAlgorithms.HmacSha256)
);
return token;
}
Now we will use Identity User Manager and Role Manager to authenticate the user if he is a valid user of the system and will create it claim according to his role.
private readonly UserManager<IdentityUser> _userManager;
private readonly RoleManager<IdentityRole> _roleManager;
public LoginCommandHandler(UserManager<IdentityUser> userManager,
RoleManager<IdentityRole> roleManager)
{
_userManager = userManager;
_roleManager = roleManager;
}
public async Task<LoginCommandResponse> Handle(LoginCommandRequest request, CancellationToken cancellationToken)
{
var user = await _userManager.FindByEmailAsync(request.Email);
if (user != null && await _userManager.CheckPasswordAsync(user, request.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));
}
return new LoginCommandResponse() {authClaims = authClaims};
}
throw new Exception("User Email or Password is Incorrect");
}
Now after the successful login of the user, we are returning the Bearer token, which will be used to authenticate and authorize the user if he has access to the following api.
Now create a test API to check if the authentication and Authorization are working.
[Authorize(Roles = UserRoles.Admin)]
[HttpGet("GetUserList")]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult> Get()
{
var result = await _mediator.Send(new GetUsersRequest());
return Ok(result);
}
The authorization tag will secure the application.
Now we need to add authentication and authorization configuration in our program.cs file, and we are good to go.
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 = configuration["JWT:ValidAudience"],
ValidIssuer = configuration["JWT:ValidIssuer"],
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(configuration["JWT:Secret"]))
};
});