Files
zitadel/internal/notification/handlers/back_channel_logout.go
T
Livio SpringandGitHub ce30a5a98e feat(oidc): move back-channel logout from beta to GA (#11493)
# Which Problems Are Solved

This PR marks the OIDC Back-Channel Logout as general available (GA).

# How the Problems Are Solved

- API:
- deprecated the feature toggle (to prevent breaking changes) in v2beta
and v2 API
  - removed all related event logic and updated the projection iteration
  - removed the toggle usage for back-channel logout
  - removed related translations
- Management Console:
  - removed the feature toggle and its translations
- Docs:
  - removed all beta labels and feature toggle notes

# Additional Changes

none

# Additional Context

- closes #11277 
- requires backport to v4.x
2026-02-05 10:51:09 +00:00

173 lines
4.9 KiB
Go

package handlers
import (
"context"
"slices"
http_util "github.com/zitadel/zitadel/internal/api/http"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/eventstore/handler/v2"
"github.com/zitadel/zitadel/internal/notification/backchannel"
_ "github.com/zitadel/zitadel/internal/notification/statik"
"github.com/zitadel/zitadel/internal/queue"
"github.com/zitadel/zitadel/internal/repository/session"
"github.com/zitadel/zitadel/internal/repository/sessionlogout"
"github.com/zitadel/zitadel/internal/repository/user"
"github.com/zitadel/zitadel/internal/zerrors"
)
const (
BackChannelLogoutNotificationsProjectionTable = "projections.notifications_back_channel_logout"
)
type backChannelLogoutNotifier struct {
queries *NotificationQueries
queue Queue
maxAttempts uint8
}
func NewBackChannelLogoutNotifier(
ctx context.Context,
config handler.Config,
queries *NotificationQueries,
queue Queue,
maxAttempts uint8,
) *handler.Handler {
return handler.NewHandler(ctx, &config, &backChannelLogoutNotifier{
queries: queries,
queue: queue,
maxAttempts: maxAttempts,
})
}
func (*backChannelLogoutNotifier) Name() string {
return BackChannelLogoutNotificationsProjectionTable
}
func (u *backChannelLogoutNotifier) Reducers() []handler.AggregateReducer {
return []handler.AggregateReducer{
{
Aggregate: session.AggregateType,
EventReducers: []handler.EventReducer{
{
Event: session.TerminateType,
Reduce: u.reduceSessionTerminated,
},
},
}, {
Aggregate: user.AggregateType,
EventReducers: []handler.EventReducer{
{
Event: user.HumanSignedOutType,
Reduce: u.reduceUserSignedOut,
},
},
},
}
}
func (u *backChannelLogoutNotifier) reduceUserSignedOut(event eventstore.Event) (*handler.Statement, error) {
e, ok := event.(*user.HumanSignedOutEvent)
if !ok {
return nil, zerrors.ThrowInvalidArgumentf(nil, "HANDL-Gr63h", "reduce.wrong.event.type %s", user.HumanSignedOutType)
}
return handler.NewStatement(event, func(ctx context.Context, ex handler.Executer, projectionName string) error {
if e.SessionID == "" {
return nil
}
ctx, err := u.queries.Origin(ctx, e)
if err != nil {
return err
}
return u.queue.Insert(ctx,
&backchannel.LogoutRequest{
Aggregate: e.Aggregate(),
SessionID: e.SessionID,
TriggeredAtOrigin: http_util.DomainContext(ctx).Origin(),
TriggeringEventType: event.Type(),
},
queue.WithQueueName(backchannel.QueueName),
queue.WithMaxAttempts(u.maxAttempts),
)
}), nil
}
func (u *backChannelLogoutNotifier) reduceSessionTerminated(event eventstore.Event) (*handler.Statement, error) {
e, ok := event.(*session.TerminateEvent)
if !ok {
return nil, zerrors.ThrowInvalidArgumentf(nil, "HANDL-D6H2h", "reduce.wrong.event.type %s", session.TerminateType)
}
return handler.NewStatement(event, func(ctx context.Context, ex handler.Executer, projectionName string) error {
ctx, err := u.queries.Origin(ctx, e)
if err != nil {
return err
}
return u.queue.Insert(ctx,
&backchannel.LogoutRequest{
Aggregate: e.Aggregate(),
SessionID: e.Aggregate().ID,
TriggeredAtOrigin: http_util.DomainContext(ctx).Origin(),
TriggeringEventType: event.Type(),
},
queue.WithQueueName(backchannel.QueueName),
queue.WithMaxAttempts(u.maxAttempts),
)
}), nil
}
type backChannelLogoutSession struct {
sessionID string
// sessions contain a map of oidc session IDs and their corresponding clientID
sessions []backChannelLogoutOIDCSessions
}
type LogoutTokenMessage struct {
LogoutToken string `schema:"logout_token"`
}
type backChannelLogoutOIDCSessions struct {
SessionID string
OIDCSessionID string
UserID string
ClientID string
BackChannelLogoutURI string
}
func (b *backChannelLogoutSession) Reduce() error {
return nil
}
func (b *backChannelLogoutSession) AppendEvents(events ...eventstore.Event) {
for _, event := range events {
switch e := event.(type) {
case *sessionlogout.BackChannelLogoutRegisteredEvent:
b.sessions = append(b.sessions, backChannelLogoutOIDCSessions{
SessionID: b.sessionID,
OIDCSessionID: e.OIDCSessionID,
UserID: e.UserID,
ClientID: e.ClientID,
BackChannelLogoutURI: e.BackChannelLogoutURI,
})
case *sessionlogout.BackChannelLogoutSentEvent:
b.sessions = slices.DeleteFunc(b.sessions, func(session backChannelLogoutOIDCSessions) bool {
return session.OIDCSessionID == e.OIDCSessionID
})
}
}
}
func (b *backChannelLogoutSession) Query() *eventstore.SearchQueryBuilder {
return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent).
AddQuery().
AggregateTypes(sessionlogout.AggregateType).
AggregateIDs(b.sessionID).
EventTypes(
sessionlogout.BackChannelLogoutRegisteredType,
sessionlogout.BackChannelLogoutSentType).
Builder()
}