
REST APIs are stateless, so server does not store any information about the client. So we can not authorize the rest application in a traditional way. How does a server knows if the user is authenticated user or not? In this situation the Json Web Token (JWT) saves the day.
JSON Web Tokens are an open, industry standard RFC 7519 method for representing claims securely between two parties. [Source: jwt.io]
💻 Source code : https://github.com/rd003/NetRefreshTokenDemo
Let’s understand how it works?
First, user passes the valid credential to the server through the API(eg. Login api). Server generates the JWT and returns it to the client. JWT contains information like Name, Email, Role etc. about the User. To access the protected resource , user have to pass the JWT in the Authorization header.
Press enter or click to view image in full size

basic authentication with JWT
Refresh token
Usually, JWTs are short-lived (mostly 10 to 15 minutes). To mitigate security risks we make them short lived. If a token is stolen, attacker have less time to use it. However, this creates a problem — users have to authenticate themselves in every 10 to 15 minutes. It is bothersome to enter credentials again and again. In such situations, refresh tokens come in handy.
Press enter or click to view image in full size

authentication with refresh token
First, the user uses an authentication API (e.g., login) to authenticate itself and enters the valid credentials. The server validates the client and returns the JWT and refresh token. Refresh tokens are long-lived. The client has to store the JWT and refresh token. If the JWT expires, the client needs to use the refresh endpoint and pass the stored expired JWT and the refresh token. In return, the client gets the latest JWT and refresh token. Every time the JWT expires, the client has to use the refresh endpoint until the refresh token expires.
Tools and tech used
VS code (text editor) with c# dev kit extension
PostgreSql running in docker
.NET 9
Entity Framework Core
Identity
Creating an application using NET CLI
First and foremost create a new controllers API project in .net. Run these commands in a sequence.
# create new solution file
dotnet new sln -o NetRefreshTokenDemo
cd NetRefreshTokenDemo
# create new controllers api project
dotnet new webapi --use-controllers -o NetRefreshTokenDemo.Api
# registering NetRefreshTokenDemo.Api project to the solution
dotnet sln add .\NetRefreshTokenDemo.Api\
# open the current folder in the vs code
code .Nuget packages
Run these commands in a sequence to install the required nuget packages.
# An EF package for the postgresql
dotnet add package Npgsql.EntityFrameworkCore.PostgreSQL
# A package need to work with the ef migrations
dotnet add package Microsoft.EntityFrameworkCore.Design
# Packages for the identity
dotnet add package Microsoft.AspNetCore.Identity.EntityFrameworkCore
# A package to generate the jwt tokens
dotnet add package Microsoft.AspNetCore.Authentication.JwtBearerConnection String
Add the connection string in the appsettings.json .
"ConnectionStrings": {
"default": "Host=localhost; Port=5432; Database=NetRefreshTokenDemo;
Username=postgres;Password=your_strong_password"
}Models
Create a new folder named Models. Then create a new class namedApplicationUser in the Models folder.
// Models/ApplicationUser.cs
public class ApplicationUser : IdentityUser
{
public string Name { get; set; } = string.Empty;
}When we use the aspnetcore identity, lots of classes related to identity gets generated during the migration. AspNetUsers is One of them, which have lots of default columns. If you want to store additional information about the user, you need to create a class named ApplicationUser and inherit the class named IdentityUser. We are adding just one property(Name) to the class. If you want to store more information , you can add more properties. Now a new column named Name will be added to the AspNetUsers.
We need one more model to store the refresh token info.
// Models/TokenInfo.cs
public class TokenInfo
{
public int Id { get; set; }
[Required]
[MaxLength(30)]
public string Username { get; set; } = string.Empty;
[Required]
[MaxLength(200)]
public string RefreshToken { get; set; } = string.Empty;
[Required]
public DateTime ExpiredAt { get; set; }
}Create a new class namedAppDbContext within the Models folder.
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
namespace NetRefreshTokenDemo.Api.Models;
public class AppDbContext : IdentityDbContext<ApplicationUser>
{
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options)
{
}
public DbSet<TokenInfo> TokenInfos { get; set; }
}Now, we need to register the AppDbContext in the Program class.
string connectionString = builder.Configuration.GetConnectionString("default");
builder.Services.AddDbContext<AppDbContext>(options =>
options.UseNpgsql(connectionString));
// For Identity
builder.Services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<AppDbContext>()
.AddDefaultTokenProviders();
// Use these middlewares before app.MapControllers()
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers() // existing codeJwt Related configuration
Add following lines in the appsettings.json.
Add the following lines in appsettings.json.
"JWT": {
"ValidAudience": "https://localhost:7014",
"ValidIssuer": "https://localhost:7014"
}ValidAudience
Determines the valid recipient of the token. The value https://localhost:7014 determines that only an application running on https://localhost:7014 can consume the token. It determines the aud claim in the token. e.g.
{
"aud": "https://localhost:7014"
}ValidIssuer
Defines the issuer of the token. This is usually the URL or identifier of the authentication server (e.g., an identity provider like Azure AD, IdentityServer, or your custom auth service). It adds the iss claims in the token payload.
{
"iss": "https://localhost:7014",
}We also need to store secret-key. We are not going to store it in the appsettings. We will store it in the secret manager, which is a good choice for local development. But for the production you need to use something like azure key vault or its other cloud counter parts.
First of all we need to initialize the user-secrets in our project. Make sure you are in the projects directory.
cd NetRefreshTokenDemo.ApiNow initialize the user- secrets.
dotnet user-secrets initTo verify, open the NetRefreshTokenDemo.Api.csproj file.
Press enter or click to view image in full size

