diff --git a/cmd/setup/27.go b/cmd/setup/27.go index ee420ddfa..22ded046e 100644 --- a/cmd/setup/27.go +++ b/cmd/setup/27.go @@ -23,5 +23,5 @@ func (mig *IDPTemplate6SAMLNameIDFormat) Execute(ctx context.Context, _ eventsto } func (mig *IDPTemplate6SAMLNameIDFormat) String() string { - return "26_idp_templates6_add_saml_name_id_format" + return "27_idp_templates6_add_saml_name_id_format" } diff --git a/cmd/setup/33.go b/cmd/setup/33.go new file mode 100644 index 000000000..2dd8038f5 --- /dev/null +++ b/cmd/setup/33.go @@ -0,0 +1,27 @@ +package setup + +import ( + "context" + _ "embed" + + "github.com/zitadel/zitadel/internal/database" + "github.com/zitadel/zitadel/internal/eventstore" +) + +var ( + //go:embed 33.sql + addTwilioVerifyServiceSID string +) + +type SMSConfigs3TwilioAddVerifyServiceSid struct { + dbClient *database.DB +} + +func (mig *SMSConfigs3TwilioAddVerifyServiceSid) Execute(ctx context.Context, _ eventstore.Event) error { + _, err := mig.dbClient.ExecContext(ctx, addTwilioVerifyServiceSID) + return err +} + +func (mig *SMSConfigs3TwilioAddVerifyServiceSid) String() string { + return "33_sms_configs3_twilio_add_verification_sid" +} diff --git a/cmd/setup/33.sql b/cmd/setup/33.sql new file mode 100644 index 000000000..62353e58e --- /dev/null +++ b/cmd/setup/33.sql @@ -0,0 +1 @@ +ALTER TABLE IF EXISTS projections.sms_configs3_twilio ADD COLUMN IF NOT EXISTS verify_service_sid TEXT; diff --git a/cmd/setup/config.go b/cmd/setup/config.go index 465aa5aa3..bb1f030b1 100644 --- a/cmd/setup/config.go +++ b/cmd/setup/config.go @@ -119,6 +119,7 @@ type Steps struct { s30FillFieldsForOrgDomainVerified *FillFieldsForOrgDomainVerified s31AddAggregateIndexToFields *AddAggregateIndexToFields s32AddAuthSessionID *AddAuthSessionID + s33SMSConfigs3TwilioAddVerifyServiceSid *SMSConfigs3TwilioAddVerifyServiceSid } func MustNewSteps(v *viper.Viper) *Steps { diff --git a/cmd/setup/setup.go b/cmd/setup/setup.go index 37688b795..6391c271e 100644 --- a/cmd/setup/setup.go +++ b/cmd/setup/setup.go @@ -161,6 +161,7 @@ func Setup(ctx context.Context, config *Config, steps *Steps, masterKey string) steps.s30FillFieldsForOrgDomainVerified = &FillFieldsForOrgDomainVerified{eventstore: eventstoreClient} steps.s31AddAggregateIndexToFields = &AddAggregateIndexToFields{dbClient: esPusherDBClient} steps.s32AddAuthSessionID = &AddAuthSessionID{dbClient: esPusherDBClient} + steps.s33SMSConfigs3TwilioAddVerifyServiceSid = &SMSConfigs3TwilioAddVerifyServiceSid{dbClient: esPusherDBClient} err = projection.Create(ctx, projectionDBClient, eventstoreClient, config.Projections, nil, nil, nil) logging.OnError(err).Fatal("unable to start projections") @@ -218,6 +219,7 @@ func Setup(ctx context.Context, config *Config, steps *Steps, masterKey string) steps.s25User11AddLowerFieldsToVerifiedEmail, steps.s27IDPTemplate6SAMLNameIDFormat, steps.s32AddAuthSessionID, + steps.s33SMSConfigs3TwilioAddVerifyServiceSid, } { mustExecuteMigration(ctx, eventstoreClient, step, "migration failed") } diff --git a/console/src/app/modules/policies/notification-sms-provider/dialog-add-sms-provider/dialog-add-sms-provider.component.html b/console/src/app/modules/policies/notification-sms-provider/dialog-add-sms-provider/dialog-add-sms-provider.component.html index 78f70c7fb..442b11b6d 100644 --- a/console/src/app/modules/policies/notification-sms-provider/dialog-add-sms-provider/dialog-add-sms-provider.component.html +++ b/console/src/app/modules/policies/notification-sms-provider/dialog-add-sms-provider/dialog-add-sms-provider.component.html @@ -18,6 +18,14 @@ + + {{ 'SETTING.SMS.TWILIO.VERIFYSERVICESID' | translate }} + + + {{ + 'SETTING.SMS.TWILIO.VERIFYSERVICESID_DESCRIPTION' | translate + }} + diff --git a/console/src/app/modules/policies/notification-sms-provider/dialog-add-sms-provider/dialog-add-sms-provider.component.ts b/console/src/app/modules/policies/notification-sms-provider/dialog-add-sms-provider/dialog-add-sms-provider.component.ts index e766f6c36..7f3e3f3b0 100644 --- a/console/src/app/modules/policies/notification-sms-provider/dialog-add-sms-provider/dialog-add-sms-provider.component.ts +++ b/console/src/app/modules/policies/notification-sms-provider/dialog-add-sms-provider/dialog-add-sms-provider.component.ts @@ -41,7 +41,9 @@ export class DialogAddSMSProviderComponent { ) { this.twilioForm = this.fb.group({ sid: ['', [requiredValidator]], - senderNumber: ['', [requiredValidator]], + senderNumber: [''], + // NB: not required if not using verification service + verifyServiceSid: [''], }); this.smsProviders = data.smsProviders; @@ -62,12 +64,14 @@ export class DialogAddSMSProviderComponent { this.req.setId(this.twilioProvider.id); this.req.setSid(this.sid?.value); this.req.setSenderNumber(this.senderNumber?.value); + this.req.setVerifyServiceSid(this.verifyServiceSid?.value ?? ''); this.dialogRef.close(this.req); } else { this.req = new AddSMSProviderTwilioRequest(); this.req.setSid(this.sid?.value); this.req.setToken(this.token?.value); this.req.setSenderNumber(this.senderNumber?.value); + this.req.setVerifyServiceSid(this.verifyServiceSid?.value ?? ''); this.dialogRef.close(this.req); } } @@ -104,6 +108,10 @@ export class DialogAddSMSProviderComponent { return this.twilioForm.get('senderNumber'); } + public get verifyServiceSid(): AbstractControl | null { + return this.twilioForm.get('verifyServiceSid'); + } + public get token(): AbstractControl | null { return this.twilioForm.get('token'); } diff --git a/console/src/assets/i18n/bg.json b/console/src/assets/i18n/bg.json index 88cc18662..3dbab7b72 100644 --- a/console/src/assets/i18n/bg.json +++ b/console/src/assets/i18n/bg.json @@ -1419,6 +1419,8 @@ "SID": "Сид", "TOKEN": "Токен", "SENDERNUMBER": "Номер на изпращача", + "VERIFYSERVICESID": "Идентификатор на услугата за проверка", + "VERIFYSERVICESID_DESCRIPTION": "Задаването на идентификатор на услугата за проверка позволява използването на услугата за проверка на Twilio вместо услугата за съобщения за проверка на телефонни номера и OTP SMS.", "ADDED": "Twilio добави успешно.", "UPDATED": "Twilio се актуализира успешно.", "REMOVED": "Twilio премахнат", diff --git a/console/src/assets/i18n/cs.json b/console/src/assets/i18n/cs.json index c49d2ce87..92e6d02da 100644 --- a/console/src/assets/i18n/cs.json +++ b/console/src/assets/i18n/cs.json @@ -1420,6 +1420,8 @@ "SID": "Sid", "TOKEN": "Token", "SENDERNUMBER": "Číslo odesílatele", + "VERIFYSERVICESID": "ID služby ověření", + "VERIFYSERVICESID_DESCRIPTION": "Nastavení ID služby ověření umožňuje použití služby Twilio Verify místo služby Zprávy pro ověření telefonních čísel a SMS OTP.", "ADDED": "Twilio bylo úspěšně přidáno.", "UPDATED": "Twilio bylo úspěšně aktualizováno.", "REMOVED": "Twilio bylo odebráno", diff --git a/console/src/assets/i18n/de.json b/console/src/assets/i18n/de.json index f0f01e468..871cdbaae 100644 --- a/console/src/assets/i18n/de.json +++ b/console/src/assets/i18n/de.json @@ -1420,6 +1420,8 @@ "SID": "SID", "TOKEN": "Token", "SENDERNUMBER": "Sender Number", + "VERIFYSERVICESID": "Verification Service Sid", + "VERIFYSERVICESID_DESCRIPTION": "Das Setzen einer Verification Service Sid ermöglicht die Verwendung des Twilio Verify-Dienstes anstelle des Nachrichtendienstes zur Überprüfung von Telefonnummern und OTP-SMS.", "ADDED": "Twilio erfolgreich hinzugefügt.", "UPDATED": "Twilio wurde erfolgreich aktualisiert.", "REMOVED": "Twilio entfernt", diff --git a/console/src/assets/i18n/en.json b/console/src/assets/i18n/en.json index 63b07aba7..023fb6e80 100644 --- a/console/src/assets/i18n/en.json +++ b/console/src/assets/i18n/en.json @@ -1420,6 +1420,8 @@ "SID": "Sid", "TOKEN": "Token", "SENDERNUMBER": "Sender Number", + "VERIFYSERVICESID": "Verification Service Sid", + "VERIFYSERVICESID_DESCRIPTION": "Setting a Verification Service Sid, allows using the Twilio Verify Service instead of the Messages Service for verification of phone numbers and OTP SMS", "ADDED": "Twilio added successfully.", "UPDATED": "Twilio updated successfully.", "REMOVED": "Twilio removed", diff --git a/console/src/assets/i18n/es.json b/console/src/assets/i18n/es.json index 979541eb4..92f74c884 100644 --- a/console/src/assets/i18n/es.json +++ b/console/src/assets/i18n/es.json @@ -1421,6 +1421,8 @@ "SID": "Sid", "TOKEN": "Token", "SENDERNUMBER": "Número de emisor", + "VERIFYSERVICESID": "SID del servicio de verificación", + "VERIFYSERVICESID_DESCRIPTION": "Establecer un SID del servicio de verificación permite usar el servicio Twilio Verify en lugar del servicio de mensajes para la verificación de números de teléfono y SMS OTP.", "ADDED": "Twilio añadido con éxito.", "UPDATED": "Twilio actualizado con éxito", "REMOVED": "Twilio eliminado", diff --git a/console/src/assets/i18n/fr.json b/console/src/assets/i18n/fr.json index e7f99670a..65257fb26 100644 --- a/console/src/assets/i18n/fr.json +++ b/console/src/assets/i18n/fr.json @@ -1420,6 +1420,8 @@ "SID": "Sid", "TOKEN": "Jeton", "SENDERNUMBER": "Numéro d'expéditeur", + "VERIFYSERVICESID": "SID du service de vérification", + "VERIFYSERVICESID_DESCRIPTION": "Le réglage d'un SID du service de vérification permet d'utiliser le service Twilio Verify au lieu du service de messagerie pour la vérification des numéros de téléphone et des SMS OTP.", "ADDED": "Twilio a été ajouté avec succès.", "UPDATED": "Twilio a été mis à jour avec succès.", "REMOVED": "Twilio a été supprimé avec succès", diff --git a/console/src/assets/i18n/id.json b/console/src/assets/i18n/id.json index 256983501..f4d76cf07 100644 --- a/console/src/assets/i18n/id.json +++ b/console/src/assets/i18n/id.json @@ -1294,6 +1294,8 @@ "SID": "Sid", "TOKEN": "Token", "SENDERNUMBER": "Nomor Pengirim", + "VERIFYSERVICESID": "ID Layanan Verifikasi", + "VERIFYSERVICESID_DESCRIPTION": "Menetapkan ID Layanan Verifikasi memungkinkan penggunaan Layanan Verifikasi Twilio alih-alih Layanan Pesan untuk verifikasi nomor telepon dan SMS OTP.", "ADDED": "Twilio menambahkan dengan sukses.", "UPDATED": "Twilio berhasil diperbarui.", "REMOVED": "Twilio dihapus", diff --git a/console/src/assets/i18n/it.json b/console/src/assets/i18n/it.json index cf793e78b..8d81c8471 100644 --- a/console/src/assets/i18n/it.json +++ b/console/src/assets/i18n/it.json @@ -1420,6 +1420,8 @@ "SID": "Sid", "TOKEN": "Token", "SENDERNUMBER": "Sender Number", + "VERIFYSERVICESID": "SID del servizio di verifica", + "VERIFYSERVICESID_DESCRIPTION": "Impostando un SID del servizio di verifica, è possibile utilizzare il servizio Twilio Verify invece del servizio messaggi per la verifica dei numeri di telefono e degli SMS OTP.", "ADDED": "Twilio aggiunto con successo.", "UPDATED": "Twilio aggiornato correttamente.", "REMOVED": "Twilio rimosso con successo.", diff --git a/console/src/assets/i18n/ja.json b/console/src/assets/i18n/ja.json index 4ba6a69d7..c617d82ec 100644 --- a/console/src/assets/i18n/ja.json +++ b/console/src/assets/i18n/ja.json @@ -1420,6 +1420,8 @@ "SID": "Sid", "TOKEN": "トークン", "SENDERNUMBER": "送信者番号", + "VERIFYSERVICESID": "検証サービス SID", + "VERIFYSERVICESID_DESCRIPTION": "検証サービス SID を設定すると、電話番号と OTP SMS の検証にメッセージサービスの代わりに Twilio Verify サービスを使用できるようになります。", "ADDED": "Twilioは正常に追加されました。", "UPDATED": "Twilio が正常に更新されました。", "REMOVED": "Twilioが削除されました", diff --git a/console/src/assets/i18n/mk.json b/console/src/assets/i18n/mk.json index ecf038eac..a975731e0 100644 --- a/console/src/assets/i18n/mk.json +++ b/console/src/assets/i18n/mk.json @@ -1421,6 +1421,8 @@ "SID": "Sid", "TOKEN": "Токен", "SENDERNUMBER": "Број на испраќач", + "VERIFYSERVICESID": "Идентификатор на услугата за проверка", + "VERIFYSERVICESID_DESCRIPTION": "Поставувањето на идентификатор на услугата за проверка овозможува користење на услугата за проверка на Twilio наместо услугата за пораки за проверка на телефонски броеви и OTP SMS.", "ADDED": "Twilio e успешно додаден.", "UPDATED": "Twilio се ажурираше успешно.", "REMOVED": "Twilio отстранет", diff --git a/console/src/assets/i18n/nl.json b/console/src/assets/i18n/nl.json index 525113e0b..92592ba42 100644 --- a/console/src/assets/i18n/nl.json +++ b/console/src/assets/i18n/nl.json @@ -1419,6 +1419,8 @@ "SID": "Sid", "TOKEN": "Token", "SENDERNUMBER": "Verzender Nummer", + "VERIFYSERVICESID": "Verificatieservice-SID", + "VERIFYSERVICESID_DESCRIPTION": "Het instellen van een Verificatieservice-SID maakt het mogelijk om de Twilio Verify-service te gebruiken in plaats van de Berichten-service voor het verifiëren van telefoonnummers en OTP-SMS.", "ADDED": "Twilio succesvol toegevoegd.", "UPDATED": "Twilio succesvol bijgewerkt.", "REMOVED": "Twilio verwijderd", diff --git a/console/src/assets/i18n/pl.json b/console/src/assets/i18n/pl.json index a739bc08d..9e8df3397 100644 --- a/console/src/assets/i18n/pl.json +++ b/console/src/assets/i18n/pl.json @@ -1419,6 +1419,8 @@ "SID": "Sid", "TOKEN": "Token", "SENDERNUMBER": "Numer nadawcy", + "VERIFYSERVICESID": "SID usługi weryfikacji", + "VERIFYSERVICESID_DESCRIPTION": "Ustawienie SID usługi weryfikacji umożliwia korzystanie z usługi Twilio Verify zamiast usługi wiadomości do weryfikacji numerów telefonów i SMS OTP.", "ADDED": "Twilio dodano pomyślnie.", "UPDATED": "Twilio zostało pomyślnie zaktualizowane.", "REMOVED": "Twilio usunięte", diff --git a/console/src/assets/i18n/pt.json b/console/src/assets/i18n/pt.json index 1bd825e7a..18389e0fc 100644 --- a/console/src/assets/i18n/pt.json +++ b/console/src/assets/i18n/pt.json @@ -1421,6 +1421,8 @@ "SID": "SID", "TOKEN": "Token", "SENDERNUMBER": "Número do remetente", + "VERIFYSERVICESID": "SID do serviço de verificação", + "VERIFYSERVICESID_DESCRIPTION": "Configurar um SID do serviço de verificação permite usar o serviço Twilio Verify em vez do serviço de mensagens para a verificação de números de telefone e SMS OTP.", "ADDED": "Twilio adicionado com sucesso.", "UPDATED": "Twilio atualizado com sucesso.", "REMOVED": "Twilio removido", diff --git a/console/src/assets/i18n/ru.json b/console/src/assets/i18n/ru.json index 292280af3..663a0d100 100644 --- a/console/src/assets/i18n/ru.json +++ b/console/src/assets/i18n/ru.json @@ -1464,6 +1464,8 @@ "SID": "ID Безопасности", "TOKEN": "Токен", "SENDERNUMBER": "Номер отправителя", + "VERIFYSERVICESID": "Идентификатор службы проверки", + "VERIFYSERVICESID_DESCRIPTION": "Установка идентификатора службы проверки позволяет использовать службу проверки Twilio вместо службы сообщений для проверки телефонных номеров и SMS OTP.", "ADDED": "Twilio успешно добавлен.", "REMOVED": "Twilio удалён", "CHANGETOKEN": "Изменить токен", diff --git a/console/src/assets/i18n/sv.json b/console/src/assets/i18n/sv.json index 061cbb6bb..ab267adf1 100644 --- a/console/src/assets/i18n/sv.json +++ b/console/src/assets/i18n/sv.json @@ -1424,6 +1424,8 @@ "SID": "Sid", "TOKEN": "Token", "SENDERNUMBER": "Avsändarnummer", + "VERIFYSERVICESID": "Verifieringstjänst-SID", + "VERIFYSERVICESID_DESCRIPTION": "Inställning av en Verifieringstjänst-SID gör det möjligt att använda Twilio Verify-tjänsten istället för Meddelandetjänsten för verifiering av telefonnummer och OTP-SMS.", "ADDED": "Twilio tillagd framgångsrikt.", "UPDATED": "Twilio uppdaterad framgångsrikt.", "REMOVED": "Twilio borttagen", diff --git a/console/src/assets/i18n/zh.json b/console/src/assets/i18n/zh.json index 48f7ae849..346f29745 100644 --- a/console/src/assets/i18n/zh.json +++ b/console/src/assets/i18n/zh.json @@ -1420,6 +1420,8 @@ "SID": "Sid", "TOKEN": "令牌", "SENDERNUMBER": "发件人号码", + "VERIFYSERVICESID": "验证服务 SID", + "VERIFYSERVICESID_DESCRIPTION": "设置验证服务 SID,允许使用 Twilio 验证服务而不是消息服务来验证电话号码和 OTP SMS。", "ADDED": "Twilio 添加成功。", "UPDATED": "Twilio 更新成功。", "REMOVED": "Twilio 已删除", diff --git a/docs/docs/guides/manage/console/default-settings.mdx b/docs/docs/guides/manage/console/default-settings.mdx index 493613102..dce9f4648 100644 --- a/docs/docs/guides/manage/console/default-settings.mdx +++ b/docs/docs/guides/manage/console/default-settings.mdx @@ -118,9 +118,10 @@ In the SMTP providers table you can hover on a provider row to show buttons that ### SMS -No default provider is configured to send some SMS to your users. If you like to validate the phone numbers of your users make sure to add your twilio configuration by adding your Sid, Token and Sender Number. +No default provider is configured to send some SMS to your users. If you like to validate the phone numbers of your users make sure to add your twilio configuration by adding your Sid, Token and either a Sender Number or a Verification Service Sid. +Setting a Verification Service Sid allows using the Twilio Verify Service instead of the Messages Service for verification. -Twilio +Twilio ## Login Behavior and Access diff --git a/docs/static/img/guides/console/twilio.png b/docs/static/img/guides/console/twilio.png index eb253128a..e8fb891d9 100644 Binary files a/docs/static/img/guides/console/twilio.png and b/docs/static/img/guides/console/twilio.png differ diff --git a/go.mod b/go.mod index 2d6c81552..4f14fd526 100644 --- a/go.mod +++ b/go.mod @@ -42,7 +42,6 @@ require ( github.com/jarcoal/jpath v0.0.0-20140328210829-f76b8b2dbf52 github.com/jinzhu/gorm v1.9.16 github.com/k3a/html2text v1.2.1 - github.com/kevinburke/twilio-go v0.0.0-20240623211326-c7334b537077 github.com/lucasb-eyer/go-colorful v1.2.0 github.com/minio/minio-go/v7 v7.0.73 github.com/mitchellh/mapstructure v1.5.0 @@ -60,6 +59,7 @@ require ( github.com/stretchr/testify v1.9.0 github.com/superseriousbusiness/exifremove v0.0.0-20210330092427-6acd27eac203 github.com/ttacon/libphonenumber v1.2.1 + github.com/twilio/twilio-go v1.22.2 github.com/zitadel/logging v0.6.0 github.com/zitadel/oidc/v3 v3.28.1 github.com/zitadel/passwap v0.6.0 @@ -103,6 +103,7 @@ require ( github.com/goccy/go-json v0.10.3 // indirect github.com/golang-jwt/jwt/v4 v4.5.0 // indirect github.com/golang-jwt/jwt/v5 v5.2.1 // indirect + github.com/golang/mock v1.6.0 // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/google/go-tpm v0.9.0 // indirect github.com/google/pprof v0.0.0-20240528025155-186aa0362fba // indirect @@ -157,7 +158,6 @@ require ( github.com/go-sourcemap/sourcemap v2.1.4+incompatible // indirect github.com/go-xmlfmt/xmlfmt v1.1.2 // indirect github.com/gofrs/flock v0.8.1 // indirect - github.com/gofrs/uuid v4.4.0+incompatible // indirect github.com/golang/geo v0.0.0-20230421003525-6adc56603217 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/google/uuid v1.6.0 @@ -172,8 +172,6 @@ require ( github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jonboulle/clockwork v0.4.0 - github.com/kevinburke/go-types v0.0.0-20210723172823-2deba1f80ba7 // indirect - github.com/kevinburke/rest v0.0.0-20240617045629-3ed0ad3487f0 // indirect github.com/klauspost/compress v1.17.9 // indirect github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect diff --git a/go.sum b/go.sum index 606f4048c..ac626ea25 100644 --- a/go.sum +++ b/go.sum @@ -239,7 +239,6 @@ github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LB github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI= github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= -github.com/go-stack/stack v1.8.1/go.mod h1:dcoOX6HbPZSZptuspn9bctJ+N/CnF5gGygcUP3XYfe4= github.com/go-webauthn/webauthn v0.10.2 h1:OG7B+DyuTytrEPFmTX503K77fqs3HDK/0Iv+z8UYbq4= github.com/go-webauthn/webauthn v0.10.2/go.mod h1:Gd1IDsGAybuvK1NkwUTLbGmeksxuRJjVN2PE/xsPxHs= github.com/go-webauthn/x v0.1.9 h1:v1oeLmoaa+gPOaZqUdDentu6Rl7HkSSsmOT6gxEQHhE= @@ -254,13 +253,12 @@ github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA= github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw= github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= -github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA= -github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gogo/googleapis v1.1.0/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= @@ -396,7 +394,6 @@ github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpO github.com/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmKTg= github.com/improbable-eng/grpc-web v0.15.0 h1:BN+7z6uNXZ1tQGcNAuaU1YjsLTApzkjt2tzCixLaUPQ= github.com/improbable-eng/grpc-web v0.15.0/go.mod h1:1sy9HKV4Jt9aEs9JSnkWlRJPuPtwNr0l57L4f878wP8= -github.com/inconshreveable/log15 v3.0.0-testing.5+incompatible/go.mod h1:cOaXtrgN4ScfRrD9Bre7U1thNq5RtJ8ZoP4iXVGRj6o= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= @@ -450,12 +447,6 @@ github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7V github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= github.com/k3a/html2text v1.2.1 h1:nvnKgBvBR/myqrwfLuiqecUtaK1lB9hGziIJKatNFVY= github.com/k3a/html2text v1.2.1/go.mod h1:ieEXykM67iT8lTvEWBh6fhpH4B23kB9OMKPdIBmgUqA= -github.com/kevinburke/go-types v0.0.0-20210723172823-2deba1f80ba7 h1:K8qael4LemsmJCGt+ccI8b0fCNFDttmEu3qtpFt3G0M= -github.com/kevinburke/go-types v0.0.0-20210723172823-2deba1f80ba7/go.mod h1:/Pk5i/SqYdYv1cie5wGwoZ4P6TpgMi+Yf58mtJSHdOw= -github.com/kevinburke/rest v0.0.0-20240617045629-3ed0ad3487f0 h1:qksAIHu0d4vkA0rIePBn+K9eO33RxkUMiceFn3T7lO4= -github.com/kevinburke/rest v0.0.0-20240617045629-3ed0ad3487f0/go.mod h1:dcLMT8KO9krIMJQ4578Lex1Su6ewuJUqEDeQ1nTORug= -github.com/kevinburke/twilio-go v0.0.0-20240623211326-c7334b537077 h1:IBBYDggH8Ra+xBzJ/7e+X9MG5TUNQ6kjyU2qf08m8c8= -github.com/kevinburke/twilio-go v0.0.0-20240623211326-c7334b537077/go.mod h1:ywO98mC3XU46KlDLgCDChBOpf5CihdaETKriwam/8eE= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= @@ -489,6 +480,8 @@ github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM= github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4= +github.com/localtunnel/go-localtunnel v0.0.0-20170326223115-8a804488f275 h1:IZycmTpoUtQK3PD60UYBwjaCUHUP7cML494ao9/O8+Q= +github.com/localtunnel/go-localtunnel v0.0.0-20170326223115-8a804488f275/go.mod h1:zt6UU74K6Z6oMOYJbJzYpYucqdcQwSMPBEdSvGiaUMw= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ= @@ -556,6 +549,7 @@ github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32 h1:W6apQkHrMkS0Muv8G/TipAy github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms= github.com/nicksnyder/go-i18n/v2 v2.4.0 h1:3IcvPOAvnCKwNm0TB0dLDTuawWEj+ax/RERNC+diLMM= github.com/nicksnyder/go-i18n/v2 v2.4.0/go.mod h1:nxYSZE9M0bf3Y70gPQjN9ha7XNHX7gMc814+6wVyEI4= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/oklog/oklog v0.3.2/go.mod h1:FCV+B7mhrz4o+ueLpx+KqkyXRGMWOYEvfiXtdGtbWGs= github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= @@ -706,6 +700,8 @@ github.com/ttacon/builder v0.0.0-20170518171403-c099f663e1c2 h1:5u+EJUQiosu3JFX0 github.com/ttacon/builder v0.0.0-20170518171403-c099f663e1c2/go.mod h1:4kyMkleCiLkgY6z8gK5BkI01ChBtxR0ro3I1ZDcGM3w= github.com/ttacon/libphonenumber v1.2.1 h1:fzOfY5zUADkCkbIafAed11gL1sW+bJ26p6zWLBMElR4= github.com/ttacon/libphonenumber v1.2.1/go.mod h1:E0TpmdVMq5dyVlQ7oenAkhsLu86OkUl+yR4OAxyEg/M= +github.com/twilio/twilio-go v1.22.2 h1:LUz6OTWKY4/oW4e+O2ah2JMq03gJvGu6bxaF0Y7l+Xc= +github.com/twilio/twilio-go v1.22.2/go.mod h1:zRkMjudW7v7MqQ3cWNZmSoZJ7EBjPZ4OpNh2zm7Q6ko= github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= @@ -719,6 +715,7 @@ github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913 h1:+qGGcbkzsfDQNPPe github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913/go.mod h1:4aEEwZQutDLsQv2Deui4iYQ6DWTxR14g6m8Wv88+Xqk= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/zenazn/goji v1.0.1 h1:4lbD8Mx2h7IvloP7r2C0D6ltZP6Ufip8Hn0wmSK5LR8= github.com/zenazn/goji v1.0.1/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= @@ -815,6 +812,7 @@ golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzB golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -849,6 +847,7 @@ golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81R golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= @@ -870,6 +869,7 @@ golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= @@ -901,7 +901,9 @@ golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -912,8 +914,6 @@ golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= @@ -922,7 +922,6 @@ golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9sn golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= -golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -958,6 +957,7 @@ golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapK golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -1018,6 +1018,7 @@ gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLks gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= diff --git a/internal/api/grpc/admin/sms_converter.go b/internal/api/grpc/admin/sms_converter.go index d13b68f55..72f33cb54 100644 --- a/internal/api/grpc/admin/sms_converter.go +++ b/internal/api/grpc/admin/sms_converter.go @@ -64,8 +64,9 @@ func HTTPConfigToPb(http *query.HTTP) *settings_pb.SMSProvider_Http { func TwilioConfigToPb(twilio *query.Twilio) *settings_pb.SMSProvider_Twilio { return &settings_pb.SMSProvider_Twilio{ Twilio: &settings_pb.TwilioConfig{ - Sid: twilio.SID, - SenderNumber: twilio.SenderNumber, + Sid: twilio.SID, + SenderNumber: twilio.SenderNumber, + VerifyServiceSid: twilio.VerifyServiceSID, }, } } @@ -83,21 +84,23 @@ func smsStateToPb(state domain.SMSConfigState) settings_pb.SMSProviderConfigStat func addSMSConfigTwilioToConfig(ctx context.Context, req *admin_pb.AddSMSProviderTwilioRequest) *command.AddTwilioConfig { return &command.AddTwilioConfig{ - ResourceOwner: authz.GetInstance(ctx).InstanceID(), - Description: req.Description, - SID: req.Sid, - SenderNumber: req.SenderNumber, - Token: req.Token, + ResourceOwner: authz.GetInstance(ctx).InstanceID(), + Description: req.Description, + SID: req.Sid, + SenderNumber: req.SenderNumber, + Token: req.Token, + VerifyServiceSID: req.VerifyServiceSid, } } func updateSMSConfigTwilioToConfig(ctx context.Context, req *admin_pb.UpdateSMSProviderTwilioRequest) *command.ChangeTwilioConfig { return &command.ChangeTwilioConfig{ - ResourceOwner: authz.GetInstance(ctx).InstanceID(), - ID: req.Id, - Description: gu.Ptr(req.Description), - SID: gu.Ptr(req.Sid), - SenderNumber: gu.Ptr(req.SenderNumber), + ResourceOwner: authz.GetInstance(ctx).InstanceID(), + ID: req.Id, + Description: gu.Ptr(req.Description), + SID: gu.Ptr(req.Sid), + SenderNumber: gu.Ptr(req.SenderNumber), + VerifyServiceSID: gu.Ptr(req.VerifyServiceSid), } } diff --git a/internal/command/command.go b/internal/command/command.go index 30e383c5d..4ee831052 100644 --- a/internal/command/command.go +++ b/internal/command/command.go @@ -24,6 +24,7 @@ import ( "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/id" + "github.com/zitadel/zitadel/internal/notification/senders" "github.com/zitadel/zitadel/internal/static" "github.com/zitadel/zitadel/internal/telemetry/tracing" webauthn_helper "github.com/zitadel/zitadel/internal/webauthn" @@ -64,6 +65,7 @@ type Commands struct { defaultAccessTokenLifetime time.Duration defaultRefreshTokenLifetime time.Duration defaultRefreshTokenIdleLifetime time.Duration + phoneCodeVerifier func(ctx context.Context, id string) (senders.CodeGenerator, error) multifactors domain.MultifactorConfigs webauthnConfig *webauthn_helper.Config @@ -179,6 +181,7 @@ func StartCommands( if defaultSecretGenerators != nil && defaultSecretGenerators.ClientSecret != nil { repo.newHashedSecret = newHashedSecretWithDefault(secretHasher, defaultSecretGenerators.ClientSecret) } + repo.phoneCodeVerifier = repo.phoneCodeVerifierFromConfig return repo, nil } diff --git a/internal/command/crypto.go b/internal/command/crypto.go index 45c597fd9..94b009aaa 100644 --- a/internal/command/crypto.go +++ b/internal/command/crypto.go @@ -13,6 +13,8 @@ type encrypedCodeFunc func(ctx context.Context, filter preparation.FilterToQuery type encryptedCodeWithDefaultFunc func(ctx context.Context, filter preparation.FilterToQueryReducer, typ domain.SecretGeneratorType, alg crypto.EncryptionAlgorithm, defaultConfig *crypto.GeneratorConfig) (*EncryptedCode, error) +type encryptedCodeGeneratorWithDefaultFunc func(ctx context.Context, filter preparation.FilterToQueryReducer, typ domain.SecretGeneratorType, alg crypto.EncryptionAlgorithm, defaultConfig *crypto.GeneratorConfig) (*EncryptedCode, string, error) + var emptyConfig = &crypto.GeneratorConfig{} type EncryptedCode struct { @@ -21,6 +23,27 @@ type EncryptedCode struct { Expiry time.Duration } +func (e *EncryptedCode) CryptedCode() *crypto.CryptoValue { + if e == nil { + return nil + } + return e.Crypted +} + +func (e *EncryptedCode) PlainCode() string { + if e == nil { + return "" + } + return e.Plain +} + +func (e *EncryptedCode) CodeExpiry() time.Duration { + if e == nil { + return 0 + } + return e.Expiry +} + func newEncryptedCode(ctx context.Context, filter preparation.FilterToQueryReducer, typ domain.SecretGeneratorType, alg crypto.EncryptionAlgorithm) (*EncryptedCode, error) { return newEncryptedCodeWithDefaultConfig(ctx, filter, typ, alg, emptyConfig) } diff --git a/internal/command/crypto_test.go b/internal/command/crypto_test.go index 815539120..03a5f6197 100644 --- a/internal/command/crypto_test.go +++ b/internal/command/crypto_test.go @@ -49,6 +49,27 @@ func mockEncryptedCodeWithDefault(code string, exp time.Duration) encryptedCodeW } } +func mockEncryptedCodeGeneratorWithDefault(code string, exp time.Duration) encryptedCodeGeneratorWithDefaultFunc { + return func(ctx context.Context, filter preparation.FilterToQueryReducer, _ domain.SecretGeneratorType, alg crypto.EncryptionAlgorithm, _ *crypto.GeneratorConfig) (*EncryptedCode, string, error) { + return &EncryptedCode{ + Crypted: &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte(code), + }, + Plain: code, + Expiry: exp, + }, "", nil + } +} + +func mockEncryptedCodeGeneratorWithDefaultExternal(id string) encryptedCodeGeneratorWithDefaultFunc { + return func(ctx context.Context, filter preparation.FilterToQueryReducer, _ domain.SecretGeneratorType, alg crypto.EncryptionAlgorithm, _ *crypto.GeneratorConfig) (*EncryptedCode, string, error) { + return nil, id, nil + } +} + func mockHashedSecret(secret string) hashedSecretFunc { return func(_ context.Context, _ preparation.FilterToQueryReducer) (encodedHash string, plain string, err error) { return secret, secret, nil diff --git a/internal/command/phone.go b/internal/command/phone.go index 077023196..8d971e2f9 100644 --- a/internal/command/phone.go +++ b/internal/command/phone.go @@ -16,6 +16,16 @@ type Phone struct { ReturnCode bool } -func (c *Commands) newPhoneCode(ctx context.Context, filter preparation.FilterToQueryReducer, alg crypto.EncryptionAlgorithm) (*EncryptedCode, error) { - return c.newEncryptedCode(ctx, filter, domain.SecretGeneratorTypeVerifyPhoneCode, alg) +// newPhoneCode generates a new code to be sent out to via SMS or +// returns the ID of the external code provider (e.g. when using Twilio verification API) +func (c *Commands) newPhoneCode(ctx context.Context, filter preparation.FilterToQueryReducer, secretGeneratorType domain.SecretGeneratorType, alg crypto.EncryptionAlgorithm, defaultConfig *crypto.GeneratorConfig) (*EncryptedCode, string, error) { + externalID, err := c.activeSMSProvider(ctx) + if err != nil { + return nil, "", err + } + if externalID != "" { + return nil, externalID, nil + } + code, err := c.newEncryptedCodeWithDefault(ctx, filter, secretGeneratorType, alg, defaultConfig) + return code, "", err } diff --git a/internal/command/session.go b/internal/command/session.go index ccc224cab..dafb10b81 100644 --- a/internal/command/session.go +++ b/internal/command/session.go @@ -16,6 +16,7 @@ import ( "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/id" + "github.com/zitadel/zitadel/internal/notification/senders" "github.com/zitadel/zitadel/internal/repository/session" "github.com/zitadel/zitadel/internal/repository/user" "github.com/zitadel/zitadel/internal/zerrors" @@ -31,13 +32,15 @@ type SessionCommands struct { eventstore *eventstore.Eventstore eventCommands []eventstore.Command - hasher *crypto.Hasher - intentAlg crypto.EncryptionAlgorithm - totpAlg crypto.EncryptionAlgorithm - otpAlg crypto.EncryptionAlgorithm - createCode encryptedCodeWithDefaultFunc - createToken func(sessionID string) (id string, token string, err error) - now func() time.Time + hasher *crypto.Hasher + intentAlg crypto.EncryptionAlgorithm + totpAlg crypto.EncryptionAlgorithm + otpAlg crypto.EncryptionAlgorithm + createCode encryptedCodeWithDefaultFunc + createPhoneCode encryptedCodeGeneratorWithDefaultFunc + createToken func(sessionID string) (id string, token string, err error) + getCodeVerifier func(ctx context.Context, id string) (senders.CodeGenerator, error) + now func() time.Time } func (c *Commands) NewSessionCommands(cmds []SessionCommand, session *SessionWriteModel) *SessionCommands { @@ -50,6 +53,7 @@ func (c *Commands) NewSessionCommands(cmds []SessionCommand, session *SessionWri totpAlg: c.multifactors.OTP.CryptoMFA, otpAlg: c.userEncryption, createCode: c.newEncryptedCodeWithDefault, + createPhoneCode: c.newPhoneCode, createToken: c.sessionTokenCreator, now: time.Now, } @@ -188,8 +192,8 @@ func (s *SessionCommands) TOTPChecked(ctx context.Context, checkedAt time.Time) s.eventCommands = append(s.eventCommands, session.NewTOTPCheckedEvent(ctx, s.sessionWriteModel.aggregate, checkedAt)) } -func (s *SessionCommands) OTPSMSChallenged(ctx context.Context, code *crypto.CryptoValue, expiry time.Duration, returnCode bool) { - s.eventCommands = append(s.eventCommands, session.NewOTPSMSChallengedEvent(ctx, s.sessionWriteModel.aggregate, code, expiry, returnCode)) +func (s *SessionCommands) OTPSMSChallenged(ctx context.Context, code *crypto.CryptoValue, expiry time.Duration, returnCode bool, generatorID string) { + s.eventCommands = append(s.eventCommands, session.NewOTPSMSChallengedEvent(ctx, s.sessionWriteModel.aggregate, code, expiry, returnCode, generatorID)) } func (s *SessionCommands) OTPSMSChecked(ctx context.Context, checkedAt time.Time) { diff --git a/internal/command/session_model.go b/internal/command/session_model.go index e418a3adb..646bee97e 100644 --- a/internal/command/session_model.go +++ b/internal/command/session_model.go @@ -20,9 +20,11 @@ type WebAuthNChallengeModel struct { } type OTPCode struct { - Code *crypto.CryptoValue - Expiry time.Duration - CreationDate time.Time + Code *crypto.CryptoValue + Expiry time.Duration + CreationDate time.Time + GeneratorID string + VerificationID string } func (p *WebAuthNChallengeModel) WebAuthNLogin(human *domain.Human, credentialAssertionData []byte) *domain.WebAuthNLogin { @@ -92,6 +94,8 @@ func (wm *SessionWriteModel) Reduce() error { wm.reduceTOTPChecked(e) case *session.OTPSMSChallengedEvent: wm.reduceOTPSMSChallenged(e) + case *session.OTPSMSSentEvent: + wm.reduceOTPSMSSent(e) case *session.OTPSMSCheckedEvent: wm.reduceOTPSMSChecked(e) case *session.OTPEmailChallengedEvent: @@ -123,6 +127,7 @@ func (wm *SessionWriteModel) Query() *eventstore.SearchQueryBuilder { session.WebAuthNCheckedType, session.TOTPCheckedType, session.OTPSMSChallengedType, + session.OTPSMSSentType, session.OTPSMSCheckedType, session.OTPEmailChallengedType, session.OTPEmailCheckedType, @@ -183,9 +188,15 @@ func (wm *SessionWriteModel) reduceOTPSMSChallenged(e *session.OTPSMSChallengedE Code: e.Code, Expiry: e.Expiry, CreationDate: e.CreationDate(), + GeneratorID: e.GeneratorID, } } +func (wm *SessionWriteModel) reduceOTPSMSSent(e *session.OTPSMSSentEvent) { + wm.OTPSMSCodeChallenge.GeneratorID = e.GeneratorInfo.GetID() + wm.OTPSMSCodeChallenge.VerificationID = e.GeneratorInfo.GetVerificationID() +} + func (wm *SessionWriteModel) reduceOTPSMSChecked(e *session.OTPSMSCheckedEvent) { wm.OTPSMSCodeChallenge = nil wm.OTPSMSCheckedAt = e.CheckedAt diff --git a/internal/command/session_otp.go b/internal/command/session_otp.go index 6b7e20fb1..6a4517c98 100644 --- a/internal/command/session_otp.go +++ b/internal/command/session_otp.go @@ -8,6 +8,7 @@ import ( "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/notification/senders" "github.com/zitadel/zitadel/internal/repository/session" "github.com/zitadel/zitadel/internal/repository/user" "github.com/zitadel/zitadel/internal/zerrors" @@ -33,19 +34,19 @@ func (c *Commands) createOTPSMSChallenge(returnCode bool, dst *string) SessionCo if !writeModel.OTPAdded() { return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-BJ2g3", "Errors.User.MFA.OTP.NotReady") } - code, err := cmd.createCode(ctx, cmd.eventstore.Filter, domain.SecretGeneratorTypeOTPSMS, cmd.otpAlg, c.defaultSecretGenerators.OTPSMS) + code, generatorID, err := cmd.createPhoneCode(ctx, cmd.eventstore.Filter, domain.SecretGeneratorTypeOTPSMS, cmd.otpAlg, c.defaultSecretGenerators.OTPSMS) //nolint:staticcheck if err != nil { return nil, err } if returnCode { *dst = code.Plain } - cmd.OTPSMSChallenged(ctx, code.Crypted, code.Expiry, returnCode) + cmd.OTPSMSChallenged(ctx, code.CryptedCode(), code.CodeExpiry(), returnCode, generatorID) return nil, nil } } -func (c *Commands) OTPSMSSent(ctx context.Context, sessionID, resourceOwner string) error { +func (c *Commands) OTPSMSSent(ctx context.Context, sessionID, resourceOwner string, generatorInfo *senders.CodeGeneratorInfo) error { sessionWriteModel := NewSessionWriteModel(sessionID, resourceOwner) err := c.eventstore.FilterToQueryReducer(ctx, sessionWriteModel) if err != nil { @@ -55,7 +56,7 @@ func (c *Commands) OTPSMSSent(ctx context.Context, sessionID, resourceOwner stri return zerrors.ThrowPreconditionFailed(nil, "COMMAND-G3t31", "Errors.User.Code.NotFound") } return c.pushAppendAndReduce(ctx, sessionWriteModel, - session.NewOTPSMSSentEvent(ctx, &session.NewAggregate(sessionID, sessionWriteModel.ResourceOwner).Aggregate), + session.NewOTPSMSSentEvent(ctx, &session.NewAggregate(sessionID, sessionWriteModel.ResourceOwner).Aggregate, generatorInfo), ) } @@ -86,7 +87,7 @@ func (c *Commands) createOTPEmailChallenge(returnCode bool, urlTmpl string, dst if !writeModel.OTPAdded() { return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-JKLJ3", "Errors.User.MFA.OTP.NotReady") } - code, err := cmd.createCode(ctx, cmd.eventstore.Filter, domain.SecretGeneratorTypeOTPEmail, cmd.otpAlg, c.defaultSecretGenerators.OTPEmail) + code, err := cmd.createCode(ctx, cmd.eventstore.Filter, domain.SecretGeneratorTypeOTPEmail, cmd.otpAlg, c.defaultSecretGenerators.OTPEmail) //nolint:staticcheck if err != nil { return nil, err } @@ -130,7 +131,19 @@ func CheckOTPSMS(code string) SessionCommand { failedEvent := func(ctx context.Context, aggregate *eventstore.Aggregate, info *user.AuthRequestInfo) eventstore.Command { return user.NewHumanOTPSMSCheckFailedEvent(ctx, aggregate, nil) } - commands, err := checkOTP(ctx, cmd.sessionWriteModel.UserID, code, "", nil, writeModel, cmd.eventstore.FilterToQueryReducer, cmd.otpAlg, succeededEvent, failedEvent) + commands, err := checkOTP( + ctx, + cmd.sessionWriteModel.UserID, + code, + "", + nil, + writeModel, + cmd.eventstore.FilterToQueryReducer, + cmd.otpAlg, + cmd.getCodeVerifier, + succeededEvent, + failedEvent, + ) if err != nil { return commands, err } @@ -158,7 +171,19 @@ func CheckOTPEmail(code string) SessionCommand { failedEvent := func(ctx context.Context, aggregate *eventstore.Aggregate, info *user.AuthRequestInfo) eventstore.Command { return user.NewHumanOTPEmailCheckFailedEvent(ctx, aggregate, nil) } - commands, err := checkOTP(ctx, cmd.sessionWriteModel.UserID, code, "", nil, writeModel, cmd.eventstore.FilterToQueryReducer, cmd.otpAlg, succeededEvent, failedEvent) + commands, err := checkOTP( + ctx, + cmd.sessionWriteModel.UserID, + code, + "", + nil, + writeModel, + cmd.eventstore.FilterToQueryReducer, + cmd.otpAlg, + cmd.getCodeVerifier, + succeededEvent, + failedEvent, + ) if err != nil { return commands, err } diff --git a/internal/command/session_otp_test.go b/internal/command/session_otp_test.go index d6934d047..3e061749b 100644 --- a/internal/command/session_otp_test.go +++ b/internal/command/session_otp_test.go @@ -11,6 +11,7 @@ import ( "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/notification/senders" "github.com/zitadel/zitadel/internal/repository/org" "github.com/zitadel/zitadel/internal/repository/session" "github.com/zitadel/zitadel/internal/repository/user" @@ -19,9 +20,9 @@ import ( func TestCommands_CreateOTPSMSChallengeReturnCode(t *testing.T) { type fields struct { - userID string - eventstore func(*testing.T) *eventstore.Eventstore - createCode encryptedCodeWithDefaultFunc + userID string + eventstore func(*testing.T) *eventstore.Eventstore + createPhoneCode encryptedCodeGeneratorWithDefaultFunc } type res struct { err error @@ -66,7 +67,7 @@ func TestCommands_CreateOTPSMSChallengeReturnCode(t *testing.T) { ), ), ), - createCode: mockEncryptedCodeWithDefault("1234567", 5*time.Minute), + createPhoneCode: mockEncryptedCodeGeneratorWithDefault("1234567", 5*time.Minute), }, res: res{ returnCode: "1234567", @@ -80,6 +81,7 @@ func TestCommands_CreateOTPSMSChallengeReturnCode(t *testing.T) { }, 5*time.Minute, true, + "", ), }, }, @@ -107,7 +109,7 @@ func TestCommands_CreateOTPSMSChallengeReturnCode(t *testing.T) { sessionCommands: []SessionCommand{cmd}, sessionWriteModel: sessionModel, eventstore: tt.fields.eventstore(t), - createCode: tt.fields.createCode, + createPhoneCode: tt.fields.createPhoneCode, now: time.Now, } @@ -122,9 +124,9 @@ func TestCommands_CreateOTPSMSChallengeReturnCode(t *testing.T) { func TestCommands_CreateOTPSMSChallenge(t *testing.T) { type fields struct { - userID string - eventstore func(*testing.T) *eventstore.Eventstore - createCode encryptedCodeWithDefaultFunc + userID string + eventstore func(*testing.T) *eventstore.Eventstore + createPhoneCode encryptedCodeGeneratorWithDefaultFunc } type res struct { err error @@ -168,7 +170,7 @@ func TestCommands_CreateOTPSMSChallenge(t *testing.T) { ), ), ), - createCode: mockEncryptedCodeWithDefault("1234567", 5*time.Minute), + createPhoneCode: mockEncryptedCodeGeneratorWithDefault("1234567", 5*time.Minute), }, res: res{ commands: []eventstore.Command{ @@ -181,6 +183,31 @@ func TestCommands_CreateOTPSMSChallenge(t *testing.T) { }, 5*time.Minute, false, + "", + ), + }, + }, + }, + { + name: "generate code externally", + fields: fields{ + userID: "userID", + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + user.NewHumanOTPSMSAddedEvent(context.Background(), &user.NewAggregate("userID", "org").Aggregate), + ), + ), + ), + createPhoneCode: mockEncryptedCodeGeneratorWithDefaultExternal("generatorID"), + }, + res: res{ + commands: []eventstore.Command{ + session.NewOTPSMSChallengedEvent(context.Background(), &session.NewAggregate("sessionID", "instanceID").Aggregate, + nil, + 0, + false, + "generatorID", ), }, }, @@ -208,7 +235,7 @@ func TestCommands_CreateOTPSMSChallenge(t *testing.T) { sessionCommands: []SessionCommand{cmd}, sessionWriteModel: sessionModel, eventstore: tt.fields.eventstore(t), - createCode: tt.fields.createCode, + createPhoneCode: tt.fields.createPhoneCode, now: time.Now, } @@ -228,6 +255,7 @@ func TestCommands_OTPSMSSent(t *testing.T) { ctx context.Context sessionID string resourceOwner string + generatorInfo *senders.CodeGeneratorInfo } tests := []struct { name string @@ -246,6 +274,7 @@ func TestCommands_OTPSMSSent(t *testing.T) { ctx: context.Background(), sessionID: "sessionID", resourceOwner: "instanceID", + generatorInfo: &senders.CodeGeneratorInfo{}, }, wantErr: zerrors.ThrowPreconditionFailed(nil, "COMMAND-G3t31", "Errors.User.Code.NotFound"), }, @@ -264,11 +293,12 @@ func TestCommands_OTPSMSSent(t *testing.T) { }, 5*time.Minute, false, + "", ), ), ), expectPush( - session.NewOTPSMSSentEvent(context.Background(), &session.NewAggregate("sessionID", "instanceID").Aggregate), + session.NewOTPSMSSentEvent(context.Background(), &session.NewAggregate("sessionID", "instanceID").Aggregate, &senders.CodeGeneratorInfo{}), ), ), }, @@ -276,6 +306,37 @@ func TestCommands_OTPSMSSent(t *testing.T) { ctx: context.Background(), sessionID: "sessionID", resourceOwner: "instanceID", + generatorInfo: &senders.CodeGeneratorInfo{}, + }, + wantErr: nil, + }, + { + name: "challenged and sent (externally)", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + session.NewOTPSMSChallengedEvent(context.Background(), &session.NewAggregate("sessionID", "instanceID").Aggregate, + nil, + 0, + false, + "", + ), + ), + ), + expectPush( + session.NewOTPSMSSentEvent(context.Background(), &session.NewAggregate("sessionID", "instanceID").Aggregate, &senders.CodeGeneratorInfo{ID: "generatorID", VerificationID: "verificationID"}), + ), + ), + }, + args: args{ + ctx: context.Background(), + sessionID: "sessionID", + resourceOwner: "instanceID", + generatorInfo: &senders.CodeGeneratorInfo{ + ID: "generatorID", + VerificationID: "verificationID", + }, }, wantErr: nil, }, @@ -285,7 +346,7 @@ func TestCommands_OTPSMSSent(t *testing.T) { c := &Commands{ eventstore: tt.fields.eventstore(t), } - err := c.OTPSMSSent(tt.args.ctx, tt.args.sessionID, tt.args.resourceOwner) + err := c.OTPSMSSent(tt.args.ctx, tt.args.sessionID, tt.args.resourceOwner, tt.args.generatorInfo) assert.ErrorIs(t, err, tt.wantErr) }) } diff --git a/internal/command/sms_config.go b/internal/command/sms_config.go index 82eae763d..bc1638021 100644 --- a/internal/command/sms_config.go +++ b/internal/command/sms_config.go @@ -14,10 +14,11 @@ type AddTwilioConfig struct { ResourceOwner string ID string - Description string - SID string - Token string - SenderNumber string + Description string + SID string + Token string + SenderNumber string + VerifyServiceSID string } func (c *Commands) AddSMSConfigTwilio(ctx context.Context, config *AddTwilioConfig) (err error) { @@ -52,6 +53,7 @@ func (c *Commands) AddSMSConfigTwilio(ctx context.Context, config *AddTwilioConf config.SID, config.SenderNumber, token, + config.VerifyServiceSID, ), ) if err != nil { @@ -66,10 +68,11 @@ type ChangeTwilioConfig struct { ResourceOwner string ID string - Description *string - SID *string - Token *string - SenderNumber *string + Description *string + SID *string + Token *string + SenderNumber *string + VerifyServiceSID *string } func (c *Commands) ChangeSMSConfigTwilio(ctx context.Context, config *ChangeTwilioConfig) (err error) { @@ -92,7 +95,9 @@ func (c *Commands) ChangeSMSConfigTwilio(ctx context.Context, config *ChangeTwil config.ID, config.Description, config.SID, - config.SenderNumber) + config.SenderNumber, + config.VerifyServiceSID, + ) if err != nil { return err } @@ -330,3 +335,13 @@ func (c *Commands) getSMSConfig(ctx context.Context, instanceID, id string) (_ * } return writeModel, nil } + +// getActiveSMSConfig returns the last activated SMS configuration +func (c *Commands) getActiveSMSConfig(ctx context.Context, instanceID string) (_ *IAMSMSConfigWriteModel, err error) { + writeModel := NewIAMSMSLastActivatedConfigWriteModel(instanceID) + err = c.eventstore.FilterToQueryReducer(ctx, writeModel) + if err != nil { + return nil, err + } + return c.getSMSConfig(ctx, instanceID, writeModel.activeID) +} diff --git a/internal/command/sms_config_model.go b/internal/command/sms_config_model.go index 06360a5cf..922c43ada 100644 --- a/internal/command/sms_config_model.go +++ b/internal/command/sms_config_model.go @@ -20,9 +20,10 @@ type IAMSMSConfigWriteModel struct { } type TwilioConfig struct { - SID string - Token *crypto.CryptoValue - SenderNumber string + SID string + Token *crypto.CryptoValue + SenderNumber string + VerifyServiceSID string } type HTTPConfig struct { @@ -48,9 +49,10 @@ func (wm *IAMSMSConfigWriteModel) Reduce() error { continue } wm.Twilio = &TwilioConfig{ - SID: e.SID, - Token: e.Token, - SenderNumber: e.SenderNumber, + SID: e.SID, + Token: e.Token, + SenderNumber: e.SenderNumber, + VerifyServiceSID: e.VerifyServiceSID, } wm.Description = e.Description wm.State = domain.SMSConfigStateInactive @@ -67,6 +69,9 @@ func (wm *IAMSMSConfigWriteModel) Reduce() error { if e.SenderNumber != nil { wm.Twilio.SenderNumber = *e.SenderNumber } + if e.VerifyServiceSID != nil { + wm.Twilio.VerifyServiceSID = *e.VerifyServiceSID + } case *instance.SMSConfigTwilioTokenChangedEvent: if wm.ID != e.ID { continue @@ -131,6 +136,7 @@ func (wm *IAMSMSConfigWriteModel) Reduce() error { } return wm.WriteModel.Reduce() } + func (wm *IAMSMSConfigWriteModel) Query() *eventstore.SearchQueryBuilder { return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent). ResourceOwner(wm.ResourceOwner). @@ -152,7 +158,7 @@ func (wm *IAMSMSConfigWriteModel) Query() *eventstore.SearchQueryBuilder { Builder() } -func (wm *IAMSMSConfigWriteModel) NewTwilioChangedEvent(ctx context.Context, aggregate *eventstore.Aggregate, id string, description, sid, senderNumber *string) (*instance.SMSConfigTwilioChangedEvent, bool, error) { +func (wm *IAMSMSConfigWriteModel) NewTwilioChangedEvent(ctx context.Context, aggregate *eventstore.Aggregate, id string, description, sid, senderNumber, verifyServiceSID *string) (*instance.SMSConfigTwilioChangedEvent, bool, error) { changes := make([]instance.SMSConfigTwilioChanges, 0) var err error @@ -169,6 +175,9 @@ func (wm *IAMSMSConfigWriteModel) NewTwilioChangedEvent(ctx context.Context, agg if senderNumber != nil && wm.Twilio.SenderNumber != *senderNumber { changes = append(changes, instance.ChangeSMSConfigTwilioSenderNumber(*senderNumber)) } + if verifyServiceSID != nil && wm.Twilio.VerifyServiceSID != *verifyServiceSID { + changes = append(changes, instance.ChangeSMSConfigTwilioVerifyServiceSID(*verifyServiceSID)) + } if len(changes) == 0 { return nil, false, nil @@ -204,3 +213,46 @@ func (wm *IAMSMSConfigWriteModel) NewHTTPChangedEvent(ctx context.Context, aggre } return changeEvent, true, nil } + +type IAMSMSLastActivatedConfigWriteModel struct { + eventstore.WriteModel + + activeID string +} + +func NewIAMSMSLastActivatedConfigWriteModel(instanceID string) *IAMSMSLastActivatedConfigWriteModel { + return &IAMSMSLastActivatedConfigWriteModel{ + WriteModel: eventstore.WriteModel{ + AggregateID: instanceID, + ResourceOwner: instanceID, + InstanceID: instanceID, + }, + } +} + +func (wm *IAMSMSLastActivatedConfigWriteModel) Reduce() error { + for _, event := range wm.Events { + switch e := event.(type) { + case *instance.SMSConfigActivatedEvent: + wm.activeID = e.ID + case *instance.SMSConfigTwilioActivatedEvent: + wm.activeID = e.ID + } + } + return wm.WriteModel.Reduce() +} + +func (wm *IAMSMSLastActivatedConfigWriteModel) Query() *eventstore.SearchQueryBuilder { + return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent). + ResourceOwner(wm.ResourceOwner). + OrderDesc(). + Limit(1). + AddQuery(). + AggregateTypes(instance.AggregateType). + AggregateIDs(wm.AggregateID). + EventTypes( + instance.SMSConfigActivatedEventType, + instance.SMSConfigTwilioActivatedEventType, + ). + Builder() +} diff --git a/internal/command/sms_config_test.go b/internal/command/sms_config_test.go index b0936ab8f..12fcd8661 100644 --- a/internal/command/sms_config_test.go +++ b/internal/command/sms_config_test.go @@ -72,6 +72,7 @@ func TestCommandSide_AddSMSConfigTwilio(t *testing.T) { KeyID: "id", Crypted: []byte("token"), }, + "", ), ), ), @@ -81,11 +82,12 @@ func TestCommandSide_AddSMSConfigTwilio(t *testing.T) { args: args{ ctx: context.Background(), sms: &AddTwilioConfig{ - ResourceOwner: "INSTANCE", - Description: "description", - SID: "sid", - Token: "token", - SenderNumber: "senderName", + ResourceOwner: "INSTANCE", + Description: "description", + SID: "sid", + Token: "token", + SenderNumber: "senderName", + VerifyServiceSID: "", }, }, res: res{ @@ -206,6 +208,7 @@ func TestCommandSide_ChangeSMSConfigTwilio(t *testing.T) { KeyID: "id", Crypted: []byte("token"), }, + "", ), ), ), @@ -214,11 +217,12 @@ func TestCommandSide_ChangeSMSConfigTwilio(t *testing.T) { args: args{ ctx: context.Background(), sms: &ChangeTwilioConfig{ - ResourceOwner: "INSTANCE", - ID: "providerid", - SID: gu.Ptr("sid"), - Token: gu.Ptr("token"), - SenderNumber: gu.Ptr("senderName"), + ResourceOwner: "INSTANCE", + ID: "providerid", + SID: gu.Ptr("sid"), + Token: gu.Ptr("token"), + SenderNumber: gu.Ptr("senderName"), + VerifyServiceSID: gu.Ptr(""), }, }, res: res{ @@ -246,6 +250,7 @@ func TestCommandSide_ChangeSMSConfigTwilio(t *testing.T) { KeyID: "id", Crypted: []byte("token"), }, + "verifyServiceSid", ), ), ), @@ -256,6 +261,7 @@ func TestCommandSide_ChangeSMSConfigTwilio(t *testing.T) { "sid2", "senderName2", "description2", + "verifyServiceSid2", ), ), ), @@ -263,12 +269,13 @@ func TestCommandSide_ChangeSMSConfigTwilio(t *testing.T) { args: args{ ctx: context.Background(), sms: &ChangeTwilioConfig{ - ResourceOwner: "INSTANCE", - ID: "providerid", - Description: gu.Ptr("description2"), - SID: gu.Ptr("sid2"), - Token: gu.Ptr("token2"), - SenderNumber: gu.Ptr("senderName2"), + ResourceOwner: "INSTANCE", + ID: "providerid", + Description: gu.Ptr("description2"), + SID: gu.Ptr("sid2"), + Token: gu.Ptr("token2"), + SenderNumber: gu.Ptr("senderName2"), + VerifyServiceSID: gu.Ptr("verifyServiceSid2"), }, }, res: res{ @@ -626,6 +633,7 @@ func TestCommandSide_ActivateSMSConfig(t *testing.T) { "sid", "sender-name", &crypto.CryptoValue{}, + "", ), ), eventFromEventPusher( @@ -663,6 +671,7 @@ func TestCommandSide_ActivateSMSConfig(t *testing.T) { "sid", "sender-name", &crypto.CryptoValue{}, + "", ), ), ), @@ -820,6 +829,7 @@ func TestCommandSide_DeactivateSMSConfig(t *testing.T) { "sid", "sender-name", &crypto.CryptoValue{}, + "", ), ), eventFromEventPusher( @@ -864,6 +874,7 @@ func TestCommandSide_DeactivateSMSConfig(t *testing.T) { "sid", "sender-name", &crypto.CryptoValue{}, + "", ), ), eventFromEventPusher( @@ -1036,6 +1047,7 @@ func TestCommandSide_RemoveSMSConfig(t *testing.T) { "sid", "sender-name", &crypto.CryptoValue{}, + "", ), ), ), @@ -1114,11 +1126,12 @@ func TestCommandSide_RemoveSMSConfig(t *testing.T) { } } -func newSMSConfigTwilioChangedEvent(ctx context.Context, id, sid, senderName, description string) *instance.SMSConfigTwilioChangedEvent { +func newSMSConfigTwilioChangedEvent(ctx context.Context, id, sid, senderName, description, verifyServiceSid string) *instance.SMSConfigTwilioChangedEvent { changes := []instance.SMSConfigTwilioChanges{ instance.ChangeSMSConfigTwilioSID(sid), instance.ChangeSMSConfigTwilioSenderNumber(senderName), instance.ChangeSMSConfigTwilioDescription(description), + instance.ChangeSMSConfigTwilioVerifyServiceSID(verifyServiceSid), } event, _ := instance.NewSMSConfigTwilioChangedEvent(ctx, &instance.NewAggregate("INSTANCE").Aggregate, diff --git a/internal/command/user_human.go b/internal/command/user_human.go index 120829615..825ae50f9 100644 --- a/internal/command/user_human.go +++ b/internal/command/user_human.go @@ -3,6 +3,7 @@ package command import ( "context" "strings" + "time" "golang.org/x/text/language" @@ -11,6 +12,7 @@ import ( "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/notification/senders" "github.com/zitadel/zitadel/internal/repository/user" "github.com/zitadel/zitadel/internal/telemetry/tracing" "github.com/zitadel/zitadel/internal/zerrors" @@ -315,14 +317,14 @@ func (c *Commands) addHumanCommandPhone(ctx context.Context, filter preparation. if human.Phone.Verified { return append(cmds, user.NewHumanPhoneVerifiedEvent(ctx, &a.Aggregate)), nil } - phoneCode, err := c.newPhoneCode(ctx, filter, codeAlg) + phoneCode, generatorID, err := c.newPhoneCode(ctx, filter, domain.SecretGeneratorTypeVerifyPhoneCode, codeAlg, c.defaultSecretGenerators.PhoneVerificationCode) if err != nil { return nil, err } if human.Phone.ReturnCode { human.PhoneCode = &phoneCode.Plain } - return append(cmds, user.NewHumanPhoneCodeAddedEventV2(ctx, &a.Aggregate, phoneCode.Crypted, phoneCode.Expiry, human.Phone.ReturnCode)), nil + return append(cmds, user.NewHumanPhoneCodeAddedEventV2(ctx, &a.Aggregate, phoneCode.CryptedCode(), phoneCode.CodeExpiry(), human.Phone.ReturnCode, generatorID)), nil } // Deprecated: use commands.NewUserHumanWriteModel, to remove deprecated eventstore.Filter @@ -561,11 +563,11 @@ func (c *Commands) createHuman(ctx context.Context, orgID string, human *domain. } if human.Phone != nil && human.PhoneNumber != "" && !human.IsPhoneVerified { - phoneCode, err := domain.NewPhoneCode(phoneCodeGenerator) + phoneCode, generatorID, err := c.newPhoneCode(ctx, c.eventstore.Filter, domain.SecretGeneratorTypeVerifyPhoneCode, c.userEncryption, c.defaultSecretGenerators.PhoneVerificationCode) //nolint:staticcheck if err != nil { return nil, nil, err } - events = append(events, user.NewHumanPhoneCodeAddedEvent(ctx, userAgg, phoneCode.Code, phoneCode.Expiry)) + events = append(events, user.NewHumanPhoneCodeAddedEvent(ctx, userAgg, phoneCode.CryptedCode(), phoneCode.CodeExpiry(), generatorID)) } else if human.Phone != nil && human.PhoneNumber != "" && human.IsPhoneVerified { events = append(events, user.NewHumanPhoneVerifiedEvent(ctx, userAgg)) } @@ -733,3 +735,35 @@ func AddHumanFromDomain(user *domain.Human, metadataList []*domain.Metadata, aut } return human } + +func verifyCode( + ctx context.Context, + codeCreationDate time.Time, + codeExpiry time.Duration, + encryptedCode *crypto.CryptoValue, + codeProviderID string, + codeVerificationID string, + code string, + codeAlg crypto.EncryptionAlgorithm, + getCodeVerifier func(ctx context.Context, id string) (_ senders.CodeGenerator, err error), +) (err error) { + if codeProviderID == "" { + if encryptedCode == nil { + return zerrors.ThrowPreconditionFailed(nil, "COMMAND-2M9fs", "Errors.User.Code.NotFound") + } + _, spanCrypto := tracing.NewNamedSpan(ctx, "crypto.VerifyCode") + defer func() { + spanCrypto.EndWithError(err) + }() + return crypto.VerifyCode(codeCreationDate, codeExpiry, encryptedCode, code, codeAlg) + } + if getCodeVerifier == nil { + return zerrors.ThrowPreconditionFailed(nil, "COMMAND-M0g95", "Errors.User.Code.NotConfigured") + } + verifier, err := getCodeVerifier(ctx, codeProviderID) + if err != nil { + return err + } + + return verifier.VerifyCode(codeVerificationID, code) +} diff --git a/internal/command/user_human_otp.go b/internal/command/user_human_otp.go index 7f587cf9e..e505288cb 100644 --- a/internal/command/user_human_otp.go +++ b/internal/command/user_human_otp.go @@ -9,9 +9,11 @@ import ( "github.com/zitadel/zitadel/internal/api/authz" http_util "github.com/zitadel/zitadel/internal/api/http" + "github.com/zitadel/zitadel/internal/command/preparation" "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/notification/senders" "github.com/zitadel/zitadel/internal/repository/user" "github.com/zitadel/zitadel/internal/telemetry/tracing" "github.com/zitadel/zitadel/internal/zerrors" @@ -335,8 +337,8 @@ func (c *Commands) HumanSendOTPSMS(ctx context.Context, userID, resourceOwner st smsWriteModel := func(ctx context.Context, userID string, resourceOwner string) (OTPWriteModel, error) { return c.otpSMSWriteModelByID(ctx, userID, resourceOwner) } - codeAddedEvent := func(ctx context.Context, aggregate *eventstore.Aggregate, code *crypto.CryptoValue, expiry time.Duration, info *user.AuthRequestInfo) eventstore.Command { - return user.NewHumanOTPSMSCodeAddedEvent(ctx, aggregate, code, expiry, info) + codeAddedEvent := func(ctx context.Context, aggregate *eventstore.Aggregate, code *crypto.CryptoValue, expiry time.Duration, info *user.AuthRequestInfo, generatorID string) eventstore.Command { + return user.NewHumanOTPSMSCodeAddedEvent(ctx, aggregate, code, expiry, info, generatorID) } return c.sendHumanOTP( ctx, @@ -347,15 +349,16 @@ func (c *Commands) HumanSendOTPSMS(ctx context.Context, userID, resourceOwner st domain.SecretGeneratorTypeOTPSMS, c.defaultSecretGenerators.OTPSMS, codeAddedEvent, + c.newPhoneCode, ) } -func (c *Commands) HumanOTPSMSCodeSent(ctx context.Context, userID, resourceOwner string) (err error) { +func (c *Commands) HumanOTPSMSCodeSent(ctx context.Context, userID, resourceOwner string, generatorInfo *senders.CodeGeneratorInfo) (err error) { smsWriteModel := func(ctx context.Context, userID string, resourceOwner string) (OTPWriteModel, error) { return c.otpSMSWriteModelByID(ctx, userID, resourceOwner) } codeSentEvent := func(ctx context.Context, aggregate *eventstore.Aggregate) eventstore.Command { - return user.NewHumanOTPSMSCodeSentEvent(ctx, aggregate) + return user.NewHumanOTPSMSCodeSentEvent(ctx, aggregate, generatorInfo) } return c.humanOTPSent(ctx, userID, resourceOwner, smsWriteModel, codeSentEvent) } @@ -379,6 +382,7 @@ func (c *Commands) HumanCheckOTPSMS(ctx context.Context, userID, code, resourceO writeModel, c.eventstore.FilterToQueryReducer, c.userEncryption, + c.phoneCodeVerifier, succeededEvent, failedEvent, ) @@ -467,9 +471,13 @@ func (c *Commands) HumanSendOTPEmail(ctx context.Context, userID, resourceOwner smsWriteModel := func(ctx context.Context, userID string, resourceOwner string) (OTPWriteModel, error) { return c.otpEmailWriteModelByID(ctx, userID, resourceOwner) } - codeAddedEvent := func(ctx context.Context, aggregate *eventstore.Aggregate, code *crypto.CryptoValue, expiry time.Duration, info *user.AuthRequestInfo) eventstore.Command { + codeAddedEvent := func(ctx context.Context, aggregate *eventstore.Aggregate, code *crypto.CryptoValue, expiry time.Duration, info *user.AuthRequestInfo, _ string) eventstore.Command { return user.NewHumanOTPEmailCodeAddedEvent(ctx, aggregate, code, expiry, info) } + generateCode := func(ctx context.Context, filter preparation.FilterToQueryReducer, typ domain.SecretGeneratorType, alg crypto.EncryptionAlgorithm, defaultConfig *crypto.GeneratorConfig) (*EncryptedCode, string, error) { + code, err := c.newEncryptedCodeWithDefault(ctx, filter, typ, alg, defaultConfig) + return code, "", err + } return c.sendHumanOTP( ctx, userID, @@ -479,6 +487,7 @@ func (c *Commands) HumanSendOTPEmail(ctx context.Context, userID, resourceOwner domain.SecretGeneratorTypeOTPEmail, c.defaultSecretGenerators.OTPEmail, codeAddedEvent, + generateCode, ) } @@ -511,6 +520,7 @@ func (c *Commands) HumanCheckOTPEmail(ctx context.Context, userID, code, resourc writeModel, c.eventstore.FilterToQueryReducer, c.userEncryption, + nil, // email currently always uses local code checks succeededEvent, failedEvent, ) @@ -529,7 +539,8 @@ func (c *Commands) sendHumanOTP( writeModelByID func(ctx context.Context, userID string, resourceOwner string) (OTPWriteModel, error), secretGeneratorType domain.SecretGeneratorType, defaultSecretGenerator *crypto.GeneratorConfig, - codeAddedEvent func(ctx context.Context, aggregate *eventstore.Aggregate, code *crypto.CryptoValue, expiry time.Duration, info *user.AuthRequestInfo) eventstore.Command, + codeAddedEvent func(ctx context.Context, aggregate *eventstore.Aggregate, code *crypto.CryptoValue, expiry time.Duration, info *user.AuthRequestInfo, generatorID string) eventstore.Command, + generateCode func(ctx context.Context, filter preparation.FilterToQueryReducer, secretGeneratorType domain.SecretGeneratorType, alg crypto.EncryptionAlgorithm, defaultConfig *crypto.GeneratorConfig) (*EncryptedCode, string, error), ) (err error) { if userID == "" { return zerrors.ThrowInvalidArgument(nil, "COMMAND-S3SF1", "Errors.User.UserIDMissing") @@ -541,17 +552,12 @@ func (c *Commands) sendHumanOTP( if !existingOTP.OTPAdded() { return zerrors.ThrowPreconditionFailed(nil, "COMMAND-SFD52", "Errors.User.MFA.OTP.NotReady") } - config, err := cryptoGeneratorConfigWithDefault(ctx, c.eventstore.Filter, secretGeneratorType, defaultSecretGenerator) //nolint:staticcheck - if err != nil { - return err - } - gen := crypto.NewEncryptionGenerator(*config, c.userEncryption) - value, _, err := crypto.NewCode(gen) + code, generatorID, err := generateCode(ctx, c.eventstore.Filter, secretGeneratorType, c.userEncryption, defaultSecretGenerator) //nolint:staticcheck if err != nil { return err } userAgg := &user.NewAggregate(userID, resourceOwner).Aggregate - _, err = c.eventstore.Push(ctx, codeAddedEvent(ctx, userAgg, value, gen.Expiry(), authRequestDomainToAuthRequestInfo(authRequest))) + _, err = c.eventstore.Push(ctx, codeAddedEvent(ctx, userAgg, code.CryptedCode(), code.CodeExpiry(), authRequestDomainToAuthRequestInfo(authRequest), generatorID)) return err } @@ -583,6 +589,7 @@ func checkOTP( writeModelByID func(ctx context.Context, userID string, resourceOwner string) (OTPCodeWriteModel, error), queryReducer func(ctx context.Context, r eventstore.QueryReducer) error, alg crypto.EncryptionAlgorithm, + getCodeVerifier func(ctx context.Context, id string) (senders.CodeGenerator, error), checkSucceededEvent, checkFailedEvent func(ctx context.Context, aggregate *eventstore.Aggregate, info *user.AuthRequestInfo) eventstore.Command, ) ([]eventstore.Command, error) { if userID == "" { @@ -598,12 +605,21 @@ func checkOTP( if !existingOTP.OTPAdded() { return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-d2r52", "Errors.User.MFA.OTP.NotReady") } - if existingOTP.Code() == nil { + if existingOTP.Code() == nil && existingOTP.GeneratorID() == "" { return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-S34gh", "Errors.User.Code.NotFound") } userAgg := &user.NewAggregate(userID, existingOTP.ResourceOwner()).Aggregate - verifyErr := crypto.VerifyCode(existingOTP.CodeCreationDate(), existingOTP.CodeExpiry(), existingOTP.Code(), code, alg) - + verifyErr := verifyCode( + ctx, + existingOTP.CodeCreationDate(), + existingOTP.CodeExpiry(), + existingOTP.Code(), + existingOTP.GeneratorID(), + existingOTP.ProviderVerificationID(), + code, + alg, + getCodeVerifier, + ) // recheck for additional events (failed OTP checks or locks) recheckErr := queryReducer(ctx, existingOTP) if recheckErr != nil { diff --git a/internal/command/user_human_otp_model.go b/internal/command/user_human_otp_model.go index 2780a52d7..7119adfea 100644 --- a/internal/command/user_human_otp_model.go +++ b/internal/command/user_human_otp_model.go @@ -90,6 +90,8 @@ type OTPCodeWriteModel interface { Code() *crypto.CryptoValue CheckFailedCount() uint64 UserLocked() bool + GeneratorID() string + ProviderVerificationID() string eventstore.QueryReducer } @@ -192,6 +194,20 @@ func (wm *HumanOTPSMSCodeWriteModel) UserLocked() bool { return wm.userLocked } +func (wm *HumanOTPSMSCodeWriteModel) GeneratorID() string { + if wm.otpCode == nil { + return "" + } + return wm.otpCode.GeneratorID +} + +func (wm *HumanOTPSMSCodeWriteModel) ProviderVerificationID() string { + if wm.otpCode == nil { + return "" + } + return wm.otpCode.VerificationID +} + func NewHumanOTPSMSCodeWriteModel(userID, resourceOwner string) *HumanOTPSMSCodeWriteModel { return &HumanOTPSMSCodeWriteModel{ HumanOTPSMSWriteModel: NewHumanOTPSMSWriteModel(userID, resourceOwner), @@ -206,7 +222,11 @@ func (wm *HumanOTPSMSCodeWriteModel) Reduce() error { Code: e.Code, CreationDate: e.CreationDate(), Expiry: e.Expiry, + GeneratorID: e.GeneratorID, } + case *user.HumanOTPSMSCodeSentEvent: + wm.otpCode.GeneratorID = e.GeneratorInfo.GetID() + wm.otpCode.VerificationID = e.GeneratorInfo.GetVerificationID() case *user.HumanOTPSMSCheckSucceededEvent: wm.checkFailedCount = 0 case *user.HumanOTPSMSCheckFailedEvent: @@ -228,6 +248,7 @@ func (wm *HumanOTPSMSCodeWriteModel) Query() *eventstore.SearchQueryBuilder { AggregateIDs(wm.AggregateID). EventTypes( user.HumanOTPSMSCodeAddedType, + user.HumanOTPSMSCodeSentType, user.HumanOTPSMSCheckSucceededType, user.HumanOTPSMSCheckFailedType, user.UserLockedType, @@ -344,6 +365,20 @@ func (wm *HumanOTPEmailCodeWriteModel) UserLocked() bool { return wm.userLocked } +func (wm *HumanOTPEmailCodeWriteModel) GeneratorID() string { + if wm.otpCode == nil { + return "" + } + return wm.otpCode.GeneratorID +} + +func (wm *HumanOTPEmailCodeWriteModel) ProviderVerificationID() string { + if wm.otpCode == nil { + return "" + } + return wm.otpCode.VerificationID +} + func NewHumanOTPEmailCodeWriteModel(userID, resourceOwner string) *HumanOTPEmailCodeWriteModel { return &HumanOTPEmailCodeWriteModel{ HumanOTPEmailWriteModel: NewHumanOTPEmailWriteModel(userID, resourceOwner), diff --git a/internal/command/user_human_otp_test.go b/internal/command/user_human_otp_test.go index 197d22251..330025cf3 100644 --- a/internal/command/user_human_otp_test.go +++ b/internal/command/user_human_otp_test.go @@ -18,6 +18,8 @@ import ( "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/notification/senders" + "github.com/zitadel/zitadel/internal/notification/senders/mock" "github.com/zitadel/zitadel/internal/repository/instance" "github.com/zitadel/zitadel/internal/repository/org" "github.com/zitadel/zitadel/internal/repository/user" @@ -1439,9 +1441,9 @@ func TestCommandSide_HumanSendOTPSMS(t *testing.T) { }, } type fields struct { - eventstore func(*testing.T) *eventstore.Eventstore - userEncryption crypto.EncryptionAlgorithm - defaultSecretGenerators *SecretGenerators + eventstore func(*testing.T) *eventstore.Eventstore + defaultSecretGenerators *SecretGenerators + newEncryptedCodeWithDefault encryptedCodeWithDefaultFunc } type ( args struct { @@ -1506,16 +1508,33 @@ func TestCommandSide_HumanSendOTPSMS(t *testing.T) { ), expectFilter( eventFromEventPusher( - instance.NewSecretGeneratorAddedEvent(context.Background(), + instance.NewSMSConfigActivatedEvent( + context.Background(), &instance.NewAggregate("instanceID").Aggregate, - domain.SecretGeneratorTypeOTPSMS, - 8, - time.Hour, - true, - true, - true, - true, - )), + "id", + ), + ), + ), + expectFilter( + eventFromEventPusher( + instance.NewSMSConfigTwilioAddedEvent( + context.Background(), + &instance.NewAggregate("instanceID").Aggregate, + "id", + "", + "sid", + "senderNumber", + &crypto.CryptoValue{CryptoType: crypto.TypeEncryption, Algorithm: "enc", KeyID: "id", Crypted: []byte("crypted")}, + "", + ), + ), + eventFromEventPusher( + instance.NewSMSConfigActivatedEvent( + context.Background(), + &instance.NewAggregate("instanceID").Aggregate, + "id", + ), + ), ), expectPush( user.NewHumanOTPSMSCodeAddedEvent(ctx, @@ -1528,11 +1547,77 @@ func TestCommandSide_HumanSendOTPSMS(t *testing.T) { }, time.Hour, nil, + "", ), ), ), - userEncryption: crypto.CreateMockEncryptionAlgWithCode(gomock.NewController(t), "12345678"), - defaultSecretGenerators: defaultGenerators, + defaultSecretGenerators: defaultGenerators, + newEncryptedCodeWithDefault: mockEncryptedCodeWithDefault("12345678", time.Hour), + }, + args: args{ + ctx: ctx, + userID: "user1", + resourceOwner: "org1", + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + { + name: "successful add (external code)", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + user.NewHumanOTPSMSAddedEvent(ctx, + &user.NewAggregate("user1", "org1").Aggregate, + ), + ), + ), + expectFilter( + eventFromEventPusher( + instance.NewSMSConfigActivatedEvent( + context.Background(), + &instance.NewAggregate("instanceID").Aggregate, + "id", + ), + ), + ), + expectFilter( + eventFromEventPusher( + instance.NewSMSConfigTwilioAddedEvent( + context.Background(), + &instance.NewAggregate("instanceID").Aggregate, + "id", + "", + "sid", + "senderNumber", + &crypto.CryptoValue{CryptoType: crypto.TypeEncryption, Algorithm: "enc", KeyID: "id", Crypted: []byte("crypted")}, + "verifyServiceSID", + ), + ), + eventFromEventPusher( + instance.NewSMSConfigActivatedEvent( + context.Background(), + &instance.NewAggregate("instanceID").Aggregate, + "id", + ), + ), + ), + expectPush( + user.NewHumanOTPSMSCodeAddedEvent(ctx, + &user.NewAggregate("user1", "org1").Aggregate, + nil, + 0, + nil, + "id", + ), + ), + ), + defaultSecretGenerators: defaultGenerators, + newEncryptedCodeWithDefault: mockEncryptedCodeWithDefault("12345678", time.Hour), }, args: args{ ctx: ctx, @@ -1556,7 +1641,36 @@ func TestCommandSide_HumanSendOTPSMS(t *testing.T) { ), ), ), - expectFilter(), + expectFilter( + eventFromEventPusher( + instance.NewSMSConfigActivatedEvent( + context.Background(), + &instance.NewAggregate("instanceID").Aggregate, + "id", + ), + ), + ), + expectFilter( + eventFromEventPusher( + instance.NewSMSConfigTwilioAddedEvent( + context.Background(), + &instance.NewAggregate("instanceID").Aggregate, + "id", + "", + "sid", + "senderNumber", + &crypto.CryptoValue{CryptoType: crypto.TypeEncryption, Algorithm: "enc", KeyID: "id", Crypted: []byte("crypted")}, + "", + ), + ), + eventFromEventPusher( + instance.NewSMSConfigActivatedEvent( + context.Background(), + &instance.NewAggregate("instanceID").Aggregate, + "id", + ), + ), + ), expectPush( user.NewHumanOTPSMSCodeAddedEvent(ctx, &user.NewAggregate("user1", "org1").Aggregate, @@ -1568,11 +1682,12 @@ func TestCommandSide_HumanSendOTPSMS(t *testing.T) { }, time.Hour, nil, + "", ), ), ), - userEncryption: crypto.CreateMockEncryptionAlgWithCode(gomock.NewController(t), "12345678"), - defaultSecretGenerators: defaultGenerators, + defaultSecretGenerators: defaultGenerators, + newEncryptedCodeWithDefault: mockEncryptedCodeWithDefault("12345678", time.Hour), }, args: args{ ctx: ctx, @@ -1598,16 +1713,33 @@ func TestCommandSide_HumanSendOTPSMS(t *testing.T) { ), expectFilter( eventFromEventPusher( - instance.NewSecretGeneratorAddedEvent(context.Background(), + instance.NewSMSConfigActivatedEvent( + context.Background(), &instance.NewAggregate("instanceID").Aggregate, - domain.SecretGeneratorTypeOTPSMS, - 8, - time.Hour, - true, - true, - true, - true, - )), + "id", + ), + ), + ), + expectFilter( + eventFromEventPusher( + instance.NewSMSConfigTwilioAddedEvent( + context.Background(), + &instance.NewAggregate("instanceID").Aggregate, + "id", + "", + "sid", + "senderNumber", + &crypto.CryptoValue{CryptoType: crypto.TypeEncryption, Algorithm: "enc", KeyID: "id", Crypted: []byte("crypted")}, + "", + ), + ), + eventFromEventPusher( + instance.NewSMSConfigActivatedEvent( + context.Background(), + &instance.NewAggregate("instanceID").Aggregate, + "id", + ), + ), ), expectPush( user.NewHumanOTPSMSCodeAddedEvent(ctx, @@ -1628,11 +1760,12 @@ func TestCommandSide_HumanSendOTPSMS(t *testing.T) { RemoteIP: net.IP{192, 0, 2, 1}, }, }, + "", ), ), ), - userEncryption: crypto.CreateMockEncryptionAlgWithCode(gomock.NewController(t), "12345678"), - defaultSecretGenerators: defaultGenerators, + defaultSecretGenerators: defaultGenerators, + newEncryptedCodeWithDefault: mockEncryptedCodeWithDefault("12345678", time.Hour), }, args: args{ ctx: ctx, @@ -1658,9 +1791,9 @@ func TestCommandSide_HumanSendOTPSMS(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := &Commands{ - eventstore: tt.fields.eventstore(t), - userEncryption: tt.fields.userEncryption, - defaultSecretGenerators: tt.fields.defaultSecretGenerators, + eventstore: tt.fields.eventstore(t), + defaultSecretGenerators: tt.fields.defaultSecretGenerators, + newEncryptedCodeWithDefault: tt.fields.newEncryptedCodeWithDefault, } err := r.HumanSendOTPSMS(tt.args.ctx, tt.args.userID, tt.args.resourceOwner, tt.args.authRequest) assert.ErrorIs(t, err, tt.res.err) @@ -1678,6 +1811,7 @@ func TestCommandSide_HumanOTPSMSCodeSent(t *testing.T) { ctx context.Context userID string resourceOwner string + generatorInfo *senders.CodeGeneratorInfo } ) type res struct { @@ -1734,6 +1868,7 @@ func TestCommandSide_HumanOTPSMSCodeSent(t *testing.T) { expectPush( user.NewHumanOTPSMSCodeSentEvent(ctx, &user.NewAggregate("user1", "org1").Aggregate, + &senders.CodeGeneratorInfo{}, ), ), ), @@ -1742,6 +1877,44 @@ func TestCommandSide_HumanOTPSMSCodeSent(t *testing.T) { ctx: ctx, userID: "user1", resourceOwner: "org1", + generatorInfo: &senders.CodeGeneratorInfo{}, + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + { + name: "successful add (external code)", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + user.NewHumanOTPSMSAddedEvent(ctx, + &user.NewAggregate("user1", "org1").Aggregate, + ), + ), + ), + expectPush( + user.NewHumanOTPSMSCodeSentEvent(ctx, + &user.NewAggregate("user1", "org1").Aggregate, + &senders.CodeGeneratorInfo{ + ID: "generatorID", + VerificationID: "verificationID", + }, + ), + ), + ), + }, + args: args{ + ctx: ctx, + userID: "user1", + resourceOwner: "org1", + generatorInfo: &senders.CodeGeneratorInfo{ + ID: "generatorID", + VerificationID: "verificationID", + }, }, res: res{ want: &domain.ObjectDetails{ @@ -1755,7 +1928,7 @@ func TestCommandSide_HumanOTPSMSCodeSent(t *testing.T) { r := &Commands{ eventstore: tt.fields.eventstore(t), } - err := r.HumanOTPSMSCodeSent(tt.args.ctx, tt.args.userID, tt.args.resourceOwner) + err := r.HumanOTPSMSCodeSent(tt.args.ctx, tt.args.userID, tt.args.resourceOwner, tt.args.generatorInfo) assert.ErrorIs(t, err, tt.res.err) }) } @@ -1764,8 +1937,9 @@ func TestCommandSide_HumanOTPSMSCodeSent(t *testing.T) { func TestCommandSide_HumanCheckOTPSMS(t *testing.T) { ctx := authz.NewMockContext("inst1", "org1", "user1") type fields struct { - eventstore func(*testing.T) *eventstore.Eventstore - userEncryption crypto.EncryptionAlgorithm + eventstore func(*testing.T) *eventstore.Eventstore + userEncryption crypto.EncryptionAlgorithm + phoneCodeVerifier func(ctx context.Context, id string) (senders.CodeGenerator, error) } type ( args struct { @@ -1885,6 +2059,7 @@ func TestCommandSide_HumanCheckOTPSMS(t *testing.T) { RemoteIP: net.IP{192, 0, 2, 1}, }, }, + "", ), ), ), @@ -1962,6 +2137,7 @@ func TestCommandSide_HumanCheckOTPSMS(t *testing.T) { RemoteIP: net.IP{192, 0, 2, 1}, }, }, + "", ), ), ), @@ -2042,6 +2218,7 @@ func TestCommandSide_HumanCheckOTPSMS(t *testing.T) { RemoteIP: net.IP{192, 0, 2, 1}, }, }, + "", ), ), ), @@ -2113,6 +2290,7 @@ func TestCommandSide_HumanCheckOTPSMS(t *testing.T) { RemoteIP: net.IP{192, 0, 2, 1}, }, }, + "", ), ), ), @@ -2143,12 +2321,94 @@ func TestCommandSide_HumanCheckOTPSMS(t *testing.T) { err: zerrors.ThrowPreconditionFailed(nil, "COMMAND-S6h4R", "Errors.User.Locked"), }, }, + { + name: "code ok (external)", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + user.NewHumanOTPSMSAddedEvent(ctx, + &user.NewAggregate("user1", "org1").Aggregate, + ), + ), + eventFromEventPusherWithCreationDateNow( + user.NewHumanOTPSMSCodeAddedEvent(ctx, + &user.NewAggregate("user1", "org1").Aggregate, + nil, + 0, + &user.AuthRequestInfo{ + ID: "authRequestID", + UserAgentID: "userAgentID", + BrowserInfo: &user.BrowserInfo{ + UserAgent: "user-agent", + AcceptLanguage: "en", + RemoteIP: net.IP{192, 0, 2, 1}, + }, + }, + "id", + ), + ), + eventFromEventPusherWithCreationDateNow( + user.NewHumanOTPSMSCodeSentEvent(ctx, + &user.NewAggregate("user1", "org1").Aggregate, + &senders.CodeGeneratorInfo{ + ID: "id", + VerificationID: "verificationID", + }, + ), + ), + ), + expectFilter(), // recheck + expectPush( + user.NewHumanOTPSMSCheckSucceededEvent(ctx, + &user.NewAggregate("user1", "org1").Aggregate, + &user.AuthRequestInfo{ + ID: "authRequestID", + UserAgentID: "userAgentID", + BrowserInfo: &user.BrowserInfo{ + UserAgent: "user-agent", + AcceptLanguage: "en", + RemoteIP: net.IP{192, 0, 2, 1}, + }, + }, + ), + ), + ), + userEncryption: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), + phoneCodeVerifier: func(ctx context.Context, id string) (senders.CodeGenerator, error) { + sender := mock.NewMockCodeGenerator(gomock.NewController(t)) + sender.EXPECT().VerifyCode("verificationID", "code").Return(nil) + return sender, nil + }, + }, + args: args{ + ctx: ctx, + userID: "user1", + code: "code", + resourceOwner: "org1", + authRequest: &domain.AuthRequest{ + ID: "authRequestID", + AgentID: "userAgentID", + BrowserInfo: &domain.BrowserInfo{ + UserAgent: "user-agent", + AcceptLanguage: "en", + RemoteIP: net.IP{192, 0, 2, 1}, + }, + }, + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := &Commands{ - eventstore: tt.fields.eventstore(t), - userEncryption: tt.fields.userEncryption, + eventstore: tt.fields.eventstore(t), + userEncryption: tt.fields.userEncryption, + phoneCodeVerifier: tt.fields.phoneCodeVerifier, } err := r.HumanCheckOTPSMS(tt.args.ctx, tt.args.userID, tt.args.code, tt.args.resourceOwner, tt.args.authRequest) assert.ErrorIs(t, err, tt.res.err) @@ -2594,9 +2854,9 @@ func TestCommandSide_HumanSendOTPEmail(t *testing.T) { }, } type fields struct { - eventstore func(*testing.T) *eventstore.Eventstore - userEncryption crypto.EncryptionAlgorithm - defaultSecretGenerators *SecretGenerators + eventstore func(*testing.T) *eventstore.Eventstore + defaultSecretGenerators *SecretGenerators + newEncryptedCodeWithDefault encryptedCodeWithDefaultFunc } type ( args struct { @@ -2659,19 +2919,6 @@ func TestCommandSide_HumanSendOTPEmail(t *testing.T) { ), ), ), - expectFilter( - eventFromEventPusher( - instance.NewSecretGeneratorAddedEvent(context.Background(), - &instance.NewAggregate("instanceID").Aggregate, - domain.SecretGeneratorTypeOTPEmail, - 8, - time.Hour, - true, - true, - true, - true, - )), - ), expectPush( user.NewHumanOTPEmailCodeAddedEvent(ctx, &user.NewAggregate("user1", "org1").Aggregate, @@ -2686,48 +2933,8 @@ func TestCommandSide_HumanSendOTPEmail(t *testing.T) { ), ), ), - userEncryption: crypto.CreateMockEncryptionAlgWithCode(gomock.NewController(t), "12345678"), - defaultSecretGenerators: defaultGenerators, - }, - args: args{ - ctx: ctx, - userID: "user1", - resourceOwner: "org1", - }, - res: res{ - want: &domain.ObjectDetails{ - ResourceOwner: "org1", - }, - }, - }, - { - name: "successful add (without secret config)", - fields: fields{ - eventstore: expectEventstore( - expectFilter( - eventFromEventPusher( - user.NewHumanOTPEmailAddedEvent(ctx, - &user.NewAggregate("user1", "org1").Aggregate, - ), - ), - ), - expectFilter(), - expectPush( - user.NewHumanOTPEmailCodeAddedEvent(ctx, - &user.NewAggregate("user1", "org1").Aggregate, - &crypto.CryptoValue{ - CryptoType: crypto.TypeEncryption, - Algorithm: "enc", - KeyID: "id", - Crypted: []byte("12345678"), - }, - time.Hour, - nil, - ), - ), - ), - userEncryption: crypto.CreateMockEncryptionAlgWithCode(gomock.NewController(t), "12345678"), - defaultSecretGenerators: defaultGenerators, + newEncryptedCodeWithDefault: mockEncryptedCodeWithDefault("12345678", time.Hour), + defaultSecretGenerators: defaultGenerators, }, args: args{ ctx: ctx, @@ -2751,19 +2958,6 @@ func TestCommandSide_HumanSendOTPEmail(t *testing.T) { ), ), ), - expectFilter( - eventFromEventPusher( - instance.NewSecretGeneratorAddedEvent(context.Background(), - &instance.NewAggregate("instanceID").Aggregate, - domain.SecretGeneratorTypeOTPEmail, - 8, - time.Hour, - true, - true, - true, - true, - )), - ), expectPush( user.NewHumanOTPEmailCodeAddedEvent(ctx, &user.NewAggregate("user1", "org1").Aggregate, @@ -2786,8 +2980,8 @@ func TestCommandSide_HumanSendOTPEmail(t *testing.T) { ), ), ), - userEncryption: crypto.CreateMockEncryptionAlgWithCode(gomock.NewController(t), "12345678"), - defaultSecretGenerators: defaultGenerators, + newEncryptedCodeWithDefault: mockEncryptedCodeWithDefault("12345678", time.Hour), + defaultSecretGenerators: defaultGenerators, }, args: args{ ctx: ctx, @@ -2813,9 +3007,9 @@ func TestCommandSide_HumanSendOTPEmail(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := &Commands{ - eventstore: tt.fields.eventstore(t), - userEncryption: tt.fields.userEncryption, - defaultSecretGenerators: tt.fields.defaultSecretGenerators, + eventstore: tt.fields.eventstore(t), + defaultSecretGenerators: tt.fields.defaultSecretGenerators, + newEncryptedCodeWithDefault: tt.fields.newEncryptedCodeWithDefault, } err := r.HumanSendOTPEmail(tt.args.ctx, tt.args.userID, tt.args.resourceOwner, tt.args.authRequest) assert.ErrorIs(t, err, tt.res.err) diff --git a/internal/command/user_human_password.go b/internal/command/user_human_password.go index a94624231..9b686f88b 100644 --- a/internal/command/user_human_password.go +++ b/internal/command/user_human_password.go @@ -11,6 +11,7 @@ import ( "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/notification/senders" "github.com/zitadel/zitadel/internal/repository/user" "github.com/zitadel/zitadel/internal/telemetry/tracing" "github.com/zitadel/zitadel/internal/zerrors" @@ -67,7 +68,14 @@ func (c *Commands) SetPasswordWithVerifyCode(ctx context.Context, orgID, userID, "", userAgentID, changeRequired, - c.setPasswordWithVerifyCode(wm.CodeCreationDate, wm.CodeExpiry, wm.Code, code), + c.setPasswordWithVerifyCode( + wm.CodeCreationDate, + wm.CodeExpiry, + wm.Code, + wm.GeneratorID, + wm.VerificationID, + code, + ), ) } @@ -111,17 +119,22 @@ func (c *Commands) setPasswordWithVerifyCode( passwordCodeCreationDate time.Time, passwordCodeExpiry time.Duration, passwordCode *crypto.CryptoValue, + passwordCodeProviderID string, + passwordCodeVerificationID string, code string, ) setPasswordVerification { return func(ctx context.Context) (_ string, err error) { - if passwordCode == nil { - return "", zerrors.ThrowPreconditionFailed(nil, "COMMAND-2M9fs", "Errors.User.Code.NotFound") - } - _, spanCrypto := tracing.NewNamedSpan(ctx, "crypto.VerifyCode") - defer func() { - spanCrypto.EndWithError(err) - }() - return "", crypto.VerifyCode(passwordCodeCreationDate, passwordCodeExpiry, passwordCode, code, c.userEncryption) + return "", verifyCode( + ctx, + passwordCodeCreationDate, + passwordCodeExpiry, + passwordCode, + passwordCodeProviderID, + passwordCodeVerificationID, + code, + c.userEncryption, + c.phoneCodeVerifier, // password code can only be custom generated by SMS + ) } } @@ -253,11 +266,17 @@ func (c *Commands) RequestSetPassword(ctx context.Context, userID, resourceOwner return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-2M9sd", "Errors.User.NotInitialised") } userAgg := UserAggregateFromWriteModel(&existingHuman.WriteModel) - passwordCode, err := c.newEncryptedCode(ctx, c.eventstore.Filter, domain.SecretGeneratorTypePasswordResetCode, c.userEncryption) //nolint:staticcheck + var passwordCode *EncryptedCode + var generatorID string + if notifyType == domain.NotificationTypeSms { + passwordCode, generatorID, err = c.newPhoneCode(ctx, c.eventstore.Filter, domain.SecretGeneratorTypePasswordResetCode, c.userEncryption, c.defaultSecretGenerators.PasswordVerificationCode) //nolint:staticcheck + } else { + passwordCode, err = c.newEncryptedCode(ctx, c.eventstore.Filter, domain.SecretGeneratorTypePasswordResetCode, c.userEncryption) //nolint:staticcheck + } if err != nil { return nil, err } - pushedEvents, err := c.eventstore.Push(ctx, user.NewHumanPasswordCodeAddedEvent(ctx, userAgg, passwordCode.Crypted, passwordCode.Expiry, notifyType, authRequestID)) + pushedEvents, err := c.eventstore.Push(ctx, user.NewHumanPasswordCodeAddedEvent(ctx, userAgg, passwordCode.CryptedCode(), passwordCode.CodeExpiry(), notifyType, authRequestID, generatorID)) if err != nil { return nil, err } @@ -269,7 +288,7 @@ func (c *Commands) RequestSetPassword(ctx context.Context, userID, resourceOwner } // PasswordCodeSent notification send with code to change password -func (c *Commands) PasswordCodeSent(ctx context.Context, orgID, userID string) (err error) { +func (c *Commands) PasswordCodeSent(ctx context.Context, orgID, userID string, generatorInfo *senders.CodeGeneratorInfo) (err error) { if userID == "" { return zerrors.ThrowInvalidArgument(nil, "COMMAND-meEfe", "Errors.User.UserIDMissing") } @@ -282,7 +301,7 @@ func (c *Commands) PasswordCodeSent(ctx context.Context, orgID, userID string) ( return zerrors.ThrowPreconditionFailed(nil, "COMMAND-3n77z", "Errors.User.NotFound") } userAgg := UserAggregateFromWriteModel(&existingPassword.WriteModel) - _, err = c.eventstore.Push(ctx, user.NewHumanPasswordCodeSentEvent(ctx, userAgg)) + _, err = c.eventstore.Push(ctx, user.NewHumanPasswordCodeSentEvent(ctx, userAgg, generatorInfo)) return err } diff --git a/internal/command/user_human_password_model.go b/internal/command/user_human_password_model.go index ce2376981..e43731d86 100644 --- a/internal/command/user_human_password_model.go +++ b/internal/command/user_human_password_model.go @@ -19,6 +19,8 @@ type HumanPasswordWriteModel struct { CodeCreationDate time.Time CodeExpiry time.Duration PasswordCheckFailedCount uint64 + GeneratorID string + VerificationID string UserState domain.UserState } @@ -56,6 +58,10 @@ func (wm *HumanPasswordWriteModel) Reduce() error { wm.Code = e.Code wm.CodeCreationDate = e.CreationDate() wm.CodeExpiry = e.Expiry + wm.GeneratorID = e.GeneratorID + case *user.HumanPasswordCodeSentEvent: + wm.GeneratorID = e.GeneratorInfo.GetID() + wm.VerificationID = e.GeneratorInfo.GetVerificationID() case *user.HumanEmailVerifiedEvent: if wm.UserState == domain.UserStateInitial { wm.UserState = domain.UserStateActive @@ -91,6 +97,7 @@ func (wm *HumanPasswordWriteModel) Query() *eventstore.SearchQueryBuilder { user.HumanInitializedCheckSucceededType, user.HumanPasswordChangedType, user.HumanPasswordCodeAddedType, + user.HumanPasswordCodeSentType, user.HumanEmailVerifiedType, user.HumanPasswordCheckFailedType, user.HumanPasswordCheckSucceededType, @@ -104,6 +111,7 @@ func (wm *HumanPasswordWriteModel) Query() *eventstore.SearchQueryBuilder { user.UserV1InitializedCheckSucceededType, user.UserV1PasswordChangedType, user.UserV1PasswordCodeAddedType, + user.UserV1PasswordCodeSentType, user.UserV1EmailVerifiedType, user.UserV1PasswordCheckFailedType, user.UserV1PasswordCheckSucceededType, diff --git a/internal/command/user_human_password_test.go b/internal/command/user_human_password_test.go index 1eb886828..9eee9d5b9 100644 --- a/internal/command/user_human_password_test.go +++ b/internal/command/user_human_password_test.go @@ -15,6 +15,8 @@ import ( "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/notification/senders" + "github.com/zitadel/zitadel/internal/notification/senders/mock" "github.com/zitadel/zitadel/internal/repository/org" "github.com/zitadel/zitadel/internal/repository/user" "github.com/zitadel/zitadel/internal/zerrors" @@ -265,6 +267,7 @@ func TestCommandSide_SetPasswordWithVerifyCode(t *testing.T) { eventstore func(*testing.T) *eventstore.Eventstore userEncryption crypto.EncryptionAlgorithm userPasswordHasher *crypto.Hasher + phoneCodeVerifier func(ctx context.Context, id string) (senders.CodeGenerator, error) } type args struct { ctx context.Context @@ -393,6 +396,7 @@ func TestCommandSide_SetPasswordWithVerifyCode(t *testing.T) { time.Hour*1, domain.NotificationTypeEmail, "", + "", ), ), ), @@ -446,6 +450,7 @@ func TestCommandSide_SetPasswordWithVerifyCode(t *testing.T) { time.Hour*1, domain.NotificationTypeEmail, "", + "", ), ), ), @@ -522,6 +527,7 @@ func TestCommandSide_SetPasswordWithVerifyCode(t *testing.T) { time.Hour*1, domain.NotificationTypeEmail, "", + "", ), ), ), @@ -599,6 +605,7 @@ func TestCommandSide_SetPasswordWithVerifyCode(t *testing.T) { time.Hour*1, domain.NotificationTypeEmail, "", + "", ), ), ), @@ -641,6 +648,92 @@ func TestCommandSide_SetPasswordWithVerifyCode(t *testing.T) { }, }, }, + { + name: "set password (external code), ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username", + "firstname", + "lastname", + "nickname", + "displayname", + language.German, + domain.GenderUnspecified, + "email@test.ch", + true, + ), + ), + eventFromEventPusher( + user.NewHumanEmailVerifiedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + ), + ), + eventFromEventPusherWithCreationDateNow( + user.NewHumanPasswordCodeAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + nil, + 0, + domain.NotificationTypeSms, + "", + "id", + ), + ), + eventFromEventPusherWithCreationDateNow( + user.NewHumanPasswordCodeSentEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + &senders.CodeGeneratorInfo{ + ID: "id", + VerificationID: "verificationID", + }, + ), + ), + ), + expectFilter( + eventFromEventPusher( + org.NewPasswordComplexityPolicyAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + 1, + false, + false, + false, + false, + ), + ), + ), + expectPush( + user.NewHumanPasswordChangedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "$plain$x$password", + false, + "", + ), + ), + ), + userPasswordHasher: mockPasswordHasher("x"), + userEncryption: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), + phoneCodeVerifier: func(ctx context.Context, id string) (senders.CodeGenerator, error) { + sender := mock.NewMockCodeGenerator(gomock.NewController(t)) + sender.EXPECT().VerifyCode("verificationID", "a").Return(nil) + return sender, nil + }, + }, + args: args{ + ctx: context.Background(), + userID: "user1", + resourceOwner: "org1", + password: "password", + code: "a", + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -648,6 +741,7 @@ func TestCommandSide_SetPasswordWithVerifyCode(t *testing.T) { eventstore: tt.fields.eventstore(t), userPasswordHasher: tt.fields.userPasswordHasher, userEncryption: tt.fields.userEncryption, + phoneCodeVerifier: tt.fields.phoneCodeVerifier, } got, err := r.SetPasswordWithVerifyCode(tt.args.ctx, tt.args.resourceOwner, tt.args.userID, tt.args.code, tt.args.password, tt.args.userAgentID, tt.args.changeRequired) if tt.res.err == nil { @@ -1248,6 +1342,7 @@ func TestCommandSide_RequestSetPassword(t *testing.T) { time.Hour*1, domain.NotificationTypeEmail, "", + "", ), ), ), @@ -1304,6 +1399,7 @@ func TestCommandSide_RequestSetPassword(t *testing.T) { time.Hour*1, domain.NotificationTypeEmail, "authRequestID", + "", ), ), ), @@ -1350,6 +1446,7 @@ func TestCommandSide_PasswordCodeSent(t *testing.T) { ctx context.Context userID string resourceOwner string + generatorInfo *senders.CodeGeneratorInfo } type res struct { err func(error) bool @@ -1418,6 +1515,7 @@ func TestCommandSide_PasswordCodeSent(t *testing.T) { expectPush( user.NewHumanPasswordCodeSentEvent(context.Background(), &user.NewAggregate("user1", "org1").Aggregate, + &senders.CodeGeneratorInfo{}, ), ), ), @@ -1426,6 +1524,55 @@ func TestCommandSide_PasswordCodeSent(t *testing.T) { ctx: context.Background(), userID: "user1", resourceOwner: "org1", + generatorInfo: &senders.CodeGeneratorInfo{}, + }, + res: res{}, + }, + { + name: "code sent (external code), ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username", + "firstname", + "lastname", + "nickname", + "displayname", + language.German, + domain.GenderUnspecified, + "email@test.ch", + true, + ), + ), + eventFromEventPusher( + user.NewHumanPhoneChangedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "+411234567", + ), + ), + ), + expectPush( + user.NewHumanPasswordCodeSentEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + &senders.CodeGeneratorInfo{ + ID: "generatorID", + VerificationID: "verificationID", + }, + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + userID: "user1", + resourceOwner: "org1", + generatorInfo: &senders.CodeGeneratorInfo{ + ID: "generatorID", + VerificationID: "verificationID", + }, }, res: res{}, }, @@ -1435,7 +1582,7 @@ func TestCommandSide_PasswordCodeSent(t *testing.T) { r := &Commands{ eventstore: tt.fields.eventstore(t), } - err := r.PasswordCodeSent(tt.args.ctx, tt.args.resourceOwner, tt.args.userID) + err := r.PasswordCodeSent(tt.args.ctx, tt.args.resourceOwner, tt.args.userID, tt.args.generatorInfo) if tt.res.err == nil { assert.NoError(t, err) } diff --git a/internal/command/user_human_phone.go b/internal/command/user_human_phone.go index 7003c0530..0da72b022 100644 --- a/internal/command/user_human_phone.go +++ b/internal/command/user_human_phone.go @@ -5,9 +5,12 @@ import ( "github.com/zitadel/logging" + "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/notification/channels/twilio" + "github.com/zitadel/zitadel/internal/notification/senders" "github.com/zitadel/zitadel/internal/repository/user" "github.com/zitadel/zitadel/internal/telemetry/tracing" "github.com/zitadel/zitadel/internal/zerrors" @@ -40,11 +43,11 @@ func (c *Commands) ChangeHumanPhone(ctx context.Context, phone *domain.Phone, re if phone.IsPhoneVerified { events = append(events, user.NewHumanPhoneVerifiedEvent(ctx, userAgg)) } else { - phoneCode, err := domain.NewPhoneCode(phoneCodeGenerator) + phoneCode, generatorID, err := c.newPhoneCode(ctx, c.eventstore.Filter, domain.SecretGeneratorTypeVerifyPhoneCode, c.userEncryption, c.defaultSecretGenerators.PhoneVerificationCode) //nolint:staticcheck if err != nil { return nil, err } - events = append(events, user.NewHumanPhoneCodeAddedEvent(ctx, userAgg, phoneCode.Code, phoneCode.Expiry)) + events = append(events, user.NewHumanPhoneCodeAddedEvent(ctx, userAgg, phoneCode.CryptedCode(), phoneCode.CodeExpiry(), generatorID)) } pushedEvents, err := c.eventstore.Push(ctx, events...) @@ -74,12 +77,22 @@ func (c *Commands) VerifyHumanPhone(ctx context.Context, userID, code, resourceo if !existingCode.UserState.Exists() { return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-Rsj8c", "Errors.User.NotFound") } - if !existingCode.State.Exists() || existingCode.Code == nil { + if !existingCode.State.Exists() || (existingCode.Code == nil && existingCode.GeneratorID == "") { return nil, zerrors.ThrowNotFound(nil, "COMMAND-Rsj8c", "Errors.User.Code.NotFound") } userAgg := UserAggregateFromWriteModel(&existingCode.WriteModel) - err = crypto.VerifyCode(existingCode.CodeCreationDate, existingCode.CodeExpiry, existingCode.Code, code, phoneCodeGenerator.Alg()) + err = verifyCode( + ctx, + existingCode.CodeCreationDate, + existingCode.CodeExpiry, + existingCode.Code, + existingCode.GeneratorID, + existingCode.VerificationID, + code, + phoneCodeGenerator.Alg(), + c.phoneCodeVerifier, + ) if err == nil { pushedEvents, err := c.eventstore.Push(ctx, user.NewHumanPhoneVerifiedEvent(ctx, userAgg)) if err != nil { @@ -92,10 +105,47 @@ func (c *Commands) VerifyHumanPhone(ctx context.Context, userID, code, resourceo return writeModelToObjectDetails(&existingCode.WriteModel), nil } _, err = c.eventstore.Push(ctx, user.NewHumanPhoneVerificationFailedEvent(ctx, userAgg)) - logging.LogWithFields("COMMAND-5M9ds", "userID", userAgg.ID).OnError(err).Error("NewHumanPhoneVerificationFailedEvent push failed") + logging.WithFields("userID", userAgg.ID).OnError(err).Error("NewHumanPhoneVerificationFailedEvent push failed") return nil, zerrors.ThrowInvalidArgument(err, "COMMAND-sM0cs", "Errors.User.Code.Invalid") } +func (c *Commands) phoneCodeVerifierFromConfig(ctx context.Context, id string) (senders.CodeGenerator, error) { + config, err := c.getSMSConfig(ctx, authz.GetInstance(ctx).InstanceID(), id) + if err != nil { + return nil, err + } + if config.State != domain.SMSConfigStateActive { + return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-M0odsf", "Errors.SMSConfig.NotFound") + } + if config.Twilio != nil { + if config.Twilio.VerifyServiceSID == "" { + return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-Sgb4h", "Errors.SMSConfig.NotExternalVerification") + } + token, err := crypto.DecryptString(config.Twilio.Token, c.smsEncryption) + if err != nil { + return nil, err + } + return &twilio.Config{ + SID: config.Twilio.SID, + Token: token, + SenderNumber: config.Twilio.SenderNumber, + VerifyServiceSID: config.Twilio.VerifyServiceSID, + }, nil + } + return nil, nil +} + +func (c *Commands) activeSMSProvider(ctx context.Context) (string, error) { + config, err := c.getActiveSMSConfig(ctx, authz.GetInstance(ctx).InstanceID()) + if err != nil { + return "", err + } + if config.State == domain.SMSConfigStateActive && config.Twilio != nil && config.Twilio.VerifyServiceSID != "" { + return config.ID, nil + } + return "", err +} + func (c *Commands) CreateHumanPhoneVerificationCode(ctx context.Context, userID, resourceowner string) (*domain.ObjectDetails, error) { if userID == "" { return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-4M0ds", "Errors.User.UserIDMissing") @@ -115,23 +165,19 @@ func (c *Commands) CreateHumanPhoneVerificationCode(ctx context.Context, userID, if existingPhone.IsPhoneVerified { return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-2M9sf", "Errors.User.Phone.AlreadyVerified") } - config, err := cryptoGeneratorConfig(ctx, c.eventstore.Filter, domain.SecretGeneratorTypeVerifyPhoneCode) //nolint:staticcheck - if err != nil { - return nil, err - } - phoneCode, err := domain.NewPhoneCode(crypto.NewEncryptionGenerator(*config, c.userEncryption)) + phoneCode, generatorID, err := c.newPhoneCode(ctx, c.eventstore.Filter, domain.SecretGeneratorTypeVerifyPhoneCode, c.userEncryption, c.defaultSecretGenerators.PhoneVerificationCode) //nolint:staticcheck if err != nil { return nil, err } userAgg := UserAggregateFromWriteModel(&existingPhone.WriteModel) - if err = c.pushAppendAndReduce(ctx, existingPhone, user.NewHumanPhoneCodeAddedEvent(ctx, userAgg, phoneCode.Code, phoneCode.Expiry)); err != nil { + if err = c.pushAppendAndReduce(ctx, existingPhone, user.NewHumanPhoneCodeAddedEvent(ctx, userAgg, phoneCode.CryptedCode(), phoneCode.CodeExpiry(), generatorID)); err != nil { return nil, err } return writeModelToObjectDetails(&existingPhone.WriteModel), nil } -func (c *Commands) HumanPhoneVerificationCodeSent(ctx context.Context, orgID, userID string) (err error) { +func (c *Commands) HumanPhoneVerificationCodeSent(ctx context.Context, orgID, userID string, generatorInfo *senders.CodeGeneratorInfo) (err error) { if userID == "" { return zerrors.ThrowInvalidArgument(nil, "COMMAND-3m9Fs", "Errors.User.UserIDMissing") } @@ -148,7 +194,7 @@ func (c *Commands) HumanPhoneVerificationCodeSent(ctx context.Context, orgID, us } userAgg := UserAggregateFromWriteModel(&existingPhone.WriteModel) - _, err = c.eventstore.Push(ctx, user.NewHumanPhoneCodeSentEvent(ctx, userAgg)) + _, err = c.eventstore.Push(ctx, user.NewHumanPhoneCodeSentEvent(ctx, userAgg, generatorInfo)) return err } diff --git a/internal/command/user_human_phone_model.go b/internal/command/user_human_phone_model.go index ddac65f4b..41e8e8457 100644 --- a/internal/command/user_human_phone_model.go +++ b/internal/command/user_human_phone_model.go @@ -19,6 +19,8 @@ type HumanPhoneWriteModel struct { Code *crypto.CryptoValue CodeCreationDate time.Time CodeExpiry time.Duration + GeneratorID string + VerificationID string State domain.PhoneState UserState domain.UserState @@ -64,6 +66,10 @@ func (wm *HumanPhoneWriteModel) Reduce() error { wm.Code = e.Code wm.CodeCreationDate = e.CreationDate() wm.CodeExpiry = e.Expiry + wm.GeneratorID = e.GeneratorID + case *user.HumanPhoneCodeSentEvent: + wm.GeneratorID = e.GeneratorInfo.GetID() + wm.VerificationID = e.GeneratorInfo.GetVerificationID() case *user.HumanPhoneRemovedEvent: wm.State = domain.PhoneStateRemoved wm.IsPhoneVerified = false @@ -90,6 +96,7 @@ func (wm *HumanPhoneWriteModel) Query() *eventstore.SearchQueryBuilder { user.HumanPhoneChangedType, user.HumanPhoneVerifiedType, user.HumanPhoneCodeAddedType, + user.HumanPhoneCodeSentType, user.HumanPhoneRemovedType, user.UserRemovedType, user.UserV1AddedType, diff --git a/internal/command/user_human_phone_test.go b/internal/command/user_human_phone_test.go index 9ee3fcbdd..66835ef3f 100644 --- a/internal/command/user_human_phone_test.go +++ b/internal/command/user_human_phone_test.go @@ -13,14 +13,29 @@ import ( "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/eventstore/v1/models" + "github.com/zitadel/zitadel/internal/notification/senders" + "github.com/zitadel/zitadel/internal/notification/senders/mock" "github.com/zitadel/zitadel/internal/repository/instance" "github.com/zitadel/zitadel/internal/repository/user" "github.com/zitadel/zitadel/internal/zerrors" ) func TestCommandSide_ChangeHumanPhone(t *testing.T) { + defaultGenerators := &SecretGenerators{ + OTPSMS: &crypto.GeneratorConfig{ + Length: 8, + Expiry: time.Hour, + IncludeLowerLetters: true, + IncludeUpperLetters: true, + IncludeDigits: true, + IncludeSymbols: true, + }, + } type fields struct { - eventstore *eventstore.Eventstore + eventstore func(*testing.T) *eventstore.Eventstore + userEncryption crypto.EncryptionAlgorithm + defaultSecretGenerators *SecretGenerators + newEncryptedCodeWithDefault encryptedCodeWithDefaultFunc } type args struct { ctx context.Context @@ -41,9 +56,7 @@ func TestCommandSide_ChangeHumanPhone(t *testing.T) { { name: "invalid phone, invalid argument error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), }, args: args{ ctx: context.Background(), @@ -61,8 +74,7 @@ func TestCommandSide_ChangeHumanPhone(t *testing.T) { { name: "user not existing, precondition error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter(), ), }, @@ -83,8 +95,7 @@ func TestCommandSide_ChangeHumanPhone(t *testing.T) { { name: "phone not changed, precondition error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( user.NewHumanAddedEvent(context.Background(), @@ -126,8 +137,7 @@ func TestCommandSide_ChangeHumanPhone(t *testing.T) { { name: "verified phone changed, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( user.NewHumanAddedEvent(context.Background(), @@ -186,8 +196,7 @@ func TestCommandSide_ChangeHumanPhone(t *testing.T) { { name: "phone changed to verified, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( user.NewHumanAddedEvent(context.Background(), @@ -242,8 +251,7 @@ func TestCommandSide_ChangeHumanPhone(t *testing.T) { { name: "phone changed to verified, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( user.NewHumanAddedEvent(context.Background(), @@ -298,8 +306,7 @@ func TestCommandSide_ChangeHumanPhone(t *testing.T) { { name: "phone changed with code, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( user.NewHumanAddedEvent(context.Background(), @@ -316,6 +323,36 @@ func TestCommandSide_ChangeHumanPhone(t *testing.T) { ), ), ), + expectFilter( + eventFromEventPusher( + instance.NewSMSConfigActivatedEvent( + context.Background(), + &instance.NewAggregate("instanceID").Aggregate, + "id", + ), + ), + ), + expectFilter( + eventFromEventPusher( + instance.NewSMSConfigTwilioAddedEvent( + context.Background(), + &instance.NewAggregate("instanceID").Aggregate, + "id", + "", + "sid", + "senderNumber", + &crypto.CryptoValue{CryptoType: crypto.TypeEncryption, Algorithm: "enc", KeyID: "id", Crypted: []byte("crypted")}, + "", + ), + ), + eventFromEventPusher( + instance.NewSMSConfigActivatedEvent( + context.Background(), + &instance.NewAggregate("instanceID").Aggregate, + "id", + ), + ), + ), expectPush( user.NewHumanPhoneChangedEvent(context.Background(), &user.NewAggregate("user1", "org1").Aggregate, @@ -330,9 +367,100 @@ func TestCommandSide_ChangeHumanPhone(t *testing.T) { Crypted: []byte("a"), }, time.Hour*1, + "", ), ), ), + userEncryption: crypto.NewMockEncryptionAlgorithm(gomock.NewController(t)), + defaultSecretGenerators: defaultGenerators, + newEncryptedCodeWithDefault: mockEncryptedCodeWithDefault("a", time.Hour), + }, + args: args{ + ctx: context.Background(), + email: &domain.Phone{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "user1", + }, + PhoneNumber: "+41711234567", + }, + resourceOwner: "org1", + secretGenerator: GetMockSecretGenerator(t), + }, + res: res{ + want: &domain.Phone{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "user1", + ResourceOwner: "org1", + }, + PhoneNumber: "+41711234567", + }, + }, + }, + { + name: "phone changed with code (external), ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username", + "firstname", + "lastname", + "nickname", + "displayname", + language.German, + domain.GenderUnspecified, + "email@test.ch", + true, + ), + ), + ), + expectFilter( + eventFromEventPusher( + instance.NewSMSConfigActivatedEvent( + context.Background(), + &instance.NewAggregate("instanceID").Aggregate, + "id", + ), + ), + ), + expectFilter( + eventFromEventPusher( + instance.NewSMSConfigTwilioAddedEvent( + context.Background(), + &instance.NewAggregate("instanceID").Aggregate, + "id", + "", + "sid", + "senderNumber", + &crypto.CryptoValue{CryptoType: crypto.TypeEncryption, Algorithm: "enc", KeyID: "id", Crypted: []byte("crypted")}, + "verifyServiceSID", + ), + ), + eventFromEventPusher( + instance.NewSMSConfigActivatedEvent( + context.Background(), + &instance.NewAggregate("instanceID").Aggregate, + "id", + ), + ), + ), + expectPush( + user.NewHumanPhoneChangedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "+41711234567", + ), + user.NewHumanPhoneCodeAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + nil, + 0, + "id", + ), + ), + ), + userEncryption: crypto.NewMockEncryptionAlgorithm(gomock.NewController(t)), + defaultSecretGenerators: defaultGenerators, }, args: args{ ctx: context.Background(), @@ -359,7 +487,10 @@ func TestCommandSide_ChangeHumanPhone(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := &Commands{ - eventstore: tt.fields.eventstore, + eventstore: tt.fields.eventstore(t), + userEncryption: tt.fields.userEncryption, + defaultSecretGenerators: tt.fields.defaultSecretGenerators, + newEncryptedCodeWithDefault: tt.fields.newEncryptedCodeWithDefault, } got, err := r.ChangeHumanPhone(tt.args.ctx, tt.args.email, tt.args.resourceOwner, tt.args.secretGenerator) if tt.res.err == nil { @@ -377,7 +508,8 @@ func TestCommandSide_ChangeHumanPhone(t *testing.T) { func TestCommandSide_VerifyHumanPhone(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore + eventstore func(*testing.T) *eventstore.Eventstore + phoneCodeVerifier func(ctx context.Context, id string) (senders.CodeGenerator, error) } type args struct { ctx context.Context @@ -399,9 +531,7 @@ func TestCommandSide_VerifyHumanPhone(t *testing.T) { { name: "userid missing, invalid argument error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), }, args: args{ ctx: context.Background(), @@ -415,9 +545,7 @@ func TestCommandSide_VerifyHumanPhone(t *testing.T) { { name: "code missing, invalid argument error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), }, args: args{ ctx: context.Background(), @@ -431,8 +559,7 @@ func TestCommandSide_VerifyHumanPhone(t *testing.T) { { name: "user not existing, precondition error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter(), ), }, @@ -449,8 +576,7 @@ func TestCommandSide_VerifyHumanPhone(t *testing.T) { { name: "code not existing, not found error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( user.NewHumanAddedEvent(context.Background(), @@ -482,8 +608,7 @@ func TestCommandSide_VerifyHumanPhone(t *testing.T) { { name: "invalid code, invalid argument error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( user.NewHumanAddedEvent(context.Background(), @@ -515,6 +640,7 @@ func TestCommandSide_VerifyHumanPhone(t *testing.T) { Crypted: []byte("a"), }, time.Hour*1, + "", ), ), ), @@ -539,8 +665,7 @@ func TestCommandSide_VerifyHumanPhone(t *testing.T) { { name: "valid code, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( user.NewHumanAddedEvent(context.Background(), @@ -572,6 +697,7 @@ func TestCommandSide_VerifyHumanPhone(t *testing.T) { Crypted: []byte("a"), }, time.Hour*1, + "", ), ), ), @@ -595,11 +721,80 @@ func TestCommandSide_VerifyHumanPhone(t *testing.T) { }, }, }, + { + name: "valid code (external), ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username", + "firstname", + "lastname", + "nickname", + "displayname", + language.German, + domain.GenderUnspecified, + "email@test.ch", + true, + ), + ), + eventFromEventPusher( + user.NewHumanPhoneChangedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "+411234567", + ), + ), + eventFromEventPusherWithCreationDateNow( + user.NewHumanPhoneCodeAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + nil, + 0, + "id", + ), + ), + eventFromEventPusherWithCreationDateNow( + user.NewHumanPhoneCodeSentEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + &senders.CodeGeneratorInfo{ + ID: "id", + VerificationID: "verificationID", + }, + ), + ), + ), + expectPush( + user.NewHumanPhoneVerifiedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + ), + ), + ), + phoneCodeVerifier: func(ctx context.Context, id string) (senders.CodeGenerator, error) { + sender := mock.NewMockCodeGenerator(gomock.NewController(t)) + sender.EXPECT().VerifyCode("verificationID", "a") + return sender, nil + }, + }, + args: args{ + ctx: context.Background(), + userID: "user1", + code: "a", + resourceOwner: "org1", + secretGenerator: GetMockSecretGenerator(t), + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := &Commands{ - eventstore: tt.fields.eventstore, + eventstore: tt.fields.eventstore(t), + phoneCodeVerifier: tt.fields.phoneCodeVerifier, } got, err := r.VerifyHumanPhone(tt.args.ctx, tt.args.userID, tt.args.code, tt.args.resourceOwner, tt.args.secretGenerator) if tt.res.err == nil { @@ -616,9 +811,21 @@ func TestCommandSide_VerifyHumanPhone(t *testing.T) { } func TestCommandSide_CreateVerificationCodeHumanPhone(t *testing.T) { + defaultGenerators := &SecretGenerators{ + OTPSMS: &crypto.GeneratorConfig{ + Length: 8, + Expiry: time.Hour, + IncludeLowerLetters: true, + IncludeUpperLetters: true, + IncludeDigits: true, + IncludeSymbols: true, + }, + } type fields struct { - eventstore *eventstore.Eventstore - userEncryption crypto.EncryptionAlgorithm + eventstore func(*testing.T) *eventstore.Eventstore + userEncryption crypto.EncryptionAlgorithm + defaultSecretGenerators *SecretGenerators + newEncryptedCodeWithDefault encryptedCodeWithDefaultFunc } type args struct { ctx context.Context @@ -638,9 +845,7 @@ func TestCommandSide_CreateVerificationCodeHumanPhone(t *testing.T) { { name: "userid missing, invalid argument error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), }, args: args{ ctx: context.Background(), @@ -653,8 +858,7 @@ func TestCommandSide_CreateVerificationCodeHumanPhone(t *testing.T) { { name: "user not existing, precondition error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter(), ), }, @@ -670,8 +874,7 @@ func TestCommandSide_CreateVerificationCodeHumanPhone(t *testing.T) { { name: "phone already verified, precondition error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( user.NewHumanAddedEvent(context.Background(), @@ -713,8 +916,7 @@ func TestCommandSide_CreateVerificationCodeHumanPhone(t *testing.T) { { name: "new code, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( user.NewHumanAddedEvent(context.Background(), @@ -739,16 +941,33 @@ func TestCommandSide_CreateVerificationCodeHumanPhone(t *testing.T) { ), expectFilter( eventFromEventPusher( - instance.NewSecretGeneratorAddedEvent(context.Background(), + instance.NewSMSConfigActivatedEvent( + context.Background(), &instance.NewAggregate("instanceID").Aggregate, - domain.SecretGeneratorTypeVerifyPhoneCode, - 8, - time.Hour, - true, - true, - true, - true, - )), + "id", + ), + ), + ), + expectFilter( + eventFromEventPusher( + instance.NewSMSConfigTwilioAddedEvent( + context.Background(), + &instance.NewAggregate("instanceID").Aggregate, + "id", + "", + "sid", + "senderNumber", + &crypto.CryptoValue{CryptoType: crypto.TypeEncryption, Algorithm: "enc", KeyID: "id", Crypted: []byte("crypted")}, + "", + ), + ), + eventFromEventPusher( + instance.NewSMSConfigActivatedEvent( + context.Background(), + &instance.NewAggregate("instanceID").Aggregate, + "id", + ), + ), ), expectPush( user.NewHumanPhoneCodeAddedEvent(context.Background(), @@ -760,10 +979,92 @@ func TestCommandSide_CreateVerificationCodeHumanPhone(t *testing.T) { Crypted: []byte("12345678"), }, time.Hour*1, + "", ), ), ), - userEncryption: crypto.CreateMockEncryptionAlgWithCode(gomock.NewController(t), "12345678"), + userEncryption: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), + defaultSecretGenerators: defaultGenerators, + newEncryptedCodeWithDefault: mockEncryptedCodeWithDefault("12345678", time.Hour), + }, + args: args{ + ctx: context.Background(), + userID: "user1", + resourceOwner: "org1", + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + { + name: "new code (external), ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username", + "firstname", + "lastname", + "nickname", + "displayname", + language.German, + domain.GenderUnspecified, + "email@test.ch", + true, + ), + ), + eventFromEventPusher( + user.NewHumanPhoneChangedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "+411234567", + ), + ), + ), + expectFilter( + eventFromEventPusher( + instance.NewSMSConfigActivatedEvent( + context.Background(), + &instance.NewAggregate("instanceID").Aggregate, + "id", + ), + ), + ), + expectFilter( + eventFromEventPusher( + instance.NewSMSConfigTwilioAddedEvent( + context.Background(), + &instance.NewAggregate("instanceID").Aggregate, + "id", + "", + "sid", + "senderNumber", + &crypto.CryptoValue{CryptoType: crypto.TypeEncryption, Algorithm: "enc", KeyID: "id", Crypted: []byte("crypted")}, + "verifyServiceSID", + ), + ), + eventFromEventPusher( + instance.NewSMSConfigActivatedEvent( + context.Background(), + &instance.NewAggregate("instanceID").Aggregate, + "id", + ), + ), + ), + expectPush( + user.NewHumanPhoneCodeAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + nil, + 0, + "id", + ), + ), + ), + userEncryption: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), + defaultSecretGenerators: defaultGenerators, }, args: args{ ctx: context.Background(), @@ -780,8 +1081,10 @@ func TestCommandSide_CreateVerificationCodeHumanPhone(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := &Commands{ - eventstore: tt.fields.eventstore, - userEncryption: tt.fields.userEncryption, + eventstore: tt.fields.eventstore(t), + userEncryption: tt.fields.userEncryption, + defaultSecretGenerators: tt.fields.defaultSecretGenerators, + newEncryptedCodeWithDefault: tt.fields.newEncryptedCodeWithDefault, } got, err := r.CreateHumanPhoneVerificationCode(tt.args.ctx, tt.args.userID, tt.args.resourceOwner) if tt.res.err == nil { @@ -799,12 +1102,13 @@ func TestCommandSide_CreateVerificationCodeHumanPhone(t *testing.T) { func TestCommandSide_PhoneVerificationCodeSent(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore + eventstore func(*testing.T) *eventstore.Eventstore } type args struct { ctx context.Context userID string resourceOwner string + generatorInfo *senders.CodeGeneratorInfo } type res struct { err func(error) bool @@ -818,9 +1122,7 @@ func TestCommandSide_PhoneVerificationCodeSent(t *testing.T) { { name: "userid missing, invalid argument error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), }, args: args{ ctx: context.Background(), @@ -833,8 +1135,7 @@ func TestCommandSide_PhoneVerificationCodeSent(t *testing.T) { { name: "user not existing, precondition error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter(), ), }, @@ -850,8 +1151,7 @@ func TestCommandSide_PhoneVerificationCodeSent(t *testing.T) { { name: "code sent, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( user.NewHumanAddedEvent(context.Background(), @@ -877,6 +1177,7 @@ func TestCommandSide_PhoneVerificationCodeSent(t *testing.T) { expectPush( user.NewHumanPhoneCodeSentEvent(context.Background(), &user.NewAggregate("user1", "org1").Aggregate, + &senders.CodeGeneratorInfo{}, ), ), ), @@ -885,6 +1186,55 @@ func TestCommandSide_PhoneVerificationCodeSent(t *testing.T) { ctx: context.Background(), userID: "user1", resourceOwner: "org1", + generatorInfo: &senders.CodeGeneratorInfo{}, + }, + res: res{}, + }, + { + name: "code sent (external), ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username", + "firstname", + "lastname", + "nickname", + "displayname", + language.German, + domain.GenderUnspecified, + "email@test.ch", + true, + ), + ), + eventFromEventPusher( + user.NewHumanPhoneChangedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "+411234567", + ), + ), + ), + expectPush( + user.NewHumanPhoneCodeSentEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + &senders.CodeGeneratorInfo{ + ID: "generatorID", + VerificationID: "verificationID", + }, + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + userID: "user1", + resourceOwner: "org1", + generatorInfo: &senders.CodeGeneratorInfo{ + ID: "generatorID", + VerificationID: "verificationID", + }, }, res: res{}, }, @@ -892,9 +1242,9 @@ func TestCommandSide_PhoneVerificationCodeSent(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := &Commands{ - eventstore: tt.fields.eventstore, + eventstore: tt.fields.eventstore(t), } - err := r.HumanPhoneVerificationCodeSent(tt.args.ctx, tt.args.resourceOwner, tt.args.userID) + err := r.HumanPhoneVerificationCodeSent(tt.args.ctx, tt.args.resourceOwner, tt.args.userID, tt.args.generatorInfo) if tt.res.err == nil { assert.NoError(t, err) } @@ -907,7 +1257,7 @@ func TestCommandSide_PhoneVerificationCodeSent(t *testing.T) { func TestCommandSide_RemoveHumanPhone(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore + eventstore func(*testing.T) *eventstore.Eventstore } type args struct { ctx context.Context @@ -927,9 +1277,7 @@ func TestCommandSide_RemoveHumanPhone(t *testing.T) { { name: "userid missing, invalid argument error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), }, args: args{ ctx: context.Background(), @@ -942,8 +1290,7 @@ func TestCommandSide_RemoveHumanPhone(t *testing.T) { { name: "user not existing, precondition error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter(), ), }, @@ -959,8 +1306,7 @@ func TestCommandSide_RemoveHumanPhone(t *testing.T) { { name: "phone not existing, precondition error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( user.NewHumanAddedEvent(context.Background(), @@ -991,8 +1337,7 @@ func TestCommandSide_RemoveHumanPhone(t *testing.T) { { name: "remove phone, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( user.NewHumanAddedEvent(context.Background(), @@ -1037,7 +1382,7 @@ func TestCommandSide_RemoveHumanPhone(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := &Commands{ - eventstore: tt.fields.eventstore, + eventstore: tt.fields.eventstore(t), } got, err := r.RemoveHumanPhone(tt.args.ctx, tt.args.userID, tt.args.resourceOwner) if tt.res.err == nil { diff --git a/internal/command/user_human_test.go b/internal/command/user_human_test.go index 468a27e8b..fbf3523fc 100644 --- a/internal/command/user_human_test.go +++ b/internal/command/user_human_test.go @@ -19,18 +19,31 @@ import ( "github.com/zitadel/zitadel/internal/id" id_mock "github.com/zitadel/zitadel/internal/id/mock" "github.com/zitadel/zitadel/internal/repository/idp" + "github.com/zitadel/zitadel/internal/repository/instance" "github.com/zitadel/zitadel/internal/repository/org" "github.com/zitadel/zitadel/internal/repository/user" "github.com/zitadel/zitadel/internal/zerrors" ) func TestCommandSide_AddHuman(t *testing.T) { + defaultGenerators := &SecretGenerators{ + OTPSMS: &crypto.GeneratorConfig{ + Length: 8, + Expiry: time.Hour, + IncludeLowerLetters: true, + IncludeUpperLetters: true, + IncludeDigits: true, + IncludeSymbols: true, + }, + } type fields struct { - eventstore func(t *testing.T) *eventstore.Eventstore - idGenerator id.Generator - userPasswordHasher *crypto.Hasher - codeAlg crypto.EncryptionAlgorithm - newCode encrypedCodeFunc + eventstore func(t *testing.T) *eventstore.Eventstore + idGenerator id.Generator + userPasswordHasher *crypto.Hasher + codeAlg crypto.EncryptionAlgorithm + newCode encrypedCodeFunc + newEncryptedCodeWithDefault encryptedCodeWithDefaultFunc + defaultSecretGenerators *SecretGenerators } type args struct { ctx context.Context @@ -660,7 +673,6 @@ func TestCommandSide_AddHuman(t *testing.T) { idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), userPasswordHasher: mockPasswordHasher("x"), codeAlg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), - newCode: mockEncryptedCode("emailCode", time.Hour), }, args: args{ ctx: context.Background(), @@ -1042,6 +1054,36 @@ func TestCommandSide_AddHuman(t *testing.T) { ), ), ), + expectFilter( + eventFromEventPusher( + instance.NewSMSConfigActivatedEvent( + context.Background(), + &instance.NewAggregate("instanceID").Aggregate, + "id", + ), + ), + ), + expectFilter( + eventFromEventPusher( + instance.NewSMSConfigTwilioAddedEvent( + context.Background(), + &instance.NewAggregate("instanceID").Aggregate, + "id", + "", + "sid", + "senderNumber", + &crypto.CryptoValue{CryptoType: crypto.TypeEncryption, Algorithm: "enc", KeyID: "id", Crypted: []byte("crypted")}, + "", + ), + ), + eventFromEventPusher( + instance.NewSMSConfigActivatedEvent( + context.Background(), + &instance.NewAggregate("instanceID").Aggregate, + "id", + ), + ), + ), expectPush( newAddHumanEvent("$plain$x$password", false, true, "+41711234567", AllowedLanguage), user.NewHumanEmailVerifiedEvent( @@ -1057,13 +1099,15 @@ func TestCommandSide_AddHuman(t *testing.T) { Crypted: []byte("phonecode"), }, time.Hour*1, + "", ), ), ), - idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), - userPasswordHasher: mockPasswordHasher("x"), - codeAlg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), - newCode: mockEncryptedCode("phonecode", time.Hour), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), + userPasswordHasher: mockPasswordHasher("x"), + codeAlg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), + newEncryptedCodeWithDefault: mockEncryptedCodeWithDefault("phonecode", time.Hour), + defaultSecretGenerators: defaultGenerators, }, args: args{ ctx: context.Background(), @@ -1183,6 +1227,36 @@ func TestCommandSide_AddHuman(t *testing.T) { ), ), ), + expectFilter( + eventFromEventPusher( + instance.NewSMSConfigActivatedEvent( + context.Background(), + &instance.NewAggregate("instanceID").Aggregate, + "id", + ), + ), + ), + expectFilter( + eventFromEventPusher( + instance.NewSMSConfigTwilioAddedEvent( + context.Background(), + &instance.NewAggregate("instanceID").Aggregate, + "id", + "", + "sid", + "senderNumber", + &crypto.CryptoValue{CryptoType: crypto.TypeEncryption, Algorithm: "enc", KeyID: "id", Crypted: []byte("crypted")}, + "", + ), + ), + eventFromEventPusher( + instance.NewSMSConfigActivatedEvent( + context.Background(), + &instance.NewAggregate("instanceID").Aggregate, + "id", + ), + ), + ), expectPush( newAddHumanEvent("$plain$x$password", false, true, "+41711234567", AllowedLanguage), user.NewHumanEmailVerifiedEvent(context.Background(), @@ -1198,13 +1272,15 @@ func TestCommandSide_AddHuman(t *testing.T) { }, 1*time.Hour, true, + "", ), ), ), - idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), - userPasswordHasher: mockPasswordHasher("x"), - codeAlg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), - newCode: mockEncryptedCode("phoneCode", time.Hour), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), + userPasswordHasher: mockPasswordHasher("x"), + codeAlg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), + newEncryptedCodeWithDefault: mockEncryptedCodeWithDefault("phoneCode", time.Hour), + defaultSecretGenerators: defaultGenerators, }, args: args{ ctx: context.Background(), @@ -1307,11 +1383,13 @@ func TestCommandSide_AddHuman(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := &Commands{ - eventstore: tt.fields.eventstore(t), - userPasswordHasher: tt.fields.userPasswordHasher, - userEncryption: tt.fields.codeAlg, - idGenerator: tt.fields.idGenerator, - newEncryptedCode: tt.fields.newCode, + eventstore: tt.fields.eventstore(t), + userPasswordHasher: tt.fields.userPasswordHasher, + userEncryption: tt.fields.codeAlg, + idGenerator: tt.fields.idGenerator, + newEncryptedCode: tt.fields.newCode, + newEncryptedCodeWithDefault: tt.fields.newEncryptedCodeWithDefault, + defaultSecretGenerators: tt.fields.defaultSecretGenerators, } err := r.AddHuman(tt.args.ctx, tt.args.orgID, tt.args.human, tt.args.allowInitMail) if tt.res.err == nil { @@ -1332,10 +1410,22 @@ func TestCommandSide_AddHuman(t *testing.T) { } func TestCommandSide_ImportHuman(t *testing.T) { + defaultGenerators := &SecretGenerators{ + OTPSMS: &crypto.GeneratorConfig{ + Length: 8, + Expiry: time.Hour, + IncludeLowerLetters: true, + IncludeUpperLetters: true, + IncludeDigits: true, + IncludeSymbols: true, + }, + } type fields struct { - eventstore func(*testing.T) *eventstore.Eventstore - idGenerator id.Generator - userPasswordHasher *crypto.Hasher + eventstore func(*testing.T) *eventstore.Eventstore + idGenerator id.Generator + userPasswordHasher *crypto.Hasher + newEncryptedCodeWithDefault encryptedCodeWithDefaultFunc + defaultSecretGenerators *SecretGenerators } type args struct { ctx context.Context @@ -1889,6 +1979,36 @@ func TestCommandSide_ImportHuman(t *testing.T) { ), ), ), + expectFilter( + eventFromEventPusher( + instance.NewSMSConfigActivatedEvent( + context.Background(), + &instance.NewAggregate("instanceID").Aggregate, + "id", + ), + ), + ), + expectFilter( + eventFromEventPusher( + instance.NewSMSConfigTwilioAddedEvent( + context.Background(), + &instance.NewAggregate("instanceID").Aggregate, + "id", + "", + "sid", + "senderNumber", + &crypto.CryptoValue{CryptoType: crypto.TypeEncryption, Algorithm: "enc", KeyID: "id", Crypted: []byte("crypted")}, + "", + ), + ), + eventFromEventPusher( + instance.NewSMSConfigActivatedEvent( + context.Background(), + &instance.NewAggregate("instanceID").Aggregate, + "id", + ), + ), + ), expectPush( newAddHumanEvent("$plain$x$password", false, true, "+41711234567", AllowedLanguage), user.NewHumanInitialCodeAddedEvent(context.Background(), @@ -1910,11 +2030,15 @@ func TestCommandSide_ImportHuman(t *testing.T) { KeyID: "id", Crypted: []byte("a"), }, - time.Hour*1), + time.Hour*1, + "", + ), ), ), - idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), - userPasswordHasher: mockPasswordHasher("x"), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), + userPasswordHasher: mockPasswordHasher("x"), + newEncryptedCodeWithDefault: mockEncryptedCodeWithDefault("a", time.Hour), + defaultSecretGenerators: defaultGenerators, }, args{ ctx: context.Background(), @@ -2866,9 +2990,11 @@ func TestCommandSide_ImportHuman(t *testing.T) { t.Run(tt.name, func(t *testing.T) { f, a := tt.given(t) r := &Commands{ - eventstore: f.eventstore(t), - idGenerator: f.idGenerator, - userPasswordHasher: f.userPasswordHasher, + eventstore: f.eventstore(t), + idGenerator: f.idGenerator, + userPasswordHasher: f.userPasswordHasher, + newEncryptedCodeWithDefault: f.newEncryptedCodeWithDefault, + defaultSecretGenerators: f.defaultSecretGenerators, } gotHuman, gotCode, err := r.ImportHuman(a.ctx, a.orgID, a.human, a.passwordless, a.links, a.secretGenerator, a.secretGenerator, a.secretGenerator, a.secretGenerator) if tt.res.err == nil { diff --git a/internal/command/user_v2_human.go b/internal/command/user_v2_human.go index 08b8da72b..a85f905e0 100644 --- a/internal/command/user_v2_human.go +++ b/internal/command/user_v2_human.go @@ -352,11 +352,11 @@ func (c *Commands) changeUserPhone(ctx context.Context, cmds []eventstore.Comman if phone.Verified { return append(cmds, user.NewHumanPhoneVerifiedEvent(ctx, &wm.Aggregate().Aggregate)), code, nil } else { - cryptoCode, err := c.newPhoneCode(ctx, c.eventstore.Filter, alg) //nolint:staticcheck + cryptoCode, generatorID, err := c.newPhoneCode(ctx, c.eventstore.Filter, domain.SecretGeneratorTypeVerifyPhoneCode, alg, c.defaultSecretGenerators.PhoneVerificationCode) //nolint:staticcheck if err != nil { return cmds, code, err } - cmds = append(cmds, user.NewHumanPhoneCodeAddedEventV2(ctx, &wm.Aggregate().Aggregate, cryptoCode.Crypted, cryptoCode.Expiry, phone.ReturnCode)) + cmds = append(cmds, user.NewHumanPhoneCodeAddedEventV2(ctx, &wm.Aggregate().Aggregate, cryptoCode.CryptedCode(), cryptoCode.CodeExpiry(), phone.ReturnCode, generatorID)) if phone.ReturnCode { code = &cryptoCode.Plain } @@ -389,7 +389,14 @@ func (c *Commands) changeUserPassword(ctx context.Context, cmds []eventstore.Com verification := c.setPasswordWithPermission(wm.AggregateID, wm.ResourceOwner) // otherwise check the password code... if password.PasswordCode != "" { - verification = c.setPasswordWithVerifyCode(wm.PasswordCodeCreationDate, wm.PasswordCodeExpiry, wm.PasswordCode, password.PasswordCode) + verification = c.setPasswordWithVerifyCode( + wm.PasswordCodeCreationDate, + wm.PasswordCodeExpiry, + wm.PasswordCode, + wm.PasswordCodeGeneratorID, + wm.PasswordCodeVerificationID, + password.PasswordCode, + ) } // ...or old password if password.OldPassword != "" { diff --git a/internal/command/user_v2_human_test.go b/internal/command/user_v2_human_test.go index 37cd2837e..24899a930 100644 --- a/internal/command/user_v2_human_test.go +++ b/internal/command/user_v2_human_test.go @@ -18,18 +18,31 @@ import ( "github.com/zitadel/zitadel/internal/id" id_mock "github.com/zitadel/zitadel/internal/id/mock" "github.com/zitadel/zitadel/internal/repository/idp" + "github.com/zitadel/zitadel/internal/repository/instance" "github.com/zitadel/zitadel/internal/repository/org" "github.com/zitadel/zitadel/internal/repository/user" "github.com/zitadel/zitadel/internal/zerrors" ) func TestCommandSide_AddUserHuman(t *testing.T) { + defaultGenerators := &SecretGenerators{ + OTPSMS: &crypto.GeneratorConfig{ + Length: 8, + Expiry: time.Hour, + IncludeLowerLetters: true, + IncludeUpperLetters: true, + IncludeDigits: true, + IncludeSymbols: true, + }, + } type fields struct { - eventstore func(t *testing.T) *eventstore.Eventstore - idGenerator id.Generator - userPasswordHasher *crypto.Hasher - newCode encrypedCodeFunc - checkPermission domain.PermissionCheck + eventstore func(t *testing.T) *eventstore.Eventstore + idGenerator id.Generator + userPasswordHasher *crypto.Hasher + newCode encrypedCodeFunc + newEncryptedCodeWithDefault encryptedCodeWithDefaultFunc + checkPermission domain.PermissionCheck + defaultSecretGenerators *SecretGenerators } type args struct { ctx context.Context @@ -1027,6 +1040,36 @@ func TestCommandSide_AddUserHuman(t *testing.T) { ), ), ), + expectFilter( + eventFromEventPusher( + instance.NewSMSConfigActivatedEvent( + context.Background(), + &instance.NewAggregate("instanceID").Aggregate, + "id", + ), + ), + ), + expectFilter( + eventFromEventPusher( + instance.NewSMSConfigTwilioAddedEvent( + context.Background(), + &instance.NewAggregate("instanceID").Aggregate, + "id", + "", + "sid", + "senderNumber", + &crypto.CryptoValue{CryptoType: crypto.TypeEncryption, Algorithm: "enc", KeyID: "id", Crypted: []byte("crypted")}, + "", + ), + ), + eventFromEventPusher( + instance.NewSMSConfigActivatedEvent( + context.Background(), + &instance.NewAggregate("instanceID").Aggregate, + "id", + ), + ), + ), expectPush( newAddHumanEvent("$plain$x$password", false, true, "+41711234567", language.English), user.NewHumanEmailVerifiedEvent( @@ -1042,13 +1085,120 @@ func TestCommandSide_AddUserHuman(t *testing.T) { Crypted: []byte("phonecode"), }, time.Hour*1, + "", ), ), ), - checkPermission: newMockPermissionCheckAllowed(), - idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), - userPasswordHasher: mockPasswordHasher("x"), - newCode: mockEncryptedCode("phonecode", time.Hour), + checkPermission: newMockPermissionCheckAllowed(), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), + userPasswordHasher: mockPasswordHasher("x"), + newEncryptedCodeWithDefault: mockEncryptedCodeWithDefault("phonecode", time.Hour), + defaultSecretGenerators: defaultGenerators, + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + human: &AddHuman{ + Username: "username", + FirstName: "firstname", + LastName: "lastname", + Password: "password", + Email: Email{ + Address: "email@test.ch", + Verified: true, + }, + Phone: Phone{ + Number: "+41711234567", + }, + PreferredLanguage: language.English, + }, + secretGenerator: GetMockSecretGenerator(t), + allowInitMail: true, + codeAlg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + wantID: "user1", + }, + }, + { + name: "add human (with phone), ok (external)", + fields: fields{ + eventstore: expectEventstore( + expectFilter(), + expectFilter( + eventFromEventPusher( + org.NewDomainPolicyAddedEvent(context.Background(), + &userAgg.Aggregate, + true, + true, + true, + ), + ), + ), + expectFilter( + eventFromEventPusher( + org.NewPasswordComplexityPolicyAddedEvent(context.Background(), + &userAgg.Aggregate, + 1, + false, + false, + false, + false, + ), + ), + ), + expectFilter( + eventFromEventPusher( + instance.NewSMSConfigActivatedEvent( + context.Background(), + &instance.NewAggregate("instanceID").Aggregate, + "id", + ), + ), + ), + expectFilter( + eventFromEventPusher( + instance.NewSMSConfigTwilioAddedEvent( + context.Background(), + &instance.NewAggregate("instanceID").Aggregate, + "id", + "", + "sid", + "senderNumber", + &crypto.CryptoValue{CryptoType: crypto.TypeEncryption, Algorithm: "enc", KeyID: "id", Crypted: []byte("crypted")}, + "verifiyServiceSid", + ), + ), + eventFromEventPusher( + instance.NewSMSConfigActivatedEvent( + context.Background(), + &instance.NewAggregate("instanceID").Aggregate, + "id", + ), + ), + ), + expectPush( + newAddHumanEvent("$plain$x$password", false, true, "+41711234567", language.English), + user.NewHumanEmailVerifiedEvent( + context.Background(), + &userAgg.Aggregate, + ), + user.NewHumanPhoneCodeAddedEvent(context.Background(), + &userAgg.Aggregate, + nil, + 0, + "id", + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), + userPasswordHasher: mockPasswordHasher("x"), + newEncryptedCodeWithDefault: mockEncryptedCodeWithDefault("phonecode", time.Hour), + defaultSecretGenerators: defaultGenerators, }, args: args{ ctx: context.Background(), @@ -1171,6 +1321,36 @@ func TestCommandSide_AddUserHuman(t *testing.T) { ), ), ), + expectFilter( + eventFromEventPusher( + instance.NewSMSConfigActivatedEvent( + context.Background(), + &instance.NewAggregate("instanceID").Aggregate, + "id", + ), + ), + ), + expectFilter( + eventFromEventPusher( + instance.NewSMSConfigTwilioAddedEvent( + context.Background(), + &instance.NewAggregate("instanceID").Aggregate, + "id", + "", + "sid", + "senderNumber", + &crypto.CryptoValue{CryptoType: crypto.TypeEncryption, Algorithm: "enc", KeyID: "id", Crypted: []byte("crypted")}, + "", + ), + ), + eventFromEventPusher( + instance.NewSMSConfigActivatedEvent( + context.Background(), + &instance.NewAggregate("instanceID").Aggregate, + "id", + ), + ), + ), expectPush( newAddHumanEvent("$plain$x$password", false, true, "+41711234567", language.English), user.NewHumanEmailVerifiedEvent(context.Background(), @@ -1186,13 +1366,15 @@ func TestCommandSide_AddUserHuman(t *testing.T) { }, 1*time.Hour, true, + "", ), ), ), - checkPermission: newMockPermissionCheckAllowed(), - idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), - userPasswordHasher: mockPasswordHasher("x"), - newCode: mockEncryptedCode("phoneCode", time.Hour), + checkPermission: newMockPermissionCheckAllowed(), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), + userPasswordHasher: mockPasswordHasher("x"), + newEncryptedCodeWithDefault: mockEncryptedCodeWithDefault("phoneCode", time.Hour), + defaultSecretGenerators: defaultGenerators, }, args: args{ ctx: context.Background(), @@ -1547,11 +1729,13 @@ func TestCommandSide_AddUserHuman(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := &Commands{ - eventstore: tt.fields.eventstore(t), - userPasswordHasher: tt.fields.userPasswordHasher, - idGenerator: tt.fields.idGenerator, - newEncryptedCode: tt.fields.newCode, - checkPermission: tt.fields.checkPermission, + eventstore: tt.fields.eventstore(t), + userPasswordHasher: tt.fields.userPasswordHasher, + idGenerator: tt.fields.idGenerator, + newEncryptedCode: tt.fields.newCode, + newEncryptedCodeWithDefault: tt.fields.newEncryptedCodeWithDefault, + defaultSecretGenerators: tt.fields.defaultSecretGenerators, + checkPermission: tt.fields.checkPermission, multifactors: domain.MultifactorConfigs{ OTP: domain.OTPConfig{ Issuer: "zitadel.com", @@ -1578,11 +1762,23 @@ func TestCommandSide_AddUserHuman(t *testing.T) { } func TestCommandSide_ChangeUserHuman(t *testing.T) { + defaultGenerators := &SecretGenerators{ + OTPSMS: &crypto.GeneratorConfig{ + Length: 8, + Expiry: time.Hour, + IncludeLowerLetters: true, + IncludeUpperLetters: true, + IncludeDigits: true, + IncludeSymbols: true, + }, + } type fields struct { - eventstore func(t *testing.T) *eventstore.Eventstore - userPasswordHasher *crypto.Hasher - newCode encrypedCodeFunc - checkPermission domain.PermissionCheck + eventstore func(t *testing.T) *eventstore.Eventstore + userPasswordHasher *crypto.Hasher + newCode encrypedCodeFunc + newEncryptedCodeWithDefault encryptedCodeWithDefaultFunc + checkPermission domain.PermissionCheck + defaultSecretGenerators *SecretGenerators } type args struct { ctx context.Context @@ -2107,6 +2303,36 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) { newAddHumanEvent("$plain$x$password", true, true, "", language.English), ), ), + expectFilter( + eventFromEventPusher( + instance.NewSMSConfigActivatedEvent( + context.Background(), + &instance.NewAggregate("instanceID").Aggregate, + "id", + ), + ), + ), + expectFilter( + eventFromEventPusher( + instance.NewSMSConfigTwilioAddedEvent( + context.Background(), + &instance.NewAggregate("instanceID").Aggregate, + "id", + "", + "sid", + "senderNumber", + &crypto.CryptoValue{CryptoType: crypto.TypeEncryption, Algorithm: "enc", KeyID: "id", Crypted: []byte("crypted")}, + "", + ), + ), + eventFromEventPusher( + instance.NewSMSConfigActivatedEvent( + context.Background(), + &instance.NewAggregate("instanceID").Aggregate, + "id", + ), + ), + ), expectPush( user.NewHumanPhoneChangedEvent(context.Background(), &userAgg.Aggregate, @@ -2122,11 +2348,13 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) { }, time.Hour, false, + "", ), ), ), - checkPermission: newMockPermissionCheckAllowed(), - newCode: mockEncryptedCode("phoneCode", time.Hour), + checkPermission: newMockPermissionCheckAllowed(), + newEncryptedCodeWithDefault: mockEncryptedCodeWithDefault("phoneCode", time.Hour), + defaultSecretGenerators: defaultGenerators, }, args: args{ ctx: context.Background(), @@ -2145,7 +2373,83 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) { ResourceOwner: "org1", }, }, - }, { + }, + { + name: "change human phone, ok (external)", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + newAddHumanEvent("$plain$x$password", true, true, "", language.English), + ), + ), + expectFilter( + eventFromEventPusher( + instance.NewSMSConfigActivatedEvent( + context.Background(), + &instance.NewAggregate("instanceID").Aggregate, + "id", + ), + ), + ), + expectFilter( + eventFromEventPusher( + instance.NewSMSConfigTwilioAddedEvent( + context.Background(), + &instance.NewAggregate("instanceID").Aggregate, + "id", + "", + "sid", + "senderNumber", + &crypto.CryptoValue{CryptoType: crypto.TypeEncryption, Algorithm: "enc", KeyID: "id", Crypted: []byte("crypted")}, + "verifyServiceSid", + ), + ), + eventFromEventPusher( + instance.NewSMSConfigActivatedEvent( + context.Background(), + &instance.NewAggregate("instanceID").Aggregate, + "id", + ), + ), + ), + expectPush( + user.NewHumanPhoneChangedEvent(context.Background(), + &userAgg.Aggregate, + "+41791234567", + ), + user.NewHumanPhoneCodeAddedEventV2(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + nil, + 0, + false, + "id", + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + newEncryptedCodeWithDefault: mockEncryptedCodeWithDefault("phoneCode", time.Hour), + defaultSecretGenerators: defaultGenerators, + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + human: &ChangeHuman{ + Phone: &Phone{ + Number: "+41791234567", + }, + }, + codeAlg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), + }, + res: res{ + want: &domain.ObjectDetails{ + Sequence: 0, + EventDate: time.Time{}, + ResourceOwner: "org1", + }, + }, + }, + { name: "change human phone verified, not allowed", fields: fields{ eventstore: expectEventstore( @@ -2255,6 +2559,36 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) { newAddHumanEvent("$plain$x$password", true, true, "", language.English), ), ), + expectFilter( + eventFromEventPusher( + instance.NewSMSConfigActivatedEvent( + context.Background(), + &instance.NewAggregate("instanceID").Aggregate, + "id", + ), + ), + ), + expectFilter( + eventFromEventPusher( + instance.NewSMSConfigTwilioAddedEvent( + context.Background(), + &instance.NewAggregate("instanceID").Aggregate, + "id", + "", + "sid", + "senderNumber", + &crypto.CryptoValue{CryptoType: crypto.TypeEncryption, Algorithm: "enc", KeyID: "id", Crypted: []byte("crypted")}, + "", + ), + ), + eventFromEventPusher( + instance.NewSMSConfigActivatedEvent( + context.Background(), + &instance.NewAggregate("instanceID").Aggregate, + "id", + ), + ), + ), expectPush( user.NewHumanPhoneChangedEvent(context.Background(), &userAgg.Aggregate, @@ -2270,11 +2604,13 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) { }, time.Hour, true, + "", ), ), ), - checkPermission: newMockPermissionCheckAllowed(), - newCode: mockEncryptedCode("phoneCode", time.Hour), + checkPermission: newMockPermissionCheckAllowed(), + newEncryptedCodeWithDefault: mockEncryptedCodeWithDefault("phoneCode", time.Hour), + defaultSecretGenerators: defaultGenerators, }, args: args{ ctx: context.Background(), @@ -2929,11 +3265,13 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := &Commands{ - eventstore: tt.fields.eventstore(t), - userPasswordHasher: tt.fields.userPasswordHasher, - newEncryptedCode: tt.fields.newCode, - checkPermission: tt.fields.checkPermission, - userEncryption: tt.args.codeAlg, + eventstore: tt.fields.eventstore(t), + userPasswordHasher: tt.fields.userPasswordHasher, + newEncryptedCode: tt.fields.newCode, + newEncryptedCodeWithDefault: tt.fields.newEncryptedCodeWithDefault, + checkPermission: tt.fields.checkPermission, + defaultSecretGenerators: tt.fields.defaultSecretGenerators, + userEncryption: tt.args.codeAlg, } err := r.ChangeUserHuman(tt.args.ctx, tt.args.human, tt.args.codeAlg) if tt.res.err == nil { diff --git a/internal/command/user_v2_model.go b/internal/command/user_v2_model.go index d31c3e667..25150ab65 100644 --- a/internal/command/user_v2_model.go +++ b/internal/command/user_v2_model.go @@ -42,13 +42,15 @@ type UserV2WriteModel struct { InitCodeExpiry time.Duration InitCheckFailedCount uint64 - PasswordWriteModel bool - PasswordEncodedHash string - PasswordChangeRequired bool - PasswordCode *crypto.CryptoValue - PasswordCodeCreationDate time.Time - PasswordCodeExpiry time.Duration - PasswordCheckFailedCount uint64 + PasswordWriteModel bool + PasswordEncodedHash string + PasswordChangeRequired bool + PasswordCode *crypto.CryptoValue + PasswordCodeCreationDate time.Time + PasswordCodeExpiry time.Duration + PasswordCheckFailedCount uint64 + PasswordCodeGeneratorID string + PasswordCodeVerificationID string EmailWriteModel bool Email domain.EmailAddress @@ -270,7 +272,9 @@ func (wm *UserV2WriteModel) Reduce() error { wm.PasswordChangeRequired = e.ChangeRequired wm.EmptyPasswordCode() case *user.HumanPasswordCodeAddedEvent: - wm.SetPasswordCode(e.Code, e.Expiry, e.CreationDate()) + wm.SetPasswordCode(e) + case *user.HumanPasswordCodeSentEvent: + wm.SetPasswordCodeSent(e) case *user.UserIDPLinkAddedEvent: wm.AddIDPLink(e.IDPConfigID, e.DisplayName, e.ExternalUserID) case *user.UserIDPLinkRemovedEvent: @@ -337,10 +341,16 @@ func (wm *UserV2WriteModel) EmptyPasswordCode() { wm.PasswordCodeExpiry = 0 wm.PasswordCodeCreationDate = time.Time{} } -func (wm *UserV2WriteModel) SetPasswordCode(code *crypto.CryptoValue, expiry time.Duration, creationDate time.Time) { - wm.PasswordCode = code - wm.PasswordCodeExpiry = expiry - wm.PasswordCodeCreationDate = creationDate +func (wm *UserV2WriteModel) SetPasswordCode(e *user.HumanPasswordCodeAddedEvent) { + wm.PasswordCode = e.Code + wm.PasswordCodeExpiry = e.Expiry + wm.PasswordCodeCreationDate = e.CreationDate() + wm.PasswordCodeGeneratorID = e.GeneratorID +} + +func (wm *UserV2WriteModel) SetPasswordCodeSent(e *user.HumanPasswordCodeSentEvent) { + wm.PasswordCodeGeneratorID = e.GeneratorInfo.GetID() + wm.PasswordCodeVerificationID = e.GeneratorInfo.GetVerificationID() } func (wm *UserV2WriteModel) Query() *eventstore.SearchQueryBuilder { diff --git a/internal/command/user_v2_model_test.go b/internal/command/user_v2_model_test.go index a62bf4546..ba5180cae 100644 --- a/internal/command/user_v2_model_test.go +++ b/internal/command/user_v2_model_test.go @@ -1021,6 +1021,7 @@ func TestCommandSide_userHumanWriteModel_phone(t *testing.T) { }, time.Hour*1, false, + "", ), ), ), @@ -1064,6 +1065,65 @@ func TestCommandSide_userHumanWriteModel_phone(t *testing.T) { }, }, }, + { + name: "user added with phone code external", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + newRegisterHumanEvent("username", "$plain$x$password", true, true, "", language.English), + ), + eventFromEventPusher( + user.NewHumanPhoneChangedEvent(context.Background(), + &userAgg.Aggregate, + "+41791234567", + ), + ), + eventFromEventPusher( + user.NewHumanPhoneCodeAddedEventV2(context.Background(), + &userAgg.Aggregate, + nil, + 0, + false, + "id", + ), + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + userID: "user1", + }, + res: res{ + want: &UserV2WriteModel{ + HumanWriteModel: true, + PhoneWriteModel: true, + StateWriteModel: true, + WriteModel: eventstore.WriteModel{ + AggregateID: "user1", + Events: []eventstore.Event{}, + ProcessedSequence: 0, + ResourceOwner: "org1", + }, + UserName: "username", + FirstName: "firstname", + LastName: "lastname", + DisplayName: "firstname lastname", + PreferredLanguage: language.English, + PasswordEncodedHash: "$plain$x$password", + PasswordChangeRequired: true, + Email: "email@test.ch", + IsEmailVerified: false, + Phone: "+41791234567", + IsPhoneVerified: false, + PhoneCode: nil, + PhoneCodeCreationDate: time.Time{}, + PhoneCodeExpiry: 0, + UserState: domain.UserStateActive, + }, + }, + }, { name: "user added with phone code verified", fields: fields{ @@ -1089,6 +1149,7 @@ func TestCommandSide_userHumanWriteModel_phone(t *testing.T) { }, time.Hour*1, false, + "", ), ), eventFromEventPusher( @@ -1155,6 +1216,7 @@ func TestCommandSide_userHumanWriteModel_phone(t *testing.T) { }, time.Hour*1, false, + "", ), ), eventFromEventPusher( @@ -1229,6 +1291,7 @@ func TestCommandSide_userHumanWriteModel_phone(t *testing.T) { }, time.Hour*1, false, + "", ), ), eventFromEventPusher( diff --git a/internal/command/user_v2_phone.go b/internal/command/user_v2_phone.go index 7ebbd4b4d..8b754b36f 100644 --- a/internal/command/user_v2_phone.go +++ b/internal/command/user_v2_phone.go @@ -6,9 +6,11 @@ import ( "github.com/zitadel/logging" "github.com/zitadel/zitadel/internal/api/authz" + "github.com/zitadel/zitadel/internal/command/preparation" "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/notification/senders" "github.com/zitadel/zitadel/internal/repository/user" "github.com/zitadel/zitadel/internal/zerrors" ) @@ -88,7 +90,7 @@ func (c *Commands) changeUserPhoneWithGenerator(ctx context.Context, userID, pho if err = cmd.Change(ctx, domain.PhoneNumber(phone)); err != nil { return nil, err } - if err = cmd.AddGeneratedCode(ctx, gen, returnCode); err != nil { + if err = cmd.AddGeneratedCode(ctx, returnCode); err != nil { return nil, err } return cmd.Push(ctx) @@ -107,10 +109,10 @@ func (c *Commands) resendUserPhoneCodeWithGenerator(ctx context.Context, userID return nil, err } } - if cmd.model.Code == nil { + if cmd.model.Code == nil && cmd.model.GeneratorID == "" { return nil, zerrors.ThrowPreconditionFailed(err, "PHONE-5xrra88eq8", "Errors.User.Code.Empty") } - if err = cmd.AddGeneratedCode(ctx, gen, returnCode); err != nil { + if err = cmd.AddGeneratedCode(ctx, returnCode); err != nil { return nil, err } return cmd.Push(ctx) @@ -166,10 +168,12 @@ func (c *Commands) removeUserPhone(ctx context.Context, userID string) (*domain. // UserPhoneEvents allows step-by-step additions of events, // operating on the Human Phone Model. type UserPhoneEvents struct { - eventstore *eventstore.Eventstore - aggregate *eventstore.Aggregate - events []eventstore.Command - model *HumanPhoneWriteModel + eventstore *eventstore.Eventstore + aggregate *eventstore.Aggregate + events []eventstore.Command + model *HumanPhoneWriteModel + generateCode func(ctx context.Context, filter preparation.FilterToQueryReducer) (*EncryptedCode, string, error) + getCodeVerifier func(ctx context.Context, id string) (senders.CodeGenerator, error) plainCode *string } @@ -196,6 +200,10 @@ func (c *Commands) NewUserPhoneEvents(ctx context.Context, userID string) (*User eventstore: c.eventstore, aggregate: UserAggregateFromWriteModel(&model.WriteModel), model: model, + generateCode: func(ctx context.Context, filter preparation.FilterToQueryReducer) (*EncryptedCode, string, error) { + return c.newPhoneCode(ctx, filter, domain.SecretGeneratorTypeVerifyPhoneCode, c.userEncryption, c.defaultSecretGenerators.PhoneVerificationCode) + }, + getCodeVerifier: c.phoneCodeVerifier, }, nil } @@ -229,15 +237,14 @@ func (c *UserPhoneEvents) SetVerified(ctx context.Context) { // AddGeneratedCode generates a new encrypted code and sets it to the phone number. // When returnCode a plain text of the code will be returned from Push. -func (c *UserPhoneEvents) AddGeneratedCode(ctx context.Context, gen crypto.Generator, returnCode bool) error { - value, plain, err := crypto.NewCode(gen) +func (c *UserPhoneEvents) AddGeneratedCode(ctx context.Context, returnCode bool) error { + code, generatorID, err := c.generateCode(ctx, c.eventstore.Filter) //nolint:staticcheck if err != nil { return err } - - c.events = append(c.events, user.NewHumanPhoneCodeAddedEventV2(ctx, c.aggregate, value, gen.Expiry(), returnCode)) + c.events = append(c.events, user.NewHumanPhoneCodeAddedEventV2(ctx, c.aggregate, code.CryptedCode(), code.CodeExpiry(), returnCode, generatorID)) if returnCode { - c.plainCode = &plain + c.plainCode = &code.Plain } return nil } @@ -247,7 +254,17 @@ func (c *UserPhoneEvents) VerifyCode(ctx context.Context, code string, gen crypt return zerrors.ThrowInvalidArgument(nil, "COMMAND-Fia4a", "Errors.User.Code.Empty") } - err := crypto.VerifyCode(c.model.CodeCreationDate, c.model.CodeExpiry, c.model.Code, code, gen.Alg()) + err := verifyCode( + ctx, + c.model.CodeCreationDate, + c.model.CodeExpiry, + c.model.Code, + c.model.GeneratorID, + c.model.VerificationID, + code, + gen.Alg(), + c.getCodeVerifier, + ) if err == nil { c.events = append(c.events, user.NewHumanPhoneVerifiedEvent(ctx, c.aggregate)) return nil diff --git a/internal/command/user_v2_phone_test.go b/internal/command/user_v2_phone_test.go index d64960efd..cba3b6d38 100644 --- a/internal/command/user_v2_phone_test.go +++ b/internal/command/user_v2_phone_test.go @@ -22,7 +22,7 @@ import ( func TestCommands_ChangeUserPhone(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore + eventstore func(*testing.T) *eventstore.Eventstore checkPermission domain.PermissionCheck } type args struct { @@ -38,8 +38,7 @@ func TestCommands_ChangeUserPhone(t *testing.T) { { name: "missing permission", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( instance.NewSecretGeneratorAddedEvent(context.Background(), @@ -81,8 +80,7 @@ func TestCommands_ChangeUserPhone(t *testing.T) { { name: "missing phone", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( instance.NewSecretGeneratorAddedEvent(context.Background(), @@ -124,8 +122,7 @@ func TestCommands_ChangeUserPhone(t *testing.T) { { name: "not changed", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( instance.NewSecretGeneratorAddedEvent(context.Background(), @@ -168,7 +165,7 @@ func TestCommands_ChangeUserPhone(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { c := &Commands{ - eventstore: tt.fields.eventstore, + eventstore: tt.fields.eventstore(t), checkPermission: tt.fields.checkPermission, } _, err := c.ChangeUserPhone(context.Background(), tt.args.userID, tt.args.phone, crypto.CreateMockEncryptionAlg(gomock.NewController(t))) @@ -180,7 +177,7 @@ func TestCommands_ChangeUserPhone(t *testing.T) { func TestCommands_ChangeUserPhoneReturnCode(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore + eventstore func(*testing.T) *eventstore.Eventstore checkPermission domain.PermissionCheck } type args struct { @@ -196,8 +193,7 @@ func TestCommands_ChangeUserPhoneReturnCode(t *testing.T) { { name: "missing permission", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( instance.NewSecretGeneratorAddedEvent(context.Background(), @@ -239,8 +235,7 @@ func TestCommands_ChangeUserPhoneReturnCode(t *testing.T) { { name: "missing phone", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( instance.NewSecretGeneratorAddedEvent(context.Background(), @@ -283,7 +278,7 @@ func TestCommands_ChangeUserPhoneReturnCode(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { c := &Commands{ - eventstore: tt.fields.eventstore, + eventstore: tt.fields.eventstore(t), checkPermission: tt.fields.checkPermission, } _, err := c.ChangeUserPhoneReturnCode(context.Background(), tt.args.userID, tt.args.phone, crypto.CreateMockEncryptionAlg(gomock.NewController(t))) @@ -295,7 +290,7 @@ func TestCommands_ChangeUserPhoneReturnCode(t *testing.T) { func TestCommands_ResendUserPhoneCode(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore + eventstore func(*testing.T) *eventstore.Eventstore checkPermission domain.PermissionCheck } type args struct { @@ -310,8 +305,7 @@ func TestCommands_ResendUserPhoneCode(t *testing.T) { { name: "missing permission", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( instance.NewSecretGeneratorAddedEvent(context.Background(), @@ -352,8 +346,7 @@ func TestCommands_ResendUserPhoneCode(t *testing.T) { { name: "no code", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( instance.NewSecretGeneratorAddedEvent(context.Background(), @@ -395,7 +388,7 @@ func TestCommands_ResendUserPhoneCode(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { c := &Commands{ - eventstore: tt.fields.eventstore, + eventstore: tt.fields.eventstore(t), checkPermission: tt.fields.checkPermission, } _, err := c.ResendUserPhoneCode(context.Background(), tt.args.userID, crypto.CreateMockEncryptionAlg(gomock.NewController(t))) @@ -407,7 +400,7 @@ func TestCommands_ResendUserPhoneCode(t *testing.T) { func TestCommands_ResendUserPhoneCodeReturnCode(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore + eventstore func(*testing.T) *eventstore.Eventstore checkPermission domain.PermissionCheck } type args struct { @@ -422,8 +415,7 @@ func TestCommands_ResendUserPhoneCodeReturnCode(t *testing.T) { { name: "missing permission", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( instance.NewSecretGeneratorAddedEvent(context.Background(), @@ -464,8 +456,7 @@ func TestCommands_ResendUserPhoneCodeReturnCode(t *testing.T) { { name: "no code", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( instance.NewSecretGeneratorAddedEvent(context.Background(), @@ -507,7 +498,7 @@ func TestCommands_ResendUserPhoneCodeReturnCode(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { c := &Commands{ - eventstore: tt.fields.eventstore, + eventstore: tt.fields.eventstore(t), checkPermission: tt.fields.checkPermission, } _, err := c.ResendUserPhoneCodeReturnCode(context.Background(), tt.args.userID, crypto.CreateMockEncryptionAlg(gomock.NewController(t))) @@ -519,7 +510,7 @@ func TestCommands_ResendUserPhoneCodeReturnCode(t *testing.T) { func TestCommands_ChangeUserPhoneVerified(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore + eventstore func(*testing.T) *eventstore.Eventstore checkPermission domain.PermissionCheck } type args struct { @@ -536,7 +527,7 @@ func TestCommands_ChangeUserPhoneVerified(t *testing.T) { { name: "missing userID", fields: fields{ - eventstore: eventstoreExpect(t), + eventstore: expectEventstore(), checkPermission: newMockPermissionCheckNotAllowed(), }, args: args{ @@ -548,8 +539,7 @@ func TestCommands_ChangeUserPhoneVerified(t *testing.T) { { name: "missing permission", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( func() eventstore.Command { @@ -582,8 +572,7 @@ func TestCommands_ChangeUserPhoneVerified(t *testing.T) { { name: "missing phone", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( func() eventstore.Command { @@ -616,8 +605,7 @@ func TestCommands_ChangeUserPhoneVerified(t *testing.T) { { name: "phone changed", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( func() eventstore.Command { @@ -667,7 +655,7 @@ func TestCommands_ChangeUserPhoneVerified(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { c := &Commands{ - eventstore: tt.fields.eventstore, + eventstore: tt.fields.eventstore(t), checkPermission: tt.fields.checkPermission, } got, err := c.ChangeUserPhoneVerified(context.Background(), tt.args.userID, tt.args.phone) @@ -678,9 +666,22 @@ func TestCommands_ChangeUserPhoneVerified(t *testing.T) { } func TestCommands_changeUserPhoneWithGenerator(t *testing.T) { + defaultGenerators := &SecretGenerators{ + OTPSMS: &crypto.GeneratorConfig{ + Length: 8, + Expiry: time.Hour, + IncludeLowerLetters: true, + IncludeUpperLetters: true, + IncludeDigits: true, + IncludeSymbols: true, + }, + } type fields struct { - eventstore *eventstore.Eventstore - checkPermission domain.PermissionCheck + eventstore func(*testing.T) *eventstore.Eventstore + checkPermission domain.PermissionCheck + newEncryptedCodeWithDefault encryptedCodeWithDefaultFunc + userEncryption crypto.EncryptionAlgorithm + defaultSecretGenerators *SecretGenerators } type args struct { userID string @@ -697,7 +698,7 @@ func TestCommands_changeUserPhoneWithGenerator(t *testing.T) { { name: "missing user", fields: fields{ - eventstore: eventstoreExpect(t), + eventstore: expectEventstore(), }, args: args{ userID: "", @@ -709,8 +710,7 @@ func TestCommands_changeUserPhoneWithGenerator(t *testing.T) { { name: "missing permission", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( func() eventstore.Command { @@ -744,8 +744,7 @@ func TestCommands_changeUserPhoneWithGenerator(t *testing.T) { { name: "missing phone", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( func() eventstore.Command { @@ -779,8 +778,7 @@ func TestCommands_changeUserPhoneWithGenerator(t *testing.T) { { name: "not changed", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( func() eventstore.Command { @@ -814,8 +812,7 @@ func TestCommands_changeUserPhoneWithGenerator(t *testing.T) { { name: "phone changed", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( func() eventstore.Command { @@ -836,6 +833,36 @@ func TestCommands_changeUserPhoneWithGenerator(t *testing.T) { }(), ), ), + expectFilter( + eventFromEventPusher( + instance.NewSMSConfigActivatedEvent( + context.Background(), + &instance.NewAggregate("instanceID").Aggregate, + "id", + ), + ), + ), + expectFilter( + eventFromEventPusher( + instance.NewSMSConfigTwilioAddedEvent( + context.Background(), + &instance.NewAggregate("instanceID").Aggregate, + "id", + "", + "sid", + "senderNumber", + &crypto.CryptoValue{CryptoType: crypto.TypeEncryption, Algorithm: "enc", KeyID: "id", Crypted: []byte("crypted")}, + "", + ), + ), + eventFromEventPusher( + instance.NewSMSConfigActivatedEvent( + context.Background(), + &instance.NewAggregate("instanceID").Aggregate, + "id", + ), + ), + ), expectPush( user.NewHumanPhoneChangedEvent(context.Background(), &user.NewAggregate("user1", "org1").Aggregate, @@ -851,10 +878,13 @@ func TestCommands_changeUserPhoneWithGenerator(t *testing.T) { }, time.Hour*1, false, + "", ), ), ), - checkPermission: newMockPermissionCheckAllowed(), + checkPermission: newMockPermissionCheckAllowed(), + newEncryptedCodeWithDefault: mockEncryptedCodeWithDefault("a", time.Hour), + defaultSecretGenerators: defaultGenerators, }, args: args{ userID: "user1", @@ -873,8 +903,7 @@ func TestCommands_changeUserPhoneWithGenerator(t *testing.T) { { name: "phone changed, return code", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( func() eventstore.Command { @@ -895,6 +924,36 @@ func TestCommands_changeUserPhoneWithGenerator(t *testing.T) { }(), ), ), + expectFilter( + eventFromEventPusher( + instance.NewSMSConfigActivatedEvent( + context.Background(), + &instance.NewAggregate("instanceID").Aggregate, + "id", + ), + ), + ), + expectFilter( + eventFromEventPusher( + instance.NewSMSConfigTwilioAddedEvent( + context.Background(), + &instance.NewAggregate("instanceID").Aggregate, + "id", + "", + "sid", + "senderNumber", + &crypto.CryptoValue{CryptoType: crypto.TypeEncryption, Algorithm: "enc", KeyID: "id", Crypted: []byte("crypted")}, + "", + ), + ), + eventFromEventPusher( + instance.NewSMSConfigActivatedEvent( + context.Background(), + &instance.NewAggregate("instanceID").Aggregate, + "id", + ), + ), + ), expectPush( user.NewHumanPhoneChangedEvent(context.Background(), &user.NewAggregate("user1", "org1").Aggregate, @@ -910,10 +969,13 @@ func TestCommands_changeUserPhoneWithGenerator(t *testing.T) { }, time.Hour*1, true, + "", ), ), ), - checkPermission: newMockPermissionCheckAllowed(), + checkPermission: newMockPermissionCheckAllowed(), + newEncryptedCodeWithDefault: mockEncryptedCodeWithDefault("a", time.Hour), + defaultSecretGenerators: defaultGenerators, }, args: args{ userID: "user1", @@ -934,8 +996,11 @@ func TestCommands_changeUserPhoneWithGenerator(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(t), + checkPermission: tt.fields.checkPermission, + newEncryptedCodeWithDefault: tt.fields.newEncryptedCodeWithDefault, + userEncryption: tt.fields.userEncryption, + defaultSecretGenerators: tt.fields.defaultSecretGenerators, } got, err := c.changeUserPhoneWithGenerator(context.Background(), tt.args.userID, tt.args.phone, GetMockSecretGenerator(t), tt.args.returnCode) require.ErrorIs(t, err, tt.wantErr) @@ -945,9 +1010,21 @@ func TestCommands_changeUserPhoneWithGenerator(t *testing.T) { } func TestCommands_resendUserPhoneCodeWithGenerator(t *testing.T) { + defaultGenerators := &SecretGenerators{ + OTPSMS: &crypto.GeneratorConfig{ + Length: 8, + Expiry: time.Hour, + IncludeLowerLetters: true, + IncludeUpperLetters: true, + IncludeDigits: true, + IncludeSymbols: true, + }, + } type fields struct { - eventstore *eventstore.Eventstore - checkPermission domain.PermissionCheck + eventstore func(*testing.T) *eventstore.Eventstore + checkPermission domain.PermissionCheck + newEncryptedCodeWithDefault encryptedCodeWithDefaultFunc + defaultSecretGenerators *SecretGenerators } type args struct { userID string @@ -963,7 +1040,7 @@ func TestCommands_resendUserPhoneCodeWithGenerator(t *testing.T) { { name: "missing user", fields: fields{ - eventstore: eventstoreExpect(t), + eventstore: expectEventstore(), }, args: args{ userID: "", @@ -974,8 +1051,7 @@ func TestCommands_resendUserPhoneCodeWithGenerator(t *testing.T) { { name: "missing permission", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( func() eventstore.Command { @@ -1008,8 +1084,7 @@ func TestCommands_resendUserPhoneCodeWithGenerator(t *testing.T) { { name: "no code", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( func() eventstore.Command { @@ -1042,8 +1117,7 @@ func TestCommands_resendUserPhoneCodeWithGenerator(t *testing.T) { { name: "resend", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( func() eventstore.Command { @@ -1074,6 +1148,37 @@ func TestCommands_resendUserPhoneCodeWithGenerator(t *testing.T) { }, time.Hour*1, true, + "", + ), + ), + ), + expectFilter( + eventFromEventPusher( + instance.NewSMSConfigActivatedEvent( + context.Background(), + &instance.NewAggregate("instanceID").Aggregate, + "id", + ), + ), + ), + expectFilter( + eventFromEventPusher( + instance.NewSMSConfigTwilioAddedEvent( + context.Background(), + &instance.NewAggregate("instanceID").Aggregate, + "id", + "", + "sid", + "senderNumber", + &crypto.CryptoValue{CryptoType: crypto.TypeEncryption, Algorithm: "enc", KeyID: "id", Crypted: []byte("crypted")}, + "", + ), + ), + eventFromEventPusher( + instance.NewSMSConfigActivatedEvent( + context.Background(), + &instance.NewAggregate("instanceID").Aggregate, + "id", ), ), ), @@ -1088,10 +1193,103 @@ func TestCommands_resendUserPhoneCodeWithGenerator(t *testing.T) { }, time.Hour*1, false, + "", ), ), ), - checkPermission: newMockPermissionCheckAllowed(), + checkPermission: newMockPermissionCheckAllowed(), + newEncryptedCodeWithDefault: mockEncryptedCodeWithDefault("a", time.Hour), + defaultSecretGenerators: defaultGenerators, + }, + args: args{ + userID: "user1", + returnCode: false, + }, + want: &domain.Phone{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "user1", + ResourceOwner: "org1", + }, + PhoneNumber: "+41791234567", + IsPhoneVerified: false, + }, + }, + { + name: "resend (external)", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + func() eventstore.Command { + event := user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username", + "firstname", + "lastname", + "nickname", + "displayname", + language.German, + domain.GenderUnspecified, + "email@test.ch", + true, + ) + event.AddPhoneData("+41791234567") + return event + }(), + ), + eventFromEventPusher( + user.NewHumanPhoneCodeAddedEventV2(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + nil, + 0, + true, + "id", + ), + ), + ), + expectFilter( + eventFromEventPusher( + instance.NewSMSConfigActivatedEvent( + context.Background(), + &instance.NewAggregate("instanceID").Aggregate, + "id", + ), + ), + ), + expectFilter( + eventFromEventPusher( + instance.NewSMSConfigTwilioAddedEvent( + context.Background(), + &instance.NewAggregate("instanceID").Aggregate, + "id", + "", + "sid", + "senderNumber", + &crypto.CryptoValue{CryptoType: crypto.TypeEncryption, Algorithm: "enc", KeyID: "id", Crypted: []byte("crypted")}, + "verifyServiceSid", + ), + ), + eventFromEventPusher( + instance.NewSMSConfigActivatedEvent( + context.Background(), + &instance.NewAggregate("instanceID").Aggregate, + "id", + ), + ), + ), + expectPush( + user.NewHumanPhoneCodeAddedEventV2(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + nil, + 0, + false, + "id", + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + newEncryptedCodeWithDefault: mockEncryptedCodeWithDefault("a", time.Hour), + defaultSecretGenerators: defaultGenerators, }, args: args{ userID: "user1", @@ -1109,8 +1307,7 @@ func TestCommands_resendUserPhoneCodeWithGenerator(t *testing.T) { { name: "return code", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( func() eventstore.Command { @@ -1141,6 +1338,37 @@ func TestCommands_resendUserPhoneCodeWithGenerator(t *testing.T) { }, time.Hour*1, true, + "", + ), + ), + ), + expectFilter( + eventFromEventPusher( + instance.NewSMSConfigActivatedEvent( + context.Background(), + &instance.NewAggregate("instanceID").Aggregate, + "id", + ), + ), + ), + expectFilter( + eventFromEventPusher( + instance.NewSMSConfigTwilioAddedEvent( + context.Background(), + &instance.NewAggregate("instanceID").Aggregate, + "id", + "", + "sid", + "senderNumber", + &crypto.CryptoValue{CryptoType: crypto.TypeEncryption, Algorithm: "enc", KeyID: "id", Crypted: []byte("crypted")}, + "", + ), + ), + eventFromEventPusher( + instance.NewSMSConfigActivatedEvent( + context.Background(), + &instance.NewAggregate("instanceID").Aggregate, + "id", ), ), ), @@ -1155,10 +1383,13 @@ func TestCommands_resendUserPhoneCodeWithGenerator(t *testing.T) { }, time.Hour*1, true, + "", ), ), ), - checkPermission: newMockPermissionCheckAllowed(), + checkPermission: newMockPermissionCheckAllowed(), + newEncryptedCodeWithDefault: mockEncryptedCodeWithDefault("a", time.Hour), + defaultSecretGenerators: defaultGenerators, }, args: args{ userID: "user1", @@ -1178,8 +1409,10 @@ func TestCommands_resendUserPhoneCodeWithGenerator(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(t), + checkPermission: tt.fields.checkPermission, + newEncryptedCodeWithDefault: tt.fields.newEncryptedCodeWithDefault, + defaultSecretGenerators: tt.fields.defaultSecretGenerators, } got, err := c.resendUserPhoneCodeWithGenerator(context.Background(), tt.args.userID, GetMockSecretGenerator(t), tt.args.returnCode) require.ErrorIs(t, err, tt.wantErr) diff --git a/internal/command/user_v3.go b/internal/command/user_v3.go index 2251baa13..2dede584a 100644 --- a/internal/command/user_v3.go +++ b/internal/command/user_v3.go @@ -114,6 +114,9 @@ func (c *Commands) CreateSchemaUser(ctx context.Context, user *CreateSchemaUser) func(ctx context.Context) (*EncryptedCode, error) { return c.newEmailCode(ctx, c.eventstore.Filter, c.userEncryption) //nolint:staticcheck }, + func(ctx context.Context) (*EncryptedCode, string, error) { + return c.newPhoneCode(ctx, c.eventstore.Filter, domain.SecretGeneratorTypeVerifyPhoneCode, c.userEncryption, c.defaultSecretGenerators.PhoneVerificationCode) //nolint:staticcheck + }, ) if err != nil { return nil, err @@ -220,6 +223,9 @@ func (c *Commands) ChangeSchemaUser(ctx context.Context, user *ChangeSchemaUser) func(ctx context.Context) (*EncryptedCode, error) { return c.newEmailCode(ctx, c.eventstore.Filter, c.userEncryption) //nolint:staticcheck }, + func(ctx context.Context) (*EncryptedCode, string, error) { + return c.newPhoneCode(ctx, c.eventstore.Filter, domain.SecretGeneratorTypeVerifyPhoneCode, c.userEncryption, c.defaultSecretGenerators.PhoneVerificationCode) //nolint:staticcheck + }, ) if err != nil { return nil, err diff --git a/internal/command/user_v3_model.go b/internal/command/user_v3_model.go index 574ed6cd6..46568df87 100644 --- a/internal/command/user_v3_model.go +++ b/internal/command/user_v3_model.go @@ -222,7 +222,8 @@ func (wm *UserV3WriteModel) NewCreated( data json.RawMessage, email *Email, phone *Phone, - code func(context.Context) (*EncryptedCode, error), + emailCode func(context.Context) (*EncryptedCode, error), + phoneCode func(context.Context) (*EncryptedCode, string, error), ) (_ []eventstore.Command, codeEmail string, codePhone string, err error) { if err := wm.checkPermissionWrite(ctx, wm.ResourceOwner, wm.AggregateID); err != nil { return nil, "", "", err @@ -239,7 +240,7 @@ func (wm *UserV3WriteModel) NewCreated( if email != nil { emailEvents, plainCodeEmail, err := wm.NewEmailCreate(ctx, email, - code, + emailCode, ) if err != nil { return nil, "", "", err @@ -253,7 +254,7 @@ func (wm *UserV3WriteModel) NewCreated( if phone != nil { phoneEvents, plainCodePhone, err := wm.NewPhoneCreate(ctx, phone, - code, + phoneCode, ) if err != nil { return nil, "", "", err @@ -310,7 +311,8 @@ func (wm *UserV3WriteModel) NewUpdate( user *SchemaUser, email *Email, phone *Phone, - code func(context.Context) (*EncryptedCode, error), + emailCode func(context.Context) (*EncryptedCode, error), + phoneCode func(context.Context) (*EncryptedCode, string, error), ) (_ []eventstore.Command, codeEmail string, codePhone string, err error) { if err := wm.checkPermissionWrite(ctx, wm.ResourceOwner, wm.AggregateID); err != nil { return nil, "", "", err @@ -334,7 +336,7 @@ func (wm *UserV3WriteModel) NewUpdate( if email != nil { emailEvents, plainCodeEmail, err := wm.NewEmailUpdate(ctx, email, - code, + emailCode, ) if err != nil { return nil, "", "", err @@ -348,7 +350,7 @@ func (wm *UserV3WriteModel) NewUpdate( if phone != nil { phoneEvents, plainCodePhone, err := wm.NewPhoneCreate(ctx, phone, - code, + phoneCode, ) if err != nil { return nil, "", "", err @@ -564,7 +566,7 @@ func (wm *UserV3WriteModel) newEmailCodeAddedEvent( func (wm *UserV3WriteModel) NewPhoneCreate( ctx context.Context, phone *Phone, - code func(context.Context) (*EncryptedCode, error), + code func(context.Context) (*EncryptedCode, string, error), ) (_ []eventstore.Command, plainCode string, err error) { if err := wm.checkPermissionWrite(ctx, wm.ResourceOwner, wm.AggregateID); err != nil { return nil, "", err @@ -596,7 +598,7 @@ func (wm *UserV3WriteModel) NewPhoneCreate( func (wm *UserV3WriteModel) NewPhoneUpdate( ctx context.Context, phone *Phone, - code func(context.Context) (*EncryptedCode, error), + code func(context.Context) (*EncryptedCode, string, error), ) (_ []eventstore.Command, plainCode string, err error) { if !wm.PhoneWM { return nil, "", nil @@ -637,7 +639,7 @@ func (wm *UserV3WriteModel) newPhoneVerifiedEvent( func (wm *UserV3WriteModel) NewResendPhoneCode( ctx context.Context, - code func(context.Context) (*EncryptedCode, error), + code func(context.Context) (*EncryptedCode, string, error), isReturnCode bool, ) (_ []eventstore.Command, plainCode string, err error) { if !wm.PhoneWM { @@ -661,10 +663,10 @@ func (wm *UserV3WriteModel) NewResendPhoneCode( func (wm *UserV3WriteModel) newPhoneCodeAddedEvent( ctx context.Context, - code func(context.Context) (*EncryptedCode, error), + code func(context.Context) (*EncryptedCode, string, error), isReturnCode bool, ) (_ *schemauser.PhoneCodeAddedEvent, plainCode string, err error) { - cryptoCode, err := code(ctx) + cryptoCode, generatorID, err := code(ctx) if err != nil { return nil, "", err } @@ -673,8 +675,9 @@ func (wm *UserV3WriteModel) newPhoneCodeAddedEvent( } return schemauser.NewPhoneCodeAddedEvent(ctx, UserV3AggregateFromWriteModel(&wm.WriteModel), - cryptoCode.Crypted, - cryptoCode.Expiry, + cryptoCode.CryptedCode(), + cryptoCode.CodeExpiry(), isReturnCode, + generatorID, ), plainCode, nil } diff --git a/internal/command/user_v3_phone.go b/internal/command/user_v3_phone.go index 65ca36a0e..fa6ed1bab 100644 --- a/internal/command/user_v3_phone.go +++ b/internal/command/user_v3_phone.go @@ -41,8 +41,8 @@ func (c *Commands) ChangeSchemaUserPhone(ctx context.Context, user *ChangeSchema events, plainCode, err := writeModel.NewPhoneUpdate(ctx, user.Phone, - func(ctx context.Context) (*EncryptedCode, error) { - return c.newPhoneCode(ctx, c.eventstore.Filter, c.userEncryption) //nolint:staticcheck + func(ctx context.Context) (*EncryptedCode, string, error) { + return c.newPhoneCode(ctx, c.eventstore.Filter, domain.SecretGeneratorTypeVerifyPhoneCode, c.userEncryption, c.defaultSecretGenerators.PhoneVerificationCode) //nolint:staticcheck }, ) if err != nil { @@ -92,8 +92,8 @@ func (c *Commands) ResendSchemaUserPhoneCode(ctx context.Context, user *ResendSc } events, plainCode, err := writeModel.NewResendPhoneCode(ctx, - func(ctx context.Context) (*EncryptedCode, error) { - return c.newPhoneCode(ctx, c.eventstore.Filter, c.userEncryption) //nolint:staticcheck + func(ctx context.Context) (*EncryptedCode, string, error) { + return c.newPhoneCode(ctx, c.eventstore.Filter, domain.SecretGeneratorTypeVerifyPhoneCode, c.userEncryption, c.defaultSecretGenerators.PhoneVerificationCode) //nolint:staticcheck }, user.ReturnCode, ) diff --git a/internal/command/user_v3_phone_test.go b/internal/command/user_v3_phone_test.go index 8a5a1ae0b..1d45bade4 100644 --- a/internal/command/user_v3_phone_test.go +++ b/internal/command/user_v3_phone_test.go @@ -14,6 +14,7 @@ import ( "github.com/zitadel/zitadel/internal/crypto" "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/user/schema" "github.com/zitadel/zitadel/internal/repository/user/schemauser" "github.com/zitadel/zitadel/internal/zerrors" @@ -21,9 +22,9 @@ import ( func TestCommands_ChangeSchemaUserPhone(t *testing.T) { type fields struct { - eventstore func(t *testing.T) *eventstore.Eventstore - checkPermission domain.PermissionCheck - newCode encrypedCodeFunc + eventstore func(t *testing.T) *eventstore.Eventstore + checkPermission domain.PermissionCheck + newEncryptedCodeWithDefault encryptedCodeWithDefaultFunc } type args struct { ctx context.Context @@ -187,6 +188,36 @@ func TestCommands_ChangeSchemaUserPhone(t *testing.T) { ), ), ), + expectFilter( + eventFromEventPusher( + instance.NewSMSConfigActivatedEvent( + context.Background(), + &instance.NewAggregate("instanceID").Aggregate, + "id", + ), + ), + ), + expectFilter( + eventFromEventPusher( + instance.NewSMSConfigTwilioAddedEvent( + context.Background(), + &instance.NewAggregate("instanceID").Aggregate, + "id", + "", + "sid", + "senderNumber", + &crypto.CryptoValue{CryptoType: crypto.TypeEncryption, Algorithm: "enc", KeyID: "id", Crypted: []byte("crypted")}, + "", + ), + ), + eventFromEventPusher( + instance.NewSMSConfigActivatedEvent( + context.Background(), + &instance.NewAggregate("instanceID").Aggregate, + "id", + ), + ), + ), expectPush( schemauser.NewPhoneUpdatedEvent(context.Background(), &schemauser.NewAggregate("user1", "org1").Aggregate, @@ -202,11 +233,12 @@ func TestCommands_ChangeSchemaUserPhone(t *testing.T) { }, time.Hour*1, true, + "", ), ), ), - checkPermission: newMockPermissionCheckAllowed(), - newCode: mockEncryptedCode("phoneverify", time.Hour), + checkPermission: newMockPermissionCheckAllowed(), + newEncryptedCodeWithDefault: mockEncryptedCodeWithDefault("phoneverify", time.Hour), }, args{ ctx: authz.NewMockContext("instanceID", "", ""), @@ -239,6 +271,36 @@ func TestCommands_ChangeSchemaUserPhone(t *testing.T) { "name": "user" }`), )), + expectFilter( + eventFromEventPusher( + instance.NewSMSConfigActivatedEvent( + context.Background(), + &instance.NewAggregate("instanceID").Aggregate, + "id", + ), + ), + ), + expectFilter( + eventFromEventPusher( + instance.NewSMSConfigTwilioAddedEvent( + context.Background(), + &instance.NewAggregate("instanceID").Aggregate, + "id", + "", + "sid", + "senderNumber", + &crypto.CryptoValue{CryptoType: crypto.TypeEncryption, Algorithm: "enc", KeyID: "id", Crypted: []byte("crypted")}, + "", + ), + ), + eventFromEventPusher( + instance.NewSMSConfigActivatedEvent( + context.Background(), + &instance.NewAggregate("instanceID").Aggregate, + "id", + ), + ), + ), expectPush( schemauser.NewPhoneUpdatedEvent(context.Background(), &schemauser.NewAggregate("user1", "org1").Aggregate, @@ -253,11 +315,87 @@ func TestCommands_ChangeSchemaUserPhone(t *testing.T) { Crypted: []byte("phoneverify"), }, time.Hour*1, false, + "", + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + newEncryptedCodeWithDefault: mockEncryptedCodeWithDefault("phoneverify", time.Hour), + }, + args{ + ctx: authz.NewMockContext("instanceID", "", ""), + user: &ChangeSchemaUserPhone{ + ID: "user1", + Phone: &Phone{ + Number: "+41791234567", + }, + }, + }, + res{ + details: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + { + "user updated, phone to verify (external)", + fields{ + eventstore: expectEventstore( + expectFilter( + schemauser.NewCreatedEvent( + context.Background(), + &schemauser.NewAggregate("user1", "org1").Aggregate, + "type", + 1, + json.RawMessage(`{ + "name": "user" + }`), + )), + expectFilter( + eventFromEventPusher( + instance.NewSMSConfigActivatedEvent( + context.Background(), + &instance.NewAggregate("instanceID").Aggregate, + "id", + ), + ), + ), + expectFilter( + eventFromEventPusher( + instance.NewSMSConfigTwilioAddedEvent( + context.Background(), + &instance.NewAggregate("instanceID").Aggregate, + "id", + "", + "sid", + "senderNumber", + &crypto.CryptoValue{CryptoType: crypto.TypeEncryption, Algorithm: "enc", KeyID: "id", Crypted: []byte("crypted")}, + "verifiyServiceSid", + ), + ), + eventFromEventPusher( + instance.NewSMSConfigActivatedEvent( + context.Background(), + &instance.NewAggregate("instanceID").Aggregate, + "id", + ), + ), + ), + expectPush( + schemauser.NewPhoneUpdatedEvent(context.Background(), + &schemauser.NewAggregate("user1", "org1").Aggregate, + "+41791234567", + ), + schemauser.NewPhoneCodeAddedEvent(context.Background(), + &schemauser.NewAggregate("user1", "org1").Aggregate, + nil, + 0, + false, + "id", ), ), ), checkPermission: newMockPermissionCheckAllowed(), - newCode: mockEncryptedCode("phoneverify", time.Hour), }, args{ ctx: authz.NewMockContext("instanceID", "", ""), @@ -321,10 +459,20 @@ func TestCommands_ChangeSchemaUserPhone(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { c := &Commands{ - eventstore: tt.fields.eventstore(t), - checkPermission: tt.fields.checkPermission, - newEncryptedCode: tt.fields.newCode, - userEncryption: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), + eventstore: tt.fields.eventstore(t), + checkPermission: tt.fields.checkPermission, + newEncryptedCodeWithDefault: tt.fields.newEncryptedCodeWithDefault, + userEncryption: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), + defaultSecretGenerators: &SecretGenerators{ + PhoneVerificationCode: &crypto.GeneratorConfig{ + Length: 8, + Expiry: time.Hour, + IncludeLowerLetters: true, + IncludeUpperLetters: true, + IncludeDigits: true, + IncludeSymbols: true, + }, + }, } details, err := c.ChangeSchemaUserPhone(tt.args.ctx, tt.args.user) if tt.res.err == nil { @@ -463,6 +611,7 @@ func TestCommands_VerifySchemaUserPhone(t *testing.T) { }, time.Hour*1, false, + "", ), ), eventFromEventPusher( @@ -520,6 +669,7 @@ func TestCommands_VerifySchemaUserPhone(t *testing.T) { }, time.Hour*1, false, + "", ), ), ), @@ -571,6 +721,7 @@ func TestCommands_VerifySchemaUserPhone(t *testing.T) { }, time.Hour*1, false, + "", ), ), ), @@ -622,6 +773,7 @@ func TestCommands_VerifySchemaUserPhone(t *testing.T) { }, time.Hour*1, false, + "", ), ), ), @@ -671,9 +823,9 @@ func TestCommands_VerifySchemaUserPhone(t *testing.T) { func TestCommands_ResendSchemaUserPhoneCode(t *testing.T) { type fields struct { - eventstore func(t *testing.T) *eventstore.Eventstore - checkPermission domain.PermissionCheck - newCode encrypedCodeFunc + eventstore func(t *testing.T) *eventstore.Eventstore + checkPermission domain.PermissionCheck + newEncryptedCodeWithDefault encryptedCodeWithDefaultFunc } type args struct { ctx context.Context @@ -793,6 +945,7 @@ func TestCommands_ResendSchemaUserPhoneCode(t *testing.T) { }, time.Hour*1, false, + "", ), ), eventFromEventPusher( @@ -852,6 +1005,7 @@ func TestCommands_ResendSchemaUserPhoneCode(t *testing.T) { }, time.Hour*1, false, + "", ), ), ), @@ -905,6 +1059,37 @@ func TestCommands_ResendSchemaUserPhoneCode(t *testing.T) { }, time.Hour*1, false, + "", + ), + ), + ), + expectFilter( + eventFromEventPusher( + instance.NewSMSConfigActivatedEvent( + context.Background(), + &instance.NewAggregate("instanceID").Aggregate, + "id", + ), + ), + ), + expectFilter( + eventFromEventPusher( + instance.NewSMSConfigTwilioAddedEvent( + context.Background(), + &instance.NewAggregate("instanceID").Aggregate, + "id", + "", + "sid", + "senderNumber", + &crypto.CryptoValue{CryptoType: crypto.TypeEncryption, Algorithm: "enc", KeyID: "id", Crypted: []byte("crypted")}, + "", + ), + ), + eventFromEventPusher( + instance.NewSMSConfigActivatedEvent( + context.Background(), + &instance.NewAggregate("instanceID").Aggregate, + "id", ), ), ), @@ -921,12 +1106,104 @@ func TestCommands_ResendSchemaUserPhoneCode(t *testing.T) { }, time.Hour*1, false, + "", + ), + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + newEncryptedCodeWithDefault: mockEncryptedCodeWithDefault("phoneverify2", time.Hour), + }, + args{ + ctx: authz.NewMockContext("instanceID", "", ""), + user: &ResendSchemaUserPhoneCode{ + ID: "user1", + }, + }, + res{ + details: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + { + "phone code resend, ok (external)", + fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + schemauser.NewCreatedEvent( + context.Background(), + &schemauser.NewAggregate("user1", "org1").Aggregate, + "id1", + 1, + json.RawMessage(`{ + "name": "user1" + }`), + ), + ), + eventFromEventPusher( + schemauser.NewPhoneUpdatedEvent( + context.Background(), + &schemauser.NewAggregate("user1", "org1").Aggregate, + "+41791234567", + ), + ), + eventFromEventPusherWithCreationDateNow( + schemauser.NewPhoneCodeAddedEvent( + context.Background(), + &schemauser.NewAggregate("user1", "org1").Aggregate, + nil, + 0, + false, + "id", + ), + ), + ), + expectFilter( + eventFromEventPusher( + instance.NewSMSConfigActivatedEvent( + context.Background(), + &instance.NewAggregate("instanceID").Aggregate, + "id", + ), + ), + ), + expectFilter( + eventFromEventPusher( + instance.NewSMSConfigTwilioAddedEvent( + context.Background(), + &instance.NewAggregate("instanceID").Aggregate, + "id", + "", + "sid", + "senderNumber", + &crypto.CryptoValue{CryptoType: crypto.TypeEncryption, Algorithm: "enc", KeyID: "id", Crypted: []byte("crypted")}, + "verifyServiceSid", + ), + ), + eventFromEventPusher( + instance.NewSMSConfigActivatedEvent( + context.Background(), + &instance.NewAggregate("instanceID").Aggregate, + "id", + ), + ), + ), + expectPush( + eventFromEventPusher( + schemauser.NewPhoneCodeAddedEvent( + context.Background(), + &schemauser.NewAggregate("user1", "org1").Aggregate, + nil, + 0, + false, + "id", ), ), ), ), checkPermission: newMockPermissionCheckAllowed(), - newCode: mockEncryptedCode("phoneverify2", time.Hour), }, args{ ctx: authz.NewMockContext("instanceID", "", ""), @@ -975,6 +1252,37 @@ func TestCommands_ResendSchemaUserPhoneCode(t *testing.T) { }, time.Hour*1, false, + "", + ), + ), + ), + expectFilter( + eventFromEventPusher( + instance.NewSMSConfigActivatedEvent( + context.Background(), + &instance.NewAggregate("instanceID").Aggregate, + "id", + ), + ), + ), + expectFilter( + eventFromEventPusher( + instance.NewSMSConfigTwilioAddedEvent( + context.Background(), + &instance.NewAggregate("instanceID").Aggregate, + "id", + "", + "sid", + "senderNumber", + &crypto.CryptoValue{CryptoType: crypto.TypeEncryption, Algorithm: "enc", KeyID: "id", Crypted: []byte("crypted")}, + "", + ), + ), + eventFromEventPusher( + instance.NewSMSConfigActivatedEvent( + context.Background(), + &instance.NewAggregate("instanceID").Aggregate, + "id", ), ), ), @@ -991,12 +1299,13 @@ func TestCommands_ResendSchemaUserPhoneCode(t *testing.T) { }, time.Hour*1, true, + "", ), ), ), ), - checkPermission: newMockPermissionCheckAllowed(), - newCode: mockEncryptedCode("phoneverify2", time.Hour), + checkPermission: newMockPermissionCheckAllowed(), + newEncryptedCodeWithDefault: mockEncryptedCodeWithDefault("phoneverify2", time.Hour), }, args{ ctx: authz.NewMockContext("instanceID", "", ""), @@ -1016,10 +1325,20 @@ func TestCommands_ResendSchemaUserPhoneCode(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { c := &Commands{ - eventstore: tt.fields.eventstore(t), - checkPermission: tt.fields.checkPermission, - newEncryptedCode: tt.fields.newCode, - userEncryption: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), + eventstore: tt.fields.eventstore(t), + checkPermission: tt.fields.checkPermission, + newEncryptedCodeWithDefault: tt.fields.newEncryptedCodeWithDefault, + userEncryption: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), + defaultSecretGenerators: &SecretGenerators{ + PhoneVerificationCode: &crypto.GeneratorConfig{ + Length: 8, + Expiry: time.Hour, + IncludeLowerLetters: true, + IncludeUpperLetters: true, + IncludeDigits: true, + IncludeSymbols: true, + }, + }, } details, err := c.ResendSchemaUserPhoneCode(tt.args.ctx, tt.args.user) if tt.res.err == nil { diff --git a/internal/command/user_v3_test.go b/internal/command/user_v3_test.go index 4825626b1..34ac7bc1e 100644 --- a/internal/command/user_v3_test.go +++ b/internal/command/user_v3_test.go @@ -16,17 +16,30 @@ import ( "github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/id" "github.com/zitadel/zitadel/internal/id/mock" + "github.com/zitadel/zitadel/internal/repository/instance" "github.com/zitadel/zitadel/internal/repository/user/schema" "github.com/zitadel/zitadel/internal/repository/user/schemauser" "github.com/zitadel/zitadel/internal/zerrors" ) func TestCommands_CreateSchemaUser(t *testing.T) { + defaultGenerators := &SecretGenerators{ + OTPSMS: &crypto.GeneratorConfig{ + Length: 8, + Expiry: time.Hour, + IncludeLowerLetters: true, + IncludeUpperLetters: true, + IncludeDigits: true, + IncludeSymbols: true, + }, + } type fields struct { - eventstore func(t *testing.T) *eventstore.Eventstore - idGenerator id.Generator - checkPermission domain.PermissionCheck - newCode encrypedCodeFunc + eventstore func(t *testing.T) *eventstore.Eventstore + idGenerator id.Generator + checkPermission domain.PermissionCheck + newCode encrypedCodeFunc + newEncryptedCodeWithDefault encryptedCodeWithDefaultFunc + defaultSecretGenerators *SecretGenerators } type args struct { ctx context.Context @@ -663,6 +676,36 @@ func TestCommands_CreateSchemaUser(t *testing.T) { ), ), expectFilter(), + expectFilter( + eventFromEventPusher( + instance.NewSMSConfigActivatedEvent( + context.Background(), + &instance.NewAggregate("instanceID").Aggregate, + "id", + ), + ), + ), + expectFilter( + eventFromEventPusher( + instance.NewSMSConfigTwilioAddedEvent( + context.Background(), + &instance.NewAggregate("instanceID").Aggregate, + "id", + "", + "sid", + "senderNumber", + &crypto.CryptoValue{CryptoType: crypto.TypeEncryption, Algorithm: "enc", KeyID: "id", Crypted: []byte("crypted")}, + "", + ), + ), + eventFromEventPusher( + instance.NewSMSConfigActivatedEvent( + context.Background(), + &instance.NewAggregate("instanceID").Aggregate, + "id", + ), + ), + ), expectPush( schemauser.NewCreatedEvent( context.Background(), @@ -687,12 +730,14 @@ func TestCommands_CreateSchemaUser(t *testing.T) { }, time.Hour*1, true, + "", ), ), ), - idGenerator: mock.ExpectID(t, "id1"), - checkPermission: newMockPermissionCheckAllowed(), - newCode: mockEncryptedCode("phoneverify", time.Hour), + idGenerator: mock.ExpectID(t, "id1"), + checkPermission: newMockPermissionCheckAllowed(), + newEncryptedCodeWithDefault: mockEncryptedCodeWithDefault("phoneverify", time.Hour), + defaultSecretGenerators: defaultGenerators, }, args{ ctx: authz.NewMockContext("instanceID", "", ""), @@ -741,6 +786,36 @@ func TestCommands_CreateSchemaUser(t *testing.T) { ), ), expectFilter(), + expectFilter( + eventFromEventPusher( + instance.NewSMSConfigActivatedEvent( + context.Background(), + &instance.NewAggregate("instanceID").Aggregate, + "id", + ), + ), + ), + expectFilter( + eventFromEventPusher( + instance.NewSMSConfigTwilioAddedEvent( + context.Background(), + &instance.NewAggregate("instanceID").Aggregate, + "id", + "", + "sid", + "senderNumber", + &crypto.CryptoValue{CryptoType: crypto.TypeEncryption, Algorithm: "enc", KeyID: "id", Crypted: []byte("crypted")}, + "", + ), + ), + eventFromEventPusher( + instance.NewSMSConfigActivatedEvent( + context.Background(), + &instance.NewAggregate("instanceID").Aggregate, + "id", + ), + ), + ), expectPush( schemauser.NewCreatedEvent( context.Background(), @@ -765,12 +840,117 @@ func TestCommands_CreateSchemaUser(t *testing.T) { }, time.Hour*1, false, + "", ), ), ), - idGenerator: mock.ExpectID(t, "id1"), - checkPermission: newMockPermissionCheckAllowed(), - newCode: mockEncryptedCode("phoneverify", time.Hour), + idGenerator: mock.ExpectID(t, "id1"), + checkPermission: newMockPermissionCheckAllowed(), + newEncryptedCodeWithDefault: mockEncryptedCodeWithDefault("phoneverify", time.Hour), + defaultSecretGenerators: defaultGenerators, + }, + args{ + ctx: authz.NewMockContext("instanceID", "", ""), + user: &CreateSchemaUser{ + ResourceOwner: "org1", + SchemaID: "type", + schemaRevision: 1, + Data: json.RawMessage(`{ + "name": "user" + }`), + Phone: &Phone{ + Number: "+41791234567", + }, + }, + }, + res{ + details: &domain.ObjectDetails{ + ResourceOwner: "org1", + ID: "id1", + }, + }, + }, + { + "user created, phone to verify (external)", + fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + schema.NewCreatedEvent( + context.Background(), + &schema.NewAggregate("id1", "instanceID").Aggregate, + "type", + json.RawMessage(`{ + "$schema": "urn:zitadel:schema:v1", + "type": "object", + "properties": { + "name": { + "type": "string" + } + } + }`), + []domain.AuthenticatorType{domain.AuthenticatorTypeUsername}, + ), + ), + ), + expectFilter(), + expectFilter( + eventFromEventPusher( + instance.NewSMSConfigActivatedEvent( + context.Background(), + &instance.NewAggregate("instanceID").Aggregate, + "id", + ), + ), + ), + expectFilter( + eventFromEventPusher( + instance.NewSMSConfigTwilioAddedEvent( + context.Background(), + &instance.NewAggregate("instanceID").Aggregate, + "id", + "", + "sid", + "senderNumber", + &crypto.CryptoValue{CryptoType: crypto.TypeEncryption, Algorithm: "enc", KeyID: "id", Crypted: []byte("crypted")}, + "verifyServiceSid", + ), + ), + eventFromEventPusher( + instance.NewSMSConfigActivatedEvent( + context.Background(), + &instance.NewAggregate("instanceID").Aggregate, + "id", + ), + ), + ), + expectPush( + schemauser.NewCreatedEvent( + context.Background(), + &schemauser.NewAggregate("id1", "org1").Aggregate, + "type", + 1, + json.RawMessage(`{ + "name": "user" + }`), + ), + schemauser.NewPhoneUpdatedEvent(context.Background(), + &schemauser.NewAggregate("id1", "org1").Aggregate, + "+41791234567", + ), + schemauser.NewPhoneCodeAddedEvent(context.Background(), + &schemauser.NewAggregate("id1", "org1").Aggregate, + nil, + 0, + false, + "id", + ), + ), + ), + idGenerator: mock.ExpectID(t, "id1"), + checkPermission: newMockPermissionCheckAllowed(), + newEncryptedCodeWithDefault: mockEncryptedCodeWithDefault("phoneverify", time.Hour), + defaultSecretGenerators: defaultGenerators, }, args{ ctx: authz.NewMockContext("instanceID", "", ""), @@ -870,11 +1050,13 @@ func TestCommands_CreateSchemaUser(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { c := &Commands{ - eventstore: tt.fields.eventstore(t), - idGenerator: tt.fields.idGenerator, - checkPermission: tt.fields.checkPermission, - newEncryptedCode: tt.fields.newCode, - userEncryption: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), + eventstore: tt.fields.eventstore(t), + idGenerator: tt.fields.idGenerator, + checkPermission: tt.fields.checkPermission, + newEncryptedCode: tt.fields.newCode, + newEncryptedCodeWithDefault: tt.fields.newEncryptedCodeWithDefault, + defaultSecretGenerators: tt.fields.defaultSecretGenerators, + userEncryption: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), } details, err := c.CreateSchemaUser(tt.args.ctx, tt.args.user) if tt.res.err == nil { @@ -1949,10 +2131,22 @@ func TestCommandSide_ReactivateSchemaUser(t *testing.T) { } func TestCommands_ChangeSchemaUser(t *testing.T) { + defaultGenerators := &SecretGenerators{ + OTPSMS: &crypto.GeneratorConfig{ + Length: 8, + Expiry: time.Hour, + IncludeLowerLetters: true, + IncludeUpperLetters: true, + IncludeDigits: true, + IncludeSymbols: true, + }, + } type fields struct { - eventstore func(t *testing.T) *eventstore.Eventstore - checkPermission domain.PermissionCheck - newCode encrypedCodeFunc + eventstore func(t *testing.T) *eventstore.Eventstore + checkPermission domain.PermissionCheck + newCode encrypedCodeFunc + newEncryptedCodeWithDefault encryptedCodeWithDefaultFunc + defaultSecretGenerators *SecretGenerators } type args struct { ctx context.Context @@ -3016,6 +3210,36 @@ func TestCommands_ChangeSchemaUser(t *testing.T) { }`), ), ), + expectFilter( + eventFromEventPusher( + instance.NewSMSConfigActivatedEvent( + context.Background(), + &instance.NewAggregate("instanceID").Aggregate, + "id", + ), + ), + ), + expectFilter( + eventFromEventPusher( + instance.NewSMSConfigTwilioAddedEvent( + context.Background(), + &instance.NewAggregate("instanceID").Aggregate, + "id", + "", + "sid", + "senderNumber", + &crypto.CryptoValue{CryptoType: crypto.TypeEncryption, Algorithm: "enc", KeyID: "id", Crypted: []byte("crypted")}, + "", + ), + ), + eventFromEventPusher( + instance.NewSMSConfigActivatedEvent( + context.Background(), + &instance.NewAggregate("instanceID").Aggregate, + "id", + ), + ), + ), expectPush( schemauser.NewPhoneUpdatedEvent(context.Background(), &schemauser.NewAggregate("user1", "org1").Aggregate, @@ -3031,11 +3255,13 @@ func TestCommands_ChangeSchemaUser(t *testing.T) { }, time.Hour*1, true, + "", ), ), ), - checkPermission: newMockPermissionCheckAllowed(), - newCode: mockEncryptedCode("phoneverify", time.Hour), + checkPermission: newMockPermissionCheckAllowed(), + newEncryptedCodeWithDefault: mockEncryptedCodeWithDefault("phoneverify", time.Hour), + defaultSecretGenerators: defaultGenerators, }, args{ ctx: authz.NewMockContext("instanceID", "", ""), @@ -3069,6 +3295,36 @@ func TestCommands_ChangeSchemaUser(t *testing.T) { }`), ), ), + expectFilter( + eventFromEventPusher( + instance.NewSMSConfigActivatedEvent( + context.Background(), + &instance.NewAggregate("instanceID").Aggregate, + "id", + ), + ), + ), + expectFilter( + eventFromEventPusher( + instance.NewSMSConfigTwilioAddedEvent( + context.Background(), + &instance.NewAggregate("instanceID").Aggregate, + "id", + "", + "sid", + "senderNumber", + &crypto.CryptoValue{CryptoType: crypto.TypeEncryption, Algorithm: "enc", KeyID: "id", Crypted: []byte("crypted")}, + "", + ), + ), + eventFromEventPusher( + instance.NewSMSConfigActivatedEvent( + context.Background(), + &instance.NewAggregate("instanceID").Aggregate, + "id", + ), + ), + ), expectPush( schemauser.NewPhoneUpdatedEvent(context.Background(), &schemauser.NewAggregate("user1", "org1").Aggregate, @@ -3084,11 +3340,91 @@ func TestCommands_ChangeSchemaUser(t *testing.T) { }, time.Hour*1, false, + "", ), ), ), - checkPermission: newMockPermissionCheckAllowed(), - newCode: mockEncryptedCode("phoneverify", time.Hour), + checkPermission: newMockPermissionCheckAllowed(), + newEncryptedCodeWithDefault: mockEncryptedCodeWithDefault("phoneverify", time.Hour), + defaultSecretGenerators: defaultGenerators, + }, + args{ + ctx: authz.NewMockContext("instanceID", "", ""), + user: &ChangeSchemaUser{ + ID: "user1", + Phone: &Phone{ + Number: "+41791234567", + }, + }, + }, + res{ + details: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + { + "user updated, phone to verify (external)", + fields{ + eventstore: expectEventstore( + expectFilter( + schemauser.NewCreatedEvent( + context.Background(), + &schemauser.NewAggregate("id1", "org1").Aggregate, + "type", + 1, + json.RawMessage(`{ + "name": "user" + }`), + ), + ), + expectFilter( + eventFromEventPusher( + instance.NewSMSConfigActivatedEvent( + context.Background(), + &instance.NewAggregate("instanceID").Aggregate, + "id", + ), + ), + ), + expectFilter( + eventFromEventPusher( + instance.NewSMSConfigTwilioAddedEvent( + context.Background(), + &instance.NewAggregate("instanceID").Aggregate, + "id", + "", + "sid", + "senderNumber", + &crypto.CryptoValue{CryptoType: crypto.TypeEncryption, Algorithm: "enc", KeyID: "id", Crypted: []byte("crypted")}, + "verifyServiceSid", + ), + ), + eventFromEventPusher( + instance.NewSMSConfigActivatedEvent( + context.Background(), + &instance.NewAggregate("instanceID").Aggregate, + "id", + ), + ), + ), + expectPush( + schemauser.NewPhoneUpdatedEvent(context.Background(), + &schemauser.NewAggregate("user1", "org1").Aggregate, + "+41791234567", + ), + schemauser.NewPhoneCodeAddedEvent(context.Background(), + &schemauser.NewAggregate("user1", "org1").Aggregate, + nil, + 0, + false, + "id", + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + newEncryptedCodeWithDefault: mockEncryptedCodeWithDefault("phoneverify", time.Hour), + defaultSecretGenerators: defaultGenerators, }, args{ ctx: authz.NewMockContext("instanceID", "", ""), @@ -3157,10 +3493,12 @@ func TestCommands_ChangeSchemaUser(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { c := &Commands{ - eventstore: tt.fields.eventstore(t), - checkPermission: tt.fields.checkPermission, - newEncryptedCode: tt.fields.newCode, - userEncryption: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), + eventstore: tt.fields.eventstore(t), + checkPermission: tt.fields.checkPermission, + newEncryptedCode: tt.fields.newCode, + newEncryptedCodeWithDefault: tt.fields.newEncryptedCodeWithDefault, + defaultSecretGenerators: tt.fields.defaultSecretGenerators, + userEncryption: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), } details, err := c.ChangeSchemaUser(tt.args.ctx, tt.args.user) if tt.res.err == nil { diff --git a/internal/domain/human_phone.go b/internal/domain/human_phone.go index f350ea72a..2bf60bca8 100644 --- a/internal/domain/human_phone.go +++ b/internal/domain/human_phone.go @@ -54,17 +54,6 @@ func (p *Phone) Normalize() error { return nil } -func NewPhoneCode(phoneGenerator crypto.Generator) (*PhoneCode, error) { - phoneCodeCrypto, _, err := crypto.NewCode(phoneGenerator) - if err != nil { - return nil, err - } - return &PhoneCode{ - Code: phoneCodeCrypto, - Expiry: phoneGenerator.Expiry(), - }, nil -} - type PhoneState int32 const ( diff --git a/internal/notification/channels/twilio/channel.go b/internal/notification/channels/twilio/channel.go index da04b20f5..e3e2767a0 100644 --- a/internal/notification/channels/twilio/channel.go +++ b/internal/notification/channels/twilio/channel.go @@ -1,7 +1,9 @@ package twilio import ( - "github.com/kevinburke/twilio-go" + newTwilio "github.com/twilio/twilio-go" + openapi "github.com/twilio/twilio-go/rest/api/v2010" + verify "github.com/twilio/twilio-go/rest/verify/v2" "github.com/zitadel/logging" "github.com/zitadel/zitadel/internal/notification/channels" @@ -10,8 +12,7 @@ import ( ) func InitChannel(config Config) channels.NotificationChannel { - client := twilio.NewClient(config.SID, config.Token, nil) - + client := newTwilio.NewRestClientWithParams(newTwilio.ClientParams{Username: config.SID, Password: config.Token}) logging.Debug("successfully initialized twilio sms channel") return channels.HandleMessageFunc(func(message channels.Message) error { @@ -19,11 +20,30 @@ func InitChannel(config Config) channels.NotificationChannel { if !ok { return zerrors.ThrowInternal(nil, "TWILI-s0pLc", "message is not SMS") } + if config.VerifyServiceSID != "" { + params := &verify.CreateVerificationParams{} + params.SetTo(twilioMsg.RecipientPhoneNumber) + params.SetChannel("sms") + + resp, err := client.VerifyV2.CreateVerification(config.VerifyServiceSID, params) + if err != nil { + return zerrors.ThrowInternal(err, "TWILI-0s9f2", "could not send verification") + } + logging.WithFields("sid", resp.Sid, "status", resp.Status).Debug("verification sent") + + twilioMsg.VerificationID = resp.Sid + return nil + } + content, err := twilioMsg.GetContent() if err != nil { return err } - m, err := client.Messages.SendMessage(twilioMsg.SenderPhoneNumber, twilioMsg.RecipientPhoneNumber, content, nil) + params := &openapi.CreateMessageParams{} + params.SetTo(twilioMsg.RecipientPhoneNumber) + params.SetFrom(twilioMsg.SenderPhoneNumber) + params.SetBody(content) + m, err := client.Api.CreateMessage(params) if err != nil { return zerrors.ThrowInternal(err, "TWILI-osk3S", "could not send message") } diff --git a/internal/notification/channels/twilio/config.go b/internal/notification/channels/twilio/config.go index dc9550d76..02da0ad00 100644 --- a/internal/notification/channels/twilio/config.go +++ b/internal/notification/channels/twilio/config.go @@ -1,11 +1,40 @@ package twilio +import ( + newTwilio "github.com/twilio/twilio-go" + verify "github.com/twilio/twilio-go/rest/verify/v2" + + "github.com/zitadel/zitadel/internal/zerrors" +) + type Config struct { - SID string - Token string - SenderNumber string + SID string + Token string + SenderNumber string + VerifyServiceSID string } func (t *Config) IsValid() bool { return t.SID != "" && t.Token != "" && t.SenderNumber != "" } + +func (t *Config) VerifyCode(verificationID, code string) error { + client := newTwilio.NewRestClientWithParams(newTwilio.ClientParams{Username: t.SID, Password: t.Token}) + checkParams := &verify.CreateVerificationCheckParams{} + checkParams.SetVerificationSid(verificationID) + checkParams.SetCode(code) + resp, err := client.VerifyV2.CreateVerificationCheck(t.VerifyServiceSID, checkParams) + if err != nil || resp.Status == nil { + return zerrors.ThrowInvalidArgument(err, "TWILI-JK3ta", "Errors.User.Code.NotFound") + } + switch *resp.Status { + case "approved": + return nil + case "expired": + return zerrors.ThrowInvalidArgument(nil, "TWILI-SF3ba", "Errors.User.Code.Expired") + case "max_attempts_reached": + return zerrors.ThrowInvalidArgument(nil, "TWILI-Ok39a", "Errors.User.Code.NotFound") + default: + return zerrors.ThrowInvalidArgument(nil, "TWILI-Skwe4", "Errors.User.Code.Invalid") + } +} diff --git a/internal/notification/handlers/commands.go b/internal/notification/handlers/commands.go index 3d8546f80..a50abe18a 100644 --- a/internal/notification/handlers/commands.go +++ b/internal/notification/handlers/commands.go @@ -3,6 +3,7 @@ package handlers import ( "context" + "github.com/zitadel/zitadel/internal/notification/senders" "github.com/zitadel/zitadel/internal/repository/milestone" "github.com/zitadel/zitadel/internal/repository/quota" ) @@ -10,15 +11,15 @@ import ( type Commands interface { HumanInitCodeSent(ctx context.Context, orgID, userID string) error HumanEmailVerificationCodeSent(ctx context.Context, orgID, userID string) error - PasswordCodeSent(ctx context.Context, orgID, userID string) error - HumanOTPSMSCodeSent(ctx context.Context, userID, resourceOwner string) error + PasswordCodeSent(ctx context.Context, orgID, userID string, generatorInfo *senders.CodeGeneratorInfo) error + HumanOTPSMSCodeSent(ctx context.Context, userID, resourceOwner string, generatorInfo *senders.CodeGeneratorInfo) error HumanOTPEmailCodeSent(ctx context.Context, userID, resourceOwner string) error - OTPSMSSent(ctx context.Context, sessionID, resourceOwner string) error + OTPSMSSent(ctx context.Context, sessionID, resourceOwner string, generatorInfo *senders.CodeGeneratorInfo) error OTPEmailSent(ctx context.Context, sessionID, resourceOwner string) error UserDomainClaimedSent(ctx context.Context, orgID, userID string) error HumanPasswordlessInitCodeSent(ctx context.Context, userID, resourceOwner, codeID string) error PasswordChangeSent(ctx context.Context, orgID, userID string) error - HumanPhoneVerificationCodeSent(ctx context.Context, orgID, userID string) error + HumanPhoneVerificationCodeSent(ctx context.Context, orgID, userID string, generatorInfo *senders.CodeGeneratorInfo) error InviteCodeSent(ctx context.Context, orgID, userID string) error UsageNotificationSent(ctx context.Context, dueEvent *quota.NotificationDueEvent) error MilestonePushed(ctx context.Context, msType milestone.Type, endpoints []string, primaryDomain string) error diff --git a/internal/notification/handlers/config_sms.go b/internal/notification/handlers/config_sms.go index 4698772ea..1962824c9 100644 --- a/internal/notification/handlers/config_sms.go +++ b/internal/notification/handlers/config_sms.go @@ -31,9 +31,10 @@ func (n *NotificationQueries) GetActiveSMSConfig(ctx context.Context) (*sms.Conf return &sms.Config{ ProviderConfig: provider, TwilioConfig: &twilio.Config{ - SID: config.TwilioConfig.SID, - Token: token, - SenderNumber: config.TwilioConfig.SenderNumber, + SID: config.TwilioConfig.SID, + Token: token, + SenderNumber: config.TwilioConfig.SenderNumber, + VerifyServiceSID: config.TwilioConfig.VerifyServiceSID, }, }, nil } diff --git a/internal/notification/handlers/mock/commands.mock.go b/internal/notification/handlers/mock/commands.mock.go index ab94eda2c..51942be42 100644 --- a/internal/notification/handlers/mock/commands.mock.go +++ b/internal/notification/handlers/mock/commands.mock.go @@ -13,35 +13,36 @@ import ( context "context" reflect "reflect" + senders "github.com/zitadel/zitadel/internal/notification/senders" milestone "github.com/zitadel/zitadel/internal/repository/milestone" quota "github.com/zitadel/zitadel/internal/repository/quota" gomock "go.uber.org/mock/gomock" ) -// MockCommands is a mock of Commands interface +// MockCommands is a mock of Commands interface. type MockCommands struct { ctrl *gomock.Controller recorder *MockCommandsMockRecorder } -// MockCommandsMockRecorder is the mock recorder for MockCommands +// MockCommandsMockRecorder is the mock recorder for MockCommands. type MockCommandsMockRecorder struct { mock *MockCommands } -// NewMockCommands creates a new mock instance +// NewMockCommands creates a new mock instance. func NewMockCommands(ctrl *gomock.Controller) *MockCommands { mock := &MockCommands{ctrl: ctrl} mock.recorder = &MockCommandsMockRecorder{mock} return mock } -// EXPECT returns an object that allows the caller to indicate expected use +// EXPECT returns an object that allows the caller to indicate expected use. func (m *MockCommands) EXPECT() *MockCommandsMockRecorder { return m.recorder } -// HumanEmailVerificationCodeSent mocks base method +// HumanEmailVerificationCodeSent mocks base method. func (m *MockCommands) HumanEmailVerificationCodeSent(arg0 context.Context, arg1, arg2 string) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "HumanEmailVerificationCodeSent", arg0, arg1, arg2) @@ -49,13 +50,13 @@ func (m *MockCommands) HumanEmailVerificationCodeSent(arg0 context.Context, arg1 return ret0 } -// HumanEmailVerificationCodeSent indicates an expected call of HumanEmailVerificationCodeSent -func (mr *MockCommandsMockRecorder) HumanEmailVerificationCodeSent(arg0, arg1, arg2 interface{}) *gomock.Call { +// HumanEmailVerificationCodeSent indicates an expected call of HumanEmailVerificationCodeSent. +func (mr *MockCommandsMockRecorder) HumanEmailVerificationCodeSent(arg0, arg1, arg2 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HumanEmailVerificationCodeSent", reflect.TypeOf((*MockCommands)(nil).HumanEmailVerificationCodeSent), arg0, arg1, arg2) } -// HumanInitCodeSent mocks base method +// HumanInitCodeSent mocks base method. func (m *MockCommands) HumanInitCodeSent(arg0 context.Context, arg1, arg2 string) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "HumanInitCodeSent", arg0, arg1, arg2) @@ -63,13 +64,13 @@ func (m *MockCommands) HumanInitCodeSent(arg0 context.Context, arg1, arg2 string return ret0 } -// HumanInitCodeSent indicates an expected call of HumanInitCodeSent -func (mr *MockCommandsMockRecorder) HumanInitCodeSent(arg0, arg1, arg2 interface{}) *gomock.Call { +// HumanInitCodeSent indicates an expected call of HumanInitCodeSent. +func (mr *MockCommandsMockRecorder) HumanInitCodeSent(arg0, arg1, arg2 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HumanInitCodeSent", reflect.TypeOf((*MockCommands)(nil).HumanInitCodeSent), arg0, arg1, arg2) } -// HumanOTPEmailCodeSent mocks base method +// HumanOTPEmailCodeSent mocks base method. func (m *MockCommands) HumanOTPEmailCodeSent(arg0 context.Context, arg1, arg2 string) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "HumanOTPEmailCodeSent", arg0, arg1, arg2) @@ -77,27 +78,27 @@ func (m *MockCommands) HumanOTPEmailCodeSent(arg0 context.Context, arg1, arg2 st return ret0 } -// HumanOTPEmailCodeSent indicates an expected call of HumanOTPEmailCodeSent -func (mr *MockCommandsMockRecorder) HumanOTPEmailCodeSent(arg0, arg1, arg2 interface{}) *gomock.Call { +// HumanOTPEmailCodeSent indicates an expected call of HumanOTPEmailCodeSent. +func (mr *MockCommandsMockRecorder) HumanOTPEmailCodeSent(arg0, arg1, arg2 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HumanOTPEmailCodeSent", reflect.TypeOf((*MockCommands)(nil).HumanOTPEmailCodeSent), arg0, arg1, arg2) } -// HumanOTPSMSCodeSent mocks base method -func (m *MockCommands) HumanOTPSMSCodeSent(arg0 context.Context, arg1, arg2 string) error { +// HumanOTPSMSCodeSent mocks base method. +func (m *MockCommands) HumanOTPSMSCodeSent(arg0 context.Context, arg1, arg2 string, arg3 *senders.CodeGeneratorInfo) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "HumanOTPSMSCodeSent", arg0, arg1, arg2) + ret := m.ctrl.Call(m, "HumanOTPSMSCodeSent", arg0, arg1, arg2, arg3) ret0, _ := ret[0].(error) return ret0 } -// HumanOTPSMSCodeSent indicates an expected call of HumanOTPSMSCodeSent -func (mr *MockCommandsMockRecorder) HumanOTPSMSCodeSent(arg0, arg1, arg2 interface{}) *gomock.Call { +// HumanOTPSMSCodeSent indicates an expected call of HumanOTPSMSCodeSent. +func (mr *MockCommandsMockRecorder) HumanOTPSMSCodeSent(arg0, arg1, arg2, arg3 any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HumanOTPSMSCodeSent", reflect.TypeOf((*MockCommands)(nil).HumanOTPSMSCodeSent), arg0, arg1, arg2) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HumanOTPSMSCodeSent", reflect.TypeOf((*MockCommands)(nil).HumanOTPSMSCodeSent), arg0, arg1, arg2, arg3) } -// HumanPasswordlessInitCodeSent mocks base method +// HumanPasswordlessInitCodeSent mocks base method. func (m *MockCommands) HumanPasswordlessInitCodeSent(arg0 context.Context, arg1, arg2, arg3 string) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "HumanPasswordlessInitCodeSent", arg0, arg1, arg2, arg3) @@ -105,27 +106,27 @@ func (m *MockCommands) HumanPasswordlessInitCodeSent(arg0 context.Context, arg1, return ret0 } -// HumanPasswordlessInitCodeSent indicates an expected call of HumanPasswordlessInitCodeSent -func (mr *MockCommandsMockRecorder) HumanPasswordlessInitCodeSent(arg0, arg1, arg2, arg3 interface{}) *gomock.Call { +// HumanPasswordlessInitCodeSent indicates an expected call of HumanPasswordlessInitCodeSent. +func (mr *MockCommandsMockRecorder) HumanPasswordlessInitCodeSent(arg0, arg1, arg2, arg3 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HumanPasswordlessInitCodeSent", reflect.TypeOf((*MockCommands)(nil).HumanPasswordlessInitCodeSent), arg0, arg1, arg2, arg3) } -// HumanPhoneVerificationCodeSent mocks base method -func (m *MockCommands) HumanPhoneVerificationCodeSent(arg0 context.Context, arg1, arg2 string) error { +// HumanPhoneVerificationCodeSent mocks base method. +func (m *MockCommands) HumanPhoneVerificationCodeSent(arg0 context.Context, arg1, arg2 string, arg3 *senders.CodeGeneratorInfo) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "HumanPhoneVerificationCodeSent", arg0, arg1, arg2) + ret := m.ctrl.Call(m, "HumanPhoneVerificationCodeSent", arg0, arg1, arg2, arg3) ret0, _ := ret[0].(error) return ret0 } -// HumanPhoneVerificationCodeSent indicates an expected call of HumanPhoneVerificationCodeSent -func (mr *MockCommandsMockRecorder) HumanPhoneVerificationCodeSent(arg0, arg1, arg2 interface{}) *gomock.Call { +// HumanPhoneVerificationCodeSent indicates an expected call of HumanPhoneVerificationCodeSent. +func (mr *MockCommandsMockRecorder) HumanPhoneVerificationCodeSent(arg0, arg1, arg2, arg3 any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HumanPhoneVerificationCodeSent", reflect.TypeOf((*MockCommands)(nil).HumanPhoneVerificationCodeSent), arg0, arg1, arg2) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HumanPhoneVerificationCodeSent", reflect.TypeOf((*MockCommands)(nil).HumanPhoneVerificationCodeSent), arg0, arg1, arg2, arg3) } -// InviteCodeSent mocks base method +// InviteCodeSent mocks base method. func (m *MockCommands) InviteCodeSent(arg0 context.Context, arg1, arg2 string) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "InviteCodeSent", arg0, arg1, arg2) @@ -133,13 +134,13 @@ func (m *MockCommands) InviteCodeSent(arg0 context.Context, arg1, arg2 string) e return ret0 } -// InviteCodeSent indicates an expected call of InviteCodeSent -func (mr *MockCommandsMockRecorder) InviteCodeSent(arg0, arg1, arg2 interface{}) *gomock.Call { +// InviteCodeSent indicates an expected call of InviteCodeSent. +func (mr *MockCommandsMockRecorder) InviteCodeSent(arg0, arg1, arg2 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InviteCodeSent", reflect.TypeOf((*MockCommands)(nil).InviteCodeSent), arg0, arg1, arg2) } -// MilestonePushed mocks base method +// MilestonePushed mocks base method. func (m *MockCommands) MilestonePushed(arg0 context.Context, arg1 milestone.Type, arg2 []string, arg3 string) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "MilestonePushed", arg0, arg1, arg2, arg3) @@ -147,13 +148,13 @@ func (m *MockCommands) MilestonePushed(arg0 context.Context, arg1 milestone.Type return ret0 } -// MilestonePushed indicates an expected call of MilestonePushed -func (mr *MockCommandsMockRecorder) MilestonePushed(arg0, arg1, arg2, arg3 interface{}) *gomock.Call { +// MilestonePushed indicates an expected call of MilestonePushed. +func (mr *MockCommandsMockRecorder) MilestonePushed(arg0, arg1, arg2, arg3 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MilestonePushed", reflect.TypeOf((*MockCommands)(nil).MilestonePushed), arg0, arg1, arg2, arg3) } -// OTPEmailSent mocks base method +// OTPEmailSent mocks base method. func (m *MockCommands) OTPEmailSent(arg0 context.Context, arg1, arg2 string) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "OTPEmailSent", arg0, arg1, arg2) @@ -161,27 +162,27 @@ func (m *MockCommands) OTPEmailSent(arg0 context.Context, arg1, arg2 string) err return ret0 } -// OTPEmailSent indicates an expected call of OTPEmailSent -func (mr *MockCommandsMockRecorder) OTPEmailSent(arg0, arg1, arg2 interface{}) *gomock.Call { +// OTPEmailSent indicates an expected call of OTPEmailSent. +func (mr *MockCommandsMockRecorder) OTPEmailSent(arg0, arg1, arg2 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "OTPEmailSent", reflect.TypeOf((*MockCommands)(nil).OTPEmailSent), arg0, arg1, arg2) } -// OTPSMSSent mocks base method -func (m *MockCommands) OTPSMSSent(arg0 context.Context, arg1, arg2 string) error { +// OTPSMSSent mocks base method. +func (m *MockCommands) OTPSMSSent(arg0 context.Context, arg1, arg2 string, arg3 *senders.CodeGeneratorInfo) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "OTPSMSSent", arg0, arg1, arg2) + ret := m.ctrl.Call(m, "OTPSMSSent", arg0, arg1, arg2, arg3) ret0, _ := ret[0].(error) return ret0 } -// OTPSMSSent indicates an expected call of OTPSMSSent -func (mr *MockCommandsMockRecorder) OTPSMSSent(arg0, arg1, arg2 interface{}) *gomock.Call { +// OTPSMSSent indicates an expected call of OTPSMSSent. +func (mr *MockCommandsMockRecorder) OTPSMSSent(arg0, arg1, arg2, arg3 any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "OTPSMSSent", reflect.TypeOf((*MockCommands)(nil).OTPSMSSent), arg0, arg1, arg2) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "OTPSMSSent", reflect.TypeOf((*MockCommands)(nil).OTPSMSSent), arg0, arg1, arg2, arg3) } -// PasswordChangeSent mocks base method +// PasswordChangeSent mocks base method. func (m *MockCommands) PasswordChangeSent(arg0 context.Context, arg1, arg2 string) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "PasswordChangeSent", arg0, arg1, arg2) @@ -189,27 +190,27 @@ func (m *MockCommands) PasswordChangeSent(arg0 context.Context, arg1, arg2 strin return ret0 } -// PasswordChangeSent indicates an expected call of PasswordChangeSent -func (mr *MockCommandsMockRecorder) PasswordChangeSent(arg0, arg1, arg2 interface{}) *gomock.Call { +// PasswordChangeSent indicates an expected call of PasswordChangeSent. +func (mr *MockCommandsMockRecorder) PasswordChangeSent(arg0, arg1, arg2 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PasswordChangeSent", reflect.TypeOf((*MockCommands)(nil).PasswordChangeSent), arg0, arg1, arg2) } -// PasswordCodeSent mocks base method -func (m *MockCommands) PasswordCodeSent(arg0 context.Context, arg1, arg2 string) error { +// PasswordCodeSent mocks base method. +func (m *MockCommands) PasswordCodeSent(arg0 context.Context, arg1, arg2 string, arg3 *senders.CodeGeneratorInfo) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "PasswordCodeSent", arg0, arg1, arg2) + ret := m.ctrl.Call(m, "PasswordCodeSent", arg0, arg1, arg2, arg3) ret0, _ := ret[0].(error) return ret0 } -// PasswordCodeSent indicates an expected call of PasswordCodeSent -func (mr *MockCommandsMockRecorder) PasswordCodeSent(arg0, arg1, arg2 interface{}) *gomock.Call { +// PasswordCodeSent indicates an expected call of PasswordCodeSent. +func (mr *MockCommandsMockRecorder) PasswordCodeSent(arg0, arg1, arg2, arg3 any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PasswordCodeSent", reflect.TypeOf((*MockCommands)(nil).PasswordCodeSent), arg0, arg1, arg2) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PasswordCodeSent", reflect.TypeOf((*MockCommands)(nil).PasswordCodeSent), arg0, arg1, arg2, arg3) } -// UsageNotificationSent mocks base method +// UsageNotificationSent mocks base method. func (m *MockCommands) UsageNotificationSent(arg0 context.Context, arg1 *quota.NotificationDueEvent) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "UsageNotificationSent", arg0, arg1) @@ -217,13 +218,13 @@ func (m *MockCommands) UsageNotificationSent(arg0 context.Context, arg1 *quota.N return ret0 } -// UsageNotificationSent indicates an expected call of UsageNotificationSent -func (mr *MockCommandsMockRecorder) UsageNotificationSent(arg0, arg1 interface{}) *gomock.Call { +// UsageNotificationSent indicates an expected call of UsageNotificationSent. +func (mr *MockCommandsMockRecorder) UsageNotificationSent(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UsageNotificationSent", reflect.TypeOf((*MockCommands)(nil).UsageNotificationSent), arg0, arg1) } -// UserDomainClaimedSent mocks base method +// UserDomainClaimedSent mocks base method. func (m *MockCommands) UserDomainClaimedSent(arg0 context.Context, arg1, arg2 string) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "UserDomainClaimedSent", arg0, arg1, arg2) @@ -231,8 +232,8 @@ func (m *MockCommands) UserDomainClaimedSent(arg0 context.Context, arg1, arg2 st return ret0 } -// UserDomainClaimedSent indicates an expected call of UserDomainClaimedSent -func (mr *MockCommandsMockRecorder) UserDomainClaimedSent(arg0, arg1, arg2 interface{}) *gomock.Call { +// UserDomainClaimedSent indicates an expected call of UserDomainClaimedSent. +func (mr *MockCommandsMockRecorder) UserDomainClaimedSent(arg0, arg1, arg2 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UserDomainClaimedSent", reflect.TypeOf((*MockCommands)(nil).UserDomainClaimedSent), arg0, arg1, arg2) } diff --git a/internal/notification/handlers/user_notifier.go b/internal/notification/handlers/user_notifier.go index 57aa3a925..799a006ab 100644 --- a/internal/notification/handlers/user_notifier.go +++ b/internal/notification/handlers/user_notifier.go @@ -11,6 +11,7 @@ import ( "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/eventstore/handler/v2" + "github.com/zitadel/zitadel/internal/notification/senders" "github.com/zitadel/zitadel/internal/notification/types" "github.com/zitadel/zitadel/internal/query" "github.com/zitadel/zitadel/internal/repository/session" @@ -258,9 +259,12 @@ func (u *userNotifier) reducePasswordCodeAdded(event eventstore.Event) (*handler if alreadyHandled { return nil } - code, err := crypto.DecryptString(e.Code, u.queries.UserDataCrypto) - if err != nil { - return err + var code string + if e.Code != nil { + code, err = crypto.DecryptString(e.Code, u.queries.UserDataCrypto) + if err != nil { + return err + } } colors, err := u.queries.ActiveLabelPolicyByOrg(ctx, e.Aggregate().ResourceOwner, false) if err != nil { @@ -285,15 +289,16 @@ func (u *userNotifier) reducePasswordCodeAdded(event eventstore.Event) (*handler if err != nil { return err } + generatorInfo := new(senders.CodeGeneratorInfo) notify := types.SendEmail(ctx, u.channels, string(template.Template), translator, notifyUser, colors, e) if e.NotificationType == domain.NotificationTypeSms { - notify = types.SendSMS(ctx, u.channels, translator, notifyUser, colors, e) + notify = types.SendSMS(ctx, u.channels, translator, notifyUser, colors, e, generatorInfo) } err = notify.SendPasswordCode(ctx, notifyUser, code, e.URLTemplate, e.AuthRequestID) if err != nil { return err } - return u.commands.PasswordCodeSent(ctx, e.Aggregate().ResourceOwner, e.Aggregate().ID) + return u.commands.PasswordCodeSent(ctx, e.Aggregate().ResourceOwner, e.Aggregate().ID, generatorInfo) }), nil } @@ -345,7 +350,7 @@ func (u *userNotifier) reduceOTPSMS( expiry time.Duration, userID, resourceOwner string, - sentCommand func(ctx context.Context, userID string, resourceOwner string) (err error), + sentCommand func(ctx context.Context, userID, resourceOwner string, generatorInfo *senders.CodeGeneratorInfo) (err error), eventTypes ...eventstore.EventType, ) (*handler.Statement, error) { ctx := HandlerContext(event.Aggregate()) @@ -356,9 +361,12 @@ func (u *userNotifier) reduceOTPSMS( if alreadyHandled { return handler.NewNoOpStatement(event), nil } - plainCode, err := crypto.DecryptString(code, u.queries.UserDataCrypto) - if err != nil { - return nil, err + var plainCode string + if code != nil { + plainCode, err = crypto.DecryptString(code, u.queries.UserDataCrypto) + if err != nil { + return nil, err + } } colors, err := u.queries.ActiveLabelPolicyByOrg(ctx, resourceOwner, false) if err != nil { @@ -377,12 +385,13 @@ func (u *userNotifier) reduceOTPSMS( if err != nil { return nil, err } - notify := types.SendSMS(ctx, u.channels, translator, notifyUser, colors, event) + generatorInfo := new(senders.CodeGeneratorInfo) + notify := types.SendSMS(ctx, u.channels, translator, notifyUser, colors, event, generatorInfo) err = notify.SendOTPSMSCode(ctx, plainCode, expiry) if err != nil { return nil, err } - err = sentCommand(ctx, event.Aggregate().ID, event.Aggregate().ResourceOwner) + err = sentCommand(ctx, event.Aggregate().ID, event.Aggregate().ResourceOwner, generatorInfo) if err != nil { return nil, err } @@ -691,9 +700,12 @@ func (u *userNotifier) reducePhoneCodeAdded(event eventstore.Event) (*handler.St if alreadyHandled { return nil } - code, err := crypto.DecryptString(e.Code, u.queries.UserDataCrypto) - if err != nil { - return err + var code string + if e.Code != nil { + code, err = crypto.DecryptString(e.Code, u.queries.UserDataCrypto) + if err != nil { + return err + } } colors, err := u.queries.ActiveLabelPolicyByOrg(ctx, e.Aggregate().ResourceOwner, false) if err != nil { @@ -713,12 +725,12 @@ func (u *userNotifier) reducePhoneCodeAdded(event eventstore.Event) (*handler.St if err != nil { return err } - err = types.SendSMS(ctx, u.channels, translator, notifyUser, colors, e). - SendPhoneVerificationCode(ctx, code) - if err != nil { + generatorInfo := new(senders.CodeGeneratorInfo) + if err = types.SendSMS(ctx, u.channels, translator, notifyUser, colors, e, generatorInfo). + SendPhoneVerificationCode(ctx, code); err != nil { return err } - return u.commands.HumanPhoneVerificationCodeSent(ctx, e.Aggregate().ResourceOwner, e.Aggregate().ID) + return u.commands.HumanPhoneVerificationCodeSent(ctx, e.Aggregate().ResourceOwner, e.Aggregate().ID, generatorInfo) }), nil } @@ -778,7 +790,7 @@ func (u *userNotifier) reduceInviteCodeAdded(event eventstore.Event) (*handler.S } func (u *userNotifier) checkIfCodeAlreadyHandledOrExpired(ctx context.Context, event eventstore.Event, expiry time.Duration, data map[string]interface{}, eventTypes ...eventstore.EventType) (bool, error) { - if event.CreatedAt().Add(expiry).Before(time.Now().UTC()) { + if expiry > 0 && event.CreatedAt().Add(expiry).Before(time.Now().UTC()) { return true, nil } return u.queries.IsAlreadyHandled(ctx, event, data, eventTypes...) diff --git a/internal/notification/handlers/user_notifier_test.go b/internal/notification/handlers/user_notifier_test.go index c18ddc1df..d0151b6d7 100644 --- a/internal/notification/handlers/user_notifier_test.go +++ b/internal/notification/handlers/user_notifier_test.go @@ -7,11 +7,13 @@ import ( "testing" "time" + "github.com/muhlemmer/gu" "github.com/stretchr/testify/assert" "go.uber.org/mock/gomock" "golang.org/x/text/language" "github.com/zitadel/zitadel/internal/crypto" + "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/eventstore/repository" es_repo_mock "github.com/zitadel/zitadel/internal/eventstore/repository/mock" @@ -19,6 +21,7 @@ import ( channel_mock "github.com/zitadel/zitadel/internal/notification/channels/mock" "github.com/zitadel/zitadel/internal/notification/channels/sms" "github.com/zitadel/zitadel/internal/notification/channels/smtp" + "github.com/zitadel/zitadel/internal/notification/channels/twilio" "github.com/zitadel/zitadel/internal/notification/channels/webhook" "github.com/zitadel/zitadel/internal/notification/handlers/mock" "github.com/zitadel/zitadel/internal/notification/messages" @@ -36,10 +39,13 @@ const ( codeID = "event1" logoURL = "logo.png" eventOrigin = "https://triggered.here" + eventOriginDomain = "triggered.here" assetsPath = "/assets/v1" preferredLoginName = "loginName1" lastEmail = "last@email.com" verifiedEmail = "verified@email.com" + lastPhone = "+41797654321" + verifiedPhone = "+41791234567" instancePrimaryDomain = "primary.domain" externalDomain = "external.domain" externalPort = 3000 @@ -47,6 +53,9 @@ const ( externalProtocol = "http" defaultOTPEmailTemplate = "/otp/verify?loginName={{.LoginName}}&code={{.Code}}" authRequestID = "authRequestID" + smsProviderID = "smsProviderID" + emailProviderID = "emailProviderID" + verificationID = "verificationID" ) func Test_userNotifier_reduceInitCodeAdded(t *testing.T) { @@ -59,7 +68,7 @@ func Test_userNotifier_reduceInitCodeAdded(t *testing.T) { test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w want) { givenTemplate := "{{.LogoURL}}" expectContent := fmt.Sprintf("%s%s/%s/%s", eventOrigin, assetsPath, policyID, logoURL) - w.message = messages.Email{ + w.message = &messages.Email{ Recipients: []string{lastEmail}, Subject: expectMailSubject, Content: expectContent, @@ -92,7 +101,7 @@ func Test_userNotifier_reduceInitCodeAdded(t *testing.T) { test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w want) { givenTemplate := "{{.LogoURL}}" expectContent := fmt.Sprintf("%s://%s:%d%s/%s/%s", externalProtocol, instancePrimaryDomain, externalPort, assetsPath, policyID, logoURL) - w.message = messages.Email{ + w.message = &messages.Email{ Recipients: []string{lastEmail}, Subject: expectMailSubject, Content: expectContent, @@ -131,7 +140,7 @@ func Test_userNotifier_reduceInitCodeAdded(t *testing.T) { givenTemplate := "{{.URL}}" testCode := "testcode" expectContent := fmt.Sprintf("%s/ui/login/user/init?authRequestID=%s&code=%s&loginname=%s&orgID=%s&passwordset=%t&userID=%s", eventOrigin, "", testCode, preferredLoginName, orgID, false, userID) - w.message = messages.Email{ + w.message = &messages.Email{ Recipients: []string{lastEmail}, Subject: expectMailSubject, Content: expectContent, @@ -165,7 +174,7 @@ func Test_userNotifier_reduceInitCodeAdded(t *testing.T) { givenTemplate := "{{.URL}}" testCode := "testcode" expectContent := fmt.Sprintf("%s://%s:%d/ui/login/user/init?authRequestID=%s&code=%s&loginname=%s&orgID=%s&passwordset=%t&userID=%s", externalProtocol, instancePrimaryDomain, externalPort, "", testCode, preferredLoginName, orgID, false, userID) - w.message = messages.Email{ + w.message = &messages.Email{ Recipients: []string{lastEmail}, Subject: expectMailSubject, Content: expectContent, @@ -204,7 +213,7 @@ func Test_userNotifier_reduceInitCodeAdded(t *testing.T) { givenTemplate := "{{.URL}}" testCode := "testcode" expectContent := fmt.Sprintf("%s://%s:%d/ui/login/user/init?authRequestID=%s&code=%s&loginname=%s&orgID=%s&passwordset=%t&userID=%s", externalProtocol, instancePrimaryDomain, externalPort, authRequestID, testCode, preferredLoginName, orgID, false, userID) - w.message = messages.Email{ + w.message = &messages.Email{ Recipients: []string{lastEmail}, Subject: expectMailSubject, Content: expectContent, @@ -272,7 +281,7 @@ func Test_userNotifier_reduceEmailCodeAdded(t *testing.T) { test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w want) { givenTemplate := "{{.LogoURL}}" expectContent := fmt.Sprintf("%s%s/%s/%s", eventOrigin, assetsPath, policyID, logoURL) - w.message = messages.Email{ + w.message = &messages.Email{ Recipients: []string{lastEmail}, Subject: expectMailSubject, Content: expectContent, @@ -307,7 +316,7 @@ func Test_userNotifier_reduceEmailCodeAdded(t *testing.T) { test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w want) { givenTemplate := "{{.LogoURL}}" expectContent := fmt.Sprintf("%s://%s:%d%s/%s/%s", externalProtocol, instancePrimaryDomain, externalPort, assetsPath, policyID, logoURL) - w.message = messages.Email{ + w.message = &messages.Email{ Recipients: []string{lastEmail}, Subject: expectMailSubject, Content: expectContent, @@ -348,7 +357,7 @@ func Test_userNotifier_reduceEmailCodeAdded(t *testing.T) { givenTemplate := "{{.URL}}" testCode := "testcode" expectContent := fmt.Sprintf("%s/ui/login/mail/verification?authRequestID=%s&code=%s&orgID=%s&userID=%s", eventOrigin, "", testCode, orgID, userID) - w.message = messages.Email{ + w.message = &messages.Email{ Recipients: []string{lastEmail}, Subject: expectMailSubject, Content: expectContent, @@ -385,7 +394,7 @@ func Test_userNotifier_reduceEmailCodeAdded(t *testing.T) { givenTemplate := "{{.URL}}" testCode := "testcode" expectContent := fmt.Sprintf("%s://%s:%d/ui/login/mail/verification?authRequestID=%s&code=%s&orgID=%s&userID=%s", externalProtocol, instancePrimaryDomain, externalPort, "", testCode, orgID, userID) - w.message = messages.Email{ + w.message = &messages.Email{ Recipients: []string{lastEmail}, Subject: expectMailSubject, Content: expectContent, @@ -426,7 +435,7 @@ func Test_userNotifier_reduceEmailCodeAdded(t *testing.T) { givenTemplate := "{{.URL}}" testCode := "testcode" expectContent := fmt.Sprintf("%s://%s:%d/ui/login/mail/verification?authRequestID=%s&code=%s&orgID=%s&userID=%s", externalProtocol, instancePrimaryDomain, externalPort, authRequestID, testCode, orgID, userID) - w.message = messages.Email{ + w.message = &messages.Email{ Recipients: []string{lastEmail}, Subject: expectMailSubject, Content: expectContent, @@ -469,7 +478,7 @@ func Test_userNotifier_reduceEmailCodeAdded(t *testing.T) { urlTemplate := "https://my.custom.url/org/{{.OrgID}}/user/{{.UserID}}/verify/{{.Code}}" testCode := "testcode" expectContent := fmt.Sprintf("https://my.custom.url/org/%s/user/%s/verify/%s", orgID, userID, testCode) - w.message = messages.Email{ + w.message = &messages.Email{ Recipients: []string{lastEmail}, Subject: expectMailSubject, Content: expectContent, @@ -533,14 +542,14 @@ func Test_userNotifier_reducePasswordCodeAdded(t *testing.T) { test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w want) { givenTemplate := "{{.LogoURL}}" expectContent := fmt.Sprintf("%s%s/%s/%s", eventOrigin, assetsPath, policyID, logoURL) - w.message = messages.Email{ + w.message = &messages.Email{ Recipients: []string{lastEmail}, Subject: expectMailSubject, Content: expectContent, } codeAlg, code := cryptoValue(t, ctrl, "testcode") expectTemplateQueries(queries, givenTemplate) - commands.EXPECT().PasswordCodeSent(gomock.Any(), orgID, userID).Return(nil) + commands.EXPECT().PasswordCodeSent(gomock.Any(), orgID, userID, &senders.CodeGeneratorInfo{}).Return(nil) return fields{ queries: queries, commands: commands, @@ -568,7 +577,7 @@ func Test_userNotifier_reducePasswordCodeAdded(t *testing.T) { test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w want) { givenTemplate := "{{.LogoURL}}" expectContent := fmt.Sprintf("%s://%s:%d%s/%s/%s", externalProtocol, instancePrimaryDomain, externalPort, assetsPath, policyID, logoURL) - w.message = messages.Email{ + w.message = &messages.Email{ Recipients: []string{lastEmail}, Subject: expectMailSubject, Content: expectContent, @@ -581,7 +590,7 @@ func Test_userNotifier_reducePasswordCodeAdded(t *testing.T) { }}, }, nil) expectTemplateQueries(queries, givenTemplate) - commands.EXPECT().PasswordCodeSent(gomock.Any(), orgID, userID).Return(nil) + commands.EXPECT().PasswordCodeSent(gomock.Any(), orgID, userID, &senders.CodeGeneratorInfo{}).Return(nil) return fields{ queries: queries, commands: commands, @@ -609,14 +618,14 @@ func Test_userNotifier_reducePasswordCodeAdded(t *testing.T) { givenTemplate := "{{.URL}}" testCode := "testcode" expectContent := fmt.Sprintf("%s/ui/login/password/init?authRequestID=%s&code=%s&orgID=%s&userID=%s", eventOrigin, "", testCode, orgID, userID) - w.message = messages.Email{ + w.message = &messages.Email{ Recipients: []string{lastEmail}, Subject: expectMailSubject, Content: expectContent, } codeAlg, code := cryptoValue(t, ctrl, testCode) expectTemplateQueries(queries, givenTemplate) - commands.EXPECT().PasswordCodeSent(gomock.Any(), orgID, userID).Return(nil) + commands.EXPECT().PasswordCodeSent(gomock.Any(), orgID, userID, &senders.CodeGeneratorInfo{}).Return(nil) return fields{ queries: queries, commands: commands, @@ -646,7 +655,7 @@ func Test_userNotifier_reducePasswordCodeAdded(t *testing.T) { givenTemplate := "{{.URL}}" testCode := "testcode" expectContent := fmt.Sprintf("%s://%s:%d/ui/login/password/init?authRequestID=%s&code=%s&orgID=%s&userID=%s", externalProtocol, instancePrimaryDomain, externalPort, "", testCode, orgID, userID) - w.message = messages.Email{ + w.message = &messages.Email{ Recipients: []string{lastEmail}, Subject: expectMailSubject, Content: expectContent, @@ -659,7 +668,7 @@ func Test_userNotifier_reducePasswordCodeAdded(t *testing.T) { }}, }, nil) expectTemplateQueries(queries, givenTemplate) - commands.EXPECT().PasswordCodeSent(gomock.Any(), orgID, userID).Return(nil) + commands.EXPECT().PasswordCodeSent(gomock.Any(), orgID, userID, &senders.CodeGeneratorInfo{}).Return(nil) return fields{ queries: queries, commands: commands, @@ -687,7 +696,7 @@ func Test_userNotifier_reducePasswordCodeAdded(t *testing.T) { givenTemplate := "{{.URL}}" testCode := "testcode" expectContent := fmt.Sprintf("%s://%s:%d/ui/login/password/init?authRequestID=%s&code=%s&orgID=%s&userID=%s", externalProtocol, instancePrimaryDomain, externalPort, authRequestID, testCode, orgID, userID) - w.message = messages.Email{ + w.message = &messages.Email{ Recipients: []string{lastEmail}, Subject: expectMailSubject, Content: expectContent, @@ -700,7 +709,7 @@ func Test_userNotifier_reducePasswordCodeAdded(t *testing.T) { }}, }, nil) expectTemplateQueries(queries, givenTemplate) - commands.EXPECT().PasswordCodeSent(gomock.Any(), orgID, userID).Return(nil) + commands.EXPECT().PasswordCodeSent(gomock.Any(), orgID, userID, &senders.CodeGeneratorInfo{}).Return(nil) return fields{ queries: queries, commands: commands, @@ -730,14 +739,14 @@ func Test_userNotifier_reducePasswordCodeAdded(t *testing.T) { urlTemplate := "https://my.custom.url/org/{{.OrgID}}/user/{{.UserID}}/verify/{{.Code}}" testCode := "testcode" expectContent := fmt.Sprintf("https://my.custom.url/org/%s/user/%s/verify/%s", orgID, userID, testCode) - w.message = messages.Email{ + w.message = &messages.Email{ Recipients: []string{lastEmail}, Subject: expectMailSubject, Content: expectContent, } codeAlg, code := cryptoValue(t, ctrl, testCode) expectTemplateQueries(queries, givenTemplate) - commands.EXPECT().PasswordCodeSent(gomock.Any(), orgID, userID).Return(nil) + commands.EXPECT().PasswordCodeSent(gomock.Any(), orgID, userID, &senders.CodeGeneratorInfo{}).Return(nil) return fields{ queries: queries, commands: commands, @@ -761,7 +770,44 @@ func Test_userNotifier_reducePasswordCodeAdded(t *testing.T) { }, }, w }, - }} + }, { + name: "external code", + test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w want) { + givenTemplate := "{{.URL}}" + expectContent := "We received a password reset request. Please use the button below to reset your password. (Code ) If you didn't ask for this mail, please ignore it." + w.messageSMS = &messages.SMS{ + SenderPhoneNumber: "senderNumber", + RecipientPhoneNumber: lastPhone, + Content: expectContent, + } + expectTemplateQueries(queries, givenTemplate) + commands.EXPECT().PasswordCodeSent(gomock.Any(), orgID, userID, &senders.CodeGeneratorInfo{ID: smsProviderID, VerificationID: verificationID}).Return(nil) + return fields{ + queries: queries, + commands: commands, + es: eventstore.NewEventstore(&eventstore.Config{ + Querier: es_repo_mock.NewRepo(t).ExpectFilterEvents().MockQuerier, + }), + SMSTokenCrypto: nil, + }, args{ + event: &user.HumanPasswordCodeAddedEvent{ + BaseEvent: *eventstore.BaseEventFromRepo(&repository.Event{ + AggregateID: userID, + ResourceOwner: sql.NullString{String: orgID}, + CreationDate: time.Now().UTC(), + }), + Code: nil, + Expiry: 0, + URLTemplate: "", + CodeReturned: false, + NotificationType: domain.NotificationTypeSms, + GeneratorID: smsProviderID, + TriggeredAtOrigin: eventOrigin, + }, + }, w + }, + }, + } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { ctrl := gomock.NewController(t) @@ -794,7 +840,7 @@ func Test_userNotifier_reduceDomainClaimed(t *testing.T) { test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w want) { givenTemplate := "{{.LogoURL}}" expectContent := fmt.Sprintf("%s%s/%s/%s", eventOrigin, assetsPath, policyID, logoURL) - w.message = messages.Email{ + w.message = &messages.Email{ Recipients: []string{lastEmail}, Subject: expectMailSubject, Content: expectContent, @@ -823,7 +869,7 @@ func Test_userNotifier_reduceDomainClaimed(t *testing.T) { test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w want) { givenTemplate := "{{.LogoURL}}" expectContent := fmt.Sprintf("%s://%s:%d%s/%s/%s", externalProtocol, instancePrimaryDomain, externalPort, assetsPath, policyID, logoURL) - w.message = messages.Email{ + w.message = &messages.Email{ Recipients: []string{lastEmail}, Subject: expectMailSubject, Content: expectContent, @@ -885,7 +931,7 @@ func Test_userNotifier_reducePasswordlessCodeRequested(t *testing.T) { test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w want) { givenTemplate := "{{.LogoURL}}" expectContent := fmt.Sprintf("%s%s/%s/%s", eventOrigin, assetsPath, policyID, logoURL) - w.message = messages.Email{ + w.message = &messages.Email{ Recipients: []string{lastEmail}, Subject: expectMailSubject, Content: expectContent, @@ -921,7 +967,7 @@ func Test_userNotifier_reducePasswordlessCodeRequested(t *testing.T) { test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w want) { givenTemplate := "{{.LogoURL}}" expectContent := fmt.Sprintf("%s://%s:%d%s/%s/%s", externalProtocol, instancePrimaryDomain, externalPort, assetsPath, policyID, logoURL) - w.message = messages.Email{ + w.message = &messages.Email{ Recipients: []string{lastEmail}, Subject: expectMailSubject, Content: expectContent, @@ -964,7 +1010,7 @@ func Test_userNotifier_reducePasswordlessCodeRequested(t *testing.T) { testCode := "testcode" codeAlg, code := cryptoValue(t, ctrl, testCode) expectContent := fmt.Sprintf("%s/ui/login/login/passwordless/init?userID=%s&orgID=%s&codeID=%s&code=%s", eventOrigin, userID, orgID, codeID, testCode) - w.message = messages.Email{ + w.message = &messages.Email{ Recipients: []string{lastEmail}, Subject: expectMailSubject, Content: expectContent, @@ -1002,7 +1048,7 @@ func Test_userNotifier_reducePasswordlessCodeRequested(t *testing.T) { testCode := "testcode" codeAlg, code := cryptoValue(t, ctrl, testCode) expectContent := fmt.Sprintf("%s://%s:%d/ui/login/login/passwordless/init?userID=%s&orgID=%s&codeID=%s&code=%s", externalProtocol, instancePrimaryDomain, externalPort, userID, orgID, codeID, testCode) - w.message = messages.Email{ + w.message = &messages.Email{ Recipients: []string{lastEmail}, Subject: expectMailSubject, Content: expectContent, @@ -1044,7 +1090,7 @@ func Test_userNotifier_reducePasswordlessCodeRequested(t *testing.T) { urlTemplate := "https://my.custom.url/org/{{.OrgID}}/user/{{.UserID}}/verify/{{.Code}}" testCode := "testcode" expectContent := fmt.Sprintf("https://my.custom.url/org/%s/user/%s/verify/%s", orgID, userID, testCode) - w.message = messages.Email{ + w.message = &messages.Email{ Recipients: []string{lastEmail}, Subject: expectMailSubject, Content: expectContent, @@ -1109,7 +1155,7 @@ func Test_userNotifier_reducePasswordChanged(t *testing.T) { test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w want) { givenTemplate := "{{.LogoURL}}" expectContent := fmt.Sprintf("%s%s/%s/%s", eventOrigin, assetsPath, policyID, logoURL) - w.message = messages.Email{ + w.message = &messages.Email{ Recipients: []string{lastEmail}, Subject: expectMailSubject, Content: expectContent, @@ -1141,7 +1187,7 @@ func Test_userNotifier_reducePasswordChanged(t *testing.T) { test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w want) { givenTemplate := "{{.LogoURL}}" expectContent := fmt.Sprintf("%s://%s:%d%s/%s/%s", externalProtocol, instancePrimaryDomain, externalPort, assetsPath, policyID, logoURL) - w.message = messages.Email{ + w.message = &messages.Email{ Recipients: []string{lastEmail}, Subject: expectMailSubject, Content: expectContent, @@ -1206,7 +1252,7 @@ func Test_userNotifier_reduceOTPEmailChallenged(t *testing.T) { test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w want) { givenTemplate := "{{.LogoURL}}" expectContent := fmt.Sprintf("%s%s/%s/%s", eventOrigin, assetsPath, policyID, logoURL) - w.message = messages.Email{ + w.message = &messages.Email{ Recipients: []string{verifiedEmail}, Subject: expectMailSubject, Content: expectContent, @@ -1242,7 +1288,7 @@ func Test_userNotifier_reduceOTPEmailChallenged(t *testing.T) { test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w want) { givenTemplate := "{{.LogoURL}}" expectContent := fmt.Sprintf("%s://%s:%d%s/%s/%s", externalProtocol, instancePrimaryDomain, externalPort, assetsPath, policyID, logoURL) - w.message = messages.Email{ + w.message = &messages.Email{ Recipients: []string{verifiedEmail}, Subject: expectMailSubject, Content: expectContent, @@ -1284,7 +1330,7 @@ func Test_userNotifier_reduceOTPEmailChallenged(t *testing.T) { givenTemplate := "{{.URL}}" testCode := "testcode" expectContent := fmt.Sprintf("%s/otp/verify?loginName=%s&code=%s", eventOrigin, preferredLoginName, testCode) - w.message = messages.Email{ + w.message = &messages.Email{ Recipients: []string{verifiedEmail}, Subject: expectMailSubject, Content: expectContent, @@ -1322,7 +1368,7 @@ func Test_userNotifier_reduceOTPEmailChallenged(t *testing.T) { givenTemplate := "{{.URL}}" testCode := "testcode" expectContent := fmt.Sprintf("%s://%s:%d/otp/verify?loginName=%s&code=%s", externalProtocol, instancePrimaryDomain, externalPort, preferredLoginName, testCode) - w.message = messages.Email{ + w.message = &messages.Email{ Recipients: []string{verifiedEmail}, Subject: expectMailSubject, Content: expectContent, @@ -1364,7 +1410,7 @@ func Test_userNotifier_reduceOTPEmailChallenged(t *testing.T) { urlTemplate := "https://my.custom.url/user/{{.LoginName}}/verify" testCode := "testcode" expectContent := fmt.Sprintf("https://my.custom.url/user/%s/verify", preferredLoginName) - w.message = messages.Email{ + w.message = &messages.Email{ Recipients: []string{verifiedEmail}, Subject: expectMailSubject, Content: expectContent, @@ -1413,6 +1459,107 @@ func Test_userNotifier_reduceOTPEmailChallenged(t *testing.T) { } } +func Test_userNotifier_reduceOTPSMSChallenged(t *testing.T) { + tests := []struct { + name string + test func(*gomock.Controller, *mock.MockQueries, *mock.MockCommands) (fields, args, want) + }{{ + name: "asset url with event trigger url", + test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w want) { + givenTemplate := "{{.LogoURL}}" + testCode := "" + expiry := 0 * time.Hour + expectContent := fmt.Sprintf(`%[1]s is your one-time-password for %[2]s. Use it within the next %[3]s. +@%[2]s #%[1]s`, testCode, eventOriginDomain, expiry) + w.messageSMS = &messages.SMS{ + SenderPhoneNumber: "senderNumber", + RecipientPhoneNumber: verifiedPhone, + Content: expectContent, + } + expectTemplateQueriesSMS(queries, givenTemplate) + queries.EXPECT().SessionByID(gomock.Any(), gomock.Any(), userID, gomock.Any()).Return(&query.Session{}, nil) + commands.EXPECT().OTPSMSSent(gomock.Any(), userID, orgID, &senders.CodeGeneratorInfo{ID: smsProviderID, VerificationID: verificationID}).Return(nil) + return fields{ + queries: queries, + commands: commands, + es: eventstore.NewEventstore(&eventstore.Config{ + Querier: es_repo_mock.NewRepo(t).ExpectFilterEvents().MockQuerier, + }), + }, args{ + event: &session.OTPSMSChallengedEvent{ + BaseEvent: *eventstore.BaseEventFromRepo(&repository.Event{ + AggregateID: userID, + ResourceOwner: sql.NullString{String: orgID}, + CreationDate: time.Now().UTC(), + }), + Code: nil, + Expiry: expiry, + CodeReturned: false, + GeneratorID: smsProviderID, + TriggeredAtOrigin: eventOrigin, + }, + }, w + }, + }, { + name: "asset url without event trigger url", + test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w want) { + givenTemplate := "{{.LogoURL}}" + testCode := "" + expiry := 0 * time.Hour + expectContent := fmt.Sprintf(`%[1]s is your one-time-password for %[2]s. Use it within the next %[3]s. +@%[2]s #%[1]s`, testCode, instancePrimaryDomain, expiry) + w.messageSMS = &messages.SMS{ + SenderPhoneNumber: "senderNumber", + RecipientPhoneNumber: verifiedPhone, + Content: expectContent, + } + expectTemplateQueriesSMS(queries, givenTemplate) + queries.EXPECT().SessionByID(gomock.Any(), gomock.Any(), userID, gomock.Any()).Return(&query.Session{}, nil) + queries.EXPECT().SearchInstanceDomains(gomock.Any(), gomock.Any()).Return(&query.InstanceDomains{ + Domains: []*query.InstanceDomain{{ + Domain: instancePrimaryDomain, + IsPrimary: true, + }}, + }, nil) + commands.EXPECT().OTPSMSSent(gomock.Any(), userID, orgID, &senders.CodeGeneratorInfo{ID: smsProviderID, VerificationID: verificationID}).Return(nil) + return fields{ + queries: queries, + commands: commands, + es: eventstore.NewEventstore(&eventstore.Config{ + Querier: es_repo_mock.NewRepo(t).ExpectFilterEvents().MockQuerier, + }), + }, args{ + event: &session.OTPSMSChallengedEvent{ + BaseEvent: *eventstore.BaseEventFromRepo(&repository.Event{ + AggregateID: userID, + ResourceOwner: sql.NullString{String: orgID}, + CreationDate: time.Now().UTC(), + }), + Code: nil, + Expiry: expiry, + CodeReturned: false, + GeneratorID: smsProviderID, + }, + }, w + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctrl := gomock.NewController(t) + queries := mock.NewMockQueries(ctrl) + commands := mock.NewMockCommands(ctrl) + f, a, w := tt.test(ctrl, queries, commands) + _, err := newUserNotifier(t, ctrl, queries, f, a, w).reduceSessionOTPSMSChallenged(a.event) + if w.err != nil { + w.err(t, err) + } else { + assert.NoError(t, err) + } + }) + } +} + type fields struct { queries *mock.MockQueries commands *mock.MockCommands @@ -1424,8 +1571,9 @@ type args struct { event eventstore.Event } type want struct { - message messages.Email - err assert.ErrorAssertionFunc + message *messages.Email + messageSMS *messages.SMS + err assert.ErrorAssertionFunc } func newUserNotifier(t *testing.T, ctrl *gomock.Controller, queries *mock.MockQueries, f fields, a args, w want) *userNotifier { @@ -1433,8 +1581,17 @@ func newUserNotifier(t *testing.T, ctrl *gomock.Controller, queries *mock.MockQu smtpAlg, _ := cryptoValue(t, ctrl, "smtppw") channel := channel_mock.NewMockNotificationChannel(ctrl) if w.err == nil { - w.message.TriggeringEvent = a.event - channel.EXPECT().HandleMessage(&w.message).Return(nil) + if w.message != nil { + w.message.TriggeringEvent = a.event + channel.EXPECT().HandleMessage(w.message).Return(nil) + } + if w.messageSMS != nil { + w.messageSMS.TriggeringEvent = a.event + channel.EXPECT().HandleMessage(w.messageSMS).DoAndReturn(func(message *messages.SMS) error { + message.VerificationID = gu.Ptr(verificationID) + return nil + }) + } } return &userNotifier{ commands: f.commands, @@ -1454,8 +1611,8 @@ func newUserNotifier(t *testing.T, ctrl *gomock.Controller, queries *mock.MockQu Chain: *senders.ChainChannels(channel), EmailConfig: &email.Config{ ProviderConfig: &email.Provider{ - ID: "ID", - Description: "Description", + ID: "emailProviderID", + Description: "description", }, SMTPConfig: &smtp.Config{ SMTP: smtp.SMTP{ @@ -1470,6 +1627,18 @@ func newUserNotifier(t *testing.T, ctrl *gomock.Controller, queries *mock.MockQu }, WebhookConfig: nil, }, + SMSConfig: &sms.Config{ + ProviderConfig: &sms.Provider{ + ID: "smsProviderID", + Description: "description", + }, + TwilioConfig: &twilio.Config{ + SID: "sid", + Token: "token", + SenderNumber: "senderNumber", + VerifyServiceSID: "verifyServiceSID", + }, + }, }, } } @@ -1479,6 +1648,7 @@ var _ types.ChannelChains = (*channels)(nil) type channels struct { senders.Chain EmailConfig *email.Config + SMSConfig *sms.Config } func (c *channels) Email(context.Context) (*senders.Chain, *email.Config, error) { @@ -1486,7 +1656,7 @@ func (c *channels) Email(context.Context) (*senders.Chain, *email.Config, error) } func (c *channels) SMS(context.Context) (*senders.Chain, *sms.Config, error) { - return &c.Chain, nil, nil + return &c.Chain, c.SMSConfig, nil } func (c *channels) Webhook(context.Context, webhook.Config) (*senders.Chain, error) { @@ -1510,6 +1680,31 @@ func expectTemplateQueries(queries *mock.MockQueries, template string) { LastEmail: lastEmail, VerifiedEmail: verifiedEmail, PreferredLoginName: preferredLoginName, + LastPhone: lastPhone, + VerifiedPhone: verifiedPhone, + }, nil) + queries.EXPECT().GetDefaultLanguage(gomock.Any()).Return(language.English) + queries.EXPECT().CustomTextListByTemplate(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Times(2).Return(&query.CustomTexts{}, nil) +} + +func expectTemplateQueriesSMS(queries *mock.MockQueries, template string) { + queries.EXPECT().GetInstanceRestrictions(gomock.Any()).Return(query.Restrictions{ + AllowedLanguages: []language.Tag{language.English}, + }, nil) + queries.EXPECT().ActiveLabelPolicyByOrg(gomock.Any(), gomock.Any(), gomock.Any()).Return(&query.LabelPolicy{ + ID: policyID, + Light: query.Theme{ + LogoURL: logoURL, + }, + }, nil) + queries.EXPECT().GetNotifyUserByID(gomock.Any(), gomock.Any(), gomock.Any()).Return(&query.NotifyUser{ + ID: userID, + ResourceOwner: orgID, + LastEmail: lastEmail, + VerifiedEmail: verifiedEmail, + PreferredLoginName: preferredLoginName, + LastPhone: lastPhone, + VerifiedPhone: verifiedPhone, }, nil) queries.EXPECT().GetDefaultLanguage(gomock.Any()).Return(language.English) queries.EXPECT().CustomTextListByTemplate(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Times(2).Return(&query.CustomTexts{}, nil) diff --git a/internal/notification/messages/sms.go b/internal/notification/messages/sms.go index 72c377b33..0dfaea877 100644 --- a/internal/notification/messages/sms.go +++ b/internal/notification/messages/sms.go @@ -12,6 +12,9 @@ type SMS struct { RecipientPhoneNumber string Content string TriggeringEvent eventstore.Event + + // VerificationID is set by the sender + VerificationID *string } func (msg *SMS) GetContent() (string, error) { diff --git a/internal/notification/senders/code_verifier.go b/internal/notification/senders/code_verifier.go new file mode 100644 index 000000000..3aa9ec2e0 --- /dev/null +++ b/internal/notification/senders/code_verifier.go @@ -0,0 +1,24 @@ +package senders + +type CodeGenerator interface { + VerifyCode(verificationID, code string) error +} + +type CodeGeneratorInfo struct { + ID string `json:"id,omitempty"` + VerificationID string `json:"verificationId,omitempty"` +} + +func (c *CodeGeneratorInfo) GetID() string { + if c == nil { + return "" + } + return c.ID +} + +func (c *CodeGeneratorInfo) GetVerificationID() string { + if c == nil { + return "" + } + return c.VerificationID +} diff --git a/internal/notification/senders/gen_mock.go b/internal/notification/senders/gen_mock.go new file mode 100644 index 000000000..5a0f47285 --- /dev/null +++ b/internal/notification/senders/gen_mock.go @@ -0,0 +1,3 @@ +package senders + +//go:generate mockgen -package mock -destination ./mock/code_generator.mock.go github.com/zitadel/zitadel/internal/notification/senders CodeGenerator diff --git a/internal/notification/senders/mock/code_generator.mock.go b/internal/notification/senders/mock/code_generator.mock.go new file mode 100644 index 000000000..15bdd2cc3 --- /dev/null +++ b/internal/notification/senders/mock/code_generator.mock.go @@ -0,0 +1,53 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/zitadel/zitadel/internal/notification/senders (interfaces: CodeGenerator) +// +// Generated by this command: +// +// mockgen -package mock -destination ./mock/code_generator.mock.go github.com/zitadel/zitadel/internal/notification/senders CodeGenerator +// + +// Package mock is a generated GoMock package. +package mock + +import ( + reflect "reflect" + + gomock "go.uber.org/mock/gomock" +) + +// MockCodeGenerator is a mock of CodeGenerator interface. +type MockCodeGenerator struct { + ctrl *gomock.Controller + recorder *MockCodeGeneratorMockRecorder +} + +// MockCodeGeneratorMockRecorder is the mock recorder for MockCodeGenerator. +type MockCodeGeneratorMockRecorder struct { + mock *MockCodeGenerator +} + +// NewMockCodeGenerator creates a new mock instance. +func NewMockCodeGenerator(ctrl *gomock.Controller) *MockCodeGenerator { + mock := &MockCodeGenerator{ctrl: ctrl} + mock.recorder = &MockCodeGeneratorMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockCodeGenerator) EXPECT() *MockCodeGeneratorMockRecorder { + return m.recorder +} + +// VerifyCode mocks base method. +func (m *MockCodeGenerator) VerifyCode(arg0, arg1 string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "VerifyCode", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// VerifyCode indicates an expected call of VerifyCode. +func (mr *MockCodeGeneratorMockRecorder) VerifyCode(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "VerifyCode", reflect.TypeOf((*MockCodeGenerator)(nil).VerifyCode), arg0, arg1) +} diff --git a/internal/notification/types/notification.go b/internal/notification/types/notification.go index 8d1b01316..49a437ff1 100644 --- a/internal/notification/types/notification.go +++ b/internal/notification/types/notification.go @@ -87,6 +87,7 @@ func SendSMS( user *query.NotifyUser, colors *query.LabelPolicy, triggeringEvent eventstore.Event, + generatorInfo *senders.CodeGeneratorInfo, ) Notify { return func( url string, @@ -104,6 +105,7 @@ func SendSMS( args, allowUnverifiedNotificationChannel, triggeringEvent, + generatorInfo, ) } } diff --git a/internal/notification/types/user_phone.go b/internal/notification/types/user_phone.go index 8e79f7371..0016f0f7a 100644 --- a/internal/notification/types/user_phone.go +++ b/internal/notification/types/user_phone.go @@ -8,6 +8,7 @@ import ( "github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/notification/messages" + "github.com/zitadel/zitadel/internal/notification/senders" "github.com/zitadel/zitadel/internal/notification/templates" "github.com/zitadel/zitadel/internal/query" "github.com/zitadel/zitadel/internal/zerrors" @@ -27,6 +28,7 @@ func generateSms( args map[string]interface{}, lastPhone bool, triggeringEvent eventstore.Event, + generatorInfo *senders.CodeGeneratorInfo, ) error { smsChannels, config, err := channels.SMS(ctx) logging.OnError(err).Error("could not create sms channel") @@ -48,7 +50,15 @@ func generateSms( Content: data.Text, TriggeringEvent: triggeringEvent, } - return smsChannels.HandleMessage(message) + err = smsChannels.HandleMessage(message) + if err != nil { + return err + } + if config.TwilioConfig.VerifyServiceSID != "" { + generatorInfo.ID = config.ProviderConfig.ID + generatorInfo.VerificationID = *message.VerificationID + } + return nil } if config.WebhookConfig != nil { caseArgs := make(map[string]interface{}, len(args)) diff --git a/internal/query/projection/sms.go b/internal/query/projection/sms.go index 9b157ff99..eb54d7afa 100644 --- a/internal/query/projection/sms.go +++ b/internal/query/projection/sms.go @@ -25,12 +25,13 @@ const ( SMSColumnInstanceID = "instance_id" SMSColumnDescription = "description" - smsTwilioTableSuffix = "twilio" - SMSTwilioColumnSMSID = "sms_id" - SMSTwilioColumnInstanceID = "instance_id" - SMSTwilioColumnSID = "sid" - SMSTwilioColumnSenderNumber = "sender_number" - SMSTwilioColumnToken = "token" + smsTwilioTableSuffix = "twilio" + SMSTwilioColumnSMSID = "sms_id" + SMSTwilioColumnInstanceID = "instance_id" + SMSTwilioColumnSID = "sid" + SMSTwilioColumnSenderNumber = "sender_number" + SMSTwilioColumnToken = "token" + SMSTwilioColumnVerifyServiceSID = "verify_service_sid" smsHTTPTableSuffix = "http" SMSHTTPColumnSMSID = "sms_id" @@ -69,6 +70,7 @@ func (*smsConfigProjection) Init() *old_handler.Check { handler.NewColumn(SMSTwilioColumnSID, handler.ColumnTypeText), handler.NewColumn(SMSTwilioColumnSenderNumber, handler.ColumnTypeText), handler.NewColumn(SMSTwilioColumnToken, handler.ColumnTypeJSONB), + handler.NewColumn(SMSTwilioColumnVerifyServiceSID, handler.ColumnTypeText), }, handler.NewPrimaryKey(SMSTwilioColumnInstanceID, SMSTwilioColumnSMSID), smsTwilioTableSuffix, @@ -172,6 +174,7 @@ func (p *smsConfigProjection) reduceSMSConfigTwilioAdded(event eventstore.Event) handler.NewCol(SMSTwilioColumnSID, e.SID), handler.NewCol(SMSTwilioColumnToken, e.Token), handler.NewCol(SMSTwilioColumnSenderNumber, e.SenderNumber), + handler.NewCol(SMSTwilioColumnVerifyServiceSID, e.VerifyServiceSID), }, handler.WithTableSuffix(smsTwilioTableSuffix), ), @@ -202,13 +205,16 @@ func (p *smsConfigProjection) reduceSMSConfigTwilioChanged(event eventstore.Even )) } - twilioColumns := make([]handler.Column, 0) + twilioColumns := make([]handler.Column, 0, 3) if e.SID != nil { twilioColumns = append(twilioColumns, handler.NewCol(SMSTwilioColumnSID, *e.SID)) } if e.SenderNumber != nil { twilioColumns = append(twilioColumns, handler.NewCol(SMSTwilioColumnSenderNumber, *e.SenderNumber)) } + if e.VerifyServiceSID != nil { + twilioColumns = append(twilioColumns, handler.NewCol(SMSTwilioColumnVerifyServiceSID, *e.VerifyServiceSID)) + } if len(twilioColumns) > 0 { stmts = append(stmts, handler.AddUpdateStatement( twilioColumns, diff --git a/internal/query/projection/sms_test.go b/internal/query/projection/sms_test.go index 88ce6e441..7a083c223 100644 --- a/internal/query/projection/sms_test.go +++ b/internal/query/projection/sms_test.go @@ -38,7 +38,8 @@ func TestSMSProjection_reduces(t *testing.T) { "crypted": "Y3J5cHRlZA==" }, "senderNumber": "sender-number", - "description": "description" + "description": "description", + "verifyServiceSid": "verify-service-sid" }`), ), eventstore.GenericEventMapper[instance.SMSConfigTwilioAddedEvent]), }, @@ -63,7 +64,7 @@ func TestSMSProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "INSERT INTO projections.sms_configs3_twilio (sms_id, instance_id, sid, token, sender_number) VALUES ($1, $2, $3, $4, $5)", + expectedStmt: "INSERT INTO projections.sms_configs3_twilio (sms_id, instance_id, sid, token, sender_number, verify_service_sid) VALUES ($1, $2, $3, $4, $5, $6)", expectedArgs: []interface{}{ "id", "instance-id", @@ -75,6 +76,7 @@ func TestSMSProjection_reduces(t *testing.T) { Crypted: []byte("crypted"), }, "sender-number", + "verify-service-sid", }, }, }, @@ -92,7 +94,8 @@ func TestSMSProjection_reduces(t *testing.T) { "id": "id", "sid": "sid", "senderNumber": "sender-number", - "description": "description" + "description": "description", + "verifyServiceSid": "verify-service-sid" }`), ), eventstore.GenericEventMapper[instance.SMSConfigTwilioChangedEvent]), }, @@ -113,10 +116,11 @@ func TestSMSProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "UPDATE projections.sms_configs3_twilio SET (sid, sender_number) = ($1, $2) WHERE (sms_id = $3) AND (instance_id = $4)", + expectedStmt: "UPDATE projections.sms_configs3_twilio SET (sid, sender_number, verify_service_sid) = ($1, $2, $3) WHERE (sms_id = $4) AND (instance_id = $5)", expectedArgs: []interface{}{ "sid", "sender-number", + "verify-service-sid", "id", "instance-id", }, @@ -248,6 +252,46 @@ func TestSMSProjection_reduces(t *testing.T) { }, }, }, + { + name: "instance reduceSMSConfigTwilioChanged, only sid", + args: args{ + event: getEvent( + testEvent( + instance.SMSConfigTwilioChangedEventType, + instance.AggregateType, + []byte(`{ + "id": "id", + "verifyServiceSid": "verify-service-sid" + }`), + ), eventstore.GenericEventMapper[instance.SMSConfigTwilioChangedEvent]), + }, + reduce: (&smsConfigProjection{}).reduceSMSConfigTwilioChanged, + want: wantReduce{ + aggregateType: eventstore.AggregateType("instance"), + sequence: 15, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: "UPDATE projections.sms_configs3 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", + expectedArgs: []interface{}{ + anyArg{}, + uint64(15), + "id", + "instance-id", + }, + }, + { + expectedStmt: "UPDATE projections.sms_configs3_twilio SET verify_service_sid = $1 WHERE (sms_id = $2) AND (instance_id = $3)", + expectedArgs: []interface{}{ + "verify-service-sid", + "id", + "instance-id", + }, + }, + }, + }, + }, + }, { name: "instance reduceSMSHTTPAdded", args: args{ diff --git a/internal/query/sms.go b/internal/query/sms.go index 6f0555634..ef4d1cfca 100644 --- a/internal/query/sms.go +++ b/internal/query/sms.go @@ -37,9 +37,10 @@ type SMSConfig struct { } type Twilio struct { - SID string - Token *crypto.CryptoValue - SenderNumber string + SID string + Token *crypto.CryptoValue + SenderNumber string + VerifyServiceSID string } type HTTP struct { @@ -123,6 +124,10 @@ var ( name: projection.SMSTwilioColumnSenderNumber, table: smsTwilioTable, } + SMSTwilioColumnVerifyServiceSID = Column{ + name: projection.SMSTwilioColumnVerifyServiceSID, + table: smsTwilioTable, + } ) var ( @@ -227,6 +232,7 @@ func prepareSMSConfigQuery(ctx context.Context, db prepareDatabase) (sq.SelectBu SMSTwilioColumnSID.identifier(), SMSTwilioColumnToken.identifier(), SMSTwilioColumnSenderNumber.identifier(), + SMSTwilioColumnVerifyServiceSID.identifier(), SMSHTTPColumnSMSID.identifier(), SMSHTTPColumnEndpoint.identifier(), @@ -255,6 +261,7 @@ func prepareSMSConfigQuery(ctx context.Context, db prepareDatabase) (sq.SelectBu &twilioConfig.sid, &twilioConfig.token, &twilioConfig.senderNumber, + &twilioConfig.verifyServiceSid, &httpConfig.id, &httpConfig.endpoint, @@ -289,6 +296,7 @@ func prepareSMSConfigsQuery(ctx context.Context, db prepareDatabase) (sq.SelectB SMSTwilioColumnSID.identifier(), SMSTwilioColumnToken.identifier(), SMSTwilioColumnSenderNumber.identifier(), + SMSTwilioColumnVerifyServiceSID.identifier(), SMSHTTPColumnSMSID.identifier(), SMSHTTPColumnEndpoint.identifier(), @@ -321,6 +329,7 @@ func prepareSMSConfigsQuery(ctx context.Context, db prepareDatabase) (sq.SelectB &twilioConfig.sid, &twilioConfig.token, &twilioConfig.senderNumber, + &twilioConfig.verifyServiceSid, &httpConfig.id, &httpConfig.endpoint, @@ -343,10 +352,11 @@ func prepareSMSConfigsQuery(ctx context.Context, db prepareDatabase) (sq.SelectB } type sqlTwilioConfig struct { - smsID sql.NullString - sid sql.NullString - token *crypto.CryptoValue - senderNumber sql.NullString + smsID sql.NullString + sid sql.NullString + token *crypto.CryptoValue + senderNumber sql.NullString + verifyServiceSid sql.NullString } func (c sqlTwilioConfig) set(smsConfig *SMSConfig) { @@ -354,9 +364,10 @@ func (c sqlTwilioConfig) set(smsConfig *SMSConfig) { return } smsConfig.TwilioConfig = &Twilio{ - SID: c.sid.String, - Token: c.token, - SenderNumber: c.senderNumber.String, + SID: c.sid.String, + Token: c.token, + SenderNumber: c.senderNumber.String, + VerifyServiceSID: c.verifyServiceSid.String, } } diff --git a/internal/query/sms_test.go b/internal/query/sms_test.go index 20cf62f8c..82c3659f2 100644 --- a/internal/query/sms_test.go +++ b/internal/query/sms_test.go @@ -28,6 +28,7 @@ var ( ` projections.sms_configs3_twilio.sid,` + ` projections.sms_configs3_twilio.token,` + ` projections.sms_configs3_twilio.sender_number,` + + ` projections.sms_configs3_twilio.verify_service_sid,` + // http config ` projections.sms_configs3_http.sms_id,` + @@ -50,6 +51,7 @@ var ( ` projections.sms_configs3_twilio.sid,` + ` projections.sms_configs3_twilio.token,` + ` projections.sms_configs3_twilio.sender_number,` + + ` projections.sms_configs3_twilio.verify_service_sid,` + // http config ` projections.sms_configs3_http.sms_id,` + @@ -74,6 +76,7 @@ var ( "sid", "token", "sender-number", + "verify_sid", // http config "sms_id", "endpoint", @@ -126,6 +129,7 @@ func Test_SMSConfigsPrepare(t *testing.T) { "sid", &crypto.CryptoValue{}, "sender-number", + "", // http config nil, nil, @@ -148,9 +152,10 @@ func Test_SMSConfigsPrepare(t *testing.T) { Sequence: 20211109, Description: "description", TwilioConfig: &Twilio{ - SID: "sid", - Token: &crypto.CryptoValue{}, - SenderNumber: "sender-number", + SID: "sid", + Token: &crypto.CryptoValue{}, + SenderNumber: "sender-number", + VerifyServiceSID: "", }, }, }, @@ -178,6 +183,7 @@ func Test_SMSConfigsPrepare(t *testing.T) { nil, nil, nil, + nil, // http config "sms-id", "endpoint", @@ -228,6 +234,7 @@ func Test_SMSConfigsPrepare(t *testing.T) { "sid", &crypto.CryptoValue{}, "sender-number", + "verify-service-sid", // http config nil, nil, @@ -246,6 +253,7 @@ func Test_SMSConfigsPrepare(t *testing.T) { "sid2", &crypto.CryptoValue{}, "sender-number2", + "verify-service-sid2", // http config nil, nil, @@ -264,6 +272,7 @@ func Test_SMSConfigsPrepare(t *testing.T) { nil, nil, nil, + nil, // http config "sms-id3", "endpoint3", @@ -286,9 +295,10 @@ func Test_SMSConfigsPrepare(t *testing.T) { Sequence: 20211109, Description: "description", TwilioConfig: &Twilio{ - SID: "sid", - Token: &crypto.CryptoValue{}, - SenderNumber: "sender-number", + SID: "sid", + Token: &crypto.CryptoValue{}, + SenderNumber: "sender-number", + VerifyServiceSID: "verify-service-sid", }, }, { @@ -301,9 +311,10 @@ func Test_SMSConfigsPrepare(t *testing.T) { Sequence: 20211109, Description: "description", TwilioConfig: &Twilio{ - SID: "sid2", - Token: &crypto.CryptoValue{}, - SenderNumber: "sender-number2", + SID: "sid2", + Token: &crypto.CryptoValue{}, + SenderNumber: "sender-number2", + VerifyServiceSID: "verify-service-sid2", }, }, { @@ -397,6 +408,7 @@ func Test_SMSConfigPrepare(t *testing.T) { "sid", &crypto.CryptoValue{}, "sender-number", + "verify-service-sid", // http config nil, nil, @@ -413,9 +425,10 @@ func Test_SMSConfigPrepare(t *testing.T) { Sequence: 20211109, Description: "description", TwilioConfig: &Twilio{ - SID: "sid", - SenderNumber: "sender-number", - Token: &crypto.CryptoValue{}, + SID: "sid", + SenderNumber: "sender-number", + Token: &crypto.CryptoValue{}, + VerifyServiceSID: "verify-service-sid", }, }, }, @@ -440,6 +453,7 @@ func Test_SMSConfigPrepare(t *testing.T) { nil, nil, nil, + nil, // http config "sms-id", "endpoint", diff --git a/internal/repository/instance/sms.go b/internal/repository/instance/sms.go index 309ce9aa4..7a402e67f 100644 --- a/internal/repository/instance/sms.go +++ b/internal/repository/instance/sms.go @@ -28,11 +28,12 @@ const ( type SMSConfigTwilioAddedEvent struct { *eventstore.BaseEvent `json:"-"` - ID string `json:"id,omitempty"` - Description string `json:"description,omitempty"` - SID string `json:"sid,omitempty"` - Token *crypto.CryptoValue `json:"token,omitempty"` - SenderNumber string `json:"senderNumber,omitempty"` + ID string `json:"id,omitempty"` + Description string `json:"description,omitempty"` + SID string `json:"sid,omitempty"` + Token *crypto.CryptoValue `json:"token,omitempty"` + SenderNumber string `json:"senderNumber,omitempty"` + VerifyServiceSID string `json:"verifyServiceSid,omitempty"` } func NewSMSConfigTwilioAddedEvent( @@ -43,6 +44,7 @@ func NewSMSConfigTwilioAddedEvent( sid, senderNumber string, token *crypto.CryptoValue, + verifyServiceSid string, ) *SMSConfigTwilioAddedEvent { return &SMSConfigTwilioAddedEvent{ BaseEvent: eventstore.NewBaseEventForPush( @@ -50,11 +52,12 @@ func NewSMSConfigTwilioAddedEvent( aggregate, SMSConfigTwilioAddedEventType, ), - ID: id, - Description: description, - SID: sid, - Token: token, - SenderNumber: senderNumber, + ID: id, + Description: description, + SID: sid, + Token: token, + SenderNumber: senderNumber, + VerifyServiceSID: verifyServiceSid, } } @@ -73,10 +76,11 @@ func (e *SMSConfigTwilioAddedEvent) UniqueConstraints() []*eventstore.UniqueCons type SMSConfigTwilioChangedEvent struct { *eventstore.BaseEvent `json:"-"` - ID string `json:"id,omitempty"` - Description *string `json:"description,omitempty"` - SID *string `json:"sid,omitempty"` - SenderNumber *string `json:"senderNumber,omitempty"` + ID string `json:"id,omitempty"` + Description *string `json:"description,omitempty"` + SID *string `json:"sid,omitempty"` + SenderNumber *string `json:"senderNumber,omitempty"` + VerifyServiceSID *string `json:"verifyServiceSid,omitempty"` } func NewSMSConfigTwilioChangedEvent( @@ -122,6 +126,12 @@ func ChangeSMSConfigTwilioSenderNumber(senderNumber string) func(event *SMSConfi } } +func ChangeSMSConfigTwilioVerifyServiceSID(verifyServiceSID string) func(event *SMSConfigTwilioChangedEvent) { + return func(e *SMSConfigTwilioChangedEvent) { + e.VerifyServiceSID = &verifyServiceSID + } +} + func (e *SMSConfigTwilioChangedEvent) SetBaseEvent(event *eventstore.BaseEvent) { e.BaseEvent = event } diff --git a/internal/repository/session/session.go b/internal/repository/session/session.go index 3e9b727f5..f5622fd4b 100644 --- a/internal/repository/session/session.go +++ b/internal/repository/session/session.go @@ -10,6 +10,7 @@ import ( "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/notification/senders" "github.com/zitadel/zitadel/internal/zerrors" ) @@ -323,6 +324,7 @@ type OTPSMSChallengedEvent struct { Code *crypto.CryptoValue `json:"code"` Expiry time.Duration `json:"expiry"` CodeReturned bool `json:"codeReturned,omitempty"` + GeneratorID string `json:"generatorId,omitempty"` TriggeredAtOrigin string `json:"triggerOrigin,omitempty"` } @@ -348,6 +350,7 @@ func NewOTPSMSChallengedEvent( code *crypto.CryptoValue, expiry time.Duration, codeReturned bool, + generatorID string, ) *OTPSMSChallengedEvent { return &OTPSMSChallengedEvent{ BaseEvent: *eventstore.NewBaseEventForPush( @@ -358,12 +361,15 @@ func NewOTPSMSChallengedEvent( Code: code, Expiry: expiry, CodeReturned: codeReturned, + GeneratorID: generatorID, TriggeredAtOrigin: http.DomainContext(ctx).Origin(), } } type OTPSMSSentEvent struct { eventstore.BaseEvent `json:"-"` + + GeneratorInfo *senders.CodeGeneratorInfo `json:"generatorInfo,omitempty"` } func (e *OTPSMSSentEvent) Payload() interface{} { @@ -381,6 +387,7 @@ func (e *OTPSMSSentEvent) SetBaseEvent(base *eventstore.BaseEvent) { func NewOTPSMSSentEvent( ctx context.Context, aggregate *eventstore.Aggregate, + generatorInfo *senders.CodeGeneratorInfo, ) *OTPSMSSentEvent { return &OTPSMSSentEvent{ BaseEvent: *eventstore.NewBaseEventForPush( @@ -388,6 +395,7 @@ func NewOTPSMSSentEvent( aggregate, OTPSMSSentType, ), + GeneratorInfo: generatorInfo, } } diff --git a/internal/repository/user/eventstore.go b/internal/repository/user/eventstore.go index 2b726d378..42bc96280 100644 --- a/internal/repository/user/eventstore.go +++ b/internal/repository/user/eventstore.go @@ -14,7 +14,7 @@ func init() { eventstore.RegisterFilterEventMapper(AggregateType, UserV1SignedOutType, HumanSignedOutEventMapper) eventstore.RegisterFilterEventMapper(AggregateType, UserV1PasswordChangedType, HumanPasswordChangedEventMapper) eventstore.RegisterFilterEventMapper(AggregateType, UserV1PasswordCodeAddedType, HumanPasswordCodeAddedEventMapper) - eventstore.RegisterFilterEventMapper(AggregateType, UserV1PasswordCodeSentType, HumanPasswordCodeSentEventMapper) + eventstore.RegisterFilterEventMapper(AggregateType, UserV1PasswordCodeSentType, eventstore.GenericEventMapper[HumanPasswordCodeSentEvent]) eventstore.RegisterFilterEventMapper(AggregateType, UserV1PasswordCheckSucceededType, HumanPasswordCheckSucceededEventMapper) eventstore.RegisterFilterEventMapper(AggregateType, UserV1PasswordCheckFailedType, HumanPasswordCheckFailedEventMapper) eventstore.RegisterFilterEventMapper(AggregateType, UserV1EmailChangedType, HumanEmailChangedEventMapper) @@ -27,7 +27,7 @@ func init() { eventstore.RegisterFilterEventMapper(AggregateType, UserV1PhoneVerifiedType, HumanPhoneVerifiedEventMapper) eventstore.RegisterFilterEventMapper(AggregateType, UserV1PhoneVerificationFailedType, HumanPhoneVerificationFailedEventMapper) eventstore.RegisterFilterEventMapper(AggregateType, UserV1PhoneCodeAddedType, HumanPhoneCodeAddedEventMapper) - eventstore.RegisterFilterEventMapper(AggregateType, UserV1PhoneCodeSentType, HumanPhoneCodeSentEventMapper) + eventstore.RegisterFilterEventMapper(AggregateType, UserV1PhoneCodeSentType, eventstore.GenericEventMapper[HumanPhoneCodeSentEvent]) eventstore.RegisterFilterEventMapper(AggregateType, UserV1ProfileChangedType, HumanProfileChangedEventMapper) eventstore.RegisterFilterEventMapper(AggregateType, UserV1AddressChangedType, HumanAddressChangedEventMapper) eventstore.RegisterFilterEventMapper(AggregateType, UserV1MFAInitSkippedType, HumanMFAInitSkippedEventMapper) @@ -60,7 +60,7 @@ func init() { eventstore.RegisterFilterEventMapper(AggregateType, HumanSignedOutType, HumanSignedOutEventMapper) eventstore.RegisterFilterEventMapper(AggregateType, HumanPasswordChangedType, HumanPasswordChangedEventMapper) eventstore.RegisterFilterEventMapper(AggregateType, HumanPasswordCodeAddedType, HumanPasswordCodeAddedEventMapper) - eventstore.RegisterFilterEventMapper(AggregateType, HumanPasswordCodeSentType, HumanPasswordCodeSentEventMapper) + eventstore.RegisterFilterEventMapper(AggregateType, HumanPasswordCodeSentType, eventstore.GenericEventMapper[HumanPasswordCodeSentEvent]) eventstore.RegisterFilterEventMapper(AggregateType, HumanPasswordChangeSentType, HumanPasswordChangeSentEventMapper) eventstore.RegisterFilterEventMapper(AggregateType, HumanPasswordCheckSucceededType, HumanPasswordCheckSucceededEventMapper) eventstore.RegisterFilterEventMapper(AggregateType, HumanPasswordCheckFailedType, HumanPasswordCheckFailedEventMapper) @@ -81,7 +81,7 @@ func init() { eventstore.RegisterFilterEventMapper(AggregateType, HumanPhoneVerifiedType, HumanPhoneVerifiedEventMapper) eventstore.RegisterFilterEventMapper(AggregateType, HumanPhoneVerificationFailedType, HumanPhoneVerificationFailedEventMapper) eventstore.RegisterFilterEventMapper(AggregateType, HumanPhoneCodeAddedType, HumanPhoneCodeAddedEventMapper) - eventstore.RegisterFilterEventMapper(AggregateType, HumanPhoneCodeSentType, HumanPhoneCodeSentEventMapper) + eventstore.RegisterFilterEventMapper(AggregateType, HumanPhoneCodeSentType, eventstore.GenericEventMapper[HumanPhoneCodeSentEvent]) eventstore.RegisterFilterEventMapper(AggregateType, HumanProfileChangedType, HumanProfileChangedEventMapper) eventstore.RegisterFilterEventMapper(AggregateType, HumanAvatarAddedType, HumanAvatarAddedEventMapper) eventstore.RegisterFilterEventMapper(AggregateType, HumanAvatarRemovedType, HumanAvatarRemovedEventMapper) diff --git a/internal/repository/user/human_mfa_otp.go b/internal/repository/user/human_mfa_otp.go index f0f3762c8..93706d714 100644 --- a/internal/repository/user/human_mfa_otp.go +++ b/internal/repository/user/human_mfa_otp.go @@ -7,6 +7,7 @@ import ( "github.com/zitadel/zitadel/internal/api/http" "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/notification/senders" "github.com/zitadel/zitadel/internal/zerrors" ) @@ -280,6 +281,7 @@ type HumanOTPSMSCodeAddedEvent struct { Code *crypto.CryptoValue `json:"code,omitempty"` Expiry time.Duration `json:"expiry,omitempty"` TriggeredAtOrigin string `json:"triggerOrigin,omitempty"` + GeneratorID string `json:"generatorId,omitempty"` *AuthRequestInfo } @@ -305,6 +307,7 @@ func NewHumanOTPSMSCodeAddedEvent( code *crypto.CryptoValue, expiry time.Duration, info *AuthRequestInfo, + generatorID string, ) *HumanOTPSMSCodeAddedEvent { return &HumanOTPSMSCodeAddedEvent{ BaseEvent: *eventstore.NewBaseEventForPush( @@ -316,15 +319,14 @@ func NewHumanOTPSMSCodeAddedEvent( Expiry: expiry, TriggeredAtOrigin: http.DomainContext(ctx).Origin(), AuthRequestInfo: info, + GeneratorID: generatorID, } } type HumanOTPSMSCodeSentEvent struct { eventstore.BaseEvent `json:"-"` - Code *crypto.CryptoValue `json:"code,omitempty"` - Expiry time.Duration `json:"expiry,omitempty"` - *AuthRequestInfo + GeneratorInfo *senders.CodeGeneratorInfo `json:"generatorInfo,omitempty"` } func (e *HumanOTPSMSCodeSentEvent) Payload() interface{} { @@ -342,6 +344,7 @@ func (e *HumanOTPSMSCodeSentEvent) SetBaseEvent(event *eventstore.BaseEvent) { func NewHumanOTPSMSCodeSentEvent( ctx context.Context, aggregate *eventstore.Aggregate, + generatorInfo *senders.CodeGeneratorInfo, ) *HumanOTPSMSCodeSentEvent { return &HumanOTPSMSCodeSentEvent{ BaseEvent: *eventstore.NewBaseEventForPush( @@ -349,6 +352,7 @@ func NewHumanOTPSMSCodeSentEvent( aggregate, HumanOTPSMSCodeSentType, ), + GeneratorInfo: generatorInfo, } } diff --git a/internal/repository/user/human_password.go b/internal/repository/user/human_password.go index c425c144b..4251a7987 100644 --- a/internal/repository/user/human_password.go +++ b/internal/repository/user/human_password.go @@ -8,6 +8,7 @@ import ( "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/notification/senders" "github.com/zitadel/zitadel/internal/zerrors" ) @@ -89,6 +90,7 @@ type HumanPasswordCodeAddedEvent struct { TriggeredAtOrigin string `json:"triggerOrigin,omitempty"` // AuthRequest is only used in V1 Login UI AuthRequestID string `json:"authRequestID,omitempty"` + GeneratorID string `json:"generatorId,omitempty"` } func (e *HumanPasswordCodeAddedEvent) Payload() interface{} { @@ -109,7 +111,8 @@ func NewHumanPasswordCodeAddedEvent( code *crypto.CryptoValue, expiry time.Duration, notificationType domain.NotificationType, - authRequestID string, + authRequestID, + generatorID string, ) *HumanPasswordCodeAddedEvent { return &HumanPasswordCodeAddedEvent{ BaseEvent: *eventstore.NewBaseEventForPush( @@ -122,6 +125,7 @@ func NewHumanPasswordCodeAddedEvent( NotificationType: notificationType, TriggeredAtOrigin: http.DomainContext(ctx).Origin(), AuthRequestID: authRequestID, + GeneratorID: generatorID, } } @@ -162,33 +166,34 @@ func HumanPasswordCodeAddedEventMapper(event eventstore.Event) (eventstore.Event } type HumanPasswordCodeSentEvent struct { - eventstore.BaseEvent `json:"-"` + *eventstore.BaseEvent `json:"-"` + + GeneratorInfo *senders.CodeGeneratorInfo `json:"generatorInfo,omitempty"` +} + +func (e *HumanPasswordCodeSentEvent) SetBaseEvent(event *eventstore.BaseEvent) { + e.BaseEvent = event } func (e *HumanPasswordCodeSentEvent) Payload() interface{} { - return nil + return e } func (e *HumanPasswordCodeSentEvent) UniqueConstraints() []*eventstore.UniqueConstraint { return nil } -func NewHumanPasswordCodeSentEvent(ctx context.Context, aggregate *eventstore.Aggregate) *HumanPasswordCodeSentEvent { +func NewHumanPasswordCodeSentEvent(ctx context.Context, aggregate *eventstore.Aggregate, generatorInfo *senders.CodeGeneratorInfo) *HumanPasswordCodeSentEvent { return &HumanPasswordCodeSentEvent{ - BaseEvent: *eventstore.NewBaseEventForPush( + BaseEvent: eventstore.NewBaseEventForPush( ctx, aggregate, HumanPasswordCodeSentType, ), + GeneratorInfo: generatorInfo, } } -func HumanPasswordCodeSentEventMapper(event eventstore.Event) (eventstore.Event, error) { - return &HumanPasswordCodeSentEvent{ - BaseEvent: *eventstore.BaseEventFromRepo(event), - }, nil -} - type HumanPasswordChangeSentEvent struct { eventstore.BaseEvent `json:"-"` } diff --git a/internal/repository/user/human_phone.go b/internal/repository/user/human_phone.go index 5655b1b3d..b86cbf93e 100644 --- a/internal/repository/user/human_phone.go +++ b/internal/repository/user/human_phone.go @@ -8,6 +8,7 @@ import ( "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/notification/senders" "github.com/zitadel/zitadel/internal/zerrors" ) @@ -151,6 +152,7 @@ type HumanPhoneCodeAddedEvent struct { Code *crypto.CryptoValue `json:"code,omitempty"` Expiry time.Duration `json:"expiry,omitempty"` CodeReturned bool `json:"code_returned,omitempty"` + GeneratorID string `json:"generatorId,omitempty"` TriggeredAtOrigin string `json:"triggerOrigin,omitempty"` } @@ -171,15 +173,18 @@ func NewHumanPhoneCodeAddedEvent( aggregate *eventstore.Aggregate, code *crypto.CryptoValue, expiry time.Duration, + generatorID string, ) *HumanPhoneCodeAddedEvent { - return NewHumanPhoneCodeAddedEventV2(ctx, aggregate, code, expiry, false) + return NewHumanPhoneCodeAddedEventV2(ctx, aggregate, code, expiry, false, generatorID) } + func NewHumanPhoneCodeAddedEventV2( ctx context.Context, aggregate *eventstore.Aggregate, code *crypto.CryptoValue, expiry time.Duration, codeReturned bool, + generatorID string, ) *HumanPhoneCodeAddedEvent { return &HumanPhoneCodeAddedEvent{ BaseEvent: *eventstore.NewBaseEventForPush( @@ -190,6 +195,7 @@ func NewHumanPhoneCodeAddedEventV2( Code: code, Expiry: expiry, CodeReturned: codeReturned, + GeneratorID: generatorID, TriggeredAtOrigin: http.DomainContext(ctx).Origin(), } } @@ -207,7 +213,13 @@ func HumanPhoneCodeAddedEventMapper(event eventstore.Event) (eventstore.Event, e } type HumanPhoneCodeSentEvent struct { - eventstore.BaseEvent `json:"-"` + *eventstore.BaseEvent `json:"-"` + + GeneratorInfo *senders.CodeGeneratorInfo `json:"generatorInfo,omitempty"` +} + +func (e *HumanPhoneCodeSentEvent) SetBaseEvent(event *eventstore.BaseEvent) { + e.BaseEvent = event } func (e *HumanPhoneCodeSentEvent) Payload() interface{} { @@ -218,18 +230,13 @@ func (e *HumanPhoneCodeSentEvent) UniqueConstraints() []*eventstore.UniqueConstr return nil } -func NewHumanPhoneCodeSentEvent(ctx context.Context, aggregate *eventstore.Aggregate) *HumanPhoneCodeSentEvent { +func NewHumanPhoneCodeSentEvent(ctx context.Context, aggregate *eventstore.Aggregate, generatorInfo *senders.CodeGeneratorInfo) *HumanPhoneCodeSentEvent { return &HumanPhoneCodeSentEvent{ - BaseEvent: *eventstore.NewBaseEventForPush( + BaseEvent: eventstore.NewBaseEventForPush( ctx, aggregate, HumanPhoneCodeSentType, ), + GeneratorInfo: generatorInfo, } } - -func HumanPhoneCodeSentEventMapper(event eventstore.Event) (eventstore.Event, error) { - return &HumanPhoneCodeSentEvent{ - BaseEvent: *eventstore.BaseEventFromRepo(event), - }, nil -} diff --git a/internal/repository/user/schemauser/phone.go b/internal/repository/user/schemauser/phone.go index a491dab77..9a6816819 100644 --- a/internal/repository/user/schemauser/phone.go +++ b/internal/repository/user/schemauser/phone.go @@ -8,6 +8,7 @@ import ( "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/notification/senders" ) const ( @@ -107,6 +108,7 @@ type PhoneCodeAddedEvent struct { Code *crypto.CryptoValue `json:"code,omitempty"` Expiry time.Duration `json:"expiry,omitempty"` CodeReturned bool `json:"code_returned,omitempty"` + GeneratorID string `json:"generatorId,omitempty"` TriggeredAtOrigin string `json:"triggerOrigin,omitempty"` } @@ -132,6 +134,7 @@ func NewPhoneCodeAddedEvent( code *crypto.CryptoValue, expiry time.Duration, codeReturned bool, + generatorID string, ) *PhoneCodeAddedEvent { return &PhoneCodeAddedEvent{ BaseEvent: eventstore.NewBaseEventForPush( @@ -142,12 +145,15 @@ func NewPhoneCodeAddedEvent( Code: code, Expiry: expiry, CodeReturned: codeReturned, + GeneratorID: generatorID, TriggeredAtOrigin: http.DomainContext(ctx).Origin(), } } type PhoneCodeSentEvent struct { *eventstore.BaseEvent `json:"-"` + + GeneratorInfo *senders.CodeGeneratorInfo `json:"generatorInfo,omitempty"` } func (e *PhoneCodeSentEvent) Payload() interface{} { @@ -162,12 +168,13 @@ func (e *PhoneCodeSentEvent) SetBaseEvent(event *eventstore.BaseEvent) { e.BaseEvent = event } -func NewPhoneCodeSentEvent(ctx context.Context, aggregate *eventstore.Aggregate) *PhoneCodeSentEvent { +func NewPhoneCodeSentEvent(ctx context.Context, aggregate *eventstore.Aggregate, generatorInfo *senders.CodeGeneratorInfo) *PhoneCodeSentEvent { return &PhoneCodeSentEvent{ BaseEvent: eventstore.NewBaseEventForPush( ctx, aggregate, PhoneCodeSentType, ), + GeneratorInfo: generatorInfo, } } diff --git a/internal/static/i18n/en.yaml b/internal/static/i18n/en.yaml index 4f4e06fd7..110a8d71e 100644 --- a/internal/static/i18n/en.yaml +++ b/internal/static/i18n/en.yaml @@ -53,6 +53,7 @@ Errors: NotFound: SMS configuration not found AlreadyActive: SMS configuration already active AlreadyDeactivated: SMS configuration already deactivated + NotExternalVerification: SMS configuration does not support code verification SMTP: NotEmailMessage: message is not EmailMessage RequiredAttributes: subject, recipients and content must be set but some or all of them are empty diff --git a/proto/zitadel/admin.proto b/proto/zitadel/admin.proto index 6e1020cf5..d3cf774f4 100644 --- a/proto/zitadel/admin.proto +++ b/proto/zitadel/admin.proto @@ -5176,11 +5176,10 @@ message AddSMSProviderTwilioRequest { } ]; string sender_number = 3 [ - (validate.rules).string = {min_len: 1, max_len: 200}, - (google.api.field_behavior) = REQUIRED, + (validate.rules).string = {min_len: 0, max_len: 200}, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { example: "\"AB123b9e61d238abae7d3be7b65ecbc987\""; - min_length: 1; + min_length: 0; max_length: 200; } ]; @@ -5192,6 +5191,14 @@ message AddSMSProviderTwilioRequest { max_length: 200; } ]; + string verify_service_sid = 5 [ + (validate.rules).string = {min_len: 0, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"AB123b9e61d238abae7d3be7b65ecbc987\""; + min_length: 0; + max_length: 200; + } + ]; } message AddSMSProviderTwilioResponse { @@ -5211,8 +5218,7 @@ message UpdateSMSProviderTwilioRequest { } ]; string sender_number = 3 [ - (validate.rules).string = {min_len: 1, max_len: 200}, - (google.api.field_behavior) = REQUIRED, + (validate.rules).string = {min_len: 0, max_len: 200}, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { example: "\"AB123b9e61d238abae7d3be7b65ecbc987\""; min_length: 1; @@ -5227,6 +5233,14 @@ message UpdateSMSProviderTwilioRequest { max_length: 200; } ]; + string verify_service_sid = 5 [ + (validate.rules).string = {min_len: 0, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"AB123b9e61d238abae7d3be7b65ecbc987\""; + min_length: 0; + max_length: 200; + } + ]; } message UpdateSMSProviderTwilioResponse { diff --git a/proto/zitadel/settings.proto b/proto/zitadel/settings.proto index a2a6806c6..c761e1c84 100644 --- a/proto/zitadel/settings.proto +++ b/proto/zitadel/settings.proto @@ -161,6 +161,7 @@ message SMSProvider { message TwilioConfig { string sid = 1; string sender_number = 2; + string verify_service_sid = 3; } message HTTPConfig {