diff --git a/internal/command/session_otp.go b/internal/command/session_otp.go index 475eed6f36..6b5f327a2a 100644 --- a/internal/command/session_otp.go +++ b/internal/command/session_otp.go @@ -61,7 +61,7 @@ func (c *Commands) OTPSMSSent(ctx context.Context, sessionID, resourceOwner stri } func (c *Commands) CreateOTPEmailChallengeURLTemplate(urlTmpl string) (SessionCommand, error) { - if err := domain.RenderOTPEmailURLTemplate(io.Discard, urlTmpl, "code", "userID", "loginName", "displayName", language.English); err != nil { + if err := domain.RenderOTPEmailURLTemplate(io.Discard, urlTmpl, "code", "userID", "loginName", "displayName", "sessionID", language.English); err != nil { return nil, err } return c.createOTPEmailChallenge(false, urlTmpl, nil), nil diff --git a/internal/domain/session.go b/internal/domain/session.go index 56dda951da..8ec5347f3c 100644 --- a/internal/domain/session.go +++ b/internal/domain/session.go @@ -20,16 +20,18 @@ type OTPEmailURLData struct { LoginName string DisplayName string PreferredLanguage language.Tag + SessionID string } // RenderOTPEmailURLTemplate parses and renders tmpl. // code, userID, (preferred) loginName, displayName and preferredLanguage are passed into the [OTPEmailURLData]. -func RenderOTPEmailURLTemplate(w io.Writer, tmpl, code, userID, loginName, displayName string, preferredLanguage language.Tag) error { +func RenderOTPEmailURLTemplate(w io.Writer, tmpl, code, userID, loginName, displayName, sessionID string, preferredLanguage language.Tag) error { return renderURLTemplate(w, tmpl, &OTPEmailURLData{ Code: code, UserID: userID, LoginName: loginName, DisplayName: displayName, PreferredLanguage: preferredLanguage, + SessionID: sessionID, }) } diff --git a/internal/notification/handlers/user_notifier.go b/internal/notification/handlers/user_notifier.go index 799a006abf..41d2f4dc8f 100644 --- a/internal/notification/handlers/user_notifier.go +++ b/internal/notification/handlers/user_notifier.go @@ -442,7 +442,7 @@ func (u *userNotifier) reduceSessionOTPEmailChallenged(event eventstore.Event) ( if e.URLTmpl != "" { urlTmpl = e.URLTmpl } - if err := domain.RenderOTPEmailURLTemplate(&buf, urlTmpl, code, user.ID, user.PreferredLoginName, user.DisplayName, user.PreferredLanguage); err != nil { + if err := domain.RenderOTPEmailURLTemplate(&buf, urlTmpl, code, user.ID, user.PreferredLoginName, user.DisplayName, e.Aggregate().ID, user.PreferredLanguage); err != nil { return "", err } return buf.String(), nil diff --git a/proto/zitadel/session/v2/challenge.proto b/proto/zitadel/session/v2/challenge.proto index 77a73e8e68..27aff28b06 100644 --- a/proto/zitadel/session/v2/challenge.proto +++ b/proto/zitadel/session/v2/challenge.proto @@ -42,13 +42,16 @@ message RequestChallenges { } message OTPEmail { message SendCode { + // Optionally set a url_template, which will be used in the mail sent by ZITADEL to guide the user to your verification page. + // If no template is set, the default ZITADEL url will be used. + // + // The following placeholders can be used: Code, UserID, LoginName, DisplayName, PreferredLanguage, SessionID optional string url_template = 1 [ (validate.rules).string = {min_len: 1, max_len: 200}, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { min_length: 1; max_length: 200; example: "\"https://example.com/otp/verify?userID={{.UserID}}&code={{.Code}}\""; - description: "\"Optionally set a url_template, which will be used in the mail sent by ZITADEL to guide the user to your verification page. If no template is set, the default ZITADEL url will be used.\"" } ]; } diff --git a/proto/zitadel/user/v2/auth.proto b/proto/zitadel/user/v2/auth.proto index ff72ecbba8..97e2e66ce9 100644 --- a/proto/zitadel/user/v2/auth.proto +++ b/proto/zitadel/user/v2/auth.proto @@ -15,13 +15,16 @@ enum PasskeyAuthenticator { } message SendPasskeyRegistrationLink { + // Optionally set a url_template, which will be used in the mail sent by ZITADEL to guide the user to your passkey registration page. + // If no template is set, the default ZITADEL url will be used. + // + // The following placeholders can be used: UserID, OrgID, CodeID, Code optional string url_template = 1 [ (validate.rules).string = {min_len: 1, max_len: 200}, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { min_length: 1; max_length: 200; example: "\"https://example.com/passkey/register?userID={{.UserID}}&orgID={{.OrgID}}&codeID={{.CodeID}}&code={{.Code}}\""; - description: "\"Optionally set a url_template, which will be used in the mail sent by ZITADEL to guide the user to your passkey registration page. If no template is set, the default ZITADEL url will be used.\"" } ]; } diff --git a/proto/zitadel/user/v2/email.proto b/proto/zitadel/user/v2/email.proto index cbc07b8762..e962707fcf 100644 --- a/proto/zitadel/user/v2/email.proto +++ b/proto/zitadel/user/v2/email.proto @@ -39,13 +39,16 @@ message HumanEmail { } message SendEmailVerificationCode { + // Optionally set a url_template, which will be used in the verification mail sent by ZITADEL to guide the user to your verification page. + // If no template is set, the default ZITADEL url will be used. + // + // The following placeholders can be used: UserID, OrgID, Code optional string url_template = 1 [ (validate.rules).string = {min_len: 1, max_len: 200}, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { min_length: 1; max_length: 200; example: "\"https://example.com/email/verify?userID={{.UserID}}&code={{.Code}}&orgID={{.OrgID}}\""; - description: "\"Optionally set a url_template, which will be used in the verification mail sent by ZITADEL to guide the user to your verification page. If no template is set, the default ZITADEL url will be used.\"" } ]; } diff --git a/proto/zitadel/user/v2/password.proto b/proto/zitadel/user/v2/password.proto index e306788f4c..9ed6b47f67 100644 --- a/proto/zitadel/user/v2/password.proto +++ b/proto/zitadel/user/v2/password.proto @@ -37,13 +37,16 @@ message HashedPassword { message SendPasswordResetLink { NotificationType notification_type = 1; + // Optionally set a url_template, which will be used in the password reset mail sent by ZITADEL to guide the user to your password change page. + // If no template is set, the default ZITADEL url will be used. + // + // The following placeholders can be used: UserID, OrgID, Code optional string url_template = 2 [ (validate.rules).string = {min_len: 1, max_len: 200}, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { min_length: 1; max_length: 200; example: "\"https://example.com/password/changey?userID={{.UserID}}&code={{.Code}}&orgID={{.OrgID}}\""; - description: "\"Optionally set a url_template, which will be used in the password reset mail sent by ZITADEL to guide the user to your password change page. If no template is set, the default ZITADEL url will be used.\"" } ]; } diff --git a/proto/zitadel/user/v2/user.proto b/proto/zitadel/user/v2/user.proto index e1e98eb036..cfeebbf33d 100644 --- a/proto/zitadel/user/v2/user.proto +++ b/proto/zitadel/user/v2/user.proto @@ -286,6 +286,8 @@ enum AuthFactorState { message SendInviteCode { // Optionally set a url_template, which will be used in the invite mail sent by ZITADEL to guide the user to your invitation page. // If no template is set, the default ZITADEL url will be used. + // + // The following placeholders can be used: UserID, OrgID, Code optional string url_template = 1 [ (validate.rules).string = {min_len: 1, max_len: 200}, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {