diff --git a/backend/internal/domain/idp_models.go b/backend/internal/domain/idp_models.go
index 29283318..3c71936c 100644
--- a/backend/internal/domain/idp_models.go
+++ b/backend/internal/domain/idp_models.go
@@ -8,10 +8,10 @@ import (
// BrokerUser is the standard user model used within Baron SSO business logic.
// It defines the canonical set of fields that must be supported by any underlying IDP.
type BrokerUser struct {
- ID string `json:"id" required:"true"`
- Email string `json:"email" required:"true"`
- Name string `json:"name"`
- PhoneNumber string `json:"phone_number"`
+ ID string `json:"id" required:"true"`
+ Email string `json:"email" required:"true"`
+ Name string `json:"name"`
+ PhoneNumber string `json:"phone_number"`
// Attributes stores custom user attributes.
// The "required_keys" tag specifies which keys MUST be present in the IDP's schema support.
Attributes map[string]interface{} `json:"attributes" required_keys:"grade,department"`
diff --git a/backend/internal/handler/admin_handler.go b/backend/internal/handler/admin_handler.go
index a6f8354c..32b8826f 100644
--- a/backend/internal/handler/admin_handler.go
+++ b/backend/internal/handler/admin_handler.go
@@ -55,4 +55,4 @@ func (h *AdminHandler) CheckAuth(c *fiber.Ctx) error {
return err
}
return c.Status(fiber.StatusOK).JSON(fiber.Map{"status": "ok"})
-}
\ No newline at end of file
+}
diff --git a/backend/internal/handler/auth_handler.go b/backend/internal/handler/auth_handler.go
index da1102b5..020506f0 100644
--- a/backend/internal/handler/auth_handler.go
+++ b/backend/internal/handler/auth_handler.go
@@ -35,14 +35,14 @@ const (
statusSuccess = "success"
// Durations
- defaultExpiration = 5 * time.Minute
- signupStateExpiration = 10 * time.Minute
- signupBlockDuration = 10 * time.Minute
- maxSignupFailures = 5
- emailCodeTTL = 5 * time.Minute
- smsCodeTTL = 3 * time.Minute
- prefixPwdResetToken = "pwdreset_token:"
- pwdResetExpiration = 15 * time.Minute
+ defaultExpiration = 5 * time.Minute
+ signupStateExpiration = 10 * time.Minute
+ signupBlockDuration = 10 * time.Minute
+ maxSignupFailures = 5
+ emailCodeTTL = 5 * time.Minute
+ smsCodeTTL = 3 * time.Minute
+ prefixPwdResetToken = "pwdreset_token:"
+ pwdResetExpiration = 15 * time.Minute
)
type AuthHandler struct {
@@ -125,12 +125,12 @@ func (h *AuthHandler) CheckEmail(c *fiber.Ctx) error {
// Note: Descope doesn't have a direct "exists" check, we use Load or Search.
// Since we are checking availability for signup, we want "User not found".
exists, err := h.DescopeClient.Management.User().Load(context.Background(), req.Email)
-
+
// If err is nil and exists is not nil, user exists.
if err == nil && exists != nil {
return c.JSON(fiber.Map{"available": false, "message": "Email already registered"})
}
-
+
// Check if specific error is "not found" or just assume if Load fails it might be free.
// Typically Descope Load returns error if not found? Let's assume so or check error message.
// Actually, strictly speaking, we should handle specific errors, but for MVP:
@@ -146,7 +146,7 @@ func (h *AuthHandler) SendSignupEmailCode(c *fiber.Ctx) error {
req.Type = "email" // Enforce type
key := prefixSignupEmail + req.Target
-
+
// 1. Check existing state (Rate Limit / Block)
state, _ := h.getSignupState(key)
if state != nil && state.FailCount > maxSignupFailures {
@@ -164,26 +164,26 @@ func (h *AuthHandler) SendSignupEmailCode(c *fiber.Ctx) error {
newState := &signupState{
Code: code,
Verified: false,
- FailCount: 0, // Reset fail count on new code generation? Or keep it?
- // Requirement says "Auth fail > 5 -> block". New code usually resets or continues?
- // Usually getting a new code doesn't reset verify failure count if we want strict blocking.
- // But for simplicity let's say "fail count" applies to verification attempts.
- // If we are issuing a new code, it's a new attempt cycle usually.
- // However, spamming "send code" is also an attack.
- // Let's keep FailCount if exists, or 0.
+ FailCount: 0, // Reset fail count on new code generation? Or keep it?
+ // Requirement says "Auth fail > 5 -> block". New code usually resets or continues?
+ // Usually getting a new code doesn't reset verify failure count if we want strict blocking.
+ // But for simplicity let's say "fail count" applies to verification attempts.
+ // If we are issuing a new code, it's a new attempt cycle usually.
+ // However, spamming "send code" is also an attack.
+ // Let's keep FailCount if exists, or 0.
ExpiresAt: time.Now().Add(emailCodeTTL).Unix(),
}
if state != nil {
newState.FailCount = state.FailCount
}
-
+
h.saveSignupState(key, newState, signupStateExpiration)
// 4. Send Email
if h.EmailService == nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Email service not configured"})
}
-
+
subject := "[Baron SSO] 회원가입 인증코드"
body := fmt.Sprintf(`
@@ -193,7 +193,7 @@ func (h *AuthHandler) SendSignupEmailCode(c *fiber.Ctx) error {
이 코드는 5분간 유효합니다.
`, code)
-
+
go h.EmailService.SendEmail(req.Target, subject, body)
return c.JSON(fiber.Map{"message": "Verification code sent"})
@@ -312,10 +312,18 @@ func (h *AuthHandler) Signup(c *fiber.Ctx) error {
}
// Check complexity (at least 2 types: lower, upper, digit, special)
types := 0
- if strings.ContainsAny(req.Password, "abcdefghijklmnopqrstuvwxyz") { types++ }
- if strings.ContainsAny(req.Password, "ABCDEFGHIJKLMNOPQRSTUVWXYZ") { types++ }
- if strings.ContainsAny(req.Password, "0123456789") { types++ }
- if strings.ContainsAny(req.Password, "!@#$%^&*()_+-=[]{}|;:,.<>?") { types++ }
+ if strings.ContainsAny(req.Password, "abcdefghijklmnopqrstuvwxyz") {
+ types++
+ }
+ if strings.ContainsAny(req.Password, "ABCDEFGHIJKLMNOPQRSTUVWXYZ") {
+ types++
+ }
+ if strings.ContainsAny(req.Password, "0123456789") {
+ types++
+ }
+ if strings.ContainsAny(req.Password, "!@#$%^&*()_+-=[]{}|;:,.<>?") {
+ types++
+ }
if types < 2 {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Password must contain at least 2 types of characters (letters, numbers, symbols)"})
}
@@ -359,14 +367,14 @@ func (h *AuthHandler) Signup(c *fiber.Ctx) error {
"termsAccepted": req.TermsAccepted,
"createdAt": time.Now().Format(time.RFC3339),
}
-
+
// Create user
- // Note: Descope `Create` does not set password. We usually need `Create` then `Password().Update`
- // or use a specialized signup flow.
+ // 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 {
@@ -1305,8 +1313,6 @@ func (h *AuthHandler) HandleDescopeEmailRelay(c *fiber.Ctx) error {
return c.Status(501).JSON(fiber.Map{"error": "Real email sending not implemented"})
}
-
-
// --- User Profile Handlers ---
func (h *AuthHandler) formatPhoneForDisplay(phone string) string {
@@ -1409,7 +1415,7 @@ func (h *AuthHandler) UpdateMe(c *fiber.Ctx) error {
// 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)
@@ -1456,7 +1462,7 @@ func (h *AuthHandler) SendUpdateCode(c *fiber.Ctx) error {
if token == "" {
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "Unauthorized"})
}
-
+
authorized, userToken, err := h.DescopeClient.Auth.ValidateSessionWithToken(c.Context(), token)
if err != nil || !authorized {
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "Invalid session"})
@@ -1471,7 +1477,7 @@ func (h *AuthHandler) SendUpdateCode(c *fiber.Ctx) error {
phone := h.formatPhoneForStorage(req.Phone)
code := fmt.Sprintf("%06d", rand.Intn(1000000))
-
+
// Store code in Redis
key := "otp_update_phone:" + userToken.ID + ":" + phone
h.RedisService.Set(key, code, 5*time.Minute)
diff --git a/backend/internal/logger/audit_logger.go b/backend/internal/logger/audit_logger.go
index ad1291ea..50f414a4 100644
--- a/backend/internal/logger/audit_logger.go
+++ b/backend/internal/logger/audit_logger.go
@@ -28,8 +28,8 @@ type AuditLogEntry struct {
LoginIDs map[string]string // loginId and loginId_normalized
Token string // For reset tokens, magic link tokens
DescopeError string
- DescopeStatus int // Descope HTTP status
- DescopeBody string // Descope response body (full raw)
+ DescopeStatus int // Descope HTTP status
+ DescopeBody string // Descope response body (full raw)
RefreshToken string
SessionJwt string
AccessJwt string
@@ -68,7 +68,6 @@ func NewAuditLogEntry(c *fiber.Ctx, stage string) *AuditLogEntry {
headers["Origin"] = c.Get("Origin")
headers["Referer"] = c.Get("Referer")
-
return &AuditLogEntry{
RequestID: reqID,
Stage: stage,
@@ -85,7 +84,6 @@ func NewAuditLogEntry(c *fiber.Ctx, stage string) *AuditLogEntry {
}
}
-
// Log emits an audit log entry using slog.
// It includes common fields and allows for additional custom fields.
func (ale *AuditLogEntry) Log(level slog.Level, msg string, args ...any) {
@@ -213,4 +211,4 @@ func (ale *AuditLogEntry) Log(level slog.Level, msg string, args ...any) {
}
slog.Default().LogAttrs(context.Background(), level, msg, attrs...)
-}
\ No newline at end of file
+}
diff --git a/backend/internal/service/descope_service.go b/backend/internal/service/descope_service.go
index 58cb36aa..1dca9231 100644
--- a/backend/internal/service/descope_service.go
+++ b/backend/internal/service/descope_service.go
@@ -70,28 +70,28 @@ func (d *DescopeProvider) GetMetadata() (*domain.IDPMetadata, error) {
}
func (d *DescopeProvider) InitiatePasswordReset(loginID, redirectUrl string) error {
- ctx := context.Background()
- err := d.Client.Auth.Password().SendPasswordReset(ctx, loginID, redirectUrl, nil)
- if err != nil {
- slog.Error("Descope SendPasswordReset failed (raw)",
- "loginID", loginID,
- "redirectUrl", redirectUrl,
- "err", err,
- "err_type", fmt.Sprintf("%T", err),
- )
+ ctx := context.Background()
+ err := d.Client.Auth.Password().SendPasswordReset(ctx, loginID, redirectUrl, nil)
+ if err != nil {
+ slog.Error("Descope SendPasswordReset failed (raw)",
+ "loginID", loginID,
+ "redirectUrl", redirectUrl,
+ "err", err,
+ "err_type", fmt.Sprintf("%T", err),
+ )
- if de, ok := err.(*descope.Error); ok {
- status := de.Info[descope.ErrorInfoKeys.HTTPResponseStatusCode] // "Status-Code"
- slog.Error("Descope error details",
- "code", de.Code,
- "description", de.Description,
- "message", de.Message,
- "status_code", status,
- "info", de.Info,
- )
- }
- }
- return err
+ if de, ok := err.(*descope.Error); ok {
+ status := de.Info[descope.ErrorInfoKeys.HTTPResponseStatusCode] // "Status-Code"
+ slog.Error("Descope error details",
+ "code", de.Code,
+ "description", de.Description,
+ "message", de.Message,
+ "status_code", status,
+ "info", de.Info,
+ )
+ }
+ }
+ return err
}
func (d *DescopeProvider) VerifyPasswordResetToken(token string) (*domain.AuthInfo, error) {