Skip to main content
The Intent.Security.JWT module configures JWT (JSON Web Token) Bearer authentication for your ASP.NET Core Web APIs, providing a complete token-based security infrastructure.

Overview

This module sets up JWT Bearer authentication middleware, configures token validation parameters, and integrates with the application’s security infrastructure. It provides the foundation for stateless, token-based API authentication.

What Gets Generated

JWT Configuration

Configures JWT Bearer authentication in your application:
public static class JwtConfiguration
{
    public static IServiceCollection AddJwtAuthentication(
        this IServiceCollection services,
        IConfiguration configuration)
    {
        var jwtSettings = configuration.GetSection("JwtSettings").Get<JwtSettings>();

        services.AddAuthentication(options =>
        {
            options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
            options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
        })
        .AddJwtBearer(options =>
        {
            options.RequireHttpsMetadata = true;
            options.SaveToken = true;
            options.TokenValidationParameters = new TokenValidationParameters
            {
                ValidateIssuer = true,
                ValidateAudience = true,
                ValidateLifetime = true,
                ValidateIssuerSigningKey = true,
                ValidIssuer = jwtSettings.Issuer,
                ValidAudience = jwtSettings.Audience,
                IssuerSigningKey = new SymmetricSecurityKey(
                    Encoding.UTF8.GetBytes(jwtSettings.Secret)),
                ClockSkew = TimeSpan.Zero
            };

            options.Events = new JwtBearerEvents
            {
                OnAuthenticationFailed = context =>
                {
                    if (context.Exception is SecurityTokenExpiredException)
                    {
                        context.Response.Headers.Add("Token-Expired", "true");
                    }
                    return Task.CompletedTask;
                },
                OnChallenge = context =>
                {
                    context.HandleResponse();
                    context.Response.StatusCode = StatusCodes.Status401Unauthorized;
                    context.Response.ContentType = "application/json";
                    var result = JsonSerializer.Serialize(new
                    {
                        error = "You are not authorized"
                    });
                    return context.Response.WriteAsync(result);
                }
            };
        });

        return services;
    }
}

Current User Service

Provides access to the authenticated user:
public interface ICurrentUserService
{
    Guid? UserId { get; }
    string? UserName { get; }
    string? Email { get; }
    bool IsAuthenticated { get; }
    bool IsInRole(string role);
    bool HasClaim(string claimType, string claimValue);
}

public class CurrentUserService : ICurrentUserService
{
    private readonly IHttpContextAccessor _httpContextAccessor;

    public CurrentUserService(IHttpContextAccessor httpContextAccessor)
    {
        _httpContextAccessor = httpContextAccessor;
    }

    public Guid? UserId
    {
        get
        {
            var userIdClaim = _httpContextAccessor.HttpContext?.User
                .FindFirstValue(ClaimTypes.NameIdentifier);
            return Guid.TryParse(userIdClaim, out var userId) ? userId : null;
        }
    }

    public string? UserName => 
        _httpContextAccessor.HttpContext?.User?.Identity?.Name;

    public string? Email => 
        _httpContextAccessor.HttpContext?.User
            ?.FindFirstValue(ClaimTypes.Email);

    public bool IsAuthenticated => 
        _httpContextAccessor.HttpContext?.User?.Identity?.IsAuthenticated ?? false;

    public bool IsInRole(string role) =>
        _httpContextAccessor.HttpContext?.User?.IsInRole(role) ?? false;

    public bool HasClaim(string claimType, string claimValue) =>
        _httpContextAccessor.HttpContext?.User
            ?.HasClaim(claimType, claimValue) ?? false;
}

Key Features

Bearer Authentication

Standard JWT Bearer token validation

Claims-Based

Access user identity and claims

Stateless

No server-side session storage required

Flexible Authorization

Support for roles, policies, and claims

Module Settings

JWT Bearer Authentication Type

JWT Bearer Authentication Type
select
default:"oidc"
Configure how JWT tokens are authenticatedOptions:
  • oidc - OpenID Connect JWT authentication (recommended)
  • manual - Manual JWT authentication with custom validation

Configuration

appsettings.json

Configure JWT settings:
{
  "JwtSettings": {
    "Secret": "your-256-bit-secret-key-here-minimum-32-characters",
    "Issuer": "https://yourdomain.com",
    "Audience": "https://yourdomain.com",
    "ExpiryInMinutes": 60
  }
}

Environment Variables

For production, use environment variables or Azure Key Vault:
export JwtSettings__Secret="production-secret-key"
export JwtSettings__Issuer="https://api.production.com"
export JwtSettings__Audience="https://api.production.com"

Authorization Strategies

Role-Based Authorization

Authorize based on user roles:
[Authorize(Roles = "Admin")]
[HttpDelete("{id}")]
public async Task<ActionResult> Delete(
    [FromRoute] Guid id,
    CancellationToken cancellationToken)
{
    await _service.DeleteAsync(id, cancellationToken);
    return NoContent();
}

