forked from baron/baron-sso
Merge origin/main and remove Descope deps
This commit is contained in:
@@ -573,12 +573,6 @@ func main() {
|
|||||||
dev.Get("/consents", devHandler.ListConsents)
|
dev.Get("/consents", devHandler.ListConsents)
|
||||||
dev.Delete("/consents", devHandler.RevokeConsents)
|
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)
|
// Webhook for Kratos courier (HTTP delivery)
|
||||||
auth.Post("/webhooks/kratos-courier", authHandler.HandleKratosCourierRelay)
|
auth.Post("/webhooks/kratos-courier", authHandler.HandleKratosCourierRelay)
|
||||||
|
|
||||||
|
|||||||
@@ -842,34 +842,6 @@ paths:
|
|||||||
"200":
|
"200":
|
||||||
description: OK
|
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:
|
components:
|
||||||
schemas:
|
schemas:
|
||||||
ErrorResponse:
|
ErrorResponse:
|
||||||
@@ -1456,21 +1428,3 @@ components:
|
|||||||
data:
|
data:
|
||||||
type: object
|
type: object
|
||||||
additionalProperties: true
|
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
|
|
||||||
|
|||||||
@@ -1,41 +1,16 @@
|
|||||||
package handler
|
package handler
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"log/slog"
|
|
||||||
"os"
|
|
||||||
"runtime"
|
"runtime"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/descope/go-sdk/descope/client"
|
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
type AdminHandler struct {
|
type AdminHandler struct{}
|
||||||
DescopeClient *client.DescopeClient
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewAdminHandler() *AdminHandler {
|
func NewAdminHandler() *AdminHandler {
|
||||||
projectID := os.Getenv("DESCOPE_PROJECT_ID")
|
return &AdminHandler{}
|
||||||
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,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *AdminHandler) CheckAuth(c *fiber.Ctx) error {
|
func (h *AdminHandler) CheckAuth(c *fiber.Ctx) error {
|
||||||
|
|||||||
@@ -25,8 +25,6 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/descope/go-sdk/descope"
|
|
||||||
"github.com/descope/go-sdk/descope/client"
|
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -80,18 +78,16 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type AuthHandler struct {
|
type AuthHandler struct {
|
||||||
ProjectID string
|
|
||||||
SmsService domain.SmsService
|
SmsService domain.SmsService
|
||||||
EmailService domain.EmailService
|
EmailService domain.EmailService
|
||||||
RedisService *service.RedisService
|
RedisService *service.RedisService
|
||||||
DescopeClient *client.DescopeClient
|
|
||||||
KratosAdmin *service.KratosAdminService
|
KratosAdmin *service.KratosAdminService
|
||||||
IdpProvider domain.IdentityProvider
|
IdpProvider domain.IdentityProvider
|
||||||
AuditRepo domain.AuditRepository
|
AuditRepo domain.AuditRepository
|
||||||
OathkeeperRepo domain.OathkeeperLogRepository
|
OathkeeperRepo domain.OathkeeperLogRepository
|
||||||
Hydra *service.HydraAdminService
|
Hydra *service.HydraAdminService
|
||||||
TenantService service.TenantService
|
TenantService service.TenantService
|
||||||
KetoService service.KetoService
|
KetoService service.KetoService
|
||||||
UserRepo repository.UserRepository
|
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 {
|
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{
|
return &AuthHandler{
|
||||||
ProjectID: projectID,
|
|
||||||
SmsService: service.NewSmsService(),
|
SmsService: service.NewSmsService(),
|
||||||
EmailService: service.NewEmailService(),
|
EmailService: service.NewEmailService(),
|
||||||
RedisService: redisService,
|
RedisService: redisService,
|
||||||
DescopeClient: descopeClient,
|
|
||||||
KratosAdmin: service.NewKratosAdminService(),
|
KratosAdmin: service.NewKratosAdminService(),
|
||||||
IdpProvider: idpProvider,
|
IdpProvider: idpProvider,
|
||||||
AuditRepo: auditRepo,
|
AuditRepo: auditRepo,
|
||||||
OathkeeperRepo: oathkeeperRepo,
|
OathkeeperRepo: oathkeeperRepo,
|
||||||
Hydra: service.NewHydraAdminService(),
|
Hydra: service.NewHydraAdminService(),
|
||||||
TenantService: tenantService,
|
TenantService: tenantService,
|
||||||
KetoService: ketoService,
|
KetoService: ketoService,
|
||||||
UserRepo: userRepo,
|
UserRepo: userRepo,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -424,7 +403,7 @@ func (h *AuthHandler) Signup(c *fiber.Ctx) error {
|
|||||||
} else {
|
} else {
|
||||||
slog.Warn("[Signup] Attempted to join non-active tenant", "email", req.Email, "tenant", tenant.Slug, "status", tenant.Status)
|
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?
|
// 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.
|
// 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."})
|
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 {
|
if err := c.BodyParser(&req); err != nil {
|
||||||
ale.Status = fiber.StatusBadRequest
|
ale.Status = fiber.StatusBadRequest
|
||||||
ale.LatencyMs = time.Since(startTime)
|
ale.LatencyMs = time.Since(startTime)
|
||||||
ale.DescopeError = err.Error()
|
ale.ProviderError = err.Error()
|
||||||
ale.Log(slog.LevelError, "Body parse error")
|
ale.Log(slog.LevelError, "Body parse error")
|
||||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid request body"})
|
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 {
|
if h.IdpProvider == nil {
|
||||||
ale.Status = fiber.StatusInternalServerError
|
ale.Status = fiber.StatusInternalServerError
|
||||||
ale.LatencyMs = time.Since(startTime)
|
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")
|
ale.Log(slog.LevelError, "IDP Provider is nil")
|
||||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Authentication service not configured"})
|
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.Status = fiber.StatusUnauthorized
|
||||||
ale.LatencyMs = time.Since(startTime)
|
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()))
|
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") {
|
if strings.Contains(err.Error(), "not found") || strings.Contains(err.Error(), "identity") {
|
||||||
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "User not registered"})
|
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)
|
return c.JSON(resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
// InitiatePasswordReset - 사용자가 비밀번호 재설정을 시작하면, loginID 유형에 따라 Descope를 통해 이메일 또는 SMS를 보냅니다.
|
// InitiatePasswordReset - 사용자가 비밀번호 재설정을 시작하면, loginID 유형에 따라 이메일 또는 SMS를 보냅니다.
|
||||||
func (h *AuthHandler) InitiatePasswordReset(c *fiber.Ctx) error {
|
func (h *AuthHandler) InitiatePasswordReset(c *fiber.Ctx) error {
|
||||||
startTime := time.Now()
|
startTime := time.Now()
|
||||||
ale := logger.NewAuditLogEntry(c, "initiate")
|
ale := logger.NewAuditLogEntry(c, "initiate")
|
||||||
@@ -1614,7 +1593,7 @@ func (h *AuthHandler) InitiatePasswordReset(c *fiber.Ctx) error {
|
|||||||
if err := c.BodyParser(&req); err != nil {
|
if err := c.BodyParser(&req); err != nil {
|
||||||
ale.Status = fiber.StatusBadRequest
|
ale.Status = fiber.StatusBadRequest
|
||||||
ale.LatencyMs = time.Since(startTime)
|
ale.LatencyMs = time.Since(startTime)
|
||||||
ale.DescopeError = err.Error()
|
ale.ProviderError = err.Error()
|
||||||
ale.Log(slog.LevelError, "Body parse error")
|
ale.Log(slog.LevelError, "Body parse error")
|
||||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid request body"})
|
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 == "" {
|
if loginID == "" {
|
||||||
ale.Status = fiber.StatusBadRequest
|
ale.Status = fiber.StatusBadRequest
|
||||||
ale.LatencyMs = time.Since(startTime)
|
ale.LatencyMs = time.Since(startTime)
|
||||||
ale.DescopeError = "Login ID is required"
|
ale.ProviderError = "Login ID is required"
|
||||||
ale.Log(slog.LevelWarn, "Login ID missing")
|
ale.Log(slog.LevelWarn, "Login ID missing")
|
||||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Login ID is required"})
|
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 {
|
if h.IdpProvider == nil {
|
||||||
ale.Status = fiber.StatusInternalServerError
|
ale.Status = fiber.StatusInternalServerError
|
||||||
ale.LatencyMs = time.Since(startTime)
|
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")
|
ale.Log(slog.LevelError, "IDP Provider is not initialized")
|
||||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Authentication service not configured"})
|
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 == "" {
|
if userfrontURL == "" {
|
||||||
ale.Status = fiber.StatusInternalServerError
|
ale.Status = fiber.StatusInternalServerError
|
||||||
ale.LatencyMs = time.Since(startTime)
|
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")
|
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"})
|
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 == "" {
|
if resetToken == "" {
|
||||||
ale.Status = fiber.StatusInternalServerError
|
ale.Status = fiber.StatusInternalServerError
|
||||||
ale.LatencyMs = time.Since(startTime)
|
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")
|
ale.Log(slog.LevelError, "Failed to generate reset token")
|
||||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "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 {
|
if err := h.RedisService.Set(prefixPwdResetToken+resetToken, loginID, pwdResetExpiration); err != nil {
|
||||||
ale.Status = fiber.StatusInternalServerError
|
ale.Status = fiber.StatusInternalServerError
|
||||||
ale.LatencyMs = time.Since(startTime)
|
ale.LatencyMs = time.Since(startTime)
|
||||||
ale.DescopeError = err.Error()
|
ale.ProviderError = err.Error()
|
||||||
ale.Log(slog.LevelError, "Failed to store reset token in Redis")
|
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"})
|
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 {
|
if !drySend && h.EmailService == nil {
|
||||||
ale.Status = fiber.StatusInternalServerError
|
ale.Status = fiber.StatusInternalServerError
|
||||||
ale.LatencyMs = time.Since(startTime)
|
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")
|
ale.Log(slog.LevelError, "Email service not configured")
|
||||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "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 {
|
if err := h.EmailService.SendEmail(loginID, subject, body); err != nil {
|
||||||
ale.Status = fiber.StatusInternalServerError
|
ale.Status = fiber.StatusInternalServerError
|
||||||
ale.LatencyMs = time.Since(startTime)
|
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))
|
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"})
|
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 {
|
if err := h.SmsService.SendSms(loginID, resetSms); err != nil {
|
||||||
ale.Status = fiber.StatusInternalServerError
|
ale.Status = fiber.StatusInternalServerError
|
||||||
ale.LatencyMs = time.Since(startTime)
|
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))
|
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"})
|
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 == "" {
|
if token == "" {
|
||||||
ale.Status = fiber.StatusBadRequest
|
ale.Status = fiber.StatusBadRequest
|
||||||
ale.LatencyMs = time.Since(startTime)
|
ale.LatencyMs = time.Since(startTime)
|
||||||
ale.DescopeError = "Missing token"
|
ale.ProviderError = "Missing token"
|
||||||
ale.Log(slog.LevelWarn, "Missing token in request")
|
ale.Log(slog.LevelWarn, "Missing token in request")
|
||||||
return c.Status(fiber.StatusBadRequest).SendString("Missing token")
|
return c.Status(fiber.StatusBadRequest).SendString("Missing token")
|
||||||
}
|
}
|
||||||
@@ -1802,7 +1781,7 @@ func (h *AuthHandler) ProcessPasswordResetToken(c *fiber.Ctx) error {
|
|||||||
if err != nil || loginID == "" {
|
if err != nil || loginID == "" {
|
||||||
ale.Status = fiber.StatusUnauthorized
|
ale.Status = fiber.StatusUnauthorized
|
||||||
ale.LatencyMs = time.Since(startTime)
|
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")
|
ale.Log(slog.LevelWarn, "Reset token invalid or expired")
|
||||||
return c.Status(fiber.StatusUnauthorized).SendString("Invalid or expired token")
|
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 {
|
if err := c.BodyParser(&req); err != nil {
|
||||||
ale.Status = fiber.StatusBadRequest
|
ale.Status = fiber.StatusBadRequest
|
||||||
ale.LatencyMs = time.Since(startTime)
|
ale.LatencyMs = time.Since(startTime)
|
||||||
ale.DescopeError = err.Error()
|
ale.ProviderError = err.Error()
|
||||||
ale.Log(slog.LevelError, "Body parse error")
|
ale.Log(slog.LevelError, "Body parse error")
|
||||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid request body"})
|
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 == "" {
|
if loginID == "" || req.NewPassword == "" {
|
||||||
ale.Status = fiber.StatusBadRequest
|
ale.Status = fiber.StatusBadRequest
|
||||||
ale.LatencyMs = time.Since(startTime)
|
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")
|
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"})
|
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 {
|
if err := validatePasswordWithPolicy(policy, req.NewPassword); err != nil {
|
||||||
ale.Status = fiber.StatusBadRequest
|
ale.Status = fiber.StatusBadRequest
|
||||||
ale.LatencyMs = time.Since(startTime)
|
ale.LatencyMs = time.Since(startTime)
|
||||||
ale.DescopeError = err.Error()
|
ale.ProviderError = err.Error()
|
||||||
ale.Log(slog.LevelWarn, "Validation failed: "+err.Error())
|
ale.Log(slog.LevelWarn, "Validation failed: "+err.Error())
|
||||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": 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 {
|
if h.IdpProvider == nil {
|
||||||
ale.Status = fiber.StatusInternalServerError
|
ale.Status = fiber.StatusInternalServerError
|
||||||
ale.LatencyMs = time.Since(startTime)
|
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")
|
ale.Log(slog.LevelError, "IDP Provider is nil")
|
||||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Authentication service not configured"})
|
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 {
|
if err := h.IdpProvider.UpdateUserPassword(loginID, req.NewPassword, nil); err != nil {
|
||||||
ale.Status = fiber.StatusInternalServerError
|
ale.Status = fiber.StatusInternalServerError
|
||||||
ale.LatencyMs = time.Since(startTime)
|
ale.LatencyMs = time.Since(startTime)
|
||||||
ale.DescopeError = err.Error()
|
ale.ProviderError = err.Error()
|
||||||
ale.Log(slog.LevelError, "Failed to update password via IDP")
|
ale.Log(slog.LevelError, "Failed to update password via IDP")
|
||||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to update password"})
|
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. 모바일 토큰은 승인 검증용으로만 사용하고, 웹 전용 세션을 새로 발급
|
// 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 := ""
|
approvedSessionID := ""
|
||||||
if req.Token != "" {
|
if req.Token != "" {
|
||||||
if sessionID, err := h.getKratosSessionID(req.Token); err == nil {
|
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"})
|
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 {
|
type kratosCourierRequest struct {
|
||||||
Recipient string `json:"recipient"`
|
Recipient string `json:"recipient"`
|
||||||
TemplateType string `json:"template_type"`
|
TemplateType string `json:"template_type"`
|
||||||
@@ -2480,82 +2440,6 @@ func sanitizePhoneForSms(phone string) string {
|
|||||||
return sanitized
|
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 ---
|
// --- User Profile Handlers ---
|
||||||
|
|
||||||
func (h *AuthHandler) formatPhoneForDisplay(phone string) string {
|
func (h *AuthHandler) formatPhoneForDisplay(phone string) string {
|
||||||
@@ -2581,6 +2465,7 @@ func (h *AuthHandler) GetMe(c *fiber.Ctx) error {
|
|||||||
}
|
}
|
||||||
return c.JSON(profile)
|
return c.JSON(profile)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetEnrichedProfile - Exported wrapper for resolveCurrentProfile used by middlewares
|
// GetEnrichedProfile - Exported wrapper for resolveCurrentProfile used by middlewares
|
||||||
func (h *AuthHandler) GetEnrichedProfile(c *fiber.Ctx) (*domain.UserProfileResponse, error) {
|
func (h *AuthHandler) GetEnrichedProfile(c *fiber.Ctx) (*domain.UserProfileResponse, error) {
|
||||||
return h.resolveCurrentProfile(c)
|
return h.resolveCurrentProfile(c)
|
||||||
@@ -2975,6 +2860,17 @@ func (h *AuthHandler) GetAuthTimeline(c *fiber.Ctx) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "Invalid session"})
|
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 := ""
|
subject := ""
|
||||||
if h.OathkeeperRepo != nil {
|
if h.OathkeeperRepo != nil {
|
||||||
@@ -3056,7 +2952,7 @@ func (h *AuthHandler) GetAuthTimeline(c *fiber.Ctx) error {
|
|||||||
if !isAuthEventType(log.EventType) {
|
if !isAuthEventType(log.EventType) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if !matchesAuthTimelineUser(log, profile, candidates) {
|
if !matchesAuthTimelineUser(log, profile, candidates, currentSessionID) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if shouldSkipAuthTimeline(log) {
|
if shouldSkipAuthTimeline(log) {
|
||||||
@@ -3467,7 +3363,9 @@ func (h *AuthHandler) AcceptConsentRequest(c *fiber.Ctx) error {
|
|||||||
|
|
||||||
func (h *AuthHandler) AcceptOidcLoginRequest(c *fiber.Ctx) error {
|
func (h *AuthHandler) AcceptOidcLoginRequest(c *fiber.Ctx) error {
|
||||||
var req struct {
|
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 {
|
if err := c.BodyParser(&req); err != nil {
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
|
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")
|
return fiber.NewError(fiber.StatusUnauthorized, "Authentication required")
|
||||||
}
|
}
|
||||||
c.Locals("user_id", subject)
|
c.Locals("user_id", subject)
|
||||||
if sessionID, ok := c.Locals("session_id").(string); ok && sessionID != "" {
|
approvedSessionID := strings.TrimSpace(req.ApprovedSessionID)
|
||||||
c.Locals("approved_session_id", sessionID)
|
if approvedSessionID == "" {
|
||||||
} else if token := h.getBearerToken(c); token != "" {
|
approvedSessionID = strings.TrimSpace(req.SessionID)
|
||||||
if derivedID := extractSessionIDFromJWT(token); derivedID != "" {
|
}
|
||||||
c.Locals("approved_session_id", derivedID)
|
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 h.KratosAdmin != nil {
|
||||||
if identity, err := h.KratosAdmin.GetIdentity(c.Context(), subject); err == nil && identity != nil {
|
if identity, err := h.KratosAdmin.GetIdentity(c.Context(), subject); err == nil && identity != nil {
|
||||||
if loginID := pickLoginIDFromTraits(identity.Traits); loginID != "" {
|
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) {
|
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 profile *domain.UserProfileResponse
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
token := h.getBearerToken(c)
|
token := h.getBearerToken(c)
|
||||||
if token != "" {
|
if token != "" {
|
||||||
if looksLikeJWT(token) && h.DescopeClient != nil {
|
profile, err = h.getKratosProfile(token)
|
||||||
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)
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
cookie := c.Get("Cookie")
|
cookie := c.Get("Cookie")
|
||||||
if 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) {
|
func (h *AuthHandler) resolveConsentSubject(c *fiber.Ctx) (string, error) {
|
||||||
token := h.getBearerToken(c)
|
token := h.getBearerToken(c)
|
||||||
if token != "" {
|
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)
|
identityID, resolveErr := h.resolveIdentityID(c, token)
|
||||||
if resolveErr == nil && identityID != "" {
|
if resolveErr == nil && identityID != "" {
|
||||||
return identityID, nil
|
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) {
|
func (h *AuthHandler) resolveConsentSubjects(c *fiber.Ctx) ([]string, error) {
|
||||||
token := h.getBearerToken(c)
|
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 != "" {
|
if token != "" {
|
||||||
identityID, traits, err := h.getKratosIdentity(token)
|
identityID, traits, err := h.getKratosIdentity(token)
|
||||||
if err == nil && identityID != "" {
|
if err == nil && identityID != "" {
|
||||||
@@ -4042,7 +3868,7 @@ func normalizeLoginIdentifier(value string) string {
|
|||||||
return normalizePhoneForLoginID(trimmed)
|
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 {
|
if profile == nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -4051,11 +3877,24 @@ func matchesAuthTimelineUser(log domain.AuditLog, profile *domain.UserProfileRes
|
|||||||
}
|
}
|
||||||
loginID := extractLoginIDFromAuditDetails(log.Details)
|
loginID := extractLoginIDFromAuditDetails(log.Details)
|
||||||
normalized := normalizeLoginIdentifier(loginID)
|
normalized := normalizeLoginIdentifier(loginID)
|
||||||
if normalized == "" {
|
if normalized != "" {
|
||||||
|
if _, ok := candidates[normalized]; ok {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if sessionID == "" {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
_, ok := candidates[normalized]
|
if log.SessionID != "" && log.SessionID == sessionID {
|
||||||
return ok
|
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 {
|
func extractLoginIDFromAuditDetails(details string) string {
|
||||||
@@ -4215,52 +4054,36 @@ func extractSessionIDFromAuditDetails(details string) string {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *AuthHandler) resolveIdentityID(c *fiber.Ctx, token string) (string, error) {
|
func extractApprovedSessionIDFromAuditDetails(details string) string {
|
||||||
if looksLikeJWT(token) && h.DescopeClient != nil {
|
if details == "" {
|
||||||
authorized, userToken, err := h.DescopeClient.Auth.ValidateSessionWithToken(c.Context(), token)
|
return ""
|
||||||
if err == nil && authorized {
|
}
|
||||||
if h.KratosAdmin == nil {
|
var payload map[string]any
|
||||||
return "", fmt.Errorf("kratos admin unavailable")
|
if err := json.Unmarshal([]byte(details), &payload); err != nil {
|
||||||
}
|
return ""
|
||||||
userResponse, loadErr := h.DescopeClient.Management.User().Load(c.Context(), userToken.ID)
|
}
|
||||||
if loadErr != nil {
|
if raw, ok := payload["approved_session_id"]; ok {
|
||||||
return "", loadErr
|
switch value := raw.(type) {
|
||||||
}
|
case string:
|
||||||
identityID, resolveErr := h.resolveKratosIdentityID(
|
return value
|
||||||
c.Context(),
|
default:
|
||||||
userResponse.Email,
|
return fmt.Sprint(value)
|
||||||
normalizePhoneForLoginID(userResponse.Phone),
|
|
||||||
)
|
|
||||||
if resolveErr != nil || identityID == "" {
|
|
||||||
return "", fmt.Errorf("failed to resolve kratos identity for token")
|
|
||||||
}
|
|
||||||
return identityID, nil
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
id, _, err := h.getKratosIdentity(token)
|
if raw, ok := payload["approvedSessionId"]; ok {
|
||||||
return id, err
|
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) {
|
func (h *AuthHandler) resolveIdentityID(c *fiber.Ctx, token string) (string, error) {
|
||||||
if !looksLikeJWT(token) || h.DescopeClient == nil {
|
id, _, err := h.getKratosIdentity(token)
|
||||||
return nil, "", "", nil
|
return id, err
|
||||||
}
|
|
||||||
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) resolveKratosLoginID(token string) (string, error) {
|
func (h *AuthHandler) resolveKratosLoginID(token string) (string, error) {
|
||||||
@@ -4408,38 +4231,6 @@ func (h *AuthHandler) startQrCodeLoginForQr(loginID, pendingRef, rawRef string)
|
|||||||
return nil
|
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 {
|
func pickPrimaryLoginID(loginIDs []string) string {
|
||||||
for _, id := range loginIDs {
|
for _, id := range loginIDs {
|
||||||
if strings.Contains(id, "@") {
|
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"})
|
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 (
|
var (
|
||||||
identityID string
|
identityID string
|
||||||
traits map[string]interface{}
|
traits map[string]interface{}
|
||||||
@@ -4928,18 +4632,7 @@ func (h *AuthHandler) ChangeMyPassword(c *fiber.Ctx) error {
|
|||||||
|
|
||||||
loginID := ""
|
loginID := ""
|
||||||
token := h.getBearerToken(c)
|
token := h.getBearerToken(c)
|
||||||
if token != "" && looksLikeJWT(token) && h.DescopeClient != nil {
|
if token != "" {
|
||||||
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 resolved, err := h.resolveKratosLoginID(token); err == nil {
|
if resolved, err := h.resolveKratosLoginID(token); err == nil {
|
||||||
loginID = resolved
|
loginID = resolved
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -77,8 +77,8 @@ func TestCompletePasswordReset_InvalidPasswordPolicy(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCompletePasswordReset_NilDescopeClient(t *testing.T) {
|
func TestCompletePasswordReset_NilIDPProvider(t *testing.T) {
|
||||||
h := &AuthHandler{} // DescopeClient intentionally nil to hit the configuration error branch
|
h := &AuthHandler{} // IdpProvider intentionally nil to hit the configuration error branch
|
||||||
app := newTestApp(h)
|
app := newTestApp(h)
|
||||||
|
|
||||||
body, _ := json.Marshal(map[string]string{
|
body, _ := json.Marshal(map[string]string{
|
||||||
@@ -95,7 +95,7 @@ func TestCompletePasswordReset_NilDescopeClient(t *testing.T) {
|
|||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusInternalServerError {
|
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
|
var got map[string]string
|
||||||
|
|||||||
@@ -1,26 +1,16 @@
|
|||||||
package handler
|
package handler
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"baron-sso-backend/internal/domain"
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"net/http/httptest"
|
|
||||||
"os"
|
|
||||||
"strconv"
|
|
||||||
"testing"
|
"testing"
|
||||||
"unicode"
|
"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 {
|
func generatePasswordFromPolicy(policy *domain.PasswordPolicy) string {
|
||||||
minLen := int(policy.MinLength)
|
minLen := policy.MinLength
|
||||||
if minLen < 8 {
|
if minLen < 8 {
|
||||||
minLen = 12 // 안전한 기본값
|
minLen = 12 // 안전한 기본값
|
||||||
}
|
}
|
||||||
@@ -65,29 +55,17 @@ func randomInt(n int) int {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestGeneratePasswordUsesNonAlphanumericRequirement(t *testing.T) {
|
func TestGeneratePasswordUsesNonAlphanumericRequirement(t *testing.T) {
|
||||||
mockAuth := &mocksauth.MockAuthentication{
|
policy := &domain.PasswordPolicy{
|
||||||
MockPassword: &mocksauth.MockPassword{
|
MinLength: 8,
|
||||||
PolicyResponse: &descope.PasswordPolicy{
|
Lowercase: true,
|
||||||
MinLength: 8,
|
Uppercase: true,
|
||||||
Lowercase: true,
|
Number: true,
|
||||||
Uppercase: true,
|
NonAlphanumeric: true,
|
||||||
Number: true,
|
|
||||||
NonAlphanumeric: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
policy, err := mockAuth.Password().GetPasswordPolicy(context.Background())
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("정책 조회 실패: %v", err)
|
|
||||||
}
|
|
||||||
if !policy.NonAlphanumeric {
|
|
||||||
t.Fatalf("정책에 비영문자 요구사항이 표시되지 않음")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pwd := generatePasswordFromPolicy(policy)
|
pwd := generatePasswordFromPolicy(policy)
|
||||||
|
|
||||||
if len(pwd) < int(policy.MinLength) {
|
if len(pwd) < policy.MinLength {
|
||||||
t.Fatalf("비밀번호 길이가 정책 최소 길이 미만: got %d, want >= %d", 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)
|
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
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ func getEnv(key, fallback string) string {
|
|||||||
// InitializeProvider는 환경 설정을 기반으로 IDP 공급자를 생성하고 반환합니다.
|
// InitializeProvider는 환경 설정을 기반으로 IDP 공급자를 생성하고 반환합니다.
|
||||||
// 이것은 IdentityProvider 인터페이스의 팩토리 역할을 합니다.
|
// 이것은 IdentityProvider 인터페이스의 팩토리 역할을 합니다.
|
||||||
func InitializeProvider() (domain.IdentityProvider, error) {
|
func InitializeProvider() (domain.IdentityProvider, error) {
|
||||||
rawProviders := getEnv("IDP_PROVIDER", "descope") // 기본값은 descope입니다.
|
rawProviders := getEnv("IDP_PROVIDER", "ory")
|
||||||
providers := strings.Split(rawProviders, ",")
|
providers := strings.Split(rawProviders, ",")
|
||||||
slog.Info("Initializing IDP chain", "providers", rawProviders)
|
slog.Info("Initializing IDP chain", "providers", rawProviders)
|
||||||
|
|
||||||
|
|||||||
@@ -27,9 +27,9 @@ type AuditLogEntry struct {
|
|||||||
Headers map[string]string // Core headers like Host, Cookie, Set-Cookie
|
Headers map[string]string // Core headers like Host, Cookie, Set-Cookie
|
||||||
LoginIDs map[string]string // loginId and loginId_normalized
|
LoginIDs map[string]string // loginId and loginId_normalized
|
||||||
Token string // For reset tokens, magic link tokens
|
Token string // For reset tokens, magic link tokens
|
||||||
DescopeError string
|
ProviderError string
|
||||||
DescopeStatus int // Descope HTTP status
|
ProviderStatus int // Provider HTTP status
|
||||||
DescopeBody string // Descope response body (full raw)
|
ProviderBody string // Provider response body (full raw)
|
||||||
RefreshToken string
|
RefreshToken string
|
||||||
SessionJwt string
|
SessionJwt string
|
||||||
AccessJwt string
|
AccessJwt string
|
||||||
@@ -143,14 +143,14 @@ func (ale *AuditLogEntry) Log(level slog.Level, msg string, args ...any) {
|
|||||||
if ale.Token != "" {
|
if ale.Token != "" {
|
||||||
attrs = append(attrs, slog.String("token", ale.Token))
|
attrs = append(attrs, slog.String("token", ale.Token))
|
||||||
}
|
}
|
||||||
if ale.DescopeError != "" {
|
if ale.ProviderError != "" {
|
||||||
attrs = append(attrs, slog.String("descope_error", ale.DescopeError))
|
attrs = append(attrs, slog.String("provider_error", ale.ProviderError))
|
||||||
}
|
}
|
||||||
if ale.DescopeStatus != 0 {
|
if ale.ProviderStatus != 0 {
|
||||||
attrs = append(attrs, slog.Int("descope_http_status", ale.DescopeStatus))
|
attrs = append(attrs, slog.Int("provider_http_status", ale.ProviderStatus))
|
||||||
}
|
}
|
||||||
if ale.DescopeBody != "" {
|
if ale.ProviderBody != "" {
|
||||||
attrs = append(attrs, slog.String("descope_response_body", ale.DescopeBody))
|
attrs = append(attrs, slog.String("provider_response_body", ale.ProviderBody))
|
||||||
}
|
}
|
||||||
if ale.RefreshToken != "" {
|
if ale.RefreshToken != "" {
|
||||||
attrs = append(attrs, slog.String("refresh_token", ale.RefreshToken))
|
attrs = append(attrs, slog.String("refresh_token", ale.RefreshToken))
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'package:http/http.dart' as http;
|
import 'package:http/http.dart' as http;
|
||||||
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
||||||
|
import 'auth_token_store.dart';
|
||||||
import 'http_client.dart';
|
import 'http_client.dart';
|
||||||
import 'web_window.dart';
|
import 'web_window.dart';
|
||||||
|
|
||||||
@@ -265,12 +266,17 @@ class AuthProxyService {
|
|||||||
if (token != null && token.isNotEmpty) {
|
if (token != null && token.isNotEmpty) {
|
||||||
headers['Authorization'] = 'Bearer $token';
|
headers['Authorization'] = 'Bearer $token';
|
||||||
}
|
}
|
||||||
|
final sessionId = _extractSessionIdFromJwt(token ?? AuthTokenStore.getToken() ?? '');
|
||||||
final client = createHttpClient(withCredentials: true);
|
final client = createHttpClient(withCredentials: true);
|
||||||
try {
|
try {
|
||||||
final response = await client.post(
|
final response = await client.post(
|
||||||
url,
|
url,
|
||||||
headers: headers,
|
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) {
|
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 {
|
static Future<Map<String, dynamic>> initiatePasswordReset(String loginId, {bool? drySend}) async {
|
||||||
final url = Uri.parse('$_baseUrl/api/v1/auth/password/reset/initiate');
|
final url = Uri.parse('$_baseUrl/api/v1/auth/password/reset/initiate');
|
||||||
final response = await http.post(
|
final response = await http.post(
|
||||||
|
|||||||
@@ -571,14 +571,10 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
|||||||
|
|
||||||
Widget _buildAppCell(AuditLogEntry log, {TextStyle? style}) {
|
Widget _buildAppCell(AuditLogEntry log, {TextStyle? style}) {
|
||||||
final label = _appLabelForLog(log);
|
final label = _appLabelForLog(log);
|
||||||
if (label == 'Baron 통합로그인') {
|
final clientId = log.clientId;
|
||||||
return _selectableText(label, style: style);
|
final tooltip = clientId.isEmpty ? 'Client ID 없음' : 'Client ID: $clientId';
|
||||||
}
|
|
||||||
final tooltip = log.parentSessionId.isEmpty
|
|
||||||
? '부모 세션 ID 없음'
|
|
||||||
: '부모 세션 ID: ${log.parentSessionId}';
|
|
||||||
final baseStyle = style ?? const TextStyle();
|
final baseStyle = style ?? const TextStyle();
|
||||||
final emphasisStyle = log.parentSessionId.isEmpty
|
final emphasisStyle = clientId.isEmpty
|
||||||
? baseStyle
|
? baseStyle
|
||||||
: baseStyle.copyWith(
|
: baseStyle.copyWith(
|
||||||
color: Colors.blueAccent,
|
color: Colors.blueAccent,
|
||||||
|
|||||||
Reference in New Issue
Block a user