1
0
forked from baron/baron-sso

consent 변화 내부 머지 완료

This commit is contained in:
Lectom C Han
2026-02-02 17:13:18 +09:00
parent e345570210
commit c94a369d1d
7 changed files with 482 additions and 96 deletions

View File

@@ -80,17 +80,18 @@ const (
)
type AuthHandler struct {
ProjectID string
SmsService domain.SmsService
EmailService domain.EmailService
RedisService *service.RedisService
DescopeClient *client.DescopeClient
KratosAdmin *service.KratosAdminService
IdpProvider domain.IdentityProvider
AuditRepo domain.AuditRepository
Hydra *service.HydraAdminService
TenantService service.TenantService
UserRepo repository.UserRepository
ProjectID string
SmsService domain.SmsService
EmailService domain.EmailService
RedisService *service.RedisService
DescopeClient *client.DescopeClient
KratosAdmin *service.KratosAdminService
IdpProvider domain.IdentityProvider
AuditRepo domain.AuditRepository
OathkeeperRepo domain.OathkeeperLogRepository
Hydra *service.HydraAdminService
TenantService service.TenantService
UserRepo repository.UserRepository
}
type signupState struct {
@@ -148,7 +149,7 @@ func checkPollInterval(redis *service.RedisService, key string, interval time.Du
return false, int(interval.Seconds())
}
func NewAuthHandler(redisService *service.RedisService, idpProvider domain.IdentityProvider, auditRepo domain.AuditRepository, tenantService service.TenantService, userRepo repository.UserRepository) *AuthHandler {
func NewAuthHandler(redisService *service.RedisService, idpProvider domain.IdentityProvider, auditRepo domain.AuditRepository, oathkeeperRepo domain.OathkeeperLogRepository, tenantService service.TenantService, userRepo repository.UserRepository) *AuthHandler {
projectID := os.Getenv("DESCOPE_PROJECT_ID")
managementKey := os.Getenv("DESCOPE_MANAGEMENT_KEY")
@@ -165,17 +166,18 @@ func NewAuthHandler(redisService *service.RedisService, idpProvider domain.Ident
}
return &AuthHandler{
ProjectID: projectID,
SmsService: service.NewSmsService(),
EmailService: service.NewEmailService(),
RedisService: redisService,
DescopeClient: descopeClient,
KratosAdmin: service.NewKratosAdminService(),
IdpProvider: idpProvider,
AuditRepo: auditRepo,
Hydra: service.NewHydraAdminService(),
TenantService: tenantService,
UserRepo: userRepo,
ProjectID: projectID,
SmsService: service.NewSmsService(),
EmailService: service.NewEmailService(),
RedisService: redisService,
DescopeClient: descopeClient,
KratosAdmin: service.NewKratosAdminService(),
IdpProvider: idpProvider,
AuditRepo: auditRepo,
OathkeeperRepo: oathkeeperRepo,
Hydra: service.NewHydraAdminService(),
TenantService: tenantService,
UserRepo: userRepo,
}
}
@@ -1155,8 +1157,8 @@ func (h *AuthHandler) VerifyLoginCode(c *fiber.Ctx) error {
// VerifyLoginShortCode - Verify short code (2 letters + 6 digits) and issue/approve session.
func (h *AuthHandler) VerifyLoginShortCode(c *fiber.Ctx) error {
var req struct {
ShortCode string `json:"shortCode"`
VerifyOnly bool `json:"verifyOnly,omitempty"`
ShortCode string `json:"shortCode"`
VerifyOnly bool `json:"verifyOnly,omitempty"`
}
if err := c.BodyParser(&req); err != nil {
slog.Error("[LoginShortCode] Body parse error", "error", err)
@@ -2700,8 +2702,31 @@ func extractClientIPFromHeaders(c *fiber.Ctx) string {
return c.IP()
}
type authTimelineItem struct {
EventID string `json:"event_id"`
Timestamp time.Time `json:"timestamp"`
UserID string `json:"user_id"`
SessionID string `json:"session_id,omitempty"`
EventType string `json:"event_type"`
Status string `json:"status"`
AuthMethod string `json:"auth_method,omitempty"`
IPAddress string `json:"ip_address"`
UserAgent string `json:"user_agent"`
Details string `json:"details,omitempty"`
Source string `json:"source,omitempty"`
ClientID string `json:"client_id,omitempty"`
AppName string `json:"app_name,omitempty"`
ParentSessionID string `json:"parent_session_id,omitempty"`
}
type consentClientInfo struct {
ClientID string
Name string
ConsentAt time.Time
}
func (h *AuthHandler) GetAuthTimeline(c *fiber.Ctx) error {
if h.AuditRepo == nil {
if h.AuditRepo == nil && h.OathkeeperRepo == nil {
return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{"error": "Audit service unavailable"})
}
@@ -2728,6 +2753,51 @@ func (h *AuthHandler) GetAuthTimeline(c *fiber.Ctx) error {
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "Invalid session"})
}
subject := ""
if h.OathkeeperRepo != nil {
if value, err := h.resolveConsentSubject(c); err == nil {
subject = value
}
}
consentMap := make(map[string]consentClientInfo)
if subject != "" && h.Hydra != nil {
if sessions, err := h.Hydra.ListConsentSessions(c.Context(), subject, ""); err == nil {
for _, session := range sessions {
clientID := strings.TrimSpace(session.Client.ClientID)
if clientID == "" {
continue
}
name := strings.TrimSpace(session.Client.ClientName)
if name == "" {
name = clientID
}
consentAt := time.Time{}
if session.AuthenticatedAt != nil {
consentAt = *session.AuthenticatedAt
} else if session.RequestedAt != nil {
consentAt = *session.RequestedAt
}
if existing, ok := consentMap[clientID]; ok {
if !consentAt.IsZero() && (existing.ConsentAt.IsZero() || consentAt.Before(existing.ConsentAt)) {
existing.ConsentAt = consentAt
consentMap[clientID] = existing
}
if existing.Name == "" {
existing.Name = name
consentMap[clientID] = existing
}
continue
}
consentMap[clientID] = consentClientInfo{
ClientID: clientID,
Name: name,
ConsentAt: consentAt,
}
}
}
}
candidates := buildLoginCandidates(profile)
fetchLimit := limit * 10
if fetchLimit < limit {
@@ -2737,69 +2807,177 @@ func (h *AuthHandler) GetAuthTimeline(c *fiber.Ctx) error {
fetchLimit = 500
}
items := make([]domain.AuditLog, 0, limit)
nextCursor := ""
currentCursor := cursor
const maxBatches = 10
for batch := 0; batch < maxBatches && len(items) < limit; batch++ {
logs, err := h.AuditRepo.FindPage(c.Context(), fetchLimit, currentCursor)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to retrieve audit logs"})
}
if len(logs) == 0 {
nextCursor = ""
break
}
var lastScanned *domain.AuditLog
for i := range logs {
log := logs[i]
lastScanned = &log
if !isAuthEventType(log.EventType) {
continue
authLogs := make([]domain.AuditLog, 0, fetchLimit)
if h.AuditRepo != nil {
currentCursor := cursor
const maxBatches = 10
for batch := 0; batch < maxBatches && len(authLogs) < fetchLimit; batch++ {
logs, err := h.AuditRepo.FindPage(c.Context(), fetchLimit, currentCursor)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to retrieve audit logs"})
}
if !matchesAuthTimelineUser(log, profile, candidates) {
continue
}
if shouldSkipAuthTimeline(log) {
continue
}
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 {
nextCursor = encodeAuditCursor(log)
if len(logs) == 0 {
break
}
}
if len(items) >= limit {
break
}
var lastScanned *domain.AuditLog
for i := range logs {
log := logs[i]
lastScanned = &log
if !isAuthEventType(log.EventType) {
continue
}
if !matchesAuthTimelineUser(log, profile, candidates) {
continue
}
if shouldSkipAuthTimeline(log) {
continue
}
if log.UserID == "" {
log.UserID = profile.ID
}
log.AuthMethod = deriveAuthMethod(log)
if log.AuthMethod == "" {
continue
}
if log.SessionID == "" {
log.SessionID = extractSessionIDFromAuditDetails(log.Details)
}
authLogs = append(authLogs, log)
if len(authLogs) >= fetchLimit {
break
}
}
if len(logs) < fetchLimit {
nextCursor = ""
break
if len(logs) < fetchLimit || lastScanned == nil {
break
}
currentCursor = &domain.AuditCursor{
Timestamp: lastScanned.Timestamp,
EventID: lastScanned.EventID,
}
}
}
if lastScanned == nil {
nextCursor = ""
break
}
oathkeeperLogs := make([]domain.OathkeeperAccessLog, 0, fetchLimit)
if h.OathkeeperRepo != nil && subject != "" {
currentCursor := cursor
const maxBatches = 10
for batch := 0; batch < maxBatches && len(oathkeeperLogs) < fetchLimit; batch++ {
logs, err := h.OathkeeperRepo.FindPageBySubject(c.Context(), subject, fetchLimit, currentCursor)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to retrieve oathkeeper logs"})
}
if len(logs) == 0 {
break
}
var lastScanned *domain.OathkeeperAccessLog
for i := range logs {
log := logs[i]
lastScanned = &log
clientID := extractClientIDFromOathkeeperLog(log)
if clientID == "" {
continue
}
consent, ok := consentMap[clientID]
if !ok {
continue
}
if !consent.ConsentAt.IsZero() && log.Timestamp.Before(consent.ConsentAt) {
continue
}
oathkeeperLogs = append(oathkeeperLogs, log)
if len(oathkeeperLogs) >= fetchLimit {
break
}
}
currentCursor = &domain.AuditCursor{
Timestamp: lastScanned.Timestamp,
EventID: lastScanned.EventID,
if len(logs) < fetchLimit || lastScanned == nil {
break
}
currentCursor = &domain.AuditCursor{
Timestamp: lastScanned.Timestamp,
EventID: oathkeeperEventID(*lastScanned),
}
}
nextCursor = encodeAuditCursor(*lastScanned)
}
items := make([]authTimelineItem, 0, len(authLogs)+len(oathkeeperLogs))
for i := range authLogs {
log := authLogs[i]
item := authTimelineItem{
EventID: log.EventID,
Timestamp: log.Timestamp,
UserID: log.UserID,
SessionID: log.SessionID,
EventType: log.EventType,
Status: log.Status,
AuthMethod: log.AuthMethod,
IPAddress: log.IPAddress,
UserAgent: log.UserAgent,
Details: log.Details,
Source: "backend",
AppName: "Baron 통합로그인",
}
items = append(items, item)
}
for i := range oathkeeperLogs {
log := oathkeeperLogs[i]
clientID := extractClientIDFromOathkeeperLog(log)
if clientID == "" {
continue
}
consent := consentMap[clientID]
appName := consent.Name
if appName == "" {
appName = clientID
}
details := map[string]any{
"path": log.Path,
"client_id": clientID,
"decision": log.Decision,
"status_code": log.Status,
}
detailsJSON, _ := json.Marshal(details)
status := "success"
if log.Status >= 400 {
status = "failure"
}
eventID := oathkeeperEventID(log)
item := authTimelineItem{
EventID: eventID,
Timestamp: log.Timestamp,
UserID: profile.ID,
EventType: fmt.Sprintf("%s %s", log.Method, log.Path),
Status: status,
AuthMethod: "세션 위임",
IPAddress: log.ClientIP,
UserAgent: log.UserAgent,
Details: string(detailsJSON),
Source: "oathkeeper",
ClientID: clientID,
AppName: appName,
}
items = append(items, item)
}
sort.Slice(items, func(i, j int) bool {
if items[i].Timestamp.Equal(items[j].Timestamp) {
return items[i].EventID > items[j].EventID
}
return items[i].Timestamp.After(items[j].Timestamp)
})
nextCursor := ""
hasMore := len(authLogs) >= fetchLimit || len(oathkeeperLogs) >= fetchLimit
if len(items) > limit {
items = items[:limit]
last := items[len(items)-1]
nextCursor = encodeTimelineCursor(last.Timestamp, last.EventID)
} else if hasMore && len(items) > 0 {
last := items[len(items)-1]
nextCursor = encodeTimelineCursor(last.Timestamp, last.EventID)
}
return c.JSON(fiber.Map{
@@ -2810,6 +2988,27 @@ func (h *AuthHandler) GetAuthTimeline(c *fiber.Ctx) error {
})
}
func encodeTimelineCursor(timestamp time.Time, eventID string) string {
if eventID == "" {
eventID = fmt.Sprintf("%d", timestamp.UnixNano())
}
payload := timestamp.UTC().Format(time.RFC3339Nano) + "|" + eventID
return base64.RawURLEncoding.EncodeToString([]byte(payload))
}
func oathkeeperEventID(log domain.OathkeeperAccessLog) string {
if log.RequestID != "" {
return log.RequestID
}
if log.TraceID != "" {
return log.TraceID
}
if log.SpanID != "" {
return log.SpanID
}
return fmt.Sprintf("%d", log.Timestamp.UnixNano())
}
type linkedRpSummary struct {
ID string `json:"id"`
Name string `json:"name"`
@@ -3365,6 +3564,80 @@ func extractLoginIDFromAuditDetails(details string) string {
return ""
}
func extractClientIDFromOathkeeperLog(log domain.OathkeeperAccessLog) string {
if value := strings.TrimSpace(log.RP); value != "" {
return value
}
if value := parseClientIDFromURL(log.Target); value != "" {
return value
}
if value := parseClientIDFromURL(log.Path); value != "" {
return value
}
return parseClientIDFromRaw(log.Raw)
}
func parseClientIDFromURL(raw string) string {
raw = strings.TrimSpace(raw)
if raw == "" {
return ""
}
parsed, err := url.Parse(raw)
if err != nil {
return ""
}
if id := strings.TrimSpace(parsed.Query().Get("client_id")); id != "" {
return id
}
if id := strings.TrimSpace(parsed.Query().Get("clientId")); id != "" {
return id
}
return ""
}
func parseClientIDFromRaw(raw string) string {
raw = strings.TrimSpace(raw)
if raw == "" {
return ""
}
var payload map[string]any
if err := json.Unmarshal([]byte(raw), &payload); err != nil {
return ""
}
if id := readClientIDFromPayload(payload); id != "" {
return id
}
if request, ok := payload["request"].(map[string]any); ok {
if id := readClientIDFromPayload(request); id != "" {
return id
}
if urlRaw, ok := request["url"].(string); ok {
if id := parseClientIDFromURL(urlRaw); id != "" {
return id
}
}
if pathRaw, ok := request["path"].(string); ok {
if id := parseClientIDFromURL(pathRaw); id != "" {
return id
}
}
}
return ""
}
func readClientIDFromPayload(payload map[string]any) string {
if payload == nil {
return ""
}
if raw, ok := payload["client_id"].(string); ok && strings.TrimSpace(raw) != "" {
return strings.TrimSpace(raw)
}
if raw, ok := payload["clientId"].(string); ok && strings.TrimSpace(raw) != "" {
return strings.TrimSpace(raw)
}
return ""
}
func extractSessionIDFromAuditDetails(details string) string {
if details == "" {
return ""