EasyReasy.Auth 2.0.0

dotnet add package EasyReasy.Auth --version 2.0.0
                    
NuGet\Install-Package EasyReasy.Auth -Version 2.0.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="EasyReasy.Auth" Version="2.0.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="EasyReasy.Auth" Version="2.0.0" />
                    
Directory.Packages.props
<PackageReference Include="EasyReasy.Auth" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add EasyReasy.Auth --version 2.0.0
                    
#r "nuget: EasyReasy.Auth, 2.0.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package EasyReasy.Auth@2.0.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=EasyReasy.Auth&version=2.0.0
                    
Install as a Cake Addin
#tool nuget:?package=EasyReasy.Auth&version=2.0.0
                    
Install as a Cake Tool

EasyReasy.Auth

← Back to EasyReasy System

NuGet

A lightweight .NET library for internal JWT authentication and claims handling, designed for simplicity and security.

Overview

EasyReasy.Auth makes it easy to issue, validate, and work with JWT tokens in your .NET applications, with built-in support for roles, custom claims, and progressive brute-force protection.

Why Use EasyReasy.Auth?

  • Simple JWT issuing: Create signed tokens with standard and custom claims
  • Claims injection: Access user and tenant IDs easily in your controllers
  • Role access: Retrieve all roles for the current user with a single call
  • Claim access: Retrieve any claim value by key or enum with a single call
  • Progressive delay: Built-in middleware to slow down brute-force attacks (enabled by default)
  • Refresh token rotation: Opt-in refresh tokens with automatic theft detection via token family tracking
  • Flexible configuration: Optional issuer validation, easy integration with ASP.NET Core
  • Clear error messages: Enforces minimum secret length for security

Quick Start

1. Add to your project

Install via NuGet:

# In your web/API project
dotnet add package EasyReasy.Auth
dotnet add package Microsoft.IdentityModel.JsonWebTokens

Important note! You will always get 401 Unauthorized if you forget to install Microsoft.IdentityModel.JsonWebTokens

2. Configure in Program.cs

string jwtSecret = Environment.GetEnvironmentVariable("JWT_SIGNING_SECRET")!;
builder.Services.AddEasyReasyAuth(jwtSecret, issuer: "my-issuer");

var app = builder.Build();
app.UseEasyReasyAuth(); // Progressive delay enabled by default

3. Issue tokens

You probably want to get an instance of IJWtTokenService via dependency injection in your controller class and create an endpoint in that is responsible for issuing tokens if they should be issued.

IJwtTokenService tokenService = new JwtTokenService(jwtSecret, issuer: "my-issuer");
string token = tokenService.CreateToken(
    subject: "user-123",
    authType: "apikey",
    additionalClaims: new[] { new Claim("tenant_id", "tenant-42") },
    roles: new[] { "admin", "user" },
    expiresAt: DateTime.UtcNow.AddHours(1));

The library can automatically create authentication endpoints for you. First, implement the validation service:

public class MyAuthService : IAuthRequestValidationService
{
    private readonly IUserRepository _userRepository;
    private readonly IPasswordHasher _passwordHasher;

    public MyAuthService(IUserRepository userRepository, IPasswordHasher passwordHasher)
    {
        _userRepository = userRepository;
        _passwordHasher = passwordHasher;
    }

    public async Task<AuthResponse?> ValidateApiKeyRequestAsync(ApiKeyAuthRequest request, IJwtTokenService jwtTokenService, HttpContext? httpContext = null)
    {
        // Validate API key (e.g., check database, external service, etc.)
        var user = await _userRepository.GetByApiKeyAsync(request.ApiKey);
        if (user == null) return null;

        // Extract tenant ID from header if available
        string? tenantId = user.TenantId;
        if (httpContext?.Request.Headers.TryGetValue("X-Tenant-ID", out var headerTenantId) == true)
        {
            tenantId = headerTenantId.ToString();
        }

        // Create JWT token
        DateTime expiresAt = DateTime.UtcNow.AddHours(1);
        string token = jwtTokenService.CreateToken(
            subject: user.Id,
            authType: "apikey",
            additionalClaims: new[] { new Claim("tenant_id", tenantId) },
            roles: user.Roles.ToArray(),
            expiresAt: expiresAt);

        return new AuthResponse(token, expiresAt.ToString("o"));
    }

