forked from baron/baron-sso
한 endpoint URL로 전체 서빙 #120
This commit is contained in:
@@ -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 == "" {
|
||||
|
||||
Reference in New Issue
Block a user