// Multiple roles (OR logic)
[Authorize(Roles = "Admin,Manager")]
[HttpGet("reports")]
public async Task<ActionResult<List<ReportDto>>> GetReports()
{
    // User must be in Admin OR Manager role
}

Policy-Based Authorization

Define custom authorization policies:
// In Program.cs or Startup
services.AddAuthorization(options =>
{
    options.AddPolicy("RequireAdminRole", policy =>
        policy.RequireRole("Admin"));

    options.AddPolicy("RequireManagerOrHigher", policy =>
        policy.RequireRole("Admin", "Manager"));

    options.AddPolicy("RequireEditPermission", policy =>
        policy.RequireClaim("Permission", "Edit"));

    options.AddPolicy("RequireAgeOver18", policy =>
        policy.Requirements.Add(new MinimumAgeRequirement(18)));

    options.AddPolicy("RequireAdminAndVerified", policy =>
    {
        policy.RequireRole("Admin");
        policy.RequireClaim("Verified", "true");
    });
});
Use policies in controllers:
[Authorize(Policy = "RequireEditPermission")]
[HttpPut("{id}")]
public async Task<ActionResult> Update(
    [FromRoute] Guid id,
    [FromBody] UpdateDto dto,
    CancellationToken cancellationToken)
{
    await _service.UpdateAsync(id, dto, cancellationToken);
    return NoContent();
}

Custom Authorization Handlers

Implement custom authorization logic:
public class MinimumAgeRequirement : IAuthorizationRequirement
{
    public int MinimumAge { get; }

    public MinimumAgeRequirement(int minimumAge)
    {
        MinimumAge = minimumAge;
    }
}

public class MinimumAgeHandler : AuthorizationHandler<MinimumAgeRequirement>
{
    protected override Task HandleRequirementAsync(
        AuthorizationHandlerContext context,
        MinimumAgeRequirement requirement)
    {
        var dateOfBirthClaim = context.User.FindFirst(
            c => c.Type == "DateOfBirth");

        if (dateOfBirthClaim == null)
        {
            return Task.CompletedTask;
        }

        var dateOfBirth = DateTime.Parse(dateOfBirthClaim.Value);
        var age = DateTime.Today.Year - dateOfBirth.Year;

        if (dateOfBirth.Date > DateTime.Today.AddYears(-age))
        {
            age--;
        }

        if (age >= requirement.MinimumAge)
        {
            context.Succeed(requirement);
        }

        return Task.CompletedTask;
    }
}

// Register the handler
services.AddScoped<IAuthorizationHandler, MinimumAgeHandler>();

Claims-Based Authorization

Authorize based on specific claims:
[Authorize]
[HttpGet("my-data")]
public async Task<ActionResult<UserDataDto>> GetMyData()
{
    var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
    var email = User.FindFirstValue(ClaimTypes.Email);
    var roles = User.FindAll(ClaimTypes.Role).Select(c => c.Value).ToList();

    // Custom claims
    var department = User.FindFirstValue("Department");
    var employeeId = User.FindFirstValue("EmployeeId");

    return Ok(new UserDataDto
    {
        UserId = Guid.Parse(userId),
        Email = email,
        Roles = roles,
        Department = department,
        EmployeeId = employeeId
    });
}

Token Validation

The module configures comprehensive token validation:

Validation Parameters

TokenValidationParameters = new TokenValidationParameters
{
    ValidateIssuer = true,              // Validate token issuer
    ValidateAudience = true,            // Validate token audience
    ValidateLifetime = true,            // Check token expiration
    ValidateIssuerSigningKey = true,    // Validate signing key
    ValidIssuer = jwtSettings.Issuer,
    ValidAudience = jwtSettings.Audience,
    IssuerSigningKey = new SymmetricSecurityKey(
        Encoding.UTF8.GetBytes(jwtSettings.Secret)),
    ClockSkew = TimeSpan.Zero           // No tolerance for expired tokens
};

Custom Validation

Add custom token validation:
options.Events = new JwtBearerEvents
{
    OnTokenValidated = context =>
    {
        var userService = context.HttpContext.RequestServices
            .GetRequiredService<IUserService>();
        
        var userId = context.Principal.FindFirstValue(ClaimTypes.NameIdentifier);
        
        // Check if user still exists and is active
        var user = userService.GetByIdAsync(Guid.Parse(userId)).Result;
        if (user == null || !user.IsActive)
        {
            context.Fail("User is no longer active");
        }

        return Task.CompletedTask;
    },
    OnAuthenticationFailed = context =>
    {
        if (context.Exception is SecurityTokenExpiredException)
        {
            context.Response.Headers.Add("Token-Expired", "true");
        }
        return Task.CompletedTask;
    }
};

Protecting Endpoints

Controller Level

Protect entire controller:
[Authorize]
[ApiController]
[Route("api/orders")]
public class OrdersController : ControllerBase
{
    // All endpoints require authentication
    
