Featured
Secure your minimal api .net 6 using JWT
Last modified: March 02, 2022In previous article , we learned how we build Minimal APIs in .NET 6 and this article, we will show how we can protect our APIs using JWT (JSON Web Token). Please follow and code that article first before following this article as we are going to use code from there.
Table of Content
Edit project.csproj to install two nuget packages.
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="6.0.2" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="6.16.0" />
1. appsettings.json
"Jwt": {
"Key": "ertwet3245sgf2342werwergww4352345",
"Issuer": "https://localhost:7085/",
"Audience": "https://localhost:7085/"
}
Model
Create a folder called ViewModels and create a class file name UserViewModel that hold username and password.
public class UserViewModel
{
public int Id { get; set; }
public string Username { get; set; }
public string Password { get; set; }
public string Roles { get; set; }
}
3. Interfaces
Create a folder called Interfaces and create a interface file name IUserService as shown below
public interface IUserService
{
(bool, string?) IsLoginValid(UserViewModel user);
}
In the same folder, create another interface file name ITokenService as shown below.
public interface ITokenService
{
string GenerateToken(UserViewModel user, string key, string issuer, string audience);
}
4. Services
Create a folder called Services and create a class file name UserService. The function IsLoginValid return more then one output aka Tuple.
public class UserService : IUserService
{
private List<UserViewModel> _users()
{
return new List<UserViewModel>
{
new UserViewModel{Username = "admin", Password="password" },
new UserViewModel {Username = "admin1", Password = "password1", Roles = "administrator"}
};
}
public (bool, string?) IsLoginValid(UserViewModel user)
{
var _user = _users().Where(w => w.Username.Equals(user.Username) && w.Password.Equals(user.Password));
return (_user.Any(), _user.Select(s=> s.Roles).FirstOrDefault()) ;
}
}
Create another class file called TokenService and saved it to Services folder. The token will valid for 30 minutes. We store userId and User role in the clam, hence pass it to the token which is generated in this method.
public class TokenService : ITokenService
{
public string GenerateToken(UserViewModel user, string secretKey, string issuer, string audience)
{
var handler = new JwtSecurityTokenHandler();
var key = Encoding.ASCII.GetBytes(secretKey);
var descriptor = new SecurityTokenDescriptor
{
Issuer = issuer,
Audience = audience,
Subject = new ClaimsIdentity(new[] { new Claim("id", user.Id.ToString()), new Claim(ClaimTypes.Role, user.Roles) }),
Expires = DateTime.UtcNow.AddMinutes(30),
SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature)
};
var token = handler.CreateToken(descriptor);
return handler.WriteToken(token);
}
}
5. Program.cs
Now we need to register our dependencies in WebApplicationBuilder Services
builder.Services.AddSingleton<TokenService>(new TokenService());
builder.Services.AddSingleton<IUserService>(new UserService());
We need to configure AddAuthentication service to same parameters as we use to generate JWT token in TokenService code. This is to check if token sent to API for valid.
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(opt=>
{
opt.TokenValidationParameters = new()
{
ValidIssuer = builder.Configuration["Jwt:Issuer"],
ValidAudience = builder.Configuration["Jwt:Audience"],
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"]))
};
});
In the login method, it take in login information and check it to IsLoginValid method(userService). If login valid (Item1 value) and it pass role to generate the token
app.MapPost("/login", [AllowAnonymous] async ([FromBodyAttribute]UserViewModel userModel, IUserService userService, TokenService tokenService, HttpResponse response) =>
{
var user = userService.IsLoginValid(userModel);
if (user.Item1) //login match
{
userModel.Roles = user.Item2 ?? "guest";// map the users role, if no role then assign as guest
var token = tokenService.GenerateToken(userModel, builder.Configuration["Jwt:Key"], builder.Configuration["Jwt:Issuer"], builder.Configuration["Jwt:Audience"]);
await response.WriteAsJsonAsync(new { token = token });
}
return;
});
In this method, a user need to pass valid token that can authenticate
app.MapGet("/auth", [Authorize] () =>
{
return Results.Ok("This endpoint requires authorization");
});
OR use "RequireAuthorization" instead
app.MapGet("/auth", () => "This endpoint requires authorization")
.RequireAuthorization();
We create a policy called "AdminsOnly" that has administrator role
builder.Services.AddAuthorization(o => o.AddPolicy("AdminsOnly",
b => b.RequireRole("administrator", "true")));
In this method, a user need to pass valid token that can authenticate and meet AdminsOnly policy.
app.MapGet("/admin", [Authorize("AdminsOnly")] () =>
"The admin endpoint is for admins only.");
OR
app.MapGet("/admin2", () => "The admin endpoint is for admins only.")
.RequireAuthorization("AdminsOnly");
6. Test the application
- Login to generate token
- Pass the token to authenticate "auth" endpoint
- Login as admin and pass its token to authenticate "admin" endpoint