.NET: Refresh token implementasyonu ile JWT Bearer Authentication
JWT Bearer ile Authentication işlemlerini kolayca yapabilsek bile, token expired olduğu zaman tekrardan kullanıcı bilgilerini almadan yeni bir token üretmemiz lazım bunun için tokeni yenileyen bir refresh token yapısı oluşturacağız.
Yazıda JWT token oluşturma ve API’nın sunumu ile ilgili detaylara girilmeyecektir. Sadece Refresh token yapısının ayrıntılarından bahsedilecektir.
Data
Entityler
namespace dotnet_jwt_refresh_token.Data;
public class User
{
public int Id { get; set; }
public string UserName { get; set; }
public string Password { get; set; }
}
public class RefreshToken
{
public int Id { get; set; }
public string UserId { get; set; }
public string Token { get; set; } = Guid.NewGuid().ToString();
}
DatabaseContext
using Microsoft.EntityFrameworkCore;
namespace dotnet_jwt_refresh_token.Data;
public class DatabaseContext : DbContext
{
public DatabaseContext(DbContextOptions<DatabaseContext> options) : base(options) { }
protected override void OnModelCreating(ModelBuilder builder)
{
builder.Entity<User>(entity =>
{
entity.Property(user => user.Id).ValueGeneratedOnAdd();
});
builder.Entity<RefreshToken>(entity =>
{
entity.Property(refreshToken => refreshToken.Id).ValueGeneratedOnAdd();
});
base.OnModelCreating(builder);
}
public virtual DbSet<User> Users { get; set; }
public virtual DbSet<RefreshToken> RefreshTokens { get; set; }
}
Basit bir User entity’si ve RefreshToken bilgilerinin tutulacağı entity ile DatabaseContext tanımı.
Servisler
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using dotnet_jwt_refresh_token.Models;
using Microsoft.IdentityModel.Tokens;
namespace dotnet_jwt_refresh_token.Services;
public interface ITokenService
{
/// <summary>
/// Gets the principal claims from the exist token.
/// </summary>
ClaimsPrincipal GetClaimsPrincipal(string bearerToken);
/// <summary>
/// Creates a new token with refresh token.
/// </summary>
TokenModel Create(string userId);
}
public class TokenService : ITokenService
{
private readonly SymmetricSecurityKey _jwtSymmetricSecurityKey;
private const string SecurityAlgorithm = SecurityAlgorithms.HmacSha256;
public TokenService(IConfiguration configuration) =>
_jwtSymmetricSecurityKey = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(configuration["Jwt:Key"]
?? throw new ApiException("Jwt:Key is not set in configuration.")));
public ClaimsPrincipal GetClaimsPrincipal(string bearerToken)
{
var tokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = false,
ValidateAudience = false,
ValidateLifetime = false,
ValidateIssuerSigningKey = true,
IssuerSigningKey = _jwtSymmetricSecurityKey,
ClockSkew = TimeSpan.Zero
};
var tokenHandler = new JwtSecurityTokenHandler();
var principal = tokenHandler.ValidateToken(bearerToken, tokenValidationParameters, out var securityToken);
var validToken = securityToken is JwtSecurityToken jwtSecurityToken
&& jwtSecurityToken.Header.Alg.Equals(SecurityAlgorithm, StringComparison.InvariantCultureIgnoreCase);
if (validToken is false)
throw new ApiException("Invalid token");
return principal;
}
public TokenModel Create(string userId)
{
var claims = new List<Claim>
{
new(JwtRegisteredClaimNames.NameId, userId)
};
var expiresIn = DateTime.Now.AddMinutes(1);
var token = new JwtSecurityToken(
expires: expiresIn,
claims: claims,
signingCredentials: new SigningCredentials(_jwtSymmetricSecurityKey, SecurityAlgorithm)
);
return new TokenModel
{
BearerToken = new JwtSecurityTokenHandler().WriteToken(token),
RefreshToken = Guid.NewGuid().ToString(),
ExpiresIn = expiresIn
};
}
}
Token servisi refresh token değeri ile birlikte bearer token oluşturmak ve var olan tokenden değer elde etmek için iki metoda sahip.
Create metotu, iletilen kullanıcı bilgileri ile yeni bir JWT bearer token ve Guid.NewGuid().ToString()
ile refresh token oluşturur ve bunları TokenModel
nesnesinde döndürür.
GetClaimsPrincipal metotu, bir bearer token’in geçerli olup olmadığını doğrular ve geçerliyse oluşturulurken iletilen kullanıcı bilgileri ile ClaimsPrincipal
nesnesi döndürür.
using dotnet_jwt_refresh_token.Data;
using Microsoft.EntityFrameworkCore;
namespace dotnet_jwt_refresh_token.Services;
public interface IRefreshTokenService
{
/// <summary>
/// Creates new refresh token.
/// </summary>
Task AddAsync(RefreshToken refreshToken);
/// <summary>
/// Gets the refresh token.
/// </summary>
Task<RefreshToken> GetAsync(string userId, string refreshTokenValue);
/// <summary>
/// Deletes the refresh token.
/// </summary>
public Task DeleteAsync(RefreshToken refreshToken);
/// <summary>
/// Saves the changes.
/// </summary>
Task<int> SaveChangesAsync();
}
public class RefreshTokenService : IRefreshTokenService
{
private readonly DatabaseContext _databaseContext;
public RefreshTokenService(DatabaseContext databaseContext)
=> _databaseContext = databaseContext;
public async Task AddAsync(RefreshToken refreshToken)
=> await _databaseContext.RefreshTokens.AddAsync(refreshToken);
public async Task<RefreshToken> GetAsync(string userId, string refreshTokenValue)
=> await _databaseContext.RefreshTokens
.FirstOrDefaultAsync(x => x.UserId == userId && x.Token == refreshTokenValue);
public Task DeleteAsync(RefreshToken refreshToken)
{
if (refreshToken is not null)
_databaseContext.Remove(refreshToken);
return Task.CompletedTask;
}
public async Task<int> SaveChangesAsync()
=> await _databaseContext.SaveChangesAsync();
}
RefreshToken servisi ise refresh token ile ilgili database işlemleri için kullanılan basit bir servis.
Program.cs
builder.Services.AddDbContext<DatabaseContext>(o => o.UseInMemoryDatabase(nameof(DatabaseContext)));
builder.Services.AddSingleton<ITokenService, TokenService>();
builder.Services.AddScoped<IRefreshTokenService, RefreshTokenService>();
builder.Services.AddScoped<IUserService, UserService>();
Oluşturduğumuz servisleri için ve bir database kullanacağımızı belirtmemiz için Program.cs’de veya eski versiyon bir .NET kullanıyorsanız Startup’da tanımlamalar yapmak zorundasınız.
// add authentication with jwt bearer scheme
builder.Services.AddAuthentication(x =>
{
x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(o =>
{ // configure jwt bearer
o.SaveToken = true;
o.TokenValidationParameters = new TokenValidationParameters
{
ValidIssuer = builder.Configuration["Jwt:Issuer"],
ValidateIssuer = false, // should be true in production
ValidAudience = builder.Configuration["Jwt:Audience"],
ValidateAudience = false, // should be true in production
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"])),
ClockSkew = TimeSpan.Zero
};
});
Controllers
AuthenticateController
using dotnet_jwt_refresh_token.Data;
using dotnet_jwt_refresh_token.Models;
using dotnet_jwt_refresh_token.Services;
using Microsoft.AspNetCore.Mvc;
namespace dotnet_jwt_refresh_token.Controllers;
[ApiController]
[Route("[controller]")]
public class AuthenticateController : ControllerBase
{
private readonly IUserService _userService;
private readonly ITokenService _tokenService;
private readonly IRefreshTokenService _refreshTokenService;
private readonly IHttpContextAccessor _httpContextAccessor;
public AuthenticateController(IUserService userService, ITokenService tokenService, IRefreshTokenService refreshTokenService, IHttpContextAccessor httpContextAccessor)
{
_userService = userService;
_tokenService = tokenService;
_refreshTokenService = refreshTokenService;
_httpContextAccessor = httpContextAccessor;
}
[HttpPost]
public async Task<TokenModel> AuthenticateAsync([FromBody] AuthenticateUserRequest request)
{
var user = await _userService.GetUserByCredentialsAsync(request);
if (user is null)
{
throw new ApiException("Invalid credentials.");
}
var token = _tokenService.Create(user.Id.ToString());
await _refreshTokenService.AddAsync(new RefreshToken()
{
UserId = user.Id.ToString(),
Token = token.RefreshToken,
});
await _refreshTokenService.SaveChangesAsync();
return token;
}
[HttpPost("refresh")]
public async Task<TokenModel> RefreshTokenAsync([FromBody] RefreshTokenRequest request)
{
_httpContextAccessor.HttpContext?.Request.Headers.TryGetValue("Authorization", out var authorizationValue);
var token = authorizationValue.ToString().Replace("Bearer ", "");
if (string.IsNullOrEmpty(token))
{
throw new ApiException("Authorization header is missing or value is empty.");
}
var claimsPrincipal = _tokenService.GetClaims(token);
var userId = claimsPrincipal.GetId();
var activeRefreshToken = await _refreshTokenService.GetAsync(userId, request.ActiveRefreshToken);
if (activeRefreshToken == null)
{
throw new ApiException("Invalid refresh token.");
}
var newToken = _tokenService.Create(userId);
await _refreshTokenService.DeleteAsync(activeRefreshToken);
await _refreshTokenService.AddAsync(new RefreshToken
{
Token = newToken.RefreshToken,
UserId = userId
});
await _refreshTokenService.SaveChangesAsync();
return newToken;
}
}
İki API’ye sahip bu controller, bir user için authenticate olmasını ve kullanılan bearer token’i, refresh token ile yenilemeyi sağlıyor.
RefreshTokenAsync metotu: İlk olarak, Authorization header’ından JWT bearer tokenini çıkarır. Daha sonra, çıkarılan token değerini kullanarak ilgili kullanıcıya ulaşılır. Ardından, ulaşılan kullanıcının, request body’den iletilen refresh token’inin geçerli olup olmadığı kontrol edilir. Geçerli ise, yeni bir refresh token ile yeni bir JWT bearer token oluşturulur. En son olarak, eski refresh token silinir, yeni refresh token veritabanına kaydedilir ve yeni oluşturulan JWT berarer token ile refresh token döndürülür.
Kullanılmış refresh token’ın silinmesi ve yenisinin kaydedilmesini asenkron şekilde çalışacak bir yapıya bırakmanızı tavsiye ederim.
Postman üzerinden request örnekleri:
https://api.postman.com/collections/18365191-525fc0cd-a001-493e-bc0d-b6e8621e74c9?access_key=PMAT-01GXTK8T7XXZS13C4T2N118SDA
Böylelikle bir kullanıcı mobil veya web application üzerinden bir kere authenticate olduğu zaman tekrar tekrar credential bilgilerini istemeye gerek duymadan refresh token ile eski bearer token’ı yenileyebilirsiniz.