1
0
forked from baron/baron-sso

한 endpoint URL로 전체 서빙 #120

This commit is contained in:
Lectom C Han
2026-01-29 14:42:15 +09:00
parent 209314fea7
commit 77d4e9fd77
12 changed files with 254 additions and 73 deletions

View File

@@ -22,6 +22,7 @@ import (
"strings"
"time"
"github.com/descope/go-sdk/descope"
"github.com/descope/go-sdk/descope/client"
"github.com/gofiber/fiber/v2"
)
@@ -1357,7 +1358,6 @@ func (h *AuthHandler) CompletePasswordReset(c *fiber.Ctx) error {
// InitQRLogin - Step 1: Web 패널에서 QR 로그인 세션을 생성합니다.
func (h *AuthHandler) InitQRLogin(c *fiber.Ctx) error {
pendingRef := GenerateSecureToken(16)
userCode := GenerateUserCode()
// QR 코드 페이로드를 실제 접속 가능한 URL로 변경합니다.
userfrontURL := os.Getenv("USERFRONT_URL")
@@ -1376,7 +1376,6 @@ func (h *AuthHandler) InitQRLogin(c *fiber.Ctx) error {
"pendingRef": pendingRef,
"expiresIn": 300,
"interval": int(minPollInterval.Seconds()),
"userCode": userCode,
})
}
@@ -1433,17 +1432,26 @@ func (h *AuthHandler) ScanQRLogin(c *fiber.Ctx) error {
slog.Info("[QR] Scan & Approve", "pendingRef", req.PendingRef)
if req.Token == "" {
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "Missing session token"})
}
// 1. Redis에서 세션 확인
val, err := h.RedisService.Get(prefixSession + req.PendingRef)
if err != nil || val == "" {
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "Session expired or not found"})
}
// 2. 모바일 유저의 토큰으로 새 세션 토큰(웹용)을 발행하거나 그대로 전달
// 2. 모바일 토큰은 승인 검증용으로만 사용하고, 웹 전용 세션을 새로 발급
sessionToken, err := h.issueQRWebSession(c, req.Token)
if 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"})
}
sessionData, _ := json.Marshal(map[string]string{
"status": statusSuccess,
"jwt": req.Token,
"jwt": sessionToken,
})
h.RedisService.Set(prefixSession+req.PendingRef, string(sessionData), 5*time.Minute)
@@ -1910,6 +1918,108 @@ func (h *AuthHandler) resolveIdentityID(c *fiber.Ctx, token string) (string, err
return id, err
}
func (h *AuthHandler) issueQRWebSession(c *fiber.Ctx, token string) (string, error) {
if looksLikeJWT(token) && h.DescopeClient != nil {
authorized, userToken, err := h.DescopeClient.Auth.ValidateSessionWithToken(c.Context(), token)
if err == nil && authorized {
loginID, err := h.resolveDescopeLoginID(c.Context(), userToken)
if err != nil {
return "", err
}
authInfo, err := h.IdpProvider.IssueSession(loginID)
if err != nil {
return "", err
}
if authInfo == nil || authInfo.SessionToken == nil || authInfo.SessionToken.JWT == "" {
return "", fmt.Errorf("descope issue session returned empty token")
}
return authInfo.SessionToken.JWT, nil
}
}
identityID, _, err := h.getKratosIdentity(token)
if err != nil {
return "", err
}
return h.issueKratosSession(c.Context(), identityID)
}
func (h *AuthHandler) resolveDescopeLoginID(ctx context.Context, token *descope.Token) (string, error) {
if token == nil {
return "", fmt.Errorf("descope token is nil")
}
if loginID := extractLoginIDFromClaims(token.Claims); loginID != "" {
return loginID, nil
}
if h.DescopeClient == nil {
return "", fmt.Errorf("descope client is nil")
}
user, err := h.DescopeClient.Management.User().Load(ctx, token.ID)
if err != nil {
return "", err
}
if user == nil {
return "", fmt.Errorf("descope user not found")
}
if loginID := pickPrimaryLoginID(user.LoginIDs); loginID != "" {
return loginID, nil
}
if user.Email != "" {
return user.Email, nil
}
if user.Phone != "" {
return user.Phone, nil
}
return "", fmt.Errorf("descope login id not found")
}
func pickPrimaryLoginID(loginIDs []string) string {
for _, id := range loginIDs {
if strings.Contains(id, "@") {
return id
}
}
if len(loginIDs) > 0 {
return loginIDs[0]
}
return ""
}
func extractLoginIDFromClaims(claims map[string]any) string {
if claims == nil {
return ""
}
candidateKeys := []string{"loginId", "login_id", "email", "phone_number", "phone", "phoneNumber"}
for _, key := range candidateKeys {
if raw, ok := claims[key]; ok {
if value, ok := raw.(string); ok && value != "" {
return value
}
}
}
if raw, ok := claims["loginIds"]; ok {
switch ids := raw.(type) {
case []string:
return pickPrimaryLoginID(ids)
case []any:
casted := make([]string, 0, len(ids))
for _, item := range ids {
if value, ok := item.(string); ok && value != "" {
casted = append(casted, value)
}
}
return pickPrimaryLoginID(casted)
}
}
return ""
}
func (h *AuthHandler) getKratosIdentity(sessionToken string) (string, map[string]interface{}, error) {
kratosURL := strings.TrimRight(os.Getenv("KRATOS_PUBLIC_URL"), "/")
if kratosURL == "" {
@@ -1944,6 +2054,49 @@ func (h *AuthHandler) getKratosIdentity(sessionToken string) (string, map[string
return result.Identity.ID, result.Identity.Traits, nil
}
func (h *AuthHandler) issueKratosSession(ctx context.Context, identityID string) (string, error) {
if identityID == "" {
return "", fmt.Errorf("kratos identity id is empty")
}
kratosAdminURL := strings.TrimRight(os.Getenv("KRATOS_ADMIN_URL"), "/")
if kratosAdminURL == "" {
kratosAdminURL = "http://kratos:4434"
}
payload := map[string]interface{}{
"identity_id": identityID,
}
body, _ := json.Marshal(payload)
req, err := http.NewRequestWithContext(ctx, http.MethodPost, kratosAdminURL+"/admin/sessions", bytes.NewReader(body))
if err != nil {
return "", err
}
req.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
respBody, _ := io.ReadAll(io.LimitReader(resp.Body, 4096))
if resp.StatusCode >= 300 {
return "", fmt.Errorf("kratos admin create session failed status=%d body=%s", resp.StatusCode, string(respBody))
}
var parsed struct {
SessionToken string `json:"session_token"`
}
if err := json.Unmarshal(respBody, &parsed); err != nil {
return "", err
}
if parsed.SessionToken == "" {
return "", fmt.Errorf("kratos admin session token missing: %s", string(respBody))
}
return parsed.SessionToken, nil
}
func (h *AuthHandler) getKratosIdentityWithCookie(cookie string) (string, map[string]interface{}, error) {
kratosURL := strings.TrimRight(os.Getenv("KRATOS_PUBLIC_URL"), "/")
if kratosURL == "" {