diff --git a/cmd/defaults.yaml b/cmd/defaults.yaml
index 397e9af376..7bb44b743f 100644
--- a/cmd/defaults.yaml
+++ b/cmd/defaults.yaml
@@ -312,6 +312,16 @@ Caches:
AddSource: true
Formatter:
Format: text
+ # Federated logouts store the information needed to handle federated logout and their state transfer
+ FederatedLogouts:
+ Connector: "postgres"
+ MaxAge: 1h
+ LastUseAge: 10m
+ Log:
+ Level: error
+ AddSource: true
+ Formatter:
+ Format: text
Machine:
# Cloud-hosted VMs need to specify their metadata endpoint so that the machine can be uniquely identified.
diff --git a/cmd/setup/56.go b/cmd/setup/56.go
new file mode 100644
index 0000000000..72ccb5e6ff
--- /dev/null
+++ b/cmd/setup/56.go
@@ -0,0 +1,27 @@
+package setup
+
+import (
+ "context"
+ _ "embed"
+
+ "github.com/zitadel/zitadel/internal/database"
+ "github.com/zitadel/zitadel/internal/eventstore"
+)
+
+var (
+ //go:embed 56.sql
+ addSAMLFederatedLogout string
+)
+
+type IDPTemplate6SAMLFederatedLogout struct {
+ dbClient *database.DB
+}
+
+func (mig *IDPTemplate6SAMLFederatedLogout) Execute(ctx context.Context, _ eventstore.Event) error {
+ _, err := mig.dbClient.ExecContext(ctx, addSAMLFederatedLogout)
+ return err
+}
+
+func (mig *IDPTemplate6SAMLFederatedLogout) String() string {
+ return "56_idp_templates6_add_saml_federated_logout"
+}
diff --git a/cmd/setup/56.sql b/cmd/setup/56.sql
new file mode 100644
index 0000000000..f5544eddf5
--- /dev/null
+++ b/cmd/setup/56.sql
@@ -0,0 +1 @@
+ALTER TABLE IF EXISTS projections.idp_templates6_saml ADD COLUMN IF NOT EXISTS federated_logout_enabled BOOLEAN DEFAULT FALSE;
diff --git a/cmd/setup/config.go b/cmd/setup/config.go
index 5e5c842b14..bd2abde9ea 100644
--- a/cmd/setup/config.go
+++ b/cmd/setup/config.go
@@ -152,6 +152,7 @@ type Steps struct {
s53InitPermittedOrgsFunction *InitPermittedOrgsFunction53
s54InstancePositionIndex *InstancePositionIndex
s55ExecutionHandlerStart *ExecutionHandlerStart
+ s56IDPTemplate6SAMLFederatedLogout *IDPTemplate6SAMLFederatedLogout
}
func MustNewSteps(v *viper.Viper) *Steps {
diff --git a/cmd/setup/setup.go b/cmd/setup/setup.go
index 58bc89d2e4..c84976f282 100644
--- a/cmd/setup/setup.go
+++ b/cmd/setup/setup.go
@@ -214,6 +214,7 @@ func Setup(ctx context.Context, config *Config, steps *Steps, masterKey string)
steps.s53InitPermittedOrgsFunction = &InitPermittedOrgsFunction53{dbClient: dbClient}
steps.s54InstancePositionIndex = &InstancePositionIndex{dbClient: dbClient}
steps.s55ExecutionHandlerStart = &ExecutionHandlerStart{dbClient: dbClient}
+ steps.s56IDPTemplate6SAMLFederatedLogout = &IDPTemplate6SAMLFederatedLogout{dbClient: dbClient}
err = projection.Create(ctx, dbClient, eventstoreClient, config.Projections, nil, nil, nil)
logging.OnError(err).Fatal("unable to start projections")
@@ -258,6 +259,7 @@ func Setup(ctx context.Context, config *Config, steps *Steps, masterKey string)
steps.s53InitPermittedOrgsFunction,
steps.s54InstancePositionIndex,
steps.s55ExecutionHandlerStart,
+ steps.s56IDPTemplate6SAMLFederatedLogout,
} {
setupErr = executeMigration(ctx, eventstoreClient, step, "migration failed")
if setupErr != nil {
diff --git a/cmd/start/start.go b/cmd/start/start.go
index 1d83197062..af76b29e99 100644
--- a/cmd/start/start.go
+++ b/cmd/start/start.go
@@ -74,12 +74,14 @@ import (
"github.com/zitadel/zitadel/internal/authz"
authz_repo "github.com/zitadel/zitadel/internal/authz/repository"
authz_es "github.com/zitadel/zitadel/internal/authz/repository/eventsourcing/eventstore"
+ "github.com/zitadel/zitadel/internal/cache"
"github.com/zitadel/zitadel/internal/cache/connector"
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/crypto"
cryptoDB "github.com/zitadel/zitadel/internal/crypto/database"
"github.com/zitadel/zitadel/internal/database"
"github.com/zitadel/zitadel/internal/domain"
+ "github.com/zitadel/zitadel/internal/domain/federatedlogout"
"github.com/zitadel/zitadel/internal/eventstore"
old_es "github.com/zitadel/zitadel/internal/eventstore/repository/sql"
new_es "github.com/zitadel/zitadel/internal/eventstore/v3"
@@ -511,7 +513,12 @@ func startAPIs(
assetsCache := middleware.AssetsCacheInterceptor(config.AssetStorage.Cache.MaxAge, config.AssetStorage.Cache.SharedMaxAge)
apis.RegisterHandlerOnPrefix(assets.HandlerPrefix, assets.NewHandler(commands, verifier, config.SystemAuthZ, config.InternalAuthZ, id.SonyFlakeGenerator(), store, queries, middleware.CallDurationHandler, instanceInterceptor.Handler, assetsCache.Handler, limitingAccessInterceptor.Handle))
- apis.RegisterHandlerOnPrefix(idp.HandlerPrefix, idp.NewHandler(commands, queries, keys.IDPConfig, instanceInterceptor.Handler))
+ federatedLogoutsCache, err := connector.StartCache[federatedlogout.Index, string, *federatedlogout.FederatedLogout](ctx, []federatedlogout.Index{federatedlogout.IndexRequestID}, cache.PurposeFederatedLogout, cacheConnectors.Config.FederatedLogouts, cacheConnectors)
+ if err != nil {
+ return nil, err
+ }
+
+ apis.RegisterHandlerOnPrefix(idp.HandlerPrefix, idp.NewHandler(commands, queries, keys.IDPConfig, instanceInterceptor.Handler, federatedLogoutsCache))
userAgentInterceptor, err := middleware.NewUserAgentHandler(config.UserAgentCookie, keys.UserAgentCookieKey, id.SonyFlakeGenerator(), config.ExternalSecure, login.EndpointResources, login.EndpointExternalLoginCallbackFormPost, login.EndpointSAMLACS)
if err != nil {
@@ -532,7 +539,25 @@ func startAPIs(
}
apis.RegisterHandlerOnPrefix(openapi.HandlerPrefix, openAPIHandler)
- oidcServer, err := oidc.NewServer(ctx, config.OIDC, login.DefaultLoggedOutPath, config.ExternalSecure, commands, queries, authRepo, keys.OIDC, keys.OIDCKey, eventstore, dbClient, userAgentInterceptor, instanceInterceptor.Handler, limitingAccessInterceptor, config.Log.Slog(), config.SystemDefaults.SecretHasher)
+ oidcServer, err := oidc.NewServer(
+ ctx,
+ config.OIDC,
+ login.DefaultLoggedOutPath,
+ config.ExternalSecure,
+ commands,
+ queries,
+ authRepo,
+ keys.OIDC,
+ keys.OIDCKey,
+ eventstore,
+ dbClient,
+ userAgentInterceptor,
+ instanceInterceptor.Handler,
+ limitingAccessInterceptor,
+ config.Log.Slog(),
+ config.SystemDefaults.SecretHasher,
+ federatedLogoutsCache,
+ )
if err != nil {
return nil, fmt.Errorf("unable to start oidc provider: %w", err)
}
@@ -581,6 +606,7 @@ func startAPIs(
keys.IDPConfig,
keys.CSRFCookieKey,
cacheConnectors,
+ federatedLogoutsCache,
)
if err != nil {
return nil, fmt.Errorf("unable to start login: %w", err)
diff --git a/console/src/app/modules/providers/provider-oidc/provider-oidc.component.html b/console/src/app/modules/providers/provider-oidc/provider-oidc.component.html
index e326d3be26..c7bb48ea71 100644
--- a/console/src/app/modules/providers/provider-oidc/provider-oidc.component.html
+++ b/console/src/app/modules/providers/provider-oidc/provider-oidc.component.html
@@ -98,13 +98,12 @@
{{ 'IDP.ISIDTOKENMAPPING_DESC' | translate }}
{{ 'IDP.ISIDTOKENMAPPING' | translate }}
-
-
-
-
-
{{ 'IDP.USEPKCE_DESC' | translate }}
-
{{ 'IDP.USEPKCE' | translate }}
-
+
+
+
{{ 'IDP.USEPKCE_DESC' | translate }}
+
{{ 'IDP.USEPKCE' | translate }}
+
+
diff --git a/console/src/app/modules/providers/provider-saml-sp/provider-saml-sp.component.html b/console/src/app/modules/providers/provider-saml-sp/provider-saml-sp.component.html
index d738b3fec9..245a963fa8 100644
--- a/console/src/app/modules/providers/provider-saml-sp/provider-saml-sp.component.html
+++ b/console/src/app/modules/providers/provider-saml-sp/provider-saml-sp.component.html
@@ -82,7 +82,7 @@
-
{{ 'IDP.SAML.TRANSIENTMAPPINGATTRIBUTENAME_DESC' | translate }}
+
{{ 'IDP.SAML.TRANSIENTMAPPINGATTRIBUTENAME_DESC' | translate }}
@@ -90,6 +90,15 @@
+
+
+
+
{{ 'IDP.FEDERATEDLOGOUTENABLED_DESC' | translate }}
+
{{
+ 'IDP.FEDERATEDLOGOUTENABLED' | translate
+ }}
+
+
{{.Form}}`))
+)
+
+type samlSLOPostData struct {
+ Form template.HTML
+}
+
+func samlPostLogoutRequest(w http.ResponseWriter, sp crewjam_saml.ServiceProvider, slo, nameID, sessionID string) error {
+ lr, err := sp.MakeLogoutRequest(slo, nameID)
+ if err != nil {
+ return err
+ }
+
+ return samlSLOPostTemplate.Execute(w, &samlSLOPostData{Form: template.HTML(lr.Post(sessionID))})
+}
+
+func (l *Login) externalUserID(ctx context.Context, userID, idpID string) (string, error) {
+ userIDQuery, err := query.NewIDPUserLinksUserIDSearchQuery(userID)
+ if err != nil {
+ return "", err
+ }
+ idpIDQuery, err := query.NewIDPUserLinkIDPIDSearchQuery(idpID)
+ if err != nil {
+ return "", err
+ }
+ links, err := l.query.IDPUserLinks(ctx, &query.IDPUserLinksSearchQuery{Queries: []query.SearchQuery{userIDQuery, idpIDQuery}}, nil)
+ if err != nil || len(links.Links) != 1 {
+ return "", zerrors.ThrowPreconditionFailed(err, "LOGIN-ADK21", "Errors.User.ExternalIDP.NotFound")
+ }
+ return links.Links[0].ProvidedUserID, nil
+}
+
// IdPError wraps an error from an external IDP to be able to distinguish it from other errors and to display it
// more prominent (popup style) .
// It's used if an error occurs during the login process with an external IDP and local authentication is allowed,
diff --git a/internal/api/ui/login/login.go b/internal/api/ui/login/login.go
index 444c5aaa85..f1ce9bfa2a 100644
--- a/internal/api/ui/login/login.go
+++ b/internal/api/ui/login/login.go
@@ -21,6 +21,7 @@ import (
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/domain"
+ "github.com/zitadel/zitadel/internal/domain/federatedlogout"
"github.com/zitadel/zitadel/internal/form"
"github.com/zitadel/zitadel/internal/query"
"github.com/zitadel/zitadel/internal/static"
@@ -60,25 +61,20 @@ const (
DefaultLoggedOutPath = HandlerPrefix + EndpointLogoutDone
)
-func CreateLogin(config Config,
+func CreateLogin(
+ config Config,
command *command.Commands,
query *query.Queries,
authRepo *eventsourcing.EsRepository,
staticStorage static.Storage,
consolePath string,
- oidcAuthCallbackURL func(context.Context, string) string,
- samlAuthCallbackURL func(context.Context, string) string,
+ oidcAuthCallbackURL, samlAuthCallbackURL func(context.Context, string) string,
externalSecure bool,
- userAgentCookie,
- issuerInterceptor,
- oidcInstanceHandler,
- samlInstanceHandler,
- assetCache,
- accessHandler mux.MiddlewareFunc,
- userCodeAlg crypto.EncryptionAlgorithm,
- idpConfigAlg crypto.EncryptionAlgorithm,
+ userAgentCookie, issuerInterceptor, oidcInstanceHandler, samlInstanceHandler, assetCache, accessHandler mux.MiddlewareFunc,
+ userCodeAlg, idpConfigAlg crypto.EncryptionAlgorithm,
csrfCookieKey []byte,
cacheConnectors connector.Connectors,
+ federateLogoutCache cache.Cache[federatedlogout.Index, string, *federatedlogout.FederatedLogout],
) (*Login, error) {
login := &Login{
oidcAuthCallbackURL: oidcAuthCallbackURL,
@@ -101,7 +97,7 @@ func CreateLogin(config Config,
login.parser = form.NewParser()
var err error
- login.caches, err = startCaches(context.Background(), cacheConnectors)
+ login.caches, err = startCaches(context.Background(), cacheConnectors, federateLogoutCache)
if err != nil {
return nil, err
}
@@ -112,7 +108,11 @@ func csp() *middleware.CSP {
csp := middleware.DefaultSCP
csp.ObjectSrc = middleware.CSPSourceOptsSelf()
csp.StyleSrc = csp.StyleSrc.AddNonce()
- csp.ScriptSrc = csp.ScriptSrc.AddNonce().AddHash("sha256", "AjPdJSbZmeWHnEc5ykvJFay8FTWeTeRbs9dutfZ0HqE=")
+ csp.ScriptSrc = csp.ScriptSrc.AddNonce().
+ // SAML POST ACS
+ AddHash("sha256", "AjPdJSbZmeWHnEc5ykvJFay8FTWeTeRbs9dutfZ0HqE=").
+ // SAML POST SLO
+ AddHash("sha256", "4Su6mBWzEIFnH4pAGMOuaeBrstwJN4Z3pq/s1Kn4/KQ=")
return &csp
}
@@ -215,14 +215,16 @@ func (l *Login) baseURL(ctx context.Context) string {
type Caches struct {
idpFormCallbacks cache.Cache[idpFormCallbackIndex, string, *idpFormCallback]
+ federatedLogouts cache.Cache[federatedlogout.Index, string, *federatedlogout.FederatedLogout]
}
-func startCaches(background context.Context, connectors connector.Connectors) (_ *Caches, err error) {
+func startCaches(background context.Context, connectors connector.Connectors, federateLogoutCache cache.Cache[federatedlogout.Index, string, *federatedlogout.FederatedLogout]) (_ *Caches, err error) {
caches := new(Caches)
caches.idpFormCallbacks, err = connector.StartCache[idpFormCallbackIndex, string, *idpFormCallback](background, []idpFormCallbackIndex{idpFormCallbackIndexRequestID}, cache.PurposeIdPFormCallback, connectors.Config.IdPFormCallbacks, connectors)
if err != nil {
return nil, err
}
+ caches.federatedLogouts = federateLogoutCache
return caches, nil
}
diff --git a/internal/api/ui/login/router.go b/internal/api/ui/login/router.go
index 6e346c9da0..459a0aab5d 100644
--- a/internal/api/ui/login/router.go
+++ b/internal/api/ui/login/router.go
@@ -14,6 +14,7 @@ const (
EndpointExternalLogin = "/login/externalidp"
EndpointExternalLoginCallback = "/login/externalidp/callback"
EndpointExternalLoginCallbackFormPost = "/login/externalidp/callback/form"
+ EndpointExternalLogout = "/logout/externalidp"
EndpointSAMLACS = "/login/externalidp/saml/acs"
EndpointJWTAuthorize = "/login/jwt/authorize"
EndpointJWTCallback = "/login/jwt/callback"
@@ -77,6 +78,7 @@ func CreateRouter(login *Login, interceptors ...mux.MiddlewareFunc) *mux.Router
router.HandleFunc(EndpointExternalLogin, login.handleExternalLogin).Methods(http.MethodGet)
router.HandleFunc(EndpointExternalLoginCallback, login.handleExternalLoginCallback).Methods(http.MethodGet)
router.HandleFunc(EndpointExternalLoginCallbackFormPost, login.handleExternalLoginCallbackForm).Methods(http.MethodPost)
+ router.HandleFunc(EndpointExternalLogout, login.handleExternalLogout).Methods(http.MethodGet)
router.HandleFunc(EndpointSAMLACS, login.handleExternalLoginCallback).Methods(http.MethodGet)
router.HandleFunc(EndpointSAMLACS, login.handleExternalLoginCallbackForm).Methods(http.MethodPost)
router.HandleFunc(EndpointJWTAuthorize, login.handleJWTRequest).Methods(http.MethodGet)
diff --git a/internal/auth/repository/eventsourcing/eventstore/user.go b/internal/auth/repository/eventsourcing/eventstore/user.go
index 61895c263d..6c5375a6b9 100644
--- a/internal/auth/repository/eventsourcing/eventstore/user.go
+++ b/internal/auth/repository/eventsourcing/eventstore/user.go
@@ -13,6 +13,7 @@ import (
"github.com/zitadel/zitadel/internal/query"
"github.com/zitadel/zitadel/internal/repository/user"
usr_view "github.com/zitadel/zitadel/internal/user/repository/view"
+ "github.com/zitadel/zitadel/internal/user/repository/view/model"
"github.com/zitadel/zitadel/internal/zerrors"
)
@@ -49,6 +50,10 @@ func (repo *UserRepo) UserAgentIDBySessionID(ctx context.Context, sessionID stri
return repo.View.UserAgentIDBySessionID(ctx, sessionID, authz.GetInstance(ctx).InstanceID())
}
+func (repo *UserRepo) UserSessionByID(ctx context.Context, sessionID string) (*model.UserSessionView, error) {
+ return repo.View.UserSessionByID(ctx, sessionID, authz.GetInstance(ctx).InstanceID())
+}
+
func (repo *UserRepo) ActiveUserSessionsBySessionID(ctx context.Context, sessionID string) (userAgentID string, signoutSessions []command.HumanSignOutSession, err error) {
userAgentID, sessions, err := repo.View.ActiveUserSessionsBySessionID(ctx, sessionID, authz.GetInstance(ctx).InstanceID())
if err != nil {
diff --git a/internal/auth/repository/eventsourcing/view/user_session.go b/internal/auth/repository/eventsourcing/view/user_session.go
index a4618e11fb..777bc3213f 100644
--- a/internal/auth/repository/eventsourcing/view/user_session.go
+++ b/internal/auth/repository/eventsourcing/view/user_session.go
@@ -16,6 +16,10 @@ func (v *View) UserSessionByIDs(ctx context.Context, agentID, userID, instanceID
return view.UserSessionByIDs(ctx, v.client, agentID, userID, instanceID)
}
+func (v *View) UserSessionByID(ctx context.Context, userSessionID, instanceID string) (*model.UserSessionView, error) {
+ return view.UserSessionByID(ctx, v.client, userSessionID, instanceID)
+}
+
func (v *View) UserSessionsByAgentID(ctx context.Context, agentID, instanceID string) ([]*model.UserSessionView, error) {
return view.UserSessionsByAgentID(ctx, v.client, agentID, instanceID)
}
diff --git a/internal/auth/repository/user.go b/internal/auth/repository/user.go
index f09581b32e..b51ca27f24 100644
--- a/internal/auth/repository/user.go
+++ b/internal/auth/repository/user.go
@@ -4,10 +4,12 @@ import (
"context"
"github.com/zitadel/zitadel/internal/command"
+ "github.com/zitadel/zitadel/internal/user/repository/view/model"
)
type UserRepository interface {
UserSessionsByAgentID(ctx context.Context, agentID string) (sessions []command.HumanSignOutSession, err error)
UserAgentIDBySessionID(ctx context.Context, sessionID string) (string, error)
+ UserSessionByID(ctx context.Context, sessionID string) (*model.UserSessionView, error)
ActiveUserSessionsBySessionID(ctx context.Context, sessionID string) (userAgentID string, sessions []command.HumanSignOutSession, err error)
}
diff --git a/internal/cache/cache.go b/internal/cache/cache.go
index dc05208caa..233308a3cb 100644
--- a/internal/cache/cache.go
+++ b/internal/cache/cache.go
@@ -18,6 +18,7 @@ const (
PurposeMilestones
PurposeOrganization
PurposeIdPFormCallback
+ PurposeFederatedLogout
)
// Cache stores objects with a value of type `V`.
diff --git a/internal/cache/connector/connector.go b/internal/cache/connector/connector.go
index 1a0534759a..532e795a81 100644
--- a/internal/cache/connector/connector.go
+++ b/internal/cache/connector/connector.go
@@ -23,6 +23,7 @@ type CachesConfig struct {
Milestones *cache.Config
Organization *cache.Config
IdPFormCallbacks *cache.Config
+ FederatedLogouts *cache.Config
}
type Connectors struct {
diff --git a/internal/cache/purpose_enumer.go b/internal/cache/purpose_enumer.go
index a93a978efb..f721435593 100644
--- a/internal/cache/purpose_enumer.go
+++ b/internal/cache/purpose_enumer.go
@@ -7,11 +7,11 @@ import (
"strings"
)
-const _PurposeName = "unspecifiedauthz_instancemilestonesorganizationid_p_form_callback"
+const _PurposeName = "unspecifiedauthz_instancemilestonesorganizationid_p_form_callbackfederated_logout"
-var _PurposeIndex = [...]uint8{0, 11, 25, 35, 47, 65}
+var _PurposeIndex = [...]uint8{0, 11, 25, 35, 47, 65, 81}
-const _PurposeLowerName = "unspecifiedauthz_instancemilestonesorganizationid_p_form_callback"
+const _PurposeLowerName = "unspecifiedauthz_instancemilestonesorganizationid_p_form_callbackfederated_logout"
func (i Purpose) String() string {
if i < 0 || i >= Purpose(len(_PurposeIndex)-1) {
@@ -29,9 +29,10 @@ func _PurposeNoOp() {
_ = x[PurposeMilestones-(2)]
_ = x[PurposeOrganization-(3)]
_ = x[PurposeIdPFormCallback-(4)]
+ _ = x[PurposeFederatedLogout-(5)]
}
-var _PurposeValues = []Purpose{PurposeUnspecified, PurposeAuthzInstance, PurposeMilestones, PurposeOrganization, PurposeIdPFormCallback}
+var _PurposeValues = []Purpose{PurposeUnspecified, PurposeAuthzInstance, PurposeMilestones, PurposeOrganization, PurposeIdPFormCallback, PurposeFederatedLogout}
var _PurposeNameToValueMap = map[string]Purpose{
_PurposeName[0:11]: PurposeUnspecified,
@@ -44,6 +45,8 @@ var _PurposeNameToValueMap = map[string]Purpose{
_PurposeLowerName[35:47]: PurposeOrganization,
_PurposeName[47:65]: PurposeIdPFormCallback,
_PurposeLowerName[47:65]: PurposeIdPFormCallback,
+ _PurposeName[65:81]: PurposeFederatedLogout,
+ _PurposeLowerName[65:81]: PurposeFederatedLogout,
}
var _PurposeNames = []string{
@@ -52,6 +55,7 @@ var _PurposeNames = []string{
_PurposeName[25:35],
_PurposeName[35:47],
_PurposeName[47:65],
+ _PurposeName[65:81],
}
// PurposeString retrieves an enum value from the enum constants string name.
diff --git a/internal/command/idp.go b/internal/command/idp.go
index 821a577900..06d8f473c0 100644
--- a/internal/command/idp.go
+++ b/internal/command/idp.go
@@ -122,6 +122,7 @@ type SAMLProvider struct {
WithSignedRequest bool
NameIDFormat *domain.SAMLNameIDFormat
TransientMappingAttributeName string
+ FederatedLogoutEnabled bool
IDPOptions idp.Options
}
diff --git a/internal/command/idp_intent_test.go b/internal/command/idp_intent_test.go
index 1be3971e87..6cf835f521 100644
--- a/internal/command/idp_intent_test.go
+++ b/internal/command/idp_intent_test.go
@@ -743,6 +743,7 @@ func TestCommands_AuthFromProvider_SAML(t *testing.T) {
false,
gu.Ptr(domain.SAMLNameIDFormatUnspecified),
"",
+ false,
rep_idp.Options{},
)),
),
@@ -763,6 +764,7 @@ func TestCommands_AuthFromProvider_SAML(t *testing.T) {
false,
gu.Ptr(domain.SAMLNameIDFormatUnspecified),
"",
+ false,
rep_idp.Options{},
)),
),
diff --git a/internal/command/idp_model.go b/internal/command/idp_model.go
index 5257d38bf4..188d45e6ec 100644
--- a/internal/command/idp_model.go
+++ b/internal/command/idp_model.go
@@ -1760,6 +1760,7 @@ type SAMLIDPWriteModel struct {
WithSignedRequest bool
NameIDFormat *domain.SAMLNameIDFormat
TransientMappingAttributeName string
+ FederatedLogoutEnabled bool
idp.Options
State domain.IDPState
@@ -1788,6 +1789,7 @@ func (wm *SAMLIDPWriteModel) reduceAddedEvent(e *idp.SAMLIDPAddedEvent) {
wm.WithSignedRequest = e.WithSignedRequest
wm.NameIDFormat = e.NameIDFormat
wm.TransientMappingAttributeName = e.TransientMappingAttributeName
+ wm.FederatedLogoutEnabled = e.FederatedLogoutEnabled
wm.Options = e.Options
wm.State = domain.IDPStateActive
}
@@ -1817,6 +1819,9 @@ func (wm *SAMLIDPWriteModel) reduceChangedEvent(e *idp.SAMLIDPChangedEvent) {
if e.TransientMappingAttributeName != nil {
wm.TransientMappingAttributeName = *e.TransientMappingAttributeName
}
+ if e.FederatedLogoutEnabled != nil {
+ wm.FederatedLogoutEnabled = *e.FederatedLogoutEnabled
+ }
wm.Options.ReduceChanges(e.OptionChanges)
}
@@ -1830,6 +1835,7 @@ func (wm *SAMLIDPWriteModel) NewChanges(
withSignedRequest bool,
nameIDFormat *domain.SAMLNameIDFormat,
transientMappingAttributeName string,
+ federatedLogoutEnabled bool,
options idp.Options,
) ([]idp.SAMLIDPChanges, error) {
changes := make([]idp.SAMLIDPChanges, 0)
@@ -1861,6 +1867,9 @@ func (wm *SAMLIDPWriteModel) NewChanges(
if wm.TransientMappingAttributeName != transientMappingAttributeName {
changes = append(changes, idp.ChangeSAMLTransientMappingAttributeName(transientMappingAttributeName))
}
+ if wm.FederatedLogoutEnabled != federatedLogoutEnabled {
+ changes = append(changes, idp.ChangeSAMLFederatedLogoutEnabled(federatedLogoutEnabled))
+ }
opts := wm.Options.Changes(options)
if !opts.IsZero() {
changes = append(changes, idp.ChangeSAMLOptions(opts))
@@ -1899,6 +1908,7 @@ func (wm *SAMLIDPWriteModel) ToProvider(callbackURL string, idpAlg crypto.Encryp
if wm.TransientMappingAttributeName != "" {
opts = append(opts, saml2.WithTransientMappingAttributeName(wm.TransientMappingAttributeName))
}
+ // TODO: ? if wm.FederatedLogoutEnabled
opts = append(opts, saml2.WithCustomRequestTracker(
requesttracker.New(
addRequest,
diff --git a/internal/command/instance_idp.go b/internal/command/instance_idp.go
index 348f55cd9c..90efb3edd4 100644
--- a/internal/command/instance_idp.go
+++ b/internal/command/instance_idp.go
@@ -1795,6 +1795,7 @@ func (c *Commands) prepareAddInstanceSAMLProvider(a *instance.Aggregate, writeMo
provider.WithSignedRequest,
provider.NameIDFormat,
provider.TransientMappingAttributeName,
+ provider.FederatedLogoutEnabled,
provider.IDPOptions,
),
}, nil
@@ -1848,6 +1849,7 @@ func (c *Commands) prepareUpdateInstanceSAMLProvider(a *instance.Aggregate, writ
provider.WithSignedRequest,
provider.NameIDFormat,
provider.TransientMappingAttributeName,
+ provider.FederatedLogoutEnabled,
provider.IDPOptions,
)
if err != nil || event == nil {
@@ -1893,6 +1895,7 @@ func (c *Commands) prepareRegenerateInstanceSAMLProviderCertificate(a *instance.
writeModel.WithSignedRequest,
writeModel.NameIDFormat,
writeModel.TransientMappingAttributeName,
+ writeModel.FederatedLogoutEnabled,
writeModel.Options,
)
if err != nil || event == nil {
diff --git a/internal/command/instance_idp_model.go b/internal/command/instance_idp_model.go
index d94c19d318..03d9cd9c36 100644
--- a/internal/command/instance_idp_model.go
+++ b/internal/command/instance_idp_model.go
@@ -923,6 +923,7 @@ func (wm *InstanceSAMLIDPWriteModel) NewChangedEvent(
withSignedRequest bool,
nameIDFormat *domain.SAMLNameIDFormat,
transientMappingAttributeName string,
+ federatedLogoutEnabled bool,
options idp.Options,
) (*instance.SAMLIDPChangedEvent, error) {
changes, err := wm.SAMLIDPWriteModel.NewChanges(
@@ -935,6 +936,7 @@ func (wm *InstanceSAMLIDPWriteModel) NewChangedEvent(
withSignedRequest,
nameIDFormat,
transientMappingAttributeName,
+ federatedLogoutEnabled,
options,
)
if err != nil || len(changes) == 0 {
diff --git a/internal/command/instance_idp_test.go b/internal/command/instance_idp_test.go
index 7002598af5..4805d075dd 100644
--- a/internal/command/instance_idp_test.go
+++ b/internal/command/instance_idp_test.go
@@ -5437,6 +5437,7 @@ func TestCommandSide_AddInstanceSAMLIDP(t *testing.T) {
false,
nil,
"",
+ false,
idp.Options{},
),
),
@@ -5478,6 +5479,7 @@ func TestCommandSide_AddInstanceSAMLIDP(t *testing.T) {
true,
gu.Ptr(domain.SAMLNameIDFormatTransient),
"customAttribute",
+ true,
idp.Options{
IsCreationAllowed: true,
IsLinkingAllowed: true,
@@ -5500,6 +5502,7 @@ func TestCommandSide_AddInstanceSAMLIDP(t *testing.T) {
WithSignedRequest: true,
NameIDFormat: gu.Ptr(domain.SAMLNameIDFormatTransient),
TransientMappingAttributeName: "customAttribute",
+ FederatedLogoutEnabled: true,
IDPOptions: idp.Options{
IsCreationAllowed: true,
IsLinkingAllowed: true,
@@ -5665,6 +5668,7 @@ func TestCommandSide_UpdateInstanceGenericSAMLIDP(t *testing.T) {
false,
nil,
"",
+ false,
idp.Options{},
)),
),
@@ -5703,6 +5707,7 @@ func TestCommandSide_UpdateInstanceGenericSAMLIDP(t *testing.T) {
false,
gu.Ptr(domain.SAMLNameIDFormatUnspecified),
"",
+ false,
idp.Options{},
)),
),
@@ -5718,6 +5723,7 @@ func TestCommandSide_UpdateInstanceGenericSAMLIDP(t *testing.T) {
idp.ChangeSAMLWithSignedRequest(true),
idp.ChangeSAMLNameIDFormat(gu.Ptr(domain.SAMLNameIDFormatTransient)),
idp.ChangeSAMLTransientMappingAttributeName("customAttribute"),
+ idp.ChangeSAMLFederatedLogoutEnabled(true),
idp.ChangeSAMLOptions(idp.OptionChanges{
IsCreationAllowed: &t,
IsLinkingAllowed: &t,
@@ -5742,6 +5748,7 @@ func TestCommandSide_UpdateInstanceGenericSAMLIDP(t *testing.T) {
WithSignedRequest: true,
NameIDFormat: gu.Ptr(domain.SAMLNameIDFormatTransient),
TransientMappingAttributeName: "customAttribute",
+ FederatedLogoutEnabled: true,
IDPOptions: idp.Options{
IsCreationAllowed: true,
IsLinkingAllowed: true,
@@ -5845,6 +5852,7 @@ func TestCommandSide_RegenerateInstanceSAMLProviderCertificate(t *testing.T) {
false,
gu.Ptr(domain.SAMLNameIDFormatUnspecified),
"",
+ false,
idp.Options{},
)),
),
diff --git a/internal/command/org_idp.go b/internal/command/org_idp.go
index b72fc1fd77..6cae78acc7 100644
--- a/internal/command/org_idp.go
+++ b/internal/command/org_idp.go
@@ -1768,6 +1768,7 @@ func (c *Commands) prepareAddOrgSAMLProvider(a *org.Aggregate, writeModel *OrgSA
provider.WithSignedRequest,
provider.NameIDFormat,
provider.TransientMappingAttributeName,
+ provider.FederatedLogoutEnabled,
provider.IDPOptions,
),
}, nil
@@ -1821,6 +1822,7 @@ func (c *Commands) prepareUpdateOrgSAMLProvider(a *org.Aggregate, writeModel *Or
provider.WithSignedRequest,
provider.NameIDFormat,
provider.TransientMappingAttributeName,
+ provider.FederatedLogoutEnabled,
provider.IDPOptions,
)
if err != nil || event == nil {
@@ -1866,6 +1868,7 @@ func (c *Commands) prepareRegenerateOrgSAMLProviderCertificate(a *org.Aggregate,
writeModel.WithSignedRequest,
writeModel.NameIDFormat,
writeModel.TransientMappingAttributeName,
+ writeModel.FederatedLogoutEnabled,
writeModel.Options,
)
if err != nil || event == nil {
diff --git a/internal/command/org_idp_model.go b/internal/command/org_idp_model.go
index 3baea11495..fdafb7f087 100644
--- a/internal/command/org_idp_model.go
+++ b/internal/command/org_idp_model.go
@@ -935,6 +935,7 @@ func (wm *OrgSAMLIDPWriteModel) NewChangedEvent(
withSignedRequest bool,
nameIDFormat *domain.SAMLNameIDFormat,
transientMappingAttributeName string,
+ federatedLogoutEnabled bool,
options idp.Options,
) (*org.SAMLIDPChangedEvent, error) {
changes, err := wm.SAMLIDPWriteModel.NewChanges(
@@ -947,6 +948,7 @@ func (wm *OrgSAMLIDPWriteModel) NewChangedEvent(
withSignedRequest,
nameIDFormat,
transientMappingAttributeName,
+ federatedLogoutEnabled,
options,
)
if err != nil || len(changes) == 0 {
diff --git a/internal/command/org_idp_test.go b/internal/command/org_idp_test.go
index 9959ced97d..54f508da30 100644
--- a/internal/command/org_idp_test.go
+++ b/internal/command/org_idp_test.go
@@ -5519,6 +5519,7 @@ func TestCommandSide_AddOrgSAMLIDP(t *testing.T) {
false,
nil,
"",
+ false,
idp.Options{},
),
),
@@ -5560,6 +5561,7 @@ func TestCommandSide_AddOrgSAMLIDP(t *testing.T) {
true,
gu.Ptr(domain.SAMLNameIDFormatTransient),
"customAttribute",
+ true,
idp.Options{
IsCreationAllowed: true,
IsLinkingAllowed: true,
@@ -5583,6 +5585,7 @@ func TestCommandSide_AddOrgSAMLIDP(t *testing.T) {
WithSignedRequest: true,
NameIDFormat: gu.Ptr(domain.SAMLNameIDFormatTransient),
TransientMappingAttributeName: "customAttribute",
+ FederatedLogoutEnabled: true,
IDPOptions: idp.Options{
IsCreationAllowed: true,
IsLinkingAllowed: true,
@@ -5756,6 +5759,7 @@ func TestCommandSide_UpdateOrgSAMLIDP(t *testing.T) {
false,
nil,
"",
+ false,
idp.Options{},
)),
),
@@ -5795,6 +5799,7 @@ func TestCommandSide_UpdateOrgSAMLIDP(t *testing.T) {
false,
gu.Ptr(domain.SAMLNameIDFormatUnspecified),
"",
+ false,
idp.Options{},
)),
),
@@ -5810,6 +5815,7 @@ func TestCommandSide_UpdateOrgSAMLIDP(t *testing.T) {
idp.ChangeSAMLWithSignedRequest(true),
idp.ChangeSAMLNameIDFormat(gu.Ptr(domain.SAMLNameIDFormatTransient)),
idp.ChangeSAMLTransientMappingAttributeName("customAttribute"),
+ idp.ChangeSAMLFederatedLogoutEnabled(true),
idp.ChangeSAMLOptions(idp.OptionChanges{
IsCreationAllowed: &t,
IsLinkingAllowed: &t,
@@ -5835,6 +5841,7 @@ func TestCommandSide_UpdateOrgSAMLIDP(t *testing.T) {
WithSignedRequest: true,
NameIDFormat: gu.Ptr(domain.SAMLNameIDFormatTransient),
TransientMappingAttributeName: "customAttribute",
+ FederatedLogoutEnabled: true,
IDPOptions: idp.Options{
IsCreationAllowed: true,
IsLinkingAllowed: true,
@@ -5943,6 +5950,7 @@ func TestCommandSide_RegenerateOrgSAMLProviderCertificate(t *testing.T) {
false,
gu.Ptr(domain.SAMLNameIDFormatUnspecified),
"",
+ false,
idp.Options{},
)),
),
diff --git a/internal/domain/federatedlogout/logout.go b/internal/domain/federatedlogout/logout.go
new file mode 100644
index 0000000000..ca208a129a
--- /dev/null
+++ b/internal/domain/federatedlogout/logout.go
@@ -0,0 +1,37 @@
+package federatedlogout
+
+type Index int
+
+const (
+ IndexUnspecified Index = iota
+ IndexRequestID
+)
+
+type FederatedLogout struct {
+ InstanceID string
+ FingerPrintID string
+ SessionID string
+ IDPID string
+ UserID string
+ PostLogoutRedirectURI string
+ State State
+}
+
+// Keys implements cache.Entry
+func (c *FederatedLogout) Keys(i Index) []string {
+ if i == IndexRequestID {
+ return []string{Key(c.InstanceID, c.SessionID)}
+ }
+ return nil
+}
+
+func Key(instanceID, sessionID string) string {
+ return instanceID + "-" + sessionID
+}
+
+type State int
+
+const (
+ StateCreated State = iota
+ StateRedirected
+)
diff --git a/internal/query/idp_template.go b/internal/query/idp_template.go
index f51e9a11a7..c1c47e73bc 100644
--- a/internal/query/idp_template.go
+++ b/internal/query/idp_template.go
@@ -165,6 +165,7 @@ type SAMLIDPTemplate struct {
WithSignedRequest bool
NameIDFormat sql.Null[domain.SAMLNameIDFormat]
TransientMappingAttributeName string
+ FederatedLogoutEnabled bool
}
var (
@@ -724,6 +725,10 @@ var (
name: projection.SAMLTransientMappingAttributeName,
table: samlIdpTemplateTable,
}
+ SAMLFederatedLogoutEnabledCol = Column{
+ name: projection.SAMLFederatedLogoutEnabled,
+ table: samlIdpTemplateTable,
+ }
)
// IDPTemplateByID searches for the requested id with permission check if necessary
@@ -948,6 +953,7 @@ func prepareIDPTemplateByIDQuery() (sq.SelectBuilder, func(*sql.Row) (*IDPTempla
SAMLWithSignedRequestCol.identifier(),
SAMLNameIDFormatCol.identifier(),
SAMLTransientMappingAttributeNameCol.identifier(),
+ SAMLFederatedLogoutEnabledCol.identifier(),
// ldap
LDAPIDCol.identifier(),
LDAPServersCol.identifier(),
@@ -1067,6 +1073,7 @@ func prepareIDPTemplateByIDQuery() (sq.SelectBuilder, func(*sql.Row) (*IDPTempla
samlWithSignedRequest := sql.NullBool{}
samlNameIDFormat := sql.Null[domain.SAMLNameIDFormat]{}
samlTransientMappingAttributeName := sql.NullString{}
+ samlFederatedLogoutEnabled := sql.NullBool{}
ldapID := sql.NullString{}
ldapServers := database.TextArray[string]{}
@@ -1184,6 +1191,7 @@ func prepareIDPTemplateByIDQuery() (sq.SelectBuilder, func(*sql.Row) (*IDPTempla
&samlWithSignedRequest,
&samlNameIDFormat,
&samlTransientMappingAttributeName,
+ &samlFederatedLogoutEnabled,
// ldap
&ldapID,
&ldapServers,
@@ -1323,6 +1331,7 @@ func prepareIDPTemplateByIDQuery() (sq.SelectBuilder, func(*sql.Row) (*IDPTempla
WithSignedRequest: samlWithSignedRequest.Bool,
NameIDFormat: samlNameIDFormat,
TransientMappingAttributeName: samlTransientMappingAttributeName.String,
+ FederatedLogoutEnabled: samlFederatedLogoutEnabled.Bool,
}
}
if ldapID.Valid {
@@ -1456,6 +1465,7 @@ func prepareIDPTemplatesQuery() (sq.SelectBuilder, func(*sql.Rows) (*IDPTemplate
SAMLWithSignedRequestCol.identifier(),
SAMLNameIDFormatCol.identifier(),
SAMLTransientMappingAttributeNameCol.identifier(),
+ SAMLFederatedLogoutEnabledCol.identifier(),
// ldap
LDAPIDCol.identifier(),
LDAPServersCol.identifier(),
@@ -1580,6 +1590,7 @@ func prepareIDPTemplatesQuery() (sq.SelectBuilder, func(*sql.Rows) (*IDPTemplate
samlWithSignedRequest := sql.NullBool{}
samlNameIDFormat := sql.Null[domain.SAMLNameIDFormat]{}
samlTransientMappingAttributeName := sql.NullString{}
+ samlFederatedLogoutEnabled := sql.NullBool{}
ldapID := sql.NullString{}
ldapServers := database.TextArray[string]{}
@@ -1697,6 +1708,7 @@ func prepareIDPTemplatesQuery() (sq.SelectBuilder, func(*sql.Rows) (*IDPTemplate
&samlWithSignedRequest,
&samlNameIDFormat,
&samlTransientMappingAttributeName,
+ &samlFederatedLogoutEnabled,
// ldap
&ldapID,
&ldapServers,
@@ -1835,6 +1847,7 @@ func prepareIDPTemplatesQuery() (sq.SelectBuilder, func(*sql.Rows) (*IDPTemplate
WithSignedRequest: samlWithSignedRequest.Bool,
NameIDFormat: samlNameIDFormat,
TransientMappingAttributeName: samlTransientMappingAttributeName.String,
+ FederatedLogoutEnabled: samlFederatedLogoutEnabled.Bool,
}
}
if ldapID.Valid {
diff --git a/internal/query/idp_template_test.go b/internal/query/idp_template_test.go
index 702e5d0ced..aed243c61e 100644
--- a/internal/query/idp_template_test.go
+++ b/internal/query/idp_template_test.go
@@ -99,6 +99,7 @@ var (
` projections.idp_templates6_saml.with_signed_request,` +
` projections.idp_templates6_saml.name_id_format,` +
` projections.idp_templates6_saml.transient_mapping_attribute_name,` +
+ ` projections.idp_templates6_saml.federated_logout_enabled,` +
// ldap
` projections.idp_templates6_ldap2.idp_id,` +
` projections.idp_templates6_ldap2.servers,` +
@@ -228,6 +229,7 @@ var (
"with_signed_request",
"name_id_format",
"transient_mapping_attribute_name",
+ "federated_logout_enabled",
// ldap config
"idp_id",
"servers",
@@ -344,6 +346,7 @@ var (
` projections.idp_templates6_saml.with_signed_request,` +
` projections.idp_templates6_saml.name_id_format,` +
` projections.idp_templates6_saml.transient_mapping_attribute_name,` +
+ ` projections.idp_templates6_saml.federated_logout_enabled,` +
// ldap
` projections.idp_templates6_ldap2.idp_id,` +
` projections.idp_templates6_ldap2.servers,` +
@@ -474,6 +477,7 @@ var (
"with_signed_request",
"name_id_format",
"transient_mapping_attribute_name",
+ "federated_logout_enabled",
// ldap config
"idp_id",
"servers",
@@ -630,6 +634,7 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) {
nil,
nil,
nil,
+ nil,
// ldap config
nil,
nil,
@@ -784,6 +789,7 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) {
nil,
nil,
nil,
+ nil,
// ldap config
nil,
nil,
@@ -936,6 +942,7 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) {
nil,
nil,
nil,
+ nil,
// ldap config
nil,
nil,
@@ -1086,6 +1093,7 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) {
nil,
nil,
nil,
+ nil,
// ldap config
nil,
nil,
@@ -1235,6 +1243,7 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) {
nil,
nil,
nil,
+ nil,
// ldap config
nil,
nil,
@@ -1384,6 +1393,7 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) {
nil,
nil,
nil,
+ nil,
// ldap config
nil,
nil,
@@ -1534,6 +1544,7 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) {
nil,
nil,
nil,
+ nil,
// ldap config
nil,
nil,
@@ -1683,6 +1694,7 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) {
false,
domain.SAMLNameIDFormatTransient,
"customAttribute",
+ true,
// ldap config
nil,
nil,
@@ -1742,6 +1754,7 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) {
WithSignedRequest: false,
NameIDFormat: sql.Null[domain.SAMLNameIDFormat]{V: domain.SAMLNameIDFormatTransient, Valid: true},
TransientMappingAttributeName: "customAttribute",
+ FederatedLogoutEnabled: true,
},
},
},
@@ -1836,6 +1849,7 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) {
nil,
nil,
nil,
+ nil,
// ldap config
"idp-id",
database.TextArray[string]{"server"},
@@ -2006,6 +2020,7 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) {
nil,
nil,
nil,
+ nil,
// ldap config
nil,
nil,
@@ -2157,6 +2172,7 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) {
nil,
nil,
nil,
+ nil,
// ldap config
nil,
nil,
@@ -2336,6 +2352,7 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) {
nil,
nil,
nil,
+ nil,
// ldap config
"idp-id",
database.TextArray[string]{"server"},
@@ -2515,6 +2532,7 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) {
nil,
nil,
nil,
+ nil,
// ldap config
nil,
nil,
@@ -2667,6 +2685,7 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) {
nil,
nil,
nil,
+ nil,
// ldap config
"idp-id-ldap",
database.TextArray[string]{"server"},
@@ -2784,6 +2803,7 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) {
false,
domain.SAMLNameIDFormatTransient,
"customAttribute",
+ true,
// ldap config
nil,
nil,
@@ -2901,6 +2921,7 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) {
nil,
nil,
nil,
+ nil,
// ldap config
nil,
nil,
@@ -3018,6 +3039,7 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) {
nil,
nil,
nil,
+ nil,
// ldap config
nil,
nil,
@@ -3135,6 +3157,7 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) {
nil,
nil,
nil,
+ nil,
// ldap config
nil,
nil,
@@ -3252,6 +3275,7 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) {
nil,
nil,
nil,
+ nil,
// ldap config
nil,
nil,
@@ -3360,6 +3384,7 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) {
WithSignedRequest: false,
NameIDFormat: sql.Null[domain.SAMLNameIDFormat]{V: domain.SAMLNameIDFormatTransient, Valid: true},
TransientMappingAttributeName: "customAttribute",
+ FederatedLogoutEnabled: true,
},
},
{
diff --git a/internal/query/projection/idp_template.go b/internal/query/projection/idp_template.go
index 55c74b851c..11eeb8c613 100644
--- a/internal/query/projection/idp_template.go
+++ b/internal/query/projection/idp_template.go
@@ -173,6 +173,7 @@ const (
SAMLWithSignedRequestCol = "with_signed_request"
SAMLNameIDFormatCol = "name_id_format"
SAMLTransientMappingAttributeName = "transient_mapping_attribute_name"
+ SAMLFederatedLogoutEnabled = "federated_logout_enabled"
)
type idpTemplateProjection struct{}
@@ -377,6 +378,7 @@ func (*idpTemplateProjection) Init() *old_handler.Check {
handler.NewColumn(SAMLWithSignedRequestCol, handler.ColumnTypeBool, handler.Nullable()),
handler.NewColumn(SAMLNameIDFormatCol, handler.ColumnTypeEnum, handler.Nullable()),
handler.NewColumn(SAMLTransientMappingAttributeName, handler.ColumnTypeText, handler.Nullable()),
+ handler.NewColumn(SAMLFederatedLogoutEnabled, handler.ColumnTypeBool, handler.Default(false)),
},
handler.NewPrimaryKey(SAMLInstanceIDCol, SAMLIDCol),
IDPTemplateSAMLSuffix,
@@ -1990,6 +1992,7 @@ func (p *idpTemplateProjection) reduceSAMLIDPAdded(event eventstore.Event) (*han
handler.NewCol(SAMLBindingCol, idpEvent.Binding),
handler.NewCol(SAMLWithSignedRequestCol, idpEvent.WithSignedRequest),
handler.NewCol(SAMLTransientMappingAttributeName, idpEvent.TransientMappingAttributeName),
+ handler.NewCol(SAMLFederatedLogoutEnabled, idpEvent.FederatedLogoutEnabled),
}
if idpEvent.NameIDFormat != nil {
columns = append(columns, handler.NewCol(SAMLNameIDFormatCol, *idpEvent.NameIDFormat))
@@ -2525,5 +2528,8 @@ func reduceSAMLIDPChangedColumns(idpEvent idp.SAMLIDPChangedEvent) []handler.Col
if idpEvent.TransientMappingAttributeName != nil {
SAMLCols = append(SAMLCols, handler.NewCol(SAMLTransientMappingAttributeName, *idpEvent.TransientMappingAttributeName))
}
+ if idpEvent.FederatedLogoutEnabled != nil {
+ SAMLCols = append(SAMLCols, handler.NewCol(SAMLFederatedLogoutEnabled, *idpEvent.FederatedLogoutEnabled))
+ }
return SAMLCols
}
diff --git a/internal/query/projection/idp_template_test.go b/internal/query/projection/idp_template_test.go
index cebf3f8791..6ba6dabd47 100644
--- a/internal/query/projection/idp_template_test.go
+++ b/internal/query/projection/idp_template_test.go
@@ -2793,7 +2793,8 @@ func TestIDPTemplateProjection_reducesSAML(t *testing.T) {
"isLinkingAllowed": true,
"isAutoCreation": true,
"isAutoUpdate": true,
- "autoLinkingOption": 1
+ "autoLinkingOption": 1,
+ "federatedLogoutEnabled": true
}`),
), instance.SAMLIDPAddedEventMapper),
},
@@ -2824,7 +2825,7 @@ func TestIDPTemplateProjection_reducesSAML(t *testing.T) {
},
},
{
- expectedStmt: "INSERT INTO projections.idp_templates6_saml (idp_id, instance_id, metadata, key, certificate, binding, with_signed_request, transient_mapping_attribute_name, name_id_format) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)",
+ expectedStmt: "INSERT INTO projections.idp_templates6_saml (idp_id, instance_id, metadata, key, certificate, binding, with_signed_request, transient_mapping_attribute_name, federated_logout_enabled, name_id_format) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)",
expectedArgs: []interface{}{
"idp-id",
"instance-id",
@@ -2834,6 +2835,7 @@ func TestIDPTemplateProjection_reducesSAML(t *testing.T) {
"binding",
true,
"customAttribute",
+ true,
domain.SAMLNameIDFormatTransient,
},
},
@@ -2865,7 +2867,8 @@ func TestIDPTemplateProjection_reducesSAML(t *testing.T) {
"isLinkingAllowed": true,
"isAutoCreation": true,
"isAutoUpdate": true,
- "autoLinkingOption": 1
+ "autoLinkingOption": 1,
+ "federatedLogoutEnabled": true
}`),
), org.SAMLIDPAddedEventMapper),
},
@@ -2896,7 +2899,7 @@ func TestIDPTemplateProjection_reducesSAML(t *testing.T) {
},
},
{
- expectedStmt: "INSERT INTO projections.idp_templates6_saml (idp_id, instance_id, metadata, key, certificate, binding, with_signed_request, transient_mapping_attribute_name, name_id_format) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)",
+ expectedStmt: "INSERT INTO projections.idp_templates6_saml (idp_id, instance_id, metadata, key, certificate, binding, with_signed_request, transient_mapping_attribute_name, federated_logout_enabled, name_id_format) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)",
expectedArgs: []interface{}{
"idp-id",
"instance-id",
@@ -2906,6 +2909,7 @@ func TestIDPTemplateProjection_reducesSAML(t *testing.T) {
"binding",
true,
"customAttribute",
+ true,
domain.SAMLNameIDFormatTransient,
},
},
@@ -2976,7 +2980,8 @@ func TestIDPTemplateProjection_reducesSAML(t *testing.T) {
"isLinkingAllowed": true,
"isAutoCreation": true,
"isAutoUpdate": true,
- "autoLinkingOption": 1
+ "autoLinkingOption": 1,
+ "federatedLogoutEnabled": true
}`),
), instance.SAMLIDPChangedEventMapper),
},
@@ -3002,13 +3007,14 @@ func TestIDPTemplateProjection_reducesSAML(t *testing.T) {
},
},
{
- expectedStmt: "UPDATE projections.idp_templates6_saml SET (metadata, key, certificate, binding, with_signed_request) = ($1, $2, $3, $4, $5) WHERE (idp_id = $6) AND (instance_id = $7)",
+ expectedStmt: "UPDATE projections.idp_templates6_saml SET (metadata, key, certificate, binding, with_signed_request, federated_logout_enabled) = ($1, $2, $3, $4, $5, $6) WHERE (idp_id = $7) AND (instance_id = $8)",
expectedArgs: []interface{}{
[]byte("metadata"),
anyArg{},
anyArg{},
"binding",
true,
+ true,
"idp-id",
"instance-id",
},
diff --git a/internal/repository/idp/saml.go b/internal/repository/idp/saml.go
index da6a52b1be..209d8371b3 100644
--- a/internal/repository/idp/saml.go
+++ b/internal/repository/idp/saml.go
@@ -19,6 +19,7 @@ type SAMLIDPAddedEvent struct {
WithSignedRequest bool `json:"withSignedRequest,omitempty"`
NameIDFormat *domain.SAMLNameIDFormat `json:"nameIDFormat,omitempty"`
TransientMappingAttributeName string `json:"transientMappingAttributeName,omitempty"`
+ FederatedLogoutEnabled bool `json:"federatedLogoutEnabled,omitempty"`
Options
}
@@ -33,6 +34,7 @@ func NewSAMLIDPAddedEvent(
withSignedRequest bool,
nameIDFormat *domain.SAMLNameIDFormat,
transientMappingAttributeName string,
+ federatedLogoutEnabled bool,
options Options,
) *SAMLIDPAddedEvent {
return &SAMLIDPAddedEvent{
@@ -46,6 +48,7 @@ func NewSAMLIDPAddedEvent(
WithSignedRequest: withSignedRequest,
NameIDFormat: nameIDFormat,
TransientMappingAttributeName: transientMappingAttributeName,
+ FederatedLogoutEnabled: federatedLogoutEnabled,
Options: options,
}
}
@@ -83,6 +86,7 @@ type SAMLIDPChangedEvent struct {
WithSignedRequest *bool `json:"withSignedRequest,omitempty"`
NameIDFormat *domain.SAMLNameIDFormat `json:"nameIDFormat,omitempty"`
TransientMappingAttributeName *string `json:"transientMappingAttributeName,omitempty"`
+ FederatedLogoutEnabled *bool `json:"federatedLogoutEnabled,omitempty"`
OptionChanges
}
@@ -154,6 +158,12 @@ func ChangeSAMLTransientMappingAttributeName(name string) func(*SAMLIDPChangedEv
}
}
+func ChangeSAMLFederatedLogoutEnabled(federatedLogoutEnabled bool) func(*SAMLIDPChangedEvent) {
+ return func(e *SAMLIDPChangedEvent) {
+ e.FederatedLogoutEnabled = &federatedLogoutEnabled
+ }
+}
+
func ChangeSAMLOptions(options OptionChanges) func(*SAMLIDPChangedEvent) {
return func(e *SAMLIDPChangedEvent) {
e.OptionChanges = options
diff --git a/internal/repository/instance/idp.go b/internal/repository/instance/idp.go
index 6ab60c0dd5..f0f324cb7d 100644
--- a/internal/repository/instance/idp.go
+++ b/internal/repository/instance/idp.go
@@ -1024,6 +1024,7 @@ func NewSAMLIDPAddedEvent(
withSignedRequest bool,
nameIDFormat *domain.SAMLNameIDFormat,
transientMappingAttributeName string,
+ federatedLogoutEnabled bool,
options idp.Options,
) *SAMLIDPAddedEvent {
return &SAMLIDPAddedEvent{
@@ -1042,6 +1043,7 @@ func NewSAMLIDPAddedEvent(
withSignedRequest,
nameIDFormat,
transientMappingAttributeName,
+ federatedLogoutEnabled,
options,
),
}
diff --git a/internal/repository/org/idp.go b/internal/repository/org/idp.go
index 0070f71a95..5f061370f1 100644
--- a/internal/repository/org/idp.go
+++ b/internal/repository/org/idp.go
@@ -1025,6 +1025,7 @@ func NewSAMLIDPAddedEvent(
withSignedRequest bool,
nameIDFormat *domain.SAMLNameIDFormat,
transientMappingAttributeName string,
+ federatedLogoutEnabled bool,
options idp.Options,
) *SAMLIDPAddedEvent {
@@ -1044,6 +1045,7 @@ func NewSAMLIDPAddedEvent(
withSignedRequest,
nameIDFormat,
transientMappingAttributeName,
+ federatedLogoutEnabled,
options,
),
}
diff --git a/internal/user/repository/view/user_session.sql b/internal/user/repository/view/user_session.sql
new file mode 100644
index 0000000000..3e2a97206b
--- /dev/null
+++ b/internal/user/repository/view/user_session.sql
@@ -0,0 +1,29 @@
+SELECT s.creation_date,
+ s.change_date,
+ s.resource_owner,
+ s.state,
+ s.user_agent_id,
+ s.user_id,
+ u.username,
+ l.login_name,
+ h.display_name,
+ h.avatar_key,
+ s.selected_idp_config_id,
+ s.password_verification,
+ s.passwordless_verification,
+ s.external_login_verification,
+ s.second_factor_verification,
+ s.second_factor_verification_type,
+ s.multi_factor_verification,
+ s.multi_factor_verification_type,
+ s.sequence,
+ s.instance_id,
+ s.id
+FROM auth.user_sessions s
+ LEFT JOIN projections.users14 u ON s.user_id = u.id AND s.instance_id = u.instance_id
+ LEFT JOIN projections.users14_humans h ON s.user_id = h.user_id AND s.instance_id = h.instance_id
+ LEFT JOIN projections.login_names3 l ON s.user_id = l.user_id AND s.instance_id = l.instance_id AND l.is_primary = true
+WHERE (s.id = $1)
+ AND (s.instance_id = $2)
+LIMIT 1
+;
\ No newline at end of file
diff --git a/internal/user/repository/view/user_session_view.go b/internal/user/repository/view/user_session_view.go
index b3d155f1ec..dec22a181d 100644
--- a/internal/user/repository/view/user_session_view.go
+++ b/internal/user/repository/view/user_session_view.go
@@ -12,6 +12,9 @@ import (
)
//go:embed user_session_by_id.sql
+var userSessionByIDsQuery string
+
+//go:embed user_session.sql
var userSessionByIDQuery string
//go:embed user_sessions_by_user_agent.sql
@@ -30,7 +33,7 @@ func UserSessionByIDs(ctx context.Context, db *database.DB, agentID, userID, ins
userSession, err = scanUserSession(row)
return err
},
- userSessionByIDQuery,
+ userSessionByIDsQuery,
agentID,
userID,
instanceID,
@@ -38,6 +41,20 @@ func UserSessionByIDs(ctx context.Context, db *database.DB, agentID, userID, ins
return userSession, err
}
+func UserSessionByID(ctx context.Context, db *database.DB, userSessionID, instanceID string) (userSession *model.UserSessionView, err error) {
+ err = db.QueryRowContext(
+ ctx,
+ func(row *sql.Row) error {
+ userSession, err = scanUserSession(row)
+ return err
+ },
+ userSessionByIDQuery,
+ userSessionID,
+ instanceID,
+ )
+ return userSession, err
+}
+
func UserSessionsByAgentID(ctx context.Context, db *database.DB, agentID, instanceID string) (userSessions []*model.UserSessionView, err error) {
err = db.QueryContext(
ctx,
diff --git a/proto/zitadel/admin.proto b/proto/zitadel/admin.proto
index 9033fd8668..1e7f3b7407 100644
--- a/proto/zitadel/admin.proto
+++ b/proto/zitadel/admin.proto
@@ -7035,6 +7035,9 @@ message AddSAMLProviderRequest {
// Optionally specify the name of the attribute, which will be used to map the user
// in case the nameid-format returned is `urn:oasis:names:tc:SAML:2.0:nameid-format:transient`.
optional string transient_mapping_attribute_name = 8;
+ // Optionally enable federated logout. If enabled, ZITADEL will send a logout request to the identity provider,
+ // if the user terminates the session in ZITADEL. Be sure to provide a SLO endpoint as part of the metadata.
+ optional bool federated_logout_enabled = 9;
}
message AddSAMLProviderResponse {
@@ -7069,6 +7072,9 @@ message UpdateSAMLProviderRequest {
// Optionally specify the name of the attribute, which will be used to map the user
// in case the nameid-format returned is `urn:oasis:names:tc:SAML:2.0:nameid-format:transient`.
optional string transient_mapping_attribute_name = 9;
+ // Optionally enable federated logout. If enabled, ZITADEL will send a logout request to the identity provider,
+ // if the user terminates the session in ZITADEL. Be sure to provide a SLO endpoint as part of the metadata.
+ optional bool federated_logout_enabled = 10;
}
message UpdateSAMLProviderResponse {
diff --git a/proto/zitadel/idp.proto b/proto/zitadel/idp.proto
index 82e32aa873..de497d8c93 100644
--- a/proto/zitadel/idp.proto
+++ b/proto/zitadel/idp.proto
@@ -479,6 +479,9 @@ message SAMLConfig {
// Optional name of the attribute, which will be used to map the user
// in case the nameid-format returned is `urn:oasis:names:tc:SAML:2.0:nameid-format:transient`.
optional string transient_mapping_attribute_name = 5;
+ // Boolean weather federated logout is enabled. If enabled, ZITADEL will send a logout request to the identity provider,
+ // if the user terminates the session in ZITADEL. Be sure to provide a SLO endpoint as part of the metadata.
+ optional bool federated_logout_enabled = 6;
}
message AzureADConfig {
diff --git a/proto/zitadel/idp/v2/idp.proto b/proto/zitadel/idp/v2/idp.proto
index 0c95b742f1..663581a659 100644
--- a/proto/zitadel/idp/v2/idp.proto
+++ b/proto/zitadel/idp/v2/idp.proto
@@ -303,6 +303,9 @@ message SAMLConfig {
// in case the nameid-format returned is
// `urn:oasis:names:tc:SAML:2.0:nameid-format:transient`.
optional string transient_mapping_attribute_name = 5;
+ // Boolean weather federated logout is enabled. If enabled, ZITADEL will send a logout request to the identity provider,
+ // if the user terminates the session in ZITADEL. Be sure to provide a SLO endpoint as part of the metadata.
+ optional bool federated_logout_enabled = 6;
}
message AzureADConfig {
diff --git a/proto/zitadel/management.proto b/proto/zitadel/management.proto
index c90c44667c..3018ebe600 100644
--- a/proto/zitadel/management.proto
+++ b/proto/zitadel/management.proto
@@ -13408,6 +13408,9 @@ message AddSAMLProviderRequest {
// Optionally specify the name of the attribute, which will be used to map the user
// in case the nameid-format returned is `urn:oasis:names:tc:SAML:2.0:nameid-format:transient`.
optional string transient_mapping_attribute_name = 8;
+ // Optionally enable federated logout. If enabled, ZITADEL will send a logout request to the identity provider,
+ // if the user terminates the session in ZITADEL. Be sure to provide a SLO endpoint as part of the metadata.
+ optional bool federated_logout_enabled = 9;
}
message AddSAMLProviderResponse {
@@ -13442,6 +13445,9 @@ message UpdateSAMLProviderRequest {
// Optionally specify the name of the attribute, which will be used to map the user
// in case the nameid-format returned is `urn:oasis:names:tc:SAML:2.0:nameid-format:transient`.
optional string transient_mapping_attribute_name = 9;
+ // Optionally enable federated logout. If enabled, ZITADEL will send a logout request to the identity provider,
+ // if the user terminates the session in ZITADEL. Be sure to provide a SLO endpoint as part of the metadata.
+ optional bool federated_logout_enabled = 10;
}
message UpdateSAMLProviderResponse {