
Loading ..
Please wait while we prepare...
9/4/2025
Securing APIs is critical in modern web development.
A proven approach is to combine JWT (JSON Web Token) with Refresh Tokens.
This balances security (short-lived tokens), scalability (stateless APIs), and user experience (refresh without re-login).
In this guide, we will:
A JSON Web Token (JWT) is a compact, URL-safe way to transfer claims between two parties.
It is widely used for authentication & authorization in APIs.
A JWT is made of three parts:
HS256
)Example:
xxxxx.yyyyy.zzzzz
Unlike access tokens (short-lived), a Refresh Token is long-lived and used to obtain new JWTs.
Benefits:
Flow:
JWT + Refresh Token
.We’ll now implement the full authentication system.
appsettings.json
)"Jwt": {
"Key": "SuperSecretKey123456789",
"Issuer": "AspCoreWithJWT",
"Audience": "AspCoreWithJWTUsers",
"TokenExpirationMinutes": 15,
"RefreshTokenExpirationDays": 7
}
What it does: Holds security settings: key, issuer, audience, and token lifetimes.
Why it’s important: Centralizes configuration and avoids hardcoding sensitive values.
Where it’s used:
Read by Program.cs
and TokenService
for signing and validating tokens.
Program.cs
)var jwtKey = builder.Configuration["Jwt:Key"];
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = builder.Configuration["Jwt:Issuer"],
ValidAudience = builder.Configuration["Jwt:Audience"],
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtKey!)),
ClockSkew = TimeSpan.Zero
};
});
What it does: Adds JWT authentication middleware and sets validation rules.
Why it’s important: Ensures only trusted, non-expired tokens are accepted.
Where it’s used:
Applied globally — required for any [Authorize]
endpoints.
public class TokenService(IConfiguration configuration) : ITokenService
{
public string GenerateJwtToken(User user)
{
var key = Encoding.ASCII.GetBytes(configuration["Jwt:Key"]!);
var claims = new List<Claim>
{
new(ClaimTypes.NameIdentifier, user.Id.ToString()),
new(ClaimTypes.Name, user.UserName),
new(ClaimTypes.Email, user.Email),
new(ClaimTypes.Role, user.Role)
};
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(claims),
Expires = DateTime.UtcNow.AddMinutes(double.Parse(configuration["Jwt:TokenExpirationMinutes"]!)),
Issuer = configuration["Jwt:Issuer"],
Audience = configuration["Jwt:Audience"],
SigningCredentials = new SigningCredentials(
new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256)
};
return new JwtSecurityTokenHandler().WriteToken(
new JwtSecurityTokenHandler().CreateToken(tokenDescriptor)
);
}
public RefreshToken GenerateRefreshToken(string ipAddress)
{
using var rng = RandomNumberGenerator.Create();
var bytes = new byte[64];
rng.GetBytes(bytes);
return new RefreshToken
{
Token = Convert.ToBase64String(bytes),
Expires = DateTime.UtcNow.AddDays(double.Parse(configuration["Jwt:RefreshTokenExpirationDays"]!)),
Created = DateTime.UtcNow,
CreatedByIp = ipAddress
};
}
}
What it does:
GenerateJwtToken
→ creates a JWT containing user claims.GenerateRefreshToken
→ creates a secure random token for refresh flow.Why it’s important: This is the core of authentication: JWTs prove identity; refresh tokens extend sessions securely.
Where it’s used:
Called by UserService
when authenticating or refreshing tokens.
public class RefreshToken
{
public int Id { get; set; }
public string Token { get; set; } = string.Empty;
public DateTime Expires { get; set; }
public DateTime Created { get; set; }
public string CreatedByIp { get; set; } = string.Empty;
public DateTime? Revoked { get; set; }
public string? RevokedByIp { get; set; }
public string? ReplacedByToken { get; set; }
public bool IsExpired => DateTime.UtcNow >= Expires;
public bool IsRevoked => Revoked != null;
public bool IsActive => !IsRevoked && !IsExpired;
}
What it does: Represents a refresh token entity with lifecycle tracking (active, expired, revoked).
Why it’s important: Allows safe token rotation, detection of compromised tokens, and revocation when necessary.
Where it’s used:
Stored in DB for each user (User.RefreshTokens
).
public class UserService : IUserService
{
private readonly ApplicationDbContext _context;
private readonly ITokenService _tokenService;
public UserService(ApplicationDbContext context, ITokenService tokenService)
{
_context = context;
_tokenService = tokenService;
}
public async Task<AuthResponse?> Authenticate(AuthRequest model, string ipAddress)
{
var user = await _context.Users.SingleOrDefaultAsync(x => x.UserName == model.Username);
if (user == null || !BCrypt.Net.BCrypt.Verify(model.Password, user.PasswordHash))
return null;
var jwtToken = _tokenService.GenerateJwtToken(user);
var refreshToken = _tokenService.GenerateRefreshToken(ipAddress);
user.RefreshTokens.Add(refreshToken);
_context.Update(user);
await _context.SaveChangesAsync();
return new AuthResponse(user, jwtToken, refreshToken.Token);
}
public async Task<AuthResponse?> RefreshToken(string token, string ipAddress)
{
var user = await _context.Users.Include(u => u.RefreshTokens)
.SingleOrDefaultAsync(u => u.RefreshTokens.Any(t => t.Token == token));
var refreshToken = user?.RefreshTokens.SingleOrDefault(rt => rt.Token == token);
if (refreshToken == null || !refreshToken.IsActive)
return null;
var newRefreshToken = _tokenService.GenerateRefreshToken(ipAddress);
refreshToken.Revoked = DateTime.UtcNow;
refreshToken.RevokedByIp = ipAddress;
refreshToken.ReplacedByToken = newRefreshToken.Token;
user!.RefreshTokens.Add(newRefreshToken);
_context.Update(user);
await _context.SaveChangesAsync();
var jwtToken = _tokenService.GenerateJwtToken(user);
return new AuthResponse(user, jwtToken, newRefreshToken.Token);
}
}
What it does:
Authenticate
→ validates credentials, issues JWT + refresh token.RefreshToken
→ revokes old refresh token, issues new pair (JWT + refresh).Why it’s important: Implements the refresh flow that keeps sessions alive securely while preventing reuse of stolen tokens.
Where it’s used:
Called by /api/auth/login
and /api/auth/refresh-token
.
[Authorize]
[ApiController]
[Route("api/[controller]")]
public class UsersController : ControllerBase
{
[Authorize(Roles = "Admin")]
[HttpGet]
public async Task<IActionResult> GetAll()
{
// Admin-only endpoint
}
}
What it does: Requires JWTs for access and restricts roles where needed.
Why it’s important: Protects sensitive endpoints from anonymous users.
Where it’s used: Controllers that manage user data, admin dashboards, etc.
Now let’s test the flow step by step.
POST https://localhost:7240/api/users/register
Content-Type: application/json
{
"username": "saif",
"email": "saif@example.com",
"password": "P@ssword123",
"role": 1 // admin 0, user 1
}
POST https://localhost:7240/api/auth/login
Content-Type: application/json
{
"username": "saif",
"password": "P@ssword123"
}
Response:
{
"id": 1,
"username": "saif",
"email": "saif@example.com",
"role": "User",
"jwtToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"refreshToken": "0HL1R+e2fVB6+Z9gQeZT+KYe..."
}
POST https://localhost:7240/api/users/profile
Authorization: Bearer <jwtToken>
POST https://localhost:7240/api/auth/refresh-token
Content-Type: application/json
"0HL1R+e2fVB6+Z9gQeZT+KYe..."
Response:
{
"id": 1,
"username": "saif",
"email": "saif@example.com",
"role": "User",
"jwtToken": "newJwtHere...",
"refreshToken": "newRefreshTokenHere..."
}