On this page
Authentication and authorization with JWT in ASP.NET Core
Authentication vs Authorization
- Authentication — Who are you? (Is the token valid? Does the user exist?)
- Authorization — What can you do? (Do you have the required role/permission?)
In ASP.NET Core, both are middleware: UseAuthentication identifies the user, UseAuthorization verifies their permissions.
JWT (JSON Web Token)
A JWT is a signed token with three parts:
Header.Payload.Signature
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 ← Header (alg, type)
.eyJzdWIiOiIxMjM0NTY3ODkwIn0 ← Payload (claims)
.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c ← SignatureThe server signs the token with a secret key. The client includes the token in each request:
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...Configure JWT in appsettings
{
"Jwt": {
"Key": "superSecretKeyThatShouldBeAtLeast32CharactersLong!",
"Issuer": "https://api.myapp.com",
"Audience": "https://myapp.com",
"ExpirationHours": 8
}
}Login endpoint
[ApiController]
[Route("api/[controller]")]
public class AuthController : ControllerBase
{
private readonly IUserService _users;
private readonly TokenService _tokens;
public AuthController(IUserService users, TokenService tokens)
{
_users = users;
_tokens = tokens;
}
[HttpPost("login")]
public async Task<ActionResult<LoginResponse>> Login([FromBody] LoginRequest req)
{
// 1. Verify credentials
var user = await _users.VerifyCredentialsAsync(req.Email, req.Password);
if (user is null)
return Unauthorized(new { message = "Invalid credentials" });
// 2. Generate JWT token
string token = _tokens.GenerateToken(user.Id, user.Email, user.Role);
return Ok(new LoginResponse(
Token: token,
ExpiresAt: DateTime.UtcNow.AddHours(8),
User: new UserDto(user.Id, user.Email, user.Name)
));
}
[HttpPost("register")]
public async Task<ActionResult<UserDto>> Register([FromBody] RegisterRequest req)
{
if (await _users.EmailExistsAsync(req.Email))
return Conflict(new { message = "Email already registered" });
var user = await _users.CreateAsync(req);
return CreatedAtAction(nameof(Login), new UserDto(user.Id, user.Email, user.Name));
}
}
record LoginRequest(string Email, string Password);
record LoginResponse(string Token, DateTime ExpiresAt, UserDto User);
record RegisterRequest(string Name, string Email, string Password);
record UserDto(int Id, string Email, string Name);[Authorize] and its variants
// Requires authentication (any valid user)
[Authorize]
public IActionResult MyProfile() { ... }
// Requires a specific role
[Authorize(Roles = "Admin")]
public IActionResult AdminPanel() { ... }
// Requires one of several roles
[Authorize(Roles = "Admin,Manager")]
public IActionResult TeamManagement() { ... }
// Requires a policy
[Authorize(Policy = "ExecutivesOnly")]
public IActionResult ExecutiveReport() { ... }
// Allows access without authentication (overrides controller-level [Authorize])
[AllowAnonymous]
public IActionResult PublicWelcome() { ... }Authorization policies
For complex authorization logic:
// Register policies
builder.Services.AddAuthorization(opts =>
{
// Claim-based policy
opts.AddPolicy("HasPremiumSubscription", p =>
p.RequireClaim("subscription", "premium", "enterprise"));
// Policy based on multiple requirements
opts.AddPolicy("CanPublishContent", p =>
{
p.RequireAuthenticatedUser();
p.RequireRole("Editor", "Admin");
p.RequireClaim("email_verified", "true");
});
// Custom policy with IAuthorizationRequirement
opts.AddPolicy("OwnerOrAdmin", p =>
p.AddRequirements(new OwnerOrAdminRequirement()));
});
// Custom authorization requirement
public class OwnerOrAdminRequirement : IAuthorizationRequirement { }
public class OwnerOrAdminHandler
: AuthorizationHandler<OwnerOrAdminRequirement>
{
protected override Task HandleRequirementAsync(
AuthorizationHandlerContext context,
OwnerOrAdminRequirement requirement)
{
var userId = context.User.FindFirstValue(ClaimTypes.NameIdentifier);
var isAdmin = context.User.IsInRole("Admin");
if (isAdmin /* || owner logic */)
context.Succeed(requirement);
return Task.CompletedTask;
}
}Reading user claims
[HttpGet("profile")]
[Authorize]
public IActionResult GetProfile()
{
// Access JWT token claims
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
var email = User.FindFirstValue(ClaimTypes.Email);
var role = User.FindFirstValue(ClaimTypes.Role);
var jti = User.FindFirstValue(JwtRegisteredClaimNames.Jti);
// Check role
bool isAdmin = User.IsInRole("Admin");
// Check custom claim
bool isPremium = User.HasClaim("subscription", "premium");
return Ok(new { userId, email, role, isAdmin, isPremium });
}Refresh Tokens (secure pattern)
// Short-lived JWTs + refresh tokens are more secure
public class RefreshTokenService
{
private readonly Dictionary<string, RefreshToken> _tokens = new();
public string Create(int userId)
{
var token = new RefreshToken
{
Token = Guid.NewGuid().ToString("N") + Guid.NewGuid().ToString("N"),
UserId = userId,
ExpiresAt = DateTime.UtcNow.AddDays(30),
CreatedAt = DateTime.UtcNow
};
_tokens[token.Token] = token;
return token.Token;
}
public RefreshToken? Validate(string token)
{
if (!_tokens.TryGetValue(token, out var rt)) return null;
if (rt.ExpiresAt < DateTime.UtcNow || rt.Revoked) return null;
return rt;
}
public void Revoke(string token)
{
if (_tokens.TryGetValue(token, out var rt))
rt.Revoked = true;
}
}
public class RefreshToken
{
public string Token { get; set; } = string.Empty;
public int UserId { get; set; }
public DateTime ExpiresAt { get; set; }
public DateTime CreatedAt { get; set; }
public bool Revoked { get; set; }
}Practice
- Basic login: Implement an
AuthControllerwithPOST /loginthat verifies hardcoded credentials and returns a JWT valid for 1 hour. - Protected endpoint: Create
GET /api/privatewith[Authorize]andGET /api/adminwith[Authorize(Roles = "Admin")]. Test with Swagger by sending the token. - Custom policy: Define a
"WithVerifiedEmail"policy that requires theemail_verifiedclaim to be"true". Apply it to an endpoint and test it.
In the next lesson we will learn how to create custom middleware and action filters to add cross-cutting behavior to the API.
Store Jwt:Key in environment variables
Never store the JWT secret in appsettings.json as plain text for production. Use environment variables (ASPNETCORE_Jwt__Key=...), Azure Key Vault, AWS Secrets Manager, or any secrets manager. The key must be at least 32 characters for HMAC-SHA256.
Claims as user data in the token
Claims are key-value pairs inside the JWT that identify the user. Include only the necessary information (ID, email, role) because the token is sent on every request. For sensitive data that changes frequently (such as permissions), validate them against the database on each request.
UseAuthentication BEFORE UseAuthorization
Middleware order is critical: app.UseAuthentication() MUST come before app.UseAuthorization(). If you reverse them, authorization will not know who the user is and will reject all requests with 401 even when the token is valid.
// Program.cs — complete JWT configuration
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using System.Text;
var builder = WebApplication.CreateBuilder(args);
// ── JWT Authentication ────────────────────────────────
var jwtKey = builder.Configuration["Jwt:Key"]
?? throw new InvalidOperationException("Jwt:Key not configured");
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(opts =>
{
opts.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(jwtKey)),
ValidateIssuer = true,
ValidIssuer = builder.Configuration["Jwt:Issuer"],
ValidateAudience = true,
ValidAudience = builder.Configuration["Jwt:Audience"],
ValidateLifetime = true,
ClockSkew = TimeSpan.Zero, // no extra margin
};
});
// ── Authorization with policies ───────────────────────
builder.Services.AddAuthorization(opts =>
{
opts.AddPolicy("AdminOnly",
p => p.RequireRole("Admin"));
opts.AddPolicy("EditorOrAdmin",
p => p.RequireRole("Editor", "Admin"));
opts.AddPolicy("PremiumUser",
p => p.RequireClaim("subscription", "premium"));
});
var app = builder.Build();
app.UseAuthentication(); // BEFORE Authorization
app.UseAuthorization();
Sign in to track your progress