mirror of
https://github.com/andwati/zitadel.git
synced 2026-10-18 05:52:51 +00:00
feat: allow adding trusted domains in instance setup (#11169)
# Which Problems Are Solved When running Zitadel behind a reverse proxy and especially when the API and the login UI don't run on the same domain, Zitadel needs to be configured to trust the corresponding domains and use them in public responses, like email links and more. This can be done by adding a trusted domain. However it's currently only possible through the API and not in the instance setup process. # How the Problems Are Solved Added a possibility to configure multiple trusted domains in the first instance setup process. # Additional Changes None # Additional Context - closes #11153
This commit is contained in:
@@ -28,6 +28,7 @@ type FirstInstance struct {
|
|||||||
PatPath string
|
PatPath string
|
||||||
LoginClientPatPath string
|
LoginClientPatPath string
|
||||||
Features *command.InstanceFeatures
|
Features *command.InstanceFeatures
|
||||||
|
TrustedDomains []string
|
||||||
|
|
||||||
Skip bool
|
Skip bool
|
||||||
|
|
||||||
@@ -102,6 +103,7 @@ func (mig *FirstInstance) Execute(ctx context.Context, _ eventstore.Event) error
|
|||||||
|
|
||||||
mig.instanceSetup.InstanceName = mig.InstanceName
|
mig.instanceSetup.InstanceName = mig.InstanceName
|
||||||
mig.instanceSetup.CustomDomain = mig.externalDomain
|
mig.instanceSetup.CustomDomain = mig.externalDomain
|
||||||
|
mig.instanceSetup.TrustedDomains = mig.TrustedDomains
|
||||||
mig.instanceSetup.DefaultLanguage = mig.DefaultLanguage
|
mig.instanceSetup.DefaultLanguage = mig.DefaultLanguage
|
||||||
mig.instanceSetup.Org = mig.Org
|
mig.instanceSetup.Org = mig.Org
|
||||||
// check if username is email style or else append @<orgname>.<custom-domain>
|
// check if username is email style or else append @<orgname>.<custom-domain>
|
||||||
|
|||||||
@@ -121,6 +121,11 @@ FirstInstance:
|
|||||||
PatPath: # ZITADEL_FIRSTINSTANCE_PATPATH
|
PatPath: # ZITADEL_FIRSTINSTANCE_PATPATH
|
||||||
LoginClientPatPath: # ZITADEL_FIRSTINSTANCE_LOGINCLIENTPATPATH
|
LoginClientPatPath: # ZITADEL_FIRSTINSTANCE_LOGINCLIENTPATPATH
|
||||||
InstanceName: ZITADEL # ZITADEL_FIRSTINSTANCE_INSTANCENAME
|
InstanceName: ZITADEL # ZITADEL_FIRSTINSTANCE_INSTANCENAME
|
||||||
|
# A list of trusted domains, which are allowed to be used for public responses like email links or when running
|
||||||
|
# ZITADEL or the login UI behind a reverse proxy. If no trusted domain is set, the instance domain is used.
|
||||||
|
TrustedDomains: # ZITADEL_FIRSTINSTANCE_TRUSTEDDOMAINS
|
||||||
|
# - login.example.com
|
||||||
|
# - api.example.com
|
||||||
DefaultLanguage: en # ZITADEL_FIRSTINSTANCE_DEFAULTLANGUAGE
|
DefaultLanguage: en # ZITADEL_FIRSTINSTANCE_DEFAULTLANGUAGE
|
||||||
Org:
|
Org:
|
||||||
Name: ZITADEL # ZITADEL_FIRSTINSTANCE_ORG_NAME
|
Name: ZITADEL # ZITADEL_FIRSTINSTANCE_ORG_NAME
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ type InstanceSetup struct {
|
|||||||
zitadel ZitadelConfig
|
zitadel ZitadelConfig
|
||||||
InstanceName string
|
InstanceName string
|
||||||
CustomDomain string
|
CustomDomain string
|
||||||
|
TrustedDomains []string
|
||||||
DefaultLanguage language.Tag
|
DefaultLanguage language.Tag
|
||||||
Org InstanceOrgSetup
|
Org InstanceOrgSetup
|
||||||
SecretGenerators *SecretGenerators
|
SecretGenerators *SecretGenerators
|
||||||
@@ -294,6 +295,7 @@ func setUpInstance(ctx context.Context, c *Commands, setup *InstanceSetup) (vali
|
|||||||
return nil, nil, nil, nil, err
|
return nil, nil, nil, nil, err
|
||||||
}
|
}
|
||||||
setupCustomDomain(c, &validations, instanceAgg, setup.CustomDomain)
|
setupCustomDomain(c, &validations, instanceAgg, setup.CustomDomain)
|
||||||
|
setupTrustedDomains(c, &validations, instanceAgg, setup.TrustedDomains)
|
||||||
|
|
||||||
// optional setting if set
|
// optional setting if set
|
||||||
setupMessageTexts(&validations, setup.MessageTexts, instanceAgg)
|
setupMessageTexts(&validations, setup.MessageTexts, instanceAgg)
|
||||||
@@ -511,6 +513,14 @@ func setupGeneratedDomain(ctx context.Context, commands *Commands, validations *
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func setupTrustedDomains(commands *Commands, validations *[]preparation.Validation, instanceAgg *instance.Aggregate, trustedDomains []string) {
|
||||||
|
for _, trustedDomain := range trustedDomains {
|
||||||
|
*validations = append(*validations,
|
||||||
|
commands.setupTrustedDomain(instanceAgg, trustedDomain),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func setupMinimalInterfaces(commands *Commands, validations *[]preparation.Validation, instanceAgg *instance.Aggregate, orgAgg *org.Aggregate, projectOwner string, ids ZitadelConfig) {
|
func setupMinimalInterfaces(commands *Commands, validations *[]preparation.Validation, instanceAgg *instance.Aggregate, orgAgg *org.Aggregate, projectOwner string, ids ZitadelConfig) {
|
||||||
projectAgg := project.NewAggregate(ids.projectID, orgAgg.ID)
|
projectAgg := project.NewAggregate(ids.projectID, orgAgg.ID)
|
||||||
|
|
||||||
|
|||||||
@@ -365,19 +365,28 @@ func instanceElementsConfig() *SecretGenerators {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func setupInstanceFilters(instanceID, orgID, projectID, appID, domain string) []expect {
|
func setupInstanceFilters(instanceID, orgID, projectID, appID, domain string, trustedDomains []string) []expect {
|
||||||
return slices.Concat(
|
return slices.Concat(
|
||||||
setupInstanceElementsFilters(instanceID),
|
setupInstanceElementsFilters(instanceID),
|
||||||
orgFilters(orgID, true, true, true),
|
orgFilters(orgID, true, true, true),
|
||||||
generatedDomainFilters(instanceID, orgID, projectID, appID, domain),
|
generatedDomainFilters(instanceID, orgID, projectID, appID, domain),
|
||||||
|
generatedTrustedDomainsFilters(trustedDomains),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func setupInstanceEvents(ctx context.Context, instanceID, orgID, projectID, appID, instanceName, orgName string, defaultLanguage language.Tag, domain string, externalSecure bool) []eventstore.Command {
|
func setupInstanceEvents(
|
||||||
|
ctx context.Context,
|
||||||
|
instanceID, orgID, projectID, appID, instanceName, orgName string,
|
||||||
|
defaultLanguage language.Tag,
|
||||||
|
domain string,
|
||||||
|
externalSecure bool,
|
||||||
|
trustedDomains []string,
|
||||||
|
) []eventstore.Command {
|
||||||
return slices.Concat(
|
return slices.Concat(
|
||||||
setupInstanceElementsEvents(ctx, instanceID, instanceName, defaultLanguage),
|
setupInstanceElementsEvents(ctx, instanceID, instanceName, defaultLanguage),
|
||||||
orgEvents(ctx, instanceID, orgID, orgName, projectID, domain, externalSecure, true, true, true),
|
orgEvents(ctx, instanceID, orgID, orgName, projectID, domain, externalSecure, true, true, true),
|
||||||
generatedDomainEvents(ctx, instanceID, orgID, projectID, appID, domain),
|
generatedDomainEvents(ctx, instanceID, orgID, projectID, appID, domain),
|
||||||
|
generatedTrustedDomainEvents(ctx, instanceID, trustedDomains),
|
||||||
instanceCreatedMilestoneEvent(ctx, instanceID),
|
instanceCreatedMilestoneEvent(ctx, instanceID),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -391,6 +400,7 @@ func setupInstanceConfig() *InstanceSetup {
|
|||||||
LoginClient: instanceSetupLoginClientConfig(),
|
LoginClient: instanceSetupLoginClientConfig(),
|
||||||
}
|
}
|
||||||
conf.CustomDomain = ""
|
conf.CustomDomain = ""
|
||||||
|
conf.TrustedDomains = []string{"trusted-domain.com", "another-trusted-domain.com"}
|
||||||
return conf
|
return conf
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -409,6 +419,14 @@ func generatedDomainEvents(ctx context.Context, instanceID, orgID, projectID, ap
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func generatedTrustedDomainEvents(ctx context.Context, instanceID string, trustedDomains []string) []eventstore.Command {
|
||||||
|
commands := make([]eventstore.Command, len(trustedDomains))
|
||||||
|
for i, trustedDomain := range trustedDomains {
|
||||||
|
commands[i] = instance.NewTrustedDomainAddedEvent(ctx, &instance.NewAggregate(instanceID).Aggregate, trustedDomain)
|
||||||
|
}
|
||||||
|
return commands
|
||||||
|
}
|
||||||
|
|
||||||
func instanceCreatedMilestoneEvent(ctx context.Context, instanceID string) []eventstore.Command {
|
func instanceCreatedMilestoneEvent(ctx context.Context, instanceID string) []eventstore.Command {
|
||||||
return []eventstore.Command{
|
return []eventstore.Command{
|
||||||
milestone.NewReachedEvent(ctx, milestone.NewInstanceAggregate(instanceID), milestone.InstanceCreated),
|
milestone.NewReachedEvent(ctx, milestone.NewInstanceAggregate(instanceID), milestone.InstanceCreated),
|
||||||
@@ -463,6 +481,14 @@ func generatedDomainFilters(instanceID, orgID, projectID, appID, generatedDomain
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func generatedTrustedDomainsFilters(trustedDomains []string) []expect {
|
||||||
|
filters := make([]expect, len(trustedDomains))
|
||||||
|
for i := range trustedDomains {
|
||||||
|
filters[i] = expectFilter()
|
||||||
|
}
|
||||||
|
return filters
|
||||||
|
}
|
||||||
|
|
||||||
func humanFilters(orgID string) []expect {
|
func humanFilters(orgID string) []expect {
|
||||||
return []expect{
|
return []expect{
|
||||||
expectFilter(),
|
expectFilter(),
|
||||||
@@ -1324,7 +1350,7 @@ func TestCommandSide_setUpInstance(t *testing.T) {
|
|||||||
fields: fields{
|
fields: fields{
|
||||||
eventstore: expectEventstore(
|
eventstore: expectEventstore(
|
||||||
slices.Concat(
|
slices.Concat(
|
||||||
setupInstanceFilters("INSTANCE", "ORG", "PROJECT", "console-id", "DOMAIN"),
|
setupInstanceFilters("INSTANCE", "ORG", "PROJECT", "console-id", "DOMAIN", []string{"trusted-domain.com", "another-trusted-domain.com"}),
|
||||||
[]expect{
|
[]expect{
|
||||||
expectPush(
|
expectPush(
|
||||||
setupInstanceEvents(context.Background(),
|
setupInstanceEvents(context.Background(),
|
||||||
@@ -1337,6 +1363,7 @@ func TestCommandSide_setUpInstance(t *testing.T) {
|
|||||||
language.English,
|
language.English,
|
||||||
"DOMAIN",
|
"DOMAIN",
|
||||||
false,
|
false,
|
||||||
|
[]string{"trusted-domain.com", "another-trusted-domain.com"},
|
||||||
)...,
|
)...,
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -5,21 +5,20 @@ import (
|
|||||||
"slices"
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/zitadel/zitadel/internal/command/preparation"
|
||||||
"github.com/zitadel/zitadel/internal/domain"
|
"github.com/zitadel/zitadel/internal/domain"
|
||||||
|
"github.com/zitadel/zitadel/internal/eventstore"
|
||||||
"github.com/zitadel/zitadel/internal/repository/instance"
|
"github.com/zitadel/zitadel/internal/repository/instance"
|
||||||
"github.com/zitadel/zitadel/internal/zerrors"
|
"github.com/zitadel/zitadel/internal/zerrors"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (c *Commands) AddTrustedDomain(ctx context.Context, trustedDomain string) (*domain.ObjectDetails, error) {
|
func (c *Commands) AddTrustedDomain(ctx context.Context, trustedDomain string) (details *domain.ObjectDetails, err error) {
|
||||||
trustedDomain = strings.TrimSpace(trustedDomain)
|
trustedDomain, err = validateTrustedDomain(trustedDomain)
|
||||||
if trustedDomain == "" || len(trustedDomain) > 253 {
|
if err != nil {
|
||||||
return nil, zerrors.ThrowInvalidArgument(nil, "COMMA-Stk21", "Errors.Invalid.Argument")
|
return nil, err
|
||||||
}
|
|
||||||
if !allowDomainRunes.MatchString(trustedDomain) {
|
|
||||||
return nil, zerrors.ThrowInvalidArgument(nil, "COMMA-S3v3w", "Errors.Instance.Domain.InvalidCharacter")
|
|
||||||
}
|
}
|
||||||
model := NewInstanceTrustedDomainsWriteModel(ctx)
|
model := NewInstanceTrustedDomainsWriteModel(ctx)
|
||||||
err := c.eventstore.FilterToQueryReducer(ctx, model)
|
err = c.eventstore.FilterToQueryReducer(ctx, model)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -33,6 +32,42 @@ func (c *Commands) AddTrustedDomain(ctx context.Context, trustedDomain string) (
|
|||||||
return writeModelToObjectDetails(&model.WriteModel), nil
|
return writeModelToObjectDetails(&model.WriteModel), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// setupTrustedDomain is only used for the instance setup process where all events are prepared and pushed in a single transaction
|
||||||
|
func (c *Commands) setupTrustedDomain(a *instance.Aggregate, trustedDomain string) preparation.Validation {
|
||||||
|
return func() (_ preparation.CreateCommands, err error) {
|
||||||
|
trustedDomain, err = validateTrustedDomain(trustedDomain)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) {
|
||||||
|
model := NewInstanceTrustedDomainsWriteModel(ctx)
|
||||||
|
events, err := filter(ctx, model.Query())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
err = AppendAndReduce(model, events...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if slices.Contains(model.Domains, trustedDomain) {
|
||||||
|
return nil, zerrors.ThrowPreconditionFailed(nil, "COMMA-hg42a", "Errors.Instance.Domain.AlreadyExists")
|
||||||
|
}
|
||||||
|
return []eventstore.Command{instance.NewTrustedDomainAddedEvent(ctx, &a.Aggregate, trustedDomain)}, nil
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateTrustedDomain(trustedDomain string) (string, error) {
|
||||||
|
trustedDomain = strings.TrimSpace(trustedDomain)
|
||||||
|
if trustedDomain == "" || len(trustedDomain) > 253 {
|
||||||
|
return "", zerrors.ThrowInvalidArgument(nil, "COMMA-Stk21", "Errors.Invalid.Argument")
|
||||||
|
}
|
||||||
|
if !allowDomainRunes.MatchString(trustedDomain) {
|
||||||
|
return "", zerrors.ThrowInvalidArgument(nil, "COMMA-S3v3w", "Errors.Instance.Domain.InvalidCharacter")
|
||||||
|
}
|
||||||
|
return trustedDomain, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (c *Commands) RemoveTrustedDomain(ctx context.Context, trustedDomain string, errorIfNotFound bool) (*domain.ObjectDetails, error) {
|
func (c *Commands) RemoveTrustedDomain(ctx context.Context, trustedDomain string, errorIfNotFound bool) (*domain.ObjectDetails, error) {
|
||||||
trustedDomain = strings.TrimSpace(trustedDomain)
|
trustedDomain = strings.TrimSpace(trustedDomain)
|
||||||
if trustedDomain == "" || len(trustedDomain) > 253 {
|
if trustedDomain == "" || len(trustedDomain) > 253 {
|
||||||
|
|||||||
Reference in New Issue
Block a user