Introduction
This article is a step-by-step guide to implementing JWT-based Authentication in ASP.NET Core API.
The goal of this article is to first start by learning how JSON Web Tokens (or JWTs) work in detail, including how they can be used for User Authentication, how to refresh tokens, and how to get user details using JWT tokens.
Way to implement JWT
- The client sends a login request with a username and password to the server.
- The server receives the username and password, and authenticates the user.
- If authentication is successful, then the server creates a JWT token called accessToken that stores the user's public info and sends it back to the client.
- The client receives the accessToken, from now on, the client sends any request to the server like getting the current user, the client just attaches the accessToken with the request.
- The server receives a request, authorizes the JWT token, continues processing the request, and then returns the result to the client.
What is JWT?(JSON Web Token)
JSON Web Token (JWT) is an open standard 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. The tokens are signed either using a private secret or a public/private key pair using RSA or ECDSA.
How to Implement JWT Authentication in an ASP.NET Core Web API project
Prerequisites
- Software
- Dot NET Core
- Visual Studio 2017 with last update or Visual Studio 2019
- SQL Server
- Skills
Step 1. Create Project.
Open Visual Studio Click on “Create a new project”.
Select the ASP.NET Core Web Application option.
Add the Project name and Solution name.
Select the “API” option with “.NET Core” and “ASP .NET Core 3.1” to create ASP.NET API
Use can see the default folder structure.
Step 2. Install Nuget Packages.
In this step, we need to install the following NuGet packages.
- Microsoft.EntityFrameworkCore.SqlServer
- Microsoft.EntityFrameworkCore.Tools
- Microsoft.IdentityModel.Tokens
- System.IdentityModel.Tokens.Jwt
- Microsoft.AspNetCore.Authentication.JwtBearer
Now, we'll proceed to install the above package from Nuget, and right-click on TokenDemo.Web project.
Change to browse tab and type Microsoft.EntityFrameworkCore.SqlServer.
Next,
- Install Microsoft.EntityFrameworkCore.Tools package
- Install Microsoft.IdentityModel.Tokens package
- Install System.IdentityModel.Tokens.Jwt package
- Install Microsoft.AspNetCore.Authentication.JwtBearer package
Step 3. Create DataContext.
Here we will follow the database first approach. If you want to use the code first approach, you can find database models in the “DataContext” folder in the attached project.
Shown below is the relationship between the tables.
Now go to the Package Manager Console and fire the command given below with your database server name and database name.
PM> Scaffold-DbContext "Server=*SERVER_NAME*;Database=*DATABASE_NAME*;Trusted_Connection=True;" Microsoft.EntityFrameworkCore.SqlServer -ContextDir DataContext -Context DemoTokenContext -OutputDir DataContext -Force
- Replace *SERVER_NAME* with your database server name
- Replace *DATABASE_NAME* with your database name
Step 4. Create Models for the controller.
Now, create a directory with the name Models and add the following files.
- ResponseModel.cs
- LoginModel.cs
- AuthenticationResult.cs
- ServiceConfiguration.cs
- ResponseModel.cs will contain definitions for the response model.
- LoginModel.cs will contain definitions for the Login Model.
- AuthenticationResult.cs will contain definitions for Authentication and TokenModel Model.
- ServiceConfiguration.cs will contain definitions for Authentication and TokenModel Model.
Code for AuthenticationResult.cs file
using Newtonsoft.Json;
using System.Collections.Generic;
namespace TokenDemo.Web.Models
{
public class TokenModel
{
[JsonProperty("token")]
public string Token { get; set; }
[JsonProperty("refreshToken")]
public string RefreshToken { get; set; }
}
public class AuthenticationResult : TokenModel
{
public bool Success { get; set; }
public IEnumerable<string> Errors { get; set; }
}
}
Code for ResponseModel.cs file
using Newtonsoft.Json;
namespace TokenDemo.Web.Models
{
public class ResponseModel<T>
{
public ResponseModel()
{
IsSuccess = true;
Message = "";
}
[JsonProperty("isSuccess")]
public bool IsSuccess { get; set; }
[JsonProperty("message")]
public string Message { get; set; }
[JsonProperty("data")]
public T Data { get; set; }
}
}
Code for LoginModel.cs file
using Newtonsoft.Json;
namespace TokenDemo.Web.Models
{
public class LoginModel
{
[JsonProperty("userName")]
public string UserName { get; set; }
[JsonProperty("password")]
public string Password { get; set; }
}
}
Code for ServiceConfiguration.cs file
using System;
namespace TokenDemo.Web.Models
{
public class ServiceConfiguration
{
public JwtSettings JwtSettings { get; set; }
}
public class JwtSettings
{
public string Secret { get; set; }
public TimeSpan TokenLifetime { get; set; }
}
}
Step 5. Update appsettings.Development.json.
Code for appsettings.Development.json file
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"ConnectionString": {
"DefaultConnection": "Data Source=*SERVER_NAME*;Initial Catalog=*DATABASE_NAME*;Persist Security Info=True;User ID=*DATABASE_USERNAME*;Password=*DATABASE_PASSWORD*"
},
"ServiceConfiguration": {
"JwtSettings": {
"Secret": "*SECRET*",
"TokenLifetime": "00:00:45"
}
}
}
- Replace *SERVER_NAME* with your database server name
- Replace *DATABASE_NAME* with your database name
- Replace *DATABASE_ USERNAME * with your database username
- Replace *DATABASE_PASSWORD* with your database password
- Replace *SECRET* with any string like "DWEYGZH2K4M5N7Q8R9TBUCVEXFYGZJ3K4M6P7Q8SATBUDWEXFZH2J3M5N6"
Step 6. Create Service.
Now, create a directory with the name Services and add the following files.
Code for IdentityService.cs file
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
using System;
using System.Collections.Generic;
using System.IdentityModel.Tokens.Jwt;
using System.Linq;
using System.Security.Claims;
using System.Text;
using System.Threading.Tasks;
using TokenDemo.Web.DataContext;
using TokenDemo.Web.Helpers;
using TokenDemo.Web.Models;
using JwtRegisteredClaimNames = System.IdentityModel.Tokens.Jwt.JwtRegisteredClaimNames;
namespace TokenDemo.Web.Services
{
public interface IIdentityService
{
Task<ResponseModel<TokenModel>> LoginAsync(LoginModel login);
}
public class IdentityService : IIdentityService
{
private readonly DemoTokenContext _context;
private readonly ServiceConfiguration _appSettings;
private readonly TokenValidationParameters _tokenValidationParameters;
public IdentityService(DemoTokenContext context,
IOptions<ServiceConfiguration> settings,
TokenValidationParameters tokenValidationParameters)
{
_context = context;
_appSettings = settings.Value;
_tokenValidationParameters = tokenValidationParameters;
}
public async Task<ResponseModel<TokenModel>> LoginAsync(LoginModel login)
{
ResponseModel<TokenModel> response = new ResponseModel<TokenModel>();
try
{
string md5Password = MD5Helpers.GenerateMd5Hash(login.Password);
UsersMaster loginUser = _context.UsersMaster.FirstOrDefault(c => c.UserName == login.UserName && c.Password == md5Password);
if (loginUser == null)
{
response.IsSuccess = false;
response.Message = "Invalid Username And Password";
return response;
}
AuthenticationResult authenticationResult = await AuthenticateAsync(loginUser);
if (authenticationResult != null && authenticationResult.Success)
{
response.Data = new TokenModel() { Token = authenticationResult.Token, RefreshToken = authenticationResult.RefreshToken };
}
else
{
response.Message = "Something went wrong!";
response.IsSuccess = false;
}
return response;
}
catch (Exception ex)
{
throw ex;
}
}
private List<RolesMaster> GetUserRole(long UserId)
{
try
{
List<RolesMaster> rolesMasters = (from UM in _context.UsersMaster
join UR in _context.UserRoles on UM.UserId equals UR.UserId
join RM in _context.RolesMaster on UR.RoleId equals RM.RoleId
where UM.UserId == UserId
select RM).ToList();
return rolesMasters;
}
catch (Exception)
{
return new List<RolesMaster>();
}
}
public async Task<AuthenticationResult> AuthenticateAsync(UsersMaster user)
{
// authentication successful so generate jwt token
AuthenticationResult authenticationResult = new AuthenticationResult();
var tokenHandler = new JwtSecurityTokenHandler();
try
{
var key = Encoding.ASCII.GetBytes(_appSettings.JwtSettings.Secret);
ClaimsIdentity Subject = new ClaimsIdentity(new Claim[]
{
new Claim("UserId", user.UserId.ToString()),
new Claim("FirstName", user.FirstName),
new Claim("LastName",user.LastName),
new Claim("EmailId",user.Email==null?"":user.Email),
new Claim("UserName",user.UserName==null?"":user.UserName),
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
});
foreach (var item in GetUserRole(user.UserId))
{
Subject.AddClaim(new Claim(ClaimTypes.Role, item.RoleName));
}
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = Subject,
Expires = DateTime.UtcNow.Add(_appSettings.JwtSettings.TokenLifetime),
SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature)
};
var token = tokenHandler.CreateToken(tokenDescriptor);
authenticationResult.Token = tokenHandler.WriteToken(token);
var refreshToken = new RefreshToken
{
Token = Guid.NewGuid().ToString(),
JwtId = token.Id,
UserId = user.UserId,
CreationDate = DateTime.UtcNow,
ExpiryDate = DateTime.UtcNow.AddMonths(6)
};
await _context.RefreshToken.AddAsync(refreshToken);
await _context.SaveChangesAsync();
authenticationResult.RefreshToken = refreshToken.Token;
authenticationResult.Success = true;
return authenticationResult;
}
catch (Exception ex)
{
return null;
}
}
}
}
Step 7. Update Startup.cs.
Code for Startup.cs file
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.IdentityModel.Tokens;
using System.Text;
using TokenDemo.Web.DataContext;
using TokenDemo.Web.Models;
namespace TokenDemo.Web
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
//services.AddControllers();
services.AddMvc();
services.AddDbContext<DemoTokenContext>(opts => opts.UseSqlServer(Configuration["ConnectionString:DefaultConnection"]));
// configure strongly typed settings objects
var appSettingsSection = Configuration.GetSection("ServiceConfiguration");
services.Configure<ServiceConfiguration>(appSettingsSection);
services.AddTransient<Services.IIdentityService, Services.IdentityService>();
// configure jwt authentication
var serviceConfiguration = appSettingsSection.Get<ServiceConfiguration>();
var JwtSecretkey = Encoding.ASCII.GetBytes(serviceConfiguration.JwtSettings.Secret);
var tokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(JwtSecretkey),
ValidateIssuer = false,
ValidateAudience = false,
RequireExpirationTime = false,
ValidateLifetime = true
};
services.AddSingleton(tokenValidationParameters);
services.AddAuthentication(x =>
{
x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
x.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(x =>
{
x.RequireHttpsMetadata = false;
x.SaveToken = true;
x.TokenValidationParameters = tokenValidationParameters;
});
services.AddCors(options =>
{
options.AddPolicy("CorsPolicy", builder =>
builder.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader()
);
});
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseHttpsRedirection();
app.UseRouting();
app.UseCors("CorsPolicy");
app.UseAuthentication();
app.UseAuthorization();
app.UseStaticFiles();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
}
}
Step 8. Add Controller.
Now, add the AuthController.cs files in the Controller’s folder
Code for AuthController.cs file
using Microsoft.AspNetCore.Mvc;
using System.Threading.Tasks;
using TokenDemo.Web.Models;
using TokenDemo.Web.Services;
namespace TokenDemo.Web.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class AuthController : ControllerBase
{
private readonly IIdentityService _identityService;
public AuthController(IIdentityService identityService)
{
_identityService = identityService;
}
[Route("login")]
[HttpPost]
public async Task<IActionResult> LoginAsync([FromBody]LoginModel loginModel)
{
var result = await _identityService.LoginAsync(loginModel);
return Ok(result);
}
}
}
Step 9. Running Web API.
Now, press F5 to start debugging the Web API project, if everything is OK, we'll get the following output in the browser
Now, we will call the login API and get the Access Token and Refresh Token. For calling login API, we have to use Postman.
As per the below screenshot, we will call Login API and get the Access Token, and Refresh.
Now we decode our Access Token on the site “https://jwt.io/”. As per the below screenshot you can see the Decoded user information which we added for Encoded “AuthenticateAsync” in the IdentityService service.
Now we have to implement the Refresh Token functionality, the below diagram shows a way to implement the refresh token functionality.
Step 10. Update IdentityService.
Now, the following is the code for the refresh token, so append the below code in IdentityService.cs.
public async Task<ResponseModel<TokenModel>> RefreshTokenAsync(TokenModel request)
{
ResponseModel<TokenModel> response = new ResponseModel<TokenModel>();
try
{
var authResponse = await GetRefreshTokenAsync(request.Token, request.RefreshToken);
if (!authResponse.Success)
{
response.IsSuccess = false;
response.Message = string.Join(",", authResponse.Errors);
return response;
}
TokenModel refreshTokenModel = new TokenModel();
refreshTokenModel.Token = authResponse.Token;
refreshTokenModel.RefreshToken = authResponse.RefreshToken;
response.Data = refreshTokenModel;
return response;
}
catch (Exception ex)
{
response.IsSuccess = false;
response.Message = "Something went wrong!";
return response;
}
}
private async Task<AuthenticationResult> GetRefreshTokenAsync(string token, string refreshToken)
{
var validatedToken = GetPrincipalFromToken(token);
if (validatedToken == null)
{
return new AuthenticationResult { Errors = new[] { "Invalid Token" } };
}
var expiryDateUnix = long.Parse(validatedToken.Claims.Single(x => x.Type == JwtRegisteredClaimNames.Exp).Value);
var expiryDateTimeUtc = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc)
.AddSeconds(expiryDateUnix);
if (expiryDateTimeUtc > DateTime.UtcNow)
{
return new AuthenticationResult { Errors = new[] { "This token hasn't expired yet" } };
}
var jti = validatedToken.Claims.Single(x => x.Type == JwtRegisteredClaimNames.Jti).Value;
var storedRefreshToken = _context.RefreshToken.FirstOrDefault(x => x.Token == refreshToken);
if (storedRefreshToken == null)
{
return new AuthenticationResult { Errors = new[] { "This refresh token does not exist" } };
}
if (DateTime.UtcNow > storedRefreshToken.ExpiryDate)
{
return new AuthenticationResult { Errors = new[] { "This refresh token has expired" } };
}
if (storedRefreshToken.Used.HasValue && storedRefreshToken.Used == true)
{
return new AuthenticationResult { Errors = new[] { "This refresh token has been used" } };
}
if (storedRefreshToken.JwtId != jti)
{
return new AuthenticationResult { Errors = new[] { "This refresh token does not match this JWT" } };
}
storedRefreshToken.Used = true;
_context.RefreshToken.Update(storedRefreshToken);
await _context.SaveChangesAsync();
string strUserId = validatedToken.Claims.Single(x => x.Type == "UserId").Value;
long userId = 0;
long.TryParse(strUserId, out userId);
var user = _context.UsersMaster.FirstOrDefault(c => c.UserId == userId);
if (user == null)
{
return new AuthenticationResult { Errors = new[] { "User Not Found" } };
}
return await AuthenticateAsync(user);
}
private ClaimsPrincipal GetPrincipalFromToken(string token)
{
var tokenHandler = new JwtSecurityTokenHandler();
try
{
var tokenValidationParameters = _tokenValidationParameters.Clone();
tokenValidationParameters.ValidateLifetime = false;
var principal = tokenHandler.ValidateToken(token, tokenValidationParameters, out var validatedToken);
if (!IsJwtWithValidSecurityAlgorithm(validatedToken))
{
return null;
}
return principal;
}
catch
{
return null;
}
}
private bool IsJwtWithValidSecurityAlgorithm(SecurityToken validatedToken)
{
return (validatedToken is JwtSecurityToken jwtSecurityToken) &&
jwtSecurityToken.Header.Alg.Equals(SecurityAlgorithms.HmacSha256,
StringComparison.InvariantCultureIgnoreCase);
}
Update IIdentityService interface
public interface IIdentityService
{
Task<ResponseModel<TokenModel>> LoginAsync(LoginModel login);
Task<ResponseModel<TokenModel>> RefreshTokenAsync(TokenModel request);
}
Step 11. Update AuthController.
[Route("refresh")]
[HttpPost]
public async Task<IActionResult> Refresh([FromBody] TokenModel request)
{
var result = await _identityService.RefreshTokenAsync(request);
return Ok(result);
}
Step 12. Running Web API
Again we will call the login API endpoint and copy the Access Token and Refresh Token or if you have an old Access Token and Refresh copy which we get in the last call.
As per the below screenshot we called the Login API endpoint and copied Access Token and Refresh Token to call Refresh Token API endpoint.
As per the below screenshot, we called the Refresh Token API endpoint and regenerated the Access Token and Refresh Token.
Now we have user information using the Access Token.
Step 13. Add UserService.
Now, add the UserService.cs files in the Services folder.
Code for UserService.cs file
using System;
using System.Collections.Generic;
using System.Linq;
using TokenDemo.Web.DataContext;
using TokenDemo.Web.Models;
namespace TokenDemo.Web.Services
{
public interface IUserService
{
ResponseModel<UserModel> Get(long UserId);
}
public class UserService : IUserService
{
private readonly DemoTokenContext _context;
public UserService(DemoTokenContext context)
{
_context = context;
}
public ResponseModel<UserModel> Get(long UserId)
{
ResponseModel<UserModel> response = new ResponseModel<UserModel>();
try
{
UserModel user = (from UM in _context.UsersMaster
where UM.UserId == UserId
select new UserModel
{
UserId = UM.UserId,
FirstName = UM.FirstName,
LastName = UM.LastName,
Email = UM.Email,
PhoneNumber = UM.PhoneNumber,
UserName = UM.UserName
}).FirstOrDefault();
List<string> roleNames = (from UM in _context.UsersMaster
join UR in _context.UserRoles on UM.UserId equals UR.UserId
join RM in _context.RolesMaster on UR.RoleId equals RM.RoleId
where UM.UserId == UserId
select RM.RoleName).ToList();
if (user != null)
{
user.UserRoles = roleNames;
response.Data = user;
return response;
}
else
{
response.IsSuccess = false;
response.Message = "User Not Found!";
return response;
}
}
catch (Exception ex)
{
throw ex;
}
}
}
}
Step 14. Add UsersController.
Now, add the UsersController.cs files in the Controllers folder.
Code for UsersController.cs file
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using System.Collections.Generic;
using System.Security.Claims;
using TokenDemo.Web.Services;
namespace TokenDemo.Web.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class UsersController : ControllerBase
{
private readonly IUserService _userService;
public UsersController(IUserService userService)
{
_userService = userService;
}
[Route("getCurrentUser")]
[HttpGet]
[Authorize]
public IActionResult GetCurrentUser()
{
long UserId = GetUserIdFromToken();
var result = _userService.Get(UserId);
return Ok(result);
}
protected long GetUserIdFromToken()
{
long UserId = 0;
try
{
if (HttpContext.User.Identity.IsAuthenticated)
{
var identity = HttpContext.User.Identity as ClaimsIdentity;
if (identity != null)
{
IEnumerable<Claim> claims = identity.Claims;
string strUserId = identity.FindFirst("UserId").Value;
long.TryParse(strUserId, out UserId);
}
}
return UserId;
}
catch
{
return UserId;
}
}
}
}
Step 15. Update Startup.cs.
Append the below code in Startup.cs file ConfigureServices method.
services.AddTransient<Services.IUserService, Services.UserService>();
Step 16. Run Application.
Run your application and get user information using Authorize.
In the below screenshot, we are getting user information using Access Token which we get in the call to login to the API endpoint.