Follow the command below, to store the jwt-secret in user-secrets.
dotnet user-secrets set "JWT:secret" "your-32-characters-long-super-strong-jwt-secret-key"You can find the secrets in following location.
# in windows
%APPDATA%\Microsoft\UserSecrets\<user_secrets_id>\secrets.json
# in mac/linux
~/.microsoft/usersecrets/<user_secrets_id>/secrets.jsonNow add the following configuration in Program.cs.
// Authentication
builder.Services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
}
)
.AddJwtBearer(options =>
{
options.SaveToken = true;
options.RequireHttpsMetadata = false;
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidAudience = builder.Configuration["JWT:ValidAudience"],
ValidIssuer = builder.Configuration["JWT:ValidIssuer"],
ClockSkew = TimeSpan.Zero,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(builder.Configuration["JWT:secret"]))
};
}
);Migration Commands
Run the following migration commands in a sequence.
dotnet ef migrations add init
dotnet ef database updateAt this period to time, our database has been generated and look like this.

Default Identity tables with the table TokenInfos
Role Constants
Create a folder named Constants, within the folder create a class named Roles.
// Constants/Roles.cs
namespace NetRefreshTokenDemo.Api.Constants;
public class Roles
{
public const string Admin = "Admin";
public const string User = "User";
}Seeding admin data
Since there won’t be any API for creating an admin, we will create an admin when our application runs for the first time. Create a folder named Data. In this folder, create a new class named DbSeeder.
// Data/DbSeeder.cs
namespace NetRefreshTokenDemo.Api.Data;
public class DbSeeder
{
}Within a DbSeeder class, create an asynchronous static method named SeedData.
public class DbSeeder
{
public static async Task SeedData(IApplicationBuilder app)
{
}
}Now, we will implement the SeedData method. In this method we checks if there is any user is in the database or not. If there won’t be any user in the database, we will create a user with username [email protected]. In this way we will create an admin record when our application runs.
Get Ravindra Devrani’s stories in your inbox
Join Medium for free to get updates from this writer.
Subscribe
Remember me for faster sign in
I have added comments, so code is self explanatory.
public static async Task SeedData(IApplicationBuilder app)
{
// Create a scoped service provider to resolve dependencies
using var scope = app.ApplicationServices.CreateScope();
// resolve the logger service
var logger = scope.ServiceProvider.GetRequiredService<ILogger<DbSeeder>>();
try
{
// resolve other dependencies
var userManager = scope.ServiceProvider.GetService<UserManager<ApplicationUser>>();
var roleManager = scope.ServiceProvider.GetService<RoleManager<IdentityRole>>();
// Check if any users exist to prevent duplicate seeding
if (userManager.Users.Any() == false)
{
var user = new ApplicationUser
{
Name = "Admin",
UserName = "[email protected]",
Email = "[email protected]",
EmailConfirmed = true,
SecurityStamp = Guid.NewGuid().ToString()
};
// Create Admin role if it doesn't exist
if ((await roleManager.RoleExistsAsync(Roles.Admin)) == false)
{
logger.LogInformation("Admin role is creating");
var roleResult = await roleManager
.CreateAsync(new IdentityRole(Roles.Admin));
if (roleResult.Succeeded == false)
{
var roleErros = roleResult.Errors.Select(e => e.Description);
logger.LogError($"Failed to create admin role. Errors : {string.Join(",", roleErros)}");
return;
}
logger.LogInformation("Admin role is created");
}
// Attempt to create admin user
var createUserResult = await userManager
.CreateAsync(user: user, password: "Admin@123");
// Validate user creation
if (createUserResult.Succeeded == false)
{
var errors = createUserResult.Errors.Select(e => e.Description);
logger.LogError(
$"Failed to create admin user. Errors: {string.Join(", ", errors)}"
);
return;
}
// adding role to user
var addUserToRoleResult = await userManager
.AddToRoleAsync(user: user, role: Roles.Admin);
if (addUserToRoleResult.Succeeded == false)
{
var errors = addUserToRoleResult.Errors.Select(e => e.Description);
logger.LogError($"Failed to add admin role to user. Errors : {string.Join(",", errors)}");
}
logger.LogInformation("Admin user is created");
}
}
catch (Exception ex)
{
logger.LogCritical(ex.Message);
}
}Make sure to add these namespaces at the top of the file.
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using NetRefreshTokenDemo.Api.Constants;
using NetRefreshTokenDemo.Api.Models;Now, we need to call the seeding method in the Program.cs .
// Program.cs
// Existing code removed for brevity
await DbSeeder.SeedData(app); // Call this method to seed the data
app.Run();Run the project
Run the project with the following command.
dotnet runOnce the project is run successfully, stop it by pressing ctrl+c in the terminal. At this point the default admin user is created.
Signup & Login models
Create a new folder named DTOs within the Models folder. Within the folder named DTOs, create a new class named SignupModel.
using System.ComponentModel.DataAnnotations;
namespace NetRefreshTokenDemo.Api.Models.DTOs;
public class SignupModel
{
[Required]
[MaxLength(30)]
public string Name { get; set; } = string.Empty;
[Required]
[MaxLength(30)]
[EmailAddress]
public string Email { get; set; } = string.Empty;
[Required]
[MaxLength(30)]
public string Password { get; set; } = string.Empty;
}Within the folder named DTOs, create a class named LoginModel.
using System.ComponentModel.DataAnnotations;
namespace NetRefreshTokenDemo.Api.Models.DTOs;
public class LoginModel
{
[Required]
public string Username { get; set; } = string.Empty;
[Required]
public string Password { get; set; } = string.Empty;
}AuthController
Within the folder named Controllers, create a controller named AuthController.
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
namespace NetRefreshTokenDemo.Api.Controllers;
[Route("api/[controller]")]
[ApiController]
public class AuthController : ControllerBase
{
}Inject the ILogger, UserManager and RoleManager services to the controller.








Join the conversation! Your thoughts help the community grow.