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

@@ -573,12 +573,6 @@ func main() {
dev.Get("/consents", devHandler.ListConsents)
dev.Delete("/consents", devHandler.RevokeConsents)
// Webhook for Descope Generic SMS Gateway
auth.Post("/webhooks/descope-sms", authHandler.HandleDescopeSmsRelay)
// Webhook for Descope Generic Email Gateway (Fake Email Strategy)
auth.Post("/webhooks/descope-email", authHandler.HandleDescopeEmailRelay)
// Webhook for Kratos courier (HTTP delivery)
auth.Post("/webhooks/kratos-courier", authHandler.HandleKratosCourierRelay)

View File

@@ -842,34 +842,6 @@ paths:
"200":
description: OK
/api/v1/auth/webhooks/descope-sms:
post:
tags: [Webhook]
summary: Descope SMS 릴레이
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/DescopeSmsWebhookRequest"
responses:
"200":
description: OK
/api/v1/auth/webhooks/descope-email:
post:
tags: [Webhook]
summary: Descope Email 릴레이
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/DescopeEmailWebhookRequest"
responses:
"200":
description: OK
components:
schemas:
ErrorResponse:
@@ -1456,21 +1428,3 @@ components:
data:
type: object
additionalProperties: true
DescopeSmsWebhookRequest:
type: object
properties:
recipient:
type: string
body:
type: string
DescopeEmailWebhookRequest:
type: object
properties:
to:
type: string
subject:
type: string
text:
type: string

View File

@@ -1,41 +1,16 @@
package handler
import (
"log/slog"
"os"
"runtime"
"time"
"github.com/descope/go-sdk/descope/client"
"github.com/gofiber/fiber/v2"
)
type AdminHandler struct {
DescopeClient *client.DescopeClient
}
type AdminHandler struct{}
func NewAdminHandler() *AdminHandler {
projectID := os.Getenv("DESCOPE_PROJECT_ID")
managementKey := os.Getenv("DESCOPE_MANAGEMENT_KEY")
var descopeClient *client.DescopeClient
var err error
if projectID != "" && managementKey != "" {
descopeClient, err = client.NewWithConfig(&client.Config{
ProjectID: projectID,
ManagementKey: managementKey,
})
if err != nil {
slog.Warn("Failed to initialize Descope Client for Admin", "error", err)
}
} else {
slog.Warn("DESCOPE_PROJECT_ID or DESCOPE_MANAGEMENT_KEY missing. Admin functions will fail.")
}
return &AdminHandler{
DescopeClient: descopeClient,
}
return &AdminHandler{}
}
func (h *AdminHandler) CheckAuth(c *fiber.Ctx) error {

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
}

View File

@@ -77,8 +77,8 @@ func TestCompletePasswordReset_InvalidPasswordPolicy(t *testing.T) {
}
}
func TestCompletePasswordReset_NilDescopeClient(t *testing.T) {
h := &AuthHandler{} // DescopeClient intentionally nil to hit the configuration error branch
func TestCompletePasswordReset_NilIDPProvider(t *testing.T) {
h := &AuthHandler{} // IdpProvider intentionally nil to hit the configuration error branch
app := newTestApp(h)
body, _ := json.Marshal(map[string]string{
@@ -95,7 +95,7 @@ func TestCompletePasswordReset_NilDescopeClient(t *testing.T) {
defer resp.Body.Close()
if resp.StatusCode != http.StatusInternalServerError {
t.Fatalf("expected 500 when Descope client is nil, got %d", resp.StatusCode)
t.Fatalf("expected 500 when IDP provider is nil, got %d", resp.StatusCode)
}
var got map[string]string

View File

@@ -1,26 +1,16 @@
package handler
import (
"context"
"baron-sso-backend/internal/domain"
"crypto/rand"
"encoding/binary"
"errors"
"fmt"
"net/http"
"net/http/httptest"
"os"
"strconv"
"testing"
"unicode"
"github.com/descope/go-sdk/descope"
"github.com/descope/go-sdk/descope/client"
mocksauth "github.com/descope/go-sdk/descope/tests/mocks/auth"
)
// 정책을 받아 필수 요구사항을 모두 포함하는 비밀번호를 생성한다.
func generatePasswordFromPolicy(policy *descope.PasswordPolicy) string {
minLen := int(policy.MinLength)
func generatePasswordFromPolicy(policy *domain.PasswordPolicy) string {
minLen := policy.MinLength
if minLen < 8 {
minLen = 12 // 안전한 기본값
}
@@ -65,29 +55,17 @@ func randomInt(n int) int {
}
func TestGeneratePasswordUsesNonAlphanumericRequirement(t *testing.T) {
mockAuth := &mocksauth.MockAuthentication{
MockPassword: &mocksauth.MockPassword{
PolicyResponse: &descope.PasswordPolicy{
MinLength: 8,
Lowercase: true,
Uppercase: true,
Number: true,
NonAlphanumeric: true,
},
},
}
policy, err := mockAuth.Password().GetPasswordPolicy(context.Background())
if err != nil {
t.Fatalf("정책 조회 실패: %v", err)
}
if !policy.NonAlphanumeric {
t.Fatalf("정책에 비영문자 요구사항이 표시되지 않음")
policy := &domain.PasswordPolicy{
MinLength: 8,
Lowercase: true,
Uppercase: true,
Number: true,
NonAlphanumeric: true,
}
pwd := generatePasswordFromPolicy(policy)
if len(pwd) < int(policy.MinLength) {
if len(pwd) < policy.MinLength {
t.Fatalf("비밀번호 길이가 정책 최소 길이 미만: got %d, want >= %d", len(pwd), policy.MinLength)
}
@@ -118,115 +96,3 @@ func TestGeneratePasswordUsesNonAlphanumericRequirement(t *testing.T) {
t.Fatalf("비영문자 요구사항 미충족: %q", pwd)
}
}
// 통합 테스트: 실제 Descope 정책으로 비밀번호를 생성하고 교체 플로우를 검증한다.
// 필요 env:
// DESCOPE_PROJECT_ID, DESCOPE_MANAGEMENT_KEY, TEST_DESCOPE_LOGIN_ID, TEST_DESCOPE_CURRENT_PASSWORD
func TestDescopePasswordPolicyAndChange(t *testing.T) {
projectID := os.Getenv("DESCOPE_PROJECT_ID")
managementKey := os.Getenv("DESCOPE_MANAGEMENT_KEY")
loginID := os.Getenv("DESCOPE_TEST_ACCOUNT")
if projectID == "" || managementKey == "" || loginID == "" {
t.Skip("환경변수(DESCOPE_PROJECT_ID, DESCOPE_MANAGEMENT_KEY, DESCOPE_TEST_ACCOUNT) 미설정으로 통합 테스트 건너뜀")
}
logf := func(format string, args ...any) {
t.Logf(format, args...)
fmt.Printf(format+"\n", args...)
}
ctx := context.Background()
cl, err := client.NewWithConfig(&client.Config{
ProjectID: projectID,
ManagementKey: managementKey,
})
if err != nil {
t.Fatalf("Descope 클라이언트 초기화 실패: %v", err)
}
policy, err := cl.Auth.Password().GetPasswordPolicy(ctx)
if err != nil {
t.Fatalf("비밀번호 정책 조회 실패: %v", err)
}
logf("정책: min=%d lower=%v upper=%v number=%v nonAlpha=%v", policy.MinLength, policy.Lowercase, policy.Uppercase, policy.Number, policy.NonAlphanumeric)
// 테스트 계정이 없으면 생성
users, _, err := cl.Management.User().SearchAll(ctx, &descope.UserSearchOptions{
LoginIDs: []string{loginID},
Limit: 1,
Page: 0,
})
if err != nil {
t.Fatalf("테스트 계정 검색 실패: %v", err)
}
if len(users) == 0 {
logf("테스트 계정 미존재, 생성 시도: %s", loginID)
if _, err := cl.Management.User().CreateTestUser(ctx, loginID, &descope.UserRequest{
User: descope.User{
Email: loginID,
},
}); err != nil {
t.Fatalf("테스트 계정 생성 실패: %v", err)
}
} else {
logf("테스트 계정 존재 확인: %s", loginID)
}
// 1) 기초 비밀번호 설정 (알려진 값으로 초기화)
basePassword := generatePasswordFromPolicy(policy)
if err := cl.Management.User().SetActivePassword(ctx, loginID, basePassword); err != nil {
logf("초기 비밀번호 설정 실패: status=%d err=%v", statusFromError(err), err)
t.Fatalf("초기 비밀번호 설정 실패: %v", err)
}
logf("초기 비밀번호 설정 완료: %s", basePassword)
// 2) 초기 비밀번호 로그인 검증
wOld := httptest.NewRecorder()
_, err = cl.Auth.Password().SignIn(ctx, loginID, basePassword, wOld)
logf("기초 비밀번호 로그인: status=%d err=%v", statusFromError(err), err)
if err != nil {
t.Fatalf("기초 비밀번호 로그인 실패: %v", err)
}
// 3) 새 비밀번호 생성 및 변경
newPassword := generatePasswordFromPolicy(policy)
if newPassword == basePassword {
newPassword = newPassword + "Z9!"
}
logf("새 비밀번호 생성: %s", newPassword)
if err := cl.Management.User().SetActivePassword(ctx, loginID, newPassword); err != nil {
logf("비밀번호 변경 실패: status=%d err=%v", statusFromError(err), err)
t.Fatalf("비밀번호 변경 실패: %v", err)
}
logf("비밀번호 변경 성공(status=200)")
// 4) 새 비밀번호로 로그인 확인
wNew := httptest.NewRecorder()
_, err = cl.Auth.Password().SignIn(ctx, loginID, newPassword, wNew)
logf("새 비밀번호 로그인: status=%d err=%v", statusFromError(err), err)
if err != nil {
t.Fatalf("새 비밀번호 로그인 실패: %v", err)
}
}
func statusFromError(err error) int {
if err == nil {
return http.StatusOK
}
var de *descope.Error
if errors.As(err, &de) {
if statusRaw, ok := de.Info[descope.ErrorInfoKeys.HTTPResponseStatusCode]; ok {
switch v := statusRaw.(type) {
case int:
return v
case string:
if n, convErr := strconv.Atoi(v); convErr == nil {
return n
}
}
}
}
return http.StatusInternalServerError
}

View File

@@ -34,7 +34,7 @@ func getEnv(key, fallback string) string {
// InitializeProvider는 환경 설정을 기반으로 IDP 공급자를 생성하고 반환합니다.
// 이것은 IdentityProvider 인터페이스의 팩토리 역할을 합니다.
func InitializeProvider() (domain.IdentityProvider, error) {
rawProviders := getEnv("IDP_PROVIDER", "descope") // 기본값은 descope입니다.
rawProviders := getEnv("IDP_PROVIDER", "ory")
providers := strings.Split(rawProviders, ",")
slog.Info("Initializing IDP chain", "providers", rawProviders)

View File

@@ -27,9 +27,9 @@ type AuditLogEntry struct {
Headers map[string]string // Core headers like Host, Cookie, Set-Cookie
LoginIDs map[string]string // loginId and loginId_normalized
Token string // For reset tokens, magic link tokens
DescopeError string
DescopeStatus int // Descope HTTP status
DescopeBody string // Descope response body (full raw)
ProviderError string
ProviderStatus int // Provider HTTP status
ProviderBody string // Provider response body (full raw)
RefreshToken string
SessionJwt string
AccessJwt string
@@ -143,14 +143,14 @@ func (ale *AuditLogEntry) Log(level slog.Level, msg string, args ...any) {
if ale.Token != "" {
attrs = append(attrs, slog.String("token", ale.Token))
}
if ale.DescopeError != "" {
attrs = append(attrs, slog.String("descope_error", ale.DescopeError))
if ale.ProviderError != "" {
attrs = append(attrs, slog.String("provider_error", ale.ProviderError))
}
if ale.DescopeStatus != 0 {
attrs = append(attrs, slog.Int("descope_http_status", ale.DescopeStatus))
if ale.ProviderStatus != 0 {
attrs = append(attrs, slog.Int("provider_http_status", ale.ProviderStatus))
}
if ale.DescopeBody != "" {
attrs = append(attrs, slog.String("descope_response_body", ale.DescopeBody))
if ale.ProviderBody != "" {
attrs = append(attrs, slog.String("provider_response_body", ale.ProviderBody))
}
if ale.RefreshToken != "" {
attrs = append(attrs, slog.String("refresh_token", ale.RefreshToken))

View File

@@ -1,6 +1,7 @@
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'auth_token_store.dart';
import 'http_client.dart';
import 'web_window.dart';
@@ -265,12 +266,17 @@ class AuthProxyService {
if (token != null && token.isNotEmpty) {
headers['Authorization'] = 'Bearer $token';
}
final sessionId = _extractSessionIdFromJwt(token ?? AuthTokenStore.getToken() ?? '');
final client = createHttpClient(withCredentials: true);
try {
final response = await client.post(
url,
headers: headers,
body: jsonEncode({'login_challenge': loginChallenge}),
body: jsonEncode({
'login_challenge': loginChallenge,
if (sessionId != null && sessionId.isNotEmpty)
'approved_session_id': sessionId,
}),
);
if (response.statusCode == 200) {
@@ -284,6 +290,36 @@ class AuthProxyService {
}
}
static String? _extractSessionIdFromJwt(String token) {
if (token.isEmpty) {
return null;
}
try {
final parts = token.split('.');
if (parts.length != 3) {
return null;
}
final payload = utf8.decode(base64Url.decode(base64Url.normalize(parts[1])));
final data = json.decode(payload) as Map<String, dynamic>;
for (final key in ['sid', 'session_id', 'sessionId', 'jti']) {
final value = data[key];
if (value == null) {
continue;
}
if (value is String && value.isNotEmpty) {
return value;
}
final converted = value.toString();
if (converted.isNotEmpty) {
return converted;
}
}
} catch (_) {
return null;
}
return null;
}
static Future<Map<String, dynamic>> initiatePasswordReset(String loginId, {bool? drySend}) async {
final url = Uri.parse('$_baseUrl/api/v1/auth/password/reset/initiate');
final response = await http.post(

View File

@@ -571,14 +571,10 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
Widget _buildAppCell(AuditLogEntry log, {TextStyle? style}) {
final label = _appLabelForLog(log);
if (label == 'Baron 통합로그인') {
return _selectableText(label, style: style);
}
final tooltip = log.parentSessionId.isEmpty
? '부모 세션 ID 없음'
: '부모 세션 ID: ${log.parentSessionId}';
final clientId = log.clientId;
final tooltip = clientId.isEmpty ? 'Client ID 없음' : 'Client ID: $clientId';
final baseStyle = style ?? const TextStyle();
final emphasisStyle = log.parentSessionId.isEmpty
final emphasisStyle = clientId.isEmpty
? baseStyle
: baseStyle.copyWith(
color: Colors.blueAccent,