Add project files.
This commit is contained in:
382
Gateway/Controllers/AuthController.cs
Normal file
382
Gateway/Controllers/AuthController.cs
Normal file
@@ -0,0 +1,382 @@
|
||||
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);
|
||||
|
||||
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 spSession with: {Rqst}", rqst);
|
||||
|
||||
try
|
||||
{
|
||||
var resp = await _sql.ExecProcAsync("dbo.spSession", "createFromIdentity", rqst, ct: ct);
|
||||
|
||||
_log.LogWarning("[Session] spSession 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 });
|
||||
|
||||
try
|
||||
{
|
||||
await _sql.ExecProcAsync("dbo.spSession", "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
|
||||
});
|
||||
|
||||
try
|
||||
{
|
||||
var resp = await _sql.ExecProcAsync("dbo.spSession", "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.spSession", "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.spSession", "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 (for session tokens, not JWTs)
|
||||
var auth = Request.Headers.Authorization.FirstOrDefault();
|
||||
if (!string.IsNullOrWhiteSpace(auth) && auth.StartsWith("Session ", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return auth.Substring(8).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; }
|
||||
}
|
||||
Reference in New Issue
Block a user