using System.IdentityModel.Tokens.Jwt; using System.Security.Claims; using System.Security.Cryptography; using System.Text; using GermanApp.Application.DTOs.Auth; using GermanApp.Application.Interfaces; using GermanApp.Domain.Entities; using GermanApp.Infrastructure.Data.DbContext; using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.IdentityModel.Tokens; namespace GermanApp.Infrastructure.Services; /// /// Authentication service implementation. /// Part of the Infrastructure layer. /// public class AuthService : IAuthService { private readonly AppDbContext _dbContext; private readonly IPasswordHasher _passwordHasher; private readonly IConfiguration _configuration; public AuthService( AppDbContext dbContext, IPasswordHasher passwordHasher, IConfiguration configuration) { _dbContext = dbContext; _passwordHasher = passwordHasher; _configuration = configuration; } /// /// Generates a cryptographically secure random token string. /// private static string GenerateRefreshTokenString(int length = 32) { var randomNumber = new byte[length]; using var rng = RandomNumberGenerator.Create(); rng.GetBytes(randomNumber); return Convert.ToBase64String(randomNumber); } /// /// Registers a new user. /// public async Task RegisterAsync(RegisterDto registerDto) { // Check if username or email already exists if (await _dbContext.Users.AnyAsync(u => u.Username == registerDto.Username)) throw new InvalidOperationException("Username already taken"); if (await _dbContext.Users.AnyAsync(u => u.Email == registerDto.Email)) throw new InvalidOperationException("Email already in use"); // Hash password and create user var user = User.Create(registerDto.Username, registerDto.Email.ToLowerInvariant(), string.Empty); var passwordHash = _passwordHasher.HashPassword(user, registerDto.Password); user.ChangePassword(passwordHash); _dbContext.Users.Add(user); await _dbContext.SaveChangesAsync(); // Generate JWT token var token = GenerateJwtToken(user); // Generate and store refresh token var refreshTokenString = GenerateRefreshTokenString(); var refreshToken = RefreshToken.Create(user.Id, refreshTokenString); _dbContext.RefreshTokens.Add(refreshToken); await _dbContext.SaveChangesAsync(); return new AuthResponse { UserId = user.Id, Username = user.Username, Email = user.Email, Token = token, RefreshToken = refreshTokenString, ExpiresAt = DateTime.UtcNow.AddHours(24) }; } /// /// Authenticates a user and returns a JWT token. /// public async Task LoginAsync(LoginDto loginDto) { var user = await _dbContext.Users.FirstOrDefaultAsync(u => u.Email == loginDto.Email); if (user == null) throw new UnauthorizedAccessException("Invalid email or password"); // Verify password var result = _passwordHasher.VerifyHashedPassword(user, user.PasswordHash, loginDto.Password); if (result == PasswordVerificationResult.Failed) throw new UnauthorizedAccessException("Invalid email or password"); // Revoke any existing refresh tokens for this user (optional: rotate tokens) var existingRefreshTokens = await _dbContext.RefreshTokens .Where(rt => rt.UserId == user.Id && rt.IsActive) .ToListAsync(); foreach (var rt in existingRefreshTokens) { rt.Revoke(); } // Generate JWT token var token = GenerateJwtToken(user); // Generate and store new refresh token var refreshTokenString = GenerateRefreshTokenString(); var refreshToken = RefreshToken.Create(user.Id, refreshTokenString); _dbContext.RefreshTokens.Add(refreshToken); await _dbContext.SaveChangesAsync(); return new AuthResponse { UserId = user.Id, Username = user.Username, Email = user.Email, Token = token, RefreshToken = refreshTokenString, ExpiresAt = DateTime.UtcNow.AddHours(24) }; } /// /// Gets the current authenticated user. /// public async Task GetCurrentUserAsync(int userId) { return await _dbContext.Users.FirstOrDefaultAsync(u => u.Id == userId); } /// /// Generates a JWT token for the given user. /// private string GenerateJwtToken(User user) { var securityKey = new SymmetricSecurityKey( Encoding.UTF8.GetBytes(_configuration["Jwt:Key"] ?? "super-secret-key-at-least-32-characters")); var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256); var claims = new[] { new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()), new Claim(ClaimTypes.Name, user.Username), new Claim(ClaimTypes.Email, user.Email), new Claim(ClaimTypes.Role, "User") }; var token = new JwtSecurityToken( issuer: _configuration["Jwt:Issuer"] ?? "DeutschLernen", audience: _configuration["Jwt:Audience"] ?? "DeutschLernen", claims: claims, expires: DateTime.UtcNow.AddHours(24), signingCredentials: credentials ); return new JwtSecurityTokenHandler().WriteToken(token); } /// /// Refreshes the access token using a refresh token. /// Rotates the refresh token (generates a new one, revokes the old one). /// /// The refresh token /// New access token and refresh token /// Thrown when refresh token is invalid public async Task RefreshTokenAsync(string refreshToken) { // Find the refresh token in the database var storedToken = await _dbContext.RefreshTokens .FirstOrDefaultAsync(rt => rt.Token == refreshToken); if (storedToken == null) throw new UnauthorizedAccessException("Invalid refresh token"); if (!storedToken.IsValid()) throw new UnauthorizedAccessException("Invalid refresh token"); // Get the user associated with this refresh token var user = await _dbContext.Users.FirstOrDefaultAsync(u => u.Id == storedToken.UserId); if (user == null) throw new UnauthorizedAccessException("User not found for refresh token"); // Revoke the current refresh token storedToken.Revoke(); // Generate new JWT access token var newAccessToken = GenerateJwtToken(user); // Generate new refresh token (rotate) var newRefreshTokenString = GenerateRefreshTokenString(); var newRefreshToken = RefreshToken.Create(user.Id, newRefreshTokenString); _dbContext.RefreshTokens.Add(newRefreshToken); await _dbContext.SaveChangesAsync(); return new RefreshTokenResponse { Token = newAccessToken, RefreshToken = newRefreshTokenString, ExpiresAt = DateTime.UtcNow.AddHours(24) }; } /// /// Revokes a refresh token. /// /// The refresh token to revoke /// Thrown when refresh token is not found public async Task RevokeRefreshTokenAsync(string refreshToken) { var storedToken = await _dbContext.RefreshTokens .FirstOrDefaultAsync(rt => rt.Token == refreshToken); if (storedToken == null) throw new UnauthorizedAccessException("Refresh token not found"); storedToken.Revoke(); await _dbContext.SaveChangesAsync(); } }