using Microsoft.Identity.Client; using System.IdentityModel.Tokens.Jwt; using System.Security.Claims; using System.Security.Cryptography; using System.Text; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.IdentityModel.Tokens; using UniVerse.Application.DTOs.Auth; using UniVerse.Application.DTOs.Users; using UniVerse.Application.Interfaces; using UniVerse.Application.Mappings; using UniVerse.Domain.Entities; using UniVerse.Domain.Enums; using UniVerse.Domain.Exceptions; using UniVerse.Infrastructure.Data; namespace UniVerse.Infrastructure.Services; public class AuthService : IAuthService { private readonly AppDbContext _db; private readonly IConfiguration _config; private readonly IGamificationService _gamification; public AuthService(AppDbContext db, IConfiguration config, IGamificationService gamification) { _db = db; _config = config; _gamification = gamification; } public async Task LoginWithMicrosoftAsync(string authorizationCode, string? redirectUri = null) { var tenantId = _config["AzureAd:TenantId"]; var clientId = _config["AzureAd:ClientId"]; var clientSecret = _config["AzureAd:ClientSecret"]; var instance = _config["AzureAd:Instance"] ?? "https://login.microsoftonline.com/"; if (string.IsNullOrWhiteSpace(tenantId) || string.IsNullOrWhiteSpace(clientId) || string.IsNullOrWhiteSpace(clientSecret)) throw new UnauthorizedException("Microsoft authentication is not configured (AzureAd:TenantId/ClientId/ClientSecret)."); var effectiveRedirectUri = redirectUri ?? _config["AzureAd:RedirectUri"] ?? "http://localhost:5173/auth/callback"; var authority = $"{instance.TrimEnd('/')}/{tenantId}"; var app = ConfidentialClientApplicationBuilder.Create(clientId) .WithClientSecret(clientSecret) .WithAuthority(new Uri(authority)) .WithRedirectUri(effectiveRedirectUri) .Build(); AuthenticationResult result; try { result = await app.AcquireTokenByAuthorizationCode(new[] { "User.Read" }, authorizationCode) .ExecuteAsync(); } catch (MsalException ex) { throw new UnauthorizedException($"Microsoft authentication failed: {ex.Message}"); } // Parse claims directly from the ID token provided by Microsoft var handler = new JwtSecurityTokenHandler(); var idToken = handler.ReadJwtToken(result.IdToken); var email = idToken.Claims.FirstOrDefault(c => c.Type == "preferred_username" || c.Type == "email" || c.Type == ClaimTypes.Upn)?.Value; var name = idToken.Claims.FirstOrDefault(c => c.Type == "name")?.Value; if (string.IsNullOrEmpty(email)) throw new UnauthorizedException("Email not found in Microsoft token claims."); // Automatically provision user var user = await _db.Users.FirstOrDefaultAsync(u => u.Email == email); if (user == null) { user = new User { Email = email, DisplayName = name ?? email.Split('@')[0], Role = UserRole.Student, // Default role IsActive = true }; _db.Users.Add(user); await _db.SaveChangesAsync(); // Create corresponding profile _db.StudentProfiles.Add(new StudentProfile { UserId = user.Id }); await _db.SaveChangesAsync(); } else if (!user.IsActive) { throw new ForbiddenException("Account is deactivated."); } var accessToken = GenerateAccessToken(user); var refreshToken = await GenerateRefreshTokenAsync(user.Id); return new AuthResult( new AuthResponse( accessToken, DateTime.UtcNow.AddMinutes(GetAccessTokenExpiration()), user.ToAuthDto() ), refreshToken ); } public async Task DevLoginAsync(string email, string? displayName, UserRole role) { var user = await _db.Users.FirstOrDefaultAsync(u => u.Email == email); if (user == null) { user = new User { Email = email, DisplayName = displayName ?? email.Split('@')[0], Role = role, IsActive = true }; _db.Users.Add(user); await _db.SaveChangesAsync(); // Create profile based on role if (role == UserRole.Student) { _db.StudentProfiles.Add(new StudentProfile { UserId = user.Id }); await _db.SaveChangesAsync(); } else if (role == UserRole.Teacher) { _db.TeacherProfiles.Add(new TeacherProfile { UserId = user.Id }); await _db.SaveChangesAsync(); } } if (!user.IsActive) throw new ForbiddenException("Account is deactivated."); var accessToken = GenerateAccessToken(user); var refreshToken = await GenerateRefreshTokenAsync(user.Id); return new AuthResult( new AuthResponse( accessToken, DateTime.UtcNow.AddMinutes(GetAccessTokenExpiration()), user.ToAuthDto() ), refreshToken ); } public async Task RefreshTokenAsync(string refreshToken) { var token = await _db.RefreshTokens .Include(rt => rt.User) .FirstOrDefaultAsync(rt => rt.Token == refreshToken); if (token == null || !token.IsActive) throw new ForbiddenException("Invalid or expired refresh token."); // Revoke old token token.RevokedAt = DateTime.UtcNow; // Generate new tokens var accessToken = GenerateAccessToken(token.User); var newRefreshToken = await GenerateRefreshTokenAsync(token.UserId); await _db.SaveChangesAsync(); return new AuthResult( new AuthResponse( accessToken, DateTime.UtcNow.AddMinutes(GetAccessTokenExpiration()), token.User.ToAuthDto() ), newRefreshToken ); } public async Task RevokeRefreshTokenAsync(string refreshToken) { var token = await _db.RefreshTokens.FirstOrDefaultAsync(rt => rt.Token == refreshToken); if (token != null && token.IsActive) { token.RevokedAt = DateTime.UtcNow; await _db.SaveChangesAsync(); } } public async Task GetCurrentUserAsync(int userId) { var user = await _db.Users.FindAsync(userId) ?? throw new NotFoundException("User", userId); return user.ToDto(_gamification.CalculateLevel(user.Xp)); } private string GenerateAccessToken(User user) { var key = new SymmetricSecurityKey( Encoding.UTF8.GetBytes(_config["Jwt:Secret"]!)); var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256); var claims = new[] { new Claim(JwtRegisteredClaimNames.Sub, user.Id.ToString()), new Claim(JwtRegisteredClaimNames.Email, user.Email), new Claim(ClaimTypes.Role, user.Role.ToString()), new Claim("display_name", user.DisplayName ?? "") }; var token = new JwtSecurityToken( issuer: _config["Jwt:Issuer"], audience: _config["Jwt:Audience"], claims: claims, expires: DateTime.UtcNow.AddMinutes(GetAccessTokenExpiration()), signingCredentials: creds ); return new JwtSecurityTokenHandler().WriteToken(token); } private async Task GenerateRefreshTokenAsync(int userId) { var randomBytes = RandomNumberGenerator.GetBytes(64); var tokenString = Convert.ToBase64String(randomBytes); var refreshToken = new RefreshToken { UserId = userId, Token = tokenString, ExpiresAt = DateTime.UtcNow.AddDays(GetRefreshTokenExpiration()), CreatedAt = DateTime.UtcNow }; _db.RefreshTokens.Add(refreshToken); await _db.SaveChangesAsync(); return tokenString; } private int GetAccessTokenExpiration() => int.Parse(_config["Jwt:AccessTokenExpirationMinutes"] ?? "30"); private int GetRefreshTokenExpiration() => int.Parse(_config["Jwt:RefreshTokenExpirationDays"] ?? "30"); }