|
|
|
|
@@ -7,6 +7,7 @@ import (
|
|
|
|
|
"bytes"
|
|
|
|
|
"context"
|
|
|
|
|
crand "crypto/rand"
|
|
|
|
|
"encoding/base64"
|
|
|
|
|
"encoding/hex"
|
|
|
|
|
"encoding/json"
|
|
|
|
|
"errors"
|
|
|
|
|
@@ -41,6 +42,8 @@ const (
|
|
|
|
|
prefixLoginCodeQr = "login_code_qr:"
|
|
|
|
|
prefixPollMeta = "poll_meta:"
|
|
|
|
|
prefixQrRef = "qr_ref:"
|
|
|
|
|
prefixQrMeta = "qr_meta:"
|
|
|
|
|
prefixQrApproverSession = "qr_approver_session:"
|
|
|
|
|
prefixQrPending = "qr_pending:"
|
|
|
|
|
prefixSignupEmail = "signup:email:"
|
|
|
|
|
prefixSignupPhone = "signup:phone:"
|
|
|
|
|
@@ -605,6 +608,8 @@ func (h *AuthHandler) VerifySms(c *fiber.Ctx) error {
|
|
|
|
|
if authInfo == nil || authInfo.SessionToken == nil || authInfo.SessionToken.JWT == "" {
|
|
|
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to issue session"})
|
|
|
|
|
}
|
|
|
|
|
c.Locals("login_id", loginID)
|
|
|
|
|
setSessionIDLocal(c, authInfo.SessionToken)
|
|
|
|
|
|
|
|
|
|
return c.JSON(fiber.Map{
|
|
|
|
|
"token": authInfo.SessionToken.JWT,
|
|
|
|
|
@@ -855,6 +860,8 @@ func (h *AuthHandler) VerifyMagicLink(c *fiber.Ctx) error {
|
|
|
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to issue session"})
|
|
|
|
|
}
|
|
|
|
|
sessionToken := authInfo.SessionToken.JWT
|
|
|
|
|
c.Locals("login_id", loginID)
|
|
|
|
|
setSessionIDLocal(c, authInfo.SessionToken)
|
|
|
|
|
|
|
|
|
|
slog.Info("[Verify] Success! Updating Redis session", "pendingRef", pendingRef)
|
|
|
|
|
sessionData, _ := json.Marshal(map[string]string{
|
|
|
|
|
@@ -912,6 +919,8 @@ func (h *AuthHandler) VerifyLoginCode(c *fiber.Ctx) error {
|
|
|
|
|
if authInfo == nil || authInfo.SessionToken == nil || authInfo.SessionToken.JWT == "" {
|
|
|
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to issue session"})
|
|
|
|
|
}
|
|
|
|
|
c.Locals("login_id", lookupLoginID)
|
|
|
|
|
setSessionIDLocal(c, authInfo.SessionToken)
|
|
|
|
|
|
|
|
|
|
h.RedisService.Delete(prefixLoginCode + lookupLoginID)
|
|
|
|
|
h.RedisService.Delete(prefixLoginCodeSmsTarget + lookupLoginID)
|
|
|
|
|
@@ -995,6 +1004,8 @@ func (h *AuthHandler) VerifyLoginShortCode(c *fiber.Ctx) error {
|
|
|
|
|
if authInfo == nil || authInfo.SessionToken == nil || authInfo.SessionToken.JWT == "" {
|
|
|
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to issue session"})
|
|
|
|
|
}
|
|
|
|
|
c.Locals("login_id", payload.LoginID)
|
|
|
|
|
setSessionIDLocal(c, authInfo.SessionToken)
|
|
|
|
|
|
|
|
|
|
h.RedisService.Delete(prefixLoginCode + payload.LoginID)
|
|
|
|
|
h.RedisService.Delete(prefixLoginCodeShort + shortCode)
|
|
|
|
|
@@ -1080,6 +1091,7 @@ func (h *AuthHandler) PasswordLogin(c *fiber.Ctx) error {
|
|
|
|
|
ale.Status = fiber.StatusOK
|
|
|
|
|
ale.LatencyMs = time.Since(startTime)
|
|
|
|
|
ale.SessionJwt = authInfo.SessionToken.JWT
|
|
|
|
|
setSessionIDLocal(c, authInfo.SessionToken)
|
|
|
|
|
ale.Log(slog.LevelInfo, "Login successful", slog.String("provider", h.IdpProvider.Name()), slog.String("subject", authInfo.Subject))
|
|
|
|
|
|
|
|
|
|
resp := fiber.Map{
|
|
|
|
|
@@ -1430,6 +1442,7 @@ func (h *AuthHandler) InitQRLogin(c *fiber.Ctx) error {
|
|
|
|
|
// Redis에 초기 상태 저장 (5분 만료)
|
|
|
|
|
h.RedisService.Set(prefixSession+pendingRef, fmt.Sprintf(`{"status":"%s"}`, statusPending), 5*time.Minute)
|
|
|
|
|
h.RedisService.Set(prefixQrRef+qrRef, pendingRef, 5*time.Minute)
|
|
|
|
|
h.storeQrMeta(pendingRef, c)
|
|
|
|
|
|
|
|
|
|
return c.JSON(fiber.Map{
|
|
|
|
|
"qrCode": qrPayload, // 프론트엔드에서 이 텍스트로 QR을 생성하거나, 이미지를 반환
|
|
|
|
|
@@ -1514,6 +1527,9 @@ func (h *AuthHandler) ScanQRLogin(c *fiber.Ctx) error {
|
|
|
|
|
slog.Warn("[QR] Cookie session invalid", "error", err)
|
|
|
|
|
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "Invalid session"})
|
|
|
|
|
}
|
|
|
|
|
if sessionID, err := h.getKratosSessionIDWithCookie(cookie); err == nil && sessionID != "" {
|
|
|
|
|
h.storeQrApproverSessionID(pendingRef, sessionID)
|
|
|
|
|
}
|
|
|
|
|
loginID := pickLoginIDFromTraits(traits)
|
|
|
|
|
if loginID == "" {
|
|
|
|
|
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "Invalid session"})
|
|
|
|
|
@@ -1529,18 +1545,30 @@ func (h *AuthHandler) ScanQRLogin(c *fiber.Ctx) error {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 2. 모바일 토큰은 승인 검증용으로만 사용하고, 웹 전용 세션을 새로 발급
|
|
|
|
|
if sessionToken, err := h.tryIssueDescopeQrSession(c, req.Token); err != nil {
|
|
|
|
|
if sessionToken, loginID, approvedSessionID, err := h.tryIssueDescopeQrSession(c, req.Token); err != nil {
|
|
|
|
|
slog.Error("[QR] Issue web session failed", "error", err)
|
|
|
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to issue web session"})
|
|
|
|
|
} else if sessionToken != "" {
|
|
|
|
|
} else if sessionToken != nil && sessionToken.JWT != "" {
|
|
|
|
|
h.storeQrApproverSessionID(pendingRef, approvedSessionID)
|
|
|
|
|
h.writeQrAuditLog(loginID, pendingRef, sessionToken, approvedSessionID)
|
|
|
|
|
sessionData, _ := json.Marshal(map[string]string{
|
|
|
|
|
"status": statusSuccess,
|
|
|
|
|
"jwt": sessionToken,
|
|
|
|
|
"jwt": sessionToken.JWT,
|
|
|
|
|
})
|
|
|
|
|
h.RedisService.Set(prefixSession+pendingRef, string(sessionData), 5*time.Minute)
|
|
|
|
|
return c.JSON(fiber.Map{"message": "QR Login Approved"})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
approvedSessionID := ""
|
|
|
|
|
if req.Token != "" {
|
|
|
|
|
if sessionID, err := h.getKratosSessionID(req.Token); err == nil {
|
|
|
|
|
approvedSessionID = sessionID
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if approvedSessionID != "" {
|
|
|
|
|
h.storeQrApproverSessionID(pendingRef, approvedSessionID)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
loginID, err := h.resolveKratosLoginID(req.Token)
|
|
|
|
|
if err != nil {
|
|
|
|
|
slog.Warn("[QR] Invalid token", "error", err)
|
|
|
|
|
@@ -1613,6 +1641,7 @@ func (h *AuthHandler) HandleKratosCourierRelay(c *fiber.Ctx) error {
|
|
|
|
|
"jwt": authInfo.SessionToken.JWT,
|
|
|
|
|
})
|
|
|
|
|
h.RedisService.Set(prefixSession+pendingRef, string(sessionData), loginCodeExpiration)
|
|
|
|
|
h.writeQrAuditLog(loginID, pendingRef, authInfo.SessionToken, "")
|
|
|
|
|
h.RedisService.Delete(prefixLoginCodeQrPending + loginID)
|
|
|
|
|
h.RedisService.Delete(prefixLoginCode + loginID)
|
|
|
|
|
h.RedisService.Delete(prefixLoginCodeQr + pendingRef)
|
|
|
|
|
@@ -1640,6 +1669,7 @@ func (h *AuthHandler) HandleKratosCourierRelay(c *fiber.Ctx) error {
|
|
|
|
|
"jwt": authInfo.SessionToken.JWT,
|
|
|
|
|
})
|
|
|
|
|
h.RedisService.Set(prefixSession+pendingRef, string(sessionData), loginCodeExpiration)
|
|
|
|
|
h.writeQrAuditLog(loginID, pendingRef, authInfo.SessionToken, "")
|
|
|
|
|
h.RedisService.Delete(prefixQrPending + loginID)
|
|
|
|
|
h.RedisService.Delete(prefixLoginCode + loginID)
|
|
|
|
|
h.RedisService.Delete(prefixLoginCodeSmsTarget + loginID)
|
|
|
|
|
@@ -2083,6 +2113,176 @@ func looksLikeJWT(token string) bool {
|
|
|
|
|
return strings.Count(token, ".") == 2
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func setSessionIDLocal(c *fiber.Ctx, token *domain.Token) {
|
|
|
|
|
if c == nil || token == nil {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if sessionID := extractSessionIDFromToken(token); sessionID != "" {
|
|
|
|
|
c.Locals("session_id", sessionID)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func extractSessionIDFromToken(token *domain.Token) string {
|
|
|
|
|
if token == nil {
|
|
|
|
|
return ""
|
|
|
|
|
}
|
|
|
|
|
if token.SessionID != "" {
|
|
|
|
|
return token.SessionID
|
|
|
|
|
}
|
|
|
|
|
if token.JWT != "" {
|
|
|
|
|
return extractSessionIDFromJWT(token.JWT)
|
|
|
|
|
}
|
|
|
|
|
return ""
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func extractSessionIDFromJWT(token string) string {
|
|
|
|
|
if !looksLikeJWT(token) {
|
|
|
|
|
return ""
|
|
|
|
|
}
|
|
|
|
|
parts := strings.Split(token, ".")
|
|
|
|
|
if len(parts) != 3 {
|
|
|
|
|
return ""
|
|
|
|
|
}
|
|
|
|
|
payload, err := base64.RawURLEncoding.DecodeString(parts[1])
|
|
|
|
|
if err != nil {
|
|
|
|
|
payload, err = base64.URLEncoding.DecodeString(parts[1])
|
|
|
|
|
if err != nil {
|
|
|
|
|
return ""
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
var claims map[string]any
|
|
|
|
|
if err := json.Unmarshal(payload, &claims); err != nil {
|
|
|
|
|
return ""
|
|
|
|
|
}
|
|
|
|
|
for _, key := range []string{"sid", "session_id", "sessionId", "jti"} {
|
|
|
|
|
if raw, ok := claims[key]; ok {
|
|
|
|
|
switch value := raw.(type) {
|
|
|
|
|
case string:
|
|
|
|
|
if value != "" {
|
|
|
|
|
return value
|
|
|
|
|
}
|
|
|
|
|
default:
|
|
|
|
|
return fmt.Sprint(value)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return ""
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type qrMeta struct {
|
|
|
|
|
IPAddress string `json:"ip_address"`
|
|
|
|
|
UserAgent string `json:"user_agent"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (h *AuthHandler) storeQrMeta(pendingRef string, c *fiber.Ctx) {
|
|
|
|
|
if h.RedisService == nil || pendingRef == "" || c == nil {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
meta := qrMeta{
|
|
|
|
|
IPAddress: extractClientIPFromHeaders(c),
|
|
|
|
|
UserAgent: c.Get("User-Agent"),
|
|
|
|
|
}
|
|
|
|
|
raw, err := json.Marshal(meta)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
_ = h.RedisService.Set(prefixQrMeta+pendingRef, string(raw), 5*time.Minute)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (h *AuthHandler) loadQrMeta(pendingRef string) (qrMeta, bool) {
|
|
|
|
|
if h.RedisService == nil || pendingRef == "" {
|
|
|
|
|
return qrMeta{}, false
|
|
|
|
|
}
|
|
|
|
|
val, err := h.RedisService.Get(prefixQrMeta + pendingRef)
|
|
|
|
|
if err != nil || val == "" {
|
|
|
|
|
return qrMeta{}, false
|
|
|
|
|
}
|
|
|
|
|
var meta qrMeta
|
|
|
|
|
if err := json.Unmarshal([]byte(val), &meta); err != nil {
|
|
|
|
|
return qrMeta{}, false
|
|
|
|
|
}
|
|
|
|
|
return meta, true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (h *AuthHandler) storeQrApproverSessionID(pendingRef, sessionID string) {
|
|
|
|
|
if h.RedisService == nil || pendingRef == "" || sessionID == "" {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
_ = h.RedisService.Set(prefixQrApproverSession+pendingRef, sessionID, loginCodeExpiration)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (h *AuthHandler) loadQrApproverSessionID(pendingRef string) string {
|
|
|
|
|
if h.RedisService == nil || pendingRef == "" {
|
|
|
|
|
return ""
|
|
|
|
|
}
|
|
|
|
|
val, err := h.RedisService.Get(prefixQrApproverSession + pendingRef)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return ""
|
|
|
|
|
}
|
|
|
|
|
return strings.TrimSpace(val)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (h *AuthHandler) writeQrAuditLog(loginID, pendingRef string, sessionToken *domain.Token, approvedSessionID string) {
|
|
|
|
|
if h.AuditRepo == nil || pendingRef == "" {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
meta, ok := h.loadQrMeta(pendingRef)
|
|
|
|
|
if !ok {
|
|
|
|
|
meta = qrMeta{
|
|
|
|
|
IPAddress: "",
|
|
|
|
|
UserAgent: "",
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if approvedSessionID == "" {
|
|
|
|
|
approvedSessionID = h.loadQrApproverSessionID(pendingRef)
|
|
|
|
|
}
|
|
|
|
|
sessionID := extractSessionIDFromToken(sessionToken)
|
|
|
|
|
details := map[string]any{
|
|
|
|
|
"path": "/api/v1/auth/qr/approve",
|
|
|
|
|
"login_id": loginID,
|
|
|
|
|
"pending_ref": pendingRef,
|
|
|
|
|
}
|
|
|
|
|
if sessionID != "" {
|
|
|
|
|
details["session_id"] = sessionID
|
|
|
|
|
}
|
|
|
|
|
if approvedSessionID != "" {
|
|
|
|
|
details["approved_session_id"] = approvedSessionID
|
|
|
|
|
}
|
|
|
|
|
detailsJSON, _ := json.Marshal(details)
|
|
|
|
|
|
|
|
|
|
log := &domain.AuditLog{
|
|
|
|
|
EventID: GenerateSecureToken(16),
|
|
|
|
|
Timestamp: time.Now(),
|
|
|
|
|
UserID: "",
|
|
|
|
|
SessionID: sessionID,
|
|
|
|
|
EventType: "POST /api/v1/auth/qr/approve",
|
|
|
|
|
Status: "success",
|
|
|
|
|
IPAddress: meta.IPAddress,
|
|
|
|
|
UserAgent: meta.UserAgent,
|
|
|
|
|
Details: string(detailsJSON),
|
|
|
|
|
AuthMethod: "QR",
|
|
|
|
|
}
|
|
|
|
|
_ = h.AuditRepo.Create(log)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func extractClientIPFromHeaders(c *fiber.Ctx) string {
|
|
|
|
|
if c == nil {
|
|
|
|
|
return ""
|
|
|
|
|
}
|
|
|
|
|
if forwarded := c.Get("X-Forwarded-For"); forwarded != "" {
|
|
|
|
|
parts := strings.Split(forwarded, ",")
|
|
|
|
|
if len(parts) > 0 {
|
|
|
|
|
if ip := strings.TrimSpace(parts[0]); ip != "" {
|
|
|
|
|
return ip
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if realIP := strings.TrimSpace(c.Get("X-Real-IP")); realIP != "" {
|
|
|
|
|
return realIP
|
|
|
|
|
}
|
|
|
|
|
return c.IP()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (h *AuthHandler) GetAuthTimeline(c *fiber.Ctx) error {
|
|
|
|
|
if h.AuditRepo == nil {
|
|
|
|
|
return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{"error": "Audit service unavailable"})
|
|
|
|
|
@@ -2126,6 +2326,13 @@ func (h *AuthHandler) GetAuthTimeline(c *fiber.Ctx) error {
|
|
|
|
|
if log.UserID == "" {
|
|
|
|
|
log.UserID = profile.ID
|
|
|
|
|
}
|
|
|
|
|
log.AuthMethod = deriveAuthMethod(log)
|
|
|
|
|
if log.AuthMethod == "" {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
if log.SessionID == "" {
|
|
|
|
|
log.SessionID = extractSessionIDFromAuditDetails(log.Details)
|
|
|
|
|
}
|
|
|
|
|
items = append(items, log)
|
|
|
|
|
if len(items) >= limit {
|
|
|
|
|
break
|
|
|
|
|
@@ -2182,6 +2389,141 @@ func isAuthEventType(eventType string) bool {
|
|
|
|
|
return strings.Contains(normalized, " /api/v1/auth/")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func extractAuditPath(log domain.AuditLog) string {
|
|
|
|
|
if log.Details != "" {
|
|
|
|
|
if payload, err := parseAuditDetails(log.Details); err == nil {
|
|
|
|
|
if path, ok := payload["path"].(string); ok && path != "" {
|
|
|
|
|
return path
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
parts := strings.SplitN(log.EventType, " ", 2)
|
|
|
|
|
if len(parts) == 2 {
|
|
|
|
|
return strings.TrimSpace(parts[1])
|
|
|
|
|
}
|
|
|
|
|
return ""
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func parseAuditDetails(details string) (map[string]any, error) {
|
|
|
|
|
var payload map[string]any
|
|
|
|
|
if details == "" {
|
|
|
|
|
return nil, fmt.Errorf("empty details")
|
|
|
|
|
}
|
|
|
|
|
if err := json.Unmarshal([]byte(details), &payload); err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
return payload, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func extractRequestBody(details map[string]any) map[string]any {
|
|
|
|
|
if details == nil {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
raw, ok := details["request_body"].(string)
|
|
|
|
|
if !ok || raw == "" {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
var body map[string]any
|
|
|
|
|
if err := json.Unmarshal([]byte(raw), &body); err != nil {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
return body
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func loginIDKind(loginID string) string {
|
|
|
|
|
normalized := strings.TrimSpace(loginID)
|
|
|
|
|
if normalized == "" {
|
|
|
|
|
return ""
|
|
|
|
|
}
|
|
|
|
|
if strings.Contains(normalized, "@") {
|
|
|
|
|
return "email"
|
|
|
|
|
}
|
|
|
|
|
return "phone"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func deriveAuthMethod(log domain.AuditLog) string {
|
|
|
|
|
path := strings.ToLower(extractAuditPath(log))
|
|
|
|
|
if path == "" {
|
|
|
|
|
return ""
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
loginID := extractLoginIDFromAuditDetails(log.Details)
|
|
|
|
|
kind := loginIDKind(loginID)
|
|
|
|
|
details, _ := parseAuditDetails(log.Details)
|
|
|
|
|
requestBody := extractRequestBody(details)
|
|
|
|
|
|
|
|
|
|
switch {
|
|
|
|
|
case strings.Contains(path, "/api/v1/auth/password/login"):
|
|
|
|
|
if kind == "email" {
|
|
|
|
|
return "비밀번호(Email)"
|
|
|
|
|
}
|
|
|
|
|
if kind == "phone" {
|
|
|
|
|
return "비밀번호(전화번호)"
|
|
|
|
|
}
|
|
|
|
|
return "비밀번호"
|
|
|
|
|
case strings.Contains(path, "/api/v1/auth/enchanted-link/init"):
|
|
|
|
|
if requestBody != nil {
|
|
|
|
|
if raw, ok := requestBody["codeOnly"]; ok {
|
|
|
|
|
if value, ok := raw.(bool); ok && value {
|
|
|
|
|
if kind == "phone" {
|
|
|
|
|
return "코드(SMS)"
|
|
|
|
|
}
|
|
|
|
|
if kind == "email" {
|
|
|
|
|
return "코드(Email)"
|
|
|
|
|
}
|
|
|
|
|
return "코드"
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if requestBody != nil {
|
|
|
|
|
if raw, ok := requestBody["method"].(string); ok {
|
|
|
|
|
method := strings.ToLower(strings.TrimSpace(raw))
|
|
|
|
|
if method == "sms" {
|
|
|
|
|
return "링크(SMS)"
|
|
|
|
|
}
|
|
|
|
|
if method == "email" {
|
|
|
|
|
return "링크(Email)"
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if kind == "phone" {
|
|
|
|
|
return "링크(SMS)"
|
|
|
|
|
}
|
|
|
|
|
if kind == "email" {
|
|
|
|
|
return "링크(Email)"
|
|
|
|
|
}
|
|
|
|
|
return "링크"
|
|
|
|
|
case strings.Contains(path, "/api/v1/auth/magic-link/verify"):
|
|
|
|
|
if kind == "phone" {
|
|
|
|
|
return "링크(SMS)"
|
|
|
|
|
}
|
|
|
|
|
if kind == "email" {
|
|
|
|
|
return "링크(Email)"
|
|
|
|
|
}
|
|
|
|
|
return "링크"
|
|
|
|
|
case strings.Contains(path, "/api/v1/auth/login/code/verify"):
|
|
|
|
|
if kind == "phone" {
|
|
|
|
|
return "코드(SMS)"
|
|
|
|
|
}
|
|
|
|
|
if kind == "email" {
|
|
|
|
|
return "코드(Email)"
|
|
|
|
|
}
|
|
|
|
|
return "코드"
|
|
|
|
|
case strings.Contains(path, "/api/v1/auth/login/code/verify-short"):
|
|
|
|
|
return "코드(간편)"
|
|
|
|
|
case strings.Contains(path, "/api/v1/auth/verify-sms"):
|
|
|
|
|
return "코드(SMS)"
|
|
|
|
|
case strings.Contains(path, "/api/v1/auth/qr/approve"):
|
|
|
|
|
return "QR"
|
|
|
|
|
case strings.Contains(path, "/api/v1/auth/qr/init"):
|
|
|
|
|
return "QR"
|
|
|
|
|
case strings.Contains(path, "/api/v1/auth/qr/poll"):
|
|
|
|
|
return "QR"
|
|
|
|
|
default:
|
|
|
|
|
return ""
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func buildLoginCandidates(profile *domain.UserProfileResponse) map[string]struct{} {
|
|
|
|
|
candidates := make(map[string]struct{})
|
|
|
|
|
if profile == nil {
|
|
|
|
|
@@ -2256,6 +2598,25 @@ func extractLoginIDFromAuditDetails(details string) string {
|
|
|
|
|
return ""
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func extractSessionIDFromAuditDetails(details string) string {
|
|
|
|
|
if details == "" {
|
|
|
|
|
return ""
|
|
|
|
|
}
|
|
|
|
|
var payload map[string]any
|
|
|
|
|
if err := json.Unmarshal([]byte(details), &payload); err != nil {
|
|
|
|
|
return ""
|
|
|
|
|
}
|
|
|
|
|
if raw, ok := payload["session_id"]; ok {
|
|
|
|
|
switch value := raw.(type) {
|
|
|
|
|
case string:
|
|
|
|
|
return value
|
|
|
|
|
default:
|
|
|
|
|
return fmt.Sprint(value)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return ""
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (h *AuthHandler) resolveIdentityID(c *fiber.Ctx, token string) (string, error) {
|
|
|
|
|
if looksLikeJWT(token) && h.DescopeClient != nil {
|
|
|
|
|
authorized, userToken, err := h.DescopeClient.Auth.ValidateSessionWithToken(c.Context(), token)
|
|
|
|
|
@@ -2267,26 +2628,26 @@ func (h *AuthHandler) resolveIdentityID(c *fiber.Ctx, token string) (string, err
|
|
|
|
|
return id, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (h *AuthHandler) tryIssueDescopeQrSession(c *fiber.Ctx, token string) (string, error) {
|
|
|
|
|
func (h *AuthHandler) tryIssueDescopeQrSession(c *fiber.Ctx, token string) (*domain.Token, string, string, error) {
|
|
|
|
|
if !looksLikeJWT(token) || h.DescopeClient == nil {
|
|
|
|
|
return "", nil
|
|
|
|
|
return nil, "", "", nil
|
|
|
|
|
}
|
|
|
|
|
authorized, userToken, err := h.DescopeClient.Auth.ValidateSessionWithToken(c.Context(), token)
|
|
|
|
|
if err != nil || !authorized {
|
|
|
|
|
return "", nil
|
|
|
|
|
return nil, "", "", nil
|
|
|
|
|
}
|
|
|
|
|
loginID, err := h.resolveDescopeLoginID(c.Context(), userToken)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return "", err
|
|
|
|
|
return nil, "", "", err
|
|
|
|
|
}
|
|
|
|
|
authInfo, err := h.IdpProvider.IssueSession(loginID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return "", err
|
|
|
|
|
return nil, "", "", err
|
|
|
|
|
}
|
|
|
|
|
if authInfo == nil || authInfo.SessionToken == nil || authInfo.SessionToken.JWT == "" {
|
|
|
|
|
return "", fmt.Errorf("descope issue session returned empty token")
|
|
|
|
|
return nil, "", "", fmt.Errorf("descope issue session returned empty token")
|
|
|
|
|
}
|
|
|
|
|
return authInfo.SessionToken.JWT, nil
|
|
|
|
|
return authInfo.SessionToken, loginID, userToken.ID, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (h *AuthHandler) resolveKratosLoginID(token string) (string, error) {
|
|
|
|
|
@@ -2544,6 +2905,36 @@ func (h *AuthHandler) getKratosIdentity(sessionToken string) (string, map[string
|
|
|
|
|
return result.Identity.ID, result.Identity.Traits, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (h *AuthHandler) getKratosSessionID(sessionToken string) (string, error) {
|
|
|
|
|
kratosURL := strings.TrimRight(os.Getenv("KRATOS_PUBLIC_URL"), "/")
|
|
|
|
|
if kratosURL == "" {
|
|
|
|
|
kratosURL = "http://kratos:4433"
|
|
|
|
|
}
|
|
|
|
|
req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, kratosURL+"/sessions/whoami", nil)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return "", err
|
|
|
|
|
}
|
|
|
|
|
req.Header.Set("X-Session-Token", sessionToken)
|
|
|
|
|
|
|
|
|
|
resp, err := http.DefaultClient.Do(req)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return "", err
|
|
|
|
|
}
|
|
|
|
|
defer resp.Body.Close()
|
|
|
|
|
if resp.StatusCode >= 300 {
|
|
|
|
|
body, _ := io.ReadAll(io.LimitReader(resp.Body, 2048))
|
|
|
|
|
return "", fmt.Errorf("kratos whoami failed status=%d body=%s", resp.StatusCode, string(body))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var result struct {
|
|
|
|
|
ID string `json:"id"`
|
|
|
|
|
}
|
|
|
|
|
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
|
|
|
|
return "", err
|
|
|
|
|
}
|
|
|
|
|
return result.ID, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (h *AuthHandler) issueKratosSession(ctx context.Context, identityID string) (string, error) {
|
|
|
|
|
if identityID == "" {
|
|
|
|
|
return "", fmt.Errorf("kratos identity id is empty")
|
|
|
|
|
@@ -2621,6 +3012,36 @@ func (h *AuthHandler) getKratosIdentityWithCookie(cookie string) (string, map[st
|
|
|
|
|
return result.Identity.ID, result.Identity.Traits, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (h *AuthHandler) getKratosSessionIDWithCookie(cookie string) (string, error) {
|
|
|
|
|
kratosURL := strings.TrimRight(os.Getenv("KRATOS_PUBLIC_URL"), "/")
|
|
|
|
|
if kratosURL == "" {
|
|
|
|
|
kratosURL = "http://kratos:4433"
|
|
|
|
|
}
|
|
|
|
|
req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, kratosURL+"/sessions/whoami", nil)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return "", err
|
|
|
|
|
}
|
|
|
|
|
req.Header.Set("Cookie", cookie)
|
|
|
|
|
|
|
|
|
|
resp, err := http.DefaultClient.Do(req)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return "", err
|
|
|
|
|
}
|
|
|
|
|
defer resp.Body.Close()
|
|
|
|
|
if resp.StatusCode >= 300 {
|
|
|
|
|
body, _ := io.ReadAll(io.LimitReader(resp.Body, 2048))
|
|
|
|
|
return "", fmt.Errorf("kratos whoami failed status=%d body=%s", resp.StatusCode, string(body))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var result struct {
|
|
|
|
|
ID string `json:"id"`
|
|
|
|
|
}
|
|
|
|
|
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
|
|
|
|
return "", err
|
|
|
|
|
}
|
|
|
|
|
return result.ID, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (h *AuthHandler) updateKratosIdentity(identityID string, traits map[string]interface{}) error {
|
|
|
|
|
kratosAdminURL := strings.TrimRight(os.Getenv("KRATOS_ADMIN_URL"), "/")
|
|
|
|
|
if kratosAdminURL == "" {
|
|
|
|
|
|