1
0
forked from baron/baron-sso

ory-hosting 기본구동

This commit is contained in:
Lectom C Han
2026-01-27 22:58:49 +09:00
parent 41f0549435
commit c3f7b18afc
31 changed files with 1910 additions and 176 deletions

View File

@@ -2,7 +2,6 @@ package handler
import (
"baron-sso-backend/internal/domain"
"baron-sso-backend/internal/idp"
"baron-sso-backend/internal/logger"
"baron-sso-backend/internal/service"
"context"
@@ -14,7 +13,6 @@ import (
"math/rand"
"os"
"regexp"
"strconv"
"strings"
"time"
@@ -70,7 +68,7 @@ func GenerateSecureToken(length int) string {
return hex.EncodeToString(b)
}
func NewAuthHandler(redisService *service.RedisService) *AuthHandler {
func NewAuthHandler(redisService *service.RedisService, idpProvider domain.IdentityProvider) *AuthHandler {
projectID := os.Getenv("DESCOPE_PROJECT_ID")
managementKey := os.Getenv("DESCOPE_MANAGEMENT_KEY")
@@ -86,13 +84,6 @@ func NewAuthHandler(redisService *service.RedisService) *AuthHandler {
}
}
idpProvider, err := idp.InitializeProvider()
if err != nil {
slog.Error("Failed to initialize IDP Provider", "error", err)
// Depending on the application's needs, you might want to panic here
// if the IDP provider is essential for the application to run.
}
return &AuthHandler{
ProjectID: projectID,
SmsService: service.NewSmsService(),
@@ -342,12 +333,11 @@ func (h *AuthHandler) Signup(c *fiber.Ctx) error {
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "Phone not verified"})
}
// 3. Create User in Descope
if h.DescopeClient == nil {
if h.IdpProvider == nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Identity provider unavailable"})
}
// Normalize Phone for Descope (E.164)
// Normalize Phone (E.164 형태로 보관)
normalizedPhone := strings.ReplaceAll(req.Phone, "-", "")
normalizedPhone = strings.ReplaceAll(normalizedPhone, " ", "")
if strings.HasPrefix(normalizedPhone, "010") {
@@ -356,54 +346,42 @@ func (h *AuthHandler) Signup(c *fiber.Ctx) error {
normalizedPhone = "+" + normalizedPhone
}
descopeUser := &descope.UserRequest{}
descopeUser.Email = req.Email
descopeUser.Phone = normalizedPhone
descopeUser.Name = req.Name
descopeUser.CustomAttributes = map[string]any{
// IDP에 전달할 BrokerUser 스키마 구성
attributes := map[string]interface{}{
"department": req.Department,
"affiliationType": req.AffiliationType,
"companyCode": req.CompanyCode,
"department": req.Department,
"termsAccepted": req.TermsAccepted,
"createdAt": time.Now().Format(time.RFC3339),
// grade는 기존 스키마 필수 키이므로 기본값을 설정
"grade": "member",
}
brokerUser := &domain.BrokerUser{
Email: req.Email,
Name: req.Name,
PhoneNumber: normalizedPhone,
Attributes: attributes,
}
// Create user
// Note: Descope `Create` does not set password. We usually need `Create` then `Password().Update`
// or use a specialized signup flow.
// `Management.User().Create` creates a user but doesn't set a password credential immediately unless specified?
// Actually `User().Create` creates the identity.
// To set password, we use `h.DescopeClient.Management.User().SetPassword(...)`
// Check if user exists (Double check)
exists, _ := h.DescopeClient.Management.User().Load(context.Background(), req.Email)
if exists != nil {
return c.Status(fiber.StatusConflict).JSON(fiber.Map{"error": "User already exists"})
}
// Create
_, err := h.DescopeClient.Management.User().Create(context.Background(), req.Email, descopeUser)
providerID, err := h.IdpProvider.CreateUser(brokerUser, req.Password)
if err != nil {
slog.Error("[Signup] Failed to create user in Descope", "error", err)
slog.Error("[Signup] Failed to create user via IDP", "provider", h.IdpProvider.Name(), "error", err)
if strings.Contains(err.Error(), "already exists") {
return c.Status(fiber.StatusConflict).JSON(fiber.Map{"error": "User already exists"})
}
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to create user"})
}
// Set Password
err = h.DescopeClient.Management.User().SetPassword(context.Background(), req.Email, req.Password)
if err != nil {
slog.Error("[Signup] Failed to set password", "error", err)
// Rollback? Delete user?
h.DescopeClient.Management.User().Delete(context.Background(), req.Email)
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": fmt.Sprintf("Failed to set password: %v", err)})
}
// 4. Cleanup Redis
h.RedisService.Delete(emailKey)
h.RedisService.Delete(phoneKey)
slog.Info("[Signup] New user registered", "email", req.Email, "type", req.AffiliationType)
slog.Info("[Signup] New user registered", "email", req.Email, "type", req.AffiliationType, "provider", h.IdpProvider.Name(), "subject", providerID)
return c.JSON(fiber.Map{"success": true, "message": "User registered successfully"})
return c.JSON(fiber.Map{
"success": true,
"message": "User registered successfully",
"provider": h.IdpProvider.Name(),
"subject": providerID,
})
}
// --- Helpers ---
@@ -750,38 +728,43 @@ func (h *AuthHandler) PasswordLogin(c *fiber.Ctx) error {
ale.Log(slog.LevelInfo, "Attempting to login")
if h.DescopeClient == nil {
if h.IdpProvider == nil {
ale.Status = fiber.StatusInternalServerError
ale.LatencyMs = time.Since(startTime)
ale.DescopeError = "Descope Client is nil!"
ale.Log(slog.LevelError, "Descope Client is nil")
ale.DescopeError = "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"})
}
// Sign in using Descope
authInfo, err := h.DescopeClient.Auth.Password().SignIn(context.Background(), req.LoginID, req.Password, nil)
authInfo, err := h.IdpProvider.SignIn(loginID, req.Password)
if err != nil {
ale.Status = fiber.StatusUnauthorized
ale.LatencyMs = time.Since(startTime)
ale.DescopeError = err.Error()
ale.Log(slog.LevelWarn, "Descope sign-in failed")
// [Changed] Check if it's a "User not found" error to be more specific
if strings.Contains(err.Error(), "E062107") || strings.Contains(err.Error(), "not found") {
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"})
}
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "Invalid credentials"})
}
ale.Status = fiber.StatusOK
ale.LatencyMs = time.Since(startTime)
ale.SessionJwt = authInfo.SessionToken.JWT
ale.Log(slog.LevelInfo, "Login successful")
return c.JSON(fiber.Map{
ale.Log(slog.LevelInfo, "Login successful", slog.String("provider", h.IdpProvider.Name()), slog.String("subject", authInfo.Subject))
resp := fiber.Map{
"sessionJwt": authInfo.SessionToken.JWT,
"status": "ok",
})
"provider": h.IdpProvider.Name(),
}
if authInfo.RefreshToken != nil {
resp["refreshJwt"] = authInfo.RefreshToken.JWT
}
if authInfo.Subject != "" {
resp["subject"] = authInfo.Subject
}
return c.JSON(resp)
}
// InitiatePasswordReset - 사용자가 비밀번호 재설정을 시작하면, loginID 유형에 따라 Descope를 통해 이메일 또는 SMS를 보냅니다.
@@ -997,6 +980,12 @@ func (h *AuthHandler) CompletePasswordReset(c *fiber.Ctx) error {
ale := logger.NewAuditLogEntry(c, "complete")
ale.Operation = "UpdateUserPassword"
providerName := "unknown"
if h.IdpProvider != nil {
providerName = h.IdpProvider.Name()
}
isDescopeProvider := strings.Contains(strings.ToLower(providerName), "descope")
var req struct {
NewPassword string `json:"newPassword"`
}
@@ -1044,78 +1033,79 @@ func (h *AuthHandler) CompletePasswordReset(c *fiber.Ctx) error {
// 디버깅을 위해 요청된 새 비밀번호를 로그로 출력
ale.Log(slog.LevelInfo, "Received new password for reset")
// Validate password complexity dynamically based on Descope policy
policy, err := h.DescopeClient.Auth.Password().GetPasswordPolicy(context.Background())
if err != nil {
// If policy fetch fails, log warning and proceed (or fallback to basic check)
ale.Log(slog.LevelWarn, "Failed to fetch password policy, skipping dynamic validation: "+err.Error())
} else {
if len(req.NewPassword) < int(policy.MinLength) {
ale.Status = fiber.StatusBadRequest
ale.LatencyMs = time.Since(startTime)
ale.DescopeError = fmt.Sprintf("Password must be at least %d characters long", policy.MinLength)
ale.Log(slog.LevelWarn, "Validation failed: password too short")
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": ale.DescopeError})
}
if policy.Lowercase {
if ok, _ := regexp.MatchString(`[a-z]`, req.NewPassword); !ok {
ale.Status = fiber.StatusBadRequest
ale.LatencyMs = time.Since(startTime)
ale.DescopeError = "Password must contain at least one lowercase letter"
ale.Log(slog.LevelWarn, "Validation failed: no lowercase letter")
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Password must contain at least one lowercase letter"})
}
}
if policy.Uppercase {
if ok, _ := regexp.MatchString(`[A-Z]`, req.NewPassword); !ok {
ale.Status = fiber.StatusBadRequest
ale.LatencyMs = time.Since(startTime)
ale.DescopeError = "Password must contain at least one uppercase letter"
ale.Log(slog.LevelWarn, "Validation failed: no uppercase letter")
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Password must contain at least one uppercase letter"})
}
}
if policy.Number {
if ok, _ := regexp.MatchString(`[0-9]`, req.NewPassword); !ok {
ale.Status = fiber.StatusBadRequest
ale.LatencyMs = time.Since(startTime)
ale.DescopeError = "Password must contain at least one number"
ale.Log(slog.LevelWarn, "Validation failed: no number")
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Password must contain at least one number"})
}
}
if policy.NonAlphanumeric {
if ok, _ := regexp.MatchString(`[\W_]`, req.NewPassword); !ok {
ale.Status = fiber.StatusBadRequest
ale.LatencyMs = time.Since(startTime)
ale.DescopeError = "Password must contain at least one special character"
ale.Log(slog.LevelWarn, "Validation failed: no special character")
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Password must contain at least one special character"})
}
}
if len(req.NewPassword) < 8 {
ale.Status = fiber.StatusBadRequest
ale.LatencyMs = time.Since(startTime)
ale.DescopeError = "Password must be at least 8 characters long"
ale.Log(slog.LevelWarn, "Validation failed: password too short (fallback policy)")
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": ale.DescopeError})
}
ale.Log(slog.LevelInfo, "Attempting to update password via Descope Auth API")
if isDescopeProvider && h.DescopeClient != nil {
// Validate password complexity (Descope only)
policy, err := h.DescopeClient.Auth.Password().GetPasswordPolicy(context.Background())
if err != nil {
ale.Log(slog.LevelWarn, "Failed to fetch password policy, skipping dynamic validation: "+err.Error())
} else {
if len(req.NewPassword) < int(policy.MinLength) {
ale.Status = fiber.StatusBadRequest
ale.LatencyMs = time.Since(startTime)
ale.DescopeError = fmt.Sprintf("Password must be at least %d characters long", policy.MinLength)
ale.Log(slog.LevelWarn, "Validation failed: password too short")
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": ale.DescopeError})
}
if policy.Lowercase {
if ok, _ := regexp.MatchString(`[a-z]`, req.NewPassword); !ok {
ale.Status = fiber.StatusBadRequest
ale.LatencyMs = time.Since(startTime)
ale.DescopeError = "Password must contain at least one lowercase letter"
ale.Log(slog.LevelWarn, "Validation failed: no lowercase letter")
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Password must contain at least one lowercase letter"})
}
}
if policy.Uppercase {
if ok, _ := regexp.MatchString(`[A-Z]`, req.NewPassword); !ok {
ale.Status = fiber.StatusBadRequest
ale.LatencyMs = time.Since(startTime)
ale.DescopeError = "Password must contain at least one uppercase letter"
ale.Log(slog.LevelWarn, "Validation failed: no uppercase letter")
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Password must contain at least one uppercase letter"})
}
}
if policy.Number {
if ok, _ := regexp.MatchString(`[0-9]`, req.NewPassword); !ok {
ale.Status = fiber.StatusBadRequest
ale.LatencyMs = time.Since(startTime)
ale.DescopeError = "Password must contain at least one number"
ale.Log(slog.LevelWarn, "Validation failed: no number")
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Password must contain at least one number"})
}
}
if policy.NonAlphanumeric {
if ok, _ := regexp.MatchString(`[\W_]`, req.NewPassword); !ok {
ale.Status = fiber.StatusBadRequest
ale.LatencyMs = time.Since(startTime)
ale.DescopeError = "Password must contain at least one special character"
ale.Log(slog.LevelWarn, "Validation failed: no special character")
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Password must contain at least one special character"})
}
}
}
} else if isDescopeProvider && h.DescopeClient == nil {
ale.Log(slog.LevelWarn, "Descope selected but client is nil; skipping policy validation")
}
// Descope Management API를 통해 비밀번호 업데이트
if h.DescopeClient == nil {
ale.Log(slog.LevelInfo, "Attempting to update password via IDP", slog.String("idp", providerName))
if h.IdpProvider == nil {
ale.Status = fiber.StatusInternalServerError
ale.LatencyMs = time.Since(startTime)
ale.DescopeError = "Descope Client is nil!"
ale.Log(slog.LevelError, "Descope Client is nil")
ale.DescopeError = "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"})
}
if err := h.DescopeClient.Management.User().SetActivePassword(context.Background(), loginID, req.NewPassword); err != nil {
// Descope 에러 상세를 감사 로그에 포함
if de, ok := err.(*descope.Error); ok {
if statusRaw, ok := de.Info[descope.ErrorInfoKeys.HTTPResponseStatusCode]; ok {
if statusInt, convErr := strconv.Atoi(fmt.Sprintf("%v", statusRaw)); convErr == nil {
ale.DescopeStatus = statusInt
}
}
ale.DescopeBody = de.Message
}
if err := h.IdpProvider.UpdateUserPassword(loginID, req.NewPassword, nil); err != nil {
ale.Status = fiber.StatusInternalServerError
ale.LatencyMs = time.Since(startTime)
ale.DescopeError = err.Error()