mirror of
https://github.com/zitadel/zitadel.git
synced 2026-08-19 01:14:48 -05:00
fix(api): honor request-provided IDs for OIDC apps (#11506)
Problem: OIDC CreateApplication requests in both app/v2beta and application/v2 accept an ID field (id / application_id), but the OIDC creation path dropped it, always generating a new app ID/client ID. Changes: Thread the request-provided ID into the OIDC domain request in both handlers. Update the OIDC command path to use the provided AppID when present (fallback to generated ID when empty), mirroring API apps. Add integration tests covering provided IDs for OIDC CreateApplication on v2beta and v2. Tests: go test -count=1 -tags integration ./internal/api/grpc/app/v2beta/integration_test -run TestCreateOIDCApplication_WithProvidedID go test -count=1 -tags integration ./internal/api/grpc/application/v2/integration_test -run TestCreateOIDCApplication_WithProvidedID Co-Authored-By: [TheRobotCarlson](https://github.com/TheRobotCarlson) Co-Authored-By: Warp [agent@warp.dev](mailto:agent@warp.dev) --------- Co-authored-by: Brian Carlson <briancarlson6174@gmail.com> Co-authored-by: Warp <agent@warp.dev> --------- Shoutout: [TheRobotCarlson](https://github.com/TheRobotCarlson) Shoutout: [ostempel](https://github.com/ostempel)
This commit is contained in:
committed by
GitHub
co-authored by
[TheRobotCarlson](https://github.com/TheRobotCarlson)
Warp [agent@warp.dev](mailto:agent@warp.dev)
Brian Carlson
Warp
parent
e77e0831a6
commit
944765f89c
@@ -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
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user