Step-by-step guide
Step 1: * Install Necessary NuGet Packages
First, ensure that you have the following NuGet packages installed in your project:
dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer
dotnet add package System.IdentityModel.Tokens.Jwt
Step 2: * Add JWT configuration support to appsettings.json
for development purpose the following in your appsettings.json is fine, adjust the http addresses as required
"JWT": {
"Issuer": "http://localhost:5246",
"Audience": "http://localhost:5246",
"SigningKey": "sdgfijjh3466iu345g87g08c24g7204gr803g30587ghh35807fg39074fvg80493745gf082b507807g807fgf"
}
Security note:
In development its normal to hold these values in appsettings.json however the SigningKey is sensitive and is the key to your JWT encryption, thus in production is better placed somewhere restricted and secure eg
- Azure key vault (if hosting on Azure)
- AWS Secrets Manager or AWS parameter store (if hosting on AWS)
- Environment variables (as a fallback for secrets storage)
Using Environment variables as a more secure example:
"JWT": {
"Issuer": "http://localhost:5246",
"Audience": "http://localhost:5246",
"SigningKey": "YOUR_SIGNING_KEY_ENV_VAR"
}
Then, in your application configuration setup in .NET
, you can retrieve this from the environment like this:
var signingKey = Environment.GetEnvironmentVariable("YOUR_SIGNING_KEY_ENV_VAR");
Step 3: * Configure JWT Authentication in Program.cs
In your Program.cs
file, you need to configure the JWT authentication middleware.
...
builder.Services.AddControllers(); // only the code below is required
...
// JWT stuff starts here Configure JWT authentication
var jwtSettings = builder.Configuration.GetSection("JWT");
var key = jwtSettings.GetValue<string>("SigningKey");
var issuer = jwtSettings.GetValue<string>("Issuer");
var audience = jwtSettings.GetValue<string>("Audience");
builder.Services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = issuer,
ValidAudience = audience,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(key))
};
});
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));
//JWT TokenService
builder.Services.AddScoped<ITokenService, TokenService>();
var app = builder.Build();
// Configure the HTTP request pipeline including JWT authentication
app.UseAuthentication(); // further JWT support + Ensure users are authenticated before authorization checks
app.UseAuthorization(); //Support for [Authorize] attribute on APIs
Step 4: * Create the JWT ITokenService interface
In your services folder create ITokenService.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using API.Dtos;
namespace API.Services
{
public interface ITokenService
{
string CreateToken(AppUser user);
}
}
Step 5: * Create the JWT TokenService
Create the TokenService.cs in your services folder
using API.Dtos;
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
namespace API.Services
{
public class TokenService : ITokenService
{
private readonly IConfiguration _config;
private readonly SymmetricSecurityKey _key;
public TokenService(IConfiguration config)
{
_config = config;
_key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_config["JWT:SigningKey"]));
}
public string CreateToken(AppUser user)
{
var claims = new List<Claim>
{
new Claim(JwtRegisteredClaimNames.Email, user.Email),
new Claim(JwtRegisteredClaimNames.GivenName, user.UserName),
new Claim("client_id", user.OtherData) // Client ID
};
var creds = new SigningCredentials(_key, SecurityAlgorithms.HmacSha512Signature);
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(claims),
Expires = DateTime.Now.AddDays(7),
SigningCredentials = creds,
Issuer = _config["JWT:Issuer"],
Audience = _config["JWT:Audience"]
};
var tokenHandler = new JwtSecurityTokenHandler();
var token = tokenHandler.CreateToken(tokenDescriptor);
return tokenHandler.WriteToken(token);
}
}
}
Step 6: Create supporting AppUser, LoginDto and NewUser
We will need these classes or your own equivalents to support the login process.
In your models or DTOs folder create the AppUser.cs
using System.ComponentModel.DataAnnotations;
namespace API.Dtos
{
public class AppUser
{
[Required]
public string? UserName { get; set; }
[Required]
[EmailAddress]
public string? Email { get; set; }
[Required]
public string? Password { get; set; }
public string? OtherData { get; set; }
}
}
Step 7: Create supporting LoginDto
In your models or DTOs folder create the LoginDto.cs
using System.ComponentModel.DataAnnotations;
namespace API.Dtos
{
public class LoginDto
{
[Required]
public string Username { get; set; }
[Required]
public string Password { get; set; }
}
}
Step 8: Create supporting NewUserDto
In your models or DTOs folder create the NewUserDto.cs
namespace API.Dtos
{
public class NewUserDto
{
public string UserName { get; set; }
public string Email { get; set; }
public string Token { get; set; }
public string OtherData { get; set; }
}
}
NOTE: the field OtherData is used to indicate how additional data can be included in the claims list of the token.
Step 9: Create Login() method in your controller
In your controller folder create the AccountController.cs, or alternativley add the Login() method one of your existing controllers.
using API.Dtos;
using API.Services;
using Data.Models;
using Microsoft.AspNetCore.Mvc;
namespace API.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class AccountController : ControllerBase
{
private readonly ApplicationDbContext _context;
private readonly ITokenService _tokenService;
public AccountController(ApplicationDbContext context, ITokenService tokenService)
{
_context = context;
_tokenService = tokenService;
}
[HttpPost("login")]
public async Task<IActionResult> Login(LoginDto loginDto)
{
//if (!ModelState.IsValid)
// return BadRequest(ModelState);
//var user = await _userManager.Users.FirstOrDefaultAsync(x => x.UserName == loginDto.Username.ToLower());
//if (user == null) return Unauthorized("Invalid username!");
//var result = await _signinManager.CheckPasswordSignInAsync(user, loginDto.Password, false);
//if (!result.Succeeded) return Unauthorized("Username not found and/or password incorrect");
//query against db to verify the user and return a user object containing at least the required fields
//that we will need username, email, additional and any addtional custom fields info for the token
var user = new AppUser { Email = loginDto.Username, UserName = "Nick", Password = loginDto.Password, OtherData = "someotherdbvalue" };
return Ok(
new NewUserDto
{
UserName = user.UserName,
Email = user.Email,
Token = _tokenService.CreateToken(user),
OtherData = user.OtherData
}
);
}
}
}
NOTE: In the code above we use Dependency Injection to insert the DbContext and the JWT ITokenService via the constructor, we call _tokenService.CreateToken(user) to create our JWT token and then return it to the caller.
Step 10: Secure your API Endpoint(s) by using the [authorize] attribute
We can now secure our endpoints using the [authorize] attribute.
This example assumes we have a ContactController with the following GET endpoint
// GET: api/Contacts
[Authorize]
[HttpGet]
public async Task<ActionResult<IEnumerable<Contact>>> GetContacts()
{
return await _context.Contacts.ToListAsync();
}
NOTE: see the usage of the [Authorize] attribute, this now secures this endpoint and with a valid token the method is not available and the server will return a 401 Unauthorized response.
Example Client usage:
We will assume that this is a Web app calling a Web API
In program.cs we will need to add the HttpClient to the services collection to converse with the API, the HttpClient is now available for dependency injection.
...
builder.Services.AddHttpClient(); // Registers HttpClient
var app = builder.Build();
Step 1: Create the Login view
Under the Home controller (change as required) add the Login.cshtml page
@using Site.Models
@*
For more information on enabling MVC for empty projects, visit https://go.microsoft.com/fwlink/?LinkID=397860
*@
@model LoginDto
@{
ViewData["Title"] = "Login";
}
<form asp-action="Login" method="post" class="mt-5 w-30 mx-auto p-4 shadow rounded">
<h3 class="text-center mb-4">Login</h3>
<div class="mb-3">
<label asp-for="Username" class="form-label">Username</label>
<input asp-for="Username" type="text" class="form-control" placeholder="Enter your username" required />
<span asp-validation-for="Username" class="text-danger"></span>
</div>
<div class="mb-3">
<label asp-for="Password" class="form-label">Password</label>
<input asp-for="Password" class="form-control" placeholder="Enter your password" required />
<span asp-validation-for="Password" class="text-danger"></span>
</div>
<div class="d-grid gap-2">
<button type="submit" class="btn btn-primary btn-block">Login</button>
</div>
</form>
Step 2: Add a link to the Login page
In your Shared/_Layout.cshtml for example add the following link (change as required)
<li><a class="dropdown-item" asp-controller="Home" asp-action="Login">Login</a></li>
Step 3: Call Login()
if we are using an MVC controller this would require a Get (initiated by the link above) and a Post method initiated by the Login.cshtml (above) form POST
public class HomeController : Controller
{
private readonly ILogger<HomeController> _logger;
private readonly HttpClient _httpClient;
public HomeController(ILogger<HomeController> logger, HttpClient httpClient)
{
_logger = logger;
_httpClient = httpClient;
_httpClient.BaseAddress = new Uri("http://localhost:5111/api/"); // Replace with your API's base URL
}
....
....
[HttpGet]
public IActionResult Login()
{
return View(new LoginDto()); // Show the empty form
}
[HttpPost]
public async Task<IActionResult> Login(LoginDto loginDto)
{
if (!ModelState.IsValid)
{
return View(loginDto); // Return the form with validation errors
}
// Send the login DTO to the API using a POST request
var response = await _httpClient.PostAsJsonAsync("contacts/login", loginDto);
if (response.IsSuccessStatusCode)
{
// Read the response body as NewUserDto
var newUser = await response.Content.ReadFromJsonAsync<NewUserDto>();
// Example: store token in TempData or Session
TempData["Token"] = newUser.Token;
//You can verify the token at JWT.IO
string cookiename = "MyAPIAccess";
string tokenValue = newUser.Token;
Response.Cookies.Append(
cookiename,
tokenValue,
new CookieOptions
{
HttpOnly = true,
SameSite = SameSiteMode.Strict
});
// Redirect to Home page or another view after successful login
return RedirectToAction("Index", "Home");
}
// If login fails, display an error message and return the view
ModelState.AddModelError("", "Invalid login attempt. Please try again.");
return View(loginDto);
}
The controller constructor recieves the httpClient by dependency injection and sets up the base address for the API call using _httpClient.BaseAddress =(...)
The call to the API is executed using
var response = await _httpClient.PostAsJsonAsync("contacts/login", loginDto);
If the response has status code of IsSuccessStatusCode then we read from the response and convert the content into a NewUserDto using the link
// Read the response body as NewUserDto
var newUser = await response.Content.ReadFromJsonAsync<NewUserDto>();
We then store the token as a cookie, so that the token can be reused later either for further calls to the API or for use by other pages if they need any of the values held within
string cookiename = "MyAPIAccess"; //any name you want here
string tokenValue = newUser.Token;
Response.Cookies.Append(
cookiename,
tokenValue,
new CookieOptions
{
HttpOnly = true,
SameSite = SameSiteMode.Strict
});
Subsequant use of the JWT from the cookie
Heres an example of subsequantly using the cookie this time to call an API endpoint.
namespace Site.Controllers
{
public class ContactsController : Controller
{
private readonly ApplicationDbContext _context;
private readonly HttpClient _httpClient;
public ContactsController(ApplicationDbContext context, HttpClient httpClient)
{
_context = context;
_httpClient = httpClient;
_httpClient.BaseAddress = new Uri("http://localhost:5111/api/"); // Replace with your API's base URL
}
// GET: Contacts
public async Task<IActionResult> Index()
{
/**********************************************************/
//Add JWT Cookie to header - support code
// Retrieve the JWT from the cookie
var jwtToken = HttpContext.Request.Cookies["MyAPIAccess"]; // Replace with the actual cookie name
if (string.IsNullOrEmpty(jwtToken))
{
// Handle the case where the token doesn't exist
return View("Error"); // Optionally, redirect to login page or show error
}
// Add the JWT token to the Authorization header
_httpClient.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", jwtToken);
/**********************************************************/
var response = await _httpClient.GetAsync("contacts");
if (response.IsSuccessStatusCode)
{
var jsonData = await response.Content.ReadAsStringAsync();
var contacts = JsonConvert.DeserializeObject<List<Contact>>(jsonData);
return View(contacts);
}
return View("Error"); // Error handling
//return View(await _context.Contacts.ToListAsync());
}
Explanation:
This example calls the API Contacts (GET) endpoint. See below how the httpClient is passed in to the constructor via dependency injection and then the _httpClient.BaseAddress is configured.
public ContactsController(ApplicationDbContext context, HttpClient httpClient)
{
_context = context;
_httpClient = httpClient;
_httpClient.BaseAddress = new Uri("http://localhost:5111/api/"); // Replace with your API's base URL
}
The default endpoint is Index (our GET) endpoint and here we retreive the JWT token from the cookie and put it into the GET header
/**********************************************************/
//Add JWT Cookie to header - support code
// Retrieve the JWT from the cookie
var jwtToken = HttpContext.Request.Cookies["MyAPIAccess"]; // Replace with the actual cookie name
if (string.IsNullOrEmpty(jwtToken))
{
// Handle the case where the token doesn't exist
return View("Error"); // Optionally, redirect to login page or show error
}
// Add the JWT token to the Authorization header
_httpClient.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", jwtToken);
/**********************************************************/
Finally we call the API, and then on status success we navigate to the contact view
var response = await _httpClient.GetAsync("contacts");
if (response.IsSuccessStatusCode)
{
var jsonData = await response.Content.ReadAsStringAsync();
var contacts = JsonConvert.DeserializeObject<List<Contact>>(jsonData);
return View(contacts);
}
return View("Error"); // Error handling