forked from baron/baron-sso
ory-hosting 기본구동
This commit is contained in:
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user