In this article, we are going to generate JWT Access Token to authenticate users against .NET6 Web API application.
Create A .NET6 API Project:
Sample SQL User Query:
Let's create a 'User' table class like 'User.cs' in the 'Data/Entities' folder.
Register the 'IAccountService', 'AccountService' into the 'Program.cs'.
In the next part of the article, we are going to implement the refresh token.
JWT Token(Or Access Token):
JWT Token(JSON Web Token) is a digitally signed and secured token for user validation. JWT Token building components are like:
- Header
- Payload
- Signature
JWT Access Token Flow:
- User request API with user credentials
- API validates the user credentials and generates the JWT token returns it as a response to the client application.
- The client application on receiving the JWT token makes the user authenticated and sends the JWT token as a header to every subsequent API request.
- API reads the JWT token from the request header, then API validates the token if it is a valid token then API allows the request to consume its authorized resources.
Create A .NET6 API Project:
Let's create a .Net6 Web API sample application to accomplish our demo. We can use either Visual Studio 2022 or Visual Studio Code(using .NET CLI commands) to create any.Net6 application. For this demo, I'm using the 'Visual Studio Code'(using the .NET CLI command) editor.
CLI command
dotnet new webapi -o Your_Project_Name
dotnet new webapi -o Your_Project_Name
Sample SQL User Query:
Run the below query to create a sample 'User' table.
CREATE TABLE [dbo].[User]( [Id] [int] IDENTITY(1,1) NOT NULL, [FirstName] [varchar](300) NULL, [LastName] [varchar](300) NULL, [Email] [varchar](300) NOT NULL, [Password] [varchar](300) NOT NULL, [PhoneNumber] [varchar](15) NULL, CONSTRAINT [PK_User] PRIMARY KEY CLUSTERED ( [Id] ASC )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] ) ON [PRIMARY]
Configure Database Context:
Install the Entity Framework Core NuGet Package
CLI command
dotnet add package Microsoft.EntityFrameworkCore --version 6.0.5
dotnet add package Microsoft.EntityFrameworkCore --version 6.0.5
Package Manager
Install-Package Microsoft.EntityFrameworkCore -Version 6.0.5
Install-Package Microsoft.EntityFrameworkCore -Version 6.0.5
Install the Entity Framework Core SQL Server NuGet Package
CLI command
dotnet add package Microsoft.EntityFrameworkCore.SqlServer --version 6.0.5
dotnet add package Microsoft.EntityFrameworkCore.SqlServer --version 6.0.5
Package Manager
Install-Package Microsoft.EntityFrameworkCore.SqlServer -Version 6.0.5
Install-Package Microsoft.EntityFrameworkCore.SqlServer -Version 6.0.5
Let's create a 'User' table class like 'User.cs' in the 'Data/Entities' folder.
Data/Entities/User.cs:
namespace Dot6.Jwt.API.Data.Entities; public class User { public int Id { get; set; } public string FirstName { get; set; } public string LastName { get; set; } public string Email { get; set; } public string Password { get; set; } public string PhoneNumber { get; set; } }Let's add the database context class like 'MyAuthContext.cs' in the 'Data' folder.
Data/MyAuthContext.cs:
using Dot6.Jwt.API.Data.Entities; using Microsoft.EntityFrameworkCore; namespace Dot6.Jwt.API.Data; public class MyAuthContext : DbContext { public MyAuthContext(DbContextOptions<MyAuthContext> context) : base(context) { } public DbSet<User> User { get; set; } }
- Here register all our table classes into the database context class.
Program.cs:
builder.Services.AddDbContext<MyAuthContext>(options => { options.UseSqlServer(builder.Configuration.GetConnectionString("MyAuthConnection")); });
Install Identity Model JWT NuGet:
Let's install the 'System.IdentityModel.Token.Jwt' NuGet package.
CLI command
dotnet add package System.IdentityModel.Tokens.Jwt --version 6.18.0
dotnet add package System.IdentityModel.Tokens.Jwt --version 6.18.0
Package Manager
Install-Package System.IdentityModel.Tokens.Jwt -Version 6.18.0
Install-Package System.IdentityModel.Tokens.Jwt -Version 6.18.0
Configure Token Settings:
Let's configure the required token settings into the 'appsettings.Development.json' file.
appsettings.Development.json:
"TokenSettings":{ "SecretKey":"mysecretkeymysecretkeymysecretkey", "Issuer":"localhost:7225", "Audience":"API" }
- The 'Secretkey' is the mandatory setting for token generation, it is used as an ingredient in generating the digital signature. You can generate a random key by using any online tools, make sure it can't be guessed easily.
- The 'Issuer' represent the API that generates the JWT token. It is recommended to use the domain name of the API.
- The 'Audience' will be the client application. In general, you can give the name of your angular or react or js application. Here for our demo we won't have any client application so we simple give 'API' as value
Settings/TokenSettings.cs:
namespace Dot6.Jwt.API.Settings; public class TokenSettings { public string SecretKey { get; set; } public string Issuer { get; set; } public string Audience { get; set; } }Now register the 'TokenSetings' entity in the 'Program.cs' to load settings from the JSON file.
Program.cs:
builder.Services .Configure<TokenSettings>(builder.Configuration.GetSection("TokenSettings"));
Create AccountService:
To implement authentication logic let's create service files like 'AccountService.cs', 'IAccountService.cs' in the 'Services' folder.
Let's create the 'IAccountService.cs'.
Services/IAccountService.cs:
namespace Dot6.Jwt.API.Services; public interface IAccountService { }Let's create the 'AccountService.cs'.
Services/AccountService.cs:
using Dot6.Jwt.API.Data; using Dot6.Jwt.API.Settings; namespace Dot6.Jwt.API.Services; public class AccountService : IAccountService { private readonly MyAuthContext _myAuthContext; private readonly TokenSettings _tokenSettings; public AccountService(MyAuthContext myAuthContext, IOptions<TokenSettings> tokenSettings) { _myAuthContext = myAuthContext; _tokenSettings = tokenSettings.Value; } }
- Here injected the 'MyAuthContext' and 'TokenSettings'.
Services/AccountService.cs:
using Dot6.Jwt.API.Data.Entities; using Microsoft.IdentityModel.Tokens; using System.IdentityModel.Tokens.Jwt; using System.Security.Claims; private string CreateJwtToken(User user) { var symmetricSecurityKey = new SymmetricSecurityKey( Encoding.UTF8.GetBytes(_tokenSettings.SecretKey) ); var credentials = new SigningCredentials( symmetricSecurityKey, SecurityAlgorithms.HmacSha256 ); var userCliams = new Claim[]{ new Claim("email", user.Email), new Claim("phone", user.PhoneNumber), }; var jwtToken = new JwtSecurityToken( issuer: _tokenSettings.Issuer, expires: DateTime.Now.AddMinutes(20), signingCredentials: credentials, claims: userCliams, audience: _tokenSettings.Audience ); string token = new JwtSecurityTokenHandler().WriteToken(jwtToken); return token; }
- (Line: 6) Here for the method 'CreateJwtToken()' we will pass the authenticated user information.
- (Line: 8-10) Convert our secret key value as an array of byte data and then pass it to the instance of the 'Microsoft.IdentityModel.Tokens.SymmetricSecurityKey'.
- (Line: 11-14) Generate the 'Microsoft.IdentityModel.Tokens.SignInCredentials' using the 'symmetricsecuritykey' and 'Hmacsha256' algorithm. SignInCredentials helps to generate the digital signature.
- (Line: 16-19) Collection of claims prepared to use it in JWT token.
- (Line: 21-26) Initialize the instance of 'JwtSecurityToken' that takes input parameter like 'issuer', 'expires'(expiration time of jwt token), 'signingCredentials', 'claims', 'audience'.
- (Line: 28) Finally, generate the JWT token.
Dtos/LoginDto.cs:
namespace Dot6.Jwt.API.Dtos; public class LoginDto { public string Email { get; set; } public string Password { get; set; } }
- Here our payload entity contains properties like 'Email' and 'Password' for authentication.
Dtos/TokenDto.cs:
namespace Dot6.Jwt.API.Dtos; public class TokenDto { public string AccessToken { get; set; } }Now let's define a public method definition in 'IAccountService' as below.
Services/IAccountService.cs:
Task<TokenDto> GetAuthTokens(LoginDto login);Now implement the 'GetAuthTokens' method in 'AccountService'.
Services/AccountService.cs:
using Dot6.Jwt.API.Data.Entities; using Dot6.Jwt.API.Dtos; public async Task<TokenDto> GetAuthTokens(LoginDto login) { //Note: demo purpose saved plain password to the database // checking the plain password // In real time application please make sure to save the hash password into the database // need to hash the password while comparing also User user = await _myAuthContext.User .Where(_ => _.Email.ToLower() == login.Email.ToLower() && _.Password == login.Password).FirstOrDefaultAsync(); if (user != null) { var token = new TokenDto { AccessToken = CreateJwtToken(user) }; return token; } return null; }
- (Line: 4) The 'LoginDto' passed as input to our 'GetAuthTokens' method.
- (Line: 10-12) Based on user login payload tries to fetch user from the database.
- (Line: 14-21) If the user is a valid user then generate the token and return it.
- (Line: 22) If the user doesn't exist in a database which means an invalid user trying to login then simply return the 'null' response.
Note: For demo purposes, I saved the raw password directly in the database. But in a real-time application make sure to hash the password before saving it into the database.
Program.cs:
builder.Services.AddScoped<IAccountService,AccountService>();
Create A Login Endpoint:
Let's create a new controller like 'AccountController' and add an endpoint for getting the JWT authentication token.
Controllers/AccountController.cs:
using Dot6.Jwt.API.Dtos; using Dot6.Jwt.API.Services; using Microsoft.AspNetCore.Mvc; namespace Dot6.Jwt.API.Controllers; [ApiController] [Route("[controller]")] public class AccountController : ControllerBase { private readonly IAccountService _accountService; public AccountController(IAccountService accountService) { _accountService = accountService; } [Route("login-token")] [HttpPost] public async Task<IActionResult> GetLoginToken(LoginDto login) { var result = await _accountService.GetAuthTokens(login); if (result == null) { return ValidationProblem("invalid credentials"); } return Ok(result); } }
- (Line: 11-15) Injected the 'IAccountService' into the constructor.
- (Line: 17-27) Endpoint to fetch the authentication JWT token.
- (Line: 21) Trying to fetch JWT token based on user credentials.
- (Line: 22-25) If the user payload is invalid then we return a response as a bad request with a 400 status code using the 'ValidationProblem()' method.
- (Line: 26) If the user payload is valid then return the JWT token as a success response.
step 3:
Install JwtBearer NuGet:
Let's install the 'Microsoft.AspNetCore.Authentication.JwtBearer' NuGet
CLI command
dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer --version 6.0.5
dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer --version 6.0.5
Package Manager
Install-Package Microsoft.AspNetCore.Authentication.JwtBearer -Version 6.0.5
Install-Package Microsoft.AspNetCore.Authentication.JwtBearer -Version 6.0.5
Register JwtBearer Authentication Service:
Let's register the JwtBearer authentication service into the 'Program.cs'
Program.cs:
using Dot6.Jwt.API.Settings; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.IdentityModel.Tokens; builder .Services .AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options => { var tokenSettings = builder.Configuration .GetSection("TokenSettings").Get<TokenSettings>(); options.TokenValidationParameters = new TokenValidationParameters { ValidIssuer = tokenSettings.Issuer, ValidateIssuer = true, ValidateIssuerSigningKey = true, IssuerSigningKey = new SymmetricSecurityKey( Encoding.UTF8.GetBytes(tokenSettings.SecretKey) ), ClockSkew = TimeSpan.Zero, ValidateAudience = true, ValidAudience = tokenSettings.Audience }; });
- (Line: 7) Defined the authentication type by assigning the name of the authentication type like 'JwtBearerDefaults.AuthenticationScheme'.
- (Line: 8-24) Defined the JwtBearer service with the required configuration to validate the JWT token.
- (Line: 10&11) Loading the token settings from the JSON file.
- (Line: 12) Initialized the 'TokenValidationParameter' instance.
- (Line: 14&15) The 'ValidIssuer' assign the value from the 'TokenSettings' and enable issuer validation by setting 'true' for 'ValidateIssuer'. So these configuration checks the issuer value inside of the JWT token matches with the value at 'ValidIssuer'.
- (Line: 16&17) The 'IssuerSingInkey' should have the same value as we did in 'AccountServie' while generating the JWT token. So to check the signature of the JWT token from an incoming request we need to assign a 'true' value for 'ValidateIssuerSingingKey'.
- (Line: 20) The 'ClockSkew' making '0' seconds considers token expiration exactly, the default value of 'CokcSkew' is '300s' seconds which means the token will live for extra 5minutes original expiration time.
- (Line: 21&22) The 'ValidAudience' value will be checked against the JWT token from the request if enable the 'ValidateAudience'.
Add Authentication Middleware:
Let's register the authentication middleware just above the authorization middleware in 'Program.cs'.
Program.cs:
app.UseAuthentication(); app.UseAuthorization();
Create A Sample Secured Endpoint To Test JWT Token:
To test our JWT token let's create a simply secured endpoint which means an action method decorated with the 'Authorize' attribute which means the endpoint is only consumed by the authenticated users.
Controllers/AccountController.cs:
[HttpGet] [Route("test-auth")] [Authorize] public IActionResult GetTest() { return Ok("Only authenticated user can consume this endpoint"); }Step 1: Now let's try to consume this new endpoint without any token. Then you can see status codes like 401(UnAuthorized)Step 2: Now let's try to consume this new endpoint by sending JWT token as one of the headers to the request. In general, to add the Jwt token as header value we must prefix it with the 'Bearer' keyword and there must be space(' ') between the 'Bearer' keyword and the jwt token.
Support Me!
Buy Me A Coffee
PayPal Me
Video Session:
Wrapping Up:
Hopefully, I think this article delivered some useful information on generating JWT tokens in .NET6 Web API. I love to have your feedback, suggestions, and better techniques in the comment section below.
I'm using asp.net core webapi in the backend in its own solution file and Blazor Server Side in another solution as the frontend.
ReplyDeleteIf I implement your refresh JWT part in my backend, will I have to modify anything in the frontend in order to make it work.