A standalone JWT authentication and authorization library for ASP.NET Core Web APIs with refresh token support, RS256/HS256 algorithms, automatic token refresh middleware, and claims-based access control.
$ dotnet add package TamimahAuthCoreA standalone JWT authentication and authorization library for ASP.NET Core Web APIs targeting .NET 9, with refresh token support and claims-based access control.
X-New-Token headerdotnet add package TamimahAuthCore
// Program.cs
using Tamimah.Extensions;
var builder = WebApplication.CreateBuilder(args);
// Add Tamimah authentication and authorization
builder.Services.AddTamimah(
auth =>
{
auth.SecretKey = "your-secret-key-at-least-32-characters-long!";
auth.Issuer = "your-app";
auth.Audience = "your-api";
auth.AccessTokenExpirationMinutes = 15;
auth.RefreshTokenExpirationDays = 7;
},
authz =>
{
authz.AddPolicy("AdminOnly", policy =>
policy.RequireRole("Admin"));
authz.AddPolicy("CanEditUsers", policy =>
policy.RequirePermission("users:edit"));
}
);
var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
app.Run();
using Tamimah.Claims;
using Tamimah.Services;
public class AuthController : ControllerBase
{
private readonly IJwtTokenService _jwtTokenService;
private readonly IRefreshTokenService _refreshTokenService;
public AuthController(
IJwtTokenService jwtTokenService,
IRefreshTokenService refreshTokenService)
{
_jwtTokenService = jwtTokenService;
_refreshTokenService = refreshTokenService;
}
[HttpPost("login")]
public async Task<IActionResult> Login([FromBody] LoginRequest request)
{
// Validate credentials (your logic here)
var userId = "user123";
// Build claims using the fluent builder
var claims = ClaimsBuilder.Create()
.AddUserId(userId)
.AddEmail("user@example.com")
.AddRoles("Admin", "User")
.AddPermissions("users:read", "users:write")
.AddTenantId("tenant1")
.Build();
// Generate tokens
var accessToken = _jwtTokenService.GenerateAccessToken(claims);
var refreshToken = await _refreshTokenService.CreateRefreshTokenAsync(userId);
return Ok(new
{
accessToken = accessToken.Token,
expiresAt = accessToken.ExpiresAt,
refreshToken = refreshToken.Token
});
}
[HttpPost("refresh")]
public async Task<IActionResult> Refresh([FromBody] RefreshRequest request)
{
// Validate refresh token
var isValid = await _refreshTokenService.ValidateRefreshTokenAsync(request.RefreshToken);
if (!isValid)
{
return Unauthorized("Invalid refresh token");
}
// Get the old token to extract user info
var oldToken = await _refreshTokenService.GetRefreshTokenAsync(request.RefreshToken);
// Rotate refresh token
var newRefreshToken = await _refreshTokenService.RotateRefreshTokenAsync(request.RefreshToken);
// Generate new access token
var accessToken = _jwtTokenService.GenerateAccessToken(oldToken!.UserId);
return Ok(new
{
accessToken = accessToken.Token,
refreshToken = newRefreshToken!.Token
});
}
}
using Microsoft.AspNetCore.Authorization;
using Tamimah.Authorization.Policies;
using Tamimah.Claims;
[ApiController]
[Route("api/[controller]")]
public class UsersController : ControllerBase
{
// Require authentication
[Authorize]
[HttpGet]
public IActionResult GetUsers()
{
var userId = User.GetUserId();
var permissions = User.GetPermissions();
return Ok(new { userId, permissions });
}
// Require specific role
[Authorize(Roles = "Admin")]
[HttpDelete("{id}")]
public IActionResult DeleteUser(string id)
{
return Ok();
}
// Require specific permission
[Authorize(Policy = "CanEditUsers")]
[HttpPut("{id}")]
public IActionResult UpdateUser(string id)
{
return Ok();
}
// Use built-in policy names
[Authorize(Policy = PolicyNames.AdminOnly)]
[HttpGet("admin")]
public IActionResult AdminEndpoint()
{
return Ok();
}
}
| Property | Type | Default | Description |
|---|---|---|---|
Algorithm | JwtAlgorithm | HS256 | Signing algorithm (HS256 or RS256) |
SecretKey | string | - | Secret key for HS256 (min 32 chars) |
RsaPrivateKey | string | - | RSA private key in PEM format (for RS256) |
RsaPublicKey | string | - | RSA public key in PEM format (for RS256) |
Issuer | string | - | Token issuer (iss claim) |
Audience | string | - | Token audience (aud claim) |
AccessTokenExpirationMinutes | int | 15 | Access token lifetime |
RefreshTokenExpirationDays | int | 7 | Refresh token lifetime |
ValidateIssuer | bool | true | Validate issuer during token validation |
ValidateAudience | bool | true | Validate audience during token validation |
ValidateLifetime | bool | true | Validate token expiration |
ValidateIssuerSigningKey | bool | true | Validate signing key |
ClockSkew | TimeSpan | 0 | Clock skew tolerance |
RequireHttps | bool | true | Require HTTPS for auth endpoints |
EnableTokenRefresh | bool | false | Enable automatic token refresh middleware |
HS256 (HMAC - Default):
{
"Tamimah": {
"Algorithm": "HS256",
"SecretKey": "your-secret-key-at-least-32-characters-long!",
"Issuer": "your-app",
"Audience": "your-api",
"AccessTokenExpirationMinutes": 15,
"RefreshTokenExpirationDays": 7
}
}
RS256 (RSA):
{
"Tamimah": {
"Algorithm": "RS256",
"RsaPrivateKey": "-----BEGIN RSA PRIVATE KEY-----\nMIIE...\n-----END RSA PRIVATE KEY-----",
"RsaPublicKey": "-----BEGIN RSA PUBLIC KEY-----\nMIIB...\n-----END RSA PUBLIC KEY-----",
"Issuer": "your-app",
"Audience": "your-api",
"AccessTokenExpirationMinutes": 15,
"RefreshTokenExpirationDays": 7
}
}
builder.Services.AddTamimahAuthentication(
builder.Configuration.GetSection("Tamimah")
);
For asymmetric key signing, use RS256 with RSA key pairs:
builder.Services.AddTamimahAuthentication(options =>
{
options.Algorithm = JwtAlgorithm.RS256;
options.RsaPrivateKey = File.ReadAllText("private_key.pem");
options.RsaPublicKey = File.ReadAllText("public_key.pem");
options.Issuer = "your-app";
options.Audience = "your-api";
});
Generate RSA keys:
# Generate private key
openssl genrsa -out private_key.pem 2048
# Extract public key
openssl rsa -in private_key.pem -pubout -out public_key.pem
RS256 is useful when:
Build claims using the fluent API:
var claims = ClaimsBuilder.Create()
.AddUserId("user123")
.AddEmail("user@example.com")
.AddUsername("johndoe")
.AddFullName("John Doe")
.AddName("John", "Doe")
.AddRoles("Admin", "User")
.AddPermissions("users:read", "users:write", "posts:read")
.AddTenantId("tenant1")
.AddOrganizationId("org1")
.AddPhoneNumber("+1234567890")
.AddEmailVerified(true)
.AddScopes("read", "write")
.AddJti() // Adds unique token ID
.AddIssuedAt() // Adds issued at timestamp
.AddSessionId() // Adds session ID
.AddCustomClaim("custom_key", "custom_value")
.Build();
Access claims easily from HttpContext.User:
// Get user information
var userId = User.GetUserId();
var email = User.GetEmail();
var username = User.GetUsername();
var fullName = User.GetFullName();
var firstName = User.GetFirstName();
var lastName = User.GetLastName();
// Get roles and permissions
var roles = User.GetRoles();
var permissions = User.GetPermissions();
// Check permissions
bool canRead = User.HasPermission("users:read");
bool canReadOrWrite = User.HasAnyPermission("users:read", "users:write");
bool canReadAndWrite = User.HasAllPermissions("users:read", "users:write");
// Check roles
bool isAdmin = User.IsInRole("Admin");
bool isAdminOrMod = User.HasAnyRole("Admin", "Moderator");
bool isAdminAndUser = User.HasAllRoles("Admin", "User");
// Multi-tenant support
var tenantId = User.GetTenantId();
var orgId = User.GetOrganizationId();
// Other claims
var sessionId = User.GetSessionId();
var scopes = User.GetScopes();
bool hasScope = User.HasScope("read");
bool isEmailVerified = User.IsEmailVerified();
using Tamimah.Authorization.Policies;
// Available policy names
PolicyNames.Authenticated // Requires authenticated user
PolicyNames.AdminOnly // Requires Admin role
PolicyNames.EmailVerified // Requires verified email
builder.Services.AddTamimahAuthorization(options =>
{
// Permission-based
options.AddPolicy("CanReadUsers", policy =>
policy.RequirePermission("users:read"));
options.AddPolicy("CanManageUsers", policy =>
policy.RequireAnyPermission("users:read", "users:write", "users:delete"));
options.AddPolicy("FullUserAccess", policy =>
policy.RequireAllPermissions("users:read", "users:write", "users:delete"));
// Role-based
options.AddPolicy("AdminOrModerator", policy =>
policy.RequireAnyRole("Admin", "Moderator"));
options.AddPolicy("SuperUser", policy =>
policy.RequireAllRoles("Admin", "PowerUser"));
// Claims-based
options.AddPolicy("VerifiedEmail", policy =>
policy.RequireEmailVerified());
options.AddPolicy("SpecificTenant", policy =>
policy.RequireTenant("tenant1"));
options.AddPolicy("HasScope", policy =>
policy.RequireScope("api:full"));
// Combined policies
options.AddPolicy("AdminWithPermission", policy =>
policy.RequireRole("Admin")
.RequirePermission("users:manage"));
});
The default RefreshTokenService uses in-memory storage. For production, implement IRefreshTokenService with persistent storage:
public class DatabaseRefreshTokenService : IRefreshTokenService
{
private readonly AppDbContext _context;
public DatabaseRefreshTokenService(AppDbContext context)
{
_context = context;
}
public async Task<RefreshToken> CreateRefreshTokenAsync(string userId, string? ipAddress = null)
{
var token = RefreshToken.Create(userId, 7, ipAddress);
_context.RefreshTokens.Add(token);
await _context.SaveChangesAsync();
return token;
}
// Implement other methods...
}
// Register custom service
builder.Services.AddCustomRefreshTokenService<DatabaseRefreshTokenService>();
Custom claim types defined in TamimahClaimTypes:
| Constant | Value | Description |
|---|---|---|
Permission | permission | User permissions |
UserId | uid | User identifier |
Email | email | Email address |
Username | username | Username |
FullName | name | Full name |
FirstName | given_name | First name |
LastName | family_name | Last name |
TenantId | tenant_id | Tenant identifier |
OrganizationId | org_id | Organization identifier |
EmailVerified | email_verified | Email verification status |
SessionId | sid | Session identifier |
Scope | scope | OAuth scopes |
// Validate a token
var principal = _jwtTokenService.ValidateToken(token);
if (principal != null)
{
// Token is valid
var userId = principal.GetUserId();
}
// Validate ignoring expiration (useful for refresh)
var principal = _jwtTokenService.ValidateTokenIgnoringExpiration(expiredToken);
// Check if token is expired
bool isExpired = _jwtTokenService.IsTokenExpired(token);
// Get token expiration
DateTime? expiration = _jwtTokenService.GetTokenExpiration(token);
// Get specific claim
string? userId = _jwtTokenService.GetClaimValue(token, JwtRegisteredClaimNames.Sub);
// Get all claims
var claims = _jwtTokenService.GetClaims(token);
Refresh tokens programmatically while preserving claims:
// Refresh a valid token (returns null if token is invalid or expired)
var newToken = _jwtTokenService.RefreshToken(existingToken);
// Refresh even expired tokens (useful for sliding expiration)
var newToken = _jwtTokenService.RefreshTokenIgnoringExpiration(expiredToken);
// Add additional claims during refresh
var additionalClaims = new[] { new Claim(ClaimTypes.Role, "NewRole") };
var newToken = _jwtTokenService.RefreshToken(existingToken, additionalClaims);
Enable automatic token refresh on every successful response. The middleware validates the incoming token and returns a fresh token in the X-New-Token response header:
var app = builder.Build();
// Add token refresh middleware BEFORE authentication
app.UseTamimahTokenRefresh();
app.UseAuthentication();
app.UseAuthorization();
app.Run();
How it works:
Authorization: Bearer <token> headerX-New-Token response headerClient-side handling (JavaScript example):
async function fetchWithTokenRefresh(url, options = {}) {
const response = await fetch(url, {
...options,
headers: {
...options.headers,
'Authorization': `Bearer ${getToken()}`
}
});
// Check for refreshed token
const newToken = response.headers.get('X-New-Token');
if (newToken) {
saveToken(newToken); // Update stored token
}
return response;
}
Optional middleware for custom token handling scenarios:
app.UseTamimahJwt(); // Validates token and sets HttpContext.User
Automatic token refresh with sliding expiration:
app.UseTamimahTokenRefresh(); // Returns refreshed token in X-New-Token header
| Method | Description |
|---|---|
GenerateAccessToken(claims) | Generate token with specified claims |
GenerateAccessToken(userId, additionalClaims?) | Generate token for user ID |
ValidateToken(token) | Validate token and return ClaimsPrincipal |
ValidateTokenIgnoringExpiration(token) | Validate token ignoring expiration |
RefreshToken(token, additionalClaims?) | Refresh a valid token |
RefreshTokenIgnoringExpiration(token, additionalClaims?) | Refresh token ignoring expiration |
GetClaimValue(token, claimType) | Get specific claim value |
GetClaims(token) | Get all claims from token |
IsTokenExpired(token) | Check if token is expired |
GetTokenExpiration(token) | Get token expiration date |
| Method | Description |
|---|---|
CreateRefreshTokenAsync(userId, ipAddress?) | Create new refresh token |
ValidateRefreshTokenAsync(token) | Validate refresh token |
GetRefreshTokenAsync(token) | Get refresh token details |
RotateRefreshTokenAsync(token, ipAddress?) | Rotate refresh token |
RevokeRefreshTokenAsync(token, reason?) | Revoke refresh token |
RevokeAllUserTokensAsync(userId, reason?) | Revoke all user tokens |
MIT License