diff --git a/internal/api/grpc/app/v2beta/app.go b/internal/api/grpc/app/v2beta/app.go index e751bf503..4c5a5a2d3 100644 --- a/internal/api/grpc/app/v2beta/app.go +++ b/internal/api/grpc/app/v2beta/app.go @@ -34,7 +34,7 @@ func (s *Server) CreateApplication(ctx context.Context, req *connect.Request[app }), nil case *app.CreateApplicationRequest_OidcRequest: - oidcAppRequest, err := convert.CreateOIDCAppRequestToDomain(req.Msg.GetName(), req.Msg.GetProjectId(), req.Msg.GetOidcRequest()) + oidcAppRequest, err := convert.CreateOIDCAppRequestToDomain(req.Msg.GetName(), req.Msg.GetId(), req.Msg.GetProjectId(), req.Msg.GetOidcRequest()) if err != nil { return nil, err } diff --git a/internal/api/grpc/app/v2beta/convert/oidc_app.go b/internal/api/grpc/app/v2beta/convert/oidc_app.go index 223e43d16..f21b5a22f 100644 --- a/internal/api/grpc/app/v2beta/convert/oidc_app.go +++ b/internal/api/grpc/app/v2beta/convert/oidc_app.go @@ -10,7 +10,7 @@ import ( app "github.com/zitadel/zitadel/pkg/grpc/app/v2beta" ) -func CreateOIDCAppRequestToDomain(name, projectID string, req *app.CreateOIDCApplicationRequest) (*domain.OIDCApp, error) { +func CreateOIDCAppRequestToDomain(name, appID, projectID string, req *app.CreateOIDCApplicationRequest) (*domain.OIDCApp, error) { loginVersion, loginBaseURI, err := loginVersionToDomain(req.GetLoginVersion()) if err != nil { return nil, err @@ -19,6 +19,7 @@ func CreateOIDCAppRequestToDomain(name, projectID string, req *app.CreateOIDCApp ObjectRoot: models.ObjectRoot{ AggregateID: projectID, }, + AppID: appID, AppName: name, OIDCVersion: gu.Ptr(domain.OIDCVersionV1), RedirectUris: req.GetRedirectUris(), diff --git a/internal/api/grpc/app/v2beta/convert/oidc_app_test.go b/internal/api/grpc/app/v2beta/convert/oidc_app_test.go index a6b3f0b70..3e8266196 100644 --- a/internal/api/grpc/app/v2beta/convert/oidc_app_test.go +++ b/internal/api/grpc/app/v2beta/convert/oidc_app_test.go @@ -21,6 +21,7 @@ func TestCreateOIDCAppRequestToDomain(t *testing.T) { tt := []struct { testName string projectID string + appID string req *app.CreateOIDCApplicationRequest expectedModel *domain.OIDCApp @@ -29,6 +30,7 @@ func TestCreateOIDCAppRequestToDomain(t *testing.T) { { testName: "unparsable login version 2 URL", projectID: "pid", + appID: "aid", req: &app.CreateOIDCApplicationRequest{ LoginVersion: &app.LoginVersion{Version: &app.LoginVersion_LoginV2{ LoginV2: &app.LoginV2{BaseUri: gu.Ptr("%+o")}}, @@ -44,6 +46,7 @@ func TestCreateOIDCAppRequestToDomain(t *testing.T) { { testName: "all fields set", projectID: "project1", + appID: "app1", req: &app.CreateOIDCApplicationRequest{ RedirectUris: []string{"https://redirect"}, ResponseTypes: []app.OIDCResponseType{app.OIDCResponseType_OIDC_RESPONSE_TYPE_CODE}, @@ -67,6 +70,7 @@ func TestCreateOIDCAppRequestToDomain(t *testing.T) { expectedModel: &domain.OIDCApp{ ObjectRoot: models.ObjectRoot{AggregateID: "project1"}, AppName: "all fields set", + AppID: "app1", OIDCVersion: gu.Ptr(domain.OIDCVersionV1), RedirectUris: []string{"https://redirect"}, ResponseTypes: []domain.OIDCResponseType{domain.OIDCResponseTypeCode}, @@ -94,7 +98,7 @@ func TestCreateOIDCAppRequestToDomain(t *testing.T) { t.Parallel() // When - res, err := CreateOIDCAppRequestToDomain(tc.testName, tc.projectID, tc.req) + res, err := CreateOIDCAppRequestToDomain(tc.testName, tc.appID, tc.projectID, tc.req) // Then assert.Equal(t, tc.expectedError, err) diff --git a/internal/api/grpc/application/v2/application.go b/internal/api/grpc/application/v2/application.go index 797504d19..dd86e45d4 100644 --- a/internal/api/grpc/application/v2/application.go +++ b/internal/api/grpc/application/v2/application.go @@ -34,7 +34,7 @@ func (s *Server) CreateApplication(ctx context.Context, req *connect.Request[app }), nil case *application.CreateApplicationRequest_OidcConfiguration: - oidcAppRequest, err := convert.CreateOIDCAppRequestToDomain(req.Msg.GetName(), req.Msg.GetProjectId(), req.Msg.GetOidcConfiguration()) + oidcAppRequest, err := convert.CreateOIDCAppRequestToDomain(req.Msg.GetName(), req.Msg.GetApplicationId(), req.Msg.GetProjectId(), req.Msg.GetOidcConfiguration()) if err != nil { return nil, err } diff --git a/internal/api/grpc/application/v2/convert/oidc_app.go b/internal/api/grpc/application/v2/convert/oidc_app.go index 63a85b911..fc3765a43 100644 --- a/internal/api/grpc/application/v2/convert/oidc_app.go +++ b/internal/api/grpc/application/v2/convert/oidc_app.go @@ -10,7 +10,7 @@ import ( "github.com/zitadel/zitadel/pkg/grpc/application/v2" ) -func CreateOIDCAppRequestToDomain(name, projectID string, req *application.CreateOIDCApplicationRequest) (*domain.OIDCApp, error) { +func CreateOIDCAppRequestToDomain(name, appID, projectID string, req *application.CreateOIDCApplicationRequest) (*domain.OIDCApp, error) { loginVersion, loginBaseURI, err := loginVersionToDomain(req.GetLoginVersion()) if err != nil { return nil, err @@ -19,6 +19,7 @@ func CreateOIDCAppRequestToDomain(name, projectID string, req *application.Creat ObjectRoot: models.ObjectRoot{ AggregateID: projectID, }, + AppID: appID, AppName: name, OIDCVersion: gu.Ptr(domain.OIDCVersionV1), RedirectUris: req.GetRedirectUris(), diff --git a/internal/api/grpc/application/v2/convert/oidc_app_test.go b/internal/api/grpc/application/v2/convert/oidc_app_test.go index 508b7eda5..58ce20ed2 100644 --- a/internal/api/grpc/application/v2/convert/oidc_app_test.go +++ b/internal/api/grpc/application/v2/convert/oidc_app_test.go @@ -21,6 +21,7 @@ func TestCreateOIDCAppRequestToDomain(t *testing.T) { tt := []struct { testName string projectID string + appID string req *application.CreateOIDCApplicationRequest expectedModel *domain.OIDCApp @@ -29,6 +30,7 @@ func TestCreateOIDCAppRequestToDomain(t *testing.T) { { testName: "unparsable login version 2 URL", projectID: "pid", + appID: "aid", req: &application.CreateOIDCApplicationRequest{ LoginVersion: &application.LoginVersion{Version: &application.LoginVersion_LoginV2{ LoginV2: &application.LoginV2{BaseUri: gu.Ptr("%+o")}}, @@ -44,6 +46,7 @@ func TestCreateOIDCAppRequestToDomain(t *testing.T) { { testName: "all fields set", projectID: "project1", + appID: "app1", req: &application.CreateOIDCApplicationRequest{ RedirectUris: []string{"https://redirect"}, ResponseTypes: []application.OIDCResponseType{application.OIDCResponseType_OIDC_RESPONSE_TYPE_CODE}, @@ -67,6 +70,7 @@ func TestCreateOIDCAppRequestToDomain(t *testing.T) { expectedModel: &domain.OIDCApp{ ObjectRoot: models.ObjectRoot{AggregateID: "project1"}, AppName: "all fields set", + AppID: "app1", OIDCVersion: gu.Ptr(domain.OIDCVersionV1), RedirectUris: []string{"https://redirect"}, ResponseTypes: []domain.OIDCResponseType{domain.OIDCResponseTypeCode}, @@ -94,7 +98,7 @@ func TestCreateOIDCAppRequestToDomain(t *testing.T) { t.Parallel() // When - res, err := CreateOIDCAppRequestToDomain(tc.testName, tc.projectID, tc.req) + res, err := CreateOIDCAppRequestToDomain(tc.testName, tc.appID, tc.projectID, tc.req) // Then assert.Equal(t, tc.expectedError, err) diff --git a/internal/command/project_application_oidc.go b/internal/command/project_application_oidc.go index 2342ca9eb..7666f910a 100644 --- a/internal/command/project_application_oidc.go +++ b/internal/command/project_application_oidc.go @@ -158,10 +158,20 @@ func (c *Commands) AddOIDCApplication(ctx context.Context, oidcApp *domain.OIDCA return nil, zerrors.ThrowInvalidArgument(nil, "PROJECT-1n8df", "Errors.Project.App.Invalid") } - appID, err := c.idGenerator.Next() + appID := oidcApp.AppID + if appID == "" { + appID, err = c.idGenerator.Next() + if err != nil { + return nil, err + } + } + existingApp, err := c.getOIDCAppWriteModel(ctx, oidcApp.AggregateID, appID, resourceOwner) if err != nil { return nil, err } + if existingApp.State != domain.AppStateUnspecified { + return nil, zerrors.ThrowPreconditionFailed(nil, "PROJECT-lxowmp", "Errors.Project.App.AlreadyExisting") + } return c.addOIDCApplicationWithID(ctx, oidcApp, resourceOwner, appID) } diff --git a/internal/command/project_application_oidc_test.go b/internal/command/project_application_oidc_test.go index d728ffca4..2acb096e3 100644 --- a/internal/command/project_application_oidc_test.go +++ b/internal/command/project_application_oidc_test.go @@ -501,6 +501,7 @@ func TestCommandSide_AddOIDCApplication(t *testing.T) { ), ), expectFilter(), + expectFilter(), expectPush( project.NewApplicationAddedEvent(context.Background(), &project.NewAggregate("project1", "org1").Aggregate, @@ -596,6 +597,116 @@ func TestCommandSide_AddOIDCApplication(t *testing.T) { }, }, }, + { + name: "create oidc app with id, ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + project.NewProjectAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "project", true, true, true, + domain.PrivateLabelingSettingUnspecified), + ), + ), + expectFilter(), + expectFilter(), + expectPush( + project.NewApplicationAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "app2", + "app", + ), + project.NewOIDCConfigAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + domain.OIDCVersionV1, + "app2", + "client1", + "secret", + []string{"https://test.ch"}, + []domain.OIDCResponseType{domain.OIDCResponseTypeCode}, + []domain.OIDCGrantType{domain.OIDCGrantTypeAuthorizationCode}, + domain.OIDCApplicationTypeWeb, + domain.OIDCAuthMethodTypePost, + []string{"https://test.ch/logout"}, + true, + domain.OIDCTokenTypeBearer, + true, + true, + true, + time.Second*1, + []string{"https://sub.test.ch"}, + true, + "https://test.ch/backchannel", + domain.LoginVersion2, + "https://login.test.ch", + ), + ), + ), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "client1"), + }, + args: args{ + ctx: authz.WithInstanceID(context.Background(), "instanceID"), + oidcApp: &domain.OIDCApp{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "project1", + }, + AppID: "app2", + AppName: "app", + AuthMethodType: gu.Ptr(domain.OIDCAuthMethodTypePost), + OIDCVersion: gu.Ptr(domain.OIDCVersionV1), + RedirectUris: []string{"https://test.ch"}, + ResponseTypes: []domain.OIDCResponseType{domain.OIDCResponseTypeCode}, + GrantTypes: []domain.OIDCGrantType{domain.OIDCGrantTypeAuthorizationCode}, + ApplicationType: gu.Ptr(domain.OIDCApplicationTypeWeb), + PostLogoutRedirectUris: []string{"https://test.ch/logout"}, + DevMode: gu.Ptr(true), + AccessTokenType: gu.Ptr(domain.OIDCTokenTypeBearer), + AccessTokenRoleAssertion: gu.Ptr(true), + IDTokenRoleAssertion: gu.Ptr(true), + IDTokenUserinfoAssertion: gu.Ptr(true), + ClockSkew: gu.Ptr(time.Second * 1), + AdditionalOrigins: []string{"https://sub.test.ch"}, + SkipNativeAppSuccessPage: gu.Ptr(true), + BackChannelLogoutURI: gu.Ptr("https://test.ch/backchannel"), + LoginVersion: gu.Ptr(domain.LoginVersion2), + LoginBaseURI: gu.Ptr("https://login.test.ch"), + }, + resourceOwner: "org1", + }, + res: res{ + want: &domain.OIDCApp{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "project1", + ResourceOwner: "org1", + }, + AppID: "app2", + AppName: "app", + ClientID: "client1", + ClientSecretString: "secret", + AuthMethodType: gu.Ptr(domain.OIDCAuthMethodTypePost), + OIDCVersion: gu.Ptr(domain.OIDCVersionV1), + RedirectUris: []string{"https://test.ch"}, + ResponseTypes: []domain.OIDCResponseType{domain.OIDCResponseTypeCode}, + GrantTypes: []domain.OIDCGrantType{domain.OIDCGrantTypeAuthorizationCode}, + ApplicationType: gu.Ptr(domain.OIDCApplicationTypeWeb), + PostLogoutRedirectUris: []string{"https://test.ch/logout"}, + DevMode: gu.Ptr(true), + AccessTokenType: gu.Ptr(domain.OIDCTokenTypeBearer), + AccessTokenRoleAssertion: gu.Ptr(true), + IDTokenRoleAssertion: gu.Ptr(true), + IDTokenUserinfoAssertion: gu.Ptr(true), + ClockSkew: gu.Ptr(time.Second * 1), + AdditionalOrigins: []string{"https://sub.test.ch"}, + SkipNativeAppSuccessPage: gu.Ptr(true), + BackChannelLogoutURI: gu.Ptr("https://test.ch/backchannel"), + LoginVersion: gu.Ptr(domain.LoginVersion2), + LoginBaseURI: gu.Ptr("https://login.test.ch"), + State: domain.AppStateActive, + Compliance: &domain.Compliance{}, + }, + }, + }, { name: "create oidc app basic, ok", fields: fields{ @@ -609,6 +720,7 @@ func TestCommandSide_AddOIDCApplication(t *testing.T) { ), ), expectFilter(), + expectFilter(), expectPush( project.NewApplicationAddedEvent(context.Background(), &project.NewAggregate("project1", "org1").Aggregate,