forked from baron/baron-sso
OIDC back-channel logout 백엔드 전송 기능 추가
This commit is contained in:
@@ -85,20 +85,21 @@ const (
|
||||
)
|
||||
|
||||
type AuthHandler struct {
|
||||
SmsService domain.SmsService
|
||||
EmailService domain.EmailService
|
||||
RedisService domain.RedisRepository
|
||||
HeadlessJWKS *service.HeadlessJWKSCacheService
|
||||
KratosAdmin service.KratosAdminService
|
||||
IdpProvider domain.IdentityProvider
|
||||
AuditRepo domain.AuditRepository
|
||||
OathkeeperRepo domain.OathkeeperLogRepository
|
||||
Hydra *service.HydraAdminService
|
||||
TenantService service.TenantService
|
||||
KetoService service.KetoService
|
||||
KetoOutboxRepo repository.KetoOutboxRepository
|
||||
UserRepo repository.UserRepository
|
||||
ConsentRepo repository.ClientConsentRepository
|
||||
SmsService domain.SmsService
|
||||
EmailService domain.EmailService
|
||||
RedisService domain.RedisRepository
|
||||
HeadlessJWKS *service.HeadlessJWKSCacheService
|
||||
KratosAdmin service.KratosAdminService
|
||||
IdpProvider domain.IdentityProvider
|
||||
AuditRepo domain.AuditRepository
|
||||
OathkeeperRepo domain.OathkeeperLogRepository
|
||||
Hydra *service.HydraAdminService
|
||||
BackchannelLogout *service.BackchannelLogoutService
|
||||
TenantService service.TenantService
|
||||
KetoService service.KetoService
|
||||
KetoOutboxRepo repository.KetoOutboxRepository
|
||||
UserRepo repository.UserRepository
|
||||
ConsentRepo repository.ClientConsentRepository
|
||||
RPUserMetadataRepo repository.RPUserMetadataRepository
|
||||
}
|
||||
|
||||
@@ -221,21 +222,26 @@ func checkPollInterval(redis domain.RedisRepository, key string, interval time.D
|
||||
}
|
||||
|
||||
func NewAuthHandler(redisService domain.RedisRepository, idpProvider domain.IdentityProvider, auditRepo domain.AuditRepository, oathkeeperRepo domain.OathkeeperLogRepository, tenantService service.TenantService, ketoService service.KetoService, ketoOutboxRepo repository.KetoOutboxRepository, userRepo repository.UserRepository, consentRepo repository.ClientConsentRepository, kratos service.KratosAdminService) *AuthHandler {
|
||||
backchannelLogout, err := service.NewBackchannelLogoutService()
|
||||
if err != nil {
|
||||
slog.Warn("failed to initialize backchannel logout service", "error", err)
|
||||
}
|
||||
return &AuthHandler{
|
||||
SmsService: service.NewSmsService(),
|
||||
EmailService: service.NewEmailService(),
|
||||
RedisService: redisService,
|
||||
HeadlessJWKS: service.NewHeadlessJWKSCacheService(redisService, nil),
|
||||
KratosAdmin: kratos,
|
||||
IdpProvider: idpProvider,
|
||||
AuditRepo: auditRepo,
|
||||
OathkeeperRepo: oathkeeperRepo,
|
||||
Hydra: service.NewHydraAdminService(),
|
||||
TenantService: tenantService,
|
||||
KetoService: ketoService,
|
||||
KetoOutboxRepo: ketoOutboxRepo,
|
||||
UserRepo: userRepo,
|
||||
ConsentRepo: consentRepo,
|
||||
SmsService: service.NewSmsService(),
|
||||
EmailService: service.NewEmailService(),
|
||||
RedisService: redisService,
|
||||
HeadlessJWKS: service.NewHeadlessJWKSCacheService(redisService, nil),
|
||||
KratosAdmin: kratos,
|
||||
IdpProvider: idpProvider,
|
||||
AuditRepo: auditRepo,
|
||||
OathkeeperRepo: oathkeeperRepo,
|
||||
Hydra: service.NewHydraAdminService(),
|
||||
BackchannelLogout: backchannelLogout,
|
||||
TenantService: tenantService,
|
||||
KetoService: ketoService,
|
||||
KetoOutboxRepo: ketoOutboxRepo,
|
||||
UserRepo: userRepo,
|
||||
ConsentRepo: consentRepo,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5348,6 +5354,8 @@ func (h *AuthHandler) RevokeLinkedRp(c *fiber.Ctx) error {
|
||||
})
|
||||
}
|
||||
|
||||
h.triggerBackchannelLogoutForClient(c.Context(), c, subject, clientID, "")
|
||||
|
||||
return c.Status(fiber.StatusOK).JSON(fiber.Map{
|
||||
"status": "success",
|
||||
"message": "Link revoked successfully",
|
||||
@@ -7768,6 +7776,7 @@ func (h *AuthHandler) DeleteMySession(c *fiber.Ctx) error {
|
||||
if err := h.revokeHydraSessionAccess(c.Context(), profile.ID, targetSessionID); err != nil {
|
||||
return errorJSON(c, fiber.StatusInternalServerError, "Failed to revoke linked app sessions")
|
||||
}
|
||||
h.triggerBackchannelLogoutForSession(c.Context(), c, profile.ID, targetSessionID)
|
||||
|
||||
h.writeSessionRevokedAuditLog(c, profile.ID, h.resolveCurrentSessionID(c), targetSessionID, result)
|
||||
return c.JSON(fiber.Map{"status": "ok"})
|
||||
@@ -8187,6 +8196,129 @@ func (h *AuthHandler) revokeHydraSessionAccess(ctx context.Context, userID strin
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *AuthHandler) triggerBackchannelLogoutForSession(ctx context.Context, c *fiber.Ctx, userID string, sessionID string) {
|
||||
if h == nil || h.Hydra == nil {
|
||||
return
|
||||
}
|
||||
|
||||
clientIDs := h.loadSessionClientBindings(ctx, userID)[strings.TrimSpace(sessionID)]
|
||||
for _, clientID := range clientIDs {
|
||||
h.triggerBackchannelLogoutForClient(ctx, c, userID, clientID, sessionID)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *AuthHandler) triggerBackchannelLogoutForClient(ctx context.Context, c *fiber.Ctx, userID string, clientID string, sessionID string) {
|
||||
if h == nil || h.Hydra == nil || h.BackchannelLogout == nil {
|
||||
return
|
||||
}
|
||||
|
||||
clientID = strings.TrimSpace(clientID)
|
||||
userID = strings.TrimSpace(userID)
|
||||
sessionID = strings.TrimSpace(sessionID)
|
||||
if clientID == "" || userID == "" {
|
||||
return
|
||||
}
|
||||
|
||||
client, err := h.Hydra.GetClient(ctx, clientID)
|
||||
if err != nil {
|
||||
h.writeBackchannelLogoutAuditLog(c, "backchannel_logout.failed", userID, clientID, sessionID, "", 0, "client_lookup_failed")
|
||||
return
|
||||
}
|
||||
if client == nil {
|
||||
h.writeBackchannelLogoutAuditLog(c, "backchannel_logout.skipped", userID, clientID, sessionID, "", 0, "client_not_found")
|
||||
return
|
||||
}
|
||||
|
||||
endpoint := client.BackchannelLogoutURI()
|
||||
if endpoint == "" {
|
||||
h.writeBackchannelLogoutAuditLog(c, "backchannel_logout.skipped", userID, clientID, sessionID, "", 0, "uri_not_configured")
|
||||
return
|
||||
}
|
||||
if client.BackchannelLogoutSessionRequiredValue() && sessionID == "" {
|
||||
h.writeBackchannelLogoutAuditLog(c, "backchannel_logout.skipped", userID, clientID, sessionID, endpoint, 0, "sid_required")
|
||||
return
|
||||
}
|
||||
|
||||
logoutToken, err := h.BackchannelLogout.BuildLogoutToken(clientID, userID, sessionID)
|
||||
if err != nil {
|
||||
h.writeBackchannelLogoutAuditLog(c, "backchannel_logout.failed", userID, clientID, sessionID, endpoint, 0, "token_build_failed")
|
||||
return
|
||||
}
|
||||
|
||||
statusCode, err := h.BackchannelLogout.SendLogoutToken(ctx, endpoint, logoutToken)
|
||||
if err != nil {
|
||||
h.writeBackchannelLogoutAuditLog(c, "backchannel_logout.failed", userID, clientID, sessionID, endpoint, statusCode, "request_failed")
|
||||
return
|
||||
}
|
||||
|
||||
h.writeBackchannelLogoutAuditLog(c, "backchannel_logout.sent", userID, clientID, sessionID, endpoint, statusCode, "")
|
||||
}
|
||||
|
||||
func (h *AuthHandler) writeBackchannelLogoutAuditLog(c *fiber.Ctx, eventType string, userID string, clientID string, sessionID string, endpoint string, statusCode int, reason string) {
|
||||
if h == nil || h.AuditRepo == nil {
|
||||
return
|
||||
}
|
||||
|
||||
endpointHost := ""
|
||||
if endpoint != "" {
|
||||
if parsed, err := url.Parse(endpoint); err == nil {
|
||||
endpointHost = parsed.Host
|
||||
}
|
||||
}
|
||||
|
||||
details := map[string]any{
|
||||
"client_id": strings.TrimSpace(clientID),
|
||||
"session_id": strings.TrimSpace(sessionID),
|
||||
"endpoint_host": strings.TrimSpace(endpointHost),
|
||||
"status_code": statusCode,
|
||||
"retry_count": 0,
|
||||
"logout_issuer": h.BackchannelLogout.Issuer(),
|
||||
}
|
||||
if reason != "" {
|
||||
details["reason"] = reason
|
||||
}
|
||||
|
||||
raw, err := json.Marshal(details)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
status := "success"
|
||||
if strings.HasSuffix(eventType, ".failed") {
|
||||
status = "failure"
|
||||
} else if strings.HasSuffix(eventType, ".skipped") {
|
||||
status = "skipped"
|
||||
}
|
||||
|
||||
ipAddress := ""
|
||||
userAgent := ""
|
||||
if c != nil {
|
||||
ipAddress = extractClientIPFromHeaders(c)
|
||||
userAgent = strings.TrimSpace(c.Get("User-Agent"))
|
||||
}
|
||||
|
||||
_ = h.AuditRepo.Create(&domain.AuditLog{
|
||||
EventID: fmt.Sprintf("backchannel-logout-%d", time.Now().UnixNano()),
|
||||
Timestamp: time.Now().UTC(),
|
||||
UserID: strings.TrimSpace(userID),
|
||||
SessionID: strings.TrimSpace(sessionID),
|
||||
EventType: eventType,
|
||||
Status: status,
|
||||
IPAddress: ipAddress,
|
||||
UserAgent: userAgent,
|
||||
Details: string(raw),
|
||||
})
|
||||
}
|
||||
|
||||
func (h *AuthHandler) GetBackchannelLogoutJWKS(c *fiber.Ctx) error {
|
||||
if h == nil || h.BackchannelLogout == nil {
|
||||
return errorJSON(c, fiber.StatusServiceUnavailable, "backchannel logout jwks unavailable")
|
||||
}
|
||||
c.Set(fiber.HeaderContentType, fiber.MIMEApplicationJSONCharsetUTF8)
|
||||
c.Set(fiber.HeaderCacheControl, "no-store")
|
||||
return c.JSON(h.BackchannelLogout.PublicJWKS())
|
||||
}
|
||||
|
||||
func looksLikeInternalUserAgent(userAgent string) bool {
|
||||
normalized := strings.ToLower(strings.TrimSpace(userAgent))
|
||||
if normalized == "" {
|
||||
|
||||
Reference in New Issue
Block a user