using System.IdentityModel.Tokens.Jwt; using System.Net.Http.Headers; using System.Security.Claims; using System.Text; using System.Text.Json; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using Microsoft.IdentityModel.Tokens; using ProposalSystem.Domain.Entities; using ProposalSystem.Infrastructure.Data; namespace ProposalSystem.Api.Controllers; [ApiController] [Route("api/[controller]")] public class AuthController : ControllerBase { private readonly ProposalDbContext _db; private readonly IHttpClientFactory _httpClientFactory; private readonly IConfiguration _config; public AuthController(ProposalDbContext db, IHttpClientFactory httpClientFactory, IConfiguration config) { _db = db; _httpClientFactory = httpClientFactory; _config = config; } [HttpPost("callback")] public async Task> Callback([FromBody] AuthCallbackRequest request, CancellationToken ct) { var domain = _config["Auth:CognitoDomain"]; var clientId = _config["Auth:ClientId"]; if (string.IsNullOrEmpty(domain) || string.IsNullOrEmpty(clientId)) return StatusCode(500, new { message = "Auth not configured" }); var tokenResponse = await ExchangeCodeAsync(domain, clientId, request.Code, request.RedirectUri, ct); if (tokenResponse == null) return BadRequest(new { message = "Failed to exchange authorization code" }); var handler = new JwtSecurityTokenHandler(); var idToken = handler.ReadJwtToken(tokenResponse.IdToken); var sub = idToken.Claims.FirstOrDefault(c => c.Type == "sub")?.Value ?? throw new InvalidOperationException("No sub claim in ID token"); var email = idToken.Claims.FirstOrDefault(c => c.Type == "email")?.Value ?? ""; var name = idToken.Claims.FirstOrDefault(c => c.Type == "name")?.Value ?? idToken.Claims.FirstOrDefault(c => c.Type == "cognito:username")?.Value ?? email.Split('@')[0]; var groups = idToken.Claims.Where(c => c.Type == "cognito:groups").Select(c => c.Value).ToList(); var role = groups.Contains("sysadmins") ? UserRole.SysAdmin : groups.Contains("admins") ? UserRole.Admin : UserRole.Dispatcher; var user = await _db.Users.FirstOrDefaultAsync(u => u.CognitoSub == sub, ct); if (user == null) { user = new User { Id = Guid.NewGuid(), CognitoSub = sub, Email = email, DisplayName = name, Role = role, IsActive = true, CreatedAt = DateTime.UtcNow, UpdatedAt = DateTime.UtcNow, }; _db.Users.Add(user); await _db.SaveChangesAsync(ct); } else if (user.Email != email || user.DisplayName != name) { user.Email = email; user.DisplayName = name; user.UpdatedAt = DateTime.UtcNow; await _db.SaveChangesAsync(ct); } return Ok(new AuthResponse( user.Id.ToString(), user.Email, user.DisplayName, user.Role.ToString(), tokenResponse.AccessToken )); } [HttpPost("dev-login")] public async Task> DevLogin([FromBody] DevLoginRequest request, CancellationToken ct) { var devMode = _config.GetValue("Auth:DevMode"); if (!devMode) return NotFound(); var signingKey = _config["Auth:DevSigningKey"]; if (string.IsNullOrEmpty(signingKey)) return StatusCode(500, new { message = "Dev signing key not configured" }); var role = Enum.TryParse(request.Role, true, out var parsed) ? parsed : UserRole.Admin; var user = await _db.Users.FirstOrDefaultAsync(u => u.Email == request.Email, ct); if (user == null) { user = new User { Id = Guid.NewGuid(), CognitoSub = $"dev-{Guid.NewGuid():N}", Email = request.Email, DisplayName = request.DisplayName ?? request.Email.Split('@')[0], Role = role, IsActive = true, CreatedAt = DateTime.UtcNow, UpdatedAt = DateTime.UtcNow, }; _db.Users.Add(user); await _db.SaveChangesAsync(ct); } var claims = new List { new(ClaimTypes.NameIdentifier, user.Id.ToString()), new("sub", user.CognitoSub), new("email", user.Email), new("name", user.DisplayName), new("cognito:groups", role.ToString().ToLower() + "s"), }; var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(signingKey)); var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256); var token = new JwtSecurityToken( issuer: "proposal-system-dev", audience: "proposal-system-dev", claims: claims, expires: DateTime.UtcNow.AddHours(12), signingCredentials: creds); var tokenString = new JwtSecurityTokenHandler().WriteToken(token); return Ok(new AuthResponse( user.Id.ToString(), user.Email, user.DisplayName, user.Role.ToString(), tokenString )); } private async Task ExchangeCodeAsync( string domain, string clientId, string code, string redirectUri, CancellationToken ct) { var client = _httpClientFactory.CreateClient(); var tokenUrl = $"https://{domain}/oauth2/token"; var content = new FormUrlEncodedContent(new Dictionary { ["grant_type"] = "authorization_code", ["client_id"] = clientId, ["code"] = code, ["redirect_uri"] = redirectUri, }); var response = await client.PostAsync(tokenUrl, content, ct); if (!response.IsSuccessStatusCode) return null; var json = await response.Content.ReadAsStringAsync(ct); return JsonSerializer.Deserialize(json); } } public record AuthCallbackRequest(string Code, string RedirectUri); public record DevLoginRequest(string Email, string? DisplayName, string? Role); public record AuthResponse(string Id, string Email, string DisplayName, string Role, string Token); internal class CognitoTokenResponse { [System.Text.Json.Serialization.JsonPropertyName("access_token")] public string AccessToken { get; set; } = ""; [System.Text.Json.Serialization.JsonPropertyName("id_token")] public string IdToken { get; set; } = ""; [System.Text.Json.Serialization.JsonPropertyName("refresh_token")] public string RefreshToken { get; set; } = ""; [System.Text.Json.Serialization.JsonPropertyName("token_type")] public string TokenType { get; set; } = ""; [System.Text.Json.Serialization.JsonPropertyName("expires_in")] public int ExpiresIn { get; set; } }