    [AllowAnonymous] // Except this one
    [HttpGet("public")]
    public ActionResult<List<OrderDto>> GetPublicOrders()
    {
        // Public endpoint
    }
}

Action Level

Protect specific actions:
[ApiController]
[Route("api/products")]
public class ProductsController : ControllerBase
{
    [HttpGet] // Public
    public ActionResult<List<ProductDto>> GetAll()
    {
        // Anyone can view products
    }

    [Authorize] // Protected
    [HttpPost]
    public ActionResult<Guid> Create([FromBody] CreateProductDto dto)
    {
        // Requires authentication
    }

    [Authorize(Roles = "Admin")] // Admin only
    [HttpDelete("{id}")]
    public ActionResult Delete([FromRoute] Guid id)
    {
        // Only admins can delete
    }
}

Using Current User Service

Access current user information in your services:
public class OrderService : IOrderService
{
    private readonly IApplicationDbContext _dbContext;
    private readonly ICurrentUserService _currentUserService;

    public OrderService(
        IApplicationDbContext dbContext,
        ICurrentUserService currentUserService)
    {
        _dbContext = dbContext;
        _currentUserService = currentUserService;
    }

    public async Task<Guid> CreateOrderAsync(
        CreateOrderDto dto,
        CancellationToken cancellationToken)
    {
        if (!_currentUserService.IsAuthenticated)
            throw new UnauthorizedException("User must be authenticated");

        var order = new Order
        {
            CustomerId = _currentUserService.UserId.Value,
            CustomerEmail = _currentUserService.Email,
            CreatedBy = _currentUserService.UserName,
            // ... other properties
        };

        _dbContext.Orders.Add(order);
        await _dbContext.SaveChangesAsync(cancellationToken);

        return order.Id;
    }

    public async Task<List<OrderDto>> GetMyOrdersAsync(
        CancellationToken cancellationToken)
    {
        var userId = _currentUserService.UserId.Value;
        
        return await _dbContext.Orders
            .Where(x => x.CustomerId == userId)
            .ProjectToListAsync<OrderDto>(_mapper.ConfigurationProvider, cancellationToken);
    }
}

Error Handling

Unauthorized (401)

Returned when:
  • No token provided
  • Invalid token
  • Expired token
  • Token signature doesn’t match
options.Events = new JwtBearerEvents
{
    OnChallenge = context =>
    {
        context.HandleResponse();
        context.Response.StatusCode = 401;
        context.Response.ContentType = "application/json";
        
        var result = JsonSerializer.Serialize(new
        {
            error = "Unauthorized",
            message = "You must be authenticated to access this resource"
        });
        
        return context.Response.WriteAsync(result);
    }
};

Forbidden (403)

Returned when:
  • User is authenticated but lacks required permissions
  • Missing required role
  • Authorization policy not met
services.AddAuthorization(options =>
{
    options.FallbackPolicy = new AuthorizationPolicyBuilder()
        .RequireAuthenticatedUser()
        .Build();
});

Security Best Practices

  • Use a strong secret key (minimum 256 bits / 32 characters)
  • Store secrets in Azure Key Vault or AWS Secrets Manager
  • Never commit secrets to source control
  • Use different secrets for each environment
  • Rotate keys periodically
  • Keep access tokens short-lived (15-60 minutes)
  • Use refresh tokens for longer sessions
  • Set ClockSkew to zero for strict expiration
  • Implement token revocation for critical operations
  • Always use HTTPS in production
  • Set RequireHttpsMetadata to true
  • Enable HSTS (HTTP Strict Transport Security)
  • Use secure cookie flags
  • Validate all claims on the server
  • Don’t trust client-provided data
  • Implement additional validation in TokenValidated event
  • Check user status (active/disabled) on sensitive operations

OpenID Connect (OIDC) Authentication

When using OIDC mode:
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =>
    {
        options.Authority = "https://your-identity-provider.com";
        options.Audience = "your-api-audience";
        options.RequireHttpsMetadata = true;
        
        options.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuer = true,
            ValidateAudience = true,
            ValidateLifetime = true,
            ValidateIssuerSigningKey = true
        };
    });

Installation

Intent.Security.JWT

Dependencies

  • Intent.Application.Identity (>= 3.6.0)
  • Intent.AspNetCore (>= 6.0.3)
  • Intent.Common.CSharp
  • Intent.OutputManager.RoslynWeaver

Integration

Automatically integrates with:
  • Intent.IdentityServer4.SecureTokenServer - For IdentityServer4
  • Intent.AspNetCore.Swashbuckle - Adds auth UI to Swagger
  • Intent.AspNetCore.Identity - For user management

Next Steps

Identity

Add user management with ASP.NET Core Identity

Identity JWT

Combine Identity with JWT authentication

Swashbuckle

Add JWT auth to Swagger UI

Controllers

Protect your API endpoints

Build docs developers (and LLMs) love