fix: configure default url templates (#10416)

# Which Problems Are Solved

Emails are still send only with URLs to login v1.

# How the Problems Are Solved

Add configuration for URLs as URL templates, so that links can point at
Login v2.

# Additional Changes

None

# Additional Context

Closes #10236

---------

Co-authored-by: Marco A. <marco@zitadel.com>
This commit is contained in:
Stefan Benz
2025-08-26 12:14:41 +02:00
committed by GitHub
parent 2718d345b8
commit 0a14c01412
18 changed files with 370 additions and 77 deletions

View File

@@ -571,7 +571,11 @@ Login:
MaxAge: 12h # ZITADEL_LOGIN_CACHE_MAXAGE
# 168h is 7 days, one week
SharedMaxAge: 168h # ZITADEL_LOGIN_CACHE_SHAREDMAXAGE
DefaultOTPEmailURLV2: "/otp/verify?loginName={{.LoginName}}&code={{.Code}}" # ZITADEL_LOGIN_CACHE_DEFAULTOTPEMAILURLV2
DefaultPaths:
BasePath: "/ui/v2/login"
PasswordSetPath: "/password/set?code={{.Code}}&userId={{.UserID}}&organization={{.OrgID}}"
EmailCodePath: "/verify?code={{.Code}}&userId={{.UserID}}&organization={{.OrgID}}"
OTPEmailPath: "/otp/verify?code={{.Code}}&userID={{.UserID}}&sessionId={{.SessionID}}"
Console:
ShortCache:

View File

@@ -197,6 +197,8 @@ func projections(
config.OIDC.DefaultRefreshTokenExpiration,
config.OIDC.DefaultRefreshTokenIdleExpiration,
config.DefaultInstance.SecretGenerators,
nil,
nil,
)
logging.OnError(err).Fatal("unable to start commands")
@@ -219,7 +221,7 @@ func projections(
commands,
queries,
es,
config.Login.DefaultOTPEmailURLV2,
config.Login.DefaultPaths.OTPEmailPath,
config.SystemDefaults.Notifications.FileSystemPath,
keys.User,
keys.SMTP,

View File

@@ -92,6 +92,8 @@ func (mig *FirstInstance) Execute(ctx context.Context, _ eventstore.Event) error
0,
0,
nil,
nil,
nil,
)
if err != nil {
return err

View File

@@ -58,6 +58,8 @@ func (mig *externalConfigChange) Execute(ctx context.Context, _ eventstore.Event
0,
0,
nil,
nil,
nil,
)
if err != nil {

View File

@@ -527,6 +527,9 @@ func startCommandsQueries(
config.OIDC.DefaultRefreshTokenExpiration,
config.OIDC.DefaultRefreshTokenIdleExpiration,
config.DefaultInstance.SecretGenerators,
nil,
nil,
)
logging.OnError(err).Fatal("unable to start commands")
@@ -549,7 +552,7 @@ func startCommandsQueries(
commands,
queries,
eventstoreClient,
config.Login.DefaultOTPEmailURLV2,
config.Login.DefaultPaths.OTPEmailPath,
config.SystemDefaults.Notifications.FileSystemPath,
keys.User,
keys.SMTP,

View File

@@ -256,6 +256,8 @@ func startZitadel(ctx context.Context, config *Config, masterKey string, server
config.OIDC.DefaultRefreshTokenExpiration,
config.OIDC.DefaultRefreshTokenIdleExpiration,
config.DefaultInstance.SecretGenerators,
config.Login.DefaultEmailCodeURLTemplate,
config.Login.DefaultPasswordSetURLTemplate,
)
if err != nil {
return fmt.Errorf("cannot start commands: %w", err)
@@ -301,7 +303,7 @@ func startZitadel(ctx context.Context, config *Config, masterKey string, server
commands,
queries,
eventstoreClient,
config.Login.DefaultOTPEmailURLV2,
config.Login.DefaultPaths.OTPEmailPath,
config.SystemDefaults.Notifications.FileSystemPath,
keys.User,
keys.SMTP,

View File

@@ -4,6 +4,8 @@ import (
"context"
"golang.org/x/text/language"
"github.com/zitadel/zitadel/internal/feature"
)
type MockContextInstanceOpts func(i *instance)
@@ -14,6 +16,12 @@ func WithMockDefaultLanguage(lang language.Tag) MockContextInstanceOpts {
}
}
func WithMockFeatures(features feature.Features) MockContextInstanceOpts {
return func(i *instance) {
i.features = features
}
}
func NewMockContext(instanceID, orgID, userID string, opts ...MockContextInstanceOpts) context.Context {
ctx := context.WithValue(context.Background(), dataKey, CtxData{UserID: userID, OrgID: orgID})

View File

@@ -52,7 +52,43 @@ type Config struct {
AssetCache middleware.CacheConfig
// LoginV2
DefaultOTPEmailURLV2 string
DefaultPaths *DefaultPaths
}
type DefaultPaths struct {
BasePath string
PasswordSetPath string
EmailCodePath string
OTPEmailPath string
}
func (c *Config) defaultBaseURL(ctx context.Context) string {
loginV2 := authz.GetInstance(ctx).Features().LoginV2
if loginV2.Required {
// use the origin as default
baseURI := http_utils.DomainContext(ctx).Origin()
// use custom base URI if defined
if loginV2.BaseURI != nil && loginV2.BaseURI.String() != "" {
baseURI = loginV2.BaseURI.String()
}
return baseURI + c.DefaultPaths.BasePath
}
return ""
}
func (c *Config) DefaultEmailCodeURLTemplate(ctx context.Context) string {
basePath := c.defaultBaseURL(ctx)
if basePath == "" {
return ""
}
return basePath + c.DefaultPaths.EmailCodePath
}
func (c *Config) DefaultPasswordSetURLTemplate(ctx context.Context) string {
basePath := c.defaultBaseURL(ctx)
if basePath == "" {
return ""
}
return c.defaultBaseURL(ctx) + c.DefaultPaths.PasswordSetPath
}
const (

View File

@@ -0,0 +1,178 @@
package login
import (
"context"
"net/url"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/api/http"
"github.com/zitadel/zitadel/internal/feature"
)
func TestConfig_defaultBaseURL(t *testing.T) {
t.Parallel()
config := &Config{
DefaultPaths: &DefaultPaths{BasePath: "/basepath"},
}
baseCustomURI, err := url.Parse("https://custom")
require.Nil(t, err)
tt := []struct {
name string
inputCtx context.Context
http.DomainCtx
expected string
}{
{
name: "LoginV2 not required",
inputCtx: authz.NewMockContext("instance1", "org1", "user1"),
expected: "",
},
{
name: "LoginV2 required, no custom BaseURI",
inputCtx: http.WithDomainContext(
authz.NewMockContext("instance1", "org1", "user1",
authz.WithMockFeatures(feature.Features{LoginV2: feature.LoginV2{Required: true}}),
),
&http.DomainCtx{Protocol: "https", PublicHost: "origin"},
),
expected: "https://origin/basepath",
},
{
name: "LoginV2 required, custom BaseURI",
inputCtx: http.WithDomainContext(
authz.NewMockContext("instance1", "org1", "user1",
authz.WithMockFeatures(feature.Features{LoginV2: feature.LoginV2{Required: true, BaseURI: baseCustomURI}}),
),
&http.DomainCtx{Protocol: "https", PublicHost: "origin"},
),
expected: "https://custom/basepath",
},
{
name: "LoginV2 required, custom BaseURI empty string",
inputCtx: http.WithDomainContext(
authz.NewMockContext("instance1", "org1", "user1",
authz.WithMockFeatures(feature.Features{LoginV2: feature.LoginV2{Required: true, BaseURI: &url.URL{}}}),
),
&http.DomainCtx{Protocol: "https", PublicHost: "origin"},
),
expected: "https://origin/basepath",
},
}
for _, tc := range tt {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
result := config.defaultBaseURL(tc.inputCtx)
assert.Equal(t, tc.expected, result)
})
}
}
func TestConfig_DefaultEmailCodeURLTemplate(t *testing.T) {
t.Parallel()
tt := []struct {
testName string
inputCtx context.Context
expectedEmailURLTemplate string
}{
{
testName: "when base path is empty should return empty email url template",
inputCtx: http.WithDomainContext(
authz.NewMockContext("instance1", "org1", "user1",
authz.WithMockFeatures(feature.Features{LoginV2: feature.LoginV2{Required: false, BaseURI: &url.URL{}}}),
),
&http.DomainCtx{Protocol: "https", PublicHost: "origin"},
),
expectedEmailURLTemplate: "",
},
{
testName: "when base path is not empty should return expected url template",
inputCtx: http.WithDomainContext(
authz.NewMockContext("instance1", "org1", "user1",
authz.WithMockFeatures(feature.Features{LoginV2: feature.LoginV2{Required: true, BaseURI: &url.URL{}}}),
),
&http.DomainCtx{Protocol: "https", PublicHost: "origin"},
),
expectedEmailURLTemplate: "https://origin/basepath/email-code-path",
},
}
for _, tc := range tt {
t.Run(tc.testName, func(t *testing.T) {
t.Parallel()
// Given
c := &Config{
DefaultPaths: &DefaultPaths{
BasePath: "/basepath",
EmailCodePath: "/email-code-path"},
}
// Test
res := c.DefaultEmailCodeURLTemplate(tc.inputCtx)
// Verify
assert.Equal(t, tc.expectedEmailURLTemplate, res)
})
}
}
func TestConfig_DefaultPasswordSetURLTemplate(t *testing.T) {
t.Parallel()
tt := []struct {
testName string
inputCtx context.Context
expectedEmailURLTemplate string
}{
{
testName: "when base path is empty should return empty email url template",
inputCtx: http.WithDomainContext(
authz.NewMockContext("instance1", "org1", "user1",
authz.WithMockFeatures(feature.Features{LoginV2: feature.LoginV2{Required: false, BaseURI: &url.URL{}}}),
),
&http.DomainCtx{Protocol: "https", PublicHost: "origin"},
),
expectedEmailURLTemplate: "",
},
{
testName: "when base path is not empty should return expected url template",
inputCtx: http.WithDomainContext(
authz.NewMockContext("instance1", "org1", "user1",
authz.WithMockFeatures(feature.Features{LoginV2: feature.LoginV2{Required: true, BaseURI: &url.URL{}}}),
),
&http.DomainCtx{Protocol: "https", PublicHost: "origin"},
),
expectedEmailURLTemplate: "https://origin/basepath/password-set-path",
},
}
for _, tc := range tt {
t.Run(tc.testName, func(t *testing.T) {
t.Parallel()
// Given
c := &Config{
DefaultPaths: &DefaultPaths{
BasePath: "/basepath",
PasswordSetPath: "/password-set-path",
},
}
// Test
res := c.DefaultPasswordSetURLTemplate(tc.inputCtx)
// Verify
assert.Equal(t, tc.expectedEmailURLTemplate, res)
})
}
}

View File

@@ -99,6 +99,9 @@ type Commands struct {
// These instance's milestones never need to be invalidated,
// so the query and cache overhead can completely eliminated.
milestonesCompleted sync.Map
defaultEmailCodeURLTemplate func(ctx context.Context) string
defaultPasswordSetURLTemplate func(ctx context.Context) string
}
func StartCommands(
@@ -120,6 +123,8 @@ func StartCommands(
defaultRefreshTokenLifetime,
defaultRefreshTokenIdleLifetime time.Duration,
defaultSecretGenerators *SecretGenerators,
defaultEmailCodeURLTemplate func(ctx context.Context) string,
defaultPasswordSetURLTemplate func(ctx context.Context) string,
) (repo *Commands, err error) {
if externalDomain == "" {
return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-Df21s", "no external domain specified")
@@ -199,8 +204,10 @@ func StartCommands(
Issuer: defaults.Multifactors.OTP.Issuer,
},
},
GenerateDomain: domain.NewGeneratedInstanceDomain,
caches: caches,
GenerateDomain: domain.NewGeneratedInstanceDomain,
caches: caches,
defaultEmailCodeURLTemplate: defaultEmailCodeURLTemplate,
defaultPasswordSetURLTemplate: defaultPasswordSetURLTemplate,
}
if defaultSecretGenerators != nil && defaultSecretGenerators.ClientSecret != nil {

View File

@@ -304,6 +304,11 @@ func (c *Commands) addHumanCommandEmail(ctx context.Context, filter preparation.
if human.Email.ReturnCode {
human.EmailCode = &emailCode.Plain
}
if human.Email.URLTemplate == "" {
human.Email.URLTemplate = c.defaultEmailCodeURLTemplate(ctx)
}
return append(cmds, user.NewHumanEmailCodeAddedEventV2(ctx, &a.Aggregate, emailCode.Crypted, emailCode.Expiry, human.Email.URLTemplate, human.Email.ReturnCode, human.AuthRequestID)), nil
}
return cmds, nil

View File

@@ -8,6 +8,7 @@ import (
"github.com/muhlemmer/gu"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
"golang.org/x/text/language"
@@ -44,6 +45,7 @@ func TestCommandSide_AddHuman(t *testing.T) {
newCode encrypedCodeFunc
newEncryptedCodeWithDefault encryptedCodeWithDefaultFunc
defaultSecretGenerators *SecretGenerators
defaultEmailCodeURLTemplate func(ctx context.Context) string
}
type args struct {
ctx context.Context
@@ -539,10 +541,11 @@ func TestCommandSide_AddHuman(t *testing.T) {
),
),
),
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"),
userPasswordHasher: mockPasswordHasher("x"),
codeAlg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
newCode: mockEncryptedCode("userinit", time.Hour),
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"),
userPasswordHasher: mockPasswordHasher("x"),
codeAlg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
newCode: mockEncryptedCode("userinit", time.Hour),
defaultEmailCodeURLTemplate: func(_ context.Context) string { return "" },
},
args: args{
ctx: context.Background(),
@@ -680,16 +683,17 @@ func TestCommandSide_AddHuman(t *testing.T) {
Crypted: []byte("emailCode"),
},
1*time.Hour,
"",
"http://example.com/{{.user}}/email/{{.code}}",
true,
"",
),
),
),
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"),
userPasswordHasher: mockPasswordHasher("x"),
codeAlg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
newCode: mockEncryptedCode("emailCode", time.Hour),
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"),
userPasswordHasher: mockPasswordHasher("x"),
codeAlg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
newCode: mockEncryptedCode("emailCode", time.Hour),
defaultEmailCodeURLTemplate: func(_ context.Context) string { return "http://example.com/{{.user}}/email/{{.code}}" },
},
args: args{
ctx: context.Background(),
@@ -1477,12 +1481,11 @@ func TestCommandSide_AddHuman(t *testing.T) {
newEncryptedCode: tt.fields.newCode,
newEncryptedCodeWithDefault: tt.fields.newEncryptedCodeWithDefault,
defaultSecretGenerators: tt.fields.defaultSecretGenerators,
defaultEmailCodeURLTemplate: tt.fields.defaultEmailCodeURLTemplate,
}
err := r.AddHuman(tt.args.ctx, tt.args.orgID, tt.args.human, tt.args.allowInitMail)
if tt.res.err == nil {
if !assert.NoError(t, err) {
t.FailNow()
}
require.NoError(t, err)
} else if !tt.res.err(err) {
t.Errorf("got wrong err: %v ", err)
return

View File

@@ -154,6 +154,11 @@ func (c *Commands) changeUserEmailWithGeneratorEvents(ctx context.Context, userI
if err = cmd.Change(ctx, domain.EmailAddress(email)); err != nil {
return nil, err
}
if urlTmpl == "" {
urlTmpl = c.defaultEmailCodeURLTemplate(ctx)
}
if err = cmd.AddGeneratedCode(ctx, gen, urlTmpl, returnCode); err != nil {
return nil, err
}
@@ -171,6 +176,11 @@ func (c *Commands) sendUserEmailCodeWithGeneratorEvents(ctx context.Context, use
if existingCheck && cmd.model.Code == nil {
return nil, zerrors.ThrowPreconditionFailed(err, "EMAIL-5w5ilin4yt", "Errors.User.Code.Empty")
}
if urlTmpl == "" {
urlTmpl = c.defaultEmailCodeURLTemplate(ctx)
}
if err = cmd.AddGeneratedCode(ctx, gen, urlTmpl, returnCode); err != nil {
return nil, err
}

View File

@@ -1160,8 +1160,9 @@ func TestCommands_ChangeUserEmailVerified(t *testing.T) {
func TestCommands_changeUserEmailWithGenerator(t *testing.T) {
type fields struct {
eventstore *eventstore.Eventstore
checkPermission domain.PermissionCheck
eventstore *eventstore.Eventstore
checkPermission domain.PermissionCheck
defaultEmailCodeURLTemplate func(ctx context.Context) string
}
type args struct {
userID string
@@ -1320,11 +1321,13 @@ func TestCommands_changeUserEmailWithGenerator(t *testing.T) {
Crypted: []byte("a"),
},
time.Hour*1,
"", false, "",
"http://example.com/{{.user}}/email/{{.code}}",
false, "",
),
),
),
checkPermission: newMockPermissionCheckAllowed(),
checkPermission: newMockPermissionCheckAllowed(),
defaultEmailCodeURLTemplate: func(ctx context.Context) string { return "http://example.com/{{.user}}/email/{{.code}}" },
},
args: args{
userID: "user1",
@@ -1376,11 +1379,12 @@ func TestCommands_changeUserEmailWithGenerator(t *testing.T) {
Crypted: []byte("a"),
},
time.Hour*1,
"", true, "",
"http://example.com/{{.user}}/email/{{.code2}}", true, "",
),
),
),
checkPermission: newMockPermissionCheckAllowed(),
checkPermission: newMockPermissionCheckAllowed(),
defaultEmailCodeURLTemplate: func(ctx context.Context) string { return "http://example.com/{{.user}}/email/{{.code2}}" },
},
args: args{
userID: "user1",
@@ -1458,8 +1462,9 @@ func TestCommands_changeUserEmailWithGenerator(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := &Commands{
eventstore: tt.fields.eventstore,
checkPermission: tt.fields.checkPermission,
eventstore: tt.fields.eventstore,
checkPermission: tt.fields.checkPermission,
defaultEmailCodeURLTemplate: tt.fields.defaultEmailCodeURLTemplate,
}
got, err := c.changeUserEmailWithGenerator(context.Background(), tt.args.userID, tt.args.email, GetMockSecretGenerator(t), tt.args.returnCode, tt.args.urlTmpl)
require.ErrorIs(t, tt.wantErr, err)
@@ -1470,8 +1475,9 @@ func TestCommands_changeUserEmailWithGenerator(t *testing.T) {
func TestCommands_sendUserEmailCodeWithGeneratorEvents(t *testing.T) {
type fields struct {
eventstore *eventstore.Eventstore
checkPermission domain.PermissionCheck
eventstore *eventstore.Eventstore
checkPermission domain.PermissionCheck
defaultEmailCodeURLTemplate func(ctx context.Context) string
}
type args struct {
userID string
@@ -1573,11 +1579,12 @@ func TestCommands_sendUserEmailCodeWithGeneratorEvents(t *testing.T) {
Crypted: []byte("a"),
},
time.Hour*1,
"", false, "",
"http://example.com/{{.user}}/email/{{.code}}", false, "",
),
),
),
checkPermission: newMockPermissionCheckAllowed(),
checkPermission: newMockPermissionCheckAllowed(),
defaultEmailCodeURLTemplate: func(ctx context.Context) string { return "http://example.com/{{.user}}/email/{{.code}}" },
},
args: args{
userID: "user1",
@@ -1624,7 +1631,7 @@ func TestCommands_sendUserEmailCodeWithGeneratorEvents(t *testing.T) {
Crypted: []byte("a"),
},
time.Hour*1,
"", false, "",
"http://example.com/{{.user}}/email/{{.code2}}", false, "",
),
),
),
@@ -1638,11 +1645,12 @@ func TestCommands_sendUserEmailCodeWithGeneratorEvents(t *testing.T) {
Crypted: []byte("a"),
},
time.Hour*1,
"", false, "",
"http://example.com/{{.user}}/email/{{.code2}}", false, "",
),
),
),
checkPermission: newMockPermissionCheckAllowed(),
checkPermission: newMockPermissionCheckAllowed(),
defaultEmailCodeURLTemplate: func(ctx context.Context) string { return "http://example.com/{{.user}}/email/{{.code2}}" },
},
args: args{
userID: "user1",
@@ -1722,11 +1730,12 @@ func TestCommands_sendUserEmailCodeWithGeneratorEvents(t *testing.T) {
Crypted: []byte("a"),
},
time.Hour*1,
"", true, "",
"http://example.com/{{.user}}/email/{{.code}}", true, "",
),
),
),
checkPermission: newMockPermissionCheckAllowed(),
checkPermission: newMockPermissionCheckAllowed(),
defaultEmailCodeURLTemplate: func(ctx context.Context) string { return "http://example.com/{{.user}}/email/{{.code}}" },
},
args: args{
userID: "user1",
@@ -1800,8 +1809,9 @@ func TestCommands_sendUserEmailCodeWithGeneratorEvents(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := &Commands{
eventstore: tt.fields.eventstore,
checkPermission: tt.fields.checkPermission,
eventstore: tt.fields.eventstore,
checkPermission: tt.fields.checkPermission,
defaultEmailCodeURLTemplate: tt.fields.defaultEmailCodeURLTemplate,
}
got, err := c.sendUserEmailCodeWithGenerator(context.Background(), tt.args.userID, GetMockSecretGenerator(t), tt.args.returnCode, tt.args.urlTmpl, tt.args.checkExisting)
require.ErrorIs(t, err, tt.wantErr)

View File

@@ -411,6 +411,10 @@ func (c *Commands) changeUserEmail(ctx context.Context, cmds []eventstore.Comman
if err != nil {
return cmds, code, err
}
if email.URLTemplate == "" {
email.URLTemplate = c.defaultEmailCodeURLTemplate(ctx)
}
cmds = append(cmds, user.NewHumanEmailCodeAddedEventV2(ctx, &wm.Aggregate().Aggregate, cryptoCode.Crypted, cryptoCode.Expiry, email.URLTemplate, email.ReturnCode, ""))
if email.ReturnCode {
code = &cryptoCode.Plain

View File

@@ -43,6 +43,7 @@ func TestCommandSide_AddUserHuman(t *testing.T) {
newEncryptedCodeWithDefault encryptedCodeWithDefaultFunc
checkPermission domain.PermissionCheck
defaultSecretGenerators *SecretGenerators
defaultEmailCodeURLTemplate func(ctx context.Context) string
}
type args struct {
ctx context.Context
@@ -500,15 +501,16 @@ func TestCommandSide_AddUserHuman(t *testing.T) {
Crypted: []byte("emailverify"),
},
1*time.Hour,
"",
"http://example.com/{{.user}}/email/{{.code}}",
false,
"",
),
),
),
checkPermission: newMockPermissionCheckAllowed(),
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"),
newCode: mockEncryptedCode("emailverify", time.Hour),
checkPermission: newMockPermissionCheckAllowed(),
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"),
newCode: mockEncryptedCode("emailverify", time.Hour),
defaultEmailCodeURLTemplate: func(ctx context.Context) string { return "http://example.com/{{.user}}/email/{{.code}}" },
},
args: args{
ctx: context.Background(),
@@ -647,16 +649,17 @@ func TestCommandSide_AddUserHuman(t *testing.T) {
Crypted: []byte("emailCode"),
},
1*time.Hour,
"",
"http://example.com/{{.user}}/email/{{.code}}",
true,
"",
),
),
),
checkPermission: newMockPermissionCheckAllowed(),
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"),
userPasswordHasher: mockPasswordHasher("x"),
newCode: mockEncryptedCode("emailCode", time.Hour),
checkPermission: newMockPermissionCheckAllowed(),
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"),
userPasswordHasher: mockPasswordHasher("x"),
newCode: mockEncryptedCode("emailCode", time.Hour),
defaultEmailCodeURLTemplate: func(ctx context.Context) string { return "http://example.com/{{.user}}/email/{{.code}}" },
},
args: args{
ctx: context.Background(),
@@ -1519,7 +1522,7 @@ func TestCommandSide_AddUserHuman(t *testing.T) {
),
expectPush(
newRegisterHumanEvent("email@test.ch", "", false, true, "", language.English),
user.NewHumanEmailCodeAddedEvent(
user.NewHumanEmailCodeAddedEventV2(
context.Background(),
&userAgg.Aggregate,
&crypto.CryptoValue{
@@ -1529,6 +1532,8 @@ func TestCommandSide_AddUserHuman(t *testing.T) {
Crypted: []byte("mailVerify"),
},
time.Hour,
"http://example.com/{{.user}}/email/{{.code}}",
false,
"authRequestID",
),
user.NewUserIDPLinkAddedEvent(
@@ -1540,9 +1545,10 @@ func TestCommandSide_AddUserHuman(t *testing.T) {
),
),
),
checkPermission: newMockPermissionCheckAllowed(),
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"),
newCode: mockEncryptedCode("mailVerify", time.Hour),
checkPermission: newMockPermissionCheckAllowed(),
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"),
newCode: mockEncryptedCode("mailVerify", time.Hour),
defaultEmailCodeURLTemplate: func(ctx context.Context) string { return "http://example.com/{{.user}}/email/{{.code}}" },
},
args: args{
ctx: context.Background(),
@@ -2162,6 +2168,7 @@ func TestCommandSide_AddUserHuman(t *testing.T) {
CryptoMFA: cryptoAlg,
},
},
defaultEmailCodeURLTemplate: tt.fields.defaultEmailCodeURLTemplate,
}
err := r.AddUserHuman(tt.args.ctx, tt.args.orgID, tt.args.human, tt.args.allowInitMail, tt.args.codeAlg)
if tt.res.err == nil {
@@ -2199,6 +2206,7 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) {
newEncryptedCodeWithDefault encryptedCodeWithDefaultFunc
checkPermission domain.PermissionCheck
defaultSecretGenerators *SecretGenerators
defaultEmailCodeURLTemplate func(ctx context.Context) string
}
type args struct {
ctx context.Context
@@ -2555,14 +2563,15 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) {
Crypted: []byte("emailCode"),
},
time.Hour,
"",
"http://example.com/{{.user}}/email/{{.code}}",
false,
"",
),
),
),
checkPermission: newMockPermissionCheckAllowed(),
newCode: mockEncryptedCode("emailCode", time.Hour),
checkPermission: newMockPermissionCheckAllowed(),
newCode: mockEncryptedCode("emailCode", time.Hour),
defaultEmailCodeURLTemplate: func(ctx context.Context) string { return "http://example.com/{{.user}}/email/{{.code}}" },
},
args: args{
ctx: context.Background(),
@@ -2735,14 +2744,15 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) {
Crypted: []byte("emailCode"),
},
time.Hour,
"",
"http://example.com/{{.user}}/email/{{.code}}",
true,
"",
),
),
),
checkPermission: newMockPermissionCheckAllowed(),
newCode: mockEncryptedCode("emailCode", time.Hour),
checkPermission: newMockPermissionCheckAllowed(),
newCode: mockEncryptedCode("emailCode", time.Hour),
defaultEmailCodeURLTemplate: func(ctx context.Context) string { return "http://example.com/{{.user}}/email/{{.code}}" },
},
args: args{
ctx: context.Background(),
@@ -3747,6 +3757,7 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) {
checkPermission: tt.fields.checkPermission,
defaultSecretGenerators: tt.fields.defaultSecretGenerators,
userEncryption: tt.args.codeAlg,
defaultEmailCodeURLTemplate: tt.fields.defaultEmailCodeURLTemplate,
}
err := r.ChangeUserHuman(tt.args.ctx, tt.args.human, tt.args.codeAlg)
if tt.res.err == nil {

View File

@@ -62,6 +62,9 @@ func (c *Commands) requestPasswordReset(ctx context.Context, userID string, retu
if err != nil {
return nil, nil, err
}
if urlTmpl == "" {
urlTmpl = c.defaultPasswordSetURLTemplate(ctx)
}
cmd := user.NewHumanPasswordCodeAddedEventV2(ctx, UserAggregateFromWriteModelCtx(ctx, &model.WriteModel), passwordCode.CryptedCode(), passwordCode.CodeExpiry(), notificationType, urlTmpl, returnCode, generatorID)
if returnCode {

View File

@@ -5,11 +5,10 @@ import (
"testing"
"time"
"golang.org/x/text/language"
"github.com/muhlemmer/gu"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/text/language"
"github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/domain"
@@ -339,12 +338,13 @@ func TestCommands_requestPasswordReset(t *testing.T) {
},
}
type fields struct {
checkPermission domain.PermissionCheck
eventstore func(t *testing.T) *eventstore.Eventstore
userEncryption crypto.EncryptionAlgorithm
newCode encrypedCodeFunc
newEncryptedCodeWithDefault encryptedCodeWithDefaultFunc
defaultSecretGenerators *SecretGenerators
checkPermission domain.PermissionCheck
eventstore func(t *testing.T) *eventstore.Eventstore
userEncryption crypto.EncryptionAlgorithm
newCode encrypedCodeFunc
newEncryptedCodeWithDefault encryptedCodeWithDefaultFunc
defaultSecretGenerators *SecretGenerators
defaultPasswordSetURLTemplate func(ctx context.Context) string
}
type args struct {
ctx context.Context
@@ -460,14 +460,15 @@ func TestCommands_requestPasswordReset(t *testing.T) {
},
10*time.Minute,
domain.NotificationTypeEmail,
"",
"http://example.com/{{.user}}/password/{{.code}}",
false,
"",
),
),
),
checkPermission: newMockPermissionCheckAllowed(),
newCode: mockEncryptedCode("code", 10*time.Minute),
checkPermission: newMockPermissionCheckAllowed(),
newCode: mockEncryptedCode("code", 10*time.Minute),
defaultPasswordSetURLTemplate: func(ctx context.Context) string { return "http://example.com/{{.user}}/password/{{.code}}" },
},
args: args{
ctx: context.Background(),
@@ -686,14 +687,15 @@ func TestCommands_requestPasswordReset(t *testing.T) {
},
10*time.Minute,
domain.NotificationTypeEmail,
"",
"http://example.com/{{.user}}/password/{{.code}}",
true,
"",
),
),
),
checkPermission: newMockPermissionCheckAllowed(),
newCode: mockEncryptedCode("code", 10*time.Minute),
checkPermission: newMockPermissionCheckAllowed(),
newCode: mockEncryptedCode("code", 10*time.Minute),
defaultPasswordSetURLTemplate: func(ctx context.Context) string { return "http://example.com/{{.user}}/password/{{.code}}" },
},
args: args{
ctx: context.Background(),
@@ -711,12 +713,13 @@ func TestCommands_requestPasswordReset(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := &Commands{
checkPermission: tt.fields.checkPermission,
eventstore: tt.fields.eventstore(t),
userEncryption: tt.fields.userEncryption,
newEncryptedCode: tt.fields.newCode,
newEncryptedCodeWithDefault: tt.fields.newEncryptedCodeWithDefault,
defaultSecretGenerators: tt.fields.defaultSecretGenerators,
checkPermission: tt.fields.checkPermission,
eventstore: tt.fields.eventstore(t),
userEncryption: tt.fields.userEncryption,
newEncryptedCode: tt.fields.newCode,
newEncryptedCodeWithDefault: tt.fields.newEncryptedCodeWithDefault,
defaultSecretGenerators: tt.fields.defaultSecretGenerators,
defaultPasswordSetURLTemplate: tt.fields.defaultPasswordSetURLTemplate,
}
got, gotPlainCode, err := c.requestPasswordReset(tt.args.ctx, tt.args.userID, tt.args.returnCode, tt.args.urlTmpl, tt.args.notificationType)