1
0
forked from baron/baron-sso

Merge branch 'dev' into feat/id_login

This commit is contained in:
2026-04-01 13:40:45 +09:00
41 changed files with 2079 additions and 397 deletions

View File

@@ -66,19 +66,21 @@ const (
loginFlowLink = "link"
// 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
minPollInterval = 2 * time.Second
loginCodeExpiration = 10 * time.Minute
linkResendCooldown = 60 * time.Second
prefixDrySend = "dry_send:"
headlessJWKSFetchTTL = 5 * time.Second
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:"
prefixPwdResetUsed = "pwdreset_used:"
pwdResetExpiration = 15 * time.Minute
pwdResetUsedExpiration = 2 * time.Minute
minPollInterval = 2 * time.Second
loginCodeExpiration = 10 * time.Minute
linkResendCooldown = 60 * time.Second
prefixDrySend = "dry_send:"
headlessJWKSFetchTTL = 5 * time.Second
)
type AuthHandler struct {
@@ -1741,14 +1743,14 @@ func containsHeadlessAudience(expected []string, actual headlessAssertionAud) bo
func (h *AuthHandler) loadHeadlessJWKS(ctx context.Context, client domain.HydraClient) (*jose.JSONWebKeySet, error) {
var raw []byte
switch {
case client.JWKS != nil:
data, err := json.Marshal(client.JWKS)
case client.HeadlessJWKS() != nil:
data, err := json.Marshal(client.HeadlessJWKS())
if err != nil {
return nil, fmt.Errorf("failed to encode jwks: %w", err)
}
raw = data
case strings.TrimSpace(client.JWKSUri) != "":
req, err := http.NewRequestWithContext(ctx, http.MethodGet, strings.TrimSpace(client.JWKSUri), nil)
case client.HeadlessJWKSURI() != "":
req, err := http.NewRequestWithContext(ctx, http.MethodGet, client.HeadlessJWKSURI(), nil)
if err != nil {
return nil, fmt.Errorf("failed to build jwks request: %w", err)
}
@@ -1768,7 +1770,7 @@ func (h *AuthHandler) loadHeadlessJWKS(ctx context.Context, client domain.HydraC
}
raw = body
default:
return nil, fmt.Errorf("trusted rp public key is not configured")
return nil, fmt.Errorf("headless login public key is not configured")
}
var keySet jose.JSONWebKeySet
@@ -1776,7 +1778,7 @@ func (h *AuthHandler) loadHeadlessJWKS(ctx context.Context, client domain.HydraC
return nil, fmt.Errorf("failed to decode jwks: %w", err)
}
if len(keySet.Keys) == 0 {
return nil, fmt.Errorf("trusted rp jwks has no keys")
return nil, fmt.Errorf("headless login jwks has no keys")
}
return &keySet, nil
}
@@ -2410,9 +2412,9 @@ func (h *AuthHandler) InitiatePasswordReset(c *fiber.Ctx) error {
}
userfrontURL := h.resolveUserfrontURL(c)
// [Changed] Point to Backend API for verification (which then redirects to Frontend)
redirectURL := fmt.Sprintf("%s/api/v1/auth/password/reset/verify", userfrontURL)
ale.RedirectTo = redirectURL
// 비밀번호 재설정 링크는 backend verify 엔드포인트를 거쳐서 userfront로 이동합니다.
// 이렇게 해야 메일/SMS 링크 프리뷰나 자동 스캔으로 토큰이 직접 노출되는 경로를 줄일 수 있습니다.
verifyBaseURL := fmt.Sprintf("%s/api/v1/auth/password/reset/v", userfrontURL)
// 내부 토큰 발급 + 우리 채널로 전송
resetToken := GenerateSecureToken(32)
@@ -2432,7 +2434,7 @@ func (h *AuthHandler) InitiatePasswordReset(c *fiber.Ctx) error {
return errorJSON(c, fiber.StatusInternalServerError, "Failed to store reset token")
}
resetLink := fmt.Sprintf("%s/reset-password?token=%s", userfrontURL, resetToken)
resetLink := fmt.Sprintf("%s/%s", verifyBaseURL, resetToken)
ale.RedirectTo = resetLink
ale.Operation = "SendPasswordReset"
ale.Log(slog.LevelInfo, "Initiating password reset via internal token")
@@ -2498,6 +2500,9 @@ func (h *AuthHandler) VerifyPasswordResetPage(c *fiber.Ctx) error {
if token == "" {
token = c.Query("t")
}
if token == "" {
token = c.Params("token")
}
if token == "" {
return c.Status(fiber.StatusBadRequest).SendString("Missing token")
@@ -2551,6 +2556,9 @@ func (h *AuthHandler) ProcessPasswordResetToken(c *fiber.Ctx) error {
token = c.Query("t")
}
}
if token == "" {
token = c.Params("token")
}
ale.Token = token
if token == "" {
@@ -2625,6 +2633,14 @@ func (h *AuthHandler) CompletePasswordReset(c *fiber.Ctx) error {
if resetToken != "" {
val, err := h.RedisService.Get(prefixPwdResetToken + resetToken)
if err != nil || strings.TrimSpace(val) == "" {
if usedLoginID, usedErr := h.RedisService.Get(prefixPwdResetUsed + resetToken); usedErr == nil && strings.TrimSpace(usedLoginID) != "" {
ale.Status = fiber.StatusOK
ale.LatencyMs = time.Since(startTime)
ale.Token = resetToken
ale.LoginIDs["loginId"] = strings.TrimSpace(usedLoginID)
ale.Log(slog.LevelInfo, "Duplicate reset completion ignored after successful use")
return c.JSON(fiber.Map{"message": "Password has been reset successfully."})
}
ale.Status = fiber.StatusUnauthorized
ale.LatencyMs = time.Since(startTime)
ale.ProviderError = "Invalid or expired reset token"
@@ -2694,6 +2710,7 @@ func (h *AuthHandler) CompletePasswordReset(c *fiber.Ctx) error {
ale.Log(slog.LevelInfo, "Password updated successfully", slog.String("login_id", loginID))
if resetToken != "" {
_ = h.RedisService.Delete(prefixPwdResetToken + resetToken)
_ = h.RedisService.Set(prefixPwdResetUsed+resetToken, loginID, pwdResetUsedExpiration)
}
return c.JSON(fiber.Map{"message": "Password has been reset successfully."})
}