402 lines
14 KiB
C#
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; }
|
|
}
|