
Table of Contents
Introduction to JWT Authentication
JWT Structure & Security Fundamentals
Setting Up JWT Authentication
User Registration & Login System
Token Generation & Validation
Claims & Role-Based Authorization
Refresh Token Strategies
Password Security & Hashing
Advanced Security Middleware
Two-Factor Authentication (2FA)
Rate Limiting & Brute Force Protection
Token Blacklisting & Revocation
API Security Best Practices
Real-World E-Commerce Implementation
Testing Security Implementation
Production Deployment & Hardening
Monitoring & Security Auditing
Common Vulnerabilities & Protection
Performance & Scalability
Future Security Trends
1. Introduction to JWT Authentication {#introduction}
JWT (JSON Web Token) has become the industry standard for securing modern web applications and APIs. Unlike traditional session-based authentication, JWT provides a stateless, scalable approach to user authentication and authorization.
Why JWT is Essential for Modern Applications
public class JWTBenefits
{
// Stateless - No server-side sessions
public class StatelessExample
{
// Traditional Sessions: Server stores session data
// JWT: All data stored in token, verified by signature
}
// Scalability - Easy to scale horizontally
public class ScalabilityExample
{
// No session affinity required
// Any server can validate tokens
}
// Mobile & SPA Friendly
public class MobileFriendlyExample
{
// Works seamlessly with mobile apps
// Perfect for Single Page Applications
}
// Microservices Ready
public class MicroservicesExample
{
// Tokens can be passed between services
// Centralized authentication
}
}
2. JWT Structure & Security Fundamentals {#jwt-fundamentals}
Understanding JWT Components
public class JWTStructure
{
// JWT has three parts: Header.Payload.Signature
public class TokenComponents
{
// Header: Algorithm & token type
// Payload: Claims (user data)
// Signature: Verification
}
// Example JWT Token
public class ExampleToken
{
// Header
/*
{
"alg": "HS256",
"typ": "JWT"
}
*/
// Payload
/*
{
"sub": "1234567890",
"name": "John Doe",
"iat": 1516239022,
"exp": 1516242622,
"roles": ["User", "Admin"]
}
*/
// Signature
// HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)
}
}
3. Setting Up JWT Authentication {#setup-jwt}
Complete Project Structure
JWTAuthDemo/
├── Models/
├── Services/
├── Controllers/
├── Middleware/
├── Helpers/
├── Program.cs
└── appsettings.json
Program.cs - Complete JWT Setup
using System.Text;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using Microsoft.EntityFrameworkCore;
using JWTAuthDemo.Data;
using JWTAuthDemo.Services;
using JWTAuthDemo.Middleware;
var builder = WebApplication.CreateBuilder(args);
// Add Database Context
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));
// Configure JWT Settings
var jwtSettings = builder.Configuration.GetSection("JwtSettings");
var secretKey = Encoding.ASCII.GetBytes(jwtSettings["SecretKey"]!);
// Add Authentication
builder.Services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
options.RequireHttpsMetadata = false; // Set to true in production
options.SaveToken = true;
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(secretKey),
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidIssuer = jwtSettings["Issuer"],
ValidAudience = jwtSettings["Audience"],
ClockSkew = TimeSpan.Zero // Remove delay of token when expire
};
// Events for custom token handling
options.Events = new JwtBearerEvents
{
OnAuthenticationFailed = context =>
{
if (context.Exception.GetType() == typeof(SecurityTokenExpiredException))
{
context.Response.Headers.Add("Token-Expired", "true");
}
return Task.CompletedTask;
},
OnTokenValidated = context =>
{
// Custom validation logic
return Task.CompletedTask;
}
};
});
// Add Authorization with Policies
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("RequireAdminRole", policy =>
policy.RequireRole("Admin"));
options.AddPolicy("RequireUserRole", policy =>
policy.RequireRole("User", "Admin"));
options.AddPolicy("Over18", policy =>
policy.RequireClaim("DateOfBirth", DateTime.Now.AddYears(-18).ToString("yyyy-MM-dd")));
options.AddPolicy("EmailVerified", policy =>
policy.RequireClaim("EmailVerified", "true"));
});
// Add Services
builder.Services.AddScoped<IUserService, UserService>();
builder.Services.AddScoped<ITokenService, TokenService>();
builder.Services.AddScoped<IRefreshTokenService, RefreshTokenService>();
builder.Services.AddScoped<IEmailService, EmailService>();
// Add Controllers
builder.Services.AddControllers();
// Add Swagger with JWT Support
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new() { Title = "JWT Auth API", Version = "v1" });
// Add JWT Authentication to Swagger
c.AddSecurityDefinition("Bearer", new Microsoft.OpenApi.Models.OpenApiSecurityScheme
{
Description = "JWT Authorization header using the Bearer scheme. Example: \"Authorization: Bearer {token}\"",
Name = "Authorization",
In = Microsoft.OpenApi.Models.ParameterLocation.Header,
Type = Microsoft.OpenApi.Models.SecuritySchemeType.ApiKey,
Scheme = "Bearer"
});
c.AddSecurityRequirement(new Microsoft.OpenApi.Models.OpenApiSecurityRequirement
{
{
new Microsoft.OpenApi.Models.OpenApiSecurityScheme
{
Reference = new Microsoft.OpenApi.Models.OpenApiReference
{
Type = Microsoft.OpenApi.Models.ReferenceType.SecurityScheme,
Id = "Bearer"
}
},
Array.Empty<string>()
}
});
});
var app = builder.Build();
// Configure the HTTP request pipeline
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
app.UseDeveloperExceptionPage();
}
app.UseHttpsRedirection();
// Custom Security Middleware
app.UseSecurityHeaders();
app.UseRateLimitingMiddleware();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.Run();
Configuration Settings
appsettings.json
{
"ConnectionStrings": {
"DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=JWTAuthDemo;Trusted_Connection=true;MultipleActiveResultSets=true"
},
"JwtSettings": {
"SecretKey": "YourSuperSecretKeyThatIsAtLeast32CharactersLong!",
"Issuer": "JWTAuthDemo",
"Audience": "JWTAuthDemoUsers",
"AccessTokenExpirationMinutes": 15,
"RefreshTokenExpirationDays": 7
},
"EmailSettings": {
"SmtpServer": "smtp.gmail.com",
"Port": 587,
"SenderName": "JWT Auth Demo",
"SenderEmail": "[email protected]",
"Username": "[email protected]",
"Password": "your-app-password"
},
"RateLimitSettings": {
"MaxLoginAttempts": 5,
"LockoutTimeMinutes": 15,
"MaxRequestsPerMinute": 100
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}
4. User Registration & Login System {#registration-login}
User Models & DTOs
Models/User.cs
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace JWTAuthDemo.Models
{
public class User
{
[Key]
public int Id { get; set; }
[Required]
[StringLength(50)]
public string Username { get; set; } = string.Empty;
[Required]
[EmailAddress]
public string Email { get; set; } = string.Empty;
[Required]
public string PasswordHash { get; set; } = string.Empty;
[StringLength(100)]
public string FirstName { get; set; } = string.Empty;
[StringLength(100)]
public string LastName { get; set; } = string.Empty;
public DateTime DateOfBirth { get; set; }
public string Role { get; set; } = "User";
public bool IsActive { get; set; } = true;
public bool EmailVerified { get; set; } = false;
public string? EmailVerificationToken { get; set; }
public DateTime? EmailVerificationTokenExpires { get; set; }
public string? PasswordResetToken { get; set; }
public DateTime? PasswordResetTokenExpires { get; set; }
public int LoginAttempts { get; set; } = 0;
public DateTime? LockoutEnd { get; set; }
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public DateTime? UpdatedAt { get; set; }
public DateTime? LastLogin { get; set; }
// Navigation properties
public virtual ICollection<RefreshToken> RefreshTokens { get; set; } = new List<RefreshToken>();
public virtual ICollection<LoginHistory> LoginHistory { get; set; } = new List<LoginHistory>();
// Computed properties
[NotMapped]
public string FullName => $"{FirstName} {LastName}";
[NotMapped]
public bool IsLockedOut => LockoutEnd.HasValue && LockoutEnd > DateTime.UtcNow;
}
public class RefreshToken
{
[Key]
public int Id { get; set; }
[Required]
public int UserId { get; set; }
[Required]
public string Token { get; set; } = string.Empty;
public DateTime Expires { get; set; }
public DateTime Created { get; set; } = DateTime.UtcNow;
public string CreatedByIp { get; set; } = string.Empty;
public DateTime? Revoked { get; set; }
public string? RevokedByIp { get; set; }
public string? ReplacedByToken { get; set; }
public string? ReasonRevoked { get; set; }
public bool IsExpired => DateTime.UtcNow >= Expires;
public bool IsRevoked => Revoked != null;
public bool IsActive => !IsRevoked && !IsExpired;
// Navigation property
public virtual User User { get; set; } = null!;
}
public class LoginHistory
{
[Key]
public int Id { get; set; }
[Required]
public int UserId { get; set; }
public DateTime LoginTime { get; set; } = DateTime.UtcNow;
public string IpAddress { get; set; } = string.Empty;
public string UserAgent { get; set; } = string.Empty;
public bool Success { get; set; }
public string? FailureReason { get; set; }
// Navigation property
public virtual User User { get; set; } = null!;
}
}
// DTOs (Data Transfer Objects)
namespace JWTAuthDemo.Models
{
public class RegisterRequest
{
[Required]
[StringLength(50, MinimumLength = 3)]
public string Username { get; set; } = string.Empty;
[Required]
[EmailAddress]
public string Email { get; set; } = string.Empty;
[Required]
[StringLength(100, MinimumLength = 6)]
public string Password { get; set; } = string.Empty;
[Required]
[Compare("Password")]
public string ConfirmPassword { get; set; } = string.Empty;
[Required]
[StringLength(100)]
public string FirstName { get; set; } = string.Empty;
[Required]
[StringLength(100)]
public string LastName { get; set; } = string.Empty;
[Required]
public DateTime DateOfBirth { get; set; }
}
public class LoginRequest
{
[Required]
public string Username { get; set; } = string.Empty;
[Required]
public string Password { get; set; } = string.Empty;
public string? TwoFactorCode { get; set; }
}
public class AuthResponse
{
public bool Success { get; set; }
public string Message { get; set; } = string.Empty;
public string AccessToken { get; set; } = string.Empty;
public string RefreshToken { get; set; } = string.Empty;
public DateTime AccessTokenExpires { get; set; }
public DateTime RefreshTokenExpires { get; set; }
public UserResponse User { get; set; } = new();
}
public class UserResponse
{
public int Id { get; set; }
public string Username { get; set; } = string.Empty;
public string Email { get; set; } = string.Empty;
public string FirstName { get; set; } = string.Empty;
public string LastName { get; set; } = string.Empty;
public string FullName { get; set; } = string.Empty;
public string Role { get; set; } = string.Empty;
public bool EmailVerified { get; set; }
public DateTime? LastLogin { get; set; }
}
public class RefreshTokenRequest
{
[Required]
public string AccessToken { get; set; } = string.Empty;
[Required]
public string RefreshToken { get; set; } = string.Empty;
}
public class ChangePasswordRequest
{
[Required]
public string CurrentPassword { get; set; } = string.Empty;
[Required]
[StringLength(100, MinimumLength = 6)]
public string NewPassword { get; set; } = string.Empty;
[Required]
[Compare("NewPassword")]
public string ConfirmNewPassword { get; set; } = string.Empty;
}
public class ForgotPasswordRequest
{
[Required]
[EmailAddress]
public string Email { get; set; } = string.Empty;
}
public class ResetPasswordRequest
{
[Required]
public string Token { get; set; } = string.Empty;
[Required]
[StringLength(100, MinimumLength = 6)]
public string NewPassword { get; set; } = string.Empty;
[Required]
[Compare("NewPassword")]
public string ConfirmNewPassword { get; set; } = string.Empty;
}
}
Database Context
Data/ApplicationDbContext.cs
using JWTAuthDemo.Models;
using Microsoft.EntityFrameworkCore;
namespace JWTAuthDemo.Data
{
public class ApplicationDbContext : DbContext
{
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options)
{
}
public DbSet<User> Users { get; set; } = null!;
public DbSet<RefreshToken> RefreshTokens { get; set; } = null!;
public DbSet<LoginHistory> LoginHistory { get; set; } = null!;
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
// User configuration
modelBuilder.Entity<User>(entity =>
{
entity.HasIndex(u => u.Username).IsUnique();
entity.HasIndex(u => u.Email).IsUnique();
entity.Property(u => u.CreatedAt)
.HasDefaultValueSql("GETUTCDATE()");
entity.HasMany(u => u.RefreshTokens)
.WithOne(rt => rt.User)
.HasForeignKey(rt => rt.UserId)
.OnDelete(DeleteBehavior.Cascade);
entity.HasMany(u => u.LoginHistory)
.WithOne(lh => lh.User)
.HasForeignKey(lh => lh.UserId)
.OnDelete(DeleteBehavior.Cascade);
});
// RefreshToken configuration
modelBuilder.Entity<RefreshToken>(entity =>
{
entity.HasIndex(rt => rt.Token).IsUnique();
entity.HasIndex(rt => rt.UserId);
entity.Property(rt => rt.Created)
.HasDefaultValueSql("GETUTCDATE()");
});
// LoginHistory configuration
modelBuilder.Entity<LoginHistory>(entity =>
{
entity.HasIndex(lh => lh.UserId);
entity.HasIndex(lh => lh.LoginTime);
entity.Property(lh => lh.LoginTime)
.HasDefaultValueSql("GETUTCDATE()");
});
// Seed initial data
modelBuilder.Entity<User>().HasData(
new User
{
Id = 1,
Username = "admin",
Email = "[email protected]",
PasswordHash = BCrypt.Net.BCrypt.HashPassword("Admin123!"),
FirstName = "System",
LastName = "Administrator",
DateOfBirth = new DateTime(1980, 1, 1),
Role = "Admin",
EmailVerified = true,
IsActive = true
}
);
}
}
}
5. Token Generation & Validation {#token-generation}
Token Service Implementation
Services/ITokenService.cs
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Security.Cryptography;
using System.Text;
using Microsoft.IdentityModel.Tokens;
using JWTAuthDemo.Models;
namespace JWTAuthDemo.Services
{
public interface ITokenService
{
string GenerateAccessToken(User user);
string GenerateRefreshToken();
ClaimsPrincipal GetPrincipalFromExpiredToken(string token);
bool ValidateToken(string token);
}
public class TokenService : ITokenService
{
private readonly IConfiguration _configuration;
private readonly ILogger<TokenService> _logger;
public TokenService(IConfiguration configuration, ILogger<TokenService> logger)
{
_configuration = configuration;
_logger = logger;
}
public string GenerateAccessToken(User user)
{
try
{
var jwtSettings = _configuration.GetSection("JwtSettings");
var secretKey = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(jwtSettings["SecretKey"]!));
var signinCredentials = new SigningCredentials(secretKey, SecurityAlgorithms.HmacSha256);
var claims = new List<Claim>
{
new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()),
new Claim(ClaimTypes.Name, user.Username),
new Claim(ClaimTypes.Email, user.Email),
new Claim(ClaimTypes.GivenName, user.FirstName),
new Claim(ClaimTypes.Surname, user.LastName),
new Claim(ClaimTypes.Role, user.Role),
new Claim("DateOfBirth", user.DateOfBirth.ToString("yyyy-MM-dd")),
new Claim("EmailVerified", user.EmailVerified.ToString().ToLower()),
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
new Claim(JwtRegisteredClaimNames.Iat, DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString(), ClaimValueTypes.Integer64)
};
var tokenOptions = new JwtSecurityToken(
issuer: jwtSettings["Issuer"],
audience: jwtSettings["Audience"],
claims: claims,
expires: DateTime.UtcNow.AddMinutes(Convert.ToDouble(jwtSettings["AccessTokenExpirationMinutes"])),
signingCredentials: signinCredentials
);
var tokenString = new JwtSecurityTokenHandler().WriteToken(tokenOptions);
_logger.LogInformation("Access token generated for user: {Username}", user.Username);
return tokenString;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error generating access token for user: {Username}", user.Username);
throw;
}
}
public string GenerateRefreshToken()
{
var randomNumber = new byte[64];
using var rng = RandomNumberGenerator.Create();
rng.GetBytes(randomNumber);
return Convert.ToBase64String(randomNumber);
}
public ClaimsPrincipal GetPrincipalFromExpiredToken(string token)
{
try
{
var jwtSettings = _configuration.GetSection("JwtSettings");
var tokenValidationParameters = new TokenValidationParameters
{
ValidateAudience = false, // We might don't have audience in refresh token
ValidateIssuer = false,
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(jwtSettings["SecretKey"]!)),
ValidateLifetime = false // We want to get principal from expired token
};
var tokenHandler = new JwtSecurityTokenHandler();
var principal = tokenHandler.ValidateToken(token, tokenValidationParameters, out SecurityToken securityToken);
if (securityToken is not JwtSecurityToken jwtSecurityToken ||
!jwtSecurityToken.Header.Alg.Equals(SecurityAlgorithms.HmacSha256, StringComparison.InvariantCultureIgnoreCase))
{
throw new SecurityTokenException("Invalid token");
}
return principal;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error getting principal from expired token");
throw;
}
}
public bool ValidateToken(string token)
{
try
{
var jwtSettings = _configuration.GetSection("JwtSettings");
var tokenHandler = new JwtSecurityTokenHandler();
var validationParameters = GetValidationParameters();
tokenHandler.ValidateToken(token, validationParameters, out SecurityToken validatedToken);
return true;
}
catch
{
return false;
}
}
private TokenValidationParameters GetValidationParameters()
{
var jwtSettings = _configuration.GetSection("JwtSettings");
return new TokenValidationParameters()
{
ValidateLifetime = true,
ValidateAudience = true,
ValidateIssuer = true,
ValidIssuer = jwtSettings["Issuer"],
ValidAudience = jwtSettings["Audience"],
IssuerSigningKey = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(jwtSettings["SecretKey"]!))
};
}
// Additional token utilities
public DateTime GetTokenExpiration(string token)
{
var handler = new JwtSecurityTokenHandler();
var jwtToken = handler.ReadJwtToken(token);
return jwtToken.ValidTo;
}
public IEnumerable<Claim> GetTokenClaims(string token)
{
var handler = new JwtSecurityTokenHandler();
var jwtToken = handler.ReadJwtToken(token);
return jwtToken.Claims;
}
public bool IsTokenExpired(string token)
{
return GetTokenExpiration(token) < DateTime.UtcNow;
}
}
}
6. Claims & Role-Based Authorization {#claims-roles}
Advanced Authorization System
Services/IUserService.cs
using JWTAuthDemo.Models;
using Microsoft.EntityFrameworkCore;
namespace JWTAuthDemo.Services
{
public interface IUserService
{
Task<User?> AuthenticateAsync(string username, string password, string ipAddress);
Task<User> RegisterAsync(RegisterRequest request, string ipAddress);
Task<User?> GetUserByIdAsync(int id);
Task<User?> GetUserByUsernameAsync(string username);
Task<User?> GetUserByEmailAsync(string email);
Task<bool> UserExistsAsync(string username, string email);
Task<bool> UpdateUserAsync(User user);
Task<bool> ChangePasswordAsync(int userId, ChangePasswordRequest request);
Task<bool> VerifyEmailAsync(string token);
Task<bool> ForgotPasswordAsync(string email);
Task<bool> ResetPasswordAsync(ResetPasswordRequest request);
Task LogLoginAttemptAsync(int userId, string ipAddress, string userAgent, bool success, string? failureReason = null);
Task<bool> IsAccountLockedAsync(string username);
Task ResetLoginAttemptsAsync(string username);
}
public class UserService : IUserService
{
private readonly ApplicationDbContext _context;
private readonly ILogger<UserService> _logger;
private readonly IEmailService _emailService;
private readonly IConfiguration _configuration;
public UserService(
ApplicationDbContext context,
ILogger<UserService> logger,
IEmailService emailService,
IConfiguration configuration)
{
_context = context;
_logger = logger;
_emailService = emailService;
_configuration = configuration;
}
public async Task<User?> AuthenticateAsync(string username, string password, string ipAddress)
{
try
{
var user = await _context.Users
.Include(u => u.RefreshTokens)
.FirstOrDefaultAsync(u => u.Username == username || u.Email == username);
if (user == null)
{
await LogLoginAttemptAsync(0, ipAddress, "Unknown", false, "User not found");
return null;
}
// Check if account is locked
if (user.IsLockedOut)
{
await LogLoginAttemptAsync(user.Id, ipAddress, "Unknown", false, "Account locked");
throw new ApplicationException("Account is temporarily locked due to multiple failed login attempts.");
}
// Check if account is active
if (!user.IsActive)
{
await LogLoginAttemptAsync(user.Id, ipAddress, "Unknown", false, "Account inactive");
throw new ApplicationException("Account is inactive.");
}
// Verify password
if (!BCrypt.Net.BCrypt.Verify(password, user.PasswordHash))
{
user.LoginAttempts++;
// Lock account after maximum attempts
var maxAttempts = _configuration.GetValue<int>("RateLimitSettings:MaxLoginAttempts");
if (user.LoginAttempts >= maxAttempts)
{
var lockoutTime = _configuration.GetValue<int>("RateLimitSettings:LockoutTimeMinutes");
user.LockoutEnd = DateTime.UtcNow.AddMinutes(lockoutTime);
_logger.LogWarning("Account locked for user: {Username} after {Attempts} failed attempts",
user.Username, user.LoginAttempts);
}
await _context.SaveChangesAsync();
await LogLoginAttemptAsync(user.Id, ipAddress, "Unknown", false, "Invalid password");
return null;
}
// Successful login
user.LoginAttempts = 0;
user.LockoutEnd = null;
user.LastLogin = DateTime.UtcNow;
await _context.SaveChangesAsync();
await LogLoginAttemptAsync(user.Id, ipAddress, "Unknown", true);
_logger.LogInformation("User authenticated successfully: {Username}", user.Username);
return user;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error authenticating user: {Username}", username);
throw;
}
}
public async Task<User> RegisterAsync(RegisterRequest request, string ipAddress)
{
try
{
// Check if user already exists
if (await UserExistsAsync(request.Username, request.Email))
{
throw new ApplicationException("Username or email already exists.");
}
// Validate age (must be at least 13 years old)
if (request.DateOfBirth > DateTime.Now.AddYears(-13))
{
throw new ApplicationException("You must be at least 13 years old to register.");
}
var user = new User
{
Username = request.Username,
Email = request.Email,
PasswordHash = BCrypt.Net.BCrypt.HashPassword(request.Password),
FirstName = request.FirstName,
LastName = request.LastName,
DateOfBirth = request.DateOfBirth,
Role = "User",
IsActive = true,
EmailVerified = false,
EmailVerificationToken = GenerateRandomToken(),
EmailVerificationTokenExpires = DateTime.UtcNow.AddDays(1)
};
_context.Users.Add(user);
await _context.SaveChangesAsync();
// Send verification email
await _emailService.SendVerificationEmailAsync(user.Email, user.EmailVerificationToken!);
_logger.LogInformation("New user registered: {Username} from IP: {IpAddress}",
user.Username, ipAddress);
return user;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error registering user: {Username}", request.Username);
throw;
}
}
public async Task<User?> GetUserByIdAsync(int id)
{
return await _context.Users
.Include(u => u.RefreshTokens)
.FirstOrDefaultAsync(u => u.Id == id);
}
public async Task<User?> GetUserByUsernameAsync(string username)
{
return await _context.Users
.FirstOrDefaultAsync(u => u.Username == username);
}
public async Task<User?> GetUserByEmailAsync(string email)
{
return await _context.Users
.FirstOrDefaultAsync(u => u.Email == email);
}
public async Task<bool> UserExistsAsync(string username, string email)
{
return await _context.Users
.AnyAsync(u => u.Username == username || u.Email == email);
}
public async Task<bool> UpdateUserAsync(User user)
{
try
{
user.UpdatedAt = DateTime.UtcNow;
_context.Users.Update(user);
await _context.SaveChangesAsync();
return true;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error updating user: {UserId}", user.Id);
return false;
}
}
public async Task<bool> ChangePasswordAsync(int userId, ChangePasswordRequest request)
{
try
{
var user = await GetUserByIdAsync(userId);
if (user == null) return false;
// Verify current password
if (!BCrypt.Net.BCrypt.Verify(request.CurrentPassword, user.PasswordHash))
{
throw new ApplicationException("Current password is incorrect.");
}
// Update password
user.PasswordHash = BCrypt.Net.BCrypt.HashPassword(request.NewPassword);
user.UpdatedAt = DateTime.UtcNow;
await _context.SaveChangesAsync();
_logger.LogInformation("Password changed for user: {UserId}", userId);
return true;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error changing password for user: {UserId}", userId);
throw;
}
}
public async Task<bool> VerifyEmailAsync(string token)
{
try
{
var user = await _context.Users
.FirstOrDefaultAsync(u => u.EmailVerificationToken == token &&
u.EmailVerificationTokenExpires > DateTime.UtcNow);
if (user == null) return false;
user.EmailVerified = true;
user.EmailVerificationToken = null;
user.EmailVerificationTokenExpires = null;
user.UpdatedAt = DateTime.UtcNow;
await _context.SaveChangesAsync();
_logger.LogInformation("Email verified for user: {Username}", user.Username);
return true;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error verifying email with token: {Token}", token);
return false;
}
}
public async Task<bool> ForgotPasswordAsync(string email)
{
try
{
var user = await GetUserByEmailAsync(email);
if (user == null) return true; // Don't reveal if email exists
user.PasswordResetToken = GenerateRandomToken();
user.PasswordResetTokenExpires = DateTime.UtcNow.AddHours(1);
await _context.SaveChangesAsync();
// Send password reset email
await _emailService.SendPasswordResetEmailAsync(user.Email, user.PasswordResetToken);
_logger.LogInformation("Password reset token sent for user: {Email}", email);
return true;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error processing forgot password for: {Email}", email);
return false;
}
}
public async Task<bool> ResetPasswordAsync(ResetPasswordRequest request)
{
try
{
var user = await _context.Users
.FirstOrDefaultAsync(u => u.PasswordResetToken == request.Token &&
u.PasswordResetTokenExpires > DateTime.UtcNow);
if (user == null) return false;
user.PasswordHash = BCrypt.Net.BCrypt.HashPassword(request.NewPassword);
user.PasswordResetToken = null;
user.PasswordResetTokenExpires = null;
user.UpdatedAt = DateTime.UtcNow;
await _context.SaveChangesAsync();
_logger.LogInformation("Password reset successfully for user: {Username}", user.Username);
return true;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error resetting password with token: {Token}", request.Token);
return false;
}
}
public async Task LogLoginAttemptAsync(int userId, string ipAddress, string userAgent, bool success, string? failureReason = null)
{
try
{
var loginHistory = new LoginHistory
{
UserId = userId,
IpAddress = ipAddress,
UserAgent = userAgent,
Success = success,
FailureReason = failureReason
};
_context.LoginHistory.Add(loginHistory);
await _context.SaveChangesAsync();
}
catch (Exception ex)
{
_logger.LogError(ex, "Error logging login attempt for user: {UserId}", userId);
}
}
public async Task<bool> IsAccountLockedAsync(string username)
{
var user = await _context.Users
.FirstOrDefaultAsync(u => u.Username == username || u.Email == username);
return user?.IsLockedOut ?? false;
}
public async Task ResetLoginAttemptsAsync(string username)
{
var user = await _context.Users
.FirstOrDefaultAsync(u => u.Username == username || u.Email == username);
if (user != null)
{
user.LoginAttempts = 0;
user.LockoutEnd = null;
await _context.SaveChangesAsync();
}
}
private string GenerateRandomToken()
{
var randomNumber = new byte[32];
using var rng = RandomNumberGenerator.Create();
rng.GetBytes(randomNumber);
return Convert.ToBase64String(randomNumber)
.Replace("+", "-")
.Replace("/", "_")
.Replace("=", "");
}
}
}

Join the conversation! Your thoughts help the community grow.