Files
AdPlatform-Server/Gateway/Controllers/AuthController.cs
2026-03-14 13:50:09 -07:00

402 lines
14 KiB
C#

using Gateway.Data;
using Gateway.Security;
using Microsoft.AspNetCore.Mvc;
using System.Text.Json;
namespace Gateway.Controllers;
/// <summary>
/// Authentication endpoints for session management.
/// Sessions are created after Entra External ID authentication.
/// </summary>
[ApiController]
[Route("api/auth")]
public sealed class AuthController : ControllerBase
{
private readonly SqlService _sql;
private readonly ClientContext _client;
private readonly ILogger<AuthController> _log;
public AuthController(SqlService sql, ClientContext client, ILogger<AuthController> log)
{
_sql = sql;
_client = client;
_log = log;
}
/// <summary>
/// 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.
/// </summary>
[HttpPost("session")]
public async Task<IActionResult> 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 });
}
}
/// <summary>
/// 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.
/// </summary>
[HttpPost("register")]
public async Task<IActionResult> 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 });
}
}
/// <summary>
/// Sign off (invalidate session)
/// </summary>
[HttpPost("signoff")]
public async Task<IActionResult> 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" });
}
}
/// <summary>
/// Refresh session (extend expiration)
/// </summary>
[HttpPost("refresh")]
public async Task<IActionResult> 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" });
}
}
/// <summary>
/// Get current session info
/// </summary>
[HttpGet("me")]
public async Task<IActionResult> 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" });
}
}
/// <summary>
/// Switch to a different client context (for multi-client users)
/// </summary>
[HttpPost("switch-client")]
public async Task<IActionResult> 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 <token>" and "Bearer <token>".
// 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; }
}