forked from baron/baron-sso
비밀번호 변경
This commit is contained in:
@@ -37,6 +37,8 @@ const (
|
||||
maxSignupFailures = 5
|
||||
emailCodeTTL = 5 * time.Minute
|
||||
smsCodeTTL = 3 * time.Minute
|
||||
prefixPwdResetToken = "pwdreset_token:"
|
||||
pwdResetExpiration = 15 * time.Minute
|
||||
)
|
||||
|
||||
type AuthHandler struct {
|
||||
@@ -865,3 +867,138 @@ func (h *AuthHandler) HandleDescopeEmailRelay(c *fiber.Ctx) error {
|
||||
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"})
|
||||
}
|
||||
|
||||
func (h *AuthHandler) InitPasswordReset(c *fiber.Ctx) error {
|
||||
var req struct {
|
||||
LoginID string `json:"loginId"`
|
||||
}
|
||||
|
||||
if err := c.BodyParser(&req); err != nil || strings.TrimSpace(req.LoginID) == "" {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid request body"})
|
||||
}
|
||||
|
||||
loginID := strings.ReplaceAll(req.LoginID, "-", "")
|
||||
loginID = strings.ReplaceAll(loginID, " ", "")
|
||||
|
||||
// 토큰 생성 + Redis 저장
|
||||
token := GenerateSecureToken(16)
|
||||
tokenKey := prefixPwdResetToken + token
|
||||
|
||||
payload, _ := json.Marshal(map[string]string{
|
||||
"loginId": loginID,
|
||||
})
|
||||
h.RedisService.Set(tokenKey, string(payload), pwdResetExpiration)
|
||||
|
||||
// 링크 생성 (프론트에서 token 받아 새 비번 입력 페이지로 이동)
|
||||
frontendURL := os.Getenv("FRONTEND_URL")
|
||||
if frontendURL == "" {
|
||||
frontendURL = "https://sso.hmac.kr"
|
||||
}
|
||||
|
||||
// 예: https://sso.hmac.kr/password-reset?token=xxxx
|
||||
link := fmt.Sprintf("%s/password-reset?token=%s", frontendURL, token)
|
||||
|
||||
// 발송
|
||||
if strings.Contains(loginID, "@") {
|
||||
subject := "[Baron SSO] 비밀번호 재설정"
|
||||
body := fmt.Sprintf(`
|
||||
<div style="font-family:sans-serif;padding:20px;border:1px solid #eee;border-radius:10px;max-width:520px;">
|
||||
<h2 style="color:#1A1F2C;">비밀번호 재설정</h2>
|
||||
<p>아래 버튼을 눌러 새 비밀번호를 설정해 주세요. 이 링크는 %d분 동안 유효합니다.</p>
|
||||
<div style="margin:28px 0;">
|
||||
<a href="%s" style="background:#1A1F2C;color:#fff;padding:12px 22px;text-decoration:none;border-radius:6px;font-weight:700;">비밀번호 재설정</a>
|
||||
</div>
|
||||
<p style="font-size:12px;color:#888;">본인이 요청하지 않았다면 이 메일을 무시해 주세요.</p>
|
||||
</div>`, int(pwdResetExpiration.Minutes()), link)
|
||||
|
||||
if err := h.EmailService.SendEmail(loginID, subject, body); err != nil {
|
||||
slog.Error("[PwdResetInit] Email failed", "loginID", loginID, "error", err)
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to send Email"})
|
||||
}
|
||||
} else {
|
||||
content := fmt.Sprintf(
|
||||
"[Baron SSO] 비밀번호 재설정 링크(%d분 유효): %s",
|
||||
int(pwdResetExpiration.Minutes()),
|
||||
link,
|
||||
)
|
||||
if err := h.SmsService.SendSms(loginID, content); err != nil {
|
||||
slog.Error("[PwdResetInit] SMS failed", "loginID", loginID, "error", err)
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to send SMS"})
|
||||
}
|
||||
}
|
||||
|
||||
slog.Info("[PwdResetInit] Sent reset link", "loginID", loginID)
|
||||
return c.JSON(fiber.Map{"status": "ok"})
|
||||
}
|
||||
|
||||
func (h *AuthHandler) ConfirmPasswordReset(c *fiber.Ctx) error {
|
||||
var req struct {
|
||||
Token string `json:"token"`
|
||||
NewPassword string `json:"newPassword"`
|
||||
NewPasswordConfirm string `json:"newPasswordConfirm"`
|
||||
}
|
||||
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid request body"})
|
||||
}
|
||||
|
||||
if req.Token == "" || req.NewPassword == "" || req.NewPasswordConfirm == "" {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Missing required fields"})
|
||||
}
|
||||
|
||||
if req.NewPassword != req.NewPasswordConfirm {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Password confirmation does not match"})
|
||||
}
|
||||
|
||||
if h.DescopeClient == nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Descope Client not configured"})
|
||||
}
|
||||
|
||||
// 1) token 검증(=Redis)
|
||||
tokenKey := prefixPwdResetToken + req.Token
|
||||
val, err := h.RedisService.Get(tokenKey)
|
||||
if err != nil || val == "" {
|
||||
slog.Warn("[PwdResetConfirm] token not found/expired", "token", req.Token)
|
||||
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "Invalid or expired token"})
|
||||
}
|
||||
|
||||
var data map[string]string
|
||||
_ = json.Unmarshal([]byte(val), &data)
|
||||
|
||||
loginID := data["loginId"]
|
||||
if loginID == "" {
|
||||
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "Invalid token payload"})
|
||||
}
|
||||
|
||||
// (선택) 1회성 토큰 처리: 먼저 삭제(레이스가 걱정되면 처리 순서 조정)
|
||||
_ = h.RedisService.Delete(tokenKey)
|
||||
|
||||
// 2) Management API로 Active Password 설정
|
||||
if err := h.DescopeClient.Management.User().SetActivePassword(
|
||||
context.Background(),
|
||||
loginID,
|
||||
req.NewPassword,
|
||||
); err != nil {
|
||||
slog.Error("[PwdResetConfirm] SetActivePassword failed",
|
||||
"loginID", loginID,
|
||||
"error", err,
|
||||
)
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to update password"})
|
||||
}
|
||||
|
||||
// 3) 새 비밀번호로 자동 로그인
|
||||
authInfo, err := h.DescopeClient.Auth.Password().SignIn(context.Background(), loginID, req.NewPassword, nil)
|
||||
if err != nil {
|
||||
slog.Warn("[PwdResetConfirm] SignIn failed after reset", "loginID", loginID, "error", err)
|
||||
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "Invalid credentials"})
|
||||
}
|
||||
|
||||
slog.Info("[PwdResetConfirm] Success", "loginID", loginID)
|
||||
return c.JSON(fiber.Map{
|
||||
"status": "ok",
|
||||
"sessionJwt": authInfo.SessionToken.JWT,
|
||||
// 필요하면 refresh도 내려주기
|
||||
// "refreshJwt": authInfo.RefreshToken.JWT,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user