261 lines
9.3 KiB
C#
261 lines
9.3 KiB
C#
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<KratosApi> _logger;
|
|
|
|
public KratosApi(IConfiguration config, ILogger<KratosApi> 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<FlowDto> 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<SignupResultDto> 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<FlowDto> 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<LoginResultDto> 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<SessionDto> 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<dynamic> 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<dynamic> 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<JsonElement>(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<JsonElement>(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<JsonElement>(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";
|
|
}
|
|
}
|