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

@@ -644,7 +644,15 @@ func importOIDCApps(ctx context.Context, s *Server, errors *[]*admin_pb.ImportDa
}
for _, app := range org.GetOidcApps() {
logging.Debugf("import oidcapplication: %s", app.GetAppId())
_, err := s.command.AddOIDCApplicationWithID(ctx, management.AddOIDCAppRequestToDomain(app.App), org.GetOrgId(), app.GetAppId())
oidcApp, err := management.AddOIDCAppRequestToDomain(app.App)
if err != nil {
*errors = append(*errors, &admin_pb.ImportDataError{Type: "oidc_app", Id: app.GetAppId(), Message: err.Error()})
if isCtxTimeout(ctx) {
return err
}
continue
}
_, err = s.command.AddOIDCApplicationWithID(ctx, oidcApp, org.GetOrgId(), app.GetAppId())
if err != nil {
*errors = append(*errors, &admin_pb.ImportDataError{Type: "oidc_app", Id: app.GetAppId(), Message: err.Error()})
if isCtxTimeout(ctx) {

View File

@@ -25,6 +25,7 @@ var iamRoles = []string{
"IAM_USER_MANAGER",
"IAM_ADMIN_IMPERSONATOR",
"IAM_END_USER_IMPERSONATOR",
"IAM_LOGIN_CLIENT",
}
func TestServer_ListIAMMemberRoles(t *testing.T) {

View File

@@ -1,6 +1,10 @@
package feature
import (
"net/url"
"github.com/muhlemmer/gu"
"github.com/zitadel/zitadel/internal/api/grpc/object/v2"
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/feature"
@@ -8,7 +12,11 @@ import (
feature_pb "github.com/zitadel/zitadel/pkg/grpc/feature/v2"
)
func systemFeaturesToCommand(req *feature_pb.SetSystemFeaturesRequest) *command.SystemFeatures {
func systemFeaturesToCommand(req *feature_pb.SetSystemFeaturesRequest) (*command.SystemFeatures, error) {
loginV2, err := loginV2ToDomain(req.GetLoginV2())
if err != nil {
return nil, err
}
return &command.SystemFeatures{
LoginDefaultOrg: req.LoginDefaultOrg,
TriggerIntrospectionProjections: req.OidcTriggerIntrospectionProjections,
@@ -20,7 +28,8 @@ func systemFeaturesToCommand(req *feature_pb.SetSystemFeaturesRequest) *command.
OIDCSingleV1SessionTermination: req.OidcSingleV1SessionTermination,
DisableUserTokenEvent: req.DisableUserTokenEvent,
EnableBackChannelLogout: req.EnableBackChannelLogout,
}
LoginV2: loginV2,
}, nil
}
func systemFeaturesToPb(f *query.SystemFeatures) *feature_pb.GetSystemFeaturesResponse {
@@ -36,10 +45,15 @@ func systemFeaturesToPb(f *query.SystemFeatures) *feature_pb.GetSystemFeaturesRe
OidcSingleV1SessionTermination: featureSourceToFlagPb(&f.OIDCSingleV1SessionTermination),
DisableUserTokenEvent: featureSourceToFlagPb(&f.DisableUserTokenEvent),
EnableBackChannelLogout: featureSourceToFlagPb(&f.EnableBackChannelLogout),
LoginV2: loginV2ToLoginV2FlagPb(f.LoginV2),
}
}
func instanceFeaturesToCommand(req *feature_pb.SetInstanceFeaturesRequest) *command.InstanceFeatures {
func instanceFeaturesToCommand(req *feature_pb.SetInstanceFeaturesRequest) (*command.InstanceFeatures, error) {
loginV2, err := loginV2ToDomain(req.GetLoginV2())
if err != nil {
return nil, err
}
return &command.InstanceFeatures{
LoginDefaultOrg: req.LoginDefaultOrg,
TriggerIntrospectionProjections: req.OidcTriggerIntrospectionProjections,
@@ -53,7 +67,8 @@ func instanceFeaturesToCommand(req *feature_pb.SetInstanceFeaturesRequest) *comm
OIDCSingleV1SessionTermination: req.OidcSingleV1SessionTermination,
DisableUserTokenEvent: req.DisableUserTokenEvent,
EnableBackChannelLogout: req.EnableBackChannelLogout,
}
LoginV2: loginV2,
}, nil
}
func instanceFeaturesToPb(f *query.InstanceFeatures) *feature_pb.GetInstanceFeaturesResponse {
@@ -71,6 +86,7 @@ func instanceFeaturesToPb(f *query.InstanceFeatures) *feature_pb.GetInstanceFeat
OidcSingleV1SessionTermination: featureSourceToFlagPb(&f.OIDCSingleV1SessionTermination),
DisableUserTokenEvent: featureSourceToFlagPb(&f.DisableUserTokenEvent),
EnableBackChannelLogout: featureSourceToFlagPb(&f.EnableBackChannelLogout),
LoginV2: loginV2ToLoginV2FlagPb(f.LoginV2),
}
}
@@ -81,6 +97,39 @@ func featureSourceToImprovedPerformanceFlagPb(fs *query.FeatureSource[[]feature.
}
}
func loginV2ToDomain(loginV2 *feature_pb.LoginV2) (_ *feature.LoginV2, err error) {
if loginV2 == nil {
return nil, nil
}
var baseURI *url.URL
if loginV2.GetBaseUri() != "" {
baseURI, err = url.Parse(loginV2.GetBaseUri())
if err != nil {
return nil, err
}
}
return &feature.LoginV2{
Required: loginV2.GetRequired(),
BaseURI: baseURI,
}, nil
}
func loginV2ToLoginV2FlagPb(f query.FeatureSource[*feature.LoginV2]) *feature_pb.LoginV2FeatureFlag {
var required bool
var baseURI *string
if f.Value != nil {
required = f.Value.Required
if f.Value.BaseURI != nil && f.Value.BaseURI.String() != "" {
baseURI = gu.Ptr(f.Value.BaseURI.String())
}
}
return &feature_pb.LoginV2FeatureFlag{
Required: required,
BaseUri: baseURI,
Source: featureLevelToSourcePb(f.Level),
}
}
func featureSourceToFlagPb(fs *query.FeatureSource[bool]) *feature_pb.FeatureFlag {
return &feature_pb.FeatureFlag{
Enabled: fs.Value,

View File

@@ -1,6 +1,7 @@
package feature
import (
"net/url"
"testing"
"time"
@@ -26,6 +27,10 @@ func Test_systemFeaturesToCommand(t *testing.T) {
OidcTokenExchange: gu.Ptr(true),
ImprovedPerformance: nil,
OidcSingleV1SessionTermination: gu.Ptr(true),
LoginV2: &feature_pb.LoginV2{
Required: true,
BaseUri: gu.Ptr("https://login.com"),
},
}
want := &command.SystemFeatures{
LoginDefaultOrg: gu.Ptr(true),
@@ -36,9 +41,14 @@ func Test_systemFeaturesToCommand(t *testing.T) {
TokenExchange: gu.Ptr(true),
ImprovedPerformance: nil,
OIDCSingleV1SessionTermination: gu.Ptr(true),
LoginV2: &feature.LoginV2{
Required: true,
BaseURI: &url.URL{Scheme: "https", Host: "login.com"},
},
}
got := systemFeaturesToCommand(arg)
got, err := systemFeaturesToCommand(arg)
assert.Equal(t, want, got)
assert.NoError(t, err)
}
func Test_systemFeaturesToPb(t *testing.T) {
@@ -84,6 +94,13 @@ func Test_systemFeaturesToPb(t *testing.T) {
Level: feature.LevelSystem,
Value: true,
},
LoginV2: query.FeatureSource[*feature.LoginV2]{
Level: feature.LevelSystem,
Value: &feature.LoginV2{
Required: true,
BaseURI: &url.URL{Scheme: "https", Host: "login.com"},
},
},
}
want := &feature_pb.GetSystemFeaturesResponse{
Details: &object.Details{
@@ -131,6 +148,11 @@ func Test_systemFeaturesToPb(t *testing.T) {
Enabled: true,
Source: feature_pb.Source_SOURCE_SYSTEM,
},
LoginV2: &feature_pb.LoginV2FeatureFlag{
Required: true,
BaseUri: gu.Ptr("https://login.com"),
Source: feature_pb.Source_SOURCE_SYSTEM,
},
}
got := systemFeaturesToPb(arg)
assert.Equal(t, want, got)
@@ -149,6 +171,10 @@ func Test_instanceFeaturesToCommand(t *testing.T) {
DebugOidcParentError: gu.Ptr(true),
OidcSingleV1SessionTermination: gu.Ptr(true),
EnableBackChannelLogout: gu.Ptr(true),
LoginV2: &feature_pb.LoginV2{
Required: true,
BaseUri: gu.Ptr("https://login.com"),
},
}
want := &command.InstanceFeatures{
LoginDefaultOrg: gu.Ptr(true),
@@ -162,9 +188,14 @@ func Test_instanceFeaturesToCommand(t *testing.T) {
DebugOIDCParentError: gu.Ptr(true),
OIDCSingleV1SessionTermination: gu.Ptr(true),
EnableBackChannelLogout: gu.Ptr(true),
LoginV2: &feature.LoginV2{
Required: true,
BaseURI: &url.URL{Scheme: "https", Host: "login.com"},
},
}
got := instanceFeaturesToCommand(arg)
got, err := instanceFeaturesToCommand(arg)
assert.Equal(t, want, got)
assert.NoError(t, err)
}
func Test_instanceFeaturesToPb(t *testing.T) {
@@ -214,6 +245,13 @@ func Test_instanceFeaturesToPb(t *testing.T) {
Level: feature.LevelInstance,
Value: true,
},
LoginV2: query.FeatureSource[*feature.LoginV2]{
Level: feature.LevelInstance,
Value: &feature.LoginV2{
Required: true,
BaseURI: &url.URL{Scheme: "https", Host: "login.com"},
},
},
}
want := &feature_pb.GetInstanceFeaturesResponse{
Details: &object.Details{
@@ -269,6 +307,11 @@ func Test_instanceFeaturesToPb(t *testing.T) {
Enabled: true,
Source: feature_pb.Source_SOURCE_INSTANCE,
},
LoginV2: &feature_pb.LoginV2FeatureFlag{
Required: true,
BaseUri: gu.Ptr("https://login.com"),
Source: feature_pb.Source_SOURCE_INSTANCE,
},
}
got := instanceFeaturesToPb(arg)
assert.Equal(t, want, got)

View File

@@ -11,7 +11,11 @@ import (
)
func (s *Server) SetSystemFeatures(ctx context.Context, req *feature.SetSystemFeaturesRequest) (_ *feature.SetSystemFeaturesResponse, err error) {
details, err := s.command.SetSystemFeatures(ctx, systemFeaturesToCommand(req))
features, err := systemFeaturesToCommand(req)
if err != nil {
return nil, err
}
details, err := s.command.SetSystemFeatures(ctx, features)
if err != nil {
return nil, err
}
@@ -39,7 +43,11 @@ func (s *Server) GetSystemFeatures(ctx context.Context, req *feature.GetSystemFe
}
func (s *Server) SetInstanceFeatures(ctx context.Context, req *feature.SetInstanceFeaturesRequest) (_ *feature.SetInstanceFeaturesResponse, err error) {
details, err := s.command.SetInstanceFeatures(ctx, instanceFeaturesToCommand(req))
features, err := instanceFeaturesToCommand(req)
if err != nil {
return nil, err
}
details, err := s.command.SetInstanceFeatures(ctx, features)
if err != nil {
return nil, err
}

View File

@@ -27,7 +27,7 @@ func TestMain(m *testing.M) {
Instance = integration.NewInstance(ctx)
UserCTX = Instance.WithAuthorization(ctx, integration.UserTypeLogin)
UserCTX = Instance.WithAuthorization(ctx, integration.UserTypeNoPermission)
IamCTX = Instance.WithAuthorization(ctx, integration.UserTypeIAMOwner)
CTX = Instance.WithAuthorization(ctx, integration.UserTypeOrgOwner)
Client = Instance.Client.IDPv2

View File

@@ -80,7 +80,11 @@ func (s *Server) ListAppChanges(ctx context.Context, req *mgmt_pb.ListAppChanges
}
func (s *Server) AddOIDCApp(ctx context.Context, req *mgmt_pb.AddOIDCAppRequest) (*mgmt_pb.AddOIDCAppResponse, error) {
app, err := s.command.AddOIDCApplication(ctx, AddOIDCAppRequestToDomain(req), authz.GetCtxData(ctx).OrgID)
oidcApp, err := AddOIDCAppRequestToDomain(req)
if err != nil {
return nil, err
}
app, err := s.command.AddOIDCApplication(ctx, oidcApp, authz.GetCtxData(ctx).OrgID)
if err != nil {
return nil, err
}
@@ -128,7 +132,11 @@ func (s *Server) UpdateApp(ctx context.Context, req *mgmt_pb.UpdateAppRequest) (
}
func (s *Server) UpdateOIDCAppConfig(ctx context.Context, req *mgmt_pb.UpdateOIDCAppConfigRequest) (*mgmt_pb.UpdateOIDCAppConfigResponse, error) {
config, err := s.command.ChangeOIDCApplication(ctx, UpdateOIDCAppConfigRequestToDomain(req), authz.GetCtxData(ctx).OrgID)
oidcApp, err := UpdateOIDCAppConfigRequestToDomain(req)
if err != nil {
return nil, err
}
config, err := s.command.ChangeOIDCApplication(ctx, oidcApp, authz.GetCtxData(ctx).OrgID)
if err != nil {
return nil, err
}

View File

@@ -36,7 +36,11 @@ func ListAppsRequestToModel(req *mgmt_pb.ListAppsRequest) (*query.AppSearchQueri
}, nil
}
func AddOIDCAppRequestToDomain(req *mgmt_pb.AddOIDCAppRequest) *domain.OIDCApp {
func AddOIDCAppRequestToDomain(req *mgmt_pb.AddOIDCAppRequest) (*domain.OIDCApp, error) {
loginVersion, loginBaseURI, err := app_grpc.LoginVersionToDomain(req.GetLoginVersion())
if err != nil {
return nil, err
}
return &domain.OIDCApp{
ObjectRoot: models.ObjectRoot{
AggregateID: req.ProjectId,
@@ -58,7 +62,9 @@ func AddOIDCAppRequestToDomain(req *mgmt_pb.AddOIDCAppRequest) *domain.OIDCApp {
AdditionalOrigins: req.AdditionalOrigins,
SkipNativeAppSuccessPage: req.SkipNativeAppSuccessPage,
BackChannelLogoutURI: req.GetBackChannelLogoutUri(),
}
LoginVersion: loginVersion,
LoginBaseURI: loginBaseURI,
}, nil
}
func AddSAMLAppRequestToDomain(req *mgmt_pb.AddSAMLAppRequest) *domain.SAMLApp {
@@ -89,7 +95,11 @@ func UpdateAppRequestToDomain(app *mgmt_pb.UpdateAppRequest) domain.Application
}
}
func UpdateOIDCAppConfigRequestToDomain(app *mgmt_pb.UpdateOIDCAppConfigRequest) *domain.OIDCApp {
func UpdateOIDCAppConfigRequestToDomain(app *mgmt_pb.UpdateOIDCAppConfigRequest) (*domain.OIDCApp, error) {
loginVersion, loginBaseURI, err := app_grpc.LoginVersionToDomain(app.GetLoginVersion())
if err != nil {
return nil, err
}
return &domain.OIDCApp{
ObjectRoot: models.ObjectRoot{
AggregateID: app.ProjectId,
@@ -110,7 +120,9 @@ func UpdateOIDCAppConfigRequestToDomain(app *mgmt_pb.UpdateOIDCAppConfigRequest)
AdditionalOrigins: app.AdditionalOrigins,
SkipNativeAppSuccessPage: app.SkipNativeAppSuccessPage,
BackChannelLogoutURI: app.BackChannelLogoutUri,
}
LoginVersion: loginVersion,
LoginBaseURI: loginBaseURI,
}, nil
}
func UpdateSAMLAppConfigRequestToDomain(app *mgmt_pb.UpdateSAMLAppConfigRequest) *domain.SAMLApp {

View File

@@ -16,15 +16,18 @@ import (
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/zitadel/zitadel/internal/integration"
"github.com/zitadel/zitadel/pkg/grpc/app"
"github.com/zitadel/zitadel/pkg/grpc/object/v2"
oidc_pb "github.com/zitadel/zitadel/pkg/grpc/oidc/v2"
"github.com/zitadel/zitadel/pkg/grpc/session/v2"
)
var (
CTX context.Context
Instance *integration.Instance
Client oidc_pb.OIDCServiceClient
CTX context.Context
CTXLoginClient context.Context
Instance *integration.Instance
Client oidc_pb.OIDCServiceClient
loginV2 = &app.LoginVersion{Version: &app.LoginVersion_LoginV2{LoginV2: &app.LoginV2{BaseUri: nil}}}
)
const (
@@ -42,6 +45,7 @@ func TestMain(m *testing.M) {
Client = Instance.Client.OIDCv2
CTX = Instance.WithAuthorization(ctx, integration.UserTypeOrgOwner)
CTXLoginClient = Instance.WithAuthorization(ctx, integration.UserTypeLogin)
return m.Run()
}())
}
@@ -51,29 +55,58 @@ func TestServer_GetAuthRequest(t *testing.T) {
require.NoError(t, err)
client, err := Instance.CreateOIDCNativeClient(CTX, redirectURI, logoutRedirectURI, project.GetId(), false)
require.NoError(t, err)
authRequestID, err := Instance.CreateOIDCAuthRequest(CTX, client.GetClientId(), Instance.Users[integration.UserTypeOrgOwner].ID, redirectURI)
require.NoError(t, err)
now := time.Now()
tests := []struct {
name string
AuthRequestID string
ctx context.Context
want *oidc_pb.GetAuthRequestResponse
wantErr bool
}{
{
name: "Not found",
AuthRequestID: "123",
ctx: CTX,
wantErr: true,
},
{
name: "success",
AuthRequestID: authRequestID,
name: "success",
AuthRequestID: func() string {
authRequestID, err := Instance.CreateOIDCAuthRequest(CTX, client.GetClientId(), Instance.Users[integration.UserTypeOrgOwner].ID, redirectURI)
require.NoError(t, err)
return authRequestID
}(),
ctx: CTX,
},
{
name: "without login client, no permission",
AuthRequestID: func() string {
client, err := Instance.CreateOIDCClientLoginVersion(CTX, redirectURI, logoutRedirectURI, project.GetId(), app.OIDCAppType_OIDC_APP_TYPE_NATIVE, app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_NONE, false, loginV2)
require.NoError(t, err)
authRequestID, err := Instance.CreateOIDCAuthRequestWithoutLoginClientHeader(CTX, client.GetClientId(), redirectURI, "")
require.NoError(t, err)
return authRequestID
}(),
ctx: CTX,
wantErr: true,
},
{
name: "without login client, with permission",
AuthRequestID: func() string {
client, err := Instance.CreateOIDCClientLoginVersion(CTX, redirectURI, logoutRedirectURI, project.GetId(), app.OIDCAppType_OIDC_APP_TYPE_NATIVE, app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_NONE, false, loginV2)
require.NoError(t, err)
authRequestID, err := Instance.CreateOIDCAuthRequestWithoutLoginClientHeader(CTX, client.GetClientId(), redirectURI, "")
require.NoError(t, err)
return authRequestID
}(),
ctx: CTXLoginClient,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := Client.GetAuthRequest(CTX, &oidc_pb.GetAuthRequestRequest{
got, err := Client.GetAuthRequest(tt.ctx, &oidc_pb.GetAuthRequestRequest{
AuthRequestId: tt.AuthRequestID,
})
if tt.wantErr {
@@ -83,7 +116,7 @@ func TestServer_GetAuthRequest(t *testing.T) {
require.NoError(t, err)
authRequest := got.GetAuthRequest()
assert.NotNil(t, authRequest)
assert.Equal(t, authRequestID, authRequest.GetId())
assert.Equal(t, tt.AuthRequestID, authRequest.GetId())
assert.WithinRange(t, authRequest.GetCreationDate().AsTime(), now.Add(-time.Second), now.Add(time.Second))
assert.Contains(t, authRequest.GetScope(), "openid")
})
@@ -95,6 +128,8 @@ func TestServer_CreateCallback(t *testing.T) {
require.NoError(t, err)
client, err := Instance.CreateOIDCNativeClient(CTX, redirectURI, logoutRedirectURI, project.GetId(), false)
require.NoError(t, err)
clientV2, err := Instance.CreateOIDCClientLoginVersion(CTX, redirectURI, logoutRedirectURI, project.GetId(), app.OIDCAppType_OIDC_APP_TYPE_NATIVE, app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_NONE, false, loginV2)
require.NoError(t, err)
sessionResp, err := Instance.Client.SessionV2.CreateSession(CTX, &session.CreateSessionRequest{
Checks: &session.Checks{
User: &session.CheckUser{
@@ -108,6 +143,7 @@ func TestServer_CreateCallback(t *testing.T) {
tests := []struct {
name string
ctx context.Context
req *oidc_pb.CreateCallbackRequest
AuthError string
want *oidc_pb.CreateCallbackResponse
@@ -116,6 +152,7 @@ func TestServer_CreateCallback(t *testing.T) {
}{
{
name: "Not found",
ctx: CTX,
req: &oidc_pb.CreateCallbackRequest{
AuthRequestId: "123",
CallbackKind: &oidc_pb.CreateCallbackRequest_Session{
@@ -129,6 +166,7 @@ func TestServer_CreateCallback(t *testing.T) {
},
{
name: "session not found",
ctx: CTX,
req: &oidc_pb.CreateCallbackRequest{
AuthRequestId: func() string {
authRequestID, err := Instance.CreateOIDCAuthRequest(CTX, client.GetClientId(), Instance.Users[integration.UserTypeOrgOwner].ID, redirectURI)
@@ -146,6 +184,7 @@ func TestServer_CreateCallback(t *testing.T) {
},
{
name: "session token invalid",
ctx: CTX,
req: &oidc_pb.CreateCallbackRequest{
AuthRequestId: func() string {
authRequestID, err := Instance.CreateOIDCAuthRequest(CTX, client.GetClientId(), Instance.Users.Get(integration.UserTypeOrgOwner).ID, redirectURI)
@@ -163,6 +202,7 @@ func TestServer_CreateCallback(t *testing.T) {
},
{
name: "fail callback",
ctx: CTX,
req: &oidc_pb.CreateCallbackRequest{
AuthRequestId: func() string {
authRequestID, err := Instance.CreateOIDCAuthRequest(CTX, client.GetClientId(), Instance.Users.Get(integration.UserTypeOrgOwner).ID, redirectURI)
@@ -186,8 +226,35 @@ func TestServer_CreateCallback(t *testing.T) {
},
wantErr: false,
},
{
name: "fail callback, no login client header",
ctx: CTXLoginClient,
req: &oidc_pb.CreateCallbackRequest{
AuthRequestId: func() string {
authRequestID, err := Instance.CreateOIDCAuthRequestWithoutLoginClientHeader(CTX, clientV2.GetClientId(), redirectURI, "")
require.NoError(t, err)
return authRequestID
}(),
CallbackKind: &oidc_pb.CreateCallbackRequest_Error{
Error: &oidc_pb.AuthorizationError{
Error: oidc_pb.ErrorReason_ERROR_REASON_ACCESS_DENIED,
ErrorDescription: gu.Ptr("nope"),
ErrorUri: gu.Ptr("https://example.com/docs"),
},
},
},
want: &oidc_pb.CreateCallbackResponse{
CallbackUrl: regexp.QuoteMeta(`oidcintegrationtest://callback?error=access_denied&error_description=nope&error_uri=https%3A%2F%2Fexample.com%2Fdocs&state=state`),
Details: &object.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: Instance.ID(),
},
},
wantErr: false,
},
{
name: "code callback",
ctx: CTX,
req: &oidc_pb.CreateCallbackRequest{
AuthRequestId: func() string {
authRequestID, err := Instance.CreateOIDCAuthRequest(CTX, client.GetClientId(), Instance.Users.Get(integration.UserTypeOrgOwner).ID, redirectURI)
@@ -211,10 +278,54 @@ func TestServer_CreateCallback(t *testing.T) {
wantErr: false,
},
{
name: "implicit",
name: "code callback, no login client header, no permission, error",
ctx: CTX,
req: &oidc_pb.CreateCallbackRequest{
AuthRequestId: func() string {
client, err := Instance.CreateOIDCImplicitFlowClient(CTX, redirectURIImplicit)
authRequestID, err := Instance.CreateOIDCAuthRequestWithoutLoginClientHeader(CTX, clientV2.GetClientId(), redirectURI, "")
require.NoError(t, err)
return authRequestID
}(),
CallbackKind: &oidc_pb.CreateCallbackRequest_Session{
Session: &oidc_pb.Session{
SessionId: sessionResp.GetSessionId(),
SessionToken: sessionResp.GetSessionToken(),
},
},
},
wantErr: true,
},
{
name: "code callback, no login client header, with permission",
ctx: CTXLoginClient,
req: &oidc_pb.CreateCallbackRequest{
AuthRequestId: func() string {
authRequestID, err := Instance.CreateOIDCAuthRequestWithoutLoginClientHeader(CTX, clientV2.GetClientId(), redirectURI, "")
require.NoError(t, err)
return authRequestID
}(),
CallbackKind: &oidc_pb.CreateCallbackRequest_Session{
Session: &oidc_pb.Session{
SessionId: sessionResp.GetSessionId(),
SessionToken: sessionResp.GetSessionToken(),
},
},
},
want: &oidc_pb.CreateCallbackResponse{
CallbackUrl: `oidcintegrationtest:\/\/callback\?code=(.*)&state=state`,
Details: &object.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: Instance.ID(),
},
},
wantErr: false,
},
{
name: "implicit",
ctx: CTX,
req: &oidc_pb.CreateCallbackRequest{
AuthRequestId: func() string {
client, err := Instance.CreateOIDCImplicitFlowClient(CTX, redirectURIImplicit, nil)
require.NoError(t, err)
authRequestID, err := Instance.CreateOIDCAuthRequestImplicit(CTX, client.GetClientId(), Instance.Users.Get(integration.UserTypeOrgOwner).ID, redirectURIImplicit)
require.NoError(t, err)
@@ -236,10 +347,37 @@ func TestServer_CreateCallback(t *testing.T) {
},
wantErr: false,
},
{
name: "implicit, no login client header",
ctx: CTXLoginClient,
req: &oidc_pb.CreateCallbackRequest{
AuthRequestId: func() string {
clientV2, err := Instance.CreateOIDCImplicitFlowClient(CTX, redirectURIImplicit, loginV2)
require.NoError(t, err)
authRequestID, err := Instance.CreateOIDCAuthRequestImplicitWithoutLoginClientHeader(CTX, clientV2.GetClientId(), redirectURIImplicit)
require.NoError(t, err)
return authRequestID
}(),
CallbackKind: &oidc_pb.CreateCallbackRequest_Session{
Session: &oidc_pb.Session{
SessionId: sessionResp.GetSessionId(),
SessionToken: sessionResp.GetSessionToken(),
},
},
},
want: &oidc_pb.CreateCallbackResponse{
CallbackUrl: `http:\/\/localhost:9999\/callback#access_token=(.*)&expires_in=(.*)&id_token=(.*)&state=state&token_type=Bearer`,
Details: &object.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: Instance.ID(),
},
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := Client.CreateCallback(CTX, tt.req)
got, err := Client.CreateCallback(tt.ctx, tt.req)
if tt.wantErr {
require.Error(t, err)
return

View File

@@ -214,7 +214,7 @@ func TestServer_CreateCallback(t *testing.T) {
name: "implicit",
req: &oidc_pb.CreateCallbackRequest{
AuthRequestId: func() string {
client, err := Instance.CreateOIDCImplicitFlowClient(CTX, redirectURIImplicit)
client, err := Instance.CreateOIDCImplicitFlowClient(CTX, redirectURIImplicit, nil)
require.NoError(t, err)
authRequestID, err := Instance.CreateOIDCAuthRequestImplicit(CTX, client.GetClientId(), Instance.Users.Get(integration.UserTypeOrgOwner).ID, redirectURIImplicit)
require.NoError(t, err)

View File

@@ -35,7 +35,7 @@ func TestMain(m *testing.M) {
CTX = Instance.WithAuthorization(ctx, integration.UserTypeIAMOwner)
OwnerCTX = Instance.WithAuthorization(ctx, integration.UserTypeOrgOwner)
UserCTX = Instance.WithAuthorization(ctx, integration.UserTypeLogin)
UserCTX = Instance.WithAuthorization(ctx, integration.UserTypeNoPermission)
User = Instance.CreateHumanUser(CTX)
return m.Run()
}())

View File

@@ -1,6 +1,8 @@
package project
import (
"net/url"
"google.golang.org/protobuf/types/known/durationpb"
object_grpc "github.com/zitadel/zitadel/internal/api/grpc/object"
@@ -62,10 +64,24 @@ func AppOIDCConfigToPb(app *query.OIDCApp) *app_pb.App_OidcConfig {
AllowedOrigins: app.AllowedOrigins,
SkipNativeAppSuccessPage: app.SkipNativeAppSuccessPage,
BackChannelLogoutUri: app.BackChannelLogoutURI,
LoginVersion: loginVersionToPb(app.LoginVersion, app.LoginBaseURI),
},
}
}
func loginVersionToPb(version domain.LoginVersion, baseURI *string) *app_pb.LoginVersion {
switch version {
case domain.LoginVersionUnspecified:
return nil
case domain.LoginVersion1:
return &app_pb.LoginVersion{Version: &app_pb.LoginVersion_LoginV1{LoginV1: &app_pb.LoginV1{}}}
case domain.LoginVersion2:
return &app_pb.LoginVersion{Version: &app_pb.LoginVersion_LoginV2{LoginV2: &app_pb.LoginV2{BaseUri: baseURI}}}
default:
return nil
}
}
func AppSAMLConfigToPb(app *query.SAMLApp) app_pb.AppConfig {
return &app_pb.App_SamlConfig{
SamlConfig: &app_pb.SAMLConfig{
@@ -311,3 +327,17 @@ func AppQueryToModel(appQuery *app_pb.AppQuery) (query.SearchQuery, error) {
return nil, zerrors.ThrowInvalidArgument(nil, "APP-Add46", "List.Query.Invalid")
}
}
func LoginVersionToDomain(version *app_pb.LoginVersion) (domain.LoginVersion, string, error) {
switch v := version.GetVersion().(type) {
case nil:
return domain.LoginVersionUnspecified, "", nil
case *app_pb.LoginVersion_LoginV1:
return domain.LoginVersion1, "", nil
case *app_pb.LoginVersion_LoginV2:
_, err := url.Parse(v.LoginV2.GetBaseUri())
return domain.LoginVersion2, v.LoginV2.GetBaseUri(), err
default:
return domain.LoginVersionUnspecified, "", nil
}
}

View File

@@ -69,7 +69,7 @@ func TestServer_SetContactEmail(t *testing.T) {
},
{
name: "email patch, no permission",
ctx: instance.WithAuthorization(CTX, integration.UserTypeLogin),
ctx: instance.WithAuthorization(CTX, integration.UserTypeNoPermission),
dep: func(req *user.SetContactEmailRequest) error {
userResp := instance.CreateSchemaUser(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), schemaResp.GetDetails().GetId(), []byte("{\"name\": \"user\"}"))
req.Id = userResp.GetDetails().GetId()
@@ -412,7 +412,7 @@ func TestServer_VerifyContactEmail(t *testing.T) {
},
{
name: "email verify, no permission",
ctx: instance.WithAuthorization(CTX, integration.UserTypeLogin),
ctx: instance.WithAuthorization(CTX, integration.UserTypeNoPermission),
dep: func(req *user.VerifyContactEmailRequest) error {
userResp := instance.CreateSchemaUser(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), schemaResp.GetDetails().GetId(), []byte("{\"name\": \"user\"}"))
req.Id = userResp.GetDetails().GetId()
@@ -601,7 +601,7 @@ func TestServer_ResendContactEmailCode(t *testing.T) {
},
{
name: "email resend, no permission",
ctx: instance.WithAuthorization(CTX, integration.UserTypeLogin),
ctx: instance.WithAuthorization(CTX, integration.UserTypeNoPermission),
dep: func(req *user.ResendContactEmailCodeRequest) error {
userResp := instance.CreateSchemaUser(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), schemaResp.GetDetails().GetId(), []byte("{\"name\": \"user\"}"))
req.Id = userResp.GetDetails().GetId()

View File

@@ -68,7 +68,7 @@ func TestServer_SetContactPhone(t *testing.T) {
},
{
name: "phone patch, no permission",
ctx: instance.WithAuthorization(CTX, integration.UserTypeLogin),
ctx: instance.WithAuthorization(CTX, integration.UserTypeNoPermission),
dep: func(req *user.SetContactPhoneRequest) error {
userResp := instance.CreateSchemaUser(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), schemaResp.GetDetails().GetId(), []byte("{\"name\": \"user\"}"))
req.Id = userResp.GetDetails().GetId()
@@ -340,7 +340,7 @@ func TestServer_VerifyContactPhone(t *testing.T) {
},
{
name: "phone verify, no permission",
ctx: instance.WithAuthorization(CTX, integration.UserTypeLogin),
ctx: instance.WithAuthorization(CTX, integration.UserTypeNoPermission),
dep: func(req *user.VerifyContactPhoneRequest) error {
userResp := instance.CreateSchemaUser(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), schemaResp.GetDetails().GetId(), []byte("{\"name\": \"user\"}"))
req.Id = userResp.GetDetails().GetId()
@@ -530,7 +530,7 @@ func TestServer_ResendContactPhoneCode(t *testing.T) {
},
{
name: "phone resend, no permission",
ctx: instance.WithAuthorization(CTX, integration.UserTypeLogin),
ctx: instance.WithAuthorization(CTX, integration.UserTypeNoPermission),
dep: func(req *user.ResendContactPhoneCodeRequest) error {
userResp := instance.CreateSchemaUser(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), schemaResp.GetDetails().GetId(), []byte("{\"name\": \"user\"}"))
req.Id = userResp.GetDetails().GetId()

View File

@@ -94,7 +94,7 @@ func TestServer_CreateUser(t *testing.T) {
},
{
name: "user create, no permission",
ctx: instance.WithAuthorization(CTX, integration.UserTypeLogin),
ctx: instance.WithAuthorization(CTX, integration.UserTypeNoPermission),
req: &user.CreateUserRequest{
Organization: &object.Organization{
Property: &object.Organization_OrgId{
@@ -294,7 +294,7 @@ func TestServer_PatchUser(t *testing.T) {
},
{
name: "user patch, no permission",
ctx: instance.WithAuthorization(CTX, integration.UserTypeLogin),
ctx: instance.WithAuthorization(CTX, integration.UserTypeNoPermission),
dep: func(req *user.PatchUserRequest) error {
userResp := instance.CreateSchemaUser(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), schemaResp.GetDetails().GetId(), []byte("{\"name\": \"user\"}"))
req.Id = userResp.GetDetails().GetId()
@@ -734,7 +734,7 @@ func TestServer_DeleteUser(t *testing.T) {
},
{
name: "user delete, no permission",
ctx: instance.WithAuthorization(CTX, integration.UserTypeLogin),
ctx: instance.WithAuthorization(CTX, integration.UserTypeNoPermission),
dep: func(req *user.DeleteUserRequest) error {
userResp := instance.CreateSchemaUser(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), schemaResp.GetDetails().GetId(), []byte("{\"name\": \"user\"}"))
req.Id = userResp.GetDetails().GetId()
@@ -950,7 +950,7 @@ func TestServer_LockUser(t *testing.T) {
},
{
name: "user lock, no permission",
ctx: instance.WithAuthorization(CTX, integration.UserTypeLogin),
ctx: instance.WithAuthorization(CTX, integration.UserTypeNoPermission),
dep: func(req *user.LockUserRequest) error {
userResp := instance.CreateSchemaUser(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), schemaResp.GetDetails().GetId(), []byte("{\"name\": \"user\"}"))
req.Id = userResp.GetDetails().GetId()
@@ -1152,7 +1152,7 @@ func TestServer_UnlockUser(t *testing.T) {
},
{
name: "user unlock, no permission",
ctx: instance.WithAuthorization(CTX, integration.UserTypeLogin),
ctx: instance.WithAuthorization(CTX, integration.UserTypeNoPermission),
dep: func(req *user.UnlockUserRequest) error {
userResp := instance.CreateSchemaUser(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), schemaResp.GetDetails().GetId(), []byte("{\"name\": \"user\"}"))
req.Id = userResp.GetDetails().GetId()
@@ -1333,7 +1333,7 @@ func TestServer_DeactivateUser(t *testing.T) {
},
{
name: "user deactivate, no permission",
ctx: instance.WithAuthorization(CTX, integration.UserTypeLogin),
ctx: instance.WithAuthorization(CTX, integration.UserTypeNoPermission),
dep: func(req *user.DeactivateUserRequest) error {
userResp := instance.CreateSchemaUser(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), schemaResp.GetDetails().GetId(), []byte("{\"name\": \"user\"}"))
req.Id = userResp.GetDetails().GetId()
@@ -1535,7 +1535,7 @@ func TestServer_ActivateUser(t *testing.T) {
},
{
name: "user activate, no permission",
ctx: instance.WithAuthorization(CTX, integration.UserTypeLogin),
ctx: instance.WithAuthorization(CTX, integration.UserTypeNoPermission),
dep: func(req *user.ActivateUserRequest) error {
userResp := instance.CreateSchemaUser(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), schemaResp.GetDetails().GetId(), []byte("{\"name\": \"user\"}"))
req.Id = userResp.GetDetails().GetId()

View File

@@ -237,7 +237,7 @@ func TestServer_GetActiveIdentityProviders(t *testing.T) {
{
name: "permission error",
args: args{
ctx: instance.WithAuthorization(CTX, integration.UserTypeLogin),
ctx: instance.WithAuthorization(CTX, integration.UserTypeNoPermission),
req: &settings.GetActiveIdentityProvidersRequest{},
},
wantErr: true,

View File

@@ -43,7 +43,7 @@ func TestMain(m *testing.M) {
Instance = integration.NewInstance(ctx)
UserCTX = Instance.WithAuthorization(ctx, integration.UserTypeLogin)
UserCTX = Instance.WithAuthorization(ctx, integration.UserTypeNoPermission)
IamCTX = Instance.WithAuthorization(ctx, integration.UserTypeIAMOwner)
SystemCTX = integration.WithSystemAuthorization(ctx)
CTX = Instance.WithAuthorization(ctx, integration.UserTypeOrgOwner)

View File

@@ -41,7 +41,7 @@ func TestMain(m *testing.M) {
Instance = integration.NewInstance(ctx)
UserCTX = Instance.WithAuthorization(ctx, integration.UserTypeLogin)
UserCTX = Instance.WithAuthorization(ctx, integration.UserTypeNoPermission)
IamCTX = Instance.WithAuthorization(ctx, integration.UserTypeIAMOwner)
SystemCTX = integration.WithSystemAuthorization(ctx)
CTX = Instance.WithAuthorization(ctx, integration.UserTypeOrgOwner)