    public async Task<AuthResponse?> ValidateLoginRequestAsync(LoginAuthRequest request, IJwtTokenService jwtTokenService, HttpContext? httpContext = null)
    {
        // Validate username/password
        var user = await _userRepository.GetByUsernameAsync(request.Username);
        if (user == null) return null;

        // Validate password using username as additional salt
        if (!_passwordHasher.ValidatePassword(request.Password, user.PasswordHash, request.Username))
            return null;

        // Extract tenant ID from header if available
        string? tenantId = user.TenantId;
        if (httpContext?.Request.Headers.TryGetValue("X-Tenant-ID", out var headerTenantId) == true)
        {
            tenantId = headerTenantId.ToString();
        }

        // Create JWT token
        DateTime expiresAt = DateTime.UtcNow.AddHours(1);
        string token = jwtTokenService.CreateToken(
            subject: user.Id,
            authType: "user",
            additionalClaims: new[] { new Claim("tenant_id", tenantId) },
            roles: user.Roles.ToArray(),
            expiresAt: expiresAt);

        return new AuthResponse(token, expiresAt.ToString("o"));
    }
}

Then register the service and add endpoints in Program.cs. Here's a complete setup example:

string jwtSecret = Environment.GetEnvironmentVariable("JWT_SIGNING_SECRET")!;

// 1. Register authentication
builder.Services.AddEasyReasyAuth(jwtSecret, issuer: "my-issuer");

// 2. Register dependencies
builder.Services.AddScoped<IUserRepository, UserRepository>();
builder.Services.AddSingleton<IPasswordHasher, SecurePasswordHasher>();

// 3. Register validation service (use AddScoped for services with database dependencies)
builder.Services.AddScoped<IAuthRequestValidationService, MyAuthService>();

var app = builder.Build();

// 4. Configure middleware (UseEasyReasyAuth includes UseAuthentication/UseAuthorization)
app.UseEasyReasyAuth();

// 5. Add auth endpoints (resolved from DI automatically per-request)
app.AddAuthEndpoints(
    allowApiKeys: true,
    allowUsernamePassword: true);

app.MapControllers();

Note: IAuthRequestValidationService is resolved from DI per-request when auth endpoints are called. Use AddScoped when your validation service has database dependencies.

This will automatically create:

  • POST /api/auth/apikey - For API key authentication
  • POST /api/auth/login - For username/password authentication

Both endpoints return:

  • 200 OK with AuthResponse (token + expiration) on success
  • 401 Unauthorized on invalid credentials

4. Accessing HTTP Context in Validation

The IAuthRequestValidationService methods receive an optional HttpContext parameter, allowing you to access request headers, query parameters, and other HTTP context information during authentication. This is particularly useful for multi-tenant applications.

Example: Extracting Tenant ID from Headers

public async Task<AuthResponse?> ValidateApiKeyRequestAsync(ApiKeyAuthRequest request, IJwtTokenService jwtTokenService, HttpContext? httpContext = null)
{
    // Validate API key
    var user = await _userRepository.GetByApiKeyAsync(request.ApiKey);
    if (user == null) return null;

    // Extract tenant ID from header
    string? tenantId = null;
    if (httpContext?.Request.Headers.TryGetValue("X-Tenant-ID", out var headerTenantId) == true)
    {
        tenantId = headerTenantId.ToString();
    }

    // Create JWT token with tenant information
    DateTime expiresAt = DateTime.UtcNow.AddHours(1);
    string token = jwtTokenService.CreateToken(
        subject: user.Id,
        authType: "apikey",
        additionalClaims: new[] { new Claim("tenant_id", tenantId) },
        roles: user.Roles.ToArray(),
        expiresAt: expiresAt);

    return new AuthResponse(token, expiresAt.ToString("o"));
}

Example: Accessing Query Parameters

public async Task<AuthResponse?> ValidateLoginRequestAsync(LoginAuthRequest request, IJwtTokenService jwtTokenService, HttpContext? httpContext = null)
{
    // Validate credentials
    var user = await _userRepository.GetByUsernameAsync(request.Username);
    if (user == null || !VerifyPassword(request.Password, user.PasswordHash)) 
        return null;

    // Extract organization from query parameter
    string? organization = httpContext?.Request.Query["org"].ToString();

    // Create JWT token with organization information
    DateTime expiresAt = DateTime.UtcNow.AddHours(1);
    string token = jwtTokenService.CreateToken(
        subject: user.Id,
        authType: "user",
        additionalClaims: new[] 
        { 
            new Claim("tenant_id", user.TenantId),
            new Claim("organization", organization)
        },
        roles: user.Roles.ToArray(),
        expiresAt: expiresAt);

    return new AuthResponse(token, expiresAt.ToString("o"));
}

