1
0
forked from baron/baron-sso

Merge origin/main and remove Descope deps

This commit is contained in:
Lectom C Han
2026-02-03 18:10:31 +09:00
parent b908d71666
commit bf469b1eb4
10 changed files with 172 additions and 658 deletions

View File

@@ -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
}