feat: specify login UI version on instance and apps (#9071)

# Which Problems Are Solved

To be able to migrate or test the new login UI, admins might want to
(temporarily) switch individual apps.
At a later point admin might want to make sure all applications use the
new login UI.

# How the Problems Are Solved

- Added a feature flag `` on instance level to require all apps to use
the new login and provide an optional base url.
- if the flag is enabled, all (OIDC) applications will automatically use
the v2 login.
  - if disabled, applications can decide based on their configuration
- Added an option on OIDC apps to use the new login UI and an optional
base url.
- Removed the requirement to use `x-zitadel-login-client` to be
redirected to the login V2 and retrieve created authrequest and link
them to SSO sessions.
- Added a new "IAM_LOGIN_CLIENT" role to allow management of users,
sessions, grants and more without `x-zitadel-login-client`.

# Additional Changes

None

# Additional Context

closes https://github.com/zitadel/zitadel/issues/8702
This commit is contained in:
Livio Spring
2024-12-19 10:37:46 +01:00
committed by GitHub
parent b5e92a6144
commit 50d2b26a28
89 changed files with 1670 additions and 321 deletions

View File

@@ -92,7 +92,9 @@ func (c *Commands) LinkSessionToAuthRequest(ctx context.Context, id, sessionID,
return nil, nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-Sx208nt", "Errors.AuthRequest.AlreadyHandled")
}
if checkLoginClient && authz.GetCtxData(ctx).UserID != writeModel.LoginClient {
return nil, nil, zerrors.ThrowPermissionDenied(nil, "COMMAND-rai9Y", "Errors.AuthRequest.WrongLoginClient")
if err := c.checkPermission(ctx, domain.PermissionSessionLink, writeModel.ResourceOwner, ""); err != nil {
return nil, nil, err
}
}
sessionWriteModel := NewSessionWriteModel(sessionID, authz.GetInstance(ctx).InstanceID())
err = c.eventstore.FilterToQueryReducer(ctx, sessionWriteModel)

View File

@@ -25,7 +25,7 @@ import (
func TestCommands_AddAuthRequest(t *testing.T) {
mockCtx := authz.NewMockContext("instanceID", "orgID", "loginClient")
type fields struct {
eventstore *eventstore.Eventstore
eventstore func(*testing.T) *eventstore.Eventstore
idGenerator id.Generator
}
type args struct {
@@ -42,7 +42,7 @@ func TestCommands_AddAuthRequest(t *testing.T) {
{
"already exists error",
fields{
eventstore: eventstoreExpect(t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
authrequest.NewAddedEvent(mockCtx, &authrequest.NewAggregate("V2_id", "instanceID").Aggregate,
@@ -78,7 +78,7 @@ func TestCommands_AddAuthRequest(t *testing.T) {
{
"added",
fields{
eventstore: eventstoreExpect(t,
eventstore: expectEventstore(
expectFilter(),
expectPush(
authrequest.NewAddedEvent(mockCtx, &authrequest.NewAggregate("V2_id", "instanceID").Aggregate,
@@ -158,7 +158,7 @@ func TestCommands_AddAuthRequest(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := &Commands{
eventstore: tt.fields.eventstore,
eventstore: tt.fields.eventstore(t),
idGenerator: tt.fields.idGenerator,
}
got, err := c.AddAuthRequest(tt.args.ctx, tt.args.request)
@@ -171,8 +171,9 @@ func TestCommands_AddAuthRequest(t *testing.T) {
func TestCommands_LinkSessionToAuthRequest(t *testing.T) {
mockCtx := authz.NewMockContext("instanceID", "orgID", "loginClient")
type fields struct {
eventstore *eventstore.Eventstore
tokenVerifier func(ctx context.Context, sessionToken, sessionID, tokenID string) (err error)
eventstore func(*testing.T) *eventstore.Eventstore
tokenVerifier func(ctx context.Context, sessionToken, sessionID, tokenID string) (err error)
checkPermission domain.PermissionCheck
}
type args struct {
ctx context.Context
@@ -195,7 +196,7 @@ func TestCommands_LinkSessionToAuthRequest(t *testing.T) {
{
"authRequest not found",
fields{
eventstore: eventstoreExpect(t,
eventstore: expectEventstore(
expectFilter(),
),
tokenVerifier: newMockTokenVerifierValid(),
@@ -212,7 +213,7 @@ func TestCommands_LinkSessionToAuthRequest(t *testing.T) {
{
"authRequest not existing",
fields{
eventstore: eventstoreExpect(t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
authrequest.NewAddedEvent(mockCtx, &authrequest.NewAggregate("id", "instanceID").Aggregate,
@@ -252,9 +253,9 @@ func TestCommands_LinkSessionToAuthRequest(t *testing.T) {
},
},
{
"wrong login client",
"wrong login client / not permitted",
fields{
eventstore: eventstoreExpect(t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
authrequest.NewAddedEvent(mockCtx, &authrequest.NewAggregate("id", "instanceID").Aggregate,
@@ -278,7 +279,8 @@ func TestCommands_LinkSessionToAuthRequest(t *testing.T) {
),
),
),
tokenVerifier: newMockTokenVerifierValid(),
tokenVerifier: newMockTokenVerifierValid(),
checkPermission: newMockPermissionCheckNotAllowed(),
},
args{
ctx: authz.NewMockContext("instanceID", "orgID", "wrongLoginClient"),
@@ -288,13 +290,13 @@ func TestCommands_LinkSessionToAuthRequest(t *testing.T) {
checkLoginClient: true,
},
res{
wantErr: zerrors.ThrowPermissionDenied(nil, "COMMAND-rai9Y", "Errors.AuthRequest.WrongLoginClient"),
wantErr: zerrors.ThrowPermissionDenied(nil, "AUTHZ-HKJD33", "Errors.PermissionDenied"),
},
},
{
"session not existing",
fields{
eventstore: eventstoreExpect(t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
authrequest.NewAddedEvent(mockCtx, &authrequest.NewAggregate("V2_id", "instanceID").Aggregate,
@@ -333,7 +335,7 @@ func TestCommands_LinkSessionToAuthRequest(t *testing.T) {
{
"session expired",
fields{
eventstore: eventstoreExpect(t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
authrequest.NewAddedEvent(mockCtx, &authrequest.NewAggregate("V2_id", "instanceID").Aggregate,
@@ -395,7 +397,7 @@ func TestCommands_LinkSessionToAuthRequest(t *testing.T) {
{
"invalid session token",
fields{
eventstore: eventstoreExpect(t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
authrequest.NewAddedEvent(mockCtx, &authrequest.NewAggregate("V2_id", "instanceID").Aggregate,
@@ -446,7 +448,7 @@ func TestCommands_LinkSessionToAuthRequest(t *testing.T) {
{
"linked",
fields{
eventstore: eventstoreExpect(t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
authrequest.NewAddedEvent(mockCtx, &authrequest.NewAggregate("V2_id", "instanceID").Aggregate,
@@ -534,7 +536,7 @@ func TestCommands_LinkSessionToAuthRequest(t *testing.T) {
{
"linked with login client check",
fields{
eventstore: eventstoreExpect(t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
authrequest.NewAddedEvent(mockCtx, &authrequest.NewAggregate("V2_id", "instanceID").Aggregate,
@@ -620,12 +622,103 @@ func TestCommands_LinkSessionToAuthRequest(t *testing.T) {
},
},
},
{
"linked with permission",
fields{
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
authrequest.NewAddedEvent(mockCtx, &authrequest.NewAggregate("V2_id", "instanceID").Aggregate,
"otherLoginClient",
"clientID",
"redirectURI",
"state",
"nonce",
[]string{"openid"},
[]string{"audience"},
domain.OIDCResponseTypeCode,
domain.OIDCResponseModeQuery,
nil,
nil,
nil,
nil,
nil,
nil,
true,
),
),
),
expectFilter(
eventFromEventPusher(
session.NewAddedEvent(mockCtx,
&session.NewAggregate("sessionID", "instance1").Aggregate,
&domain.UserAgent{
FingerprintID: gu.Ptr("fp1"),
IP: net.ParseIP("1.2.3.4"),
Description: gu.Ptr("firefox"),
Header: http.Header{"foo": []string{"bar"}},
},
)),
eventFromEventPusher(
session.NewUserCheckedEvent(mockCtx, &session.NewAggregate("sessionID", "instance1").Aggregate,
"userID", "org1", testNow, &language.Afrikaans),
),
eventFromEventPusher(
session.NewPasswordCheckedEvent(mockCtx, &session.NewAggregate("sessionID", "instance1").Aggregate,
testNow),
),
eventFromEventPusherWithCreationDateNow(
session.NewLifetimeSetEvent(mockCtx, &session.NewAggregate("sessionID", "instance1").Aggregate,
2*time.Minute),
),
),
expectPush(
authrequest.NewSessionLinkedEvent(mockCtx, &authrequest.NewAggregate("V2_id", "instanceID").Aggregate,
"sessionID",
"userID",
testNow,
[]domain.UserAuthMethodType{domain.UserAuthMethodTypePassword},
),
),
),
tokenVerifier: newMockTokenVerifierValid(),
checkPermission: newMockPermissionCheckAllowed(),
},
args{
ctx: authz.NewMockContext("instanceID", "orgID", "loginClient"),
id: "V2_id",
sessionID: "sessionID",
sessionToken: "token",
checkLoginClient: true,
},
res{
details: &domain.ObjectDetails{ResourceOwner: "instanceID"},
authReq: &CurrentAuthRequest{
AuthRequest: &AuthRequest{
ID: "V2_id",
LoginClient: "otherLoginClient",
ClientID: "clientID",
RedirectURI: "redirectURI",
State: "state",
Nonce: "nonce",
Scope: []string{"openid"},
Audience: []string{"audience"},
ResponseType: domain.OIDCResponseTypeCode,
ResponseMode: domain.OIDCResponseModeQuery,
},
SessionID: "sessionID",
UserID: "userID",
AuthMethods: []domain.UserAuthMethodType{domain.UserAuthMethodTypePassword},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := &Commands{
eventstore: tt.fields.eventstore,
eventstore: tt.fields.eventstore(t),
sessionTokenVerifier: tt.fields.tokenVerifier,
checkPermission: tt.fields.checkPermission,
}
details, got, err := c.LinkSessionToAuthRequest(tt.args.ctx, tt.args.id, tt.args.sessionID, tt.args.sessionToken, tt.args.checkLoginClient)
require.ErrorIs(t, err, tt.res.wantErr)
@@ -642,7 +735,7 @@ func TestCommands_LinkSessionToAuthRequest(t *testing.T) {
func TestCommands_FailAuthRequest(t *testing.T) {
mockCtx := authz.NewMockContext("instanceID", "orgID", "loginClient")
type fields struct {
eventstore *eventstore.Eventstore
eventstore func(*testing.T) *eventstore.Eventstore
}
type args struct {
ctx context.Context
@@ -663,7 +756,7 @@ func TestCommands_FailAuthRequest(t *testing.T) {
{
"authRequest not existing",
fields{
eventstore: eventstoreExpect(t,
eventstore: expectEventstore(
expectFilter(),
),
},
@@ -679,7 +772,7 @@ func TestCommands_FailAuthRequest(t *testing.T) {
{
"failed",
fields{
eventstore: eventstoreExpect(t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
authrequest.NewAddedEvent(mockCtx, &authrequest.NewAggregate("V2_id", "instanceID").Aggregate,
@@ -735,7 +828,7 @@ func TestCommands_FailAuthRequest(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := &Commands{
eventstore: tt.fields.eventstore,
eventstore: tt.fields.eventstore(t),
}
details, got, err := c.FailAuthRequest(tt.args.ctx, tt.args.id, tt.args.reason)
require.ErrorIs(t, err, tt.res.wantErr)
@@ -748,7 +841,7 @@ func TestCommands_FailAuthRequest(t *testing.T) {
func TestCommands_AddAuthRequestCode(t *testing.T) {
mockCtx := authz.NewMockContext("instanceID", "orgID", "loginClient")
type fields struct {
eventstore *eventstore.Eventstore
eventstore func(*testing.T) *eventstore.Eventstore
}
type args struct {
ctx context.Context
@@ -764,7 +857,7 @@ func TestCommands_AddAuthRequestCode(t *testing.T) {
{
"empty code error",
fields{
eventstore: eventstoreExpect(t),
eventstore: expectEventstore(),
},
args{
ctx: mockCtx,
@@ -776,7 +869,7 @@ func TestCommands_AddAuthRequestCode(t *testing.T) {
{
"no session linked error",
fields{
eventstore: eventstoreExpect(t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
authrequest.NewAddedEvent(mockCtx, &authrequest.NewAggregate("V2_authRequestID", "instanceID").Aggregate,
@@ -814,7 +907,7 @@ func TestCommands_AddAuthRequestCode(t *testing.T) {
{
"success",
fields{
eventstore: eventstoreExpect(t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
authrequest.NewAddedEvent(mockCtx, &authrequest.NewAggregate("V2_authRequestID", "instanceID").Aggregate,
@@ -864,7 +957,7 @@ func TestCommands_AddAuthRequestCode(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := &Commands{
eventstore: tt.fields.eventstore,
eventstore: tt.fields.eventstore(t),
}
err := c.AddAuthRequestCode(tt.args.ctx, tt.args.id, tt.args.code)
assert.ErrorIs(t, tt.wantErr, err)

View File

@@ -156,6 +156,8 @@ func TestCommandSide_AddInstanceDomain(t *testing.T) {
[]string{"https://sub.test.ch"},
false,
"",
domain.LoginVersionUnspecified,
"",
),
),
),

View File

@@ -28,6 +28,7 @@ type InstanceFeatures struct {
OIDCSingleV1SessionTermination *bool
DisableUserTokenEvent *bool
EnableBackChannelLogout *bool
LoginV2 *feature.LoginV2
}
func (m *InstanceFeatures) isEmpty() bool {
@@ -43,7 +44,8 @@ func (m *InstanceFeatures) isEmpty() bool {
m.DebugOIDCParentError == nil &&
m.OIDCSingleV1SessionTermination == nil &&
m.DisableUserTokenEvent == nil &&
m.EnableBackChannelLogout == nil
m.EnableBackChannelLogout == nil &&
m.LoginV2 == nil
}
func (c *Commands) SetInstanceFeatures(ctx context.Context, f *InstanceFeatures) (*domain.ObjectDetails, error) {

View File

@@ -41,6 +41,12 @@ func (m *InstanceFeaturesWriteModel) Reduce() (err error) {
return err
}
reduceInstanceFeature(&m.InstanceFeatures, key, e.Value)
case *feature_v2.SetEvent[*feature.LoginV2]:
_, key, err := e.FeatureInfo()
if err != nil {
return err
}
reduceInstanceFeature(&m.InstanceFeatures, key, e.Value)
case *feature_v2.SetEvent[[]feature.ImprovedPerformanceType]:
_, key, err := e.FeatureInfo()
if err != nil {
@@ -72,6 +78,7 @@ func (m *InstanceFeaturesWriteModel) Query() *eventstore.SearchQueryBuilder {
feature_v2.InstanceOIDCSingleV1SessionTerminationEventType,
feature_v2.InstanceDisableUserTokenEvent,
feature_v2.InstanceEnableBackChannelLogout,
feature_v2.InstanceLoginVersion,
).
Builder().ResourceOwner(m.ResourceOwner)
}
@@ -120,6 +127,8 @@ func reduceInstanceFeature(features *InstanceFeatures, key feature.Key, value an
case feature.KeyEnableBackChannelLogout:
v := value.(bool)
features.EnableBackChannelLogout = &v
case feature.KeyLoginV2:
features.LoginV2 = value.(*feature.LoginV2)
}
}
@@ -138,5 +147,6 @@ func (wm *InstanceFeaturesWriteModel) setCommands(ctx context.Context, f *Instan
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)
cmds = appendFeatureUpdate(ctx, cmds, aggregate, wm.LoginV2, f.LoginV2, feature_v2.InstanceLoginVersion)
return cmds
}

View File

@@ -128,6 +128,8 @@ func oidcAppEvents(ctx context.Context, orgID, projectID, id, name, clientID str
nil,
false,
"",
domain.LoginVersionUnspecified,
"",
),
}
}
@@ -441,6 +443,8 @@ func generatedDomainFilters(instanceID, orgID, projectID, appID, generatedDomain
nil,
false,
"",
domain.LoginVersionUnspecified,
"",
),
),
expectFilter(

View File

@@ -32,6 +32,8 @@ type addOIDCApp struct {
AdditionalOrigins []string
SkipSuccessPageForNativeApp bool
BackChannelLogoutURI string
LoginVersion domain.LoginVersion
LoginBaseURI string
ClientID string
ClientSecret string
@@ -110,6 +112,8 @@ func (c *Commands) AddOIDCAppCommand(app *addOIDCApp) preparation.Validation {
trimStringSliceWhiteSpaces(app.AdditionalOrigins),
app.SkipSuccessPageForNativeApp,
app.BackChannelLogoutURI,
app.LoginVersion,
app.LoginBaseURI,
),
}, nil
}, nil
@@ -202,6 +206,8 @@ func (c *Commands) addOIDCApplicationWithID(ctx context.Context, oidcApp *domain
trimStringSliceWhiteSpaces(oidcApp.AdditionalOrigins),
oidcApp.SkipNativeAppSuccessPage,
strings.TrimSpace(oidcApp.BackChannelLogoutURI),
oidcApp.LoginVersion,
strings.TrimSpace(oidcApp.LoginBaseURI),
))
addedApplication.AppID = oidcApp.AppID
@@ -260,6 +266,8 @@ func (c *Commands) ChangeOIDCApplication(ctx context.Context, oidc *domain.OIDCA
trimStringSliceWhiteSpaces(oidc.AdditionalOrigins),
oidc.SkipNativeAppSuccessPage,
strings.TrimSpace(oidc.BackChannelLogoutURI),
oidc.LoginVersion,
strings.TrimSpace(oidc.LoginBaseURI),
)
if err != nil {
return nil, err

View File

@@ -37,6 +37,8 @@ type OIDCApplicationWriteModel struct {
AdditionalOrigins []string
SkipNativeAppSuccessPage bool
BackChannelLogoutURI string
LoginVersion domain.LoginVersion
LoginBaseURI string
oidc bool
}
@@ -167,6 +169,8 @@ func (wm *OIDCApplicationWriteModel) appendAddOIDCEvent(e *project.OIDCConfigAdd
wm.AdditionalOrigins = e.AdditionalOrigins
wm.SkipNativeAppSuccessPage = e.SkipNativeAppSuccessPage
wm.BackChannelLogoutURI = e.BackChannelLogoutURI
wm.LoginVersion = e.LoginVersion
wm.LoginBaseURI = e.LoginBaseURI
}
func (wm *OIDCApplicationWriteModel) appendChangeOIDCEvent(e *project.OIDCConfigChangedEvent) {
@@ -218,6 +222,12 @@ func (wm *OIDCApplicationWriteModel) appendChangeOIDCEvent(e *project.OIDCConfig
if e.BackChannelLogoutURI != nil {
wm.BackChannelLogoutURI = *e.BackChannelLogoutURI
}
if e.LoginVersion != nil {
wm.LoginVersion = *e.LoginVersion
}
if e.LoginBaseURI != nil {
wm.LoginBaseURI = *e.LoginBaseURI
}
}
func (wm *OIDCApplicationWriteModel) Query() *eventstore.SearchQueryBuilder {
@@ -260,6 +270,8 @@ func (wm *OIDCApplicationWriteModel) NewChangedEvent(
additionalOrigins []string,
skipNativeAppSuccessPage bool,
backChannelLogoutURI string,
loginVersion domain.LoginVersion,
loginBaseURI string,
) (*project.OIDCConfigChangedEvent, bool, error) {
changes := make([]project.OIDCConfigChanges, 0)
var err error
@@ -312,6 +324,12 @@ func (wm *OIDCApplicationWriteModel) NewChangedEvent(
if wm.BackChannelLogoutURI != backChannelLogoutURI {
changes = append(changes, project.ChangeBackChannelLogoutURI(backChannelLogoutURI))
}
if wm.LoginVersion != loginVersion {
changes = append(changes, project.ChangeLoginVersion(loginVersion))
}
if wm.LoginBaseURI != loginBaseURI {
changes = append(changes, project.ChangeLoginBaseURI(loginBaseURI))
}
if len(changes) == 0 {
return nil, false, nil

View File

@@ -176,6 +176,8 @@ func TestAddOIDCApp(t *testing.T) {
[]string{"https://sub.test.ch"},
false,
"",
domain.LoginVersionUnspecified,
"",
),
},
},
@@ -242,6 +244,8 @@ func TestAddOIDCApp(t *testing.T) {
nil,
false,
"",
domain.LoginVersionUnspecified,
"",
),
},
},
@@ -308,6 +312,8 @@ func TestAddOIDCApp(t *testing.T) {
nil,
false,
"",
domain.LoginVersionUnspecified,
"",
),
},
},
@@ -374,6 +380,8 @@ func TestAddOIDCApp(t *testing.T) {
nil,
false,
"",
domain.LoginVersionUnspecified,
"",
),
},
},
@@ -521,6 +529,8 @@ func TestCommandSide_AddOIDCApplication(t *testing.T) {
[]string{"https://sub.test.ch"},
true,
"https://test.ch/backchannel",
domain.LoginVersion2,
"https://login.test.ch",
),
),
),
@@ -549,6 +559,8 @@ func TestCommandSide_AddOIDCApplication(t *testing.T) {
AdditionalOrigins: []string{" https://sub.test.ch "},
SkipNativeAppSuccessPage: true,
BackChannelLogoutURI: " https://test.ch/backchannel ",
LoginVersion: domain.LoginVersion2,
LoginBaseURI: " https://login.test.ch ",
},
resourceOwner: "org1",
},
@@ -578,6 +590,8 @@ func TestCommandSide_AddOIDCApplication(t *testing.T) {
AdditionalOrigins: []string{"https://sub.test.ch"},
SkipNativeAppSuccessPage: true,
BackChannelLogoutURI: "https://test.ch/backchannel",
LoginVersion: domain.LoginVersion2,
LoginBaseURI: "https://login.test.ch",
State: domain.AppStateActive,
Compliance: &domain.Compliance{},
},
@@ -622,6 +636,8 @@ func TestCommandSide_AddOIDCApplication(t *testing.T) {
[]string{"https://sub.test.ch"},
true,
"https://test.ch/backchannel",
domain.LoginVersion2,
"https://login.test.ch",
),
),
),
@@ -650,6 +666,8 @@ func TestCommandSide_AddOIDCApplication(t *testing.T) {
AdditionalOrigins: []string{"https://sub.test.ch"},
SkipNativeAppSuccessPage: true,
BackChannelLogoutURI: "https://test.ch/backchannel",
LoginVersion: domain.LoginVersion2,
LoginBaseURI: "https://login.test.ch",
},
resourceOwner: "org1",
},
@@ -679,6 +697,8 @@ func TestCommandSide_AddOIDCApplication(t *testing.T) {
AdditionalOrigins: []string{"https://sub.test.ch"},
SkipNativeAppSuccessPage: true,
BackChannelLogoutURI: "https://test.ch/backchannel",
LoginVersion: domain.LoginVersion2,
LoginBaseURI: "https://login.test.ch",
State: domain.AppStateActive,
Compliance: &domain.Compliance{},
},
@@ -712,7 +732,7 @@ func TestCommandSide_AddOIDCApplication(t *testing.T) {
func TestCommandSide_ChangeOIDCApplication(t *testing.T) {
type fields struct {
eventstore *eventstore.Eventstore
eventstore func(*testing.T) *eventstore.Eventstore
}
type args struct {
ctx context.Context
@@ -732,9 +752,7 @@ func TestCommandSide_ChangeOIDCApplication(t *testing.T) {
{
name: "invalid app, invalid argument error",
fields: fields{
eventstore: eventstoreExpect(
t,
),
eventstore: expectEventstore(),
},
args: args{
ctx: context.Background(),
@@ -753,9 +771,7 @@ func TestCommandSide_ChangeOIDCApplication(t *testing.T) {
{
name: "missing appid, invalid argument error",
fields: fields{
eventstore: eventstoreExpect(
t,
),
eventstore: expectEventstore(),
},
args: args{
ctx: context.Background(),
@@ -777,9 +793,7 @@ func TestCommandSide_ChangeOIDCApplication(t *testing.T) {
{
name: "missing aggregateid, invalid argument error",
fields: fields{
eventstore: eventstoreExpect(
t,
),
eventstore: expectEventstore(),
},
args: args{
ctx: context.Background(),
@@ -801,8 +815,7 @@ func TestCommandSide_ChangeOIDCApplication(t *testing.T) {
{
name: "app not existing, not found error",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(),
),
},
@@ -826,8 +839,7 @@ func TestCommandSide_ChangeOIDCApplication(t *testing.T) {
{
name: "no changes, precondition error",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
project.NewApplicationAddedEvent(context.Background(),
@@ -858,6 +870,8 @@ func TestCommandSide_ChangeOIDCApplication(t *testing.T) {
[]string{"https://sub.test.ch"},
true,
"https://test.ch/backchannel",
domain.LoginVersion2,
"https://login.test.ch",
),
),
),
@@ -887,6 +901,8 @@ func TestCommandSide_ChangeOIDCApplication(t *testing.T) {
AdditionalOrigins: []string{"https://sub.test.ch"},
SkipNativeAppSuccessPage: true,
BackChannelLogoutURI: "https://test.ch/backchannel",
LoginVersion: domain.LoginVersion2,
LoginBaseURI: "https://login.test.ch",
},
resourceOwner: "org1",
},
@@ -897,8 +913,7 @@ func TestCommandSide_ChangeOIDCApplication(t *testing.T) {
{
name: "no changes whitespaces are ignored, precondition error",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
project.NewApplicationAddedEvent(context.Background(),
@@ -929,6 +944,8 @@ func TestCommandSide_ChangeOIDCApplication(t *testing.T) {
[]string{"https://sub.test.ch"},
true,
"https://test.ch/backchannel",
domain.LoginVersion2,
"https://login.test.ch",
),
),
),
@@ -958,6 +975,8 @@ func TestCommandSide_ChangeOIDCApplication(t *testing.T) {
AdditionalOrigins: []string{" https://sub.test.ch "},
SkipNativeAppSuccessPage: true,
BackChannelLogoutURI: " https://test.ch/backchannel ",
LoginVersion: domain.LoginVersion2,
LoginBaseURI: " https://login.test.ch ",
},
resourceOwner: "org1",
},
@@ -968,8 +987,7 @@ func TestCommandSide_ChangeOIDCApplication(t *testing.T) {
{
name: "change oidc app, ok",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
project.NewApplicationAddedEvent(context.Background(),
@@ -1000,6 +1018,8 @@ func TestCommandSide_ChangeOIDCApplication(t *testing.T) {
[]string{"https://sub.test.ch"},
true,
"https://test.ch/backchannel",
domain.LoginVersion1,
"",
),
),
),
@@ -1035,6 +1055,8 @@ func TestCommandSide_ChangeOIDCApplication(t *testing.T) {
AdditionalOrigins: []string{"https://sub.test.ch"},
SkipNativeAppSuccessPage: true,
BackChannelLogoutURI: "https://test.ch/backchannel",
LoginVersion: domain.LoginVersion2,
LoginBaseURI: "https://login.test.ch",
},
resourceOwner: "org1",
},
@@ -1063,6 +1085,8 @@ func TestCommandSide_ChangeOIDCApplication(t *testing.T) {
AdditionalOrigins: []string{"https://sub.test.ch"},
SkipNativeAppSuccessPage: true,
BackChannelLogoutURI: "https://test.ch/backchannel",
LoginVersion: domain.LoginVersion2,
LoginBaseURI: "https://login.test.ch",
Compliance: &domain.Compliance{},
State: domain.AppStateActive,
},
@@ -1072,7 +1096,7 @@ func TestCommandSide_ChangeOIDCApplication(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := &Commands{
eventstore: tt.fields.eventstore,
eventstore: tt.fields.eventstore(t),
}
got, err := r.ChangeOIDCApplication(tt.args.ctx, tt.args.oidcApp, tt.args.resourceOwner)
if tt.res.err == nil {
@@ -1188,6 +1212,8 @@ func TestCommandSide_ChangeOIDCApplicationSecret(t *testing.T) {
[]string{"https://sub.test.ch"},
false,
"",
domain.LoginVersionUnspecified,
"",
),
),
),
@@ -1232,6 +1258,7 @@ func TestCommandSide_ChangeOIDCApplicationSecret(t *testing.T) {
AdditionalOrigins: []string{"https://sub.test.ch"},
SkipNativeAppSuccessPage: false,
BackChannelLogoutURI: "",
LoginVersion: domain.LoginVersionUnspecified,
State: domain.AppStateActive,
},
},
@@ -1270,6 +1297,8 @@ func newOIDCAppChangedEvent(ctx context.Context, appID, projectID, resourceOwner
project.ChangeIDTokenRoleAssertion(false),
project.ChangeIDTokenUserinfoAssertion(false),
project.ChangeClockSkew(time.Second * 2),
project.ChangeLoginVersion(domain.LoginVersion2),
project.ChangeLoginBaseURI("https://login.test.ch"),
}
event, _ := project.NewOIDCConfigChangedEvent(ctx,
&project.NewAggregate(projectID, resourceOwner).Aggregate,
@@ -1347,6 +1376,8 @@ func TestCommands_VerifyOIDCClientSecret(t *testing.T) {
[]string{"https://sub.test.ch"},
false,
"",
domain.LoginVersionUnspecified,
"",
),
),
),
@@ -1383,6 +1414,8 @@ func TestCommands_VerifyOIDCClientSecret(t *testing.T) {
[]string{"https://sub.test.ch"},
false,
"",
domain.LoginVersionUnspecified,
"",
),
),
),
@@ -1418,6 +1451,8 @@ func TestCommands_VerifyOIDCClientSecret(t *testing.T) {
[]string{"https://sub.test.ch"},
false,
"",
domain.LoginVersionUnspecified,
"",
),
),
),

View File

@@ -48,6 +48,8 @@ func oidcWriteModelToOIDCConfig(writeModel *OIDCApplicationWriteModel) *domain.O
AdditionalOrigins: writeModel.AdditionalOrigins,
SkipNativeAppSuccessPage: writeModel.SkipNativeAppSuccessPage,
BackChannelLogoutURI: writeModel.BackChannelLogoutURI,
LoginVersion: writeModel.LoginVersion,
LoginBaseURI: writeModel.LoginBaseURI,
}
}

View File

@@ -20,6 +20,7 @@ type SystemFeatures struct {
OIDCSingleV1SessionTermination *bool
DisableUserTokenEvent *bool
EnableBackChannelLogout *bool
LoginV2 *feature.LoginV2
}
func (m *SystemFeatures) isEmpty() bool {
@@ -33,7 +34,8 @@ func (m *SystemFeatures) isEmpty() bool {
m.ImprovedPerformance == nil &&
m.OIDCSingleV1SessionTermination == nil &&
m.DisableUserTokenEvent == nil &&
m.EnableBackChannelLogout == nil
m.EnableBackChannelLogout == nil &&
m.LoginV2 == nil
}
func (c *Commands) SetSystemFeatures(ctx context.Context, f *SystemFeatures) (*domain.ObjectDetails, error) {

View File

@@ -34,6 +34,12 @@ func (m *SystemFeaturesWriteModel) Reduce() error {
return err
}
reduceSystemFeature(&m.SystemFeatures, key, e.Value)
case *feature_v2.SetEvent[*feature.LoginV2]:
_, key, err := e.FeatureInfo()
if err != nil {
return err
}
reduceSystemFeature(&m.SystemFeatures, key, e.Value)
case *feature_v2.SetEvent[[]feature.ImprovedPerformanceType]:
_, key, err := e.FeatureInfo()
if err != nil {
@@ -63,6 +69,7 @@ func (m *SystemFeaturesWriteModel) Query() *eventstore.SearchQueryBuilder {
feature_v2.SystemOIDCSingleV1SessionTerminationEventType,
feature_v2.SystemDisableUserTokenEvent,
feature_v2.SystemEnableBackChannelLogout,
feature_v2.SystemLoginVersion,
).
Builder().ResourceOwner(m.ResourceOwner)
}
@@ -104,6 +111,8 @@ func reduceSystemFeature(features *SystemFeatures, key feature.Key, value any) {
case feature.KeyEnableBackChannelLogout:
v := value.(bool)
features.EnableBackChannelLogout = &v
case feature.KeyLoginV2:
features.LoginV2 = value.(*feature.LoginV2)
}
}
@@ -120,6 +129,7 @@ func (wm *SystemFeaturesWriteModel) setCommands(ctx context.Context, f *SystemFe
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)
cmds = appendFeatureUpdate(ctx, cmds, aggregate, wm.LoginV2, f.LoginV2, feature_v2.SystemLoginVersion)
return cmds
}