Note: The HttpContext parameter is optional and defaults to null, so implementations that don't need HTTP context can simply omit it.

5. Access claims and roles in controllers

string? userId = HttpContext.GetUserId();
string? tenantId = HttpContext.GetTenantId();
IEnumerable<string> roles = HttpContext.GetRoles();
string? email = HttpContext.GetClaimValue("email");

// Type-safe claim access using the EasyReasyClaim enum
string? userId2 = HttpContext.GetClaimValue(EasyReasyClaim.UserId);
string? tenantId2 = HttpContext.GetClaimValue(EasyReasyClaim.TenantId);
string? issuer = HttpContext.GetClaimValue(EasyReasyClaim.Issuer);

5. Password Hashing

The library includes a secure password hasher using PBKDF2 with HMAC-SHA512. The IPasswordHasher interface provides these methods:

public interface IPasswordHasher
{
    // Hash password with username as additional salt
    string HashPassword(string password, string username);
    
    // Validate password with username as additional salt
    bool ValidatePassword(string password, string passwordHash, string username);
}

Use it in your IAuthRequestValidationService implementation (see the main example above for a complete implementation with constructor injection). Register the password hasher in Program.cs:

builder.Services.AddSingleton<IPasswordHasher, SecurePasswordHasher>();

Key Features:

  • Uses PBKDF2 with HMAC-SHA512 and 100,000 iterations
  • Username as additional salt for extra security
  • Versioned hash format for future compatibility
  • Constant-time comparison to prevent timing attacks

6. Refresh Tokens

EasyReasy.Auth supports refresh token rotation with automatic theft detection via token family tracking. The library is database-agnostic — you implement IRefreshTokenStore to persist tokens however you like.

Setup
  1. Implement IRefreshTokenStore to connect to your database:
public class MyRefreshTokenStore : IRefreshTokenStore
{
    public Task StoreAsync(StoredRefreshToken refreshToken) { /* INSERT into DB */ }
    public Task<StoredRefreshToken?> GetByTokenHashAsync(string tokenHash) { /* SELECT by hash */ }
    public Task MarkAsConsumedAsync(string tokenHash, DateTime consumedAt) { /* UPDATE consumed_at */ }
    public Task InvalidateFamilyAsync(string familyId) { /* UPDATE SET invalidated WHERE family_id = ... */ }
}
  1. Register in Program.cs:
builder.Services.AddRefreshTokenService<MyRefreshTokenStore>(
    refreshTokenLifetime: TimeSpan.FromDays(30),  // default
    accessTokenLifetime: TimeSpan.FromHours(1));   // default

// Enable the refresh endpoint alongside your auth endpoints
app.AddAuthEndpoints(allowRefresh: true);
// Or standalone: app.AddRefreshEndpoint();

This creates POST /api/auth/refresh which accepts { "refreshToken": "..." } and returns a new access + refresh token pair.

  1. Issue refresh tokens in your validation service by injecting IRefreshTokenService:
public class MyAuthService : IAuthRequestValidationService
{
    private readonly IRefreshTokenService _refreshTokenService;

    public MyAuthService(IRefreshTokenService refreshTokenService)
    {
        _refreshTokenService = refreshTokenService;
    }

    public async Task<AuthResponse?> ValidateLoginRequestAsync(
        LoginAuthRequest request, IJwtTokenService jwtTokenService, HttpContext? httpContext = null)
    {
        // ... validate credentials, create access token ...

        string refreshToken = await _refreshTokenService.CreateRefreshTokenAsync(
            subject: user.Id,
            authType: "user",
            serializedClaims: RefreshTokenService.SerializeClaims(claims),
            serializedRoles: RefreshTokenService.SerializeRoles(roles));

        return new AuthResponse(token, expiresAt.ToString("o"), refreshToken);
    }
}
How It Works
  • Refresh tokens are 32-byte cryptographic random strings, stored as SHA-256 hashes
  • Each token belongs to a family — when a token is used, a new one is issued in the same family (rotation)
  • If a token that was already used gets presented again, the library detects theft and invalidates the entire family
  • Everything is opt-in: you must register the service, enable the endpoint, and inject IRefreshTokenService in your validation service

Advanced Configuration

Service Registration Options

