forked from baron/baron-sso
refactor: backend tenant_group 제거 및 리팩터 반영
This commit is contained in:
@@ -125,11 +125,10 @@ func GenerateSecureAlnumToken(length int) string {
|
||||
|
||||
func GenerateUserCode() string {
|
||||
const letters = "ABCDEFGHJKLMNPQRSTUVWXYZ"
|
||||
// [Fixed] 요청하신 포맷 (영문 2자리 + 숫자 6자리, 하이픈 없음)으로 변경
|
||||
return fmt.Sprintf("%c%c%06d",
|
||||
return fmt.Sprintf("%c%c-%03d",
|
||||
letters[rand.Intn(len(letters))],
|
||||
letters[rand.Intn(len(letters))],
|
||||
rand.Intn(1000000),
|
||||
rand.Intn(1000),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -455,7 +454,8 @@ func (h *AuthHandler) Signup(c *fiber.Ctx) error {
|
||||
|
||||
slog.Info("[Signup] New user registered", "email", req.Email, "type", req.AffiliationType, "provider", h.IdpProvider.Name(), "subject", providerID)
|
||||
|
||||
// [New] Local DB Sync
|
||||
// [SoT Policy] Kratos가 SoT이므로 로컬 DB 저장은 비동기 Read-Model 동기화로 처리합니다.
|
||||
// 로컬 DB 저장이 실패하더라도 회원가입 프로세스는 성공으로 간주합니다.
|
||||
localUser := &domain.User{
|
||||
ID: providerID, // Match IDP Subject
|
||||
Email: req.Email,
|
||||
@@ -471,9 +471,17 @@ func (h *AuthHandler) Signup(c *fiber.Ctx) error {
|
||||
}
|
||||
|
||||
if h.UserRepo != nil {
|
||||
if err := h.UserRepo.Create(c.Context(), localUser); err != nil {
|
||||
slog.Error("[Signup] Failed to sync user to local DB", "email", req.Email, "error", err)
|
||||
}
|
||||
go func(u *domain.User) {
|
||||
// 요청 Context가 취소될 수 있으므로 Background Context 사용
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
if err := h.UserRepo.Create(ctx, u); err != nil {
|
||||
slog.Error("[Signup] Failed to sync user to Read-Model (Local DB)", "email", u.Email, "error", err)
|
||||
} else {
|
||||
slog.Debug("[Signup] Synced user to Read-Model", "email", u.Email)
|
||||
}
|
||||
}(localUser)
|
||||
}
|
||||
|
||||
// [Keto] Sync user-tenant relationship
|
||||
@@ -959,20 +967,13 @@ func (h *AuthHandler) InitEnchantedLink(c *fiber.Ctx) error {
|
||||
return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{"error": "Identity provider unavailable"})
|
||||
}
|
||||
|
||||
// [Changed] 토큰 길이를 사용자의 요청에 맞춰 6글자(3바이트)로, pendingRef를 8글자(4바이트)로 조정
|
||||
userCode := GenerateUserCode()
|
||||
token := GenerateSecureToken(3)
|
||||
pendingRef := GenerateSecureToken(3)
|
||||
|
||||
slog.Info("[Enchanted] Initiating enchanted link", "loginID", loginID, "token", token, "pendingRef", pendingRef)
|
||||
|
||||
// [Added] 사용자가 입력할 간편 코드를 Redis에 저장합니다. (이게 없으면 인증이 안 됩니다)
|
||||
shortCodePayload, _ := json.Marshal(shortLoginCodePayload{
|
||||
LoginID: lookupLoginID,
|
||||
Code: token,
|
||||
PendingRef: pendingRef,
|
||||
})
|
||||
h.RedisService.Set(prefixLoginCodeShort+userCode, string(shortCodePayload), defaultExpiration)
|
||||
|
||||
// Store in Redis
|
||||
sessionData, _ := json.Marshal(map[string]string{
|
||||
"status": statusPending,
|
||||
@@ -1026,13 +1027,12 @@ func (h *AuthHandler) InitEnchantedLink(c *fiber.Ctx) error {
|
||||
}
|
||||
} else {
|
||||
// Send SMS
|
||||
phone := sanitizePhoneForSms(loginID)
|
||||
content := fmt.Sprintf("[Baron 로그인] 로그인 링크: %s | 간편 코드: %s", link, userCode)
|
||||
content := fmt.Sprintf("[Baron 로그인] 로그인 링크: %s | 코드: %s", link, userCode)
|
||||
if drySend {
|
||||
slog.Info("[Enchanted][DrySend] SMS send skipped", "loginID", phone, "content", content)
|
||||
slog.Info("[Enchanted][DrySend] SMS send skipped", "loginID", loginID, "content", content)
|
||||
} else {
|
||||
slog.Info("[Enchanted] Sending SMS via Naver Cloud", "to", phone)
|
||||
if err := h.SmsService.SendSms(phone, content); err != nil {
|
||||
slog.Info("[Enchanted] Sending SMS via Naver Cloud", "loginID", loginID)
|
||||
if err := h.SmsService.SendSms(loginID, content); err != nil {
|
||||
slog.Error("[Enchanted] SMS Failed", "error", err)
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to send SMS"})
|
||||
}
|
||||
@@ -1526,7 +1526,7 @@ func (h *AuthHandler) PasswordLogin(c *fiber.Ctx) error {
|
||||
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.NewPassword = req.Password // For test only, logging password (sensitive)
|
||||
|
||||
ale.Log(slog.LevelInfo, "Attempting to login")
|
||||
|
||||
@@ -1568,25 +1568,22 @@ func (h *AuthHandler) PasswordLogin(c *fiber.Ctx) error {
|
||||
|
||||
// --- OIDC 로그인 흐름 처리 ---
|
||||
if req.LoginChallenge != "" {
|
||||
slog.Info("OIDC login flow detected", "challenge", req.LoginChallenge, "subject", subject)
|
||||
slog.Info("OIDC login flow detected", "challenge", req.LoginChallenge)
|
||||
|
||||
// Check if the client is active
|
||||
loginReq, err := h.Hydra.GetLoginRequest(c.Context(), req.LoginChallenge)
|
||||
if err == nil && loginReq != nil {
|
||||
slog.Info("OIDC Client Info", "client_id", loginReq.Client.ClientID, "name", loginReq.Client.ClientName)
|
||||
if loginReq.Client.Metadata != nil {
|
||||
if status, ok := loginReq.Client.Metadata["status"].(string); ok {
|
||||
if strings.ToLower(status) == "inactive" {
|
||||
slog.Warn("Login rejected for inactive client in PasswordLogin", "client_id", loginReq.Client.ClientID)
|
||||
return fiber.NewError(fiber.StatusForbidden, "The client application is disabled.")
|
||||
}
|
||||
if err == nil && loginReq != nil && loginReq.Client.Metadata != nil {
|
||||
if status, ok := loginReq.Client.Metadata["status"].(string); ok {
|
||||
if strings.ToLower(status) == "inactive" {
|
||||
slog.Warn("Login rejected for inactive client in PasswordLogin", "client_id", loginReq.Client.ClientID)
|
||||
return fiber.NewError(fiber.StatusForbidden, "The client application is disabled.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
acceptResp, err := h.Hydra.AcceptLoginRequest(c.Context(), req.LoginChallenge, subject)
|
||||
if err != nil {
|
||||
slog.Error("failed to accept hydra login request", "error", err, "challenge", req.LoginChallenge)
|
||||
slog.Error("failed to accept hydra login request", "error", err)
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to accept OIDC login request")
|
||||
}
|
||||
slog.Info("Hydra login request accepted", "redirectTo", acceptResp.RedirectTo)
|
||||
@@ -1597,13 +1594,12 @@ func (h *AuthHandler) PasswordLogin(c *fiber.Ctx) error {
|
||||
// --- OIDC 로그인 흐름 처리 끝 ---
|
||||
|
||||
resp := fiber.Map{
|
||||
"sessionToken": authInfo.SessionToken.JWT,
|
||||
"sessionJwt": authInfo.SessionToken.JWT, // Frontend compatibility
|
||||
"status": "ok",
|
||||
"provider": h.IdpProvider.Name(),
|
||||
"sessionJwt": authInfo.SessionToken.JWT,
|
||||
"status": "ok",
|
||||
"provider": h.IdpProvider.Name(),
|
||||
}
|
||||
if authInfo.RefreshToken != nil {
|
||||
resp["refreshToken"] = authInfo.RefreshToken.JWT
|
||||
resp["refreshJwt"] = authInfo.RefreshToken.JWT
|
||||
}
|
||||
if authInfo.Subject != "" {
|
||||
resp["subject"] = authInfo.Subject
|
||||
@@ -2079,16 +2075,6 @@ type kratosCourierRequest struct {
|
||||
Body string `json:"body"`
|
||||
}
|
||||
|
||||
// sanitizePhoneForSms - 네이버 SMS 등 국내 발송기를 위해 +82 형식을 010 형식으로 변환합니다.
|
||||
func sanitizePhoneForSms(phone string) string {
|
||||
p := strings.ReplaceAll(phone, "-", "")
|
||||
p = strings.ReplaceAll(p, " ", "")
|
||||
if strings.HasPrefix(p, "+82") {
|
||||
return "0" + p[3:]
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
// HandleKratosCourierRelay - Kratos courier HTTP 요청을 받아 메일/SMS 발송으로 변환합니다.
|
||||
func (h *AuthHandler) HandleKratosCourierRelay(c *fiber.Ctx) error {
|
||||
var req kratosCourierRequest
|
||||
@@ -2467,6 +2453,16 @@ func extractFirstString(data map[string]interface{}, keys ...string) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func sanitizePhoneForSms(phone string) string {
|
||||
sanitized := strings.TrimSpace(phone)
|
||||
if strings.HasPrefix(sanitized, "+82") {
|
||||
sanitized = "0" + sanitized[3:]
|
||||
}
|
||||
sanitized = strings.ReplaceAll(sanitized, "-", "")
|
||||
sanitized = strings.ReplaceAll(sanitized, " ", "")
|
||||
return sanitized
|
||||
}
|
||||
|
||||
// --- User Profile Handlers ---
|
||||
|
||||
func (h *AuthHandler) formatPhoneForDisplay(phone string) string {
|
||||
@@ -2484,56 +2480,7 @@ func (h *AuthHandler) formatPhoneForStorage(phone string) string {
|
||||
return phone
|
||||
}
|
||||
|
||||
// ProxyOidc - 프론트엔드의 OIDC 요청을 내부 Hydra 서비스로 프록시합니다.
|
||||
func (h *AuthHandler) ProxyOidc(c *fiber.Ctx) error {
|
||||
path := c.Params("*")
|
||||
// [Strict] Always use internal Docker network address for proxying to avoid external loops
|
||||
targetURL := "http://hydra:4444"
|
||||
|
||||
// 프록시 URL 구성
|
||||
u, err := url.Parse(targetURL)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "invalid hydra public url")
|
||||
}
|
||||
u.Path = strings.TrimRight(u.Path, "/") + "/" + path
|
||||
u.RawQuery = string(c.Request().URI().QueryString())
|
||||
|
||||
slog.Debug("Proxying OIDC request", "from", c.Path(), "to", u.String())
|
||||
|
||||
// 요청 준비
|
||||
req, err := http.NewRequestWithContext(c.Context(), c.Method(), u.String(), bytes.NewReader(c.Body()))
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "failed to create proxy request")
|
||||
}
|
||||
|
||||
// 헤더 복사
|
||||
c.Request().Header.VisitAll(func(key, value []byte) {
|
||||
k := string(key)
|
||||
if k != "Host" && k != "Connection" {
|
||||
req.Header.Add(k, string(value))
|
||||
}
|
||||
})
|
||||
|
||||
// 요청 실행 (Hydra 내부 HttpClient 사용)
|
||||
resp, err := h.Hydra.HttpClient().Do(req)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusServiceUnavailable, "hydra public api unavailable")
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// 응답 헤더 복사
|
||||
for k, values := range resp.Header {
|
||||
for _, v := range values {
|
||||
c.Set(k, v)
|
||||
}
|
||||
}
|
||||
|
||||
// 상태 코드 및 바디 설정
|
||||
c.Status(resp.StatusCode)
|
||||
_, err = io.Copy(c.Response().BodyWriter(), resp.Body)
|
||||
return err
|
||||
}
|
||||
|
||||
// GetMe - Returns current user's profile with enriched data from local DB
|
||||
func (h *AuthHandler) GetMe(c *fiber.Ctx) error {
|
||||
profile, err := h.resolveCurrentProfile(c)
|
||||
if err != nil {
|
||||
@@ -4006,13 +3953,6 @@ func (h *AuthHandler) resolveCurrentProfile(c *fiber.Ctx) (*domain.UserProfileRe
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch Manageable Tenants for Admins
|
||||
if profile.Role == domain.RoleSuperAdmin || profile.Role == domain.RoleTenantAdmin || profile.Role == domain.RoleRPAdmin {
|
||||
if tenants, err := h.TenantService.ListManageableTenants(c.Context(), profile.ID); err == nil {
|
||||
profile.ManageableTenants = tenants
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Save to Redis Cache (Short TTL)
|
||||
if h.RedisService != nil && cacheKey != "" {
|
||||
if data, err := json.Marshal(profile); err == nil {
|
||||
@@ -4842,7 +4782,10 @@ func extractLoginIDFromClaims(claims map[string]any) string {
|
||||
}
|
||||
|
||||
func (h *AuthHandler) getKratosIdentity(sessionToken string) (string, map[string]interface{}, error) {
|
||||
kratosURL := strings.TrimRight(utils.GetEnv("KRATOS_PUBLIC_URL", "http://kratos:4433"), "/")
|
||||
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 "", nil, err
|
||||
@@ -4850,44 +4793,33 @@ func (h *AuthHandler) getKratosIdentity(sessionToken string) (string, map[string
|
||||
req.Header.Set("X-Session-Token", sessionToken)
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err == nil {
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode == http.StatusOK {
|
||||
var result struct {
|
||||
Identity struct {
|
||||
ID string `json:"id"`
|
||||
Traits map[string]interface{} `json:"traits"`
|
||||
} `json:"identity"`
|
||||
}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&result); err == nil {
|
||||
return result.Identity.ID, result.Identity.Traits, nil
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode >= 300 {
|
||||
body, _ := io.ReadAll(io.LimitReader(resp.Body, 2048))
|
||||
return "", nil, fmt.Errorf("kratos whoami failed status=%d body=%s", resp.StatusCode, string(body))
|
||||
}
|
||||
|
||||
// 2. Kratos 실패 시 Hydra Introspection 시도 (OIDC Access Token 대응)
|
||||
if h.Hydra != nil {
|
||||
slog.Debug("[Auth] Kratos whoami failed, trying Hydra introspection", "token_prefix", sessionToken[:min(len(sessionToken), 10)])
|
||||
introspection, err := h.Hydra.IntrospectToken(context.Background(), sessionToken)
|
||||
if err == nil && introspection["active"] == true {
|
||||
subject, _ := introspection["sub"].(string)
|
||||
if subject != "" {
|
||||
// Hydra는 Traits를 직접 주지 않으므로, Kratos Admin API로 상세 정보를 가져옴
|
||||
identity, err := h.KratosAdmin.GetIdentity(context.Background(), subject)
|
||||
if err == nil && identity != nil {
|
||||
return identity.ID, identity.Traits, nil
|
||||
}
|
||||
// Identity 정보가 없더라도 최소한 Subject는 반환
|
||||
return subject, map[string]interface{}{}, nil
|
||||
}
|
||||
}
|
||||
var result struct {
|
||||
Identity struct {
|
||||
ID string `json:"id"`
|
||||
Traits map[string]interface{} `json:"traits"`
|
||||
} `json:"identity"`
|
||||
}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
return "", nil, fmt.Errorf("invalid session or token")
|
||||
return result.Identity.ID, result.Identity.Traits, nil
|
||||
}
|
||||
|
||||
func (h *AuthHandler) getKratosSessionID(sessionToken string) (string, error) {
|
||||
kratosURL := strings.TrimRight(utils.GetEnv("KRATOS_PUBLIC_URL", "http://kratos:4433"), "/")
|
||||
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
|
||||
@@ -4910,7 +4842,6 @@ func (h *AuthHandler) getKratosSessionID(sessionToken string) (string, error) {
|
||||
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return result.ID, nil
|
||||
}
|
||||
|
||||
@@ -4919,7 +4850,10 @@ func (h *AuthHandler) issueKratosSession(ctx context.Context, identityID string)
|
||||
return "", fmt.Errorf("kratos identity id is empty")
|
||||
}
|
||||
|
||||
kratosAdminURL := strings.TrimRight(utils.GetEnv("KRATOS_ADMIN_URL", "http://kratos:4434"), "/")
|
||||
kratosAdminURL := strings.TrimRight(os.Getenv("KRATOS_ADMIN_URL"), "/")
|
||||
if kratosAdminURL == "" {
|
||||
kratosAdminURL = "http://kratos:4434"
|
||||
}
|
||||
|
||||
payload := map[string]interface{}{
|
||||
"identity_id": identityID,
|
||||
|
||||
Reference in New Issue
Block a user