using Gateway.Data; using Gateway.Security; using Microsoft.AspNetCore.Mvc; using System.Text.Json; namespace Gateway.Controllers; /// /// Authentication endpoints for session management. /// Sessions are created after Entra External ID authentication. /// [ApiController] [Route("api/auth")] public sealed class AuthController : ControllerBase { private readonly SqlService _sql; private readonly ClientContext _client; private readonly ILogger _log; public AuthController(SqlService sql, ClientContext client, ILogger log) { _sql = sql; _client = client; _log = log; } /// /// Exchange Entra JWT for a session token. /// Call this after successful Entra login to get a session for API calls. /// The JWT must be validated by middleware before this endpoint. /// [HttpPost("session")] public async Task CreateSession([FromBody] CreateSessionRequest? request, CancellationToken ct) { _log.LogWarning("[Session] CreateSession called"); // ClientContext is populated by middleware after JWT validation if (!_client.IsAuthenticated) { _log.LogWarning("[Session] Not authenticated - ClientId is null/empty"); return Unauthorized(new { ok = false, error = "Valid Entra authentication required" }); } _log.LogWarning("[Session] Authenticated: ClientId={ClientId}, Email={Email}", _client.ClientId, _client.Email); // Gateway handles CIAM client sessions only. // Staff apps authenticate directly to Management API via JWT Bearer — never via Gateway. if (_client.IsStaff) { _log.LogWarning("[Session] Staff token rejected — use JWT Bearer directly to Management API"); return StatusCode(403, new { ok = false, error = "Staff authentication does not use Gateway sessions" }); } var rqst = JsonSerializer.Serialize(new { provider = _client.AuthProvider ?? "EntraExternalId", subject = _client.ClientId, email = _client.Email, displayName = _client.ClientName, clientId = request?.PreferredClientId, ipAddress = GetClientIp(), userAgent = Request.Headers.UserAgent.FirstOrDefault(), sessionDurationHours = request?.SessionDurationHours ?? 24 }); _log.LogWarning("[Session] Calling proc with: {Rqst}", rqst); _log.LogWarning("[Session] Using proc=dbo.spClientSession"); try { var resp = await _sql.ExecProcAsync("dbo.spClientSession", "createFromIdentity", rqst, ct: ct); _log.LogWarning("[Session] Proc response: {Resp}", resp ?? "(null)"); if (string.IsNullOrWhiteSpace(resp)) { return StatusCode(500, new { ok = false, error = "Session service unavailable" }); } using var doc = JsonDocument.Parse(resp); var root = doc.RootElement; if (root.TryGetProperty("ok", out var okProp) && okProp.GetBoolean()) { _log.LogWarning("[Session] Success - returning session"); return Content(resp, "application/json"); } else { var error = root.TryGetProperty("error", out var errProp) ? errProp.GetString() : "Session creation failed"; _log.LogWarning("[Session] Proc returned error: {Error}", error); return BadRequest(new { ok = false, error }); } } catch (Exception ex) { _log.LogError(ex, "[Session] EXCEPTION in CreateSession: {Message}", ex.Message); return StatusCode(500, new { ok = false, error = "Session service error", detail = ex.Message }); } } /// /// Register a new client/organization for the authenticated user. /// JWT must be validated by middleware before this endpoint. /// Called from the registration portal after CIAM sign-in. /// [HttpPost("register")] public async Task Register([FromBody] RegisterRequest request, CancellationToken ct) { _log.LogWarning("[Register] Register called"); if (!_client.IsAuthenticated) { _log.LogWarning("[Register] Not authenticated"); return Unauthorized(new { ok = false, error = "Valid Entra authentication required" }); } if (string.IsNullOrWhiteSpace(request?.CompanyName)) { return BadRequest(new { ok = false, error = "companyName is required" }); } _log.LogWarning("[Register] Authenticated: Subject={Subject}, Email={Email}, Company={Company}", _client.ClientId, _client.Email, request.CompanyName); var rqst = JsonSerializer.Serialize(new { provider = _client.AuthProvider ?? "EntraExternalId", subject = _client.ClientId, email = _client.Email, displayName = _client.ClientName, companyName = request.CompanyName, industry = request.Industry, website = request.Website }); try { var resp = await _sql.ExecProcAsync("dbo.spOnboarding", "register", rqst, ct: ct); _log.LogWarning("[Register] spOnboarding response: {Resp}", resp ?? "(null)"); if (string.IsNullOrWhiteSpace(resp)) { return StatusCode(500, new { ok = false, error = "Onboarding service unavailable" }); } using var doc = JsonDocument.Parse(resp); var root = doc.RootElement; if (root.TryGetProperty("ok", out var okProp) && okProp.GetBoolean()) { _log.LogWarning("[Register] Success"); return Content(resp, "application/json"); } else { var error = root.TryGetProperty("error", out var errProp) ? errProp.GetString() : "Registration failed"; _log.LogWarning("[Register] Error: {Error}", error); return BadRequest(new { ok = false, error }); } } catch (Exception ex) { _log.LogError(ex, "[Register] EXCEPTION: {Message}", ex.Message); return StatusCode(500, new { ok = false, error = "Registration service error", detail = ex.Message }); } } /// /// Sign off (invalidate session) /// [HttpPost("signoff")] public async Task SignOff(CancellationToken ct) { var token = ExtractSessionToken(); if (string.IsNullOrWhiteSpace(token)) { return BadRequest(new { ok = false, error = "No session token provided" }); } var rqst = JsonSerializer.Serialize(new { sessionToken = token }); var signoffProc = "dbo.spClientSession"; // Gateway handles client sessions only try { await _sql.ExecProcAsync(signoffProc, "signoff", rqst, ct: ct); return Ok(new { ok = true, message = "Signed out successfully" }); } catch (Exception ex) { _log.LogError(ex, "SignOff error"); return StatusCode(500, new { ok = false, error = "Sign off failed" }); } } /// /// Refresh session (extend expiration) /// [HttpPost("refresh")] public async Task Refresh([FromBody] RefreshRequest? request, CancellationToken ct) { var token = ExtractSessionToken(); if (string.IsNullOrWhiteSpace(token)) { return BadRequest(new { ok = false, error = "No session token provided" }); } var rqst = JsonSerializer.Serialize(new { sessionToken = token, sessionDurationHours = request?.SessionDurationHours ?? 24 }); var refreshProc = "dbo.spClientSession"; // Gateway handles client sessions only try { var resp = await _sql.ExecProcAsync(refreshProc, "refresh", rqst, ct: ct); if (string.IsNullOrWhiteSpace(resp)) { return StatusCode(500, new { ok = false, error = "Session service unavailable" }); } using var doc = JsonDocument.Parse(resp); var root = doc.RootElement; if (root.TryGetProperty("ok", out var okProp) && okProp.GetBoolean()) { return Content(resp, "application/json"); } else { var error = root.TryGetProperty("error", out var errProp) ? errProp.GetString() : "Refresh failed"; return Unauthorized(new { ok = false, error }); } } catch (Exception ex) { _log.LogError(ex, "Refresh error"); return StatusCode(500, new { ok = false, error = "Session refresh failed" }); } } /// /// Get current session info /// [HttpGet("me")] public async Task Me(CancellationToken ct) { var token = ExtractSessionToken(); if (string.IsNullOrWhiteSpace(token)) { return Unauthorized(new { ok = false, error = "No session token provided" }); } var rqst = JsonSerializer.Serialize(new { sessionToken = token }); try { var resp = await _sql.ExecProcAsync("dbo.spClientSession", "validate", rqst, ct: ct); if (string.IsNullOrWhiteSpace(resp)) { return StatusCode(500, new { ok = false, error = "Session service unavailable" }); } using var doc = JsonDocument.Parse(resp); var root = doc.RootElement; if (root.TryGetProperty("ok", out var okProp) && okProp.GetBoolean()) { return Content(resp, "application/json"); } else { var error = root.TryGetProperty("error", out var errProp) ? errProp.GetString() : "Invalid session"; return Unauthorized(new { ok = false, error }); } } catch (Exception ex) { _log.LogError(ex, "Me error"); return StatusCode(500, new { ok = false, error = "Session validation failed" }); } } /// /// Switch to a different client context (for multi-client users) /// [HttpPost("switch-client")] public async Task SwitchClient([FromBody] SwitchClientRequest request, CancellationToken ct) { var token = ExtractSessionToken(); if (string.IsNullOrWhiteSpace(token)) { return Unauthorized(new { ok = false, error = "No session token provided" }); } if (string.IsNullOrWhiteSpace(request.ClientId)) { return BadRequest(new { ok = false, error = "clientId is required" }); } var rqst = JsonSerializer.Serialize(new { sessionToken = token, clientId = request.ClientId }); try { var resp = await _sql.ExecProcAsync("dbo.spClientSession", "switchClient", rqst, ct: ct); if (string.IsNullOrWhiteSpace(resp)) { return StatusCode(500, new { ok = false, error = "Session service unavailable" }); } using var doc = JsonDocument.Parse(resp); var root = doc.RootElement; if (root.TryGetProperty("ok", out var okProp) && okProp.GetBoolean()) { return Content(resp, "application/json"); } else { var error = root.TryGetProperty("error", out var errProp) ? errProp.GetString() : "Switch failed"; return BadRequest(new { ok = false, error }); } } catch (Exception ex) { _log.LogError(ex, "SwitchClient error"); return StatusCode(500, new { ok = false, error = "Client switch failed" }); } } private string? ExtractSessionToken() { // Check X-Session-Token header first var token = Request.Headers["X-Session-Token"].FirstOrDefault(); if (!string.IsNullOrWhiteSpace(token)) return token; // Check Authorization header — accept both "Session " and "Bearer ". // NOTE: Bearer here is a session token (not an Entra JWT) because the middleware // only routes to these controller actions after session validation succeeds. // The JWT-only endpoint (/api/auth/session) never calls ExtractSessionToken(). var auth = Request.Headers.Authorization.FirstOrDefault(); if (!string.IsNullOrWhiteSpace(auth)) { if (auth.StartsWith("Session ", StringComparison.OrdinalIgnoreCase)) return auth.Substring(8).Trim(); if (auth.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase)) return auth.Substring(7).Trim(); } return null; } private string? GetClientIp() { var forwarded = Request.Headers["X-Forwarded-For"].FirstOrDefault(); if (!string.IsNullOrWhiteSpace(forwarded)) return forwarded.Split(',')[0].Trim(); return HttpContext.Connection.RemoteIpAddress?.ToString(); } } public sealed class CreateSessionRequest { public string? PreferredClientId { get; set; } public int? SessionDurationHours { get; set; } } public sealed class RefreshRequest { public int? SessionDurationHours { get; set; } } public sealed class SwitchClientRequest { public string? ClientId { get; set; } } public sealed class RegisterRequest { public string? CompanyName { get; set; } public string? Industry { get; set; } public string? Website { get; set; } }