using System.Text.Json; using Ory.Kratos.Client.Api; using khmereid_backend.Dtos; using Ory.Kratos.Client.Model; using Ory.Kratos.Client.Client; namespace khmereid_backend.Integrations; public class KratosApi : IAuthnApi { private readonly FrontendApi _frontendApi; private readonly IdentityApi _identityApi; private readonly ILogger _logger; public KratosApi(IConfiguration config, ILogger logger) { var publicCfg = new Configuration { BasePath = config["Ory:Kratos:PublicUrl"]! }; var adminCfg = new Configuration { BasePath = config["Ory:Kratos:AdminUrl"]! }; _frontendApi = new FrontendApi(publicCfg); _identityApi = new IdentityApi(adminCfg); _logger = logger; } // --------------------------- Registration Flows --------------------------- public async Task CreateOtpRegistrationFlowAsync(string phone) { var flow = await _frontendApi.CreateNativeRegistrationFlowAsync(returnSessionTokenExchangeCode: true); _logger.LogInformation("Started registration flow: {FlowId}", flow.Id); phone = phone.Trim(); var method = new KratosUpdateRegistrationFlowWithCodeMethod(method: "code", traits: new { phone }); var body = new KratosUpdateRegistrationFlowBody(method); try { var updatedFlow = await _frontendApi.UpdateRegistrationFlowAsync(flow.Id, body); return new FlowDto{}; } catch (ApiException ex) { return HandleFlowException(ex); } } public async Task CompleteOtpRegistrationFlowAsync(string otp, string phone, string flowId) { phone = phone.Trim(); var method = new KratosUpdateRegistrationFlowWithCodeMethod(code: otp, method: "code", traits: new { phone }); var body = new KratosUpdateRegistrationFlowBody(method); try { var result = await _frontendApi.UpdateRegistrationFlowAsync(flowId, body); _logger.LogInformation("Completed OTP registration flow for flow {FlowId}", flowId); return new SignupResultDto { IdentityId = Guid.Parse(result.Identity.Id), AccessToken = result.SessionToken, ExpiredAt = result.Session?.ExpiresAt }; } catch (ApiException ex) { return HandleRegistrationException(ex); } } // --------------------------- Login Flows --------------------------- public async Task CreateOtpLoginFlowAsync(string phone) { var flow = await _frontendApi.CreateNativeLoginFlowAsync(returnSessionTokenExchangeCode: true); _logger.LogInformation("Started login flow: {FlowId}", flow.Id); phone = phone.Trim(); var method = new KratosUpdateLoginFlowWithCodeMethod(method: "code", csrfToken: "dfdfdfde", identifier: phone); var body = new KratosUpdateLoginFlowBody(method); try { var updatedFlow = await _frontendApi.UpdateLoginFlowAsync(flow.Id, body); return new FlowDto{}; } catch (ApiException ex) { return HandleFlowException(ex); } } public async Task CompleteOtpLoginFlowAsync(string flowId, string phone, string otp) { phone = "+" + phone.Trim(); var method = new KratosUpdateLoginFlowWithCodeMethod(code: otp, method: "code", csrfToken: "dfdfdfde", identifier: phone); var body = new KratosUpdateLoginFlowBody(method); try { var result = await _frontendApi.UpdateLoginFlowAsync(flowId, body); _logger.LogInformation("Completed OTP login flow for flow {FlowId}", flowId); return new LoginResultDto { AccessToken = result.SessionToken, ExpiredAt = result.Session?.ExpiresAt }; } catch (ApiException ex) { return HandleLoginException(ex); } } // --------------------------- Session & Logout --------------------------- public async Task GetMeAsync(string sessionToken) { try { var session = await _frontendApi.ToSessionAsync(xSessionToken: sessionToken); return new SessionDto { Id = session.Identity.Id, ExpiresAt = session.ExpiresAt }; } catch (ApiException ex) when (ex.ErrorCode == 401 || ex.ErrorCode == 403) { throw new UnauthorizedAccessException("Invalid or expired Kratos session token", ex); } catch (ApiException ex) { throw new InvalidOperationException($"Failed to fetch session from Kratos: {ex.Message}", ex); } } public async Task Logout(string sessionToken) { var body = new KratosPerformNativeLogoutBody(sessionToken: sessionToken); await _frontendApi.PerformNativeLogoutAsync(body); _logger.LogInformation("Logged out session with token {SessionToken}", sessionToken); return new { Message = "Logged out successfully." }; } // --------------------------- Password Flow --------------------------- public async Task PasswordRegistrationFlowAsync(string flowId, string phone) { var method = new KratosUpdateRegistrationFlowWithPasswordMethod( password: "add3ae4d8ae8", traits: new { phone, identifier = phone }, method: "password" ); var body = new KratosUpdateRegistrationFlowBody(method); var result = await _frontendApi.UpdateRegistrationFlowAsync(flowId, body); _logger.LogInformation("Completed password registration flow for {Phone}", phone); return result; } // --------------------------- Private Helpers --------------------------- private static FlowDto HandleFlowException(ApiException ex) { var res = JsonSerializer.Deserialize(ex.ErrorContent?.ToString() ?? "{}"); if (ex.ErrorCode == 400 && res.TryGetProperty("state", out var stateProp) && stateProp.GetString() == "sent_email") { return new FlowDto { FlowId = res.GetProperty("id").GetString() ?? "", State = "sent_otp", ExpiresAt = res.TryGetProperty("expires_at", out var expiresAtProp) ? expiresAtProp.GetDateTime() : (DateTimeOffset?)null }; } if (ex.ErrorCode == 400) return new FlowDto { Error = "BadRequest", Code = ex.ErrorCode.ToString(), Message = GetKratosErrorMessage(res) }; throw new InvalidOperationException($"Kratos API returned {ex.ErrorCode}: {ex.Message}", ex); } private SignupResultDto HandleRegistrationException(ApiException ex) { var res = JsonSerializer.Deserialize(ex.ErrorContent?.ToString() ?? "{}"); if (ex.ErrorCode == 410 && res.TryGetProperty("use_flow_id", out var useFlowProp)) { _logger.LogWarning("Registration flow expired. Use new flow: {FlowId}", useFlowProp.GetString()); return new SignupResultDto { Error = "FlowExpired", Code = ex.ErrorCode.ToString(), Message = "Registration flow expired. Please restart registration.", UseFlowId = useFlowProp.GetString() }; } if (ex.ErrorCode == 400) return new SignupResultDto { Code = ex.ErrorCode.ToString(), Error = "BadRequest", Message = GetKratosErrorMessage(res) }; if (ex.ErrorCode == 404) return new SignupResultDto { Code = ex.ErrorCode.ToString(), Error = "NotFound", Message = "Registration flow not found." }; return new SignupResultDto { Code = ex.ErrorCode.ToString(), Error = "UnhandledException", Message = $"Unhandled Kratos API exception: {ex.Message}" }; } private static LoginResultDto HandleLoginException(ApiException ex) { var res = JsonSerializer.Deserialize(ex.ErrorContent?.ToString() ?? "{}"); if (ex.ErrorCode == 404) return new LoginResultDto { Error = "NotFound", Code = ex.ErrorCode.ToString(), Message = "Login flow not found." }; return new LoginResultDto { Error = "BadRequest", Code = ex.ErrorCode.ToString(), Message = GetKratosErrorMessage(res) }; } private static string GetKratosErrorMessage(JsonElement res) { if (res.TryGetProperty("error", out var errProp) && errProp.TryGetProperty("reason", out var reasonProp)) return reasonProp.GetString() ?? "Bad request"; if (res.TryGetProperty("ui", out var uiProp) && uiProp.TryGetProperty("messages", out var messagesProp) && messagesProp.ValueKind == JsonValueKind.Array && messagesProp.GetArrayLength() > 0 && messagesProp[0].TryGetProperty("text", out var textProp)) return textProp.GetString() ?? "Bad request"; return "Bad request"; } }