mirror of
https://github.com/zitadel/zitadel.git
synced 2025-08-12 01:37:31 +00:00
feat(OIDC): add back channel logout (#8837)
# Which Problems Are Solved Currently ZITADEL supports RP-initiated logout for clients. Back-channel logout ensures that user sessions are terminated across all connected applications, even if the user closes their browser or loses connectivity providing a more secure alternative for certain use cases. # How the Problems Are Solved If the feature is activated and the client used for the authentication has a back_channel_logout_uri configured, a `session_logout.back_channel` will be registered. Once a user terminates their session, a (notification) handler will send a SET (form POST) to the registered uri containing a logout_token (with the user's ID and session ID). - A new feature "back_channel_logout" is added on system and instance level - A `back_channel_logout_uri` can be managed on OIDC applications - Added a `session_logout` aggregate to register and inform about sent `back_channel` notifications - Added a `SecurityEventToken` channel and `Form`message type in the notification handlers - Added `TriggeredAtOrigin` fields to `HumanSignedOut` and `TerminateSession` events for notification handling - Exported various functions and types in the `oidc` package to be able to reuse for token signing in the back_channel notifier. - To prevent that current existing session termination events will be handled, a setup step is added to set the `current_states` for the `projections.notifications_back_channel_logout` to the current position - [x] requires https://github.com/zitadel/oidc/pull/671 # Additional Changes - Updated all OTEL dependencies to v1.29.0, since OIDC already updated some of them to that version. - Single Session Termination feature is correctly checked (fixed feature mapping) # Additional Context - closes https://github.com/zitadel/zitadel/issues/8467 - TODO: - Documentation - UI to be done: https://github.com/zitadel/zitadel/issues/8469 --------- Co-authored-by: Hidde Wieringa <hidde@hiddewieringa.nl>
This commit is contained in:
@@ -155,6 +155,7 @@ func TestCommandSide_AddInstanceDomain(t *testing.T) {
|
||||
time.Second*1,
|
||||
[]string{"https://sub.test.ch"},
|
||||
false,
|
||||
"",
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@@ -27,6 +27,7 @@ type InstanceFeatures struct {
|
||||
DebugOIDCParentError *bool
|
||||
OIDCSingleV1SessionTermination *bool
|
||||
DisableUserTokenEvent *bool
|
||||
EnableBackChannelLogout *bool
|
||||
}
|
||||
|
||||
func (m *InstanceFeatures) isEmpty() bool {
|
||||
@@ -41,7 +42,8 @@ func (m *InstanceFeatures) isEmpty() bool {
|
||||
m.WebKey == nil &&
|
||||
m.DebugOIDCParentError == nil &&
|
||||
m.OIDCSingleV1SessionTermination == nil &&
|
||||
m.DisableUserTokenEvent == nil
|
||||
m.DisableUserTokenEvent == nil &&
|
||||
m.EnableBackChannelLogout == nil
|
||||
}
|
||||
|
||||
func (c *Commands) SetInstanceFeatures(ctx context.Context, f *InstanceFeatures) (*domain.ObjectDetails, error) {
|
||||
|
@@ -71,6 +71,7 @@ func (m *InstanceFeaturesWriteModel) Query() *eventstore.SearchQueryBuilder {
|
||||
feature_v2.InstanceDebugOIDCParentErrorEventType,
|
||||
feature_v2.InstanceOIDCSingleV1SessionTerminationEventType,
|
||||
feature_v2.InstanceDisableUserTokenEvent,
|
||||
feature_v2.InstanceEnableBackChannelLogout,
|
||||
).
|
||||
Builder().ResourceOwner(m.ResourceOwner)
|
||||
}
|
||||
@@ -116,6 +117,9 @@ func reduceInstanceFeature(features *InstanceFeatures, key feature.Key, value an
|
||||
case feature.KeyDisableUserTokenEvent:
|
||||
v := value.(bool)
|
||||
features.DisableUserTokenEvent = &v
|
||||
case feature.KeyEnableBackChannelLogout:
|
||||
v := value.(bool)
|
||||
features.EnableBackChannelLogout = &v
|
||||
}
|
||||
}
|
||||
|
||||
@@ -133,5 +137,6 @@ func (wm *InstanceFeaturesWriteModel) setCommands(ctx context.Context, f *Instan
|
||||
cmds = appendFeatureUpdate(ctx, cmds, aggregate, wm.DebugOIDCParentError, f.DebugOIDCParentError, feature_v2.InstanceDebugOIDCParentErrorEventType)
|
||||
cmds = appendFeatureUpdate(ctx, cmds, aggregate, wm.OIDCSingleV1SessionTermination, f.OIDCSingleV1SessionTermination, feature_v2.InstanceOIDCSingleV1SessionTerminationEventType)
|
||||
cmds = appendFeatureUpdate(ctx, cmds, aggregate, wm.DisableUserTokenEvent, f.DisableUserTokenEvent, feature_v2.InstanceDisableUserTokenEvent)
|
||||
cmds = appendFeatureUpdate(ctx, cmds, aggregate, wm.EnableBackChannelLogout, f.EnableBackChannelLogout, feature_v2.InstanceEnableBackChannelLogout)
|
||||
return cmds
|
||||
}
|
||||
|
@@ -127,6 +127,7 @@ func oidcAppEvents(ctx context.Context, orgID, projectID, id, name, clientID str
|
||||
0,
|
||||
nil,
|
||||
false,
|
||||
"",
|
||||
),
|
||||
}
|
||||
}
|
||||
@@ -439,6 +440,7 @@ func generatedDomainFilters(instanceID, orgID, projectID, appID, generatedDomain
|
||||
0,
|
||||
nil,
|
||||
false,
|
||||
"",
|
||||
),
|
||||
),
|
||||
expectFilter(
|
||||
|
24
internal/command/logout_session.go
Normal file
24
internal/command/logout_session.go
Normal file
@@ -0,0 +1,24 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/repository/sessionlogout"
|
||||
"github.com/zitadel/zitadel/internal/telemetry/tracing"
|
||||
)
|
||||
|
||||
func (c *Commands) BackChannelLogoutSent(ctx context.Context, id, oidcSessionID, instanceID string) (err error) {
|
||||
ctx, span := tracing.NewSpan(ctx)
|
||||
defer func() { span.EndWithError(err) }()
|
||||
|
||||
sessionWriteModel := NewSessionLogoutWriteModel(id, instanceID, oidcSessionID)
|
||||
if err = c.eventstore.FilterToQueryReducer(ctx, sessionWriteModel); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.pushAppendAndReduce(
|
||||
ctx,
|
||||
sessionWriteModel,
|
||||
sessionlogout.NewBackChannelLogoutSentEvent(ctx, sessionWriteModel.aggregate, oidcSessionID),
|
||||
)
|
||||
}
|
74
internal/command/logout_session_model.go
Normal file
74
internal/command/logout_session_model.go
Normal file
@@ -0,0 +1,74 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/repository/sessionlogout"
|
||||
)
|
||||
|
||||
type SessionLogoutWriteModel struct {
|
||||
eventstore.WriteModel
|
||||
|
||||
UserID string
|
||||
OIDCSessionID string
|
||||
ClientID string
|
||||
BackChannelLogoutURI string
|
||||
BackChannelLogoutSent bool
|
||||
|
||||
aggregate *eventstore.Aggregate
|
||||
}
|
||||
|
||||
func NewSessionLogoutWriteModel(id string, instanceID string, sessionID string) *SessionLogoutWriteModel {
|
||||
return &SessionLogoutWriteModel{
|
||||
WriteModel: eventstore.WriteModel{
|
||||
AggregateID: id,
|
||||
ResourceOwner: instanceID,
|
||||
InstanceID: instanceID,
|
||||
},
|
||||
aggregate: &sessionlogout.NewAggregate(id, instanceID).Aggregate,
|
||||
OIDCSessionID: sessionID,
|
||||
}
|
||||
}
|
||||
|
||||
func (wm *SessionLogoutWriteModel) Reduce() error {
|
||||
for _, event := range wm.Events {
|
||||
switch e := event.(type) {
|
||||
case *sessionlogout.BackChannelLogoutRegisteredEvent:
|
||||
wm.reduceRegistered(e)
|
||||
case *sessionlogout.BackChannelLogoutSentEvent:
|
||||
wm.reduceSent(e)
|
||||
}
|
||||
}
|
||||
return wm.WriteModel.Reduce()
|
||||
}
|
||||
|
||||
func (wm *SessionLogoutWriteModel) Query() *eventstore.SearchQueryBuilder {
|
||||
query := eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent).
|
||||
AddQuery().
|
||||
AggregateTypes(sessionlogout.AggregateType).
|
||||
AggregateIDs(wm.AggregateID).
|
||||
EventTypes(
|
||||
sessionlogout.BackChannelLogoutRegisteredType,
|
||||
sessionlogout.BackChannelLogoutSentType,
|
||||
).
|
||||
EventData(map[string]interface{}{
|
||||
"oidc_session_id": wm.OIDCSessionID,
|
||||
}).
|
||||
Builder()
|
||||
return query
|
||||
}
|
||||
|
||||
func (wm *SessionLogoutWriteModel) reduceRegistered(e *sessionlogout.BackChannelLogoutRegisteredEvent) {
|
||||
if wm.OIDCSessionID != e.OIDCSessionID {
|
||||
return
|
||||
}
|
||||
wm.UserID = e.UserID
|
||||
wm.ClientID = e.ClientID
|
||||
wm.BackChannelLogoutURI = e.BackChannelLogoutURI
|
||||
}
|
||||
|
||||
func (wm *SessionLogoutWriteModel) reduceSent(e *sessionlogout.BackChannelLogoutSentEvent) {
|
||||
if wm.OIDCSessionID != e.OIDCSessionID {
|
||||
return
|
||||
}
|
||||
wm.BackChannelLogoutSent = true
|
||||
}
|
@@ -18,6 +18,7 @@ import (
|
||||
"github.com/zitadel/zitadel/internal/id"
|
||||
"github.com/zitadel/zitadel/internal/repository/authrequest"
|
||||
"github.com/zitadel/zitadel/internal/repository/oidcsession"
|
||||
"github.com/zitadel/zitadel/internal/repository/sessionlogout"
|
||||
"github.com/zitadel/zitadel/internal/repository/user"
|
||||
"github.com/zitadel/zitadel/internal/telemetry/tracing"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
@@ -133,7 +134,8 @@ func (c *Commands) CreateOIDCSessionFromAuthRequest(ctx context.Context, authReq
|
||||
func (c *Commands) CreateOIDCSession(ctx context.Context,
|
||||
userID,
|
||||
resourceOwner,
|
||||
clientID string,
|
||||
clientID,
|
||||
backChannelLogoutURI string,
|
||||
scope,
|
||||
audience []string,
|
||||
authMethods []domain.UserAuthMethodType,
|
||||
@@ -161,6 +163,7 @@ func (c *Commands) CreateOIDCSession(ctx context.Context,
|
||||
}
|
||||
|
||||
cmd.AddSession(ctx, userID, resourceOwner, sessionID, clientID, audience, scope, authMethods, authTime, nonce, preferredLanguage, userAgent)
|
||||
cmd.RegisterLogout(ctx, sessionID, userID, clientID, backChannelLogoutURI)
|
||||
if err = cmd.AddAccessToken(ctx, scope, userID, resourceOwner, reason, actor); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -433,6 +436,26 @@ func (c *OIDCSessionEvents) SetAuthRequestFailed(ctx context.Context, authReques
|
||||
c.events = append(c.events, authrequest.NewFailedEvent(ctx, authRequestAggregate, domain.OIDCErrorReasonFromError(err)))
|
||||
}
|
||||
|
||||
func (c *OIDCSessionEvents) RegisterLogout(ctx context.Context, sessionID, userID, clientID, backChannelLogoutURI string) {
|
||||
// If there's no SSO session (e.g. service accounts) we do not need to register a logout handler.
|
||||
// Also, if the client did not register a backchannel_logout_uri it will not support it (https://openid.net/specs/openid-connect-backchannel-1_0.html#BCRegistration)
|
||||
if sessionID == "" || backChannelLogoutURI == "" {
|
||||
return
|
||||
}
|
||||
if !authz.GetFeatures(ctx).EnableBackChannelLogout {
|
||||
return
|
||||
}
|
||||
|
||||
c.events = append(c.events, sessionlogout.NewBackChannelLogoutRegisteredEvent(
|
||||
ctx,
|
||||
&sessionlogout.NewAggregate(sessionID, authz.GetInstance(ctx).InstanceID()).Aggregate,
|
||||
c.oidcSessionWriteModel.AggregateID,
|
||||
userID,
|
||||
clientID,
|
||||
backChannelLogoutURI,
|
||||
))
|
||||
}
|
||||
|
||||
func (c *OIDCSessionEvents) AddAccessToken(ctx context.Context, scope []string, userID, resourceOwner string, reason domain.TokenReason, actor *domain.TokenActor) error {
|
||||
accessTokenID, err := c.idGenerator.Next()
|
||||
if err != nil {
|
||||
|
@@ -24,6 +24,7 @@ import (
|
||||
"github.com/zitadel/zitadel/internal/repository/authrequest"
|
||||
"github.com/zitadel/zitadel/internal/repository/oidcsession"
|
||||
"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"
|
||||
)
|
||||
@@ -732,21 +733,22 @@ func TestCommands_CreateOIDCSession(t *testing.T) {
|
||||
checkPermission domain.PermissionCheck
|
||||
}
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
userID string
|
||||
resourceOwner string
|
||||
clientID string
|
||||
audience []string
|
||||
scope []string
|
||||
authMethods []domain.UserAuthMethodType
|
||||
authTime time.Time
|
||||
nonce string
|
||||
preferredLanguage *language.Tag
|
||||
userAgent *domain.UserAgent
|
||||
reason domain.TokenReason
|
||||
actor *domain.TokenActor
|
||||
needRefreshToken bool
|
||||
sessionID string
|
||||
ctx context.Context
|
||||
userID string
|
||||
resourceOwner string
|
||||
clientID string
|
||||
backChannelLogoutURI string
|
||||
audience []string
|
||||
scope []string
|
||||
authMethods []domain.UserAuthMethodType
|
||||
authTime time.Time
|
||||
nonce string
|
||||
preferredLanguage *language.Tag
|
||||
userAgent *domain.UserAgent
|
||||
reason domain.TokenReason
|
||||
actor *domain.TokenActor
|
||||
needRefreshToken bool
|
||||
sessionID string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -763,16 +765,17 @@ func TestCommands_CreateOIDCSession(t *testing.T) {
|
||||
),
|
||||
},
|
||||
args: args{
|
||||
ctx: authz.WithInstanceID(context.Background(), "instanceID"),
|
||||
userID: "userID",
|
||||
resourceOwner: "orgID",
|
||||
clientID: "clientID",
|
||||
audience: []string{"audience"},
|
||||
scope: []string{"openid", "offline_access"},
|
||||
authMethods: []domain.UserAuthMethodType{domain.UserAuthMethodTypePassword},
|
||||
authTime: testNow,
|
||||
nonce: "nonce",
|
||||
preferredLanguage: &language.Afrikaans,
|
||||
ctx: authz.WithInstanceID(context.Background(), "instanceID"),
|
||||
userID: "userID",
|
||||
resourceOwner: "orgID",
|
||||
clientID: "clientID",
|
||||
backChannelLogoutURI: "backChannelLogoutURI",
|
||||
audience: []string{"audience"},
|
||||
scope: []string{"openid", "offline_access"},
|
||||
authMethods: []domain.UserAuthMethodType{domain.UserAuthMethodTypePassword},
|
||||
authTime: testNow,
|
||||
nonce: "nonce",
|
||||
preferredLanguage: &language.Afrikaans,
|
||||
userAgent: &domain.UserAgent{
|
||||
FingerprintID: gu.Ptr("fp1"),
|
||||
IP: net.ParseIP("1.2.3.4"),
|
||||
@@ -1236,6 +1239,308 @@ func TestCommands_CreateOIDCSession(t *testing.T) {
|
||||
SessionID: "sessionID",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "with backChannelLogoutURI",
|
||||
fields: fields{
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(
|
||||
user.NewHumanAddedEvent(
|
||||
context.Background(),
|
||||
&user.NewAggregate("userID", "org1").Aggregate,
|
||||
"username",
|
||||
"firstname",
|
||||
"lastname",
|
||||
"nickname",
|
||||
"displayname",
|
||||
language.Afrikaans,
|
||||
domain.GenderUnspecified,
|
||||
"email",
|
||||
false,
|
||||
),
|
||||
),
|
||||
expectFilter(), // token lifetime
|
||||
expectPush(
|
||||
oidcsession.NewAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "org1").Aggregate,
|
||||
"userID", "org1", "", "clientID", []string{"audience"}, []string{"openid", "offline_access"},
|
||||
[]domain.UserAuthMethodType{domain.UserAuthMethodTypePassword}, testNow, "nonce", &language.Afrikaans,
|
||||
&domain.UserAgent{
|
||||
FingerprintID: gu.Ptr("fp1"),
|
||||
IP: net.ParseIP("1.2.3.4"),
|
||||
Description: gu.Ptr("firefox"),
|
||||
Header: http.Header{"foo": []string{"bar"}},
|
||||
},
|
||||
),
|
||||
oidcsession.NewAccessTokenAddedEvent(context.Background(),
|
||||
&oidcsession.NewAggregate("V2_oidcSessionID", "org1").Aggregate,
|
||||
"at_accessTokenID", []string{"openid", "offline_access"}, time.Hour, domain.TokenReasonAuthRequest,
|
||||
&domain.TokenActor{
|
||||
UserID: "user2",
|
||||
Issuer: "foo.com",
|
||||
},
|
||||
),
|
||||
user.NewUserTokenV2AddedEvent(context.Background(), &user.NewAggregate("userID", "org1").Aggregate, "at_accessTokenID"),
|
||||
),
|
||||
),
|
||||
idGenerator: mock.NewIDGeneratorExpectIDs(t, "oidcSessionID", "accessTokenID"),
|
||||
defaultAccessTokenLifetime: time.Hour,
|
||||
defaultRefreshTokenLifetime: 7 * 24 * time.Hour,
|
||||
defaultRefreshTokenIdleLifetime: 24 * time.Hour,
|
||||
keyAlgorithm: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
|
||||
},
|
||||
args: args{
|
||||
ctx: authz.WithInstanceID(context.Background(), "instanceID"),
|
||||
userID: "userID",
|
||||
resourceOwner: "org1",
|
||||
clientID: "clientID",
|
||||
backChannelLogoutURI: "backChannelLogoutURI",
|
||||
audience: []string{"audience"},
|
||||
scope: []string{"openid", "offline_access"},
|
||||
authMethods: []domain.UserAuthMethodType{domain.UserAuthMethodTypePassword},
|
||||
authTime: testNow,
|
||||
nonce: "nonce",
|
||||
preferredLanguage: &language.Afrikaans,
|
||||
userAgent: &domain.UserAgent{
|
||||
FingerprintID: gu.Ptr("fp1"),
|
||||
IP: net.ParseIP("1.2.3.4"),
|
||||
Description: gu.Ptr("firefox"),
|
||||
Header: http.Header{"foo": []string{"bar"}},
|
||||
},
|
||||
reason: domain.TokenReasonAuthRequest,
|
||||
actor: &domain.TokenActor{
|
||||
UserID: "user2",
|
||||
Issuer: "foo.com",
|
||||
},
|
||||
needRefreshToken: false,
|
||||
},
|
||||
want: &OIDCSession{
|
||||
TokenID: "V2_oidcSessionID-at_accessTokenID",
|
||||
ClientID: "clientID",
|
||||
UserID: "userID",
|
||||
Audience: []string{"audience"},
|
||||
Expiration: time.Time{}.Add(time.Hour),
|
||||
Scope: []string{"openid", "offline_access"},
|
||||
AuthMethods: []domain.UserAuthMethodType{domain.UserAuthMethodTypePassword},
|
||||
AuthTime: testNow,
|
||||
Nonce: "nonce",
|
||||
PreferredLanguage: &language.Afrikaans,
|
||||
UserAgent: &domain.UserAgent{
|
||||
FingerprintID: gu.Ptr("fp1"),
|
||||
IP: net.ParseIP("1.2.3.4"),
|
||||
Description: gu.Ptr("firefox"),
|
||||
Header: http.Header{"foo": []string{"bar"}},
|
||||
},
|
||||
Reason: domain.TokenReasonAuthRequest,
|
||||
Actor: &domain.TokenActor{
|
||||
UserID: "user2",
|
||||
Issuer: "foo.com",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "with backChannelLogoutURI and sessionID",
|
||||
fields: fields{
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(
|
||||
user.NewHumanAddedEvent(
|
||||
context.Background(),
|
||||
&user.NewAggregate("userID", "org1").Aggregate,
|
||||
"username",
|
||||
"firstname",
|
||||
"lastname",
|
||||
"nickname",
|
||||
"displayname",
|
||||
language.Afrikaans,
|
||||
domain.GenderUnspecified,
|
||||
"email",
|
||||
false,
|
||||
),
|
||||
),
|
||||
expectFilter(), // token lifetime
|
||||
expectPush(
|
||||
oidcsession.NewAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "org1").Aggregate,
|
||||
"userID", "org1", "sessionID", "clientID", []string{"audience"}, []string{"openid", "offline_access"},
|
||||
[]domain.UserAuthMethodType{domain.UserAuthMethodTypePassword}, testNow, "nonce", &language.Afrikaans,
|
||||
&domain.UserAgent{
|
||||
FingerprintID: gu.Ptr("fp1"),
|
||||
IP: net.ParseIP("1.2.3.4"),
|
||||
Description: gu.Ptr("firefox"),
|
||||
Header: http.Header{"foo": []string{"bar"}},
|
||||
},
|
||||
),
|
||||
oidcsession.NewAccessTokenAddedEvent(context.Background(),
|
||||
&oidcsession.NewAggregate("V2_oidcSessionID", "org1").Aggregate,
|
||||
"at_accessTokenID", []string{"openid", "offline_access"}, time.Hour, domain.TokenReasonAuthRequest,
|
||||
&domain.TokenActor{
|
||||
UserID: "user2",
|
||||
Issuer: "foo.com",
|
||||
},
|
||||
),
|
||||
user.NewUserTokenV2AddedEvent(context.Background(), &user.NewAggregate("userID", "org1").Aggregate, "at_accessTokenID"),
|
||||
),
|
||||
),
|
||||
idGenerator: mock.NewIDGeneratorExpectIDs(t, "oidcSessionID", "accessTokenID"),
|
||||
defaultAccessTokenLifetime: time.Hour,
|
||||
defaultRefreshTokenLifetime: 7 * 24 * time.Hour,
|
||||
defaultRefreshTokenIdleLifetime: 24 * time.Hour,
|
||||
keyAlgorithm: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
|
||||
},
|
||||
args: args{
|
||||
ctx: authz.WithInstanceID(context.Background(), "instanceID"),
|
||||
userID: "userID",
|
||||
resourceOwner: "org1",
|
||||
clientID: "clientID",
|
||||
backChannelLogoutURI: "backChannelLogoutURI",
|
||||
audience: []string{"audience"},
|
||||
scope: []string{"openid", "offline_access"},
|
||||
authMethods: []domain.UserAuthMethodType{domain.UserAuthMethodTypePassword},
|
||||
authTime: testNow,
|
||||
nonce: "nonce",
|
||||
preferredLanguage: &language.Afrikaans,
|
||||
userAgent: &domain.UserAgent{
|
||||
FingerprintID: gu.Ptr("fp1"),
|
||||
IP: net.ParseIP("1.2.3.4"),
|
||||
Description: gu.Ptr("firefox"),
|
||||
Header: http.Header{"foo": []string{"bar"}},
|
||||
},
|
||||
reason: domain.TokenReasonAuthRequest,
|
||||
actor: &domain.TokenActor{
|
||||
UserID: "user2",
|
||||
Issuer: "foo.com",
|
||||
},
|
||||
needRefreshToken: false,
|
||||
sessionID: "sessionID",
|
||||
},
|
||||
want: &OIDCSession{
|
||||
TokenID: "V2_oidcSessionID-at_accessTokenID",
|
||||
ClientID: "clientID",
|
||||
UserID: "userID",
|
||||
Audience: []string{"audience"},
|
||||
Expiration: time.Time{}.Add(time.Hour),
|
||||
Scope: []string{"openid", "offline_access"},
|
||||
AuthMethods: []domain.UserAuthMethodType{domain.UserAuthMethodTypePassword},
|
||||
AuthTime: testNow,
|
||||
Nonce: "nonce",
|
||||
PreferredLanguage: &language.Afrikaans,
|
||||
UserAgent: &domain.UserAgent{
|
||||
FingerprintID: gu.Ptr("fp1"),
|
||||
IP: net.ParseIP("1.2.3.4"),
|
||||
Description: gu.Ptr("firefox"),
|
||||
Header: http.Header{"foo": []string{"bar"}},
|
||||
},
|
||||
Reason: domain.TokenReasonAuthRequest,
|
||||
Actor: &domain.TokenActor{
|
||||
UserID: "user2",
|
||||
Issuer: "foo.com",
|
||||
},
|
||||
SessionID: "sessionID",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "with backChannelLogoutURI and sessionID, backchannel logout enabled",
|
||||
fields: fields{
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(
|
||||
user.NewHumanAddedEvent(
|
||||
context.Background(),
|
||||
&user.NewAggregate("userID", "org1").Aggregate,
|
||||
"username",
|
||||
"firstname",
|
||||
"lastname",
|
||||
"nickname",
|
||||
"displayname",
|
||||
language.Afrikaans,
|
||||
domain.GenderUnspecified,
|
||||
"email",
|
||||
false,
|
||||
),
|
||||
),
|
||||
expectFilter(), // token lifetime
|
||||
expectPush(
|
||||
oidcsession.NewAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "org1").Aggregate,
|
||||
"userID", "org1", "sessionID", "clientID", []string{"audience"}, []string{"openid", "offline_access"},
|
||||
[]domain.UserAuthMethodType{domain.UserAuthMethodTypePassword}, testNow, "nonce", &language.Afrikaans,
|
||||
&domain.UserAgent{
|
||||
FingerprintID: gu.Ptr("fp1"),
|
||||
IP: net.ParseIP("1.2.3.4"),
|
||||
Description: gu.Ptr("firefox"),
|
||||
Header: http.Header{"foo": []string{"bar"}},
|
||||
},
|
||||
),
|
||||
sessionlogout.NewBackChannelLogoutRegisteredEvent(context.Background(),
|
||||
&sessionlogout.NewAggregate("sessionID", "instanceID").Aggregate,
|
||||
"V2_oidcSessionID",
|
||||
"userID",
|
||||
"clientID",
|
||||
"backChannelLogoutURI",
|
||||
),
|
||||
oidcsession.NewAccessTokenAddedEvent(context.Background(),
|
||||
&oidcsession.NewAggregate("V2_oidcSessionID", "org1").Aggregate,
|
||||
"at_accessTokenID", []string{"openid", "offline_access"}, time.Hour, domain.TokenReasonAuthRequest,
|
||||
&domain.TokenActor{
|
||||
UserID: "user2",
|
||||
Issuer: "foo.com",
|
||||
},
|
||||
),
|
||||
user.NewUserTokenV2AddedEvent(context.Background(), &user.NewAggregate("userID", "org1").Aggregate, "at_accessTokenID"),
|
||||
),
|
||||
),
|
||||
idGenerator: mock.NewIDGeneratorExpectIDs(t, "oidcSessionID", "accessTokenID"),
|
||||
defaultAccessTokenLifetime: time.Hour,
|
||||
defaultRefreshTokenLifetime: 7 * 24 * time.Hour,
|
||||
defaultRefreshTokenIdleLifetime: 24 * time.Hour,
|
||||
keyAlgorithm: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
|
||||
},
|
||||
args: args{
|
||||
ctx: authz.WithFeatures(authz.WithInstanceID(context.Background(), "instanceID"), feature.Features{EnableBackChannelLogout: true}),
|
||||
userID: "userID",
|
||||
resourceOwner: "org1",
|
||||
clientID: "clientID",
|
||||
backChannelLogoutURI: "backChannelLogoutURI",
|
||||
audience: []string{"audience"},
|
||||
scope: []string{"openid", "offline_access"},
|
||||
authMethods: []domain.UserAuthMethodType{domain.UserAuthMethodTypePassword},
|
||||
authTime: testNow,
|
||||
nonce: "nonce",
|
||||
preferredLanguage: &language.Afrikaans,
|
||||
userAgent: &domain.UserAgent{
|
||||
FingerprintID: gu.Ptr("fp1"),
|
||||
IP: net.ParseIP("1.2.3.4"),
|
||||
Description: gu.Ptr("firefox"),
|
||||
Header: http.Header{"foo": []string{"bar"}},
|
||||
},
|
||||
reason: domain.TokenReasonAuthRequest,
|
||||
actor: &domain.TokenActor{
|
||||
UserID: "user2",
|
||||
Issuer: "foo.com",
|
||||
},
|
||||
needRefreshToken: false,
|
||||
sessionID: "sessionID",
|
||||
},
|
||||
want: &OIDCSession{
|
||||
TokenID: "V2_oidcSessionID-at_accessTokenID",
|
||||
ClientID: "clientID",
|
||||
UserID: "userID",
|
||||
Audience: []string{"audience"},
|
||||
Expiration: time.Time{}.Add(time.Hour),
|
||||
Scope: []string{"openid", "offline_access"},
|
||||
AuthMethods: []domain.UserAuthMethodType{domain.UserAuthMethodTypePassword},
|
||||
AuthTime: testNow,
|
||||
Nonce: "nonce",
|
||||
PreferredLanguage: &language.Afrikaans,
|
||||
UserAgent: &domain.UserAgent{
|
||||
FingerprintID: gu.Ptr("fp1"),
|
||||
IP: net.ParseIP("1.2.3.4"),
|
||||
Description: gu.Ptr("firefox"),
|
||||
Header: http.Header{"foo": []string{"bar"}},
|
||||
},
|
||||
Reason: domain.TokenReasonAuthRequest,
|
||||
Actor: &domain.TokenActor{
|
||||
UserID: "user2",
|
||||
Issuer: "foo.com",
|
||||
},
|
||||
SessionID: "sessionID",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "impersonation not allowed",
|
||||
fields: fields{
|
||||
@@ -1412,6 +1717,7 @@ func TestCommands_CreateOIDCSession(t *testing.T) {
|
||||
tt.args.userID,
|
||||
tt.args.resourceOwner,
|
||||
tt.args.clientID,
|
||||
tt.args.backChannelLogoutURI,
|
||||
tt.args.scope,
|
||||
tt.args.audience,
|
||||
tt.args.authMethods,
|
||||
|
@@ -31,6 +31,7 @@ type addOIDCApp struct {
|
||||
ClockSkew time.Duration
|
||||
AdditionalOrigins []string
|
||||
SkipSuccessPageForNativeApp bool
|
||||
BackChannelLogoutURI string
|
||||
|
||||
ClientID string
|
||||
ClientSecret string
|
||||
@@ -108,6 +109,7 @@ func (c *Commands) AddOIDCAppCommand(app *addOIDCApp) preparation.Validation {
|
||||
app.ClockSkew,
|
||||
trimStringSliceWhiteSpaces(app.AdditionalOrigins),
|
||||
app.SkipSuccessPageForNativeApp,
|
||||
app.BackChannelLogoutURI,
|
||||
),
|
||||
}, nil
|
||||
}, nil
|
||||
@@ -199,6 +201,7 @@ func (c *Commands) addOIDCApplicationWithID(ctx context.Context, oidcApp *domain
|
||||
oidcApp.ClockSkew,
|
||||
trimStringSliceWhiteSpaces(oidcApp.AdditionalOrigins),
|
||||
oidcApp.SkipNativeAppSuccessPage,
|
||||
strings.TrimSpace(oidcApp.BackChannelLogoutURI),
|
||||
))
|
||||
|
||||
addedApplication.AppID = oidcApp.AppID
|
||||
@@ -256,6 +259,7 @@ func (c *Commands) ChangeOIDCApplication(ctx context.Context, oidc *domain.OIDCA
|
||||
oidc.ClockSkew,
|
||||
trimStringSliceWhiteSpaces(oidc.AdditionalOrigins),
|
||||
oidc.SkipNativeAppSuccessPage,
|
||||
strings.TrimSpace(oidc.BackChannelLogoutURI),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@@ -36,6 +36,7 @@ type OIDCApplicationWriteModel struct {
|
||||
State domain.AppState
|
||||
AdditionalOrigins []string
|
||||
SkipNativeAppSuccessPage bool
|
||||
BackChannelLogoutURI string
|
||||
oidc bool
|
||||
}
|
||||
|
||||
@@ -165,6 +166,7 @@ func (wm *OIDCApplicationWriteModel) appendAddOIDCEvent(e *project.OIDCConfigAdd
|
||||
wm.ClockSkew = e.ClockSkew
|
||||
wm.AdditionalOrigins = e.AdditionalOrigins
|
||||
wm.SkipNativeAppSuccessPage = e.SkipNativeAppSuccessPage
|
||||
wm.BackChannelLogoutURI = e.BackChannelLogoutURI
|
||||
}
|
||||
|
||||
func (wm *OIDCApplicationWriteModel) appendChangeOIDCEvent(e *project.OIDCConfigChangedEvent) {
|
||||
@@ -213,6 +215,9 @@ func (wm *OIDCApplicationWriteModel) appendChangeOIDCEvent(e *project.OIDCConfig
|
||||
if e.SkipNativeAppSuccessPage != nil {
|
||||
wm.SkipNativeAppSuccessPage = *e.SkipNativeAppSuccessPage
|
||||
}
|
||||
if e.BackChannelLogoutURI != nil {
|
||||
wm.BackChannelLogoutURI = *e.BackChannelLogoutURI
|
||||
}
|
||||
}
|
||||
|
||||
func (wm *OIDCApplicationWriteModel) Query() *eventstore.SearchQueryBuilder {
|
||||
@@ -254,6 +259,7 @@ func (wm *OIDCApplicationWriteModel) NewChangedEvent(
|
||||
clockSkew time.Duration,
|
||||
additionalOrigins []string,
|
||||
skipNativeAppSuccessPage bool,
|
||||
backChannelLogoutURI string,
|
||||
) (*project.OIDCConfigChangedEvent, bool, error) {
|
||||
changes := make([]project.OIDCConfigChanges, 0)
|
||||
var err error
|
||||
@@ -303,6 +309,9 @@ func (wm *OIDCApplicationWriteModel) NewChangedEvent(
|
||||
if wm.SkipNativeAppSuccessPage != skipNativeAppSuccessPage {
|
||||
changes = append(changes, project.ChangeSkipNativeAppSuccessPage(skipNativeAppSuccessPage))
|
||||
}
|
||||
if wm.BackChannelLogoutURI != backChannelLogoutURI {
|
||||
changes = append(changes, project.ChangeBackChannelLogoutURI(backChannelLogoutURI))
|
||||
}
|
||||
|
||||
if len(changes) == 0 {
|
||||
return nil, false, nil
|
||||
|
@@ -175,6 +175,7 @@ func TestAddOIDCApp(t *testing.T) {
|
||||
0,
|
||||
[]string{"https://sub.test.ch"},
|
||||
false,
|
||||
"",
|
||||
),
|
||||
},
|
||||
},
|
||||
@@ -240,6 +241,7 @@ func TestAddOIDCApp(t *testing.T) {
|
||||
0,
|
||||
nil,
|
||||
false,
|
||||
"",
|
||||
),
|
||||
},
|
||||
},
|
||||
@@ -305,6 +307,7 @@ func TestAddOIDCApp(t *testing.T) {
|
||||
0,
|
||||
nil,
|
||||
false,
|
||||
"",
|
||||
),
|
||||
},
|
||||
},
|
||||
@@ -370,6 +373,7 @@ func TestAddOIDCApp(t *testing.T) {
|
||||
0,
|
||||
nil,
|
||||
false,
|
||||
"",
|
||||
),
|
||||
},
|
||||
},
|
||||
@@ -516,6 +520,7 @@ func TestCommandSide_AddOIDCApplication(t *testing.T) {
|
||||
time.Second*1,
|
||||
[]string{"https://sub.test.ch"},
|
||||
true,
|
||||
"https://test.ch/backchannel",
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -543,6 +548,7 @@ func TestCommandSide_AddOIDCApplication(t *testing.T) {
|
||||
ClockSkew: time.Second * 1,
|
||||
AdditionalOrigins: []string{" https://sub.test.ch "},
|
||||
SkipNativeAppSuccessPage: true,
|
||||
BackChannelLogoutURI: " https://test.ch/backchannel ",
|
||||
},
|
||||
resourceOwner: "org1",
|
||||
},
|
||||
@@ -571,6 +577,7 @@ func TestCommandSide_AddOIDCApplication(t *testing.T) {
|
||||
ClockSkew: time.Second * 1,
|
||||
AdditionalOrigins: []string{"https://sub.test.ch"},
|
||||
SkipNativeAppSuccessPage: true,
|
||||
BackChannelLogoutURI: "https://test.ch/backchannel",
|
||||
State: domain.AppStateActive,
|
||||
Compliance: &domain.Compliance{},
|
||||
},
|
||||
@@ -614,6 +621,7 @@ func TestCommandSide_AddOIDCApplication(t *testing.T) {
|
||||
time.Second*1,
|
||||
[]string{"https://sub.test.ch"},
|
||||
true,
|
||||
"https://test.ch/backchannel",
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -641,6 +649,7 @@ func TestCommandSide_AddOIDCApplication(t *testing.T) {
|
||||
ClockSkew: time.Second * 1,
|
||||
AdditionalOrigins: []string{"https://sub.test.ch"},
|
||||
SkipNativeAppSuccessPage: true,
|
||||
BackChannelLogoutURI: "https://test.ch/backchannel",
|
||||
},
|
||||
resourceOwner: "org1",
|
||||
},
|
||||
@@ -669,6 +678,7 @@ func TestCommandSide_AddOIDCApplication(t *testing.T) {
|
||||
ClockSkew: time.Second * 1,
|
||||
AdditionalOrigins: []string{"https://sub.test.ch"},
|
||||
SkipNativeAppSuccessPage: true,
|
||||
BackChannelLogoutURI: "https://test.ch/backchannel",
|
||||
State: domain.AppStateActive,
|
||||
Compliance: &domain.Compliance{},
|
||||
},
|
||||
@@ -847,6 +857,7 @@ func TestCommandSide_ChangeOIDCApplication(t *testing.T) {
|
||||
time.Second*1,
|
||||
[]string{"https://sub.test.ch"},
|
||||
true,
|
||||
"https://test.ch/backchannel",
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -875,6 +886,7 @@ func TestCommandSide_ChangeOIDCApplication(t *testing.T) {
|
||||
ClockSkew: time.Second * 1,
|
||||
AdditionalOrigins: []string{"https://sub.test.ch"},
|
||||
SkipNativeAppSuccessPage: true,
|
||||
BackChannelLogoutURI: "https://test.ch/backchannel",
|
||||
},
|
||||
resourceOwner: "org1",
|
||||
},
|
||||
@@ -916,6 +928,7 @@ func TestCommandSide_ChangeOIDCApplication(t *testing.T) {
|
||||
time.Second*1,
|
||||
[]string{"https://sub.test.ch"},
|
||||
true,
|
||||
"https://test.ch/backchannel",
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -944,6 +957,7 @@ func TestCommandSide_ChangeOIDCApplication(t *testing.T) {
|
||||
ClockSkew: time.Second * 1,
|
||||
AdditionalOrigins: []string{" https://sub.test.ch "},
|
||||
SkipNativeAppSuccessPage: true,
|
||||
BackChannelLogoutURI: " https://test.ch/backchannel ",
|
||||
},
|
||||
resourceOwner: "org1",
|
||||
},
|
||||
@@ -985,6 +999,7 @@ func TestCommandSide_ChangeOIDCApplication(t *testing.T) {
|
||||
time.Second*1,
|
||||
[]string{"https://sub.test.ch"},
|
||||
true,
|
||||
"https://test.ch/backchannel",
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -1019,6 +1034,7 @@ func TestCommandSide_ChangeOIDCApplication(t *testing.T) {
|
||||
ClockSkew: time.Second * 2,
|
||||
AdditionalOrigins: []string{"https://sub.test.ch"},
|
||||
SkipNativeAppSuccessPage: true,
|
||||
BackChannelLogoutURI: "https://test.ch/backchannel",
|
||||
},
|
||||
resourceOwner: "org1",
|
||||
},
|
||||
@@ -1046,6 +1062,7 @@ func TestCommandSide_ChangeOIDCApplication(t *testing.T) {
|
||||
ClockSkew: time.Second * 2,
|
||||
AdditionalOrigins: []string{"https://sub.test.ch"},
|
||||
SkipNativeAppSuccessPage: true,
|
||||
BackChannelLogoutURI: "https://test.ch/backchannel",
|
||||
Compliance: &domain.Compliance{},
|
||||
State: domain.AppStateActive,
|
||||
},
|
||||
@@ -1170,6 +1187,7 @@ func TestCommandSide_ChangeOIDCApplicationSecret(t *testing.T) {
|
||||
time.Second*1,
|
||||
[]string{"https://sub.test.ch"},
|
||||
false,
|
||||
"",
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -1213,6 +1231,7 @@ func TestCommandSide_ChangeOIDCApplicationSecret(t *testing.T) {
|
||||
ClockSkew: time.Second * 1,
|
||||
AdditionalOrigins: []string{"https://sub.test.ch"},
|
||||
SkipNativeAppSuccessPage: false,
|
||||
BackChannelLogoutURI: "",
|
||||
State: domain.AppStateActive,
|
||||
},
|
||||
},
|
||||
@@ -1327,6 +1346,7 @@ func TestCommands_VerifyOIDCClientSecret(t *testing.T) {
|
||||
time.Second*1,
|
||||
[]string{"https://sub.test.ch"},
|
||||
false,
|
||||
"",
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -1362,6 +1382,7 @@ func TestCommands_VerifyOIDCClientSecret(t *testing.T) {
|
||||
time.Second*1,
|
||||
[]string{"https://sub.test.ch"},
|
||||
false,
|
||||
"",
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -1396,6 +1417,7 @@ func TestCommands_VerifyOIDCClientSecret(t *testing.T) {
|
||||
time.Second*1,
|
||||
[]string{"https://sub.test.ch"},
|
||||
false,
|
||||
"",
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@@ -47,6 +47,7 @@ func oidcWriteModelToOIDCConfig(writeModel *OIDCApplicationWriteModel) *domain.O
|
||||
ClockSkew: writeModel.ClockSkew,
|
||||
AdditionalOrigins: writeModel.AdditionalOrigins,
|
||||
SkipNativeAppSuccessPage: writeModel.SkipNativeAppSuccessPage,
|
||||
BackChannelLogoutURI: writeModel.BackChannelLogoutURI,
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -19,6 +19,7 @@ type SystemFeatures struct {
|
||||
ImprovedPerformance []feature.ImprovedPerformanceType
|
||||
OIDCSingleV1SessionTermination *bool
|
||||
DisableUserTokenEvent *bool
|
||||
EnableBackChannelLogout *bool
|
||||
}
|
||||
|
||||
func (m *SystemFeatures) isEmpty() bool {
|
||||
@@ -31,7 +32,8 @@ func (m *SystemFeatures) isEmpty() bool {
|
||||
// nil check to allow unset improvements
|
||||
m.ImprovedPerformance == nil &&
|
||||
m.OIDCSingleV1SessionTermination == nil &&
|
||||
m.DisableUserTokenEvent == nil
|
||||
m.DisableUserTokenEvent == nil &&
|
||||
m.EnableBackChannelLogout == nil
|
||||
}
|
||||
|
||||
func (c *Commands) SetSystemFeatures(ctx context.Context, f *SystemFeatures) (*domain.ObjectDetails, error) {
|
||||
|
@@ -62,6 +62,7 @@ func (m *SystemFeaturesWriteModel) Query() *eventstore.SearchQueryBuilder {
|
||||
feature_v2.SystemImprovedPerformanceEventType,
|
||||
feature_v2.SystemOIDCSingleV1SessionTerminationEventType,
|
||||
feature_v2.SystemDisableUserTokenEvent,
|
||||
feature_v2.SystemEnableBackChannelLogout,
|
||||
).
|
||||
Builder().ResourceOwner(m.ResourceOwner)
|
||||
}
|
||||
@@ -100,6 +101,9 @@ func reduceSystemFeature(features *SystemFeatures, key feature.Key, value any) {
|
||||
case feature.KeyDisableUserTokenEvent:
|
||||
v := value.(bool)
|
||||
features.DisableUserTokenEvent = &v
|
||||
case feature.KeyEnableBackChannelLogout:
|
||||
v := value.(bool)
|
||||
features.EnableBackChannelLogout = &v
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,6 +119,7 @@ func (wm *SystemFeaturesWriteModel) setCommands(ctx context.Context, f *SystemFe
|
||||
cmds = appendFeatureSliceUpdate(ctx, cmds, aggregate, wm.ImprovedPerformance, f.ImprovedPerformance, feature_v2.SystemImprovedPerformanceEventType)
|
||||
cmds = appendFeatureUpdate(ctx, cmds, aggregate, wm.OIDCSingleV1SessionTermination, f.OIDCSingleV1SessionTermination, feature_v2.SystemOIDCSingleV1SessionTerminationEventType)
|
||||
cmds = appendFeatureUpdate(ctx, cmds, aggregate, wm.DisableUserTokenEvent, f.DisableUserTokenEvent, feature_v2.SystemDisableUserTokenEvent)
|
||||
cmds = appendFeatureUpdate(ctx, cmds, aggregate, wm.EnableBackChannelLogout, f.EnableBackChannelLogout, feature_v2.SystemEnableBackChannelLogout)
|
||||
return cmds
|
||||
}
|
||||
|
||||
|
@@ -628,16 +628,21 @@ func createAddHumanEvent(ctx context.Context, aggregate *eventstore.Aggregate, h
|
||||
return addEvent
|
||||
}
|
||||
|
||||
func (c *Commands) HumansSignOut(ctx context.Context, agentID string, userIDs []string) error {
|
||||
type HumanSignOutSession struct {
|
||||
ID string
|
||||
UserID string
|
||||
}
|
||||
|
||||
func (c *Commands) HumansSignOut(ctx context.Context, agentID string, sessions []HumanSignOutSession) error {
|
||||
if agentID == "" {
|
||||
return zerrors.ThrowInvalidArgument(nil, "COMMAND-2M0ds", "Errors.User.UserIDMissing")
|
||||
}
|
||||
if len(userIDs) == 0 {
|
||||
if len(sessions) == 0 {
|
||||
return zerrors.ThrowInvalidArgument(nil, "COMMAND-M0od3", "Errors.User.UserIDMissing")
|
||||
}
|
||||
events := make([]eventstore.Command, 0)
|
||||
for _, userID := range userIDs {
|
||||
existingUser, err := c.getHumanWriteModelByID(ctx, userID, "")
|
||||
for _, session := range sessions {
|
||||
existingUser, err := c.getHumanWriteModelByID(ctx, session.UserID, "")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -647,7 +652,9 @@ func (c *Commands) HumansSignOut(ctx context.Context, agentID string, userIDs []
|
||||
events = append(events, user.NewHumanSignedOutEvent(
|
||||
ctx,
|
||||
UserAggregateFromWriteModel(&existingUser.WriteModel),
|
||||
agentID))
|
||||
agentID,
|
||||
session.ID,
|
||||
))
|
||||
}
|
||||
if len(events) == 0 {
|
||||
return nil
|
||||
|
@@ -3123,9 +3123,9 @@ func TestCommandSide_HumanSignOut(t *testing.T) {
|
||||
}
|
||||
type (
|
||||
args struct {
|
||||
ctx context.Context
|
||||
agentID string
|
||||
userIDs []string
|
||||
ctx context.Context
|
||||
agentID string
|
||||
sessions []HumanSignOutSession
|
||||
}
|
||||
)
|
||||
type res struct {
|
||||
@@ -3144,9 +3144,9 @@ func TestCommandSide_HumanSignOut(t *testing.T) {
|
||||
eventstore: expectEventstore(),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
agentID: "",
|
||||
userIDs: []string{"user1"},
|
||||
ctx: context.Background(),
|
||||
agentID: "",
|
||||
sessions: []HumanSignOutSession{{ID: "session1", UserID: "user1"}},
|
||||
},
|
||||
res: res{
|
||||
err: zerrors.IsErrorInvalidArgument,
|
||||
@@ -3158,9 +3158,9 @@ func TestCommandSide_HumanSignOut(t *testing.T) {
|
||||
eventstore: expectEventstore(),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
agentID: "agent1",
|
||||
userIDs: []string{},
|
||||
ctx: context.Background(),
|
||||
agentID: "agent1",
|
||||
sessions: []HumanSignOutSession{},
|
||||
},
|
||||
res: res{
|
||||
err: zerrors.IsErrorInvalidArgument,
|
||||
@@ -3174,9 +3174,9 @@ func TestCommandSide_HumanSignOut(t *testing.T) {
|
||||
),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
agentID: "agent1",
|
||||
userIDs: []string{"user1"},
|
||||
ctx: context.Background(),
|
||||
agentID: "agent1",
|
||||
sessions: []HumanSignOutSession{{ID: "session1", UserID: "user1"}},
|
||||
},
|
||||
res: res{},
|
||||
},
|
||||
@@ -3204,14 +3204,15 @@ func TestCommandSide_HumanSignOut(t *testing.T) {
|
||||
user.NewHumanSignedOutEvent(context.Background(),
|
||||
&user.NewAggregate("user1", "org1").Aggregate,
|
||||
"agent1",
|
||||
"session1",
|
||||
),
|
||||
),
|
||||
),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
agentID: "agent1",
|
||||
userIDs: []string{"user1"},
|
||||
ctx: context.Background(),
|
||||
agentID: "agent1",
|
||||
sessions: []HumanSignOutSession{{ID: "session1", UserID: "user1"}},
|
||||
},
|
||||
res: res{
|
||||
want: &domain.ObjectDetails{
|
||||
@@ -3259,18 +3260,20 @@ func TestCommandSide_HumanSignOut(t *testing.T) {
|
||||
user.NewHumanSignedOutEvent(context.Background(),
|
||||
&user.NewAggregate("user1", "org1").Aggregate,
|
||||
"agent1",
|
||||
"session1",
|
||||
),
|
||||
user.NewHumanSignedOutEvent(context.Background(),
|
||||
&user.NewAggregate("user2", "org1").Aggregate,
|
||||
"agent1",
|
||||
"session2",
|
||||
),
|
||||
),
|
||||
),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
agentID: "agent1",
|
||||
userIDs: []string{"user1", "user2"},
|
||||
ctx: context.Background(),
|
||||
agentID: "agent1",
|
||||
sessions: []HumanSignOutSession{{ID: "session1", UserID: "user1"}, {ID: "session2", UserID: "user2"}},
|
||||
},
|
||||
res: res{
|
||||
want: &domain.ObjectDetails{
|
||||
@@ -3284,7 +3287,7 @@ func TestCommandSide_HumanSignOut(t *testing.T) {
|
||||
r := &Commands{
|
||||
eventstore: tt.fields.eventstore(t),
|
||||
}
|
||||
err := r.HumansSignOut(tt.args.ctx, tt.args.agentID, tt.args.userIDs)
|
||||
err := r.HumansSignOut(tt.args.ctx, tt.args.agentID, tt.args.sessions)
|
||||
if tt.res.err == nil {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
Reference in New Issue
Block a user