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

@@ -60,6 +60,8 @@ type OIDCApp struct {
AllowedOrigins database.TextArray[string]
SkipNativeAppSuccessPage bool
BackChannelLogoutURI string
LoginVersion domain.LoginVersion
LoginBaseURI *string
}
type SAMLApp struct {
@@ -180,6 +182,10 @@ var (
name: projection.AppOIDCConfigColumnAppID,
table: appOIDCConfigsTable,
}
AppOIDCConfigColumnInstanceID = Column{
name: projection.AppOIDCConfigColumnInstanceID,
table: appOIDCConfigsTable,
}
AppOIDCConfigColumnVersion = Column{
name: projection.AppOIDCConfigColumnVersion,
table: appOIDCConfigsTable,
@@ -248,6 +254,14 @@ var (
name: projection.AppOIDCConfigColumnBackChannelLogoutURI,
table: appOIDCConfigsTable,
}
AppOIDCConfigColumnLoginVersion = Column{
name: projection.AppOIDCConfigColumnLoginVersion,
table: appOIDCConfigsTable,
}
AppOIDCConfigColumnLoginBaseURI = Column{
name: projection.AppOIDCConfigColumnLoginBaseURI,
table: appOIDCConfigsTable,
}
)
func (q *Queries) AppByProjectAndAppID(ctx context.Context, shouldTriggerBulk bool, projectID, appID string) (app *App, err error) {
@@ -501,6 +515,30 @@ func (q *Queries) SearchClientIDs(ctx context.Context, queries *AppSearchQueries
return ids, nil
}
func (q *Queries) OIDCClientLoginVersion(ctx context.Context, clientID string) (loginVersion domain.LoginVersion, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
query, scan := prepareLoginVersionByClientID(ctx, q.client)
eq := sq.Eq{
AppOIDCConfigColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(),
AppOIDCConfigColumnClientID.identifier(): clientID,
}
stmt, args, err := query.Where(eq).ToSql()
if err != nil {
return domain.LoginVersionUnspecified, zerrors.ThrowInvalidArgument(err, "QUERY-WEh31", "Errors.Query.InvalidRequest")
}
err = q.client.QueryRowContext(ctx, func(row *sql.Row) error {
loginVersion, err = scan(row)
return err
}, stmt, args...)
if err != nil {
return domain.LoginVersionUnspecified, zerrors.ThrowInternal(err, "QUERY-W2gsa", "Errors.Internal")
}
return loginVersion, nil
}
func NewAppNameSearchQuery(method TextComparison, value string) (SearchQuery, error) {
return NewTextQuery(AppColumnName, value, method)
}
@@ -542,6 +580,8 @@ func prepareAppQuery(ctx context.Context, db prepareDatabase, activeOnly bool) (
AppOIDCConfigColumnAdditionalOrigins.identifier(),
AppOIDCConfigColumnSkipNativeAppSuccessPage.identifier(),
AppOIDCConfigColumnBackChannelLogoutURI.identifier(),
AppOIDCConfigColumnLoginVersion.identifier(),
AppOIDCConfigColumnLoginBaseURI.identifier(),
AppSAMLConfigColumnAppID.identifier(),
AppSAMLConfigColumnEntityID.identifier(),
@@ -607,6 +647,8 @@ func scanApp(row *sql.Row) (*App, error) {
&oidcConfig.additionalOrigins,
&oidcConfig.skipNativeAppSuccessPage,
&oidcConfig.backChannelLogoutURI,
&oidcConfig.loginVersion,
&oidcConfig.loginBaseURI,
&samlConfig.appID,
&samlConfig.entityID,
@@ -657,6 +699,8 @@ func prepareOIDCAppQuery() (sq.SelectBuilder, func(*sql.Row) (*App, error)) {
AppOIDCConfigColumnAdditionalOrigins.identifier(),
AppOIDCConfigColumnSkipNativeAppSuccessPage.identifier(),
AppOIDCConfigColumnBackChannelLogoutURI.identifier(),
AppOIDCConfigColumnLoginVersion.identifier(),
AppOIDCConfigColumnLoginBaseURI.identifier(),
).From(appsTable.identifier()).
Join(join(AppOIDCConfigColumnAppID, AppColumnID)).
PlaceholderFormat(sq.Dollar), func(row *sql.Row) (*App, error) {
@@ -694,6 +738,8 @@ func prepareOIDCAppQuery() (sq.SelectBuilder, func(*sql.Row) (*App, error)) {
&oidcConfig.additionalOrigins,
&oidcConfig.skipNativeAppSuccessPage,
&oidcConfig.backChannelLogoutURI,
&oidcConfig.loginVersion,
&oidcConfig.loginBaseURI,
)
if err != nil {
@@ -906,6 +952,8 @@ func prepareAppsQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder
AppOIDCConfigColumnAdditionalOrigins.identifier(),
AppOIDCConfigColumnSkipNativeAppSuccessPage.identifier(),
AppOIDCConfigColumnBackChannelLogoutURI.identifier(),
AppOIDCConfigColumnLoginVersion.identifier(),
AppOIDCConfigColumnLoginBaseURI.identifier(),
AppSAMLConfigColumnAppID.identifier(),
AppSAMLConfigColumnEntityID.identifier(),
@@ -959,6 +1007,8 @@ func prepareAppsQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder
&oidcConfig.additionalOrigins,
&oidcConfig.skipNativeAppSuccessPage,
&oidcConfig.backChannelLogoutURI,
&oidcConfig.loginVersion,
&oidcConfig.loginBaseURI,
&samlConfig.appID,
&samlConfig.entityID,
@@ -1013,6 +1063,21 @@ func prepareClientIDsQuery(ctx context.Context, db prepareDatabase) (sq.SelectBu
}
}
func prepareLoginVersionByClientID(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (domain.LoginVersion, error)) {
return sq.Select(
AppOIDCConfigColumnLoginVersion.identifier(),
).From(appOIDCConfigsTable.identifier()).
PlaceholderFormat(sq.Dollar), func(row *sql.Row) (domain.LoginVersion, error) {
var loginVersion sql.NullInt16
if err := row.Scan(
&loginVersion,
); err != nil {
return domain.LoginVersionUnspecified, zerrors.ThrowInternal(err, "QUERY-KL2io", "Errors.Internal")
}
return domain.LoginVersion(loginVersion.Int16), nil
}
}
type sqlOIDCConfig struct {
appID sql.NullString
version sql.NullInt32
@@ -1032,6 +1097,8 @@ type sqlOIDCConfig struct {
grantTypes database.NumberArray[domain.OIDCGrantType]
skipNativeAppSuccessPage sql.NullBool
backChannelLogoutURI sql.NullString
loginVersion sql.NullInt16
loginBaseURI sql.NullString
}
func (c sqlOIDCConfig) set(app *App) {
@@ -1056,6 +1123,10 @@ func (c sqlOIDCConfig) set(app *App) {
GrantTypes: c.grantTypes,
SkipNativeAppSuccessPage: c.skipNativeAppSuccessPage.Bool,
BackChannelLogoutURI: c.backChannelLogoutURI.String,
LoginVersion: domain.LoginVersion(c.loginVersion.Int16),
}
if c.loginBaseURI.Valid {
app.OIDCConfig.LoginBaseURI = &c.loginBaseURI.String
}
compliance := domain.GetOIDCCompliance(app.OIDCConfig.Version, app.OIDCConfig.AppType, app.OIDCConfig.GrantTypes, app.OIDCConfig.ResponseTypes, app.OIDCConfig.AuthMethodType, app.OIDCConfig.RedirectURIs)
app.OIDCConfig.ComplianceProblems = compliance.Problems

View File

@@ -11,6 +11,7 @@ import (
"time"
sq "github.com/Masterminds/squirrel"
"github.com/muhlemmer/gu"
"github.com/zitadel/zitadel/internal/database"
"github.com/zitadel/zitadel/internal/domain"
@@ -49,6 +50,8 @@ var (
` projections.apps7_oidc_configs.additional_origins,` +
` projections.apps7_oidc_configs.skip_native_app_success_page,` +
` projections.apps7_oidc_configs.back_channel_logout_uri,` +
` projections.apps7_oidc_configs.login_version,` +
` projections.apps7_oidc_configs.login_base_uri,` +
//saml config
` projections.apps7_saml_configs.app_id,` +
` projections.apps7_saml_configs.entity_id,` +
@@ -93,6 +96,8 @@ var (
` projections.apps7_oidc_configs.additional_origins,` +
` projections.apps7_oidc_configs.skip_native_app_success_page,` +
` projections.apps7_oidc_configs.back_channel_logout_uri,` +
` projections.apps7_oidc_configs.login_version,` +
` projections.apps7_oidc_configs.login_base_uri,` +
//saml config
` projections.apps7_saml_configs.app_id,` +
` projections.apps7_saml_configs.entity_id,` +
@@ -166,6 +171,8 @@ var (
"additional_origins",
"skip_native_app_success_page",
"back_channel_logout_uri",
"login_version",
"login_base_uri",
//saml config
"app_id",
"entity_id",
@@ -238,6 +245,8 @@ func Test_AppsPrepare(t *testing.T) {
nil,
nil,
nil,
nil,
nil,
// saml config
nil,
nil,
@@ -305,6 +314,8 @@ func Test_AppsPrepare(t *testing.T) {
nil,
nil,
nil,
nil,
nil,
// saml config
nil,
nil,
@@ -375,6 +386,8 @@ func Test_AppsPrepare(t *testing.T) {
nil,
nil,
nil,
nil,
nil,
// saml config
"app-id",
"https://test.com/saml/metadata",
@@ -447,6 +460,8 @@ func Test_AppsPrepare(t *testing.T) {
database.TextArray[string]{"additional.origin"},
false,
"back.channel.logout.ch",
domain.LoginVersionUnspecified,
nil,
// saml config
nil,
nil,
@@ -490,6 +505,8 @@ func Test_AppsPrepare(t *testing.T) {
AllowedOrigins: database.TextArray[string]{"https://redirect.to", "additional.origin"},
SkipNativeAppSuccessPage: false,
BackChannelLogoutURI: "back.channel.logout.ch",
LoginVersion: domain.LoginVersionUnspecified,
LoginBaseURI: nil,
},
},
},
@@ -535,6 +552,8 @@ func Test_AppsPrepare(t *testing.T) {
database.TextArray[string]{"additional.origin"},
false,
"back.channel.logout.ch",
domain.LoginVersionUnspecified,
nil,
// saml config
nil,
nil,
@@ -578,6 +597,8 @@ func Test_AppsPrepare(t *testing.T) {
AllowedOrigins: database.TextArray[string]{"https://redirect.to", "additional.origin"},
SkipNativeAppSuccessPage: false,
BackChannelLogoutURI: "back.channel.logout.ch",
LoginVersion: domain.LoginVersionUnspecified,
LoginBaseURI: nil,
},
},
},
@@ -623,6 +644,8 @@ func Test_AppsPrepare(t *testing.T) {
database.TextArray[string]{"additional.origin"},
false,
"back.channel.logout.ch",
domain.LoginVersionUnspecified,
nil,
// saml config
nil,
nil,
@@ -666,6 +689,8 @@ func Test_AppsPrepare(t *testing.T) {
AllowedOrigins: database.TextArray[string]{"https://redirect.to", "additional.origin"},
SkipNativeAppSuccessPage: false,
BackChannelLogoutURI: "back.channel.logout.ch",
LoginVersion: domain.LoginVersionUnspecified,
LoginBaseURI: nil,
},
},
},
@@ -711,6 +736,8 @@ func Test_AppsPrepare(t *testing.T) {
database.TextArray[string]{"additional.origin"},
false,
"back.channel.logout.ch",
domain.LoginVersionUnspecified,
nil,
// saml config
nil,
nil,
@@ -754,6 +781,8 @@ func Test_AppsPrepare(t *testing.T) {
AllowedOrigins: database.TextArray[string]{"https://redirect.to", "additional.origin"},
SkipNativeAppSuccessPage: false,
BackChannelLogoutURI: "back.channel.logout.ch",
LoginVersion: domain.LoginVersionUnspecified,
LoginBaseURI: nil,
},
},
},
@@ -799,6 +828,8 @@ func Test_AppsPrepare(t *testing.T) {
database.TextArray[string]{"additional.origin"},
false,
"back.channel.logout.ch",
domain.LoginVersionUnspecified,
nil,
// saml config
nil,
nil,
@@ -842,6 +873,8 @@ func Test_AppsPrepare(t *testing.T) {
AllowedOrigins: database.TextArray[string]{"https://redirect.to", "additional.origin"},
SkipNativeAppSuccessPage: false,
BackChannelLogoutURI: "back.channel.logout.ch",
LoginVersion: domain.LoginVersionUnspecified,
LoginBaseURI: nil,
},
},
},
@@ -887,6 +920,8 @@ func Test_AppsPrepare(t *testing.T) {
database.TextArray[string]{"additional.origin"},
true,
"back.channel.logout.ch",
domain.LoginVersionUnspecified,
nil,
// saml config
nil,
nil,
@@ -930,6 +965,8 @@ func Test_AppsPrepare(t *testing.T) {
AllowedOrigins: database.TextArray[string]{"https://redirect.to", "additional.origin"},
SkipNativeAppSuccessPage: true,
BackChannelLogoutURI: "back.channel.logout.ch",
LoginVersion: domain.LoginVersionUnspecified,
LoginBaseURI: nil,
},
},
},
@@ -975,6 +1012,8 @@ func Test_AppsPrepare(t *testing.T) {
database.TextArray[string]{"additional.origin"},
false,
"back.channel.logout.ch",
domain.LoginVersion2,
"https://login.ch/",
// saml config
nil,
nil,
@@ -1013,6 +1052,8 @@ func Test_AppsPrepare(t *testing.T) {
nil,
nil,
nil,
nil,
nil,
// saml config
nil,
nil,
@@ -1051,6 +1092,8 @@ func Test_AppsPrepare(t *testing.T) {
nil,
nil,
nil,
nil,
nil,
// saml config
"saml-app-id",
"https://test.com/saml/metadata",
@@ -1094,6 +1137,8 @@ func Test_AppsPrepare(t *testing.T) {
AllowedOrigins: database.TextArray[string]{"https://redirect.to", "additional.origin"},
SkipNativeAppSuccessPage: false,
BackChannelLogoutURI: "back.channel.logout.ch",
LoginVersion: domain.LoginVersion2,
LoginBaseURI: gu.Ptr("https://login.ch/"),
},
},
{
@@ -1228,6 +1273,8 @@ func Test_AppPrepare(t *testing.T) {
nil,
nil,
nil,
nil,
nil,
// saml config
nil,
nil,
@@ -1289,6 +1336,8 @@ func Test_AppPrepare(t *testing.T) {
nil,
nil,
nil,
nil,
nil,
// saml config
nil,
nil,
@@ -1355,6 +1404,8 @@ func Test_AppPrepare(t *testing.T) {
database.TextArray[string]{"additional.origin"},
false,
"back.channel.logout.ch",
domain.LoginVersionUnspecified,
nil,
// saml config
nil,
nil,
@@ -1393,6 +1444,8 @@ func Test_AppPrepare(t *testing.T) {
AllowedOrigins: database.TextArray[string]{"https://redirect.to", "additional.origin"},
SkipNativeAppSuccessPage: false,
BackChannelLogoutURI: "back.channel.logout.ch",
LoginVersion: domain.LoginVersionUnspecified,
LoginBaseURI: nil,
},
},
},
@@ -1438,6 +1491,8 @@ func Test_AppPrepare(t *testing.T) {
database.TextArray[string]{"additional.origin"},
false,
"back.channel.logout.ch",
domain.LoginVersionUnspecified,
nil,
// saml config
nil,
nil,
@@ -1476,6 +1531,8 @@ func Test_AppPrepare(t *testing.T) {
AllowedOrigins: database.TextArray[string]{"https://redirect.to", "additional.origin"},
SkipNativeAppSuccessPage: false,
BackChannelLogoutURI: "back.channel.logout.ch",
LoginVersion: domain.LoginVersionUnspecified,
LoginBaseURI: nil,
},
},
},
@@ -1521,6 +1578,8 @@ func Test_AppPrepare(t *testing.T) {
nil,
nil,
nil,
nil,
nil,
// saml config
"app-id",
"https://test.com/saml/metadata",
@@ -1588,6 +1647,8 @@ func Test_AppPrepare(t *testing.T) {
database.TextArray[string]{"additional.origin"},
false,
"back.channel.logout.ch",
domain.LoginVersionUnspecified,
nil,
// saml config
nil,
nil,
@@ -1626,6 +1687,8 @@ func Test_AppPrepare(t *testing.T) {
AllowedOrigins: database.TextArray[string]{"https://redirect.to", "additional.origin"},
SkipNativeAppSuccessPage: false,
BackChannelLogoutURI: "back.channel.logout.ch",
LoginVersion: domain.LoginVersionUnspecified,
LoginBaseURI: nil,
},
},
},
@@ -1671,6 +1734,8 @@ func Test_AppPrepare(t *testing.T) {
database.TextArray[string]{"additional.origin"},
false,
"back.channel.logout.ch",
domain.LoginVersionUnspecified,
nil,
// saml config
nil,
nil,
@@ -1709,6 +1774,8 @@ func Test_AppPrepare(t *testing.T) {
AllowedOrigins: database.TextArray[string]{"https://redirect.to", "additional.origin"},
SkipNativeAppSuccessPage: false,
BackChannelLogoutURI: "back.channel.logout.ch",
LoginVersion: domain.LoginVersionUnspecified,
LoginBaseURI: nil,
},
},
},
@@ -1754,6 +1821,8 @@ func Test_AppPrepare(t *testing.T) {
database.TextArray[string]{"additional.origin"},
false,
"back.channel.logout.ch",
domain.LoginVersionUnspecified,
nil,
// saml config
nil,
nil,
@@ -1792,6 +1861,8 @@ func Test_AppPrepare(t *testing.T) {
AllowedOrigins: database.TextArray[string]{"https://redirect.to", "additional.origin"},
SkipNativeAppSuccessPage: false,
BackChannelLogoutURI: "back.channel.logout.ch",
LoginVersion: domain.LoginVersionUnspecified,
LoginBaseURI: nil,
},
},
},
@@ -1837,6 +1908,8 @@ func Test_AppPrepare(t *testing.T) {
database.TextArray[string]{"additional.origin"},
false,
"back.channel.logout.ch",
domain.LoginVersionUnspecified,
nil,
// saml config
nil,
nil,
@@ -1875,6 +1948,8 @@ func Test_AppPrepare(t *testing.T) {
AllowedOrigins: database.TextArray[string]{"https://redirect.to", "additional.origin"},
SkipNativeAppSuccessPage: false,
BackChannelLogoutURI: "back.channel.logout.ch",
LoginVersion: domain.LoginVersionUnspecified,
LoginBaseURI: nil,
},
},
},

View File

@@ -34,9 +34,9 @@ type AuthRequest struct {
HintUserID *string
}
func (a *AuthRequest) checkLoginClient(ctx context.Context) error {
func (a *AuthRequest) checkLoginClient(ctx context.Context, permissionCheck domain.PermissionCheck) error {
if uid := authz.GetCtxData(ctx).UserID; uid != a.LoginClient {
return zerrors.ThrowPermissionDenied(nil, "OIDCv2-aL0ag", "Errors.AuthRequest.WrongLoginClient")
return permissionCheck(ctx, domain.PermissionSessionRead, authz.GetInstance(ctx).InstanceID(), "")
}
return nil
}
@@ -89,7 +89,7 @@ func (q *Queries) AuthRequestByID(ctx context.Context, shouldTriggerBulk bool, i
dst.UiLocales = locales
if checkLoginClient {
if err = dst.checkLoginClient(ctx); err != nil {
if err = dst.checkLoginClient(ctx, q.checkPermission); err != nil {
return nil, err
}
}

View File

@@ -1,6 +1,7 @@
package query
import (
"context"
"database/sql"
"database/sql/driver"
_ "embed"
@@ -45,11 +46,12 @@ func TestQueries_AuthRequestByID(t *testing.T) {
checkLoginClient bool
}
tests := []struct {
name string
args args
expect sqlExpectation
want *AuthRequest
wantErr error
name string
args args
expect sqlExpectation
permissionCheck domain.PermissionCheck
want *AuthRequest
wantErr error
}{
{
name: "success, all values",
@@ -138,7 +140,7 @@ func TestQueries_AuthRequestByID(t *testing.T) {
wantErr: zerrors.ThrowInternal(sql.ErrConnDone, "QUERY-Ou8ue", "Errors.Internal"),
},
{
name: "wrong login client",
name: "wrong login client / not permitted",
args: args{
shouldTriggerBulk: false,
id: "123",
@@ -157,7 +159,47 @@ func TestQueries_AuthRequestByID(t *testing.T) {
nil,
nil,
}, "123", "instanceID"),
wantErr: zerrors.ThrowPermissionDeniedf(nil, "OIDCv2-aL0ag", "Errors.AuthRequest.WrongLoginClient"),
permissionCheck: func(ctx context.Context, permission, orgID, resourceID string) (err error) {
return zerrors.ThrowPermissionDenied(nil, "id", "not permitted")
},
wantErr: zerrors.ThrowPermissionDenied(nil, "id", "not permitted"),
},
{
name: "other login client / permitted",
args: args{
shouldTriggerBulk: false,
id: "123",
checkLoginClient: true,
},
expect: mockQuery(expQuery, cols, []driver.Value{
"id",
testNow,
"otherLoginClient",
"clientID",
database.TextArray[string]{"a", "b", "c"},
"example.com",
database.NumberArray[domain.Prompt]{domain.PromptLogin, domain.PromptConsent},
database.TextArray[string]{"en", "fi"},
nil,
nil,
nil,
}, "123", "instanceID"),
permissionCheck: func(ctx context.Context, permission, orgID, resourceID string) (err error) {
return nil
},
want: &AuthRequest{
ID: "id",
CreationDate: testNow,
LoginClient: "otherLoginClient",
ClientID: "clientID",
Scope: []string{"a", "b", "c"},
RedirectURI: "example.com",
Prompt: []domain.Prompt{domain.PromptLogin, domain.PromptConsent},
UiLocales: []string{"en", "fi"},
LoginHint: nil,
MaxAge: nil,
HintUserID: nil,
},
},
}
for _, tt := range tests {
@@ -168,6 +210,7 @@ func TestQueries_AuthRequestByID(t *testing.T) {
DB: db,
Database: &prepareDB{},
},
checkPermission: tt.permissionCheck,
}
ctx := authz.NewMockContext("instanceID", "orgID", "loginClient")

View File

@@ -21,6 +21,7 @@ type InstanceFeatures struct {
OIDCSingleV1SessionTermination FeatureSource[bool]
DisableUserTokenEvent FeatureSource[bool]
EnableBackChannelLogout FeatureSource[bool]
LoginV2 FeatureSource[*feature.LoginV2]
}
func (q *Queries) GetInstanceFeatures(ctx context.Context, cascade bool) (_ *InstanceFeatures, err error) {

View File

@@ -42,6 +42,8 @@ func (m *InstanceFeaturesReadModel) Reduce() (err error) {
)
case *feature_v2.SetEvent[bool]:
err = reduceInstanceFeatureSet(m.instance, e)
case *feature_v2.SetEvent[*feature.LoginV2]:
err = reduceInstanceFeatureSet(m.instance, e)
case *feature_v2.SetEvent[[]feature.ImprovedPerformanceType]:
err = reduceInstanceFeatureSet(m.instance, e)
}
@@ -72,6 +74,7 @@ func (m *InstanceFeaturesReadModel) Query() *eventstore.SearchQueryBuilder {
feature_v2.InstanceOIDCSingleV1SessionTerminationEventType,
feature_v2.InstanceDisableUserTokenEvent,
feature_v2.InstanceEnableBackChannelLogout,
feature_v2.InstanceLoginVersion,
).
Builder().ResourceOwner(m.ResourceOwner)
}
@@ -98,6 +101,7 @@ func (m *InstanceFeaturesReadModel) populateFromSystem() bool {
m.instance.OIDCSingleV1SessionTermination = m.system.OIDCSingleV1SessionTermination
m.instance.DisableUserTokenEvent = m.system.DisableUserTokenEvent
m.instance.EnableBackChannelLogout = m.system.EnableBackChannelLogout
m.instance.LoginV2 = m.system.LoginV2
return true
}
@@ -133,6 +137,8 @@ func reduceInstanceFeatureSet[T any](features *InstanceFeatures, event *feature_
features.DisableUserTokenEvent.set(level, event.Value)
case feature.KeyEnableBackChannelLogout:
features.EnableBackChannelLogout.set(level, event.Value)
case feature.KeyLoginV2:
features.LoginV2.set(level, event.Value)
}
return nil
}

View File

@@ -4,7 +4,9 @@ import (
"context"
"database/sql"
_ "embed"
"encoding/json"
"errors"
"net/url"
"time"
"github.com/zitadel/zitadel/internal/api/authz"
@@ -39,10 +41,32 @@ type OIDCClient struct {
PublicKeys map[string][]byte `json:"public_keys,omitempty"`
ProjectID string `json:"project_id,omitempty"`
ProjectRoleAssertion bool `json:"project_role_assertion,omitempty"`
LoginVersion domain.LoginVersion `json:"login_version,omitempty"`
LoginBaseURI *URL `json:"login_base_uri,omitempty"`
ProjectRoleKeys []string `json:"project_role_keys,omitempty"`
Settings *OIDCSettings `json:"settings,omitempty"`
}
type URL url.URL
func (c *URL) URL() *url.URL {
return (*url.URL)(c)
}
func (c *URL) UnmarshalJSON(src []byte) error {
var s string
err := json.Unmarshal(src, &s)
if err != nil {
return err
}
u, err := url.Parse(s)
if err != nil {
return err
}
*c = URL(*u)
return nil
}
//go:embed oidc_client_by_id.sql
var oidcClientQuery string
@@ -59,7 +83,13 @@ func (q *Queries) ActiveOIDCClientByID(ctx context.Context, clientID string, get
if err != nil {
return nil, zerrors.ThrowInternal(err, "QUERY-ieR7R", "Errors.Internal")
}
if authz.GetInstance(ctx).ConsoleClientID() == clientID {
instance := authz.GetInstance(ctx)
loginV2 := instance.Features().LoginV2
if loginV2.Required {
client.LoginVersion = domain.LoginVersion2
client.LoginBaseURI = (*URL)(loginV2.BaseURI)
}
if instance.ConsoleClientID() == clientID {
client.RedirectURIs = append(client.RedirectURIs, http_util.DomainContext(ctx).Origin()+path.RedirectPath)
client.PostLogoutRedirectURIs = append(client.PostLogoutRedirectURIs, http_util.DomainContext(ctx).Origin()+path.PostLogoutPath)
}

View File

@@ -3,7 +3,8 @@ with client as (
c.app_id, a.state, c.client_id, c.back_channel_logout_uri, c.client_secret, c.redirect_uris, c.response_types,
c.grant_types, c.application_type, c.auth_method_type, c.post_logout_redirect_uris, c.is_dev_mode,
c.access_token_type, c.access_token_role_assertion, c.id_token_role_assertion,
c.id_token_userinfo_assertion, c.clock_skew, c.additional_origins, a.project_id, p.project_role_assertion
c.id_token_userinfo_assertion, c.clock_skew, c.additional_origins, a.project_id, p.project_role_assertion,
c.login_version, c.login_base_uri
from projections.apps7_oidc_configs c
join projections.apps7 a on a.id = c.app_id and a.instance_id = c.instance_id and a.state = 1
join projections.projects4 p on p.id = a.project_id and p.instance_id = a.instance_id and p.state = 1

View File

@@ -59,6 +59,8 @@ const (
AppOIDCConfigColumnAdditionalOrigins = "additional_origins"
AppOIDCConfigColumnSkipNativeAppSuccessPage = "skip_native_app_success_page"
AppOIDCConfigColumnBackChannelLogoutURI = "back_channel_logout_uri"
AppOIDCConfigColumnLoginVersion = "login_version"
AppOIDCConfigColumnLoginBaseURI = "login_base_uri"
appSAMLTableSuffix = "saml_configs"
AppSAMLConfigColumnAppID = "app_id"
@@ -127,6 +129,8 @@ func (*appProjection) Init() *old_handler.Check {
handler.NewColumn(AppOIDCConfigColumnAdditionalOrigins, handler.ColumnTypeTextArray, handler.Nullable()),
handler.NewColumn(AppOIDCConfigColumnSkipNativeAppSuccessPage, handler.ColumnTypeBool, handler.Default(false)),
handler.NewColumn(AppOIDCConfigColumnBackChannelLogoutURI, handler.ColumnTypeText, handler.Nullable()),
handler.NewColumn(AppOIDCConfigColumnLoginVersion, handler.ColumnTypeEnum, handler.Nullable()),
handler.NewColumn(AppOIDCConfigColumnLoginBaseURI, handler.ColumnTypeText, handler.Nullable()),
},
handler.NewPrimaryKey(AppOIDCConfigColumnInstanceID, AppOIDCConfigColumnAppID),
appOIDCTableSuffix,
@@ -503,6 +507,8 @@ func (p *appProjection) reduceOIDCConfigAdded(event eventstore.Event) (*handler.
handler.NewCol(AppOIDCConfigColumnAdditionalOrigins, database.TextArray[string](e.AdditionalOrigins)),
handler.NewCol(AppOIDCConfigColumnSkipNativeAppSuccessPage, e.SkipNativeAppSuccessPage),
handler.NewCol(AppOIDCConfigColumnBackChannelLogoutURI, e.BackChannelLogoutURI),
handler.NewCol(AppOIDCConfigColumnLoginVersion, e.LoginVersion),
handler.NewCol(AppOIDCConfigColumnLoginBaseURI, e.LoginBaseURI),
},
handler.WithTableSuffix(appOIDCTableSuffix),
),
@@ -525,7 +531,7 @@ func (p *appProjection) reduceOIDCConfigChanged(event eventstore.Event) (*handle
return nil, zerrors.ThrowInvalidArgumentf(nil, "HANDL-GNHU1", "reduce.wrong.event.type %s", project.OIDCConfigChangedType)
}
cols := make([]handler.Column, 0, 16)
cols := make([]handler.Column, 0, 18)
if e.Version != nil {
cols = append(cols, handler.NewCol(AppOIDCConfigColumnVersion, *e.Version))
}
@@ -574,6 +580,12 @@ func (p *appProjection) reduceOIDCConfigChanged(event eventstore.Event) (*handle
if e.BackChannelLogoutURI != nil {
cols = append(cols, handler.NewCol(AppOIDCConfigColumnBackChannelLogoutURI, *e.BackChannelLogoutURI))
}
if e.LoginVersion != nil {
cols = append(cols, handler.NewCol(AppOIDCConfigColumnLoginVersion, *e.LoginVersion))
}
if e.LoginBaseURI != nil {
cols = append(cols, handler.NewCol(AppOIDCConfigColumnLoginBaseURI, *e.LoginBaseURI))
}
if len(cols) == 0 {
return handler.NewNoOpStatement(e), nil

View File

@@ -559,7 +559,9 @@ func TestAppProjection_reduces(t *testing.T) {
"clockSkew": 1000,
"additionalOrigins": ["origin.one.ch", "origin.two.ch"],
"skipNativeAppSuccessPage": true,
"backChannelLogoutURI": "back.channel.one.ch"
"backChannelLogoutURI": "back.channel.one.ch",
"loginVersion": 2,
"loginBaseURI": "https://login.ch/"
}`),
), project.OIDCConfigAddedEventMapper),
},
@@ -570,7 +572,7 @@ func TestAppProjection_reduces(t *testing.T) {
executer: &testExecuter{
executions: []execution{
{
expectedStmt: "INSERT INTO projections.apps7_oidc_configs (app_id, instance_id, version, client_id, client_secret, redirect_uris, response_types, grant_types, application_type, auth_method_type, post_logout_redirect_uris, is_dev_mode, access_token_type, access_token_role_assertion, id_token_role_assertion, id_token_userinfo_assertion, clock_skew, additional_origins, skip_native_app_success_page, back_channel_logout_uri) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20)",
expectedStmt: "INSERT INTO projections.apps7_oidc_configs (app_id, instance_id, version, client_id, client_secret, redirect_uris, response_types, grant_types, application_type, auth_method_type, post_logout_redirect_uris, is_dev_mode, access_token_type, access_token_role_assertion, id_token_role_assertion, id_token_userinfo_assertion, clock_skew, additional_origins, skip_native_app_success_page, back_channel_logout_uri, login_version, login_base_uri) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22)",
expectedArgs: []interface{}{
"app-id",
"instance-id",
@@ -592,6 +594,8 @@ func TestAppProjection_reduces(t *testing.T) {
database.TextArray[string]{"origin.one.ch", "origin.two.ch"},
true,
"back.channel.one.ch",
domain.LoginVersion2,
"https://login.ch/",
},
},
{
@@ -633,7 +637,9 @@ func TestAppProjection_reduces(t *testing.T) {
"clockSkew": 1000,
"additionalOrigins": ["origin.one.ch", "origin.two.ch"],
"skipNativeAppSuccessPage": true,
"backChannelLogoutURI": "back.channel.one.ch"
"backChannelLogoutURI": "back.channel.one.ch",
"loginVersion": 2,
"loginBaseURI": "https://login.ch/"
}`),
), project.OIDCConfigAddedEventMapper),
},
@@ -644,7 +650,7 @@ func TestAppProjection_reduces(t *testing.T) {
executer: &testExecuter{
executions: []execution{
{
expectedStmt: "INSERT INTO projections.apps7_oidc_configs (app_id, instance_id, version, client_id, client_secret, redirect_uris, response_types, grant_types, application_type, auth_method_type, post_logout_redirect_uris, is_dev_mode, access_token_type, access_token_role_assertion, id_token_role_assertion, id_token_userinfo_assertion, clock_skew, additional_origins, skip_native_app_success_page, back_channel_logout_uri) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20)",
expectedStmt: "INSERT INTO projections.apps7_oidc_configs (app_id, instance_id, version, client_id, client_secret, redirect_uris, response_types, grant_types, application_type, auth_method_type, post_logout_redirect_uris, is_dev_mode, access_token_type, access_token_role_assertion, id_token_role_assertion, id_token_userinfo_assertion, clock_skew, additional_origins, skip_native_app_success_page, back_channel_logout_uri, login_version, login_base_uri) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22)",
expectedArgs: []interface{}{
"app-id",
"instance-id",
@@ -666,6 +672,8 @@ func TestAppProjection_reduces(t *testing.T) {
database.TextArray[string]{"origin.one.ch", "origin.two.ch"},
true,
"back.channel.one.ch",
domain.LoginVersion2,
"https://login.ch/",
},
},
{
@@ -705,7 +713,8 @@ func TestAppProjection_reduces(t *testing.T) {
"clockSkew": 1000,
"additionalOrigins": ["origin.one.ch", "origin.two.ch"],
"skipNativeAppSuccessPage": true,
"backChannelLogoutURI": "back.channel.one.ch"
"backChannelLogoutURI": "back.channel.one.ch",
"loginVersion": 2
}`),
), project.OIDCConfigChangedEventMapper),
},
@@ -716,7 +725,7 @@ func TestAppProjection_reduces(t *testing.T) {
executer: &testExecuter{
executions: []execution{
{
expectedStmt: "UPDATE projections.apps7_oidc_configs SET (version, redirect_uris, response_types, grant_types, application_type, auth_method_type, post_logout_redirect_uris, is_dev_mode, access_token_type, access_token_role_assertion, id_token_role_assertion, id_token_userinfo_assertion, clock_skew, additional_origins, skip_native_app_success_page, back_channel_logout_uri) = ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16) WHERE (app_id = $17) AND (instance_id = $18)",
expectedStmt: "UPDATE projections.apps7_oidc_configs SET (version, redirect_uris, response_types, grant_types, application_type, auth_method_type, post_logout_redirect_uris, is_dev_mode, access_token_type, access_token_role_assertion, id_token_role_assertion, id_token_userinfo_assertion, clock_skew, additional_origins, skip_native_app_success_page, back_channel_logout_uri, login_version) = ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17) WHERE (app_id = $18) AND (instance_id = $19)",
expectedArgs: []interface{}{
domain.OIDCVersionV1,
database.TextArray[string]{"redirect.one.ch", "redirect.two.ch"},
@@ -734,6 +743,7 @@ func TestAppProjection_reduces(t *testing.T) {
database.TextArray[string]{"origin.one.ch", "origin.two.ch"},
true,
"back.channel.one.ch",
domain.LoginVersion2,
"app-id",
"instance-id",
},

View File

@@ -108,6 +108,10 @@ func (*instanceFeatureProjection) Reducers() []handler.AggregateReducer {
Event: feature_v2.InstanceEnableBackChannelLogout,
Reduce: reduceInstanceSetFeature[bool],
},
{
Event: feature_v2.InstanceLoginVersion,
Reduce: reduceInstanceSetFeature[*feature.LoginV2],
},
{
Event: instance.InstanceRemovedEventType,
Reduce: reduceInstanceRemovedHelper(InstanceDomainInstanceIDCol),

View File

@@ -88,6 +88,10 @@ func (*systemFeatureProjection) Reducers() []handler.AggregateReducer {
Event: feature_v2.SystemEnableBackChannelLogout,
Reduce: reduceSystemSetFeature[bool],
},
{
Event: feature_v2.SystemLoginVersion,
Reduce: reduceSystemSetFeature[*feature.LoginV2],
},
},
}}
}

View File

@@ -30,6 +30,7 @@ type SystemFeatures struct {
OIDCSingleV1SessionTermination FeatureSource[bool]
DisableUserTokenEvent FeatureSource[bool]
EnableBackChannelLogout FeatureSource[bool]
LoginV2 FeatureSource[*feature.LoginV2]
}
func (q *Queries) GetSystemFeatures(ctx context.Context) (_ *SystemFeatures, err error) {

View File

@@ -32,6 +32,11 @@ func (m *SystemFeaturesReadModel) Reduce() error {
if err != nil {
return err
}
case *feature_v2.SetEvent[*feature.LoginV2]:
err := reduceSystemFeatureSet(m.system, e)
if err != nil {
return err
}
case *feature_v2.SetEvent[[]feature.ImprovedPerformanceType]:
err := reduceSystemFeatureSet(m.system, e)
if err != nil {
@@ -60,6 +65,7 @@ func (m *SystemFeaturesReadModel) Query() *eventstore.SearchQueryBuilder {
feature_v2.SystemOIDCSingleV1SessionTerminationEventType,
feature_v2.SystemDisableUserTokenEvent,
feature_v2.SystemEnableBackChannelLogout,
feature_v2.SystemLoginVersion,
).
Builder().ResourceOwner(m.ResourceOwner)
}
@@ -97,6 +103,8 @@ func reduceSystemFeatureSet[T any](features *SystemFeatures, event *feature_v2.S
features.DisableUserTokenEvent.set(level, event.Value)
case feature.KeyEnableBackChannelLogout:
features.EnableBackChannelLogout.set(level, event.Value)
case feature.KeyLoginV2:
features.LoginV2.set(level, event.Value)
}
return nil
}