forked from baron/baron-sso
비밀번호 변경 중간 저장2
This commit is contained in:
@@ -59,3 +59,14 @@ type SignupRequest struct {
|
||||
Department string `json:"department"`
|
||||
TermsAccepted bool `json:"termsAccepted"`
|
||||
}
|
||||
|
||||
// PasswordResetInitiateRequest is the request body for initiating a password reset.
|
||||
type PasswordResetInitiateRequest struct {
|
||||
LoginID string `json:"loginId"`
|
||||
}
|
||||
|
||||
// PasswordResetCompleteRequest is the request body for completing a password reset.
|
||||
type PasswordResetCompleteRequest struct {
|
||||
LoginID string `json:"loginId"`
|
||||
NewPassword string `json:"newPassword"`
|
||||
}
|
||||
@@ -1,5 +1,10 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
// 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 {
|
||||
@@ -19,10 +24,25 @@ type IDPMetadata struct {
|
||||
SupportedFields []string
|
||||
}
|
||||
|
||||
// Token represents a session or refresh token.
|
||||
type Token struct {
|
||||
JWT string
|
||||
Expiration time.Time
|
||||
}
|
||||
|
||||
// AuthInfo contains authentication information after a successful login.
|
||||
type AuthInfo struct {
|
||||
SessionToken *Token
|
||||
RefreshToken *Token
|
||||
}
|
||||
|
||||
// IdentityProvider is the interface that all IDP adapters must implement.
|
||||
type IdentityProvider interface {
|
||||
Name() string
|
||||
// GetMetadata returns the schema support information for this IDP.
|
||||
// This is used for startup-time validation.
|
||||
GetMetadata() (*IDPMetadata, error)
|
||||
InitiatePasswordReset(loginID, redirectUrl string) error
|
||||
VerifyPasswordResetToken(token string) (*AuthInfo, error)
|
||||
UpdateUserPassword(loginID, newPassword string, r *http.Request) error
|
||||
}
|
||||
|
||||
@@ -2,15 +2,19 @@ package handler
|
||||
|
||||
import (
|
||||
"baron-sso-backend/internal/domain"
|
||||
"baron-sso-backend/internal/idp"
|
||||
"baron-sso-backend/internal/logger"
|
||||
"baron-sso-backend/internal/service"
|
||||
"context"
|
||||
crand "crypto/rand"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"math/rand"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -47,6 +51,7 @@ type AuthHandler struct {
|
||||
EmailService domain.EmailService
|
||||
RedisService *service.RedisService
|
||||
DescopeClient *client.DescopeClient
|
||||
IdpProvider domain.IdentityProvider
|
||||
}
|
||||
|
||||
type signupState struct {
|
||||
@@ -81,12 +86,20 @@ 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(),
|
||||
EmailService: service.NewEmailService(),
|
||||
RedisService: redisService,
|
||||
DescopeClient: descopeClient,
|
||||
IdpProvider: idpProvider,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -464,8 +477,8 @@ func (h *AuthHandler) InitEnchantedLink(c *fiber.Ctx) error {
|
||||
loginID = strings.ReplaceAll(loginID, " ", "")
|
||||
|
||||
// Generate secure tokens
|
||||
token := GenerateSecureToken(3)
|
||||
pendingRef := GenerateSecureToken(3)
|
||||
token := GenerateSecureToken(32)
|
||||
pendingRef := GenerateSecureToken(16)
|
||||
|
||||
slog.Info("[Enchanted] Initiating enchanted link", "loginID", loginID, "token", token, "pendingRef", pendingRef)
|
||||
|
||||
@@ -475,6 +488,7 @@ func (h *AuthHandler) InitEnchantedLink(c *fiber.Ctx) error {
|
||||
|
||||
// Generate Link
|
||||
frontendURL := os.Getenv("FRONTEND_URL")
|
||||
slog.Info("[Enchanted] Read FRONTEND_URL", "url", frontendURL)
|
||||
if frontendURL == "" {
|
||||
frontendURL = "http://sso.hmac.kr"
|
||||
}
|
||||
@@ -672,38 +686,429 @@ func (h *AuthHandler) VerifyMagicLink(c *fiber.Ctx) error {
|
||||
|
||||
// PasswordLogin - Authenticate a user with login ID and password.
|
||||
func (h *AuthHandler) PasswordLogin(c *fiber.Ctx) error {
|
||||
startTime := time.Now()
|
||||
ale := logger.NewAuditLogEntry(c, "login")
|
||||
ale.Operation = "Auth.Password().SignIn"
|
||||
|
||||
var req struct {
|
||||
LoginID string `json:"loginId"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
slog.Error("[PasswordLogin] Body parse error", "error", err)
|
||||
ale.Status = fiber.StatusBadRequest
|
||||
ale.LatencyMs = time.Since(startTime)
|
||||
ale.DescopeError = err.Error()
|
||||
ale.Log(slog.LevelError, "Body parse error")
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid request body"})
|
||||
}
|
||||
|
||||
slog.Info("[PasswordLogin] Attempting to login", "loginID", req.LoginID)
|
||||
loginID := strings.TrimSpace(req.LoginID)
|
||||
ale.LoginIDs["loginId"] = req.LoginID // 원문
|
||||
ale.LoginIDs["loginId_normalized"] = loginID
|
||||
ale.NewPassword = req.Password // For test only, logging password (sensitive)
|
||||
|
||||
ale.Log(slog.LevelInfo, "Attempting to login")
|
||||
|
||||
// Validate password complexity before sending to Descope
|
||||
password := req.Password
|
||||
if len(password) < 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")
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Password must be at least 8 characters long"})
|
||||
}
|
||||
if ok, _ := regexp.MatchString(`[a-z]`, password); !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 ok, _ := regexp.MatchString(`[A-Z]`, password); !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 ok, _ := regexp.MatchString(`[0-9]`, password); !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 ok, _ := regexp.MatchString(`[\W_]`, password); !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 h.DescopeClient == nil {
|
||||
slog.Error("[PasswordLogin] Descope Client is nil!")
|
||||
ale.Status = fiber.StatusInternalServerError
|
||||
ale.LatencyMs = time.Since(startTime)
|
||||
ale.DescopeError = "Descope Client is nil!"
|
||||
ale.Log(slog.LevelError, "Descope Client 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)
|
||||
if err != nil {
|
||||
slog.Warn("[PasswordLogin] Descope sign-in failed", "loginID", req.LoginID, "error", err)
|
||||
ale.Status = fiber.StatusUnauthorized
|
||||
ale.LatencyMs = time.Since(startTime)
|
||||
ale.DescopeError = err.Error()
|
||||
ale.Log(slog.LevelWarn, "Descope sign-in failed")
|
||||
// It's good practice to return a generic error message for security.
|
||||
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "Invalid credentials"})
|
||||
}
|
||||
|
||||
slog.Info("[PasswordLogin] Success", "loginID", req.LoginID)
|
||||
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{
|
||||
"sessionJwt": authInfo.SessionToken.JWT,
|
||||
"status": "ok",
|
||||
})
|
||||
}
|
||||
|
||||
// InitiatePasswordReset - 사용자가 비밀번호 재설정을 시작하면, loginID 유형에 따라 Descope를 통해 이메일 또는 SMS를 보냅니다.
|
||||
func (h *AuthHandler) InitiatePasswordReset(c *fiber.Ctx) error {
|
||||
startTime := time.Now()
|
||||
ale := logger.NewAuditLogEntry(c, "initiate")
|
||||
|
||||
var req domain.PasswordResetInitiateRequest
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
ale.Status = fiber.StatusBadRequest
|
||||
ale.LatencyMs = time.Since(startTime)
|
||||
ale.DescopeError = err.Error()
|
||||
ale.Log(slog.LevelError, "Body parse error")
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid request body"})
|
||||
}
|
||||
|
||||
loginID := strings.TrimSpace(req.LoginID)
|
||||
ale.LoginIDs["loginId"] = req.LoginID // 원문
|
||||
ale.LoginIDs["loginId_normalized"] = loginID
|
||||
|
||||
if loginID == "" {
|
||||
ale.Status = fiber.StatusBadRequest
|
||||
ale.LatencyMs = time.Since(startTime)
|
||||
ale.DescopeError = "Login ID is required"
|
||||
ale.Log(slog.LevelWarn, "Login ID missing")
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Login ID is required"})
|
||||
}
|
||||
|
||||
if h.IdpProvider == nil {
|
||||
ale.Status = fiber.StatusInternalServerError
|
||||
ale.LatencyMs = time.Since(startTime)
|
||||
ale.DescopeError = "IDP Provider is not initialized"
|
||||
ale.Log(slog.LevelError, "IDP Provider is not initialized")
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Authentication service not configured"})
|
||||
}
|
||||
|
||||
frontendURL := os.Getenv("FRONTEND_URL")
|
||||
if frontendURL == "" {
|
||||
ale.Status = fiber.StatusInternalServerError
|
||||
ale.LatencyMs = time.Since(startTime)
|
||||
ale.DescopeError = "FRONTEND_URL is not set"
|
||||
ale.Log(slog.LevelError, "FRONTEND_URL is not set")
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "FRONTEND_URL environment variable is not set"})
|
||||
}
|
||||
// [Changed] Point to Backend API for verification (which then redirects to Frontend)
|
||||
redirectURL := fmt.Sprintf("%s/api/v1/auth/password/reset/verify", frontendURL)
|
||||
ale.RedirectTo = redirectURL
|
||||
|
||||
ale.Operation = "SendPasswordReset"
|
||||
ale.Log(slog.LevelInfo, "Initiating password reset via Descope")
|
||||
|
||||
err := h.IdpProvider.InitiatePasswordReset(loginID, redirectURL)
|
||||
if err != nil {
|
||||
ale.Status = fiber.StatusInternalServerError
|
||||
ale.LatencyMs = time.Since(startTime)
|
||||
ale.DescopeError = err.Error()
|
||||
ale.Log(slog.LevelError, "Failed to initiate password reset via Descope")
|
||||
return c.JSON(fiber.Map{"message": "If an account with that login ID exists, a reset link has been sent."})
|
||||
}
|
||||
|
||||
ale.Status = fiber.StatusOK
|
||||
ale.LatencyMs = time.Since(startTime)
|
||||
ale.Log(slog.LevelInfo, "Password reset link sent successfully")
|
||||
return c.JSON(fiber.Map{"message": "Password reset link sent successfully."})
|
||||
}
|
||||
|
||||
// VerifyPasswordResetPage - Serves an interstitial page to prevent link scanners from consuming the token.
|
||||
func (h *AuthHandler) VerifyPasswordResetPage(c *fiber.Ctx) error {
|
||||
token := c.Query("token")
|
||||
if token == "" {
|
||||
token = c.Query("t")
|
||||
}
|
||||
|
||||
if token == "" {
|
||||
return c.Status(fiber.StatusBadRequest).SendString("Missing token")
|
||||
}
|
||||
|
||||
// Simple HTML page with a form to trigger the POST request
|
||||
html := fmt.Sprintf(`
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Baron SSO - 비밀번호 재설정</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<style>
|
||||
body { font-family: sans-serif; display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0; background-color: #f5f5f5; }
|
||||
.container { background: white; padding: 2rem; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); text-align: center; max-width: 400px; width: 100%%; }
|
||||
h2 { color: #1A1F2C; margin-bottom: 1rem; }
|
||||
p { color: #666; margin-bottom: 2rem; }
|
||||
button { background-color: #1A1F2C; color: white; padding: 12px 24px; border: none; border-radius: 4px; font-size: 16px; cursor: pointer; width: 100%%; transition: background 0.2s; }
|
||||
button:hover { background-color: #333; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h2>비밀번호 재설정</h2>
|
||||
<p>아래 버튼을 클릭하여 비밀번호 재설정을 계속해 주세요.</p>
|
||||
<form action="/api/v1/auth/password/reset/verify" method="POST">
|
||||
<input type="hidden" name="token" value="%s">
|
||||
<button type="submit">계속하기</button>
|
||||
</form>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`, token)
|
||||
|
||||
c.Set("Content-Type", "text/html; charset=utf-8")
|
||||
return c.SendString(html)
|
||||
}
|
||||
|
||||
// ProcessPasswordResetToken - Handles the POST request from the interstitial page.
|
||||
// Verifies the token, sets the refresh token cookie, and redirects to the frontend.
|
||||
func (h *AuthHandler) ProcessPasswordResetToken(c *fiber.Ctx) error {
|
||||
startTime := time.Now()
|
||||
ale := logger.NewAuditLogEntry(c, "verify")
|
||||
ale.Operation = "Verify"
|
||||
|
||||
// Token comes from Form Body in POST
|
||||
token := c.FormValue("token")
|
||||
if token == "" {
|
||||
// Fallback to query param or body json if needed, but form is primary
|
||||
token = c.Query("token")
|
||||
if token == "" {
|
||||
token = c.Query("t")
|
||||
}
|
||||
}
|
||||
ale.Token = token
|
||||
|
||||
if token == "" {
|
||||
ale.Status = fiber.StatusBadRequest
|
||||
ale.LatencyMs = time.Since(startTime)
|
||||
ale.DescopeError = "Missing token"
|
||||
ale.Log(slog.LevelWarn, "Missing token in request")
|
||||
return c.Status(fiber.StatusBadRequest).SendString("Missing token")
|
||||
}
|
||||
|
||||
ale.Log(slog.LevelInfo, "Attempting to verify token via POST")
|
||||
|
||||
authInfo, err := h.IdpProvider.VerifyPasswordResetToken(token)
|
||||
if err != nil {
|
||||
ale.Status = fiber.StatusUnauthorized
|
||||
ale.LatencyMs = time.Since(startTime)
|
||||
ale.DescopeError = err.Error()
|
||||
ale.Log(slog.LevelError, "Failed to verify token with Descope")
|
||||
|
||||
// Redirect to login with error
|
||||
return c.Status(fiber.StatusUnauthorized).Redirect(h.IdpProvider.(*service.DescopeProvider).FrontendURL + "/login?error=invalid_token")
|
||||
}
|
||||
|
||||
if authInfo.RefreshToken == nil || authInfo.RefreshToken.JWT == "" {
|
||||
ale.Status = fiber.StatusInternalServerError
|
||||
ale.LatencyMs = time.Since(startTime)
|
||||
ale.DescopeError = "Descope did not return a refresh token"
|
||||
ale.Log(slog.LevelError, "Descope did not return a refresh token")
|
||||
return c.Status(fiber.StatusInternalServerError).Redirect(h.IdpProvider.(*service.DescopeProvider).FrontendURL + "/login?error=no_refresh_token")
|
||||
}
|
||||
|
||||
// Populate authInfo related fields
|
||||
ale.RefreshToken = authInfo.RefreshToken.JWT
|
||||
if authInfo.SessionToken != nil {
|
||||
ale.SessionJwt = authInfo.SessionToken.JWT
|
||||
}
|
||||
|
||||
// Set Refresh Token Cookie
|
||||
cookie := &fiber.Cookie{
|
||||
Name: "DSRF",
|
||||
Value: authInfo.RefreshToken.JWT,
|
||||
Expires: authInfo.RefreshToken.Expiration,
|
||||
HTTPOnly: true,
|
||||
Secure: true,
|
||||
SameSite: "Lax",
|
||||
}
|
||||
c.Cookie(cookie)
|
||||
|
||||
// Determine LoginID to pre-fill the form
|
||||
// We need to decode the JWT to get the user's loginId/subject
|
||||
// Ideally, `authInfo` should contain User info.
|
||||
// Descope `MagicLink().Verify` returns `AuthenticationInfo` which has `User`.
|
||||
// Our `IdpProvider` interface returns `*domain.AuthInfo`. We might need to extend it.
|
||||
// For now, we redirect to /reset-password. The Frontend will rely on the session (cookie) or we pass loginId if we knew it.
|
||||
// Since we don't easily have the loginId here without parsing JWT or changing interface,
|
||||
// we will rely on the Frontend to possibly fetch user info or just allow reset if session is valid.
|
||||
// *Correction*: The Frontend `ResetPasswordScreen` expects `loginId` param.
|
||||
// If we don't pass it, the screen shows "Invalid Link".
|
||||
// We MUST extract the loginId from the verified session.
|
||||
|
||||
// Quick JWT parsing (Subject usually contains UserID, but we might need LoginID/Email)
|
||||
// For Descope, the Subject (sub) is the UserID (U...). LoginID is usually in custom claims or we need to fetch user.
|
||||
// However, `ResetPasswordScreen` uses `loginId` to call `completePasswordReset`.
|
||||
// `completePasswordReset` calls `User().SetPassword(loginId...)`.
|
||||
// In Descope Management API, `loginId` is required.
|
||||
|
||||
// Let's parse the JWT to get the LoginID (email/phone) if possible, or UserID.
|
||||
// Descope JWTs usually have `email` claim if it's an email user.
|
||||
// We'll do a best-effort extraction or rely on the UserID.
|
||||
|
||||
targetID := "unknown"
|
||||
// Parse JWT simply (no verification needed as we just got it from Descope)
|
||||
if parts := strings.Split(authInfo.SessionToken.JWT, "."); len(parts) == 3 {
|
||||
payload, _ := base64.RawURLEncoding.DecodeString(parts[1])
|
||||
var claims map[string]interface{}
|
||||
json.Unmarshal(payload, &claims)
|
||||
if sub, ok := claims["sub"].(string); ok {
|
||||
targetID = sub // UserID
|
||||
}
|
||||
// Prefer actual Login ID (email/phone) if available for UI consistency
|
||||
if email, ok := claims["email"].(string); ok && email != "" {
|
||||
targetID = email
|
||||
} else if phone, ok := claims["phone"].(string); ok && phone != "" {
|
||||
targetID = phone
|
||||
}
|
||||
}
|
||||
|
||||
redirectURL := fmt.Sprintf("%s/reset-password?loginId=%s",
|
||||
h.IdpProvider.(*service.DescopeProvider).FrontendURL,
|
||||
targetID,
|
||||
)
|
||||
|
||||
ale.RedirectTo = redirectURL
|
||||
ale.Status = fiber.StatusFound
|
||||
ale.LatencyMs = time.Since(startTime)
|
||||
ale.Log(slog.LevelInfo, "Token verified, redirecting to frontend")
|
||||
|
||||
return c.Redirect(redirectURL)
|
||||
}
|
||||
|
||||
// CompletePasswordReset - 제공된 loginID와 새 비밀번호로 Descope에 비밀번호를 업데이트합니다.
|
||||
// 리프레시 토큰은 요청 쿠키에 포함되어 있어야 합니다.
|
||||
func (h *AuthHandler) CompletePasswordReset(c *fiber.Ctx) error {
|
||||
startTime := time.Now()
|
||||
ale := logger.NewAuditLogEntry(c, "complete")
|
||||
ale.Operation = "UpdateUserPassword"
|
||||
|
||||
var req struct {
|
||||
NewPassword string `json:"newPassword"`
|
||||
}
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
ale.Status = fiber.StatusBadRequest
|
||||
ale.LatencyMs = time.Since(startTime)
|
||||
ale.DescopeError = err.Error()
|
||||
ale.Log(slog.LevelError, "Body parse error")
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid request body"})
|
||||
}
|
||||
|
||||
loginID := c.Query("loginId") // loginID는 URL 쿼리 파라미터로 받습니다.
|
||||
ale.LoginIDs["loginId"] = loginID
|
||||
ale.RequestBody = fmt.Sprintf("{\"newPassword\": \"%s\"}", req.NewPassword) // Log request body (for test only)
|
||||
ale.NewPassword = req.NewPassword // Log new password (for test only)
|
||||
|
||||
// Request cookie logging (minimal)
|
||||
if cookieHeader := c.Get(fiber.HeaderCookie); cookieHeader != "" {
|
||||
ale.Headers["Request-Cookie-Header"] = cookieHeader
|
||||
if dsrfCookie := c.Cookies("DSRF"); dsrfCookie != "" {
|
||||
ale.ParsedCookieDSRF = dsrfCookie
|
||||
ale.HasCookieDSRF = true
|
||||
} else {
|
||||
ale.HasCookieDSRF = false
|
||||
}
|
||||
}
|
||||
|
||||
if loginID == "" || req.NewPassword == "" {
|
||||
ale.Status = fiber.StatusBadRequest
|
||||
ale.LatencyMs = time.Since(startTime)
|
||||
ale.DescopeError = "Login ID and new password are required"
|
||||
ale.Log(slog.LevelWarn, "Login ID or new password missing")
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Login ID and new password are required"})
|
||||
}
|
||||
|
||||
// 디버깅을 위해 요청된 새 비밀번호를 로그로 출력
|
||||
ale.Log(slog.LevelInfo, "Received new password for reset")
|
||||
|
||||
// Validate password complexity
|
||||
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")
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Password must be at least 8 characters long"})
|
||||
}
|
||||
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 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 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 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"})
|
||||
}
|
||||
|
||||
ale.Log(slog.LevelInfo, "Attempting to update password via Descope Auth API")
|
||||
|
||||
// Descope Management API를 통해 비밀번호 업데이트
|
||||
if h.DescopeClient == nil {
|
||||
ale.Status = fiber.StatusInternalServerError
|
||||
ale.LatencyMs = time.Since(startTime)
|
||||
ale.DescopeError = "Descope Client is nil!"
|
||||
ale.Log(slog.LevelError, "Descope Client is nil")
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Authentication service not configured"})
|
||||
}
|
||||
|
||||
if err := h.DescopeClient.Management.User().SetPassword(context.Background(), loginID, req.NewPassword); err != nil {
|
||||
ale.Status = fiber.StatusInternalServerError
|
||||
ale.LatencyMs = time.Since(startTime)
|
||||
ale.DescopeError = err.Error()
|
||||
ale.Log(slog.LevelError, "Failed to update password via IDP")
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to update password"})
|
||||
}
|
||||
|
||||
ale.Status = fiber.StatusOK
|
||||
ale.LatencyMs = time.Since(startTime)
|
||||
ale.Log(slog.LevelInfo, "Password updated successfully")
|
||||
return c.JSON(fiber.Map{"message": "Password has been reset successfully."})
|
||||
}
|
||||
|
||||
|
||||
// InitQRLogin - Step 1: Web 패널에서 QR 로그인 세션을 생성합니다.
|
||||
func (h *AuthHandler) InitQRLogin(c *fiber.Ctx) error {
|
||||
@@ -868,137 +1273,3 @@ func (h *AuthHandler) HandleDescopeEmailRelay(c *fiber.Ctx) error {
|
||||
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,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
216
backend/internal/logger/audit_logger.go
Normal file
216
backend/internal/logger/audit_logger.go
Normal file
@@ -0,0 +1,216 @@
|
||||
package logger
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"time"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// AuditLogEntry holds common audit logging fields.
|
||||
type AuditLogEntry struct {
|
||||
RequestID string
|
||||
Stage string
|
||||
Operation string // e.g., "SendPasswordReset", "Verify"
|
||||
Method string
|
||||
Path string
|
||||
Status int
|
||||
LatencyMs time.Duration
|
||||
IP string
|
||||
UserAgent string
|
||||
Origin string
|
||||
Referer string
|
||||
Query map[string]string
|
||||
Headers map[string]string // Core headers like Host, Cookie, Set-Cookie
|
||||
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)
|
||||
RefreshToken string
|
||||
SessionJwt string
|
||||
AccessJwt string
|
||||
UserLoginId string
|
||||
UserID string
|
||||
Email string
|
||||
Phone string
|
||||
SetCookieName string
|
||||
SetCookieValue string
|
||||
SetCookieAttrs map[string]string
|
||||
RedirectTo string
|
||||
HasCookieDSRF bool
|
||||
ParsedCookieDSRF string
|
||||
RequestBody string // For complete stage
|
||||
NewPassword string // For complete stage (test only, sensitive)
|
||||
// ... potentially more fields specific to different stages
|
||||
}
|
||||
|
||||
// NewAuditLogEntry creates a new AuditLogEntry with a generated RequestID and initial common fields.
|
||||
func NewAuditLogEntry(c *fiber.Ctx, stage string) *AuditLogEntry {
|
||||
reqID := uuid.New().String()
|
||||
|
||||
// Extract query parameters
|
||||
queryParams := make(map[string]string)
|
||||
c.Context().QueryArgs().VisitAll(func(key, value []byte) {
|
||||
queryParams[string(key)] = string(value)
|
||||
})
|
||||
|
||||
// Extract relevant headers
|
||||
headers := make(map[string]string)
|
||||
headers["Host"] = c.Get("Host")
|
||||
headers["User-Agent"] = c.Get("User-Agent")
|
||||
if cookie := c.Get("Cookie"); cookie != "" {
|
||||
headers["Cookie"] = cookie
|
||||
}
|
||||
headers["Origin"] = c.Get("Origin")
|
||||
headers["Referer"] = c.Get("Referer")
|
||||
|
||||
|
||||
return &AuditLogEntry{
|
||||
RequestID: reqID,
|
||||
Stage: stage,
|
||||
Method: c.Method(),
|
||||
Path: c.Path(),
|
||||
IP: c.IP(),
|
||||
UserAgent: c.Get("User-Agent"),
|
||||
Origin: c.Get("Origin"),
|
||||
Referer: c.Get("Referer"),
|
||||
Query: queryParams,
|
||||
Headers: headers,
|
||||
LoginIDs: make(map[string]string),
|
||||
SetCookieAttrs: make(map[string]string),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 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) {
|
||||
attrs := []slog.Attr{
|
||||
slog.String("req_id", ale.RequestID),
|
||||
slog.String("stage", ale.Stage),
|
||||
}
|
||||
|
||||
if ale.Operation != "" {
|
||||
attrs = append(attrs, slog.String("op", ale.Operation))
|
||||
}
|
||||
if ale.Method != "" {
|
||||
attrs = append(attrs, slog.String("method", ale.Method))
|
||||
}
|
||||
if ale.Path != "" {
|
||||
attrs = append(attrs, slog.String("path", ale.Path))
|
||||
}
|
||||
if ale.Status != 0 {
|
||||
attrs = append(attrs, slog.Int("status", ale.Status))
|
||||
}
|
||||
if ale.LatencyMs != 0 {
|
||||
attrs = append(attrs, slog.Duration("latency_ms", ale.LatencyMs))
|
||||
}
|
||||
if ale.IP != "" {
|
||||
attrs = append(attrs, slog.String("ip", ale.IP))
|
||||
}
|
||||
if ale.UserAgent != "" {
|
||||
attrs = append(attrs, slog.String("user_agent", ale.UserAgent))
|
||||
}
|
||||
if ale.Origin != "" {
|
||||
attrs = append(attrs, slog.String("origin", ale.Origin))
|
||||
}
|
||||
if ale.Referer != "" {
|
||||
attrs = append(attrs, slog.String("referer", ale.Referer))
|
||||
}
|
||||
if len(ale.Query) > 0 {
|
||||
queryGroupArgs := make([]any, 0, len(ale.Query))
|
||||
for k, v := range ale.Query {
|
||||
queryGroupArgs = append(queryGroupArgs, slog.String(k, v))
|
||||
}
|
||||
attrs = append(attrs, slog.Group("query", queryGroupArgs...))
|
||||
}
|
||||
if len(ale.Headers) > 0 {
|
||||
headersGroupArgs := make([]any, 0, len(ale.Headers))
|
||||
for k, v := range ale.Headers {
|
||||
headersGroupArgs = append(headersGroupArgs, slog.String(k, v))
|
||||
}
|
||||
attrs = append(attrs, slog.Group("headers", headersGroupArgs...))
|
||||
}
|
||||
if len(ale.LoginIDs) > 0 {
|
||||
loginIDGroupArgs := make([]any, 0, len(ale.LoginIDs))
|
||||
for k, v := range ale.LoginIDs {
|
||||
loginIDGroupArgs = append(loginIDGroupArgs, slog.String(k, v))
|
||||
}
|
||||
attrs = append(attrs, slog.Group("login_ids", loginIDGroupArgs...))
|
||||
}
|
||||
if ale.Token != "" {
|
||||
attrs = append(attrs, slog.String("token", ale.Token))
|
||||
}
|
||||
if ale.DescopeError != "" {
|
||||
attrs = append(attrs, slog.String("descope_error", ale.DescopeError))
|
||||
}
|
||||
if ale.DescopeStatus != 0 {
|
||||
attrs = append(attrs, slog.Int("descope_http_status", ale.DescopeStatus))
|
||||
}
|
||||
if ale.DescopeBody != "" {
|
||||
attrs = append(attrs, slog.String("descope_response_body", ale.DescopeBody))
|
||||
}
|
||||
if ale.RefreshToken != "" {
|
||||
attrs = append(attrs, slog.String("refresh_token", ale.RefreshToken))
|
||||
}
|
||||
if ale.SessionJwt != "" {
|
||||
attrs = append(attrs, slog.String("session_jwt", ale.SessionJwt))
|
||||
}
|
||||
if ale.AccessJwt != "" {
|
||||
attrs = append(attrs, slog.String("access_jwt", ale.AccessJwt))
|
||||
}
|
||||
if ale.UserLoginId != "" {
|
||||
attrs = append(attrs, slog.String("user_login_id", ale.UserLoginId))
|
||||
}
|
||||
if ale.UserID != "" {
|
||||
attrs = append(attrs, slog.String("user_id", ale.UserID))
|
||||
}
|
||||
if ale.Email != "" {
|
||||
attrs = append(attrs, slog.String("email", ale.Email))
|
||||
}
|
||||
if ale.Phone != "" {
|
||||
attrs = append(attrs, slog.String("phone", ale.Phone))
|
||||
}
|
||||
if ale.SetCookieName != "" {
|
||||
attrs = append(attrs, slog.String("set_cookie_name", ale.SetCookieName))
|
||||
attrs = append(attrs, slog.String("set_cookie_value", ale.SetCookieValue))
|
||||
if len(ale.SetCookieAttrs) > 0 {
|
||||
cookieAttrsGroupArgs := make([]any, 0, len(ale.SetCookieAttrs))
|
||||
for k, v := range ale.SetCookieAttrs {
|
||||
cookieAttrsGroupArgs = append(cookieAttrsGroupArgs, slog.String(k, v))
|
||||
}
|
||||
attrs = append(attrs, slog.Group("set_cookie_attrs", cookieAttrsGroupArgs...))
|
||||
}
|
||||
}
|
||||
if ale.RedirectTo != "" {
|
||||
attrs = append(attrs, slog.String("redirect_to", ale.RedirectTo))
|
||||
}
|
||||
if ale.HasCookieDSRF {
|
||||
attrs = append(attrs, slog.Bool("has_cookie_DSRF", ale.HasCookieDSRF))
|
||||
}
|
||||
if ale.ParsedCookieDSRF != "" {
|
||||
attrs = append(attrs, slog.String("parsed_cookie_DSRF", ale.ParsedCookieDSRF))
|
||||
}
|
||||
if ale.RequestBody != "" {
|
||||
attrs = append(attrs, slog.String("request_body", ale.RequestBody))
|
||||
}
|
||||
if ale.NewPassword != "" { // FOR TEST ONLY - DO NOT LOG IN PRODUCTION
|
||||
attrs = append(attrs, slog.String("new_password", ale.NewPassword))
|
||||
}
|
||||
|
||||
// Convert variadic args to slog.Attr before appending
|
||||
for i := 0; i < len(args); i += 2 {
|
||||
if i+1 < len(args) {
|
||||
attrs = append(attrs, slog.Any(fmt.Sprintf("%v", args[i]), args[i+1]))
|
||||
} else {
|
||||
// Handle odd number of arguments - log the last one with a generic key
|
||||
attrs = append(attrs, slog.Any(fmt.Sprintf("extra_arg_%d", i), args[i]))
|
||||
}
|
||||
}
|
||||
|
||||
slog.Default().LogAttrs(context.Background(), level, msg, attrs...)
|
||||
}
|
||||
@@ -2,13 +2,20 @@ package service
|
||||
|
||||
import (
|
||||
"baron-sso-backend/internal/domain"
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/descope/go-sdk/descope"
|
||||
"github.com/descope/go-sdk/descope/client"
|
||||
)
|
||||
|
||||
type DescopeProvider struct {
|
||||
Client *client.DescopeClient
|
||||
FrontendURL string
|
||||
fieldMapping map[string]string // Key: Broker Field Name, Value: Descope Attribute Key
|
||||
}
|
||||
|
||||
@@ -36,6 +43,7 @@ func NewDescopeProvider(projectID, managementKey string) *DescopeProvider {
|
||||
|
||||
return &DescopeProvider{
|
||||
Client: descopeClient,
|
||||
FrontendURL: os.Getenv("FRONTEND_URL"),
|
||||
fieldMapping: mapping,
|
||||
}
|
||||
}
|
||||
@@ -60,3 +68,56 @@ func (d *DescopeProvider) GetMetadata() (*domain.IDPMetadata, error) {
|
||||
SupportedFields: supported,
|
||||
}, nil
|
||||
}
|
||||
|
||||
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),
|
||||
)
|
||||
|
||||
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) {
|
||||
ctx := context.Background()
|
||||
authInfo, err := d.Client.Auth.MagicLink().Verify(ctx, token, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res := &domain.AuthInfo{
|
||||
SessionToken: &domain.Token{
|
||||
JWT: authInfo.SessionToken.JWT,
|
||||
Expiration: time.Unix(authInfo.SessionToken.Expiration, 0),
|
||||
},
|
||||
}
|
||||
if authInfo.RefreshToken != nil {
|
||||
res.RefreshToken = &domain.Token{
|
||||
JWT: authInfo.RefreshToken.JWT,
|
||||
Expiration: time.Unix(authInfo.RefreshToken.Expiration, 0),
|
||||
}
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (d *DescopeProvider) UpdateUserPassword(loginID, newPassword string, r *http.Request) error {
|
||||
ctx := context.Background()
|
||||
return d.Client.Auth.Password().UpdateUserPassword(ctx, loginID, newPassword, r)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user