Service Registration:

  • Register IAuthRequestValidationService using standard DI. The library resolves it from DI per-request when auth endpoints are called.
    builder.Services.AddScoped<IAuthRequestValidationService, MyAuthService>();
    
  • Service Lifetime: Use AddScoped when your validation service has database dependencies (e.g., Entity Framework DbContext). Use AddSingleton only if the service is stateless and thread-safe.

Opting Out of Automatic Service Registration

If you need more control over the IJwtTokenService registration (e.g., for testing, custom implementations, or multiple configurations), you can opt out of automatic registration:

// Register authentication without automatic IJwtTokenService registration
builder.Services.AddEasyReasyAuth(jwtSecret, issuer: "my-issuer", registerJwtTokenService: false);

// Manually register your own implementation
builder.Services.AddSingleton<IJwtTokenService>(new MyCustomJwtTokenService(jwtSecret, issuer));

This is useful for:

  • Testing scenarios: Mock the service in unit tests
  • Custom implementations: Use your own IJwtTokenService implementation
  • Multiple configurations: Register different JWT services for different purposes
  • Performance optimization: Control the service lifetime (singleton vs scoped vs transient)

Progressive Delay Middleware

The progressive delay middleware helps protect your API from brute-force attacks by introducing a delay for repeated unauthorized requests from the same IP address.

  • How it works:
    • The first 10 failed (401 Unauthorized) requests from an IP have no delay.
    • After that, each additional failed request adds a 500ms delay (e.g., 11th failure = 500ms, 12th = 1000ms, etc.).
    • The delay is reset after a successful (non-401) request.
  • Enabled by default:
    • The middleware is included automatically when you call app.UseEasyReasyAuth().
  • How to disable:
    • Pass enableProgressiveDelay: false to UseEasyReasyAuth:
      app.UseEasyReasyAuth(enableProgressiveDelay: false);
      
  • How to enable:
    • Omit the parameter or set it to true (default):
      app.UseEasyReasyAuth(); // or app.UseEasyReasyAuth(enableProgressiveDelay: true);
      

Core Features

  • JWT token service: Issue tokens with custom claims, roles, and optional issuer
  • Automatic auth endpoints: Create API key and username/password authentication endpoints with minimal code
  • Flexible validation: Implement IAuthRequestValidationService to handle any authentication logic (database, external APIs, etc.)
  • Refresh token rotation: Opt-in refresh tokens with token family tracking and automatic theft detection
  • Secure password hashing: PBKDF2 with HMAC-SHA512, username-based salt, and constant-time comparison
  • Claims injection middleware: Makes user/tenant IDs available in HttpContext.Items
  • Role access: Retrieve all roles for the current user via GetRoles()
  • Claim access: Retrieve any claim value by key or enum via GetClaimValue()
  • Progressive delay middleware: Slows repeated unauthorized requests from the same IP (first 10 have no delay, then 500ms per failure)
  • Configurable issuer validation: Pass issuer: null to disable
  • Secret length enforcement: Secret must be at least 32 bytes (256 bits) for HS256
  • Async support: All validation methods are async for database lookups and external API calls

Error Handling

  • If the JWT secret is too short, JwtTokenService throws an ArgumentException with a clear message.
  • Progressive delay is enabled by default; opt out with app.UseEasyReasyAuth(enableProgressiveDelay: false);

Best Practices

  1. Use a strong, unique secret: At least 32 bytes (256 bits)
  2. Set issuer for extra validation: Use the same value when issuing and validating tokens
  3. Enable progressive delay: Protects against brute-force attacks by default
  4. Access claims and roles via extension methods: Use GetUserId(), GetTenantId(), GetRoles(), and GetClaimValue() for convenience

For more details, see XML comments in the code or explore the source. This library is designed to be easy to use and secure enough for most uses cases by default.


⚠️ Security Disclaimer: This library implements a simple token-based authentication system suitable for internal or low-risk applications. It does not follow full OAuth2/OIDC standards and lacks advanced features like key rotation, token introspection, consent management, and third-party identity federation. For production systems with complex threat models, consider using a mature identity provider such as IdentityServer, OpenIddict, or a cloud-based solution.

Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
2.0.0 67 2/18/2026
1.4.0 48 2/18/2026
1.3.1 688 9/11/2025
1.3.0 272 8/7/2025
1.2.0 139 8/3/2025
1.1.0 153 8/3/2025
1.0.0 184 7/30/2025