forked from baron/baron-sso
Merge origin/main and remove Descope deps
This commit is contained in:
@@ -25,8 +25,6 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/descope/go-sdk/descope"
|
||||
"github.com/descope/go-sdk/descope/client"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
@@ -80,18 +78,16 @@ const (
|
||||
)
|
||||
|
||||
type AuthHandler struct {
|
||||
ProjectID string
|
||||
SmsService domain.SmsService
|
||||
EmailService domain.EmailService
|
||||
RedisService *service.RedisService
|
||||
DescopeClient *client.DescopeClient
|
||||
KratosAdmin *service.KratosAdminService
|
||||
IdpProvider domain.IdentityProvider
|
||||
AuditRepo domain.AuditRepository
|
||||
OathkeeperRepo domain.OathkeeperLogRepository
|
||||
Hydra *service.HydraAdminService
|
||||
TenantService service.TenantService
|
||||
KetoService service.KetoService
|
||||
KetoService service.KetoService
|
||||
UserRepo repository.UserRepository
|
||||
}
|
||||
|
||||
@@ -151,34 +147,17 @@ func checkPollInterval(redis *service.RedisService, key string, interval time.Du
|
||||
}
|
||||
|
||||
func NewAuthHandler(redisService *service.RedisService, idpProvider domain.IdentityProvider, auditRepo domain.AuditRepository, oathkeeperRepo domain.OathkeeperLogRepository, tenantService service.TenantService, ketoService service.KetoService, userRepo repository.UserRepository) *AuthHandler {
|
||||
projectID := os.Getenv("DESCOPE_PROJECT_ID")
|
||||
managementKey := os.Getenv("DESCOPE_MANAGEMENT_KEY")
|
||||
|
||||
var descopeClient *client.DescopeClient
|
||||
var err error
|
||||
if projectID != "" {
|
||||
descopeClient, err = client.NewWithConfig(&client.Config{
|
||||
ProjectID: projectID,
|
||||
ManagementKey: managementKey,
|
||||
})
|
||||
if err != nil {
|
||||
slog.Warn("Failed to initialize Descope Client", "error", err)
|
||||
}
|
||||
}
|
||||
|
||||
return &AuthHandler{
|
||||
ProjectID: projectID,
|
||||
SmsService: service.NewSmsService(),
|
||||
EmailService: service.NewEmailService(),
|
||||
RedisService: redisService,
|
||||
DescopeClient: descopeClient,
|
||||
KratosAdmin: service.NewKratosAdminService(),
|
||||
IdpProvider: idpProvider,
|
||||
AuditRepo: auditRepo,
|
||||
OathkeeperRepo: oathkeeperRepo,
|
||||
Hydra: service.NewHydraAdminService(),
|
||||
TenantService: tenantService,
|
||||
KetoService: ketoService,
|
||||
KetoService: ketoService,
|
||||
UserRepo: userRepo,
|
||||
}
|
||||
}
|
||||
@@ -424,7 +403,7 @@ func (h *AuthHandler) Signup(c *fiber.Ctx) error {
|
||||
} else {
|
||||
slog.Warn("[Signup] Attempted to join non-active tenant", "email", req.Email, "tenant", tenant.Slug, "status", tenant.Status)
|
||||
// Policy: If tenant exists but not active, reject signup or allow as general?
|
||||
// For now, let's allow as general but log it.
|
||||
// For now, let's allow as general but log it.
|
||||
// Or return error if we want strict domain locking.
|
||||
return c.Status(fiber.StatusForbidden).JSON(fiber.Map{"error": "Your organization's tenant is currently not active."})
|
||||
}
|
||||
@@ -1528,7 +1507,7 @@ func (h *AuthHandler) PasswordLogin(c *fiber.Ctx) error {
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
ale.Status = fiber.StatusBadRequest
|
||||
ale.LatencyMs = time.Since(startTime)
|
||||
ale.DescopeError = err.Error()
|
||||
ale.ProviderError = err.Error()
|
||||
ale.Log(slog.LevelError, "Body parse error")
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid request body"})
|
||||
}
|
||||
@@ -1543,7 +1522,7 @@ func (h *AuthHandler) PasswordLogin(c *fiber.Ctx) error {
|
||||
if h.IdpProvider == nil {
|
||||
ale.Status = fiber.StatusInternalServerError
|
||||
ale.LatencyMs = time.Since(startTime)
|
||||
ale.DescopeError = "IDP Provider is nil"
|
||||
ale.ProviderError = "IDP Provider is nil"
|
||||
ale.Log(slog.LevelError, "IDP Provider is nil")
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Authentication service not configured"})
|
||||
}
|
||||
@@ -1555,7 +1534,7 @@ func (h *AuthHandler) PasswordLogin(c *fiber.Ctx) error {
|
||||
}
|
||||
ale.Status = fiber.StatusUnauthorized
|
||||
ale.LatencyMs = time.Since(startTime)
|
||||
ale.DescopeError = err.Error()
|
||||
ale.ProviderError = err.Error()
|
||||
ale.Log(slog.LevelWarn, "IDP sign-in failed", slog.String("provider", h.IdpProvider.Name()))
|
||||
if strings.Contains(err.Error(), "not found") || strings.Contains(err.Error(), "identity") {
|
||||
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "User not registered"})
|
||||
@@ -1605,7 +1584,7 @@ func (h *AuthHandler) PasswordLogin(c *fiber.Ctx) error {
|
||||
return c.JSON(resp)
|
||||
}
|
||||
|
||||
// InitiatePasswordReset - 사용자가 비밀번호 재설정을 시작하면, loginID 유형에 따라 Descope를 통해 이메일 또는 SMS를 보냅니다.
|
||||
// InitiatePasswordReset - 사용자가 비밀번호 재설정을 시작하면, loginID 유형에 따라 이메일 또는 SMS를 보냅니다.
|
||||
func (h *AuthHandler) InitiatePasswordReset(c *fiber.Ctx) error {
|
||||
startTime := time.Now()
|
||||
ale := logger.NewAuditLogEntry(c, "initiate")
|
||||
@@ -1614,7 +1593,7 @@ func (h *AuthHandler) InitiatePasswordReset(c *fiber.Ctx) error {
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
ale.Status = fiber.StatusBadRequest
|
||||
ale.LatencyMs = time.Since(startTime)
|
||||
ale.DescopeError = err.Error()
|
||||
ale.ProviderError = err.Error()
|
||||
ale.Log(slog.LevelError, "Body parse error")
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid request body"})
|
||||
}
|
||||
@@ -1626,7 +1605,7 @@ func (h *AuthHandler) InitiatePasswordReset(c *fiber.Ctx) error {
|
||||
if loginID == "" {
|
||||
ale.Status = fiber.StatusBadRequest
|
||||
ale.LatencyMs = time.Since(startTime)
|
||||
ale.DescopeError = "Login ID is required"
|
||||
ale.ProviderError = "Login ID is required"
|
||||
ale.Log(slog.LevelWarn, "Login ID missing")
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Login ID is required"})
|
||||
}
|
||||
@@ -1634,7 +1613,7 @@ func (h *AuthHandler) InitiatePasswordReset(c *fiber.Ctx) error {
|
||||
if h.IdpProvider == nil {
|
||||
ale.Status = fiber.StatusInternalServerError
|
||||
ale.LatencyMs = time.Since(startTime)
|
||||
ale.DescopeError = "IDP Provider is not initialized"
|
||||
ale.ProviderError = "IDP Provider is not initialized"
|
||||
ale.Log(slog.LevelError, "IDP Provider is not initialized")
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Authentication service not configured"})
|
||||
}
|
||||
@@ -1643,7 +1622,7 @@ func (h *AuthHandler) InitiatePasswordReset(c *fiber.Ctx) error {
|
||||
if userfrontURL == "" {
|
||||
ale.Status = fiber.StatusInternalServerError
|
||||
ale.LatencyMs = time.Since(startTime)
|
||||
ale.DescopeError = "USERFRONT_URL is not set"
|
||||
ale.ProviderError = "USERFRONT_URL is not set"
|
||||
ale.Log(slog.LevelError, "USERFRONT_URL is not set")
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "USERFRONT_URL environment variable is not set"})
|
||||
}
|
||||
@@ -1656,7 +1635,7 @@ func (h *AuthHandler) InitiatePasswordReset(c *fiber.Ctx) error {
|
||||
if resetToken == "" {
|
||||
ale.Status = fiber.StatusInternalServerError
|
||||
ale.LatencyMs = time.Since(startTime)
|
||||
ale.DescopeError = "Failed to generate reset token"
|
||||
ale.ProviderError = "Failed to generate reset token"
|
||||
ale.Log(slog.LevelError, "Failed to generate reset token")
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to generate reset token"})
|
||||
}
|
||||
@@ -1664,7 +1643,7 @@ func (h *AuthHandler) InitiatePasswordReset(c *fiber.Ctx) error {
|
||||
if err := h.RedisService.Set(prefixPwdResetToken+resetToken, loginID, pwdResetExpiration); err != nil {
|
||||
ale.Status = fiber.StatusInternalServerError
|
||||
ale.LatencyMs = time.Since(startTime)
|
||||
ale.DescopeError = err.Error()
|
||||
ale.ProviderError = err.Error()
|
||||
ale.Log(slog.LevelError, "Failed to store reset token in Redis")
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to store reset token"})
|
||||
}
|
||||
@@ -1682,7 +1661,7 @@ func (h *AuthHandler) InitiatePasswordReset(c *fiber.Ctx) error {
|
||||
if !drySend && h.EmailService == nil {
|
||||
ale.Status = fiber.StatusInternalServerError
|
||||
ale.LatencyMs = time.Since(startTime)
|
||||
ale.DescopeError = "Email service not configured"
|
||||
ale.ProviderError = "Email service not configured"
|
||||
ale.Log(slog.LevelError, "Email service not configured")
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Email service not configured"})
|
||||
}
|
||||
@@ -1703,7 +1682,7 @@ func (h *AuthHandler) InitiatePasswordReset(c *fiber.Ctx) error {
|
||||
if err := h.EmailService.SendEmail(loginID, subject, body); err != nil {
|
||||
ale.Status = fiber.StatusInternalServerError
|
||||
ale.LatencyMs = time.Since(startTime)
|
||||
ale.DescopeError = err.Error()
|
||||
ale.ProviderError = err.Error()
|
||||
ale.Log(slog.LevelError, "Failed to send reset email", slog.String("loginId", loginID))
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to send reset email"})
|
||||
}
|
||||
@@ -1716,7 +1695,7 @@ func (h *AuthHandler) InitiatePasswordReset(c *fiber.Ctx) error {
|
||||
if err := h.SmsService.SendSms(loginID, resetSms); err != nil {
|
||||
ale.Status = fiber.StatusInternalServerError
|
||||
ale.LatencyMs = time.Since(startTime)
|
||||
ale.DescopeError = err.Error()
|
||||
ale.ProviderError = err.Error()
|
||||
ale.Log(slog.LevelError, "Failed to send reset SMS", slog.String("loginId", loginID))
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to send reset SMS"})
|
||||
}
|
||||
@@ -1793,7 +1772,7 @@ func (h *AuthHandler) ProcessPasswordResetToken(c *fiber.Ctx) error {
|
||||
if token == "" {
|
||||
ale.Status = fiber.StatusBadRequest
|
||||
ale.LatencyMs = time.Since(startTime)
|
||||
ale.DescopeError = "Missing token"
|
||||
ale.ProviderError = "Missing token"
|
||||
ale.Log(slog.LevelWarn, "Missing token in request")
|
||||
return c.Status(fiber.StatusBadRequest).SendString("Missing token")
|
||||
}
|
||||
@@ -1802,7 +1781,7 @@ func (h *AuthHandler) ProcessPasswordResetToken(c *fiber.Ctx) error {
|
||||
if err != nil || loginID == "" {
|
||||
ale.Status = fiber.StatusUnauthorized
|
||||
ale.LatencyMs = time.Since(startTime)
|
||||
ale.DescopeError = "Invalid or expired reset token"
|
||||
ale.ProviderError = "Invalid or expired reset token"
|
||||
ale.Log(slog.LevelWarn, "Reset token invalid or expired")
|
||||
return c.Status(fiber.StatusUnauthorized).SendString("Invalid or expired token")
|
||||
}
|
||||
@@ -1842,7 +1821,7 @@ func (h *AuthHandler) CompletePasswordReset(c *fiber.Ctx) error {
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
ale.Status = fiber.StatusBadRequest
|
||||
ale.LatencyMs = time.Since(startTime)
|
||||
ale.DescopeError = err.Error()
|
||||
ale.ProviderError = err.Error()
|
||||
ale.Log(slog.LevelError, "Body parse error")
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid request body"})
|
||||
}
|
||||
@@ -1875,7 +1854,7 @@ func (h *AuthHandler) CompletePasswordReset(c *fiber.Ctx) error {
|
||||
if loginID == "" || req.NewPassword == "" {
|
||||
ale.Status = fiber.StatusBadRequest
|
||||
ale.LatencyMs = time.Since(startTime)
|
||||
ale.DescopeError = "Login ID and new password are required"
|
||||
ale.ProviderError = "Login ID and new password are required"
|
||||
ale.Log(slog.LevelWarn, "Login ID or new password missing")
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Login ID and new password are required"})
|
||||
}
|
||||
@@ -1887,7 +1866,7 @@ func (h *AuthHandler) CompletePasswordReset(c *fiber.Ctx) error {
|
||||
if err := validatePasswordWithPolicy(policy, req.NewPassword); err != nil {
|
||||
ale.Status = fiber.StatusBadRequest
|
||||
ale.LatencyMs = time.Since(startTime)
|
||||
ale.DescopeError = err.Error()
|
||||
ale.ProviderError = err.Error()
|
||||
ale.Log(slog.LevelWarn, "Validation failed: "+err.Error())
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": err.Error()})
|
||||
}
|
||||
@@ -1897,7 +1876,7 @@ func (h *AuthHandler) CompletePasswordReset(c *fiber.Ctx) error {
|
||||
if h.IdpProvider == nil {
|
||||
ale.Status = fiber.StatusInternalServerError
|
||||
ale.LatencyMs = time.Since(startTime)
|
||||
ale.DescopeError = "IDP Provider is nil"
|
||||
ale.ProviderError = "IDP Provider is nil"
|
||||
ale.Log(slog.LevelError, "IDP Provider is nil")
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Authentication service not configured"})
|
||||
}
|
||||
@@ -1905,7 +1884,7 @@ func (h *AuthHandler) CompletePasswordReset(c *fiber.Ctx) error {
|
||||
if err := h.IdpProvider.UpdateUserPassword(loginID, req.NewPassword, nil); err != nil {
|
||||
ale.Status = fiber.StatusInternalServerError
|
||||
ale.LatencyMs = time.Since(startTime)
|
||||
ale.DescopeError = err.Error()
|
||||
ale.ProviderError = err.Error()
|
||||
ale.Log(slog.LevelError, "Failed to update password via IDP")
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to update password"})
|
||||
}
|
||||
@@ -2042,20 +2021,6 @@ func (h *AuthHandler) ScanQRLogin(c *fiber.Ctx) error {
|
||||
}
|
||||
|
||||
// 2. 모바일 토큰은 승인 검증용으로만 사용하고, 웹 전용 세션을 새로 발급
|
||||
if sessionToken, loginID, approvedSessionID, err := h.tryIssueDescopeQrSession(c, req.Token); err != nil {
|
||||
slog.Error("[QR] Issue web session failed", "error", err)
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to issue web session"})
|
||||
} else if sessionToken != nil && sessionToken.JWT != "" {
|
||||
h.storeQrApproverSessionID(pendingRef, approvedSessionID)
|
||||
h.writeQrAuditLog(loginID, pendingRef, sessionToken, approvedSessionID)
|
||||
sessionData, _ := json.Marshal(map[string]string{
|
||||
"status": statusSuccess,
|
||||
"jwt": sessionToken.JWT,
|
||||
})
|
||||
h.RedisService.Set(prefixSession+pendingRef, string(sessionData), 5*time.Minute)
|
||||
return c.JSON(fiber.Map{"message": "QR Login Approved"})
|
||||
}
|
||||
|
||||
approvedSessionID := ""
|
||||
if req.Token != "" {
|
||||
if sessionID, err := h.getKratosSessionID(req.Token); err == nil {
|
||||
@@ -2079,11 +2044,6 @@ func (h *AuthHandler) ScanQRLogin(c *fiber.Ctx) error {
|
||||
return c.JSON(fiber.Map{"message": "QR Login Approved"})
|
||||
}
|
||||
|
||||
// ProxyToDescope (Placeholder)
|
||||
func (h *AuthHandler) ProxyToDescope(c *fiber.Ctx, path string, payload interface{}) error {
|
||||
return c.Status(501).SendString("Descope Proxy Disabled")
|
||||
}
|
||||
|
||||
type kratosCourierRequest struct {
|
||||
Recipient string `json:"recipient"`
|
||||
TemplateType string `json:"template_type"`
|
||||
@@ -2480,82 +2440,6 @@ func sanitizePhoneForSms(phone string) string {
|
||||
return sanitized
|
||||
}
|
||||
|
||||
// HandleDescopeSmsRelay
|
||||
func (h *AuthHandler) HandleDescopeSmsRelay(c *fiber.Ctx) error {
|
||||
var req struct {
|
||||
Recipient string `json:"recipient"`
|
||||
Body string `json:"body"`
|
||||
}
|
||||
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
slog.Error("Webhook Body parsing failed", "error", err)
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid request body"})
|
||||
}
|
||||
|
||||
if req.Recipient == "" || req.Body == "" {
|
||||
slog.Warn("Webhook missing recipient or body")
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Missing recipient or body"})
|
||||
}
|
||||
|
||||
slog.Info("Received SMS request", "recipient", req.Recipient)
|
||||
|
||||
phone := req.Recipient
|
||||
if strings.HasPrefix(phone, "+82") {
|
||||
phone = "0" + phone[3:]
|
||||
}
|
||||
phone = strings.ReplaceAll(phone, "-", "")
|
||||
phone = strings.ReplaceAll(phone, " ", "")
|
||||
|
||||
if err := h.SmsService.SendSms(phone, req.Body); err != nil {
|
||||
slog.Error("Failed to forward SMS to Naver", "error", err)
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to send SMS via Naver"})
|
||||
}
|
||||
|
||||
slog.Info("Successfully forwarded SMS", "phone", phone)
|
||||
return c.JSON(fiber.Map{"status": "ok"})
|
||||
}
|
||||
|
||||
// HandleDescopeEmailRelay - Webhook for Descope Generic Email Gateway
|
||||
// Used for "Fake Email Strategy" to support Polling with SMS.
|
||||
func (h *AuthHandler) HandleDescopeEmailRelay(c *fiber.Ctx) error {
|
||||
var req struct {
|
||||
To string `json:"to"` // e.g., 01012345678@sms.baron
|
||||
Subject string `json:"subject"`
|
||||
Text string `json:"text"` // Body containing the link
|
||||
}
|
||||
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
slog.Error("[Email Webhook] Body parsing failed", "error", err)
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid request body"})
|
||||
}
|
||||
|
||||
slog.Info("[Email Webhook] Received email request", "to", req.To)
|
||||
|
||||
// Check if it's a Fake Email for SMS
|
||||
if strings.HasSuffix(req.To, "@sms.baron") {
|
||||
phone := strings.Split(req.To, "@")[0]
|
||||
|
||||
// Sanitize Phone (Descope might sanitize or not, but let's be safe)
|
||||
if strings.HasPrefix(phone, "+82") {
|
||||
phone = "0" + phone[3:]
|
||||
}
|
||||
|
||||
// Send SMS with the text body (Descope template should be optimized for SMS)
|
||||
if err := h.SmsService.SendSms(phone, req.Text); err != nil {
|
||||
slog.Error("[Email Webhook] Failed to forward Email-as-SMS", "error", err)
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to send SMS"})
|
||||
}
|
||||
|
||||
slog.Info("[Email Webhook] Successfully converted Email to SMS", "phone", phone)
|
||||
return c.JSON(fiber.Map{"status": "ok"})
|
||||
}
|
||||
|
||||
// Real Email Handling (Not implemented in this Relay)
|
||||
// You would need an SMTP service here if you route ALL emails through this relay.
|
||||
slog.Warn("[Email Webhook] Real email skipped (Not implemented)", "to", req.To)
|
||||
return c.Status(501).JSON(fiber.Map{"error": "Real email sending not implemented"})
|
||||
}
|
||||
|
||||
// --- User Profile Handlers ---
|
||||
|
||||
func (h *AuthHandler) formatPhoneForDisplay(phone string) string {
|
||||
@@ -2581,6 +2465,7 @@ func (h *AuthHandler) GetMe(c *fiber.Ctx) error {
|
||||
}
|
||||
return c.JSON(profile)
|
||||
}
|
||||
|
||||
// GetEnrichedProfile - Exported wrapper for resolveCurrentProfile used by middlewares
|
||||
func (h *AuthHandler) GetEnrichedProfile(c *fiber.Ctx) (*domain.UserProfileResponse, error) {
|
||||
return h.resolveCurrentProfile(c)
|
||||
@@ -2975,6 +2860,17 @@ func (h *AuthHandler) GetAuthTimeline(c *fiber.Ctx) error {
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "Invalid session"})
|
||||
}
|
||||
currentSessionID := ""
|
||||
if token := h.getBearerToken(c); token != "" {
|
||||
currentSessionID = extractSessionIDFromJWT(token)
|
||||
}
|
||||
if currentSessionID == "" {
|
||||
if cookie := c.Get("Cookie"); cookie != "" {
|
||||
if sessionID, err := h.getKratosSessionIDWithCookie(cookie); err == nil {
|
||||
currentSessionID = sessionID
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
subject := ""
|
||||
if h.OathkeeperRepo != nil {
|
||||
@@ -3056,7 +2952,7 @@ func (h *AuthHandler) GetAuthTimeline(c *fiber.Ctx) error {
|
||||
if !isAuthEventType(log.EventType) {
|
||||
continue
|
||||
}
|
||||
if !matchesAuthTimelineUser(log, profile, candidates) {
|
||||
if !matchesAuthTimelineUser(log, profile, candidates, currentSessionID) {
|
||||
continue
|
||||
}
|
||||
if shouldSkipAuthTimeline(log) {
|
||||
@@ -3467,7 +3363,9 @@ func (h *AuthHandler) AcceptConsentRequest(c *fiber.Ctx) error {
|
||||
|
||||
func (h *AuthHandler) AcceptOidcLoginRequest(c *fiber.Ctx) error {
|
||||
var req struct {
|
||||
LoginChallenge string `json:"login_challenge"`
|
||||
LoginChallenge string `json:"login_challenge"`
|
||||
ApprovedSessionID string `json:"approved_session_id,omitempty"`
|
||||
SessionID string `json:"session_id,omitempty"`
|
||||
}
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
|
||||
@@ -3481,13 +3379,31 @@ func (h *AuthHandler) AcceptOidcLoginRequest(c *fiber.Ctx) error {
|
||||
return fiber.NewError(fiber.StatusUnauthorized, "Authentication required")
|
||||
}
|
||||
c.Locals("user_id", subject)
|
||||
if sessionID, ok := c.Locals("session_id").(string); ok && sessionID != "" {
|
||||
c.Locals("approved_session_id", sessionID)
|
||||
} else if token := h.getBearerToken(c); token != "" {
|
||||
if derivedID := extractSessionIDFromJWT(token); derivedID != "" {
|
||||
c.Locals("approved_session_id", derivedID)
|
||||
approvedSessionID := strings.TrimSpace(req.ApprovedSessionID)
|
||||
if approvedSessionID == "" {
|
||||
approvedSessionID = strings.TrimSpace(req.SessionID)
|
||||
}
|
||||
if approvedSessionID == "" {
|
||||
if sessionID, ok := c.Locals("session_id").(string); ok && sessionID != "" {
|
||||
approvedSessionID = sessionID
|
||||
}
|
||||
}
|
||||
if approvedSessionID == "" {
|
||||
if token := h.getBearerToken(c); token != "" {
|
||||
approvedSessionID = extractSessionIDFromJWT(token)
|
||||
}
|
||||
}
|
||||
if approvedSessionID == "" {
|
||||
if cookie := c.Get("Cookie"); cookie != "" {
|
||||
if derivedID, err := h.getKratosSessionIDWithCookie(cookie); err == nil {
|
||||
approvedSessionID = derivedID
|
||||
}
|
||||
}
|
||||
}
|
||||
if approvedSessionID != "" {
|
||||
c.Locals("session_id", approvedSessionID)
|
||||
c.Locals("approved_session_id", approvedSessionID)
|
||||
}
|
||||
if h.KratosAdmin != nil {
|
||||
if identity, err := h.KratosAdmin.GetIdentity(c.Context(), subject); err == nil && identity != nil {
|
||||
if loginID := pickLoginIDFromTraits(identity.Traits); loginID != "" {
|
||||
@@ -3506,65 +3422,12 @@ func (h *AuthHandler) AcceptOidcLoginRequest(c *fiber.Ctx) error {
|
||||
}
|
||||
|
||||
func (h *AuthHandler) resolveCurrentProfile(c *fiber.Ctx) (*domain.UserProfileResponse, error) {
|
||||
// [Development Mode Fallback]
|
||||
if os.Getenv("APP_ENV") != "production" {
|
||||
// 우선순위: 1. 헤더, 2. 쿠키, 3. 기본값(user)
|
||||
testRole := c.Get("X-Test-Role")
|
||||
if testRole == "" {
|
||||
testRole = c.Cookies("X-Mock-Role")
|
||||
}
|
||||
|
||||
if testRole == "" {
|
||||
testRole = domain.RoleUser // 기본값을 user로 변경하여 차단 확인
|
||||
}
|
||||
|
||||
slog.Info("Using MOCK profile", "role", testRole, "source", "dev_fallback")
|
||||
return &domain.UserProfileResponse{
|
||||
ID: "dev-admin-uuid",
|
||||
Email: "dev-admin@baron.local",
|
||||
Name: "Dev Admin (" + testRole + ")",
|
||||
Role: testRole,
|
||||
CompanyCode: "hanmac",
|
||||
}, nil
|
||||
}
|
||||
|
||||
var profile *domain.UserProfileResponse
|
||||
var err error
|
||||
|
||||
token := h.getBearerToken(c)
|
||||
if token != "" {
|
||||
if looksLikeJWT(token) && h.DescopeClient != nil {
|
||||
authorized, userToken, err := h.DescopeClient.Auth.ValidateSessionWithToken(c.Context(), token)
|
||||
if err == nil && authorized {
|
||||
userResponse, err := h.DescopeClient.Management.User().Load(c.Context(), userToken.ID)
|
||||
if err == nil {
|
||||
identityID, resolveErr := h.resolveKratosIdentityID(
|
||||
c.Context(),
|
||||
userResponse.Email,
|
||||
normalizePhoneForLoginID(userResponse.Phone),
|
||||
)
|
||||
if resolveErr == nil && identityID != "" {
|
||||
dept, _ := userResponse.CustomAttributes["department"].(string)
|
||||
affType, _ := userResponse.CustomAttributes["affiliationType"].(string)
|
||||
compCode, _ := userResponse.CustomAttributes["companyCode"].(string)
|
||||
profile = &domain.UserProfileResponse{
|
||||
ID: identityID,
|
||||
Email: userResponse.Email,
|
||||
Name: userResponse.Name,
|
||||
Phone: h.formatPhoneForDisplay(userResponse.Phone),
|
||||
Department: dept,
|
||||
AffiliationType: affType,
|
||||
CompanyCode: compCode,
|
||||
Metadata: userResponse.CustomAttributes,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if profile == nil {
|
||||
profile, err = h.getKratosProfile(token)
|
||||
}
|
||||
profile, err = h.getKratosProfile(token)
|
||||
} else {
|
||||
cookie := c.Get("Cookie")
|
||||
if cookie != "" {
|
||||
@@ -3604,23 +3467,6 @@ func (h *AuthHandler) resolveCurrentProfile(c *fiber.Ctx) (*domain.UserProfileRe
|
||||
func (h *AuthHandler) resolveConsentSubject(c *fiber.Ctx) (string, error) {
|
||||
token := h.getBearerToken(c)
|
||||
if token != "" {
|
||||
if looksLikeJWT(token) && h.DescopeClient != nil {
|
||||
authorized, userToken, err := h.DescopeClient.Auth.ValidateSessionWithToken(c.Context(), token)
|
||||
if err == nil && authorized {
|
||||
userResponse, loadErr := h.DescopeClient.Management.User().Load(c.Context(), userToken.ID)
|
||||
if loadErr == nil {
|
||||
identityID, resolveErr := h.resolveKratosIdentityID(
|
||||
c.Context(),
|
||||
userResponse.Email,
|
||||
normalizePhoneForLoginID(userResponse.Phone),
|
||||
)
|
||||
if resolveErr == nil {
|
||||
return identityID, nil
|
||||
}
|
||||
}
|
||||
return "", fmt.Errorf("failed to resolve kratos identity for consent subject")
|
||||
}
|
||||
}
|
||||
identityID, resolveErr := h.resolveIdentityID(c, token)
|
||||
if resolveErr == nil && identityID != "" {
|
||||
return identityID, nil
|
||||
@@ -3643,26 +3489,6 @@ func (h *AuthHandler) resolveConsentSubject(c *fiber.Ctx) (string, error) {
|
||||
|
||||
func (h *AuthHandler) resolveConsentSubjects(c *fiber.Ctx) ([]string, error) {
|
||||
token := h.getBearerToken(c)
|
||||
if token != "" && looksLikeJWT(token) && h.DescopeClient != nil {
|
||||
authorized, userToken, err := h.DescopeClient.Auth.ValidateSessionWithToken(c.Context(), token)
|
||||
if err == nil && authorized {
|
||||
subjects := make([]string, 0, 2)
|
||||
userResponse, loadErr := h.DescopeClient.Management.User().Load(c.Context(), userToken.ID)
|
||||
if loadErr == nil {
|
||||
subjects = appendLoginIDsFromValues(subjects, userResponse.Email, userResponse.Phone)
|
||||
identityID, resolveErr := h.resolveKratosIdentityID(
|
||||
c.Context(),
|
||||
userResponse.Email,
|
||||
normalizePhoneForLoginID(userResponse.Phone),
|
||||
)
|
||||
if resolveErr == nil && identityID != "" {
|
||||
subjects = append([]string{identityID}, subjects...)
|
||||
}
|
||||
}
|
||||
return uniqueStrings(subjects), nil
|
||||
}
|
||||
}
|
||||
|
||||
if token != "" {
|
||||
identityID, traits, err := h.getKratosIdentity(token)
|
||||
if err == nil && identityID != "" {
|
||||
@@ -4042,7 +3868,7 @@ func normalizeLoginIdentifier(value string) string {
|
||||
return normalizePhoneForLoginID(trimmed)
|
||||
}
|
||||
|
||||
func matchesAuthTimelineUser(log domain.AuditLog, profile *domain.UserProfileResponse, candidates map[string]struct{}) bool {
|
||||
func matchesAuthTimelineUser(log domain.AuditLog, profile *domain.UserProfileResponse, candidates map[string]struct{}, sessionID string) bool {
|
||||
if profile == nil {
|
||||
return false
|
||||
}
|
||||
@@ -4051,11 +3877,24 @@ func matchesAuthTimelineUser(log domain.AuditLog, profile *domain.UserProfileRes
|
||||
}
|
||||
loginID := extractLoginIDFromAuditDetails(log.Details)
|
||||
normalized := normalizeLoginIdentifier(loginID)
|
||||
if normalized == "" {
|
||||
if normalized != "" {
|
||||
if _, ok := candidates[normalized]; ok {
|
||||
return true
|
||||
}
|
||||
}
|
||||
if sessionID == "" {
|
||||
return false
|
||||
}
|
||||
_, ok := candidates[normalized]
|
||||
return ok
|
||||
if log.SessionID != "" && log.SessionID == sessionID {
|
||||
return true
|
||||
}
|
||||
if extracted := extractSessionIDFromAuditDetails(log.Details); extracted != "" && extracted == sessionID {
|
||||
return true
|
||||
}
|
||||
if approved := extractApprovedSessionIDFromAuditDetails(log.Details); approved != "" && approved == sessionID {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func extractLoginIDFromAuditDetails(details string) string {
|
||||
@@ -4215,52 +4054,36 @@ func extractSessionIDFromAuditDetails(details string) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (h *AuthHandler) resolveIdentityID(c *fiber.Ctx, token string) (string, error) {
|
||||
if looksLikeJWT(token) && h.DescopeClient != nil {
|
||||
authorized, userToken, err := h.DescopeClient.Auth.ValidateSessionWithToken(c.Context(), token)
|
||||
if err == nil && authorized {
|
||||
if h.KratosAdmin == nil {
|
||||
return "", fmt.Errorf("kratos admin unavailable")
|
||||
}
|
||||
userResponse, loadErr := h.DescopeClient.Management.User().Load(c.Context(), userToken.ID)
|
||||
if loadErr != nil {
|
||||
return "", loadErr
|
||||
}
|
||||
identityID, resolveErr := h.resolveKratosIdentityID(
|
||||
c.Context(),
|
||||
userResponse.Email,
|
||||
normalizePhoneForLoginID(userResponse.Phone),
|
||||
)
|
||||
if resolveErr != nil || identityID == "" {
|
||||
return "", fmt.Errorf("failed to resolve kratos identity for token")
|
||||
}
|
||||
return identityID, nil
|
||||
func extractApprovedSessionIDFromAuditDetails(details string) string {
|
||||
if details == "" {
|
||||
return ""
|
||||
}
|
||||
var payload map[string]any
|
||||
if err := json.Unmarshal([]byte(details), &payload); err != nil {
|
||||
return ""
|
||||
}
|
||||
if raw, ok := payload["approved_session_id"]; ok {
|
||||
switch value := raw.(type) {
|
||||
case string:
|
||||
return value
|
||||
default:
|
||||
return fmt.Sprint(value)
|
||||
}
|
||||
}
|
||||
id, _, err := h.getKratosIdentity(token)
|
||||
return id, err
|
||||
if raw, ok := payload["approvedSessionId"]; ok {
|
||||
switch value := raw.(type) {
|
||||
case string:
|
||||
return value
|
||||
default:
|
||||
return fmt.Sprint(value)
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (h *AuthHandler) tryIssueDescopeQrSession(c *fiber.Ctx, token string) (*domain.Token, string, string, error) {
|
||||
if !looksLikeJWT(token) || h.DescopeClient == nil {
|
||||
return nil, "", "", nil
|
||||
}
|
||||
authorized, userToken, err := h.DescopeClient.Auth.ValidateSessionWithToken(c.Context(), token)
|
||||
if err != nil || !authorized {
|
||||
return nil, "", "", nil
|
||||
}
|
||||
loginID, err := h.resolveDescopeLoginID(c.Context(), userToken)
|
||||
if err != nil {
|
||||
return nil, "", "", err
|
||||
}
|
||||
authInfo, err := h.IdpProvider.IssueSession(loginID)
|
||||
if err != nil {
|
||||
return nil, "", "", err
|
||||
}
|
||||
if authInfo == nil || authInfo.SessionToken == nil || authInfo.SessionToken.JWT == "" {
|
||||
return nil, "", "", fmt.Errorf("descope issue session returned empty token")
|
||||
}
|
||||
return authInfo.SessionToken, loginID, "", nil
|
||||
func (h *AuthHandler) resolveIdentityID(c *fiber.Ctx, token string) (string, error) {
|
||||
id, _, err := h.getKratosIdentity(token)
|
||||
return id, err
|
||||
}
|
||||
|
||||
func (h *AuthHandler) resolveKratosLoginID(token string) (string, error) {
|
||||
@@ -4408,38 +4231,6 @@ func (h *AuthHandler) startQrCodeLoginForQr(loginID, pendingRef, rawRef string)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *AuthHandler) resolveDescopeLoginID(ctx context.Context, token *descope.Token) (string, error) {
|
||||
if token == nil {
|
||||
return "", fmt.Errorf("descope token is nil")
|
||||
}
|
||||
|
||||
if loginID := extractLoginIDFromClaims(token.Claims); loginID != "" {
|
||||
return loginID, nil
|
||||
}
|
||||
|
||||
if h.DescopeClient == nil {
|
||||
return "", fmt.Errorf("descope client is nil")
|
||||
}
|
||||
|
||||
user, err := h.DescopeClient.Management.User().Load(ctx, token.ID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if user == nil {
|
||||
return "", fmt.Errorf("descope user not found")
|
||||
}
|
||||
if loginID := pickPrimaryLoginID(user.LoginIDs); loginID != "" {
|
||||
return loginID, nil
|
||||
}
|
||||
if user.Email != "" {
|
||||
return user.Email, nil
|
||||
}
|
||||
if user.Phone != "" {
|
||||
return user.Phone, nil
|
||||
}
|
||||
return "", fmt.Errorf("descope login id not found")
|
||||
}
|
||||
|
||||
func pickPrimaryLoginID(loginIDs []string) string {
|
||||
for _, id := range loginIDs {
|
||||
if strings.Contains(id, "@") {
|
||||
@@ -4768,93 +4559,6 @@ func (h *AuthHandler) UpdateMe(c *fiber.Ctx) error {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid request body"})
|
||||
}
|
||||
|
||||
if token != "" && looksLikeJWT(token) && h.DescopeClient != nil {
|
||||
authorized, userToken, err := h.DescopeClient.Auth.ValidateSessionWithToken(c.Context(), token)
|
||||
if err == nil && authorized {
|
||||
// 1. Load current user to check changes
|
||||
currentUser, err := h.DescopeClient.Management.User().Load(c.Context(), userToken.ID)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to load current user"})
|
||||
}
|
||||
identityID, resolveErr := h.resolveKratosIdentityID(
|
||||
c.Context(),
|
||||
currentUser.Email,
|
||||
normalizePhoneForLoginID(currentUser.Phone),
|
||||
)
|
||||
if resolveErr != nil || identityID == "" {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to resolve user identity"})
|
||||
}
|
||||
|
||||
newPhoneStorage := h.formatPhoneForStorage(req.Phone)
|
||||
oldPhoneStorage := currentUser.Phone
|
||||
|
||||
slog.Info("[UpdateMe] Checking changes", "userID", identityID, "oldPhone", oldPhoneStorage, "newPhone", newPhoneStorage, "newName", req.Name)
|
||||
|
||||
// 2. Handle Phone Number Change
|
||||
if newPhoneStorage != "" && newPhoneStorage != oldPhoneStorage {
|
||||
// Check verification status in Redis
|
||||
verifyKey := "verify_update_phone:" + identityID + ":" + newPhoneStorage
|
||||
val, _ := h.RedisService.Get(verifyKey)
|
||||
if val != "verified" {
|
||||
slog.Warn("[UpdateMe] Phone verification missing", "key", verifyKey)
|
||||
return c.Status(fiber.StatusForbidden).JSON(fiber.Map{"error": "휴대폰 번호 변경을 위해 SMS 인증이 필요합니다."})
|
||||
}
|
||||
|
||||
// Update Phone in Descope and mark as verified
|
||||
slog.Info("[UpdateMe] Updating phone number", "userID", identityID, "newPhone", newPhoneStorage)
|
||||
_, err = h.DescopeClient.Management.User().UpdatePhone(c.Context(), userToken.ID, newPhoneStorage, true, false)
|
||||
if err != nil {
|
||||
slog.Error("Failed to update phone in Descope", "error", err)
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "전화번호 업데이트에 실패했습니다."})
|
||||
}
|
||||
|
||||
// If the old phone was used as a LoginID, replace it with the new one
|
||||
for _, loginID := range currentUser.LoginIDs {
|
||||
// Normalize for comparison
|
||||
normID := strings.ReplaceAll(loginID, "+82", "0")
|
||||
normOld := strings.ReplaceAll(oldPhoneStorage, "+82", "0")
|
||||
|
||||
if loginID == oldPhoneStorage || (normOld != "" && normID == normOld) {
|
||||
slog.Info("[UpdateMe] Updating LoginID", "old", loginID, "new", newPhoneStorage)
|
||||
_, err = h.DescopeClient.Management.User().UpdateLoginID(c.Context(), loginID, newPhoneStorage)
|
||||
if err != nil {
|
||||
slog.Warn("Failed to update LoginID", "error", err)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Clear verification after successful update
|
||||
h.RedisService.Delete(verifyKey)
|
||||
}
|
||||
|
||||
// 3. Update Name if changed
|
||||
if req.Name != "" && req.Name != currentUser.Name {
|
||||
slog.Info("[UpdateMe] Updating display name", "userID", identityID, "newName", req.Name)
|
||||
_, err = h.DescopeClient.Management.User().UpdateDisplayName(c.Context(), userToken.ID, req.Name)
|
||||
if err != nil {
|
||||
slog.Error("Failed to update user name", "error", err)
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "이름 업데이트에 실패했습니다."})
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Update Custom Attributes (Department)
|
||||
if req.Department != "" {
|
||||
slog.Info("[UpdateMe] Updating department", "userID", identityID, "dept", req.Department)
|
||||
if _, err := h.DescopeClient.Management.User().UpdateCustomAttribute(c.Context(), userToken.ID, "department", req.Department); err != nil {
|
||||
slog.Error("Failed to update department", "error", err)
|
||||
}
|
||||
}
|
||||
|
||||
slog.Info("[UpdateMe] Profile update completed successfully", "userID", identityID)
|
||||
|
||||
return c.JSON(fiber.Map{
|
||||
"status": "success",
|
||||
"updatedAt": time.Now().Format(time.RFC3339),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
identityID string
|
||||
traits map[string]interface{}
|
||||
@@ -4928,18 +4632,7 @@ func (h *AuthHandler) ChangeMyPassword(c *fiber.Ctx) error {
|
||||
|
||||
loginID := ""
|
||||
token := h.getBearerToken(c)
|
||||
if token != "" && looksLikeJWT(token) && h.DescopeClient != nil {
|
||||
authorized, userToken, err := h.DescopeClient.Auth.ValidateSessionWithToken(c.Context(), token)
|
||||
if err == nil && authorized {
|
||||
resolved, err := h.resolveDescopeLoginID(c.Context(), userToken)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "Failed to resolve login ID"})
|
||||
}
|
||||
loginID = resolved
|
||||
}
|
||||
}
|
||||
|
||||
if loginID == "" && token != "" {
|
||||
if token != "" {
|
||||
if resolved, err := h.resolveKratosLoginID(token); err == nil {
|
||||
loginID = resolved
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user