Merge branch 'main' into next

# Conflicts:
#	console/src/assets/i18n/pt.json
This commit is contained in:
Livio Spring 2023-08-04 17:17:10 +02:00
commit 6959392fab
No known key found for this signature in database
GPG Key ID: 26BB1C2FA5952CF0
110 changed files with 5269 additions and 528 deletions

View File

@ -383,7 +383,12 @@ SystemDefaults:
# Hasher:
# Algorithm: "scrypt"
# Cost: 15
# Hasher:
# Algorithm: "pbkdf2"
# Rounds: 290000
# Hash: "sha256" # Can be "sha1", "sha224", "sha256", "sha384" or "sha512"
# Verifiers enable the possibility of verifying
# passwords that are previously hashed using another
# algorithm then the Hasher.
@ -402,6 +407,7 @@ SystemDefaults:
# - "bcrypt"
# - "md5"
# - "scrypt"
# - "pbkdf2" # verifier for all pbkdf2 hash modes.
Multifactors:
OTP:
# If this is empty, the issuer is the requested domain
@ -564,6 +570,20 @@ DefaultInstance:
IncludeUpperLetters: true
IncludeDigits: true
IncludeSymbols: false
OTPSMS:
Length: 8
Expiry: "5m"
IncludeLowerLetters: false
IncludeUpperLetters: false
IncludeDigits: true
IncludeSymbols: false
OTPEmail:
Length: 8
Expiry: "5m"
IncludeLowerLetters: false
IncludeUpperLetters: false
IncludeDigits: true
IncludeSymbols: false
PasswordComplexityPolicy:
MinLength: 8
HasLowercase: true

View File

@ -146,7 +146,12 @@ export class FactorTableComponent {
this.componentType === LoginMethodComponentType.MultiFactor
? [MultiFactorType.MULTI_FACTOR_TYPE_U2F_WITH_VERIFICATION]
: this.componentType === LoginMethodComponentType.SecondFactor
? [SecondFactorType.SECOND_FACTOR_TYPE_U2F, SecondFactorType.SECOND_FACTOR_TYPE_OTP]
? [
SecondFactorType.SECOND_FACTOR_TYPE_U2F,
SecondFactorType.SECOND_FACTOR_TYPE_OTP,
SecondFactorType.SECOND_FACTOR_TYPE_OTP_SMS,
SecondFactorType.SECOND_FACTOR_TYPE_OTP_EMAIL,
]
: [];
const filtered = (allTypes as Array<MultiFactorType | SecondFactorType>).filter((type) => !this.list.includes(type));

View File

@ -3,15 +3,6 @@
</h1>
<div mat-dialog-content>
<form *ngIf="specsForm" (ngSubmit)="closeDialogWithRequest()" [formGroup]="specsForm">
<cnsl-form-field class="type-form-field" label="Access Code" required="true">
<cnsl-label>{{ 'SETTING.SECRETS.GENERATORTYPE' | translate }}</cnsl-label>
<mat-select formControlName="generatorType">
<mat-option *ngFor="let gen of availableGenerators" [value]="gen">
<span>{{ 'SETTING.SECRETS.TYPE.' + gen | translate }}</span>
</mat-option>
</mat-select>
</cnsl-form-field>
<h2 class="generator-type">{{ 'SETTING.SECRETS.TYPE.' + generatorType?.value | translate }}</h2>
<cnsl-form-field class="generator-form-field" label="Expiration">

View File

@ -7,7 +7,6 @@ import {
import { Duration } from 'google-protobuf/google/protobuf/duration_pb';
import { requiredValidator } from 'src/app/modules/form-field/validators/validators';
import { UpdateSecretGeneratorRequest } from 'src/app/proto/generated/zitadel/admin_pb';
import { SecretGeneratorType } from 'src/app/proto/generated/zitadel/settings_pb';
@Component({
selector: 'cnsl-dialog-add-secret-generator',
@ -15,15 +14,6 @@ import { SecretGeneratorType } from 'src/app/proto/generated/zitadel/settings_pb
styleUrls: ['./dialog-add-secret-generator.component.scss'],
})
export class DialogAddSecretGeneratorComponent {
public SecretGeneratorType: any = SecretGeneratorType;
public availableGenerators: SecretGeneratorType[] = [
SecretGeneratorType.SECRET_GENERATOR_TYPE_INIT_CODE,
SecretGeneratorType.SECRET_GENERATOR_TYPE_VERIFY_EMAIL_CODE,
SecretGeneratorType.SECRET_GENERATOR_TYPE_VERIFY_PHONE_CODE,
SecretGeneratorType.SECRET_GENERATOR_TYPE_PASSWORD_RESET_CODE,
SecretGeneratorType.SECRET_GENERATOR_TYPE_PASSWORDLESS_INIT_CODE,
SecretGeneratorType.SECRET_GENERATOR_TYPE_APP_SECRET,
];
public req: UpdateSecretGeneratorRequest = new UpdateSecretGeneratorRequest();
public specsForm!: UntypedFormGroup;
@ -33,17 +23,19 @@ export class DialogAddSecretGeneratorComponent {
public dialogRef: MatDialogRef<DialogAddSecretGeneratorComponent>,
@Inject(MAT_DIALOG_DATA) public data: any,
) {
let exp = 1;
if (data.config?.expiry !== undefined) {
exp = this.durationToHour(data.config?.expiry);
}
this.specsForm = this.fb.group({
generatorType: [SecretGeneratorType.SECRET_GENERATOR_TYPE_APP_SECRET, [requiredValidator]],
expiry: [1, [requiredValidator]],
includeDigits: [true, [requiredValidator]],
includeLowerLetters: [true, [requiredValidator]],
includeSymbols: [true, [requiredValidator]],
includeUpperLetters: [true, [requiredValidator]],
length: [6, [requiredValidator]],
generatorType: [data.type, [requiredValidator]],
expiry: [exp, [requiredValidator]],
length: [data.config?.length ?? 6, [requiredValidator]],
includeDigits: [data.config?.includeDigits ?? true, [requiredValidator]],
includeLowerLetters: [data.config?.includeSymbols ?? true, [requiredValidator]],
includeSymbols: [data.config?.includeLowerLetters ?? true, [requiredValidator]],
includeUpperLetters: [data.config?.includeUpperLetters ?? true, [requiredValidator]],
});
this.generatorType?.setValue(data.type);
}
public closeDialog(): void {
@ -52,10 +44,7 @@ export class DialogAddSecretGeneratorComponent {
public closeDialogWithRequest(): void {
this.req.setGeneratorType(this.generatorType?.value);
const expiry = new Duration().setSeconds((this.expiry?.value ?? 1) * 60 * 60);
this.req.setExpiry(expiry);
this.req.setExpiry(this.hourToDuration(this.expiry?.value));
this.req.setIncludeDigits(this.includeDigits?.value);
this.req.setIncludeLowerLetters(this.includeLowerLetters?.value);
this.req.setIncludeSymbols(this.includeSymbols?.value);
@ -92,4 +81,18 @@ export class DialogAddSecretGeneratorComponent {
public get length(): AbstractControl | null {
return this.specsForm.get('length');
}
private durationToHour(duration: Duration.AsObject): number {
if (duration.seconds === 0) {
return 0;
}
return (duration.seconds + duration.nanos / 1000000) / 3600;
}
private hourToDuration(hour: number): Duration {
const exp = hour * 60 * 60;
const sec = Math.floor(exp);
const nanos = Math.round((exp - sec) * 1000000);
return new Duration().setSeconds(sec).setNanos(nanos);
}
}

View File

@ -1,6 +1,6 @@
import { Component, OnInit } from '@angular/core';
import { MatLegacyDialog as MatDialog } from '@angular/material/legacy-dialog';
import { UpdateSecretGeneratorRequest, UpdateSecretGeneratorResponse } from 'src/app/proto/generated/zitadel/admin_pb';
import { UpdateSecretGeneratorRequest } from 'src/app/proto/generated/zitadel/admin_pb';
import { OIDCSettings, SecretGenerator, SecretGeneratorType } from 'src/app/proto/generated/zitadel/settings_pb';
import { AdminService } from 'src/app/services/admin.service';
import { ToastService } from 'src/app/services/toast.service';
@ -25,7 +25,10 @@ export class SecretGeneratorComponent implements OnInit {
SecretGeneratorType.SECRET_GENERATOR_TYPE_PASSWORD_RESET_CODE,
SecretGeneratorType.SECRET_GENERATOR_TYPE_PASSWORDLESS_INIT_CODE,
SecretGeneratorType.SECRET_GENERATOR_TYPE_APP_SECRET,
SecretGeneratorType.SECRET_GENERATOR_TYPE_OTP_SMS,
SecretGeneratorType.SECRET_GENERATOR_TYPE_OTP_EMAIL,
];
constructor(private service: AdminService, private toast: ToastService, private dialog: MatDialog) {}
ngOnInit(): void {
@ -48,25 +51,12 @@ export class SecretGeneratorComponent implements OnInit {
});
}
private updateData(): Promise<UpdateSecretGeneratorResponse.AsObject> | void {
const dialogRef = this.dialog.open(DialogAddSecretGeneratorComponent, {
data: {},
width: '400px',
});
dialogRef.afterClosed().subscribe((req: UpdateSecretGeneratorRequest) => {
if (req) {
return (this.service as AdminService).updateSecretGenerator(req);
} else {
return;
}
});
}
public openGeneratorDialog(generatorType: SecretGeneratorType): void {
let config = this.generators.find((gen) => gen.generatorType === generatorType);
const dialogRef = this.dialog.open(DialogAddSecretGeneratorComponent, {
data: {
type: generatorType,
config: config,
},
width: '400px',
});
@ -77,6 +67,9 @@ export class SecretGeneratorComponent implements OnInit {
.updateSecretGenerator(req)
.then(() => {
this.toast.showInfo('SETTING.SECRETS.UPDATED', true);
setTimeout(() => {
this.fetchData();
}, 2000);
})
.catch((error) => {
this.toast.showError(error);
@ -86,21 +79,4 @@ export class SecretGeneratorComponent implements OnInit {
}
});
}
public savePolicy(): void {
const prom = this.updateData();
if (prom) {
prom
.then(() => {
this.toast.showInfo('SETTING.SMTP.SAVED', true);
this.loading = true;
setTimeout(() => {
this.fetchData();
}, 2000);
})
.catch((error) => {
this.toast.showError(error);
});
}
}
}

View File

@ -1095,7 +1095,9 @@
"3": "Телефонна проверка",
"4": "Нулиране на парола",
"5": "Инициализация без парола",
"6": "Тайна на приложението"
"6": "Тайна на приложението",
"7": "Еднократна парола (OTP) - SMS",
"8": "Еднократна парола (OTP) имейл"
},
"ADDGENERATOR": "Определете тайния външен вид",
"GENERATORTYPE": "Тип",
@ -1838,8 +1840,10 @@
},
"SECONDFACTORTYPES": {
"0": "неизвестен",
"1": "Еднократна парола (OTP)",
"2": "Пръстов отпечатък, ключове за сигурност, Face ID и други"
"1": "Еднократна парола чрез приложение за удостоверяване на автентичността (TOTP)",
"2": "Пръстов отпечатък, ключове за сигурност, Face ID и други",
"3": "Еднократна парола по имейл (Email OTP)",
"4": "Еднократна парола чрез SMS (SMS OTP)"
}
},
"LOGINPOLICY": {

View File

@ -1101,7 +1101,9 @@
"3": "Telefonnummer Verificationscode",
"4": "Passwort Zurücksetzen Code",
"5": "Passwordless Initialisierungscode",
"6": "Applicationssecret"
"6": "Applicationssecret",
"7": "One Time Password (OTP) - SMS",
"8": "One Time Password (OTP) - Email"
},
"ADDGENERATOR": "Secret Erscheinungsbild definieren",
"GENERATORTYPE": "Typ",
@ -1847,8 +1849,10 @@
},
"SECONDFACTORTYPES": {
"0": "Unknown",
"1": "One Time Password (OTP)",
"2": "Fingerabdruck, Security Keys, Face ID und andere"
"1": "One Time Password per Authenticator App (TOTP)",
"2": "Fingerabdruck, Security Keys, Face ID und andere",
"3": "One Time Password per Email (Email OTP)",
"4": "One Time Password per SMS (SMS OTP)"
}
},
"LOGINPOLICY": {

View File

@ -1102,7 +1102,9 @@
"3": "Phone verification",
"4": "Password Reset",
"5": "Passwordless Initialization",
"6": "App Secret"
"6": "App Secret",
"7": "One Time Password (OTP) - SMS",
"8": "One Time Password (OTP) - Email"
},
"ADDGENERATOR": "Define Secret Appearance",
"GENERATORTYPE": "Type",
@ -1844,8 +1846,10 @@
},
"SECONDFACTORTYPES": {
"0": "Unknown",
"1": "One Time Password (OTP)",
"2": "Fingerprint, Security Keys, Face ID and other"
"1": "One Time Password by Authenticator App (TOTP)",
"2": "Fingerprint, Security Keys, Face ID and other",
"3": "One Time Password by Email (Email OTP)",
"4": "One Time Password by SMS (SMS OTP)"
}
},
"LOGINPOLICY": {

View File

@ -1102,7 +1102,9 @@
"3": "Verificación de teléfono",
"4": "Restablecimiento de contraseña",
"5": "Inicialización de acceso sin contraseña",
"6": "Secreto de App"
"6": "Secreto de App",
"7": "One Time Password (OTP) - SMS",
"8": "One Time Password (OTP) - email"
},
"ADDGENERATOR": "Definir apariencia del secreto",
"GENERATORTYPE": "Tipo",
@ -1844,8 +1846,10 @@
},
"SECONDFACTORTYPES": {
"0": "Desconocido",
"1": "One Time Password (OTP)",
"2": "Huella dactilar, claves de seguridad, Face ID y otros"
"1": "One Time Password por Authenticator App (TOTP)",
"2": "Huella dactilar, claves de seguridad, Face ID y otros",
"3": "One Time Password por email (Email OTP)",
"4": "One Time Password por SMS (SMS OTP)"
}
},
"LOGINPOLICY": {

View File

@ -1101,7 +1101,9 @@
"3": "Vérification par téléphone",
"4": "Réinitialisation du mot de passe",
"5": "Initialisation sans mot de passe",
"6": "Secret de l'application"
"6": "Secret de l'application",
"7": "Mot de passe à usage unique (OTP) - SMS",
"8": "Mot de passe à usage unique (OTP) - e-mail"
},
"ADDGENERATOR": "Définir l'apparence du secret",
"GENERATORTYPE": "Type",
@ -1848,8 +1850,10 @@
},
"SECONDFACTORTYPES": {
"0": "Inconnu",
"1": "Mot de passe à usage unique (OTP)",
"2": "Empreinte digitale, clés de sécurité, Face ID et autres"
"1": "One Time Password par authenticator app (TOTP)",
"2": "Empreinte digitale, clés de sécurité, Face ID et autres",
"3": "One Time Password par email (Email OTP)",
"4": "One Time Password par SMS (SMS OTP)"
}
},
"LOGINPOLICY": {

View File

@ -1101,7 +1101,9 @@
"3": "Verificazione del numero di telefono",
"4": "Ripristino Password",
"5": "Inizializzazione Passwordless",
"6": "Segreto dell'applicazione"
"6": "Segreto dell'applicazione",
"7": "One Time Password (OTP) - SMS",
"8": "One Time Password (OTP) - email"
},
"ADDGENERATOR": "Definisci aspetto",
"GENERATORTYPE": "Tipo",
@ -1848,8 +1850,10 @@
},
"SECONDFACTORTYPES": {
"0": "Sconosciuto",
"1": "One Time Password (OTP)",
"2": "Impronta digitale, chiave di sicurezza, Face ID e altri"
"1": "One Time Password per Authenticator App (TOTP)",
"2": "Impronta digitale, chiave di sicurezza, Face ID e altri",
"3": "One Time Password per Email (Email OTP)",
"4": "One Time Password per SMS (SMS OTP)"
}
},
"LOGINPOLICY": {

View File

@ -1102,7 +1102,9 @@
"3": "電話番号認証",
"4": "パスワードのリセット",
"5": "パスワードレスの初期設定",
"6": "アプリのシークレット"
"6": "アプリのシークレット",
"7": "ワンタイムパスワード (OTP) - SMS",
"8": "ワンタイムパスワード (OTP) - 電子メール"
},
"ADDGENERATOR": "シークレットの設定を定義する",
"GENERATORTYPE": "タイプ",
@ -1839,8 +1841,10 @@
},
"SECONDFACTORTYPES": {
"0": "不明",
"1": "ワンタイムパスワードOTP",
"2": "指紋、セキュリティキー、フェイスIDなど"
"1": "認証アプリ用ワンタイムパスワード(TOTP)",
"2": "指紋、セキュリティキー、フェイスIDなど",
"3": "Eメール用ワンタイムパスワードemail OTP",
"4": "SMS用ワンタイムパスワードSMS OTP"
}
},
"LOGINPOLICY": {

View File

@ -1102,7 +1102,9 @@
"3": "Телефонска верификација",
"4": "Промена на лозинка",
"5": "Иницијализација на најава без лозинка",
"6": "Апликациска тајна"
"6": "Апликациска тајна",
"7": "Еднократна лозинка (OTP) - СМС",
"8": "Еднократна лозинка (OTP) - е-пошта"
},
"ADDGENERATOR": "Дефинирајте изглед на тајна",
"GENERATORTYPE": "Тип",
@ -1844,8 +1846,10 @@
},
"SECONDFACTORTYPES": {
"0": "Непознато",
"1": "Еднократна лозинка (OTP)",
"2": "Отисок на прст, безбедносни клучеви, Face ID и другo"
"1": "Еднократна лозинка преку апликација за автентикатор (TOTP)",
"2": "Отпечаток на прст, безбедносни клучеви, Face ID и други",
"3": "Еднократна лозинка по е-пошта (Еmail OTP)",
"4": "Еднократна лозинка преку СМС (SMS OTP)"
}
},
"LOGINPOLICY": {

View File

@ -1101,7 +1101,9 @@
"3": "Weryfikacja telefonu",
"4": "Resetowanie hasła",
"5": "Inicjalizacja bez hasła",
"6": "Sekret aplikacji"
"6": "Sekret aplikacji",
"7": "Hasło jednorazowe (OTP) - SMS",
"8": "Hasło jednorazowe (OTP) — e-mail"
},
"ADDGENERATOR": "Zdefiniuj wygląd sekretu",
"GENERATORTYPE": "Typ",
@ -1848,8 +1850,10 @@
},
"SECONDFACTORTYPES": {
"0": "Nieznany",
"1": "Jednorazowe hasło (OTP)",
"2": "Odcisk palca, klucze bezpieczeństwa, Face ID i inne"
"1": "Hasło jednorazowe dla aplikacji uwierzytelniającej (TOTP)",
"2": "Odcisk palca, Klucze Bezpieczeństwa, Face ID i inne",
"3": "Hasło jednorazowe dla wiadomości e-mail (Email OTP)",
"4": "Hasło jednorazowe dla wiadomości SMS (SMS OTP)"
}
},
"LOGINPOLICY": {

View File

@ -1102,7 +1102,9 @@
"3": "Verificação de telefone",
"4": "Redefinição de senha",
"5": "Inicialização sem senha",
"6": "Segredo do aplicativo"
"6": "Segredo do aplicativo",
"7": "Senha única (OTP) - SMS",
"8": "Senha única (OTP) - e-mail"
},
"ADDGENERATOR": "Definir aparência de segredo",
"GENERATORTYPE": "Tipo",
@ -1842,8 +1844,10 @@
},
"SECONDFACTORTYPES": {
"0": "Desconhecido",
"1": "Senha de Uso Único (OTP)",
"2": "Impressão Digital, Chaves de Segurança, Face ID e outros"
"1": "Senha de uso único para o aplicativo autenticador (TOTP)",
"2": "Impressão digital, Chaves de Segurança, Face ID e outros",
"3": "Senha de uso único para e-mail (Email OTP)",
"4": "Senha de uso único para SMS (SMS OTP)"
}
},
"LOGINPOLICY": {

View File

@ -1101,7 +1101,9 @@
"3": "电话号码验证",
"4": "重置密码",
"5": "无密码认证初始化",
"6": "App 验证"
"6": "App 验证",
"7": "一次性密码 (OTP) - SMS",
"8": "一次性密码 (OTP) - 电子邮件"
},
"ADDGENERATOR": "定义验证码外观",
"GENERATORTYPE": "类型",
@ -1847,8 +1849,10 @@
},
"SECONDFACTORTYPES": {
"0": "未知",
"1": "一次性密码 (OTP)",
"2": "指纹、安全密钥、Face ID 等"
"1": "身份验证应用程序的一次性密码TOTP",
"2": "指纹、安全密钥、Face ID 等",
"3": "电子邮件一次性密码email OTP",
"4": "短信一次性密码SMS OTP"
}
},
"LOGINPOLICY": {

View File

@ -39,7 +39,7 @@ This function allows to call HTTP servers. The function does NOT fulfil the [Fet
#### Response
If the request was valid, an error will be thrown, otherwise a Response object will be returned.
If the request was invalid, an error will be thrown, otherwise a Response object will be returned.
The object has the following fields and methods:

View File

@ -145,7 +145,6 @@ The storage layer of ZITADEL is responsible for multiple things. For example:
- Backup and restore operation for disaster recovery purpose
ZITADEL currently supports CockroachDB as first choice of storage due to its perfect match for ZITADELs needs.
Postgres is currently in [Beta](/docs/support/software-release-cycles-support#beta) and will be [Enterprise Supported](/docs/support/software-release-cycles-support#partially-supported) afterwards.
Beta state will be removed as soon as [automated tests](https://github.com/zitadel/zitadel/issues/5741) are implemented.
Alternatively you can run ZITADEL also with Postgres which is [Enterprise Supported](/docs/support/software-release-cycles-support#partially-supported).
Make sure to read our [Production Guide](/docs/self-hosting/manage/production#prefer-cockroachdb) before you decide to use it.

View File

@ -9,8 +9,7 @@ Since the storage layer takes the heavy lifting of making sure that data in sync
Depending on your projects needs our general recommendation is to run ZITADEL and ZITADELs storage layer across multiple availability zones in the same region or if you need higher guarantees run the storage layer across multiple regions.
Consult the [CockroachDB documentation](https://www.cockroachlabs.com/docs/) for more details or use the [CockroachCloud Service](https://www.cockroachlabs.com/docs/cockroachcloud/create-an-account.html)
Postgres is currently in [Beta](/docs/support/software-release-cycles-support#beta) and will be [Enterprise Supported](/docs/support/software-release-cycles-support#partially-supported) afterwards.
Beta state will be removed as soon as [automated tests](https://github.com/zitadel/zitadel/issues/5741) are implemented.
Alternatively you can run ZITADEL also with Postgres which is [Enterprise Supported](/docs/support/software-release-cycles-support#partially-supported).
Make sure to read our [Production Guide](/self-hosting/manage/production#prefer-cockroachdb) before you decide to use it.
## Scalability

View File

@ -28,8 +28,16 @@ Please note that you only have to perform this migration if you already have an
If that isn't your case please just add a new provider from scratch.
To migrate to a specific provider, you need to follow a few essential steps:
1. Create a desired IDP as Terraform resource for example [Google](https://registry.terraform.io/providers/zitadel/zitadel/latest/docs/resources/idp_google).
2. Make the corresponding API call to [migrate the IDP](./migrate#google-configuration), save the ID of the IDP for the import
3. Before applying the Terraform resources again, import the new IDP resource.
2. Remove the old terraform resource from the state as to not destroy the migrated IDP accidentally.
```bash
# terraform state rm *address*
terraform state rm zitadel_idp_oidc.oidc_idp
```
After this command you can also remove the resource from the terraform files, as it is not managed anymore but also not deleted.
3. Make the corresponding API call to [migrate the IDP](./migrate#migrate-generic-oidc-provider), save the ID of the IDP for the import
4. Before applying the Terraform resources again, import the new IDP resource.
```bash
#resource "zitadel_idp_google" "google" {
# name = "Google"
@ -44,5 +52,6 @@ To migrate to a specific provider, you need to follow a few essential steps:
# terraform import zitadel_idp_google.*resource_name* *id*:*client_secret*
terraform import zitadel_idp_google.google 222302827723096428:GOCSPX-*****
```
You have now migrated your provider and you should be able to apply the resource again. There should be no changes and the IDP is maintained by Terraform again.
You have now migrated your provider and you should be able to apply the resource again. There should be no changes and the IDP is maintained by Terraform again.

View File

@ -6,7 +6,7 @@ sidebar_label: External Identity Provider
## Flow
The prerequisite for adding an external login (social and enterprise) to your user account is a registered identity provider on your ZITADEL instance or the organization of the user.
If you havent added a provider yet, have a look at the following guide first: [Identity Providers](https://zitadel.com/docs/guides/integrate/identity-providers)
If you havent added a provider yet, have a look at the following guide first: [Identity Providers](/docs/guides/integrate/identity-providers)
![Identity Provider Flow](/img/guides/login-ui/external-login-flow.png)
@ -20,7 +20,7 @@ Send the following two URLs in the request body:
2. ErrorURL: Page that should be shown when an error happens during the authentication
In the response, you will get an authentication URL of the provider you like.
[Start Identity Provider Flow Documentation](https://zitadel.com/docs/apis/resources/user_service/user-service-start-identity-provider-flow)
[Start Identity Provider Flow Documentation](/docs/apis/resources/user_service/user-service-start-identity-provider-flow)
### Request
@ -65,7 +65,7 @@ After the user has successfully authenticated, a redirect to the ZITADEL backend
ZITADEL will take the information of the provider. After this, a redirect will be made to either the success page in case of a successful login or to the error page in case of a failure will be performed. In the parameters, you will provide the intentID, a token, and optionally, if a user could be found, a user ID.
To get the information of the provider, make a request to ZITADEL.
[Get Identity Provider Information Documentation](https://zitadel.com/docs/apis/resources/user_service/user-service-retrieve-identity-provider-information)
[Get Identity Provider Information Documentation](/docs/apis/resources/user_service/user-service-retrieve-identity-provider-information)
### Request
```bash
@ -90,8 +90,8 @@ curl --request POST \
},
"idpInformation": {
"oauth": {
"accessToken": "ya29.a0AWY7CknrOORopcJK2XX2fQXV9NQpp8JdkKYx-mQZNrR-wktWWhc3QsepT6kloSCgFPS9N0beEBlEYoY5oYUhfc7VlLHTQz5iECE386pyx5YmTueyeQ9GXk1dAw89gi8KRyjNlJApFsfLJaoiLIvKJLf23eAyXgaCgYKAUMSARESFQG1tDrpnTJ2su8BBO24zfmzgneARw0165",
"idToken": "eyJhbGciOiJSUzI1NiIsImtpZCI6Ijg1YmE5MzEzZmQ3YTdkNGFmYTg0ODg0YWJjYzg0MDMwMDQzNjMxODAiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20iLCJhenAiOiIxODI5MDIwMjY1MDgtdW1taXQ3dHZjbHBnM2NxZmM4b2ljdGE1czI1aGtudWwuYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJhdWQiOiIxODI5MDIwMjY1MDgtdW1taXQ3dHZjbHBnM2NxZmM4b2ljdGE1czI1aGtudWwuYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJzdWIiOiIxMTEzOTI4MDU5NzU3MTU4NTY2MzciLCJoZCI6InJvb3RkLmNoIiwiZW1haWwiOiJmYWJpZW5uZUByb290ZC5jaCIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJhdF9oYXNoIjoidGN5X25JTkZHNnFhRTBZTWFsQzZGdyIsIm5hbWUiOiJGYWJpZW5uZSBCw7xobGVyIiwicGljdHVyZSI6Imh0dHBzOi8vbGgzLmdvb2dsZXVzZXJjb250ZW50LmNvbS9hL0FBY0hUdGY5NzNRNk5IOEt6S1RNRVpFTFBVOWx4NDVXcFE5RlJCdXhGZFBiPXM5Ni1jIiwiZ2l2ZW5fbmFtZSI6IkZhYmllbm5lIiwiZmFtaWx5X25hbWUiOiJCw7xobGVyIiwibG9jYWxlIjoiZGUiLCJpYXQiOjE2ODY4MTE0NjUsImV4cCI6MTY4NjgxNTA2NX0.PwlAHRM44e8eYyHzdfotOrcq5GZc4D15mWvN3rGdoDmu2RRgb4T0nDgkY6AVq2vNJxPfbiB1jFtNP6dgX-OgLIxNXg_tJJhwFh-eFPA37cIiv1CEcgEC-q1zXFIa3HrwHLreeU6i7C9JkDrKpkS-AKat1krf27QXxrxHLrWehi5F2l1OZuAKFWYaYmJOd0sVTDBA2o5SDcAiQs_D4-Q-kSl5f0gh607YVHLv7zjyfHtAOs7xPEkNEUVcqGBke2Zy9kAYIgiMriNxLA2EDxFtSyWnf-bCXKnuVX2hwEY0T0lUPrOAVkz7MEOKiacE2xAOczoQvl-wECU0UofLi8XZqg"
"accessToken": "ya29...",
"idToken": "ey..."
},
"idpId": "218528353504723201",
"rawInformation": {
@ -103,7 +103,7 @@ curl --request POST \
"hd": "mouse.com",
"locale": "de",
"name": "Minnie Mouse",
"picture": "https://lh3.googleusercontent.com/a/AAcKTtf973Q6NH8KzKTMEZELPU9lx45WpQ9FRBuxFdPb=s96-c",
"picture": "https://lh3.googleusercontent.com/a/AAcKTtf973Q7NH8KzKTMEZELPU9lx45WpQ9FRBuxFdPb=s96-c",
"sub": "111392805975715856637"
}
}
@ -152,7 +152,7 @@ Fill the IdP links in the create user request to add a user with an external log
The idpId is the ID of the provider in ZITADEL, the idpExternalId is the ID of the user in the external identity provider; usually, this is sent in the “sub”.
The display name is used to list the linkings on the users.
[Create User API Documentation](https://zitadel.com/docs/apis/resources/user_service/user-service-add-human-user)
[Create User API Documentation](/docs/apis/resources/user_service/user-service-add-human-user)
#### Request
```bash
@ -190,7 +190,7 @@ curl --request POST \
If you didn't get a user ID in the parameters to your success page, you know that there is no existing user in ZITADEL with that provider and you can register a new user (read previous section), or link it to an existing account.
If you want to link/connect to an existing account you can perform the add identity provider link request.
[Add IDP Link to existing user documentation](https://zitadel.com/docs/apis/resources/user_service/user-service-add-idp-link)
[Add IDP Link to existing user documentation](/docs/apis/resources/user_service/user-service-add-idp-link)
#### Request
```bash

View File

@ -2,11 +2,6 @@
title: OIDC Standard
---
:::info
Not yet implemented, but should give you a general impression of how it will work
Subscribe to the following issue: https://github.com/orgs/zitadel/projects/2/views/1?filterQuery=oidc&pane=issue&itemId=23181369
:::
To build your own login ui for your own application it is not necessary to have the OIDC standard included or any additional work that has to be done.
However, it might make sense, if you want to connect your login to different applications especially if they are not in your control and they rely on the standard.
@ -14,13 +9,135 @@ The following flow shows you the different components you need to enable OIDC fo
![OIDC Flow](/img/guides/login-ui/oidc-flow.png)
1. Your application makes an authorization request to your login UI
2. The login UI takes the requests and sends them to the ZITADEL API. In the request to the ZITADEL API, a header to authenticate your client is needed.
2. The login UI proxies the request to the ZITADEL API. In the request to the ZITADEL API, a header to identify your client is needed.
3. The ZITADEL API parses the request and does what it needs to interpret certain parameters (e.g., organization scope, etc.)
4. Redirect to a predefined, relative URL of the login UI that includes the authrequest ID
5. Request to ZITADEL API to get all the information from the auth request
6. Create and update the session till the login flow is complete and the user is authenticated. Make sure to include the auth Request ID in the session
7. Read the callback URL from the ZITADEL API
4. Redirect to a predefined, relative URL of the login UI that includes the authrequest ID ("/login?authRequest=")
5. Request to ZITADEL API to get all the information from the auth request. This is optional and only needed if you like to get all the parsed information from the authrequest-
6. Authenticate the user in your login UI by creating and updating a session with all the checks you need.
7. Finalize the auth request by sending the session to the request, you will get the callback URL in the response
8. Redirect to your application with the callback URL you got in the previous request
9. All OIDC-specific endpoints have to be accepted in the Login UI and should be proxied and sent to the ZITADEL API
## Example
Let's assume you host your login UI on the following URL:
```
https://login.example.com
```
## Authorize Request
A user opens your application and is unauthenticated, the user will then be redirected to your login with the following auth Request:
```
https://login.example.com/oauth/v2/authorize?client_id=170086824411201793%40yourapp&redirect_uri=https%3A%2F%2Fyourapp.example.com%2Fauth%2Fcallback&response_type=code&scope=openid%20email%20profile&code_challenge=9az09PjcfuENS7oDK7jUd2xAWRb-B3N7Sr3kDoWECOY&code_challenge_method=S256&login_hint=minnie-mouse```
```
The auth request includes all the relevant information for the OIDC standard and in this example we also have a login hint for the login name "minnie-mouse".
You now have to proxy the auth request from your own UI to the authorize Endpoint of ZITADEL.
Make sure to add the user id of your login UI as a header to the request: ```x-zitadel-login-client: <userid>```
Read more about the [Authorize Endpoint Documentation](/docs/apis/openidoauth/endpoints#authorization_endpoint)
The endpoint will redirect you to the domain of your UI on the path /login and add the auth Request ID as parameter.
```https://login.example.com/login?authRequest=V2_224908753244265546```
### Get Auth Request by ID
With the ID from the redirect before you will now be able to get the information of the auth request.
[Get Auth Request By ID Documentation](/docs/apis/resources/oidc_service/oidc-service-get-auth-request)
```bash
curl --request GET \
--url https://$ZITADEL_DOMAIN/v2alpha/oidc/auth_requests/V2_224908753244265546 \
--header 'Authorization: Bearer '"$TOKEN"''\
```
Response Example:
```json
{
"authRequest": {
"id": "V2_224908753244265546",
"creationDate": "2023-07-28T13:47:43.471505Z",
"clientId": "224901977648260028@mytestproject",
"scope": [
"openid",
"profile"
],
"redirectUri": "https://myapp.example.com/auth/callback",
"loginHint": "mini@mouse.com"
}
}
```
### Perform Login
After you have initialized the OIDC flow you can implement the login.
Implement all the steps you like the user the go trough by [creating](/docs/apis/resources/session_service/session-service-create-session) and [updating](/docs/apis/resources/session_service/session-service-set-session) the user-session.
Read the following resources for more information about the different checks:
- [Username and Password](./username-password)
- [External Identity Provider](./external-login)
- [Passkeys](./passkey)
- [Multi-Factor](./mfa)
### Finalize Auth Request
To finalize the auth request and connect an existing user session with it you have to update the auth request with the session token.
On the create and update user session request you will always get a session token in the response.
The latest session token has to be sent to the following request:
Read more about the [Finalize Auth Request Documentation](/docs/apis/resources/oidc_service/oidc-service-create-callback)
Make sure that the authorization header is from the same account that you originally sent in the client id header ```x-zitadel-login-client: <userid>``` on the authorize endpoint.
```bash
curl --request POST \
--url $ZITADEL_DOMAIN/v2alpha/oidc/auth_requests/V2_224908753244265546 \
--header 'Accept: application/json' \
--header 'Authorization: Bearer '"$TOKEN"''\
--header 'Content-Type: application/json' \
--data '{
"session": {
"sessionId": "225307381909694508",
"sessionToken": "7N5kQCvC4jIf2OuBjwfyWSX2FUKbQqg4iG3uWT-TBngMhlS9miGUwpyUaN0HJ8OcbSzk4QHZy_Bvvv"
}
}'
```
In the response you will get a callback URL to which you have to redirect from your login UI.
Example Response:
```bash
{
"details": {
"sequence": "686",
"changeDate": "2023-07-31T08:09:19.314537Z",
"resourceOwner": "163840776801878273"
},
"callbackUrl": "https://myapp.example.com/auth/callback?code=k98NBLrdjVbwQQI-oM_rR_cYHv0k3dqpkqlQX8UXTWVnYSQL9g&state=testd"
}
```
### OIDC Endpoints
All OIDC relevant endpoints are provided by ZITADEL. In you login UI you just have to proxy them through and send them directly to the backend.
These are endpoints like:
- Userinfo
- Well-known
- Introspection
- Token
- etc
### End Session / Logout
The end session endpoint has to be implemented as all the other OIDC endpoints. This means you have to proxy the request from you UI to the ZITADEL.
In case the ZITADEL backend is not able to determine which session to terminate directly or requires additional approval from the user, it will redirect the browser to the following endpoint:
```/logout?post_logout_redirect=```
Prompt the user to select a session, terminate it using the [corresponding endpoint](/docs/apis/resources/session_service/session-service-delete-session) and send the user to the `post_logout_redirect` URL.

View File

@ -120,8 +120,10 @@ Multifactors:
Secondfactors (2FA):
- OTP (One Time Password), Authenticator Apps like Google/Microsoft Authenticator, Authy, etc.
- U2F (Universal Second Factor), e.g FaceID, WindowsHello, Fingerprint, Hardwaretokens like Yubikey
- Time-based One Time Password (TOTP), Authenticator Apps like Google/Microsoft Authenticator, Authy, etc.
- Universal Second Factor (U2F), e.g FaceID, WindowsHello, Fingerprint, Hardwaretokens like Yubikey
- One Time Password with Email (Email OTP)
- One Time Password with SMS (SMS OTP)
Force a user to register and use a multifactor authentication, by checking the option "Force MFA".
Ensure that you have added the MFA methods you want to allow.
@ -279,6 +281,8 @@ The following secrets can be configured:
- Password reset code
- Passwordless initialization code
- Application secrets
- One Time Password (OTP) - SMS
- One Time Password (OTP) - Email
<img
src="/docs/img/guides/console/secretappearance.png"

View File

@ -104,6 +104,20 @@ If you need custom branding on a organization (for example in a B2B scenario, wh
The behaviour of the login page, applyling custom design, is then defined on your projects detail page. Read more about it [here](./projects#branding)
## Show Organization Login
As you should know by now ZITADEL knows the concept of Organizations.
You can define [default settings](/docs/guides/manage/console/instance-settings) for your ZITADEL, or you can overwrite them for an [Organization](#organization-settings).
Per default the ZITADEL Login will always show what is defined per default. As soon as the Organization context is given, the settings defined on the specific organization can be triggered.
This means when you want to trigger the settings of an organization directly, make sure to send the organization scope in the authentication request.
``` bash
urn:zitadel:iam:org:id:{id}
```
Read more about the [scopes](/docs/apis/openidoauth/scopes#reserved-scopes) or try it out in our [OIDC Playground](/docs/apis/openidoauth/authrequest).
## Default organization
On the instance settings page ($YOUR_DOMAIN//ui/console/orgs) you can set an organization as default organization.

View File

@ -620,24 +620,3 @@ This tutorial covered how to configure ZITADEL and how to use React to build an
We hope you enjoyed the tutorial and encourage you to check out the ZITADEL [documentation](https://zitadel.com/docs) for more information on how to use the ZITADEL platform to its full potential. Thanks for joining us!

View File

@ -1,10 +1,5 @@
## Postgres
:::caution
PostgreSQL extension is currently in [Beta](/docs/support/software-release-cycles-support#beta).
Beta state will be removed as soon as automated tests are implemented. [Github Issue](https://github.com/zitadel/zitadel/issues/5741)
:::
:::caution
Be aware that PostgreSQL is only [Enterprise Supported](/docs/support/software-release-cycles-support#partially-supported).
:::
@ -56,7 +51,7 @@ CREATE DATABASE zitadel;
GRANT CONNECT, CREATE ON DATABASE zitadel TO zitadel;
```
Don't forget to adjust pg_hba.conf and set a password for the zitadel user.
Don't forget to adjust `pg_hba.conf` and set a password for the zitadel user.
With the setup done, follow the [phases guide](/docs/self-hosting/manage/updating_scaling#separating-init-and-setup-from-the-runtime)
to run the init and then setup phase to get all necessary tables and data set up.
to run the init and then setup phase to get all necessary tables and data set up.

View File

@ -25,4 +25,13 @@ import Postgres from './_postgres.mdx'
<Postgres/>
<More/>
</TabItem>
</Tabs>
</Tabs>
## Zitadel credentials
The [init phase](/docs/self-hosting/manage/updating_scaling#separating-init-and-setup-from-the-runtime) of Zitadel creates a the zitadel user (`Database.*.User.Username` & `Database.*.User.Password`) with their password if it does not exist (and Admin credentials are passed). It is though to note that it does **neither** update **nor** deprecate them. In case you provisioned a Zitadel setup with insecure or _easy-to-guess_ values you should first of all rotate them but also manually ensure, that the old role/user gets deprecated.
If you rotate the credentials you either must opt for a new username or deprecate the old user first (might lead to interruptions) since the init phase will fail if the user already exists but only the password changes. To deprecate the old user you need admin access to your database server and remove the user with commands matching your database provider.
:::caution
Recreating a database will not necessarily remove the user, make sure to check for the user and remove it if necessary.
:::

View File

@ -45,6 +45,7 @@ To apply best practices to your production setup we created a step by step check
### Security
- [ ] Ensure that your ZITADEL does not use [the default, example or _easy-to-guess_ credentials](/docs/self-hosting/manage/database#zitadel-credentials)
- [ ] Use a FQDN and a trusted valid certificate for external [TLS](/docs/self-hosting/manage/tls_modes#http2) connections
- [ ] Create service accounts for applications that interact with ZITADEL's APIs
- [ ] Make use of a CDN service to decrease the load for static assets served by ZITADEL

Binary file not shown.

Before

Width:  |  Height:  |  Size: 254 KiB

After

Width:  |  Height:  |  Size: 370 KiB

View File

@ -306,7 +306,7 @@ describe('quotas', () => {
}
return foundExpected >= 3;
}),
);
), { timeout: 60_000 };
});
});
});

2
go.mod
View File

@ -60,7 +60,7 @@ require (
github.com/ttacon/libphonenumber v1.2.1
github.com/zitadel/logging v0.3.4
github.com/zitadel/oidc/v2 v2.7.0
github.com/zitadel/passwap v0.2.0
github.com/zitadel/passwap v0.3.0
github.com/zitadel/saml v0.0.11
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.40.0
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.40.0

4
go.sum
View File

@ -898,8 +898,8 @@ github.com/zitadel/logging v0.3.4 h1:9hZsTjMMTE3X2LUi0xcF9Q9EdLo+FAezeu52ireBbHM
github.com/zitadel/logging v0.3.4/go.mod h1:aPpLQhE+v6ocNK0TWrBrd363hZ95KcI17Q1ixAQwZF0=
github.com/zitadel/oidc/v2 v2.7.0 h1:IGX4EDk6tegTjUSsZDWeTfLseFU0BdJ/Glf1tgys2lU=
github.com/zitadel/oidc/v2 v2.7.0/go.mod h1:zkUkVJS0sDVy9m0UA9RgO3f8i/C0rtjvXU36UJj7T+0=
github.com/zitadel/passwap v0.2.0 h1:rkYrax9hfRIpVdXJ7pS8JHkQOhuQTdZQxEhsY0dFFrU=
github.com/zitadel/passwap v0.2.0/go.mod h1:KRTL4LL8ugJIn2xLoQYZf5t4kDyr7w41uq3XqvUlO6w=
github.com/zitadel/passwap v0.3.0 h1:kC/vzN9xQlEQjUAZs0z2P5nKrZs9AuTqprteSQ2S4Ag=
github.com/zitadel/passwap v0.3.0/go.mod h1:sIpG6HfmnP28qwxu8kf+ot53ERbLwU9fOITstAwZSms=
github.com/zitadel/saml v0.0.11 h1:kObucnBrcu1PHCO7RGT0iVeuJL/5I50gUgr40S41nMs=
github.com/zitadel/saml v0.0.11/go.mod h1:YGWAvPZRv4DbEZ78Ht/2P0AWzGn+6WGhFf90PMXl0Po=
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=

View File

@ -10,7 +10,7 @@ import (
// equals the authenticated user in the context.
func UserIDInCTX(ctx context.Context, userID string) error {
if GetCtxData(ctx).UserID != userID {
return errors.ThrowUnauthenticated(nil, "AUTH-Bohd2", "Errors.User.UserIDWrong")
return errors.ThrowPermissionDenied(nil, "AUTH-Bohd2", "Errors.User.UserIDWrong")
}
return nil
}

View File

@ -72,6 +72,7 @@ func SecretGeneratorsToPb(generators []*query.SecretGenerator) []*settings_pb.Se
func SecretGeneratorToPb(generator *query.SecretGenerator) *settings_pb.SecretGenerator {
mapped := &settings_pb.SecretGenerator{
GeneratorType: SecretGeneratorTypeToPb(generator.GeneratorType),
Length: uint32(generator.Length),
Expiry: durationpb.New(generator.Expiry),
IncludeUpperLetters: generator.IncludeUpperLetters,
@ -97,6 +98,10 @@ func SecretGeneratorTypeToPb(generatorType domain.SecretGeneratorType) settings_
return settings_pb.SecretGeneratorType_SECRET_GENERATOR_TYPE_PASSWORDLESS_INIT_CODE
case domain.SecretGeneratorTypeAppSecret:
return settings_pb.SecretGeneratorType_SECRET_GENERATOR_TYPE_APP_SECRET
case domain.SecretGeneratorTypeOTPSMS:
return settings_pb.SecretGeneratorType_SECRET_GENERATOR_TYPE_OTP_SMS
case domain.SecretGeneratorTypeOTPEmail:
return settings_pb.SecretGeneratorType_SECRET_GENERATOR_TYPE_OTP_EMAIL
default:
return settings_pb.SecretGeneratorType_SECRET_GENERATOR_TYPE_UNSPECIFIED
}
@ -116,6 +121,10 @@ func SecretGeneratorTypeToDomain(generatorType settings_pb.SecretGeneratorType)
return domain.SecretGeneratorTypePasswordlessInitCode
case settings_pb.SecretGeneratorType_SECRET_GENERATOR_TYPE_APP_SECRET:
return domain.SecretGeneratorTypeAppSecret
case settings_pb.SecretGeneratorType_SECRET_GENERATOR_TYPE_OTP_SMS:
return domain.SecretGeneratorTypeOTPSMS
case settings_pb.SecretGeneratorType_SECRET_GENERATOR_TYPE_OTP_EMAIL:
return domain.SecretGeneratorTypeOTPEmail
default:
return domain.SecretGeneratorTypeUnspecified
}

View File

@ -552,7 +552,7 @@ func (s *Server) importData(ctx context.Context, orgs []*admin_pb.DataOrg) (*adm
if user.User.OtpCode != "" {
logging.Debugf("import user otp: %s", user.GetUserId())
if err := s.command.ImportHumanOTP(ctx, user.UserId, "", org.GetOrgId(), user.User.OtpCode); err != nil {
if err := s.command.ImportHumanTOTP(ctx, user.UserId, "", org.GetOrgId(), user.User.OtpCode); err != nil {
errors = append(errors, &admin_pb.ImportDataError{Type: "human_user_otp", Id: user.GetUserId(), Message: err.Error()})
if isCtxTimeout(ctx) {
return &admin_pb.ImportDataResponse{Errors: errors, Success: success}, count, err

View File

@ -18,7 +18,7 @@ func (s *Server) ListMyAuthFactors(ctx context.Context, _ *auth_pb.ListMyAuthFac
if err != nil {
return nil, err
}
err = query.AppendAuthMethodsQuery(domain.UserAuthMethodTypeU2F, domain.UserAuthMethodTypeOTP)
err = query.AppendAuthMethodsQuery(domain.UserAuthMethodTypeU2F, domain.UserAuthMethodTypeTOTP, domain.UserAuthMethodTypeOTPSMS, domain.UserAuthMethodTypeOTPEmail)
if err != nil {
return nil, err
}
@ -37,16 +37,16 @@ func (s *Server) ListMyAuthFactors(ctx context.Context, _ *auth_pb.ListMyAuthFac
func (s *Server) AddMyAuthFactorOTP(ctx context.Context, _ *auth_pb.AddMyAuthFactorOTPRequest) (*auth_pb.AddMyAuthFactorOTPResponse, error) {
ctxData := authz.GetCtxData(ctx)
otp, err := s.command.AddHumanOTP(ctx, ctxData.UserID, ctxData.ResourceOwner)
otp, err := s.command.AddHumanTOTP(ctx, ctxData.UserID, ctxData.ResourceOwner)
if err != nil {
return nil, err
}
return &auth_pb.AddMyAuthFactorOTPResponse{
Url: otp.Url,
Secret: otp.SecretString,
Url: otp.URI,
Secret: otp.Secret,
Details: object.AddToDetailsPb(
otp.Sequence,
otp.ChangeDate,
otp.EventDate,
otp.ResourceOwner,
),
}, nil
@ -54,7 +54,7 @@ func (s *Server) AddMyAuthFactorOTP(ctx context.Context, _ *auth_pb.AddMyAuthFac
func (s *Server) VerifyMyAuthFactorOTP(ctx context.Context, req *auth_pb.VerifyMyAuthFactorOTPRequest) (*auth_pb.VerifyMyAuthFactorOTPResponse, error) {
ctxData := authz.GetCtxData(ctx)
objectDetails, err := s.command.HumanCheckMFAOTPSetup(ctx, ctxData.UserID, req.Code, "", ctxData.ResourceOwner)
objectDetails, err := s.command.HumanCheckMFATOTPSetup(ctx, ctxData.UserID, req.Code, "", ctxData.ResourceOwner)
if err != nil {
return nil, err
}
@ -65,7 +65,7 @@ func (s *Server) VerifyMyAuthFactorOTP(ctx context.Context, req *auth_pb.VerifyM
func (s *Server) RemoveMyAuthFactorOTP(ctx context.Context, _ *auth_pb.RemoveMyAuthFactorOTPRequest) (*auth_pb.RemoveMyAuthFactorOTPResponse, error) {
ctxData := authz.GetCtxData(ctx)
objectDetails, err := s.command.HumanRemoveOTP(ctx, ctxData.UserID, ctxData.ResourceOwner)
objectDetails, err := s.command.HumanRemoveTOTP(ctx, ctxData.UserID, ctxData.ResourceOwner)
if err != nil {
return nil, err
}
@ -74,6 +74,50 @@ func (s *Server) RemoveMyAuthFactorOTP(ctx context.Context, _ *auth_pb.RemoveMyA
}, nil
}
func (s *Server) AddMyAuthFactorOTPSMS(ctx context.Context, _ *auth_pb.AddMyAuthFactorOTPSMSRequest) (*auth_pb.AddMyAuthFactorOTPSMSResponse, error) {
ctxData := authz.GetCtxData(ctx)
details, err := s.command.AddHumanOTPSMS(ctx, ctxData.UserID, ctxData.ResourceOwner)
if err != nil {
return nil, err
}
return &auth_pb.AddMyAuthFactorOTPSMSResponse{
Details: object.DomainToAddDetailsPb(details),
}, nil
}
func (s *Server) RemoveMyAuthFactorOTPSMS(ctx context.Context, _ *auth_pb.RemoveMyAuthFactorOTPSMSRequest) (*auth_pb.RemoveMyAuthFactorOTPSMSResponse, error) {
ctxData := authz.GetCtxData(ctx)
details, err := s.command.RemoveHumanOTPSMS(ctx, ctxData.UserID, ctxData.ResourceOwner)
if err != nil {
return nil, err
}
return &auth_pb.RemoveMyAuthFactorOTPSMSResponse{
Details: object.DomainToChangeDetailsPb(details),
}, nil
}
func (s *Server) AddMyAuthFactorOTPEmail(ctx context.Context, _ *auth_pb.AddMyAuthFactorOTPEmailRequest) (*auth_pb.AddMyAuthFactorOTPEmailResponse, error) {
ctxData := authz.GetCtxData(ctx)
details, err := s.command.AddHumanOTPEmail(ctx, ctxData.UserID, ctxData.ResourceOwner)
if err != nil {
return nil, err
}
return &auth_pb.AddMyAuthFactorOTPEmailResponse{
Details: object.DomainToAddDetailsPb(details),
}, nil
}
func (s *Server) RemoveMyAuthFactorOTPEmail(ctx context.Context, _ *auth_pb.RemoveMyAuthFactorOTPEmailRequest) (*auth_pb.RemoveMyAuthFactorOTPEmailResponse, error) {
ctxData := authz.GetCtxData(ctx)
details, err := s.command.RemoveHumanOTPEmail(ctx, ctxData.UserID, ctxData.ResourceOwner)
if err != nil {
return nil, err
}
return &auth_pb.RemoveMyAuthFactorOTPEmailResponse{
Details: object.DomainToChangeDetailsPb(details),
}, nil
}
func (s *Server) AddMyAuthFactorU2F(ctx context.Context, _ *auth_pb.AddMyAuthFactorU2FRequest) (*auth_pb.AddMyAuthFactorU2FResponse, error) {
ctxData := authz.GetCtxData(ctx)
u2f, err := s.command.HumanAddU2FSetup(ctx, ctxData.UserID, ctxData.ResourceOwner, false)

View File

@ -613,7 +613,7 @@ func (s *Server) ListHumanAuthFactors(ctx context.Context, req *mgmt_pb.ListHuma
if err != nil {
return nil, err
}
err = query.AppendAuthMethodsQuery(domain.UserAuthMethodTypeU2F, domain.UserAuthMethodTypeOTP)
err = query.AppendAuthMethodsQuery(domain.UserAuthMethodTypeU2F, domain.UserAuthMethodTypeTOTP, domain.UserAuthMethodTypeOTPSMS, domain.UserAuthMethodTypeOTPEmail)
if err != nil {
return nil, err
}
@ -631,7 +631,7 @@ func (s *Server) ListHumanAuthFactors(ctx context.Context, req *mgmt_pb.ListHuma
}
func (s *Server) RemoveHumanAuthFactorOTP(ctx context.Context, req *mgmt_pb.RemoveHumanAuthFactorOTPRequest) (*mgmt_pb.RemoveHumanAuthFactorOTPResponse, error) {
objectDetails, err := s.command.HumanRemoveOTP(ctx, req.UserId, authz.GetCtxData(ctx).OrgID)
objectDetails, err := s.command.HumanRemoveTOTP(ctx, req.UserId, authz.GetCtxData(ctx).OrgID)
if err != nil {
return nil, err
}

View File

@ -16,9 +16,13 @@ func SecondFactorsTypesToDomain(secondFactorTypes []policy_pb.SecondFactorType)
func SecondFactorTypeToDomain(secondFactorType policy_pb.SecondFactorType) domain.SecondFactorType {
switch secondFactorType {
case policy_pb.SecondFactorType_SECOND_FACTOR_TYPE_OTP:
return domain.SecondFactorTypeOTP
return domain.SecondFactorTypeTOTP
case policy_pb.SecondFactorType_SECOND_FACTOR_TYPE_U2F:
return domain.SecondFactorTypeU2F
case policy_pb.SecondFactorType_SECOND_FACTOR_TYPE_OTP_EMAIL:
return domain.SecondFactorTypeOTPEmail
case policy_pb.SecondFactorType_SECOND_FACTOR_TYPE_OTP_SMS:
return domain.SecondFactorTypeOTPSMS
default:
return domain.SecondFactorTypeUnspecified
}
@ -34,10 +38,14 @@ func ModelSecondFactorTypesToPb(types []domain.SecondFactorType) []policy_pb.Sec
func ModelSecondFactorTypeToPb(secondFactorType domain.SecondFactorType) policy_pb.SecondFactorType {
switch secondFactorType {
case domain.SecondFactorTypeOTP:
case domain.SecondFactorTypeTOTP:
return policy_pb.SecondFactorType_SECOND_FACTOR_TYPE_OTP
case domain.SecondFactorTypeU2F:
return policy_pb.SecondFactorType_SECOND_FACTOR_TYPE_U2F
case domain.SecondFactorTypeOTPEmail:
return policy_pb.SecondFactorType_SECOND_FACTOR_TYPE_OTP_EMAIL
case domain.SecondFactorTypeOTPSMS:
return policy_pb.SecondFactorType_SECOND_FACTOR_TYPE_OTP_SMS
default:
return policy_pb.SecondFactorType_SECOND_FACTOR_TYPE_UNSPECIFIED
}

View File

@ -396,7 +396,7 @@ func Test_ZITADEL_API_missing_authentication(t *testing.T) {
func Test_ZITADEL_API_missing_mfa(t *testing.T) {
id, token, _, _ := Tester.CreatePasswordSession(t, CTX, User.GetUserId(), integration.UserPassword)
ctx := metadata.AppendToOutgoingContext(context.Background(), "Authorization", fmt.Sprintf("Bearer %s", token))
ctx := Tester.WithAuthorizationToken(context.Background(), token)
sessionResp, err := Tester.Client.SessionV2.GetSession(ctx, &session.GetSessionRequest{SessionId: id})
require.Error(t, err)
require.Nil(t, sessionResp)
@ -405,7 +405,7 @@ func Test_ZITADEL_API_missing_mfa(t *testing.T) {
func Test_ZITADEL_API_success(t *testing.T) {
id, token, _, _ := Tester.CreatePasskeySession(t, CTX, User.GetUserId())
ctx := metadata.AppendToOutgoingContext(context.Background(), "Authorization", fmt.Sprintf("Bearer %s", token))
ctx := Tester.WithAuthorizationToken(context.Background(), token)
sessionResp, err := Tester.Client.SessionV2.GetSession(ctx, &session.GetSessionRequest{SessionId: id})
require.NoError(t, err)
require.NotNil(t, id, sessionResp.GetSession().GetFactors().GetPasskey().GetVerifiedAt().AsTime())
@ -415,7 +415,7 @@ func Test_ZITADEL_API_session_not_found(t *testing.T) {
id, token, _, _ := Tester.CreatePasskeySession(t, CTX, User.GetUserId())
// test session token works
ctx := metadata.AppendToOutgoingContext(context.Background(), "Authorization", fmt.Sprintf("Bearer %s", token))
ctx := Tester.WithAuthorizationToken(context.Background(), token)
_, err := Tester.Client.SessionV2.GetSession(ctx, &session.GetSessionRequest{SessionId: id})
require.NoError(t, err)
@ -425,7 +425,7 @@ func Test_ZITADEL_API_session_not_found(t *testing.T) {
SessionToken: gu.Ptr(token),
})
require.NoError(t, err)
ctx = metadata.AppendToOutgoingContext(context.Background(), "Authorization", fmt.Sprintf("Bearer %s", token))
ctx = Tester.WithAuthorizationToken(context.Background(), token)
_, err = Tester.Client.SessionV2.GetSession(ctx, &session.GetSessionRequest{SessionId: id})
require.Error(t, err)
}

View File

@ -62,10 +62,14 @@ func passkeysTypeToPb(passwordlessType domain.PasswordlessType) settings.Passkey
func secondFactorTypeToPb(secondFactorType domain.SecondFactorType) settings.SecondFactorType {
switch secondFactorType {
case domain.SecondFactorTypeOTP:
case domain.SecondFactorTypeTOTP:
return settings.SecondFactorType_SECOND_FACTOR_TYPE_OTP
case domain.SecondFactorTypeU2F:
return settings.SecondFactorType_SECOND_FACTOR_TYPE_U2F
case domain.SecondFactorTypeOTPEmail:
return settings.SecondFactorType_SECOND_FACTOR_TYPE_OTP_EMAIL
case domain.SecondFactorTypeOTPSMS:
return settings.SecondFactorType_SECOND_FACTOR_TYPE_OTP_SMS
case domain.SecondFactorTypeUnspecified:
return settings.SecondFactorType_SECOND_FACTOR_TYPE_UNSPECIFIED
default:

View File

@ -39,8 +39,10 @@ func Test_loginSettingsToPb(t *testing.T) {
SecondFactorCheckLifetime: time.Microsecond,
MultiFactorCheckLifetime: time.Nanosecond,
SecondFactors: []domain.SecondFactorType{
domain.SecondFactorTypeOTP,
domain.SecondFactorTypeTOTP,
domain.SecondFactorTypeU2F,
domain.SecondFactorTypeOTPEmail,
domain.SecondFactorTypeOTPSMS,
},
MultiFactors: []domain.MultiFactorType{
domain.MultiFactorTypeU2FWithPIN,
@ -69,6 +71,8 @@ func Test_loginSettingsToPb(t *testing.T) {
SecondFactors: []settings.SecondFactorType{
settings.SecondFactorType_SECOND_FACTOR_TYPE_OTP,
settings.SecondFactorType_SECOND_FACTOR_TYPE_U2F,
settings.SecondFactorType_SECOND_FACTOR_TYPE_OTP_EMAIL,
settings.SecondFactorType_SECOND_FACTOR_TYPE_OTP_SMS,
},
MultiFactors: []settings.MultiFactorType{
settings.MultiFactorType_MULTI_FACTOR_TYPE_U2F_WITH_VERIFICATION,
@ -146,13 +150,21 @@ func Test_secondFactorTypeToPb(t *testing.T) {
want settings.SecondFactorType
}{
{
args: args{domain.SecondFactorTypeOTP},
args: args{domain.SecondFactorTypeTOTP},
want: settings.SecondFactorType_SECOND_FACTOR_TYPE_OTP,
},
{
args: args{domain.SecondFactorTypeU2F},
want: settings.SecondFactorType_SECOND_FACTOR_TYPE_U2F,
},
{
args: args{domain.SecondFactorTypeOTPSMS},
want: settings.SecondFactorType_SECOND_FACTOR_TYPE_OTP_SMS,
},
{
args: args{domain.SecondFactorTypeOTPEmail},
want: settings.SecondFactorType_SECOND_FACTOR_TYPE_OTP_EMAIL,
},
{
args: args{domain.SecondFactorTypeUnspecified},
want: settings.SecondFactorType_SECOND_FACTOR_TYPE_UNSPECIFIED,

View File

@ -197,7 +197,7 @@ func AuthMethodToPb(mfa *query.AuthMethod) *user_pb.AuthFactor {
State: MFAStateToPb(mfa.State),
}
switch mfa.Type {
case domain.UserAuthMethodTypeOTP:
case domain.UserAuthMethodTypeTOTP:
factor.Type = &user_pb.AuthFactor_Otp{
Otp: &user_pb.AuthFactorOTP{},
}
@ -208,6 +208,14 @@ func AuthMethodToPb(mfa *query.AuthMethod) *user_pb.AuthFactor {
Name: mfa.Name,
},
}
case domain.UserAuthMethodTypeOTPSMS:
factor.Type = &user_pb.AuthFactor_OtpSms{
OtpSms: &user_pb.AuthFactorOTPSMS{},
}
case domain.UserAuthMethodTypeOTPEmail:
factor.Type = &user_pb.AuthFactor_OtpEmail{
OtpEmail: &user_pb.AuthFactorOTPEmail{},
}
}
return factor
}

View File

@ -0,0 +1,43 @@
package user
import (
"context"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/api/grpc/object/v2"
user "github.com/zitadel/zitadel/pkg/grpc/user/v2alpha"
)
func (s *Server) AddOTPSMS(ctx context.Context, req *user.AddOTPSMSRequest) (*user.AddOTPSMSResponse, error) {
details, err := s.command.AddHumanOTPSMS(ctx, req.GetUserId(), authz.GetCtxData(ctx).ResourceOwner)
if err != nil {
return nil, err
}
return &user.AddOTPSMSResponse{Details: object.DomainToDetailsPb(details)}, nil
}
func (s *Server) RemoveOTPSMS(ctx context.Context, req *user.RemoveOTPSMSRequest) (*user.RemoveOTPSMSResponse, error) {
objectDetails, err := s.command.RemoveHumanOTPSMS(ctx, req.GetUserId(), authz.GetCtxData(ctx).ResourceOwner)
if err != nil {
return nil, err
}
return &user.RemoveOTPSMSResponse{Details: object.DomainToDetailsPb(objectDetails)}, nil
}
func (s *Server) AddOTPEmail(ctx context.Context, req *user.AddOTPEmailRequest) (*user.AddOTPEmailResponse, error) {
details, err := s.command.AddHumanOTPEmail(ctx, req.GetUserId(), authz.GetCtxData(ctx).ResourceOwner)
if err != nil {
return nil, err
}
return &user.AddOTPEmailResponse{Details: object.DomainToDetailsPb(details)}, nil
}
func (s *Server) RemoveOTPEmail(ctx context.Context, req *user.RemoveOTPEmailRequest) (*user.RemoveOTPEmailResponse, error) {
objectDetails, err := s.command.RemoveHumanOTPEmail(ctx, req.GetUserId(), authz.GetCtxData(ctx).ResourceOwner)
if err != nil {
return nil, err
}
return &user.RemoveOTPEmailResponse{Details: object.DomainToDetailsPb(objectDetails)}, nil
}

View File

@ -0,0 +1,302 @@
//go:build integration
package user_test
import (
"context"
"testing"
"github.com/stretchr/testify/require"
"github.com/zitadel/zitadel/internal/integration"
object "github.com/zitadel/zitadel/pkg/grpc/object/v2alpha"
user "github.com/zitadel/zitadel/pkg/grpc/user/v2alpha"
)
func TestServer_AddOTPSMS(t *testing.T) {
userID := Tester.CreateHumanUser(CTX).GetUserId()
Tester.RegisterUserPasskey(CTX, userID)
_, sessionToken, _, _ := Tester.CreatePasskeySession(t, CTX, userID)
// TODO: add when phone can be added to user
/*
userIDPhone := Tester.CreateHumanUser(CTX).GetUserId()
Tester.RegisterUserPasskey(CTX, userIDPhone)
_, sessionTokenPhone, _, _ := Tester.CreatePasskeySession(t, CTX, userIDPhone)
*/
type args struct {
ctx context.Context
req *user.AddOTPSMSRequest
}
tests := []struct {
name string
args args
want *user.AddOTPSMSResponse
wantErr bool
}{
{
name: "missing user id",
args: args{
ctx: CTX,
req: &user.AddOTPSMSRequest{},
},
wantErr: true,
},
{
name: "user mismatch",
args: args{
ctx: CTX,
req: &user.AddOTPSMSRequest{
UserId: "wrong",
},
},
wantErr: true,
},
{
name: "phone not verified",
args: args{
ctx: Tester.WithAuthorizationToken(context.Background(), sessionToken),
req: &user.AddOTPSMSRequest{
UserId: userID,
},
},
wantErr: true,
},
// TODO: add when phone can be added to user
/*
{
name: "add success",
args: args{
ctx: Tester.WithAuthorizationToken(context.Background(), sessionTokenPhone),
req: &user.AddOTPSMSRequest{
UserId: userID,
},
},
want: &user.AddOTPSMSResponse{
Details: &object.Details{
ResourceOwner: Tester.Organisation.ID,
},
},
},
*/
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := Client.AddOTPSMS(tt.args.ctx, tt.args.req)
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
require.NotNil(t, got)
integration.AssertDetails(t, tt.want, got)
})
}
}
func TestServer_RemoveOTPSMS(t *testing.T) {
// TODO: add when phone can be added to user
/*
userID := Tester.CreateHumanUser(CTX).GetUserId()
Tester.RegisterUserPasskey(CTX, userID)
_, sessionToken, _, _ := Tester.CreatePasskeySession(t, CTX, userID)
*/
type args struct {
ctx context.Context
req *user.RemoveOTPSMSRequest
}
tests := []struct {
name string
args args
want *user.RemoveOTPSMSResponse
wantErr bool
}{
{
name: "not added",
args: args{
ctx: CTX,
req: &user.RemoveOTPSMSRequest{
UserId: "wrong",
},
},
wantErr: true,
},
// TODO: add when phone can be added to user
/*
{
name: "success",
args: args{
ctx: Tester.WithAuthorizationToken(context.Background(), sessionToken),
req: &user.RemoveOTPSMSRequest{
UserId: userID,
},
},
want: &user.RemoveOTPSMSResponse{
Details: &object.Details{
ResourceOwner: Tester.Organisation.ResourceOwner,
},
},
},
*/
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := Client.RemoveOTPSMS(tt.args.ctx, tt.args.req)
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
require.NotNil(t, got)
integration.AssertDetails(t, tt.want, got)
})
}
}
func TestServer_AddOTPEmail(t *testing.T) {
userID := Tester.CreateHumanUser(CTX).GetUserId()
Tester.RegisterUserPasskey(CTX, userID)
_, sessionToken, _, _ := Tester.CreatePasskeySession(t, CTX, userID)
userVerified := Tester.CreateHumanUser(CTX)
_, err := Tester.Client.UserV2.VerifyEmail(CTX, &user.VerifyEmailRequest{
UserId: userVerified.GetUserId(),
VerificationCode: userVerified.GetEmailCode(),
})
require.NoError(t, err)
Tester.RegisterUserPasskey(CTX, userVerified.GetUserId())
_, sessionTokenVerified, _, _ := Tester.CreatePasskeySession(t, CTX, userVerified.GetUserId())
type args struct {
ctx context.Context
req *user.AddOTPEmailRequest
}
tests := []struct {
name string
args args
want *user.AddOTPEmailResponse
wantErr bool
}{
{
name: "missing user id",
args: args{
ctx: CTX,
req: &user.AddOTPEmailRequest{},
},
wantErr: true,
},
{
name: "user mismatch",
args: args{
ctx: CTX,
req: &user.AddOTPEmailRequest{
UserId: "wrong",
},
},
wantErr: true,
},
{
name: "email not verified",
args: args{
ctx: Tester.WithAuthorizationToken(context.Background(), sessionToken),
req: &user.AddOTPEmailRequest{
UserId: userID,
},
},
wantErr: true,
},
{
name: "add success",
args: args{
ctx: Tester.WithAuthorizationToken(context.Background(), sessionTokenVerified),
req: &user.AddOTPEmailRequest{
UserId: userVerified.GetUserId(),
},
},
want: &user.AddOTPEmailResponse{
Details: &object.Details{
ResourceOwner: Tester.Organisation.ID,
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := Client.AddOTPEmail(tt.args.ctx, tt.args.req)
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
require.NotNil(t, got)
integration.AssertDetails(t, tt.want, got)
})
}
}
func TestServer_RemoveOTPEmail(t *testing.T) {
userID := Tester.CreateHumanUser(CTX).GetUserId()
Tester.RegisterUserPasskey(CTX, userID)
_, sessionToken, _, _ := Tester.CreatePasskeySession(t, CTX, userID)
userVerified := Tester.CreateHumanUser(CTX)
Tester.RegisterUserPasskey(CTX, userVerified.GetUserId())
_, sessionTokenVerified, _, _ := Tester.CreatePasskeySession(t, CTX, userVerified.GetUserId())
userVerifiedCtx := Tester.WithAuthorizationToken(context.Background(), sessionTokenVerified)
_, err := Tester.Client.UserV2.VerifyEmail(userVerifiedCtx, &user.VerifyEmailRequest{
UserId: userVerified.GetUserId(),
VerificationCode: userVerified.GetEmailCode(),
})
require.NoError(t, err)
_, err = Tester.Client.UserV2.AddOTPEmail(userVerifiedCtx, &user.AddOTPEmailRequest{UserId: userVerified.GetUserId()})
require.NoError(t, err)
type args struct {
ctx context.Context
req *user.RemoveOTPEmailRequest
}
tests := []struct {
name string
args args
want *user.RemoveOTPEmailResponse
wantErr bool
}{
{
name: "not added",
args: args{
ctx: Tester.WithAuthorizationToken(context.Background(), sessionToken),
req: &user.RemoveOTPEmailRequest{
UserId: userID,
},
},
wantErr: true,
},
{
name: "success",
args: args{
ctx: userVerifiedCtx,
req: &user.RemoveOTPEmailRequest{
UserId: userVerified.GetUserId(),
},
},
want: &user.RemoveOTPEmailResponse{
Details: &object.Details{
ResourceOwner: Tester.Organisation.ResourceOwner,
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := Client.RemoveOTPEmail(tt.args.ctx, tt.args.req)
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
require.NotNil(t, got)
integration.AssertDetails(t, tt.want, got)
})
}
}

View File

@ -0,0 +1,61 @@
package user
import (
"context"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/zitadel/zitadel/internal/domain"
caos_errs "github.com/zitadel/zitadel/internal/errors"
object "github.com/zitadel/zitadel/pkg/grpc/object/v2alpha"
user "github.com/zitadel/zitadel/pkg/grpc/user/v2alpha"
)
func (s *Server) SetPhone(ctx context.Context, req *user.SetPhoneRequest) (resp *user.SetPhoneResponse, err error) {
var resourceOwner string // TODO: check if still needed
var phone *domain.Phone
switch v := req.GetVerification().(type) {
case *user.SetPhoneRequest_SendCode:
phone, err = s.command.ChangeUserPhone(ctx, req.GetUserId(), resourceOwner, req.GetPhone(), s.userCodeAlg)
case *user.SetPhoneRequest_ReturnCode:
phone, err = s.command.ChangeUserPhoneReturnCode(ctx, req.GetUserId(), resourceOwner, req.GetPhone(), s.userCodeAlg)
case *user.SetPhoneRequest_IsVerified:
phone, err = s.command.ChangeUserPhoneVerified(ctx, req.GetUserId(), resourceOwner, req.GetPhone())
case nil:
phone, err = s.command.ChangeUserPhone(ctx, req.GetUserId(), resourceOwner, req.GetPhone(), s.userCodeAlg)
default:
err = caos_errs.ThrowUnimplementedf(nil, "USERv2-Ahng0", "verification oneOf %T in method SetPhone not implemented", v)
}
if err != nil {
return nil, err
}
return &user.SetPhoneResponse{
Details: &object.Details{
Sequence: phone.Sequence,
ChangeDate: timestamppb.New(phone.ChangeDate),
ResourceOwner: phone.ResourceOwner,
},
VerificationCode: phone.PlainCode,
}, nil
}
func (s *Server) VerifyPhone(ctx context.Context, req *user.VerifyPhoneRequest) (*user.VerifyPhoneResponse, error) {
details, err := s.command.VerifyUserPhone(ctx,
req.GetUserId(),
"", // TODO: check if still needed
req.GetVerificationCode(),
s.userCodeAlg,
)
if err != nil {
return nil, err
}
return &user.VerifyPhoneResponse{
Details: &object.Details{
Sequence: details.Sequence,
ChangeDate: timestamppb.New(details.EventDate),
ResourceOwner: details.ResourceOwner,
},
}, nil
}

View File

@ -0,0 +1,171 @@
//go:build integration
package user_test
import (
"testing"
"github.com/muhlemmer/gu"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/zitadel/zitadel/internal/integration"
object "github.com/zitadel/zitadel/pkg/grpc/object/v2alpha"
user "github.com/zitadel/zitadel/pkg/grpc/user/v2alpha"
)
func TestServer_SetPhone(t *testing.T) {
userID := Tester.CreateHumanUser(CTX).GetUserId()
tests := []struct {
name string
req *user.SetPhoneRequest
want *user.SetPhoneResponse
wantErr bool
}{
{
name: "default verification",
req: &user.SetPhoneRequest{
UserId: userID,
Phone: "+41791234568",
},
want: &user.SetPhoneResponse{
Details: &object.Details{
Sequence: 1,
ChangeDate: timestamppb.Now(),
ResourceOwner: Tester.Organisation.ID,
},
},
},
{
name: "send verification",
req: &user.SetPhoneRequest{
UserId: userID,
Phone: "+41791234569",
Verification: &user.SetPhoneRequest_SendCode{
SendCode: &user.SendPhoneVerificationCode{},
},
},
want: &user.SetPhoneResponse{
Details: &object.Details{
Sequence: 1,
ChangeDate: timestamppb.Now(),
ResourceOwner: Tester.Organisation.ID,
},
},
},
{
name: "return code",
req: &user.SetPhoneRequest{
UserId: userID,
Phone: "+41791234566",
Verification: &user.SetPhoneRequest_ReturnCode{
ReturnCode: &user.ReturnPhoneVerificationCode{},
},
},
want: &user.SetPhoneResponse{
Details: &object.Details{
Sequence: 1,
ChangeDate: timestamppb.Now(),
ResourceOwner: Tester.Organisation.ID,
},
VerificationCode: gu.Ptr("xxx"),
},
},
{
name: "is verified true",
req: &user.SetPhoneRequest{
UserId: userID,
Phone: "+41791234565",
Verification: &user.SetPhoneRequest_IsVerified{
IsVerified: true,
},
},
want: &user.SetPhoneResponse{
Details: &object.Details{
Sequence: 1,
ChangeDate: timestamppb.Now(),
ResourceOwner: Tester.Organisation.ID,
},
},
},
{
name: "is verified false",
req: &user.SetPhoneRequest{
UserId: userID,
Phone: "+41791234564",
Verification: &user.SetPhoneRequest_IsVerified{
IsVerified: false,
},
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := Client.SetPhone(CTX, tt.req)
if tt.wantErr {
require.Error(t, err)
} else {
require.NoError(t, err)
}
integration.AssertDetails(t, tt.want, got)
if tt.want.GetVerificationCode() != "" {
assert.NotEmpty(t, got.GetVerificationCode())
}
})
}
}
func TestServer_VerifyPhone(t *testing.T) {
userResp := Tester.CreateHumanUser(CTX)
tests := []struct {
name string
req *user.VerifyPhoneRequest
want *user.VerifyPhoneResponse
wantErr bool
}{
{
name: "wrong code",
req: &user.VerifyPhoneRequest{
UserId: userResp.GetUserId(),
VerificationCode: "xxx",
},
wantErr: true,
},
{
name: "wrong user",
req: &user.VerifyPhoneRequest{
UserId: "xxx",
VerificationCode: userResp.GetPhoneCode(),
},
wantErr: true,
},
{
name: "verify user",
req: &user.VerifyPhoneRequest{
UserId: userResp.GetUserId(),
VerificationCode: userResp.GetPhoneCode(),
},
want: &user.VerifyPhoneResponse{
Details: &object.Details{
Sequence: 1,
ChangeDate: timestamppb.Now(),
ResourceOwner: Tester.Organisation.ID,
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := Client.VerifyPhone(CTX, tt.req)
if tt.wantErr {
require.Error(t, err)
} else {
require.NoError(t, err)
}
integration.AssertDetails(t, tt.want, got)
})
}
}

View File

@ -32,6 +32,7 @@ func (s *Server) AddHumanUser(ctx context.Context, req *user.AddHumanUserRequest
UserId: human.ID,
Details: object.DomainToDetailsPb(human.Details),
EmailCode: human.EmailCode,
PhoneCode: human.PhoneCode,
}, nil
}
@ -77,9 +78,13 @@ func addUserRequestToAddHuman(req *user.AddHumanUserRequest) (*command.AddHuman,
ReturnCode: req.GetEmail().GetReturnCode() != nil,
URLTemplate: urlTemplate,
},
Phone: command.Phone{
Number: domain.PhoneNumber(req.GetPhone().GetPhone()),
Verified: req.GetPhone().GetIsVerified(),
ReturnCode: req.GetPhone().GetReturnCode() != nil,
},
PreferredLanguage: language.Make(req.GetProfile().GetPreferredLanguage()),
Gender: genderToDomain(req.GetProfile().GetGender()),
Phone: command.Phone{}, // TODO: add as soon as possible
Password: req.GetPassword().GetPassword(),
EncodedPasswordHash: req.GetHashedPassword().GetHash(),
PasswordChangeRequired: passwordChangeRequired,
@ -213,7 +218,7 @@ func authMethodTypesToPb(methodTypes []domain.UserAuthMethodType) []user.Authent
func authMethodTypeToPb(methodType domain.UserAuthMethodType) user.AuthenticationMethodType {
switch methodType {
case domain.UserAuthMethodTypeOTP:
case domain.UserAuthMethodTypeTOTP:
return user.AuthenticationMethodType_AUTHENTICATION_METHOD_TYPE_TOTP
case domain.UserAuthMethodTypeU2F:
return user.AuthenticationMethodType_AUTHENTICATION_METHOD_TYPE_U2F
@ -223,6 +228,10 @@ func authMethodTypeToPb(methodType domain.UserAuthMethodType) user.Authenticatio
return user.AuthenticationMethodType_AUTHENTICATION_METHOD_TYPE_PASSWORD
case domain.UserAuthMethodTypeIDP:
return user.AuthenticationMethodType_AUTHENTICATION_METHOD_TYPE_IDP
case domain.UserAuthMethodTypeOTPSMS:
return user.AuthenticationMethodType_AUTHENTICATION_METHOD_TYPE_OTP_SMS
case domain.UserAuthMethodTypeOTPEmail:
return user.AuthenticationMethodType_AUTHENTICATION_METHOD_TYPE_OTP_EMAIL
case domain.UserAuthMethodTypeUnspecified:
return user.AuthenticationMethodType_AUTHENTICATION_METHOD_TYPE_UNSPECIFIED
default:

View File

@ -75,6 +75,7 @@ func TestServer_AddHumanUser(t *testing.T) {
Gender: user.Gender_GENDER_DIVERSE.Enum(),
},
Email: &user.SetHumanEmail{},
Phone: &user.SetHumanPhone{},
Metadata: []*user.SetMetadataEntry{
{
Key: "somekey",
@ -97,7 +98,7 @@ func TestServer_AddHumanUser(t *testing.T) {
},
},
{
name: "return verification code",
name: "return email verification code",
args: args{
CTX,
&user.AddHumanUserRequest{
@ -187,6 +188,53 @@ func TestServer_AddHumanUser(t *testing.T) {
},
},
},
{
name: "return phone verification code",
args: args{
CTX,
&user.AddHumanUserRequest{
Organisation: &object.Organisation{
Org: &object.Organisation_OrgId{
OrgId: Tester.Organisation.ID,
},
},
Profile: &user.SetHumanProfile{
FirstName: "Donald",
LastName: "Duck",
NickName: gu.Ptr("Dukkie"),
DisplayName: gu.Ptr("Donald Duck"),
PreferredLanguage: gu.Ptr("en"),
Gender: user.Gender_GENDER_DIVERSE.Enum(),
},
Email: &user.SetHumanEmail{},
Phone: &user.SetHumanPhone{
Phone: "+41791234567",
Verification: &user.SetHumanPhone_ReturnCode{
ReturnCode: &user.ReturnPhoneVerificationCode{},
},
},
Metadata: []*user.SetMetadataEntry{
{
Key: "somekey",
Value: []byte("somevalue"),
},
},
PasswordType: &user.AddHumanUserRequest_Password{
Password: &user.Password{
Password: "DifficultPW666!",
ChangeRequired: true,
},
},
},
},
want: &user.AddHumanUserResponse{
Details: &object.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: Tester.Organisation.ID,
},
PhoneCode: gu.Ptr("something"),
},
},
{
name: "custom template error",
args: args{

View File

@ -194,8 +194,8 @@ func Test_authMethodTypeToPb(t *testing.T) {
user.AuthenticationMethodType_AUTHENTICATION_METHOD_TYPE_UNSPECIFIED,
},
{
"(t)otp",
domain.UserAuthMethodTypeOTP,
"totp",
domain.UserAuthMethodTypeTOTP,
user.AuthenticationMethodType_AUTHENTICATION_METHOD_TYPE_TOTP,
},
{
@ -218,6 +218,16 @@ func Test_authMethodTypeToPb(t *testing.T) {
domain.UserAuthMethodTypeIDP,
user.AuthenticationMethodType_AUTHENTICATION_METHOD_TYPE_IDP,
},
{
"otp sms",
domain.UserAuthMethodTypeOTPSMS,
user.AuthenticationMethodType_AUTHENTICATION_METHOD_TYPE_OTP_SMS,
},
{
"otp email",
domain.UserAuthMethodTypeOTPEmail,
user.AuthenticationMethodType_AUTHENTICATION_METHOD_TYPE_OTP_EMAIL,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {

View File

@ -22,25 +22,35 @@ const (
// [RFC 8176, section 2]: https://datatracker.ietf.org/doc/html/rfc8176#section-2
func AuthMethodTypesToAMR(methodTypes []domain.UserAuthMethodType) []string {
amr := make([]string, 0, 4)
var mfa bool
var factors, otp int
for _, methodType := range methodTypes {
switch methodType {
case domain.UserAuthMethodTypePassword:
amr = append(amr, PWD)
factors++
case domain.UserAuthMethodTypePasswordless:
mfa = true
amr = append(amr, UserPresence)
factors += 2
case domain.UserAuthMethodTypeU2F:
amr = append(amr, UserPresence)
case domain.UserAuthMethodTypeOTP:
amr = append(amr, OTP)
factors++
case domain.UserAuthMethodTypeTOTP,
domain.UserAuthMethodTypeOTPSMS,
domain.UserAuthMethodTypeOTPEmail:
// a user could use multiple (t)otp, which is a factor, but still will be returned as a single `otp` entry
otp++
factors++
case domain.UserAuthMethodTypeIDP:
// no AMR value according to specification
factors++
case domain.UserAuthMethodTypeUnspecified:
// ignore
}
}
if mfa || len(amr) >= 2 {
if otp > 0 {
amr = append(amr, OTP)
}
if factors >= 2 {
amr = append(amr, MFA)
}
return amr

View File

@ -46,12 +46,33 @@ func TestAMR(t *testing.T) {
[]string{UserPresence},
},
{
"otp checked",
"totp checked",
args{
[]domain.UserAuthMethodType{domain.UserAuthMethodTypeOTP},
[]domain.UserAuthMethodType{domain.UserAuthMethodTypeTOTP},
},
[]string{OTP},
},
{
"otp sms checked",
args{
[]domain.UserAuthMethodType{domain.UserAuthMethodTypeOTPSMS},
},
[]string{OTP},
},
{
"otp email checked",
args{
[]domain.UserAuthMethodType{domain.UserAuthMethodTypeOTPEmail},
},
[]string{OTP},
},
{
"multiple (t)otp checked",
args{
[]domain.UserAuthMethodType{domain.UserAuthMethodTypeTOTP, domain.UserAuthMethodTypeOTPEmail},
},
[]string{OTP, MFA},
},
{
"multiple checked",
args{

View File

@ -261,7 +261,7 @@ func CodeChallengeToOIDC(challenge *domain.OIDCCodeChallenge) *oidc.CodeChalleng
func AMRFromMFAType(mfaType domain.MFAType) string {
switch mfaType {
case domain.MFATypeOTP:
case domain.MFATypeTOTP:
return OTP
case domain.MFATypeU2F,
domain.MFATypeU2FUserVerification:

View File

@ -33,7 +33,7 @@ func (l *Login) handleMFAInitVerify(w http.ResponseWriter, r *http.Request) {
}
var verifyData *mfaVerifyData
switch data.MFAType {
case domain.MFATypeOTP:
case domain.MFATypeTOTP:
verifyData = l.handleOTPVerify(w, r, authReq, data)
}
@ -50,13 +50,13 @@ func (l *Login) handleMFAInitVerify(w http.ResponseWriter, r *http.Request) {
func (l *Login) handleOTPVerify(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, data *mfaInitVerifyData) *mfaVerifyData {
userAgentID, _ := http_mw.UserAgentIDFromCtx(r.Context())
_, err := l.command.HumanCheckMFAOTPSetup(setContext(r.Context(), authReq.UserOrgID), authReq.UserID, data.Code, userAgentID, authReq.UserOrgID)
_, err := l.command.HumanCheckMFATOTPSetup(setContext(r.Context(), authReq.UserOrgID), authReq.UserID, data.Code, userAgentID, authReq.UserOrgID)
if err == nil {
return nil
}
mfadata := &mfaVerifyData{
MFAType: data.MFAType,
otpData: otpData{
totpData: totpData{
Secret: data.Secret,
Url: data.URL,
},
@ -73,10 +73,10 @@ func (l *Login) renderMFAInitVerify(w http.ResponseWriter, r *http.Request, auth
translator := l.getTranslator(r.Context(), authReq)
data.baseData = l.getBaseData(r, authReq, "InitMFAOTP.Title", "InitMFAOTP.Description", errID, errMessage)
data.profileData = l.getProfileData(authReq)
if data.MFAType == domain.MFATypeOTP {
code, err := generateQrCode(data.otpData.Url)
if data.MFAType == domain.MFATypeTOTP {
code, err := generateQrCode(data.totpData.Url)
if err == nil {
data.otpData.QrCode = code
data.totpData.QrCode = code
}
}

View File

@ -80,8 +80,8 @@ func (l *Login) renderMFAPrompt(w http.ResponseWriter, r *http.Request, authReq
func (l *Login) handleMFACreation(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, data *mfaVerifyData) {
switch data.MFAType {
case domain.MFATypeOTP:
l.handleOTPCreation(w, r, authReq, data)
case domain.MFATypeTOTP:
l.handleTOTPCreation(w, r, authReq, data)
return
case domain.MFATypeU2F:
l.renderRegisterU2F(w, r, authReq, nil)
@ -90,16 +90,16 @@ func (l *Login) handleMFACreation(w http.ResponseWriter, r *http.Request, authRe
l.renderError(w, r, authReq, caos_errs.ThrowPreconditionFailed(nil, "APP-Or3HO", "Errors.User.MFA.NoProviders"))
}
func (l *Login) handleOTPCreation(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, data *mfaVerifyData) {
otp, err := l.command.AddHumanOTP(setContext(r.Context(), authReq.UserOrgID), authReq.UserID, authReq.UserOrgID)
func (l *Login) handleTOTPCreation(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, data *mfaVerifyData) {
otp, err := l.command.AddHumanTOTP(setContext(r.Context(), authReq.UserOrgID), authReq.UserID, authReq.UserOrgID)
if err != nil {
l.renderError(w, r, authReq, err)
return
}
data.otpData = otpData{
Secret: otp.SecretString,
Url: otp.Url,
data.totpData = totpData{
Secret: otp.Secret,
Url: otp.URI,
}
l.renderMFAInitVerify(w, r, authReq, data, nil)
}

View File

@ -33,7 +33,7 @@ func (l *Login) handleMFAVerify(w http.ResponseWriter, r *http.Request) {
l.renderMFAVerifySelected(w, r, authReq, step, data.SelectedProvider, nil)
return
}
if data.MFAType == domain.MFATypeOTP {
if data.MFAType == domain.MFATypeTOTP {
userAgentID, _ := http_mw.UserAgentIDFromCtx(r.Context())
err = l.authRepo.VerifyMFAOTP(setContext(r.Context(), authReq.UserOrgID), authReq.ID, authReq.UserID, authReq.UserOrgID, data.Code, userAgentID, domain.BrowserInfoFromRequest(r))
@ -45,7 +45,7 @@ func (l *Login) handleMFAVerify(w http.ResponseWriter, r *http.Request) {
}
if err != nil {
l.renderMFAVerifySelected(w, r, authReq, step, domain.MFATypeOTP, err)
l.renderMFAVerifySelected(w, r, authReq, step, domain.MFATypeTOTP, err)
return
}
}
@ -79,9 +79,9 @@ func (l *Login) renderMFAVerifySelected(w http.ResponseWriter, r *http.Request,
data.Description = translator.LocalizeWithoutArgs("VerifyMFAU2F.Description")
l.renderU2FVerification(w, r, authReq, removeSelectedProviderFromList(verificationStep.MFAProviders, domain.MFATypeU2F), nil)
return
case domain.MFATypeOTP:
data.MFAProviders = removeSelectedProviderFromList(verificationStep.MFAProviders, domain.MFATypeOTP)
data.SelectedMFAProvider = domain.MFATypeOTP
case domain.MFATypeTOTP:
data.MFAProviders = removeSelectedProviderFromList(verificationStep.MFAProviders, domain.MFATypeTOTP)
data.SelectedMFAProvider = domain.MFATypeTOTP
data.Title = translator.LocalizeWithoutArgs("VerifyMFAOTP.Title")
data.Description = translator.LocalizeWithoutArgs("VerifyMFAOTP.Description")
default:

View File

@ -673,7 +673,7 @@ type mfaVerifyData struct {
baseData
profileData
MFAType domain.MFAType
otpData
totpData
}
type mfaDoneData struct {
@ -682,7 +682,7 @@ type mfaDoneData struct {
MFAType domain.MFAType
}
type otpData struct {
type totpData struct {
Url string
Secret string
QrCode string

View File

@ -373,7 +373,7 @@ func (repo *AuthRequestRepo) VerifyMFAOTP(ctx context.Context, authRequestID, us
if err != nil {
return err
}
return repo.Command.HumanCheckMFAOTP(ctx, userID, code, resourceOwner, request.WithCurrentInfo(info))
return repo.Command.HumanCheckMFATOTP(ctx, userID, code, resourceOwner, request.WithCurrentInfo(info))
}
func (repo *AuthRequestRepo) BeginMFAU2FLogin(ctx context.Context, userID, resourceOwner, authRequestID, userAgentID string) (login *domain.WebAuthNLogin, err error) {
@ -926,11 +926,15 @@ func (repo *AuthRequestRepo) nextSteps(ctx context.Context, request *domain.Auth
steps = append(steps, new(domain.ExternalNotFoundOptionStep))
return steps, nil
}
steps = append(steps, new(domain.LoginStep))
if domain.IsPrompt(request.Prompt, domain.PromptCreate) {
return append(steps, &domain.RegistrationStep{}), nil
}
if len(request.Prompt) == 0 || domain.IsPrompt(request.Prompt, domain.PromptSelectAccount) {
// if there's a login or consent prompt, but not select account, just return the login step
if len(request.Prompt) > 0 && !domain.IsPrompt(request.Prompt, domain.PromptSelectAccount) {
return append(steps, new(domain.LoginStep)), nil
} else {
// if no user was specified, no prompt or select_account was provided,
// then check the active user sessions (of the user agent)
users, err := repo.usersForUserSelection(request)
if err != nil {
return nil, err
@ -941,11 +945,19 @@ func (repo *AuthRequestRepo) nextSteps(ctx context.Context, request *domain.Auth
if request.SelectedIDPConfigID != "" {
steps = append(steps, &domain.RedirectToExternalIDPStep{})
}
if len(request.Prompt) == 0 && len(users) > 0 {
if len(request.Prompt) == 0 && len(users) == 0 {
steps = append(steps, new(domain.LoginStep))
}
// if no prompt was provided, but there are multiple user sessions, then the user must decide which to use
if len(request.Prompt) == 0 && len(users) > 1 {
steps = append(steps, &domain.SelectUserStep{Users: users})
}
if len(steps) > 0 {
return steps, nil
}
// a single user session was found, use that automatically
request.UserID = users[0].UserID
}
return steps, nil
}
user, err := activeUserByID(ctx, repo.UserViewProvider, repo.UserEventProvider, repo.OrgViewProvider, repo.LockoutPolicyViewProvider, request.UserID, request.LoginPolicy.IgnoreUnknownUsernames)
if err != nil {

View File

@ -296,6 +296,28 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) {
[]domain.NextStep{&domain.RedirectToCallbackStep{}},
nil,
},
{
"user not set prompt create, registration step",
fields{
userSessionViewProvider: &mockViewNoUserSession{},
},
args{&domain.AuthRequest{
Prompt: []domain.Prompt{domain.PromptCreate},
}, false},
[]domain.NextStep{&domain.RegistrationStep{}},
nil,
},
{
"user not set, prompts other than select account, create step",
fields{
userSessionViewProvider: &mockViewNoUserSession{},
},
args{&domain.AuthRequest{
Prompt: []domain.Prompt{domain.PromptLogin, domain.PromptConsent},
}, false},
[]domain.NextStep{&domain.LoginStep{}},
nil,
},
{
"user not set no active session, login step",
fields{
@ -333,7 +355,7 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) {
errors.IsInternal,
},
{
"user not set, prompt select account, login and select account steps",
"user not set, prompt select account, select account step",
fields{
userSessionViewProvider: &mockViewUserSession{
Users: []mockUser{
@ -353,7 +375,6 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) {
},
args{&domain.AuthRequest{Prompt: []domain.Prompt{domain.PromptSelectAccount}}, false},
[]domain.NextStep{
&domain.LoginStep{},
&domain.SelectUserStep{
Users: []domain.UserSelection{
{
@ -373,7 +394,7 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) {
nil,
},
{
"user not set, primary domain set, prompt select account, login and select account steps",
"user not set, primary domain set, prompt select account, select account step",
fields{
userSessionViewProvider: &mockViewUserSession{
Users: []mockUser{
@ -393,7 +414,6 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) {
},
args{&domain.AuthRequest{Prompt: []domain.Prompt{domain.PromptSelectAccount}, RequestedOrgID: "orgID1"}, false},
[]domain.NextStep{
&domain.LoginStep{},
&domain.SelectUserStep{
Users: []domain.UserSelection{
{
@ -407,7 +427,7 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) {
nil,
},
{
"user not set, prompt select account, no active session, login and select account steps",
"user not set, prompt select account, no active session, select account step",
fields{
userSessionViewProvider: &mockViewUserSession{
Users: nil,
@ -416,12 +436,113 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) {
},
args{&domain.AuthRequest{Prompt: []domain.Prompt{domain.PromptSelectAccount}}, false},
[]domain.NextStep{
&domain.LoginStep{},
&domain.SelectUserStep{
Users: []domain.UserSelection{},
}},
nil,
},
{
"user not set single active session, callback step",
fields{
userSessionViewProvider: &mockViewUserSession{
PasswordVerification: time.Now().Add(-5 * time.Minute),
SecondFactorVerification: time.Now().Add(-5 * time.Minute),
Users: []mockUser{
{
"id1",
"loginname1",
"orgID1",
},
},
},
userViewProvider: &mockViewUser{
PasswordSet: true,
IsEmailVerified: true,
MFAMaxSetUp: int32(domain.MFALevelSecondFactor),
},
userEventProvider: &mockEventUser{},
orgViewProvider: &mockViewOrg{State: domain.OrgStateActive},
userGrantProvider: &mockUserGrants{},
projectProvider: &mockProject{},
applicationProvider: &mockApp{app: &query.App{OIDCConfig: &query.OIDCApp{AppType: domain.OIDCApplicationTypeWeb}}},
lockoutPolicyProvider: &mockLockoutPolicy{
policy: &query.LockoutPolicy{
ShowFailures: true,
},
},
idpUserLinksProvider: &mockIDPUserLinks{},
},
args{&domain.AuthRequest{
Request: &domain.AuthRequestOIDC{},
LoginPolicy: &domain.LoginPolicy{
SecondFactors: []domain.SecondFactorType{domain.SecondFactorTypeTOTP},
PasswordCheckLifetime: 10 * 24 * time.Hour,
SecondFactorCheckLifetime: 18 * time.Hour,
},
}, false},
[]domain.NextStep{&domain.RedirectToCallbackStep{}},
nil,
},
{
"user not set multiple active sessions, select account step",
fields{
userSessionViewProvider: &mockViewUserSession{
Users: []mockUser{
{
"id1",
"loginname1",
"orgID1",
},
{
"id2",
"loginname2",
"orgID2",
},
},
},
userViewProvider: &mockViewUser{
PasswordSet: true,
IsEmailVerified: true,
MFAMaxSetUp: int32(domain.MFALevelSecondFactor),
},
userEventProvider: &mockEventUser{},
orgViewProvider: &mockViewOrg{State: domain.OrgStateActive},
userGrantProvider: &mockUserGrants{},
projectProvider: &mockProject{},
applicationProvider: &mockApp{app: &query.App{OIDCConfig: &query.OIDCApp{AppType: domain.OIDCApplicationTypeWeb}}},
lockoutPolicyProvider: &mockLockoutPolicy{
policy: &query.LockoutPolicy{
ShowFailures: true,
},
},
idpUserLinksProvider: &mockIDPUserLinks{},
},
args{&domain.AuthRequest{
Request: &domain.AuthRequestOIDC{},
LoginPolicy: &domain.LoginPolicy{
SecondFactors: []domain.SecondFactorType{domain.SecondFactorTypeTOTP},
PasswordCheckLifetime: 10 * 24 * time.Hour,
SecondFactorCheckLifetime: 18 * time.Hour,
},
}, false},
[]domain.NextStep{&domain.SelectUserStep{
Users: []domain.UserSelection{
{
UserID: "id1",
LoginName: "loginname1",
SelectionPossible: true,
ResourceOwner: "orgID1",
},
{
UserID: "id2",
LoginName: "loginname2",
SelectionPossible: true,
ResourceOwner: "orgID2",
},
},
}},
nil,
},
{
"user not found, not found error",
fields{
@ -889,13 +1010,13 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) {
&domain.AuthRequest{
UserID: "UserID",
LoginPolicy: &domain.LoginPolicy{
SecondFactors: []domain.SecondFactorType{domain.SecondFactorTypeOTP},
SecondFactors: []domain.SecondFactorType{domain.SecondFactorTypeTOTP},
PasswordCheckLifetime: 10 * 24 * time.Hour,
SecondFactorCheckLifetime: 18 * time.Hour,
},
}, false},
[]domain.NextStep{&domain.MFAVerificationStep{
MFAProviders: []domain.MFAType{domain.MFATypeOTP},
MFAProviders: []domain.MFAType{domain.MFATypeTOTP},
}},
nil,
},
@ -923,13 +1044,13 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) {
&domain.AuthRequest{
UserID: "UserID",
LoginPolicy: &domain.LoginPolicy{
SecondFactors: []domain.SecondFactorType{domain.SecondFactorTypeOTP},
SecondFactors: []domain.SecondFactorType{domain.SecondFactorTypeTOTP},
PasswordCheckLifetime: 10 * 24 * time.Hour,
SecondFactorCheckLifetime: 18 * time.Hour,
},
}, false},
[]domain.NextStep{&domain.MFAVerificationStep{
MFAProviders: []domain.MFAType{domain.MFATypeOTP},
MFAProviders: []domain.MFAType{domain.MFATypeTOTP},
}},
nil,
},
@ -959,14 +1080,14 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) {
UserID: "UserID",
SelectedIDPConfigID: "IDPConfigID",
LoginPolicy: &domain.LoginPolicy{
SecondFactors: []domain.SecondFactorType{domain.SecondFactorTypeOTP},
SecondFactors: []domain.SecondFactorType{domain.SecondFactorTypeTOTP},
PasswordCheckLifetime: 10 * 24 * time.Hour,
ExternalLoginCheckLifetime: 10 * 24 * time.Hour,
SecondFactorCheckLifetime: 18 * time.Hour,
},
}, false},
[]domain.NextStep{&domain.MFAVerificationStep{
MFAProviders: []domain.MFAType{domain.MFATypeOTP},
MFAProviders: []domain.MFAType{domain.MFATypeTOTP},
}},
nil,
},
@ -996,7 +1117,7 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) {
&domain.AuthRequest{
UserID: "UserID",
LoginPolicy: &domain.LoginPolicy{
SecondFactors: []domain.SecondFactorType{domain.SecondFactorTypeOTP},
SecondFactors: []domain.SecondFactorType{domain.SecondFactorTypeTOTP},
PasswordCheckLifetime: 10 * 24 * time.Hour,
SecondFactorCheckLifetime: 18 * time.Hour,
},
@ -1027,7 +1148,7 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) {
args{&domain.AuthRequest{
UserID: "UserID",
LoginPolicy: &domain.LoginPolicy{
SecondFactors: []domain.SecondFactorType{domain.SecondFactorTypeOTP},
SecondFactors: []domain.SecondFactorType{domain.SecondFactorTypeTOTP},
PasswordCheckLifetime: 10 * 24 * time.Hour,
SecondFactorCheckLifetime: 18 * time.Hour,
},
@ -1059,7 +1180,7 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) {
args{&domain.AuthRequest{
UserID: "UserID",
LoginPolicy: &domain.LoginPolicy{
SecondFactors: []domain.SecondFactorType{domain.SecondFactorTypeOTP},
SecondFactors: []domain.SecondFactorType{domain.SecondFactorTypeTOTP},
PasswordCheckLifetime: 10 * 24 * time.Hour,
SecondFactorCheckLifetime: 18 * time.Hour,
},
@ -1095,7 +1216,7 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) {
UserID: "UserID",
Request: &domain.AuthRequestOIDC{},
LoginPolicy: &domain.LoginPolicy{
SecondFactors: []domain.SecondFactorType{domain.SecondFactorTypeOTP},
SecondFactors: []domain.SecondFactorType{domain.SecondFactorTypeTOTP},
PasswordCheckLifetime: 10 * 24 * time.Hour,
SecondFactorCheckLifetime: 18 * time.Hour,
},
@ -1132,7 +1253,7 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) {
Prompt: []domain.Prompt{domain.PromptNone},
Request: &domain.AuthRequestOIDC{},
LoginPolicy: &domain.LoginPolicy{
SecondFactors: []domain.SecondFactorType{domain.SecondFactorTypeOTP},
SecondFactors: []domain.SecondFactorType{domain.SecondFactorTypeTOTP},
PasswordCheckLifetime: 10 * 24 * time.Hour,
SecondFactorCheckLifetime: 18 * time.Hour,
},
@ -1169,7 +1290,7 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) {
Prompt: []domain.Prompt{domain.PromptNone},
Request: &domain.AuthRequestOIDC{},
LoginPolicy: &domain.LoginPolicy{
SecondFactors: []domain.SecondFactorType{domain.SecondFactorTypeOTP},
SecondFactors: []domain.SecondFactorType{domain.SecondFactorTypeTOTP},
PasswordCheckLifetime: 10 * 24 * time.Hour,
SecondFactorCheckLifetime: 18 * time.Hour,
},
@ -1208,7 +1329,7 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) {
Prompt: []domain.Prompt{domain.PromptNone},
Request: &domain.AuthRequestOIDC{},
LoginPolicy: &domain.LoginPolicy{
SecondFactors: []domain.SecondFactorType{domain.SecondFactorTypeOTP},
SecondFactors: []domain.SecondFactorType{domain.SecondFactorTypeTOTP},
PasswordCheckLifetime: 10 * 24 * time.Hour,
SecondFactorCheckLifetime: 18 * time.Hour,
},
@ -1248,7 +1369,7 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) {
Prompt: []domain.Prompt{domain.PromptNone},
Request: &domain.AuthRequestOIDC{},
LoginPolicy: &domain.LoginPolicy{
SecondFactors: []domain.SecondFactorType{domain.SecondFactorTypeOTP},
SecondFactors: []domain.SecondFactorType{domain.SecondFactorTypeTOTP},
PasswordCheckLifetime: 10 * 24 * time.Hour,
SecondFactorCheckLifetime: 18 * time.Hour,
},
@ -1288,7 +1409,7 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) {
Prompt: []domain.Prompt{domain.PromptNone},
Request: &domain.AuthRequestOIDC{},
LoginPolicy: &domain.LoginPolicy{
SecondFactors: []domain.SecondFactorType{domain.SecondFactorTypeOTP},
SecondFactors: []domain.SecondFactorType{domain.SecondFactorTypeTOTP},
PasswordCheckLifetime: 10 * 24 * time.Hour,
SecondFactorCheckLifetime: 18 * time.Hour,
},
@ -1329,7 +1450,7 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) {
Prompt: []domain.Prompt{domain.PromptNone},
Request: &domain.AuthRequestOIDC{},
LoginPolicy: &domain.LoginPolicy{
SecondFactors: []domain.SecondFactorType{domain.SecondFactorTypeOTP},
SecondFactors: []domain.SecondFactorType{domain.SecondFactorTypeTOTP},
PasswordCheckLifetime: 10 * 24 * time.Hour,
SecondFactorCheckLifetime: 18 * time.Hour,
},
@ -1399,7 +1520,7 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) {
SelectedIDPConfigID: "IDPConfigID",
LinkingUsers: []*domain.ExternalUser{{IDPConfigID: "IDPConfigID", ExternalUserID: "UserID", DisplayName: "DisplayName"}},
LoginPolicy: &domain.LoginPolicy{
SecondFactors: []domain.SecondFactorType{domain.SecondFactorTypeOTP},
SecondFactors: []domain.SecondFactorType{domain.SecondFactorTypeTOTP},
SecondFactorCheckLifetime: 18 * time.Hour,
PasswordCheckLifetime: 10 * 24 * time.Hour,
},
@ -1503,7 +1624,7 @@ func TestAuthRequestRepo_mfaChecked(t *testing.T) {
args{
request: &domain.AuthRequest{
LoginPolicy: &domain.LoginPolicy{
SecondFactors: []domain.SecondFactorType{domain.SecondFactorTypeOTP},
SecondFactors: []domain.SecondFactorType{domain.SecondFactorTypeTOTP},
MFAInitSkipLifetime: 30 * 24 * time.Hour,
},
},
@ -1516,7 +1637,7 @@ func TestAuthRequestRepo_mfaChecked(t *testing.T) {
},
&domain.MFAPromptStep{
MFAProviders: []domain.MFAType{
domain.MFATypeOTP,
domain.MFATypeTOTP,
},
},
false,
@ -1528,7 +1649,7 @@ func TestAuthRequestRepo_mfaChecked(t *testing.T) {
request: &domain.AuthRequest{
LoginPolicy: &domain.LoginPolicy{
ForceMFA: true,
SecondFactors: []domain.SecondFactorType{domain.SecondFactorTypeOTP},
SecondFactors: []domain.SecondFactorType{domain.SecondFactorTypeTOTP},
MFAInitSkipLifetime: 30 * 24 * time.Hour,
},
},
@ -1542,7 +1663,7 @@ func TestAuthRequestRepo_mfaChecked(t *testing.T) {
&domain.MFAPromptStep{
Required: true,
MFAProviders: []domain.MFAType{
domain.MFATypeOTP,
domain.MFATypeTOTP,
},
},
false,
@ -1573,7 +1694,7 @@ func TestAuthRequestRepo_mfaChecked(t *testing.T) {
args{
request: &domain.AuthRequest{
LoginPolicy: &domain.LoginPolicy{
SecondFactors: []domain.SecondFactorType{domain.SecondFactorTypeOTP},
SecondFactors: []domain.SecondFactorType{domain.SecondFactorTypeTOTP},
SecondFactorCheckLifetime: 18 * time.Hour,
},
},
@ -1595,7 +1716,7 @@ func TestAuthRequestRepo_mfaChecked(t *testing.T) {
args{
request: &domain.AuthRequest{
LoginPolicy: &domain.LoginPolicy{
SecondFactors: []domain.SecondFactorType{domain.SecondFactorTypeOTP},
SecondFactors: []domain.SecondFactorType{domain.SecondFactorTypeTOTP},
SecondFactorCheckLifetime: 18 * time.Hour,
},
},
@ -1610,7 +1731,7 @@ func TestAuthRequestRepo_mfaChecked(t *testing.T) {
},
&domain.MFAVerificationStep{
MFAProviders: []domain.MFAType{domain.MFATypeOTP},
MFAProviders: []domain.MFAType{domain.MFATypeTOTP},
},
false,
nil,
@ -1620,7 +1741,7 @@ func TestAuthRequestRepo_mfaChecked(t *testing.T) {
args{
request: &domain.AuthRequest{
LoginPolicy: &domain.LoginPolicy{
SecondFactors: []domain.SecondFactorType{domain.SecondFactorTypeOTP},
SecondFactors: []domain.SecondFactorType{domain.SecondFactorTypeTOTP},
SecondFactorCheckLifetime: 18 * time.Hour,
},
},
@ -1634,7 +1755,7 @@ func TestAuthRequestRepo_mfaChecked(t *testing.T) {
isInternal: false,
},
&domain.MFAVerificationStep{
MFAProviders: []domain.MFAType{domain.MFATypeOTP},
MFAProviders: []domain.MFAType{domain.MFATypeTOTP},
},
false,
nil,
@ -1644,7 +1765,7 @@ func TestAuthRequestRepo_mfaChecked(t *testing.T) {
args{
request: &domain.AuthRequest{
LoginPolicy: &domain.LoginPolicy{
SecondFactors: []domain.SecondFactorType{domain.SecondFactorTypeOTP},
SecondFactors: []domain.SecondFactorType{domain.SecondFactorTypeTOTP},
SecondFactorCheckLifetime: 18 * time.Hour,
},
},
@ -1666,7 +1787,7 @@ func TestAuthRequestRepo_mfaChecked(t *testing.T) {
args{
request: &domain.AuthRequest{
LoginPolicy: &domain.LoginPolicy{
SecondFactors: []domain.SecondFactorType{domain.SecondFactorTypeOTP},
SecondFactors: []domain.SecondFactorType{domain.SecondFactorTypeTOTP},
SecondFactorCheckLifetime: 18 * time.Hour,
ForceMFA: true,
},
@ -1682,7 +1803,7 @@ func TestAuthRequestRepo_mfaChecked(t *testing.T) {
&domain.MFAPromptStep{
Required: true,
MFAProviders: []domain.MFAType{
domain.MFATypeOTP,
domain.MFATypeTOTP,
},
},
false,
@ -1693,7 +1814,7 @@ func TestAuthRequestRepo_mfaChecked(t *testing.T) {
args{
request: &domain.AuthRequest{
LoginPolicy: &domain.LoginPolicy{
SecondFactors: []domain.SecondFactorType{domain.SecondFactorTypeOTP},
SecondFactors: []domain.SecondFactorType{domain.SecondFactorTypeTOTP},
SecondFactorCheckLifetime: 18 * time.Hour,
ForceMFA: true,
ForceMFALocalOnly: true,

View File

@ -199,12 +199,21 @@ func authMethodsFromSession(session *query.Session) []domain.UserAuthMethodType
// TODO: add checks with https://github.com/zitadel/zitadel/issues/5477
/*
if !session.TOTPFactor.TOTPCheckedAt.IsZero() {
types = append(types, domain.UserAuthMethodTypeOTP)
types = append(types, domain.UserAuthMethodTypeTOTP)
}
if !session.U2FFactor.U2FCheckedAt.IsZero() {
types = append(types, domain.UserAuthMethodTypeU2F)
}
*/
// TODO: add checks with https://github.com/zitadel/zitadel/issues/6224
/*
if !session.TOTPFactor.OTPSMSCheckedAt.IsZero() {
types = append(types, domain.UserAuthMethodTypeOTPSMS)
}
if !session.TOTPFactor.OTPEmailCheckedAt.IsZero() {
types = append(types, domain.UserAuthMethodTypeOTPEmail)
}
*/
return types
}

View File

@ -48,6 +48,8 @@ type InstanceSetup struct {
PasswordVerificationCode *crypto.GeneratorConfig
PasswordlessInitCode *crypto.GeneratorConfig
DomainVerification *crypto.GeneratorConfig
OTPSMS *crypto.GeneratorConfig
OTPEmail *crypto.GeneratorConfig
}
PasswordComplexityPolicy struct {
MinLength uint64
@ -201,6 +203,8 @@ func (c *Commands) SetUpInstance(ctx context.Context, setup *InstanceSetup) (str
prepareAddSecretGeneratorConfig(instanceAgg, domain.SecretGeneratorTypePasswordResetCode, setup.SecretGenerators.PasswordVerificationCode),
prepareAddSecretGeneratorConfig(instanceAgg, domain.SecretGeneratorTypePasswordlessInitCode, setup.SecretGenerators.PasswordlessInitCode),
prepareAddSecretGeneratorConfig(instanceAgg, domain.SecretGeneratorTypeVerifyDomain, setup.SecretGenerators.DomainVerification),
prepareAddSecretGeneratorConfig(instanceAgg, domain.SecretGeneratorTypeOTPSMS, setup.SecretGenerators.OTPSMS),
prepareAddSecretGeneratorConfig(instanceAgg, domain.SecretGeneratorTypeOTPEmail, setup.SecretGenerators.OTPEmail),
prepareAddDefaultPasswordComplexityPolicy(
instanceAgg,
@ -241,8 +245,12 @@ func (c *Commands) SetUpInstance(ctx context.Context, setup *InstanceSetup) (str
setup.LoginPolicy.SecondFactorCheckLifetime,
setup.LoginPolicy.MultiFactorCheckLifetime,
),
prepareAddSecondFactorToDefaultLoginPolicy(instanceAgg, domain.SecondFactorTypeOTP),
prepareAddSecondFactorToDefaultLoginPolicy(instanceAgg, domain.SecondFactorTypeTOTP),
prepareAddSecondFactorToDefaultLoginPolicy(instanceAgg, domain.SecondFactorTypeU2F),
/* TODO: incomment when usable
prepareAddSecondFactorToDefaultLoginPolicy(instanceAgg, domain.SecondFactorTypeOTPEmail),
prepareAddSecondFactorToDefaultLoginPolicy(instanceAgg, domain.SecondFactorTypeOTPSMS),
*/
prepareAddMultiFactorToDefaultLoginPolicy(instanceAgg, domain.MultiFactorTypeU2FWithPIN),
prepareAddDefaultPrivacyPolicy(instanceAgg, setup.PrivacyPolicy.TOSLink, setup.PrivacyPolicy.PrivacyLink, setup.PrivacyPolicy.HelpLink, setup.PrivacyPolicy.SupportEmail),

View File

@ -884,7 +884,7 @@ func TestCommandSide_AddSecondFactorDefaultLoginPolicy(t *testing.T) {
eventFromEventPusher(
instance.NewLoginPolicySecondFactorAddedEvent(context.Background(),
&instance.NewAggregate("INSTANCE").Aggregate,
domain.SecondFactorTypeOTP,
domain.SecondFactorTypeTOTP,
),
),
),
@ -892,14 +892,14 @@ func TestCommandSide_AddSecondFactorDefaultLoginPolicy(t *testing.T) {
},
args: args{
ctx: context.Background(),
factor: domain.SecondFactorTypeOTP,
factor: domain.SecondFactorTypeTOTP,
},
res: res{
err: caos_errs.IsErrorAlreadyExists,
},
},
{
name: "add factor, ok",
name: "add factor totp, ok",
fields: fields{
eventstore: eventstoreExpect(
t,
@ -910,7 +910,7 @@ func TestCommandSide_AddSecondFactorDefaultLoginPolicy(t *testing.T) {
"INSTANCE",
instance.NewLoginPolicySecondFactorAddedEvent(context.Background(),
&instance.NewAggregate("INSTANCE").Aggregate,
domain.SecondFactorTypeOTP),
domain.SecondFactorTypeTOTP),
),
},
),
@ -918,7 +918,98 @@ func TestCommandSide_AddSecondFactorDefaultLoginPolicy(t *testing.T) {
},
args: args{
ctx: authz.WithInstanceID(context.Background(), "INSTANCE"),
factor: domain.SecondFactorTypeOTP,
factor: domain.SecondFactorTypeTOTP,
},
res: res{
want: &domain.ObjectDetails{
ResourceOwner: "INSTANCE",
},
},
},
{
name: "add factor otp email, ok ",
fields: fields{
eventstore: eventstoreExpect(
t,
expectFilter(),
expectPush(
[]*repository.Event{
eventFromEventPusherWithInstanceID(
"INSTANCE",
instance.NewLoginPolicySecondFactorAddedEvent(context.Background(),
&instance.NewAggregate("INSTANCE").Aggregate,
domain.SecondFactorTypeOTPEmail),
),
},
),
),
},
args: args{
ctx: authz.WithInstanceID(context.Background(), "INSTANCE"),
factor: domain.SecondFactorTypeOTPEmail,
},
res: res{
want: &domain.ObjectDetails{
ResourceOwner: "INSTANCE",
},
},
},
{
name: "add factor otp sms, ok",
fields: fields{
eventstore: eventstoreExpect(
t,
expectFilter(),
expectPush(
[]*repository.Event{
eventFromEventPusherWithInstanceID(
"INSTANCE",
instance.NewLoginPolicySecondFactorAddedEvent(context.Background(),
&instance.NewAggregate("INSTANCE").Aggregate,
domain.SecondFactorTypeOTPSMS),
),
},
),
),
},
args: args{
ctx: authz.WithInstanceID(context.Background(), "INSTANCE"),
factor: domain.SecondFactorTypeOTPSMS,
},
res: res{
want: &domain.ObjectDetails{
ResourceOwner: "INSTANCE",
},
},
},
{
name: "add factor totp, add otp sms, ok",
fields: fields{
eventstore: eventstoreExpect(
t,
expectFilter(
eventFromEventPusher(
instance.NewLoginPolicySecondFactorAddedEvent(context.Background(),
&instance.NewAggregate("INSTANCE").Aggregate,
domain.SecondFactorTypeTOTP,
),
),
),
expectPush(
[]*repository.Event{
eventFromEventPusherWithInstanceID(
"INSTANCE",
instance.NewLoginPolicySecondFactorAddedEvent(context.Background(),
&instance.NewAggregate("INSTANCE").Aggregate,
domain.SecondFactorTypeOTPSMS),
),
},
),
),
},
args: args{
ctx: authz.WithInstanceID(context.Background(), "INSTANCE"),
factor: domain.SecondFactorTypeOTPSMS,
},
res: res{
want: &domain.ObjectDetails{
@ -989,14 +1080,14 @@ func TestCommandSide_RemoveSecondFactorDefaultLoginPolicy(t *testing.T) {
},
args: args{
ctx: context.Background(),
factor: domain.SecondFactorTypeOTP,
factor: domain.SecondFactorTypeTOTP,
},
res: res{
err: caos_errs.IsNotFound,
},
},
{
name: "factor removed, not found error",
name: "factor removed totp, not found error",
fields: fields{
eventstore: eventstoreExpect(
t,
@ -1004,13 +1095,13 @@ func TestCommandSide_RemoveSecondFactorDefaultLoginPolicy(t *testing.T) {
eventFromEventPusher(
instance.NewLoginPolicySecondFactorAddedEvent(context.Background(),
&instance.NewAggregate("INSTANCE").Aggregate,
domain.SecondFactorTypeOTP,
domain.SecondFactorTypeTOTP,
),
),
eventFromEventPusher(
instance.NewLoginPolicySecondFactorRemovedEvent(context.Background(),
&instance.NewAggregate("INSTANCE").Aggregate,
domain.SecondFactorTypeOTP,
domain.SecondFactorTypeTOTP,
),
),
),
@ -1018,14 +1109,14 @@ func TestCommandSide_RemoveSecondFactorDefaultLoginPolicy(t *testing.T) {
},
args: args{
ctx: context.Background(),
factor: domain.SecondFactorTypeOTP,
factor: domain.SecondFactorTypeTOTP,
},
res: res{
err: caos_errs.IsNotFound,
},
},
{
name: "add factor, ok",
name: "factor removed otp email, not found error",
fields: fields{
eventstore: eventstoreExpect(
t,
@ -1033,7 +1124,65 @@ func TestCommandSide_RemoveSecondFactorDefaultLoginPolicy(t *testing.T) {
eventFromEventPusher(
instance.NewLoginPolicySecondFactorAddedEvent(context.Background(),
&instance.NewAggregate("INSTANCE").Aggregate,
domain.SecondFactorTypeOTP,
domain.SecondFactorTypeOTPEmail,
),
),
eventFromEventPusher(
instance.NewLoginPolicySecondFactorRemovedEvent(context.Background(),
&instance.NewAggregate("INSTANCE").Aggregate,
domain.SecondFactorTypeOTPEmail,
),
),
),
),
},
args: args{
ctx: context.Background(),
factor: domain.SecondFactorTypeOTPEmail,
},
res: res{
err: caos_errs.IsNotFound,
},
},
{
name: "factor removed otp sms, not found error",
fields: fields{
eventstore: eventstoreExpect(
t,
expectFilter(
eventFromEventPusher(
instance.NewLoginPolicySecondFactorAddedEvent(context.Background(),
&instance.NewAggregate("INSTANCE").Aggregate,
domain.SecondFactorTypeOTPSMS,
),
),
eventFromEventPusher(
instance.NewLoginPolicySecondFactorRemovedEvent(context.Background(),
&instance.NewAggregate("INSTANCE").Aggregate,
domain.SecondFactorTypeOTPSMS,
),
),
),
),
},
args: args{
ctx: context.Background(),
factor: domain.SecondFactorTypeOTPSMS,
},
res: res{
err: caos_errs.IsNotFound,
},
},
{
name: "remove factor totp, ok",
fields: fields{
eventstore: eventstoreExpect(
t,
expectFilter(
eventFromEventPusher(
instance.NewLoginPolicySecondFactorAddedEvent(context.Background(),
&instance.NewAggregate("INSTANCE").Aggregate,
domain.SecondFactorTypeTOTP,
),
),
),
@ -1042,7 +1191,7 @@ func TestCommandSide_RemoveSecondFactorDefaultLoginPolicy(t *testing.T) {
eventFromEventPusher(
instance.NewLoginPolicySecondFactorRemovedEvent(context.Background(),
&instance.NewAggregate("INSTANCE").Aggregate,
domain.SecondFactorTypeOTP),
domain.SecondFactorTypeTOTP),
),
},
),
@ -1050,7 +1199,7 @@ func TestCommandSide_RemoveSecondFactorDefaultLoginPolicy(t *testing.T) {
},
args: args{
ctx: context.Background(),
factor: domain.SecondFactorTypeOTP,
factor: domain.SecondFactorTypeTOTP,
},
res: res{
want: &domain.ObjectDetails{
@ -1058,6 +1207,97 @@ func TestCommandSide_RemoveSecondFactorDefaultLoginPolicy(t *testing.T) {
},
},
},
{
name: "remove factor email, ok",
fields: fields{
eventstore: eventstoreExpect(
t,
expectFilter(
eventFromEventPusher(
instance.NewLoginPolicySecondFactorAddedEvent(context.Background(),
&instance.NewAggregate("INSTANCE").Aggregate,
domain.SecondFactorTypeOTPEmail,
),
),
),
expectPush(
[]*repository.Event{
eventFromEventPusher(
instance.NewLoginPolicySecondFactorRemovedEvent(context.Background(),
&instance.NewAggregate("INSTANCE").Aggregate,
domain.SecondFactorTypeOTPEmail),
),
},
),
),
},
args: args{
ctx: context.Background(),
factor: domain.SecondFactorTypeOTPEmail,
},
res: res{
want: &domain.ObjectDetails{
ResourceOwner: "INSTANCE",
},
},
},
{
name: "remove factor sms, ok",
fields: fields{
eventstore: eventstoreExpect(
t,
expectFilter(
eventFromEventPusher(
instance.NewLoginPolicySecondFactorAddedEvent(context.Background(),
&instance.NewAggregate("INSTANCE").Aggregate,
domain.SecondFactorTypeOTPSMS,
),
),
),
expectPush(
[]*repository.Event{
eventFromEventPusher(
instance.NewLoginPolicySecondFactorRemovedEvent(context.Background(),
&instance.NewAggregate("INSTANCE").Aggregate,
domain.SecondFactorTypeOTPSMS),
),
},
),
),
},
args: args{
ctx: context.Background(),
factor: domain.SecondFactorTypeOTPSMS,
},
res: res{
want: &domain.ObjectDetails{
ResourceOwner: "INSTANCE",
},
},
},
{
name: "factor added totp, removed otp sms, not found error",
fields: fields{
eventstore: eventstoreExpect(
t,
expectFilter(
eventFromEventPusher(
instance.NewLoginPolicySecondFactorAddedEvent(context.Background(),
&instance.NewAggregate("INSTANCE").Aggregate,
domain.SecondFactorTypeTOTP,
),
),
),
),
},
args: args{
ctx: context.Background(),
factor: domain.SecondFactorTypeOTPSMS,
},
res: res{
err: caos_errs.IsNotFound,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {

View File

@ -79,10 +79,26 @@ func (c *Commands) ChangeSecretGeneratorConfig(ctx context.Context, generatorTyp
if err != nil {
return nil, err
}
if generatorWriteModel.State == domain.SecretGeneratorStateUnspecified || generatorWriteModel.State == domain.SecretGeneratorStateRemoved {
return nil, errors.ThrowNotFound(nil, "COMMAND-3n9ls", "Errors.SecretGenerator.NotFound")
}
instanceAgg := InstanceAggregateFromWriteModel(&generatorWriteModel.WriteModel)
if generatorWriteModel.State == domain.SecretGeneratorStateUnspecified || generatorWriteModel.State == domain.SecretGeneratorStateRemoved {
err = c.pushAppendAndReduce(ctx, generatorWriteModel,
instance.NewSecretGeneratorAddedEvent(
ctx,
instanceAgg,
generatorType,
config.Length,
config.Expiry,
config.IncludeLowerLetters,
config.IncludeUpperLetters,
config.IncludeDigits,
config.IncludeSymbols,
),
)
if err != nil {
return nil, err
}
return writeModelToObjectDetails(&generatorWriteModel.WriteModel), nil
}
changedEvent, hasChanged, err := generatorWriteModel.NewChangedEvent(
ctx,
@ -100,12 +116,7 @@ func (c *Commands) ChangeSecretGeneratorConfig(ctx context.Context, generatorTyp
if !hasChanged {
return nil, errors.ThrowPreconditionFailed(nil, "COMMAND-m0o3f", "Errors.NoChangesFound")
}
pushedEvents, err := c.eventstore.Push(ctx, changedEvent)
if err != nil {
return nil, err
}
err = AppendAndReduce(generatorWriteModel, pushedEvents...)
if err != nil {
if err = c.pushAppendAndReduce(ctx, generatorWriteModel, changedEvent); err != nil {
return nil, err
}
return writeModelToObjectDetails(&generatorWriteModel.WriteModel), nil

View File

@ -6,8 +6,8 @@ import (
"time"
"github.com/stretchr/testify/assert"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/domain"
caos_errs "github.com/zitadel/zitadel/internal/errors"
@ -155,7 +155,7 @@ func TestCommandSide_AddSecretGenerator(t *testing.T) {
func TestCommandSide_ChangeSecretGenerator(t *testing.T) {
type fields struct {
eventstore *eventstore.Eventstore
eventstore func(t *testing.T) *eventstore.Eventstore
}
type args struct {
ctx context.Context
@ -176,12 +176,10 @@ func TestCommandSide_ChangeSecretGenerator(t *testing.T) {
{
name: "empty generatortype, invalid error",
fields: fields{
eventstore: eventstoreExpect(
t,
),
eventstore: expectEventstore(),
},
args: args{
ctx: context.Background(),
ctx: authz.WithInstanceID(context.Background(), "INSTANCE"),
generator: &crypto.GeneratorConfig{},
generatorType: domain.SecretGeneratorTypeUnspecified,
},
@ -190,26 +188,53 @@ func TestCommandSide_ChangeSecretGenerator(t *testing.T) {
},
},
{
name: "generator not existing, not found error",
name: "generator not existing, new added ok",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(),
expectPush(
[]*repository.Event{
eventFromEventPusherWithInstanceID(
"INSTANCE",
instance.NewSecretGeneratorAddedEvent(
context.Background(),
&instance.NewAggregate("INSTANCE").Aggregate,
domain.SecretGeneratorTypeInitCode,
4,
time.Hour*1,
true,
true,
true,
true,
),
),
},
uniqueConstraintsFromEventConstraintWithInstanceID("INSTANCE", instance.NewAddSecretGeneratorTypeUniqueConstraint(domain.SecretGeneratorTypeInitCode)),
),
),
},
args: args{
ctx: context.Background(),
ctx: authz.WithInstanceID(context.Background(), "INSTANCE"),
generator: &crypto.GeneratorConfig{
Length: 4,
Expiry: 1 * time.Hour,
IncludeLowerLetters: true,
IncludeUpperLetters: true,
IncludeDigits: true,
IncludeSymbols: true,
},
generatorType: domain.SecretGeneratorTypeInitCode,
},
res: res{
err: caos_errs.IsNotFound,
want: &domain.ObjectDetails{
ResourceOwner: "INSTANCE",
},
},
},
{
name: "generator removed, not found error",
name: "generator removed, new added ok",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
instance.NewSecretGeneratorAddedEvent(
@ -230,21 +255,49 @@ func TestCommandSide_ChangeSecretGenerator(t *testing.T) {
domain.SecretGeneratorTypeInitCode),
),
),
expectPush(
[]*repository.Event{
eventFromEventPusherWithInstanceID(
"INSTANCE",
instance.NewSecretGeneratorAddedEvent(
context.Background(),
&instance.NewAggregate("INSTANCE").Aggregate,
domain.SecretGeneratorTypeInitCode,
4,
time.Hour*1,
true,
true,
true,
true,
),
),
},
uniqueConstraintsFromEventConstraintWithInstanceID("INSTANCE", instance.NewAddSecretGeneratorTypeUniqueConstraint(domain.SecretGeneratorTypeInitCode)),
),
),
},
args: args{
ctx: context.Background(),
ctx: authz.WithInstanceID(context.Background(), "INSTANCE"),
generator: &crypto.GeneratorConfig{
Length: 4,
Expiry: 1 * time.Hour,
IncludeLowerLetters: true,
IncludeUpperLetters: true,
IncludeDigits: true,
IncludeSymbols: true,
},
generatorType: domain.SecretGeneratorTypeInitCode,
},
res: res{
err: caos_errs.IsNotFound,
want: &domain.ObjectDetails{
ResourceOwner: "INSTANCE",
},
},
},
{
name: "no changes, precondition error",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
instance.NewSecretGeneratorAddedEvent(
@ -263,7 +316,7 @@ func TestCommandSide_ChangeSecretGenerator(t *testing.T) {
),
},
args: args{
ctx: context.Background(),
ctx: authz.WithInstanceID(context.Background(), "INSTANCE"),
generator: &crypto.GeneratorConfig{
Length: 4,
Expiry: 1 * time.Hour,
@ -281,8 +334,7 @@ func TestCommandSide_ChangeSecretGenerator(t *testing.T) {
{
name: "secret generator change, ok",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
instance.NewSecretGeneratorAddedEvent(
@ -300,7 +352,7 @@ func TestCommandSide_ChangeSecretGenerator(t *testing.T) {
),
expectPush(
[]*repository.Event{
eventFromEventPusher(
eventFromEventPusherWithInstanceID("INSTANCE",
newSecretGeneratorChangedEvent(context.Background(),
domain.SecretGeneratorTypeInitCode,
8,
@ -308,14 +360,15 @@ func TestCommandSide_ChangeSecretGenerator(t *testing.T) {
false,
false,
false,
false),
false,
),
),
},
),
),
},
args: args{
ctx: context.Background(),
ctx: authz.WithInstanceID(context.Background(), "INSTANCE"),
generator: &crypto.GeneratorConfig{
Length: 8,
Expiry: 2 * time.Hour,
@ -336,7 +389,7 @@ func TestCommandSide_ChangeSecretGenerator(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.ChangeSecretGeneratorConfig(tt.args.ctx, tt.args.generatorType, tt.args.generator)
if tt.res.err == nil {

View File

@ -231,7 +231,7 @@ func TestCommandSide_AddLoginPolicy(t *testing.T) {
eventFromEventPusher(
org.NewLoginPolicySecondFactorAddedEvent(context.Background(),
&org.NewAggregate("org1").Aggregate,
domain.SecondFactorTypeOTP,
domain.SecondFactorTypeTOTP,
),
),
eventFromEventPusher(
@ -265,7 +265,7 @@ func TestCommandSide_AddLoginPolicy(t *testing.T) {
MFAInitSkipLifetime: time.Hour * 3,
SecondFactorCheckLifetime: time.Hour * 4,
MultiFactorCheckLifetime: time.Hour * 5,
SecondFactors: []domain.SecondFactorType{domain.SecondFactorTypeOTP},
SecondFactors: []domain.SecondFactorType{domain.SecondFactorTypeTOTP},
MultiFactors: []domain.MultiFactorType{domain.MultiFactorTypeU2FWithPIN},
},
},
@ -1504,7 +1504,7 @@ func TestCommandSide_AddSecondFactorLoginPolicy(t *testing.T) {
eventFromEventPusher(
org.NewLoginPolicySecondFactorAddedEvent(context.Background(),
&org.NewAggregate("org1").Aggregate,
domain.SecondFactorTypeOTP,
domain.SecondFactorTypeTOTP,
),
),
),
@ -1512,7 +1512,7 @@ func TestCommandSide_AddSecondFactorLoginPolicy(t *testing.T) {
},
args: args{
ctx: context.Background(),
factor: domain.SecondFactorTypeOTP,
factor: domain.SecondFactorTypeTOTP,
resourceOwner: "org1",
},
res: res{
@ -1520,7 +1520,7 @@ func TestCommandSide_AddSecondFactorLoginPolicy(t *testing.T) {
},
},
{
name: "add factor, ok",
name: "add factor totp, ok",
fields: fields{
eventstore: eventstoreExpect(
t,
@ -1530,7 +1530,7 @@ func TestCommandSide_AddSecondFactorLoginPolicy(t *testing.T) {
eventFromEventPusher(
org.NewLoginPolicySecondFactorAddedEvent(context.Background(),
&org.NewAggregate("org1").Aggregate,
domain.SecondFactorTypeOTP),
domain.SecondFactorTypeTOTP),
),
},
),
@ -1538,11 +1538,96 @@ func TestCommandSide_AddSecondFactorLoginPolicy(t *testing.T) {
},
args: args{
ctx: context.Background(),
factor: domain.SecondFactorTypeOTP,
factor: domain.SecondFactorTypeTOTP,
resourceOwner: "org1",
},
res: res{
want: domain.SecondFactorTypeOTP,
want: domain.SecondFactorTypeTOTP,
},
},
{
name: "add factor otp email, ok",
fields: fields{
eventstore: eventstoreExpect(
t,
expectFilter(),
expectPush(
[]*repository.Event{
eventFromEventPusher(
org.NewLoginPolicySecondFactorAddedEvent(context.Background(),
&org.NewAggregate("org1").Aggregate,
domain.SecondFactorTypeOTPEmail),
),
},
),
),
},
args: args{
ctx: context.Background(),
factor: domain.SecondFactorTypeOTPEmail,
resourceOwner: "org1",
},
res: res{
want: domain.SecondFactorTypeOTPEmail,
},
},
{
name: "add factor otp sms, ok",
fields: fields{
eventstore: eventstoreExpect(
t,
expectFilter(),
expectPush(
[]*repository.Event{
eventFromEventPusher(
org.NewLoginPolicySecondFactorAddedEvent(context.Background(),
&org.NewAggregate("org1").Aggregate,
domain.SecondFactorTypeOTPSMS),
),
},
),
),
},
args: args{
ctx: context.Background(),
factor: domain.SecondFactorTypeOTPSMS,
resourceOwner: "org1",
},
res: res{
want: domain.SecondFactorTypeOTPSMS,
},
},
{
name: "add factor totp, add otp sms, ok",
fields: fields{
eventstore: eventstoreExpect(
t,
expectFilter(
eventFromEventPusher(
org.NewLoginPolicySecondFactorAddedEvent(context.Background(),
&org.NewAggregate("org1").Aggregate,
domain.SecondFactorTypeTOTP,
),
),
),
expectPush(
[]*repository.Event{
eventFromEventPusher(
org.NewLoginPolicySecondFactorAddedEvent(context.Background(),
&org.NewAggregate("org1").Aggregate,
domain.SecondFactorTypeOTPSMS),
),
},
),
),
},
args: args{
ctx: context.Background(),
factor: domain.SecondFactorTypeOTPSMS,
resourceOwner: "org1",
},
res: res{
want: domain.SecondFactorTypeOTPSMS,
},
},
}
@ -1593,7 +1678,7 @@ func TestCommandSide_RemoveSecondFactoroginPolicy(t *testing.T) {
},
args: args{
ctx: context.Background(),
factor: domain.SecondFactorTypeOTP,
factor: domain.SecondFactorTypeTOTP,
},
res: res{
err: caos_errs.IsErrorInvalidArgument,
@ -1624,7 +1709,7 @@ func TestCommandSide_RemoveSecondFactoroginPolicy(t *testing.T) {
},
args: args{
ctx: context.Background(),
factor: domain.SecondFactorTypeOTP,
factor: domain.SecondFactorTypeTOTP,
resourceOwner: "org1",
},
res: res{
@ -1632,7 +1717,7 @@ func TestCommandSide_RemoveSecondFactoroginPolicy(t *testing.T) {
},
},
{
name: "factor removed, not found error",
name: "factor totp removed, not found error",
fields: fields{
eventstore: eventstoreExpect(
t,
@ -1640,13 +1725,13 @@ func TestCommandSide_RemoveSecondFactoroginPolicy(t *testing.T) {
eventFromEventPusher(
org.NewLoginPolicySecondFactorAddedEvent(context.Background(),
&org.NewAggregate("org1").Aggregate,
domain.SecondFactorTypeOTP,
domain.SecondFactorTypeTOTP,
),
),
eventFromEventPusher(
org.NewLoginPolicySecondFactorRemovedEvent(context.Background(),
&org.NewAggregate("org1").Aggregate,
domain.SecondFactorTypeOTP,
domain.SecondFactorTypeTOTP,
),
),
),
@ -1654,7 +1739,7 @@ func TestCommandSide_RemoveSecondFactoroginPolicy(t *testing.T) {
},
args: args{
ctx: context.Background(),
factor: domain.SecondFactorTypeOTP,
factor: domain.SecondFactorTypeTOTP,
resourceOwner: "org1",
},
res: res{
@ -1662,7 +1747,7 @@ func TestCommandSide_RemoveSecondFactoroginPolicy(t *testing.T) {
},
},
{
name: "add factor, ok",
name: "factor otp email removed, not found error",
fields: fields{
eventstore: eventstoreExpect(
t,
@ -1670,7 +1755,67 @@ func TestCommandSide_RemoveSecondFactoroginPolicy(t *testing.T) {
eventFromEventPusher(
org.NewLoginPolicySecondFactorAddedEvent(context.Background(),
&org.NewAggregate("org1").Aggregate,
domain.SecondFactorTypeOTP,
domain.SecondFactorTypeOTPEmail,
),
),
eventFromEventPusher(
org.NewLoginPolicySecondFactorRemovedEvent(context.Background(),
&org.NewAggregate("org1").Aggregate,
domain.SecondFactorTypeOTPEmail,
),
),
),
),
},
args: args{
ctx: context.Background(),
factor: domain.SecondFactorTypeOTPEmail,
resourceOwner: "org1",
},
res: res{
err: caos_errs.IsNotFound,
},
},
{
name: "factor otp sms removed, not found error",
fields: fields{
eventstore: eventstoreExpect(
t,
expectFilter(
eventFromEventPusher(
org.NewLoginPolicySecondFactorAddedEvent(context.Background(),
&org.NewAggregate("org1").Aggregate,
domain.SecondFactorTypeOTPSMS,
),
),
eventFromEventPusher(
org.NewLoginPolicySecondFactorRemovedEvent(context.Background(),
&org.NewAggregate("org1").Aggregate,
domain.SecondFactorTypeOTPSMS,
),
),
),
),
},
args: args{
ctx: context.Background(),
factor: domain.SecondFactorTypeOTPSMS,
resourceOwner: "org1",
},
res: res{
err: caos_errs.IsNotFound,
},
},
{
name: "add factor totp, ok",
fields: fields{
eventstore: eventstoreExpect(
t,
expectFilter(
eventFromEventPusher(
org.NewLoginPolicySecondFactorAddedEvent(context.Background(),
&org.NewAggregate("org1").Aggregate,
domain.SecondFactorTypeTOTP,
),
),
),
@ -1679,7 +1824,7 @@ func TestCommandSide_RemoveSecondFactoroginPolicy(t *testing.T) {
eventFromEventPusher(
org.NewLoginPolicySecondFactorRemovedEvent(context.Background(),
&org.NewAggregate("org1").Aggregate,
domain.SecondFactorTypeOTP),
domain.SecondFactorTypeTOTP),
),
},
),
@ -1687,7 +1832,77 @@ func TestCommandSide_RemoveSecondFactoroginPolicy(t *testing.T) {
},
args: args{
ctx: context.Background(),
factor: domain.SecondFactorTypeOTP,
factor: domain.SecondFactorTypeTOTP,
resourceOwner: "org1",
},
res: res{
want: &domain.ObjectDetails{
ResourceOwner: "org1",
},
},
},
{
name: "add factor otp email, ok",
fields: fields{
eventstore: eventstoreExpect(
t,
expectFilter(
eventFromEventPusher(
org.NewLoginPolicySecondFactorAddedEvent(context.Background(),
&org.NewAggregate("org1").Aggregate,
domain.SecondFactorTypeOTPEmail,
),
),
),
expectPush(
[]*repository.Event{
eventFromEventPusher(
org.NewLoginPolicySecondFactorRemovedEvent(context.Background(),
&org.NewAggregate("org1").Aggregate,
domain.SecondFactorTypeOTPEmail),
),
},
),
),
},
args: args{
ctx: context.Background(),
factor: domain.SecondFactorTypeOTPEmail,
resourceOwner: "org1",
},
res: res{
want: &domain.ObjectDetails{
ResourceOwner: "org1",
},
},
},
{
name: "add factor otp sms, ok",
fields: fields{
eventstore: eventstoreExpect(
t,
expectFilter(
eventFromEventPusher(
org.NewLoginPolicySecondFactorAddedEvent(context.Background(),
&org.NewAggregate("org1").Aggregate,
domain.SecondFactorTypeOTPSMS,
),
),
),
expectPush(
[]*repository.Event{
eventFromEventPusher(
org.NewLoginPolicySecondFactorRemovedEvent(context.Background(),
&org.NewAggregate("org1").Aggregate,
domain.SecondFactorTypeOTPSMS),
),
},
),
),
},
args: args{
ctx: context.Background(),
factor: domain.SecondFactorTypeOTPSMS,
resourceOwner: "org1",
},
res: res{

View File

@ -11,6 +11,9 @@ import (
type Phone struct {
Number domain.PhoneNumber
Verified bool
// ReturnCode is used if the Verified field is false
ReturnCode bool
}
func (c *Commands) newPhoneCode(ctx context.Context, filter preparation.FilterToQueryReducer, alg crypto.EncryptionAlgorithm) (*CryptoCode, error) {

View File

@ -155,6 +155,7 @@ func (wm *SessionWriteModel) AuthenticationTime() time.Time {
wm.PasskeyCheckedAt,
wm.IntentCheckedAt,
// TODO: add U2F and OTP check https://github.com/zitadel/zitadel/issues/5477
// TODO: add OTP (sms and email) check https://github.com/zitadel/zitadel/issues/6224
} {
if check.After(authTime) {
authTime = check
@ -178,11 +179,20 @@ func (wm *SessionWriteModel) AuthMethodTypes() []domain.UserAuthMethodType {
// TODO: add checks with https://github.com/zitadel/zitadel/issues/5477
/*
if !wm.TOTPCheckedAt.IsZero() {
types = append(types, domain.UserAuthMethodTypeOTP)
types = append(types, domain.UserAuthMethodTypeTOTP)
}
if !wm.U2FCheckedAt.IsZero() {
types = append(types, domain.UserAuthMethodTypeU2F)
}
*/
// TODO: add checks with https://github.com/zitadel/zitadel/issues/6224
/*
if !wm.TOTPFactor.OTPSMSCheckedAt.IsZero() {
types = append(types, domain.UserAuthMethodTypeOTPSMS)
}
if !wm.TOTPFactor.OTPEmailCheckedAt.IsZero() {
types = append(types, domain.UserAuthMethodTypeOTPEmail)
}
*/
return types
}

View File

@ -66,6 +66,9 @@ type AddHuman struct {
// EmailCode is set by the command
EmailCode *string
// PhoneCode is set by the command
PhoneCode *string
}
type AddLink struct {
@ -258,7 +261,6 @@ func (c *Commands) addHumanCommandEmail(ctx context.Context, filter preparation.
if human.Email.Verified {
cmds = append(cmds, user.NewHumanEmailVerifiedEvent(ctx, &a.Aggregate))
}
// if allowInitMail, used for v1 api (system, admin, mgmt, auth):
// add init code if
// email not verified or
@ -302,7 +304,10 @@ func (c *Commands) addHumanCommandPhone(ctx context.Context, filter preparation.
if err != nil {
return nil, err
}
return append(cmds, user.NewHumanPhoneCodeAddedEvent(ctx, &a.Aggregate, phoneCode.Crypted, phoneCode.Expiry)), nil
if human.Phone.ReturnCode {
human.PhoneCode = &phoneCode.Plain
}
return append(cmds, user.NewHumanPhoneCodeAddedEventV2(ctx, &a.Aggregate, phoneCode.Crypted, phoneCode.Expiry, human.Phone.ReturnCode)), nil
}
func (c *Commands) addHumanCommandCheckID(ctx context.Context, filter preparation.FilterToQueryReducer, human *AddHuman, orgID string) (err error) {

View File

@ -11,12 +11,11 @@ import (
"github.com/zitadel/zitadel/internal/domain"
caos_errs "github.com/zitadel/zitadel/internal/errors"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/eventstore/v1/models"
"github.com/zitadel/zitadel/internal/repository/user"
"github.com/zitadel/zitadel/internal/telemetry/tracing"
)
func (c *Commands) ImportHumanOTP(ctx context.Context, userID, userAgentID, resourceowner string, key string) error {
func (c *Commands) ImportHumanTOTP(ctx context.Context, userID, userAgentID, resourceowner string, key string) error {
encryptedSecret, err := crypto.Encrypt([]byte(key), c.multifactors.OTP.CryptoMFA)
if err != nil {
return err
@ -25,7 +24,7 @@ func (c *Commands) ImportHumanOTP(ctx context.Context, userID, userAgentID, reso
return err
}
otpWriteModel, err := c.otpWriteModelByID(ctx, userID, resourceowner)
otpWriteModel, err := c.totpWriteModelByID(ctx, userID, resourceowner)
if err != nil {
return err
}
@ -41,7 +40,7 @@ func (c *Commands) ImportHumanOTP(ctx context.Context, userID, userAgentID, reso
return err
}
func (c *Commands) AddHumanOTP(ctx context.Context, userID, resourceowner string) (*domain.OTP, error) {
func (c *Commands) AddHumanTOTP(ctx context.Context, userID, resourceowner string) (*domain.TOTP, error) {
if userID == "" {
return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-5M0sd", "Errors.User.UserIDMissing")
}
@ -49,21 +48,19 @@ func (c *Commands) AddHumanOTP(ctx context.Context, userID, resourceowner string
if err != nil {
return nil, err
}
_, err = c.eventstore.Push(ctx, prep.cmds...)
err = c.pushAppendAndReduce(ctx, prep.wm, prep.cmds...)
if err != nil {
return nil, err
}
return &domain.OTP{
ObjectRoot: models.ObjectRoot{
AggregateID: prep.userAgg.ID,
},
SecretString: prep.key.Secret(),
Url: prep.key.URL(),
return &domain.TOTP{
ObjectDetails: writeModelToObjectDetails(&prep.wm.WriteModel),
Secret: prep.key.Secret(),
URI: prep.key.URL(),
}, nil
}
type preparedTOTP struct {
wm *HumanOTPWriteModel
wm *HumanTOTPWriteModel
userAgg *eventstore.Aggregate
key *otp.Key
cmds []eventstore.Command
@ -72,21 +69,21 @@ type preparedTOTP struct {
func (c *Commands) createHumanTOTP(ctx context.Context, userID, resourceOwner string) (*preparedTOTP, error) {
human, err := c.getHuman(ctx, userID, resourceOwner)
if err != nil {
logging.Log("COMMAND-DAqe1").WithError(err).WithField("traceID", tracing.TraceIDFromCtx(ctx)).Debug("unable to get human for loginname")
logging.WithError(err).WithField("traceID", tracing.TraceIDFromCtx(ctx)).Debug("unable to get human for loginname")
return nil, caos_errs.ThrowPreconditionFailed(err, "COMMAND-MM9fs", "Errors.User.NotFound")
}
org, err := c.getOrg(ctx, human.ResourceOwner)
if err != nil {
logging.Log("COMMAND-Cm0ds").WithError(err).WithField("traceID", tracing.TraceIDFromCtx(ctx)).Debug("unable to get org for loginname")
logging.WithError(err).WithField("traceID", tracing.TraceIDFromCtx(ctx)).Debug("unable to get org for loginname")
return nil, caos_errs.ThrowPreconditionFailed(err, "COMMAND-55M9f", "Errors.Org.NotFound")
}
orgPolicy, err := c.getOrgDomainPolicy(ctx, org.AggregateID)
if err != nil {
logging.Log("COMMAND-y5zv9").WithError(err).WithField("traceID", tracing.TraceIDFromCtx(ctx)).Debug("unable to get org policy for loginname")
logging.WithError(err).WithField("traceID", tracing.TraceIDFromCtx(ctx)).Debug("unable to get org policy for loginname")
return nil, caos_errs.ThrowPreconditionFailed(err, "COMMAND-8ugTs", "Errors.Org.DomainPolicy.NotFound")
}
otpWriteModel, err := c.otpWriteModelByID(ctx, userID, resourceOwner)
otpWriteModel, err := c.totpWriteModelByID(ctx, userID, resourceOwner)
if err != nil {
return nil, err
}
@ -103,7 +100,7 @@ func (c *Commands) createHumanTOTP(ctx context.Context, userID, resourceOwner st
if issuer == "" {
issuer = authz.GetInstance(ctx).RequestedDomain()
}
key, secret, err := domain.NewOTPKey(issuer, accountName, c.multifactors.OTP.CryptoMFA)
key, secret, err := domain.NewTOTPKey(issuer, accountName, c.multifactors.OTP.CryptoMFA)
if err != nil {
return nil, err
}
@ -117,12 +114,12 @@ func (c *Commands) createHumanTOTP(ctx context.Context, userID, resourceOwner st
}, nil
}
func (c *Commands) HumanCheckMFAOTPSetup(ctx context.Context, userID, code, userAgentID, resourceowner string) (*domain.ObjectDetails, error) {
func (c *Commands) HumanCheckMFATOTPSetup(ctx context.Context, userID, code, userAgentID, resourceowner string) (*domain.ObjectDetails, error) {
if userID == "" {
return nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-8N9ds", "Errors.User.UserIDMissing")
}
existingOTP, err := c.otpWriteModelByID(ctx, userID, resourceowner)
existingOTP, err := c.totpWriteModelByID(ctx, userID, resourceowner)
if err != nil {
return nil, err
}
@ -132,7 +129,7 @@ func (c *Commands) HumanCheckMFAOTPSetup(ctx context.Context, userID, code, user
if existingOTP.State == domain.MFAStateReady {
return nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-qx4ls", "Errors.Users.MFA.OTP.AlreadyReady")
}
if err := domain.VerifyMFAOTP(code, existingOTP.Secret, c.multifactors.OTP.CryptoMFA); err != nil {
if err := domain.VerifyTOTP(code, existingOTP.Secret, c.multifactors.OTP.CryptoMFA); err != nil {
return nil, err
}
userAgg := UserAggregateFromWriteModel(&existingOTP.WriteModel)
@ -148,11 +145,11 @@ func (c *Commands) HumanCheckMFAOTPSetup(ctx context.Context, userID, code, user
return writeModelToObjectDetails(&existingOTP.WriteModel), nil
}
func (c *Commands) HumanCheckMFAOTP(ctx context.Context, userID, code, resourceowner string, authRequest *domain.AuthRequest) error {
func (c *Commands) HumanCheckMFATOTP(ctx context.Context, userID, code, resourceowner string, authRequest *domain.AuthRequest) error {
if userID == "" {
return caos_errs.ThrowPreconditionFailed(nil, "COMMAND-8N9ds", "Errors.User.UserIDMissing")
}
existingOTP, err := c.otpWriteModelByID(ctx, userID, resourceowner)
existingOTP, err := c.totpWriteModelByID(ctx, userID, resourceowner)
if err != nil {
return err
}
@ -160,22 +157,22 @@ func (c *Commands) HumanCheckMFAOTP(ctx context.Context, userID, code, resourceo
return caos_errs.ThrowPreconditionFailed(nil, "COMMAND-3Mif9s", "Errors.User.MFA.OTP.NotReady")
}
userAgg := UserAggregateFromWriteModel(&existingOTP.WriteModel)
err = domain.VerifyMFAOTP(code, existingOTP.Secret, c.multifactors.OTP.CryptoMFA)
err = domain.VerifyTOTP(code, existingOTP.Secret, c.multifactors.OTP.CryptoMFA)
if err == nil {
_, err = c.eventstore.Push(ctx, user.NewHumanOTPCheckSucceededEvent(ctx, userAgg, authRequestDomainToAuthRequestInfo(authRequest)))
return err
}
_, pushErr := c.eventstore.Push(ctx, user.NewHumanOTPCheckFailedEvent(ctx, userAgg, authRequestDomainToAuthRequestInfo(authRequest)))
logging.Log("COMMAND-9fj7s").OnError(pushErr).Error("error create password check failed event")
logging.OnError(pushErr).Error("error create password check failed event")
return err
}
func (c *Commands) HumanRemoveOTP(ctx context.Context, userID, resourceOwner string) (*domain.ObjectDetails, error) {
func (c *Commands) HumanRemoveTOTP(ctx context.Context, userID, resourceOwner string) (*domain.ObjectDetails, error) {
if userID == "" {
return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-5M0sd", "Errors.User.UserIDMissing")
}
existingOTP, err := c.otpWriteModelByID(ctx, userID, resourceOwner)
existingOTP, err := c.totpWriteModelByID(ctx, userID, resourceOwner)
if err != nil {
return nil, err
}
@ -194,11 +191,128 @@ func (c *Commands) HumanRemoveOTP(ctx context.Context, userID, resourceOwner str
return writeModelToObjectDetails(&existingOTP.WriteModel), nil
}
func (c *Commands) otpWriteModelByID(ctx context.Context, userID, resourceOwner string) (writeModel *HumanOTPWriteModel, err error) {
func (c *Commands) AddHumanOTPSMS(ctx context.Context, userID, resourceOwner string) (*domain.ObjectDetails, error) {
if userID == "" {
return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-QSF2s", "Errors.User.UserIDMissing")
}
if err := authz.UserIDInCTX(ctx, userID); err != nil {
return nil, err
}
otpWriteModel, err := c.otpSMSWriteModelByID(ctx, userID, resourceOwner)
if err != nil {
return nil, err
}
if otpWriteModel.otpAdded {
return nil, caos_errs.ThrowAlreadyExists(nil, "COMMAND-Ad3g2", "Errors.User.MFA.OTP.AlreadyReady")
}
if !otpWriteModel.phoneVerified {
return nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-Q54j2", "Errors.User.MFA.OTP.NotReady")
}
userAgg := UserAggregateFromWriteModel(&otpWriteModel.WriteModel)
if err = c.pushAppendAndReduce(ctx, otpWriteModel, user.NewHumanOTPSMSAddedEvent(ctx, userAgg)); err != nil {
return nil, err
}
return writeModelToObjectDetails(&otpWriteModel.WriteModel), nil
}
func (c *Commands) RemoveHumanOTPSMS(ctx context.Context, userID, resourceOwner string) (*domain.ObjectDetails, error) {
if userID == "" {
return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-S3br2", "Errors.User.UserIDMissing")
}
existingOTP, err := c.otpSMSWriteModelByID(ctx, userID, resourceOwner)
if err != nil {
return nil, err
}
if userID != authz.GetCtxData(ctx).UserID {
if err := c.checkPermission(ctx, domain.PermissionUserWrite, existingOTP.ResourceOwner, userID); err != nil {
return nil, err
}
}
if !existingOTP.otpAdded {
return nil, caos_errs.ThrowNotFound(nil, "COMMAND-Sr3h3", "Errors.User.MFA.OTP.NotExisting")
}
userAgg := UserAggregateFromWriteModel(&existingOTP.WriteModel)
if err = c.pushAppendAndReduce(ctx, existingOTP, user.NewHumanOTPSMSRemovedEvent(ctx, userAgg)); err != nil {
return nil, err
}
return writeModelToObjectDetails(&existingOTP.WriteModel), nil
}
func (c *Commands) AddHumanOTPEmail(ctx context.Context, userID, resourceOwner string) (*domain.ObjectDetails, error) {
if userID == "" {
return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-Sg1hz", "Errors.User.UserIDMissing")
}
otpWriteModel, err := c.otpEmailWriteModelByID(ctx, userID, resourceOwner)
if err != nil {
return nil, err
}
if otpWriteModel.otpAdded {
return nil, caos_errs.ThrowAlreadyExists(nil, "COMMAND-MKL2s", "Errors.User.MFA.OTP.AlreadyReady")
}
if !otpWriteModel.emailVerified {
return nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-KLJ2d", "Errors.User.MFA.OTP.NotReady")
}
userAgg := UserAggregateFromWriteModel(&otpWriteModel.WriteModel)
if err = c.pushAppendAndReduce(ctx, otpWriteModel, user.NewHumanOTPEmailAddedEvent(ctx, userAgg)); err != nil {
return nil, err
}
return writeModelToObjectDetails(&otpWriteModel.WriteModel), nil
}
func (c *Commands) RemoveHumanOTPEmail(ctx context.Context, userID, resourceOwner string) (*domain.ObjectDetails, error) {
if userID == "" {
return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-S2h11", "Errors.User.UserIDMissing")
}
existingOTP, err := c.otpEmailWriteModelByID(ctx, userID, resourceOwner)
if err != nil {
return nil, err
}
if userID != authz.GetCtxData(ctx).UserID {
if err := c.checkPermission(ctx, domain.PermissionUserWrite, existingOTP.ResourceOwner, userID); err != nil {
return nil, err
}
}
if !existingOTP.otpAdded {
return nil, caos_errs.ThrowNotFound(nil, "COMMAND-b312D", "Errors.User.MFA.OTP.NotExisting")
}
userAgg := UserAggregateFromWriteModel(&existingOTP.WriteModel)
if err = c.pushAppendAndReduce(ctx, existingOTP, user.NewHumanOTPEmailRemovedEvent(ctx, userAgg)); err != nil {
return nil, err
}
return writeModelToObjectDetails(&existingOTP.WriteModel), nil
}
func (c *Commands) totpWriteModelByID(ctx context.Context, userID, resourceOwner string) (writeModel *HumanTOTPWriteModel, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
writeModel = NewHumanOTPWriteModel(userID, resourceOwner)
writeModel = NewHumanTOTPWriteModel(userID, resourceOwner)
err = c.eventstore.FilterToQueryReducer(ctx, writeModel)
if err != nil {
return nil, err
}
return writeModel, nil
}
func (c *Commands) otpSMSWriteModelByID(ctx context.Context, userID, resourceOwner string) (writeModel *HumanOTPSMSWriteModel, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
writeModel = NewHumanOTPSMSWriteModel(userID, resourceOwner)
err = c.eventstore.FilterToQueryReducer(ctx, writeModel)
if err != nil {
return nil, err
}
return writeModel, nil
}
func (c *Commands) otpEmailWriteModelByID(ctx context.Context, userID, resourceOwner string) (writeModel *HumanOTPEmailWriteModel, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
writeModel = NewHumanOTPEmailWriteModel(userID, resourceOwner)
err = c.eventstore.FilterToQueryReducer(ctx, writeModel)
if err != nil {
return nil, err

View File

@ -7,15 +7,15 @@ import (
"github.com/zitadel/zitadel/internal/repository/user"
)
type HumanOTPWriteModel struct {
type HumanTOTPWriteModel struct {
eventstore.WriteModel
State domain.MFAState
Secret *crypto.CryptoValue
}
func NewHumanOTPWriteModel(userID, resourceOwner string) *HumanOTPWriteModel {
return &HumanOTPWriteModel{
func NewHumanTOTPWriteModel(userID, resourceOwner string) *HumanTOTPWriteModel {
return &HumanTOTPWriteModel{
WriteModel: eventstore.WriteModel{
AggregateID: userID,
ResourceOwner: resourceOwner,
@ -23,7 +23,7 @@ func NewHumanOTPWriteModel(userID, resourceOwner string) *HumanOTPWriteModel {
}
}
func (wm *HumanOTPWriteModel) Reduce() error {
func (wm *HumanTOTPWriteModel) Reduce() error {
for _, event := range wm.Events {
switch e := event.(type) {
case *user.HumanOTPAddedEvent:
@ -40,7 +40,7 @@ func (wm *HumanOTPWriteModel) Reduce() error {
return wm.WriteModel.Reduce()
}
func (wm *HumanOTPWriteModel) Query() *eventstore.SearchQueryBuilder {
func (wm *HumanTOTPWriteModel) Query() *eventstore.SearchQueryBuilder {
query := eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent).
AddQuery().
AggregateTypes(user.AggregateType).
@ -59,3 +59,107 @@ func (wm *HumanOTPWriteModel) Query() *eventstore.SearchQueryBuilder {
}
return query
}
type HumanOTPSMSWriteModel struct {
eventstore.WriteModel
phoneVerified bool
otpAdded bool
}
func NewHumanOTPSMSWriteModel(userID, resourceOwner string) *HumanOTPSMSWriteModel {
return &HumanOTPSMSWriteModel{
WriteModel: eventstore.WriteModel{
AggregateID: userID,
ResourceOwner: resourceOwner,
},
}
}
func (wm *HumanOTPSMSWriteModel) Reduce() error {
for _, event := range wm.Events {
switch event.(type) {
case *user.HumanPhoneVerifiedEvent:
wm.phoneVerified = true
case *user.HumanOTPSMSAddedEvent:
wm.otpAdded = true
case *user.HumanOTPSMSRemovedEvent:
wm.otpAdded = false
case *user.HumanPhoneRemovedEvent,
*user.UserRemovedEvent:
wm.phoneVerified = false
wm.otpAdded = false
}
}
return wm.WriteModel.Reduce()
}
func (wm *HumanOTPSMSWriteModel) Query() *eventstore.SearchQueryBuilder {
query := eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent).
AddQuery().
AggregateTypes(user.AggregateType).
AggregateIDs(wm.AggregateID).
EventTypes(user.HumanPhoneVerifiedType,
user.HumanOTPSMSAddedType,
user.HumanOTPSMSRemovedType,
user.HumanPhoneRemovedType,
user.UserRemovedType,
).
Builder()
if wm.ResourceOwner != "" {
query.ResourceOwner(wm.ResourceOwner)
}
return query
}
type HumanOTPEmailWriteModel struct {
eventstore.WriteModel
emailVerified bool
otpAdded bool
}
func NewHumanOTPEmailWriteModel(userID, resourceOwner string) *HumanOTPEmailWriteModel {
return &HumanOTPEmailWriteModel{
WriteModel: eventstore.WriteModel{
AggregateID: userID,
ResourceOwner: resourceOwner,
},
}
}
func (wm *HumanOTPEmailWriteModel) Reduce() error {
for _, event := range wm.Events {
switch event.(type) {
case *user.HumanEmailVerifiedEvent:
wm.emailVerified = true
case *user.HumanOTPEmailAddedEvent:
wm.otpAdded = true
case *user.HumanOTPEmailRemovedEvent:
wm.otpAdded = false
case *user.UserRemovedEvent:
wm.emailVerified = false
wm.otpAdded = false
}
}
return wm.WriteModel.Reduce()
}
func (wm *HumanOTPEmailWriteModel) Query() *eventstore.SearchQueryBuilder {
query := eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent).
AddQuery().
AggregateTypes(user.AggregateType).
AggregateIDs(wm.AggregateID).
EventTypes(user.HumanEmailVerifiedType,
user.HumanOTPEmailAddedType,
user.HumanOTPEmailRemovedType,
user.UserRemovedType,
).
Builder()
if wm.ResourceOwner != "" {
query.ResourceOwner(wm.ResourceOwner)
}
return query
}

View File

@ -22,7 +22,7 @@ import (
"github.com/zitadel/zitadel/internal/repository/user"
)
func TestCommandSide_AddHumanOTP(t *testing.T) {
func TestCommandSide_AddHumanTOTP(t *testing.T) {
type fields struct {
eventstore *eventstore.Eventstore
}
@ -223,7 +223,7 @@ func TestCommandSide_AddHumanOTP(t *testing.T) {
r := &Commands{
eventstore: tt.fields.eventstore,
}
got, err := r.AddHumanOTP(tt.args.ctx, tt.args.userID, tt.args.orgID)
got, err := r.AddHumanTOTP(tt.args.ctx, tt.args.userID, tt.args.orgID)
if tt.res.err == nil {
assert.NoError(t, err)
}
@ -237,7 +237,7 @@ func TestCommandSide_AddHumanOTP(t *testing.T) {
}
}
func TestCommands_createHumanOTP(t *testing.T) {
func TestCommands_createHumanTOTP(t *testing.T) {
type fields struct {
eventstore *eventstore.Eventstore
}
@ -527,11 +527,11 @@ func TestCommands_createHumanOTP(t *testing.T) {
}
}
func TestCommands_HumanCheckMFAOTPSetup(t *testing.T) {
func TestCommands_HumanCheckMFATOTPSetup(t *testing.T) {
ctx := authz.NewMockContext("", "org1", "user1")
cryptoAlg := crypto.CreateMockEncryptionAlg(gomock.NewController(t))
key, secret, err := domain.NewOTPKey("example.com", "user1", cryptoAlg)
key, secret, err := domain.NewTOTPKey("example.com", "user1", cryptoAlg)
require.NoError(t, err)
userAgg := &user.NewAggregate("user1", "org1").Aggregate
@ -697,7 +697,7 @@ func TestCommands_HumanCheckMFAOTPSetup(t *testing.T) {
},
},
}
got, err := c.HumanCheckMFAOTPSetup(ctx, tt.args.userID, tt.args.code, "agent1", tt.args.resourceOwner)
got, err := c.HumanCheckMFATOTPSetup(ctx, tt.args.userID, tt.args.code, "agent1", tt.args.resourceOwner)
require.ErrorIs(t, err, tt.wantErr)
if tt.want {
require.NotNil(t, got)
@ -707,7 +707,7 @@ func TestCommands_HumanCheckMFAOTPSetup(t *testing.T) {
}
}
func TestCommandSide_RemoveHumanOTP(t *testing.T) {
func TestCommandSide_RemoveHumanTOTP(t *testing.T) {
type fields struct {
eventstore *eventstore.Eventstore
}
@ -802,7 +802,7 @@ func TestCommandSide_RemoveHumanOTP(t *testing.T) {
r := &Commands{
eventstore: tt.fields.eventstore,
}
got, err := r.HumanRemoveOTP(tt.args.ctx, tt.args.userID, tt.args.orgID)
got, err := r.HumanRemoveTOTP(tt.args.ctx, tt.args.userID, tt.args.orgID)
if tt.res.err == nil {
assert.NoError(t, err)
}
@ -815,3 +815,540 @@ func TestCommandSide_RemoveHumanOTP(t *testing.T) {
})
}
}
func TestCommandSide_AddHumanOTPSMS(t *testing.T) {
ctx := authz.NewMockContext("inst1", "org1", "user1")
type fields struct {
eventstore func(*testing.T) *eventstore.Eventstore
}
type (
args struct {
ctx context.Context
userID string
resourceOwner string
}
)
type res struct {
want *domain.ObjectDetails
err error
}
tests := []struct {
name string
fields fields
args args
res res
}{
{
name: "userid missing, invalid argument error",
fields: fields{
eventstore: expectEventstore(),
},
args: args{
ctx: ctx,
userID: "",
resourceOwner: "org1",
},
res: res{
err: caos_errs.ThrowInvalidArgument(nil, "COMMAND-QSF2s", "Errors.User.UserIDMissing"),
},
},
{
name: "wrong user, permission denied error",
fields: fields{
eventstore: expectEventstore(),
},
args: args{
ctx: ctx,
userID: "other",
resourceOwner: "org1",
},
res: res{
err: caos_errs.ThrowPermissionDenied(nil, "AUTH-Bohd2", "Errors.User.UserIDWrong"),
},
},
{
name: "otp sms already exists, already exists error",
fields: fields{
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
user.NewHumanOTPSMSAddedEvent(ctx,
&user.NewAggregate("user1", "org1").Aggregate,
),
),
),
),
},
args: args{
ctx: ctx,
userID: "user1",
resourceOwner: "org1",
},
res: res{
err: caos_errs.ThrowAlreadyExists(nil, "COMMAND-Ad3g2", "Errors.User.MFA.OTP.AlreadyReady"),
},
},
{
name: "phone not verified, precondition failed error",
fields: fields{
eventstore: expectEventstore(
expectFilter(),
),
},
args: args{
ctx: ctx,
userID: "user1",
resourceOwner: "org1",
},
res: res{
err: caos_errs.ThrowPreconditionFailed(nil, "COMMAND-Q54j2", "Errors.User.MFA.OTP.NotReady"),
},
},
{
name: "phone removed, precondition failed error",
fields: fields{
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
user.NewHumanPhoneChangedEvent(ctx,
&user.NewAggregate("user1", "org1").Aggregate,
"+4179654321",
),
),
eventFromEventPusher(
user.NewHumanPhoneVerifiedEvent(ctx,
&user.NewAggregate("user1", "org1").Aggregate,
),
),
eventFromEventPusher(
user.NewHumanPhoneRemovedEvent(ctx,
&user.NewAggregate("user1", "org1").Aggregate,
),
),
),
),
},
args: args{
ctx: ctx,
userID: "user1",
resourceOwner: "org1",
},
res: res{
err: caos_errs.ThrowPreconditionFailed(nil, "COMMAND-Q54j2", "Errors.User.MFA.OTP.NotReady"),
},
},
{
name: "successful add",
fields: fields{
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
user.NewHumanPhoneChangedEvent(ctx,
&user.NewAggregate("user1", "org1").Aggregate,
"+4179654321",
),
),
eventFromEventPusher(
user.NewHumanPhoneVerifiedEvent(ctx,
&user.NewAggregate("user1", "org1").Aggregate,
),
),
),
expectPush(
[]*repository.Event{
eventFromEventPusherWithInstanceID("inst1",
user.NewHumanOTPSMSAddedEvent(ctx,
&user.NewAggregate("user1", "org1").Aggregate,
),
),
},
),
),
},
args: args{
ctx: ctx,
userID: "user1",
resourceOwner: "org1",
},
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),
}
got, err := r.AddHumanOTPSMS(tt.args.ctx, tt.args.userID, tt.args.resourceOwner)
assert.ErrorIs(t, err, tt.res.err)
assert.Equal(t, tt.res.want, got)
})
}
}
func TestCommandSide_RemoveHumanOTPSMS(t *testing.T) {
ctx := authz.NewMockContext("inst1", "org1", "user1")
type fields struct {
eventstore func(*testing.T) *eventstore.Eventstore
checkPermission domain.PermissionCheck
}
type (
args struct {
ctx context.Context
userID string
resourceOwner string
}
)
type res struct {
want *domain.ObjectDetails
err error
}
tests := []struct {
name string
fields fields
args args
res res
}{
{
name: "userid missing, invalid argument error",
fields: fields{
eventstore: expectEventstore(),
},
args: args{
ctx: ctx,
userID: "",
resourceOwner: "org1",
},
res: res{
err: caos_errs.ThrowInvalidArgument(nil, "COMMAND-S3br2", "Errors.User.UserIDMissing"),
},
},
{
name: "other user not permission, permission denied error",
fields: fields{
eventstore: expectEventstore(
expectFilter(),
),
checkPermission: newMockPermissionCheckNotAllowed(),
},
args: args{
ctx: ctx,
userID: "other",
resourceOwner: "org1",
},
res: res{
err: caos_errs.ThrowPermissionDenied(nil, "AUTHZ-HKJD33", "Errors.PermissionDenied"),
},
},
{
name: "otp sms not added, not found error",
fields: fields{
eventstore: expectEventstore(
expectFilter(),
),
checkPermission: newMockPermissionCheckAllowed(),
},
args: args{
ctx: ctx,
userID: "user1",
resourceOwner: "org1",
},
res: res{
err: caos_errs.ThrowNotFound(nil, "COMMAND-Sr3h3", "Errors.User.MFA.OTP.NotExisting"),
},
},
{
name: "successful remove",
fields: fields{
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
user.NewHumanOTPSMSAddedEvent(ctx,
&user.NewAggregate("user1", "org1").Aggregate,
),
),
),
expectPush(
[]*repository.Event{
eventFromEventPusherWithInstanceID("inst1",
user.NewHumanOTPSMSRemovedEvent(ctx,
&user.NewAggregate("user1", "org1").Aggregate,
),
),
},
),
),
checkPermission: newMockPermissionCheckAllowed(),
},
args: args{
ctx: ctx,
userID: "user1",
resourceOwner: "org1",
},
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),
checkPermission: tt.fields.checkPermission,
}
got, err := r.RemoveHumanOTPSMS(tt.args.ctx, tt.args.userID, tt.args.resourceOwner)
assert.ErrorIs(t, err, tt.res.err)
assert.Equal(t, tt.res.want, got)
})
}
}
func TestCommandSide_AddHumanOTPEmail(t *testing.T) {
ctx := authz.NewMockContext("inst1", "org1", "user1")
type fields struct {
eventstore func(*testing.T) *eventstore.Eventstore
}
type (
args struct {
ctx context.Context
userID string
resourceOwner string
}
)
type res struct {
want *domain.ObjectDetails
err error
}
tests := []struct {
name string
fields fields
args args
res res
}{
{
name: "userid missing, invalid argument error",
fields: fields{
eventstore: expectEventstore(),
},
args: args{
ctx: ctx,
userID: "",
resourceOwner: "org1",
},
res: res{
err: caos_errs.ThrowInvalidArgument(nil, "COMMAND-Sg1hz", "Errors.User.UserIDMissing"),
},
},
{
name: "otp email already exists, already exists error",
fields: fields{
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
user.NewHumanOTPEmailAddedEvent(ctx,
&user.NewAggregate("user1", "org1").Aggregate,
),
),
),
),
},
args: args{
ctx: ctx,
userID: "user1",
resourceOwner: "org1",
},
res: res{
err: caos_errs.ThrowAlreadyExists(nil, "COMMAND-MKL2s", "Errors.User.MFA.OTP.AlreadyReady"),
},
},
{
name: "email not verified, precondition failed error",
fields: fields{
eventstore: expectEventstore(
expectFilter(),
),
},
args: args{
ctx: ctx,
userID: "user1",
resourceOwner: "org1",
},
res: res{
err: caos_errs.ThrowPreconditionFailed(nil, "COMMAND-KLJ2d", "Errors.User.MFA.OTP.NotReady"),
},
},
{
name: "successful add",
fields: fields{
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
user.NewHumanEmailChangedEvent(ctx,
&user.NewAggregate("user1", "org1").Aggregate,
"email@test.ch",
),
),
eventFromEventPusher(
user.NewHumanEmailVerifiedEvent(ctx,
&user.NewAggregate("user1", "org1").Aggregate,
),
),
),
expectPush(
[]*repository.Event{
eventFromEventPusherWithInstanceID("inst1",
user.NewHumanOTPEmailAddedEvent(ctx,
&user.NewAggregate("user1", "org1").Aggregate,
),
),
},
),
),
},
args: args{
ctx: ctx,
userID: "user1",
resourceOwner: "org1",
},
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),
}
got, err := r.AddHumanOTPEmail(tt.args.ctx, tt.args.userID, tt.args.resourceOwner)
assert.ErrorIs(t, err, tt.res.err)
assert.Equal(t, tt.res.want, got)
})
}
}
func TestCommandSide_RemoveHumanOTPEmail(t *testing.T) {
ctx := authz.NewMockContext("inst1", "org1", "user1")
type fields struct {
eventstore func(*testing.T) *eventstore.Eventstore
checkPermission domain.PermissionCheck
}
type (
args struct {
ctx context.Context
userID string
resourceOwner string
}
)
type res struct {
want *domain.ObjectDetails
err error
}
tests := []struct {
name string
fields fields
args args
res res
}{
{
name: "userid missing, invalid argument error",
fields: fields{
eventstore: expectEventstore(),
},
args: args{
ctx: ctx,
userID: "",
resourceOwner: "org1",
},
res: res{
err: caos_errs.ThrowInvalidArgument(nil, "COMMAND-S2h11", "Errors.User.UserIDMissing"),
},
},
{
name: "other user not permission, permission denied error",
fields: fields{
eventstore: expectEventstore(
expectFilter(),
),
checkPermission: newMockPermissionCheckNotAllowed(),
},
args: args{
ctx: ctx,
userID: "other",
resourceOwner: "org1",
},
res: res{
err: caos_errs.ThrowPermissionDenied(nil, "AUTHZ-HKJD33", "Errors.PermissionDenied"),
},
},
{
name: "otp email not added, not found error",
fields: fields{
eventstore: expectEventstore(
expectFilter(),
),
checkPermission: newMockPermissionCheckAllowed(),
},
args: args{
ctx: ctx,
userID: "user1",
resourceOwner: "org1",
},
res: res{
err: caos_errs.ThrowNotFound(nil, "COMMAND-b312D", "Errors.User.MFA.OTP.NotExisting"),
},
},
{
name: "successful remove",
fields: fields{
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
user.NewHumanOTPEmailAddedEvent(ctx,
&user.NewAggregate("user1", "org1").Aggregate,
),
),
),
expectPush(
[]*repository.Event{
eventFromEventPusherWithInstanceID("inst1",
user.NewHumanOTPEmailRemovedEvent(ctx,
&user.NewAggregate("user1", "org1").Aggregate,
),
),
},
),
),
checkPermission: newMockPermissionCheckAllowed(),
},
args: args{
ctx: ctx,
userID: "user1",
resourceOwner: "org1",
},
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),
checkPermission: tt.fields.checkPermission,
}
got, err := r.RemoveHumanOTPEmail(tt.args.ctx, tt.args.userID, tt.args.resourceOwner)
assert.ErrorIs(t, err, tt.res.err)
assert.Equal(t, tt.res.want, got)
})
}
}

View File

@ -885,70 +885,70 @@ func TestCommandSide_RemoveHumanPhone(t *testing.T) {
args args
res res
}{
//{
// name: "userid missing, invalid argument error",
// fields: fields{
// eventstore: eventstoreExpect(
// t,
// ),
// },
// args: args{
// ctx: context.Background(),
// resourceOwner: "org1",
// },
// res: res{
// err: caos_errs.IsErrorInvalidArgument,
// },
//},
//{
// name: "user not existing, precondition error",
// fields: fields{
// eventstore: eventstoreExpect(
// t,
// expectFilter(),
// ),
// },
// args: args{
// ctx: context.Background(),
// userID: "user1",
// resourceOwner: "org1",
// },
// res: res{
// err: caos_errs.IsPreconditionFailed,
// },
//},
//{
// name: "phone not existing, precondition error",
// fields: fields{
// eventstore: eventstoreExpect(
// t,
// expectFilter(
// eventFromEventPusher(
// user.NewHumanAddedEvent(context.Background(),
// &user.NewAggregate("user1", "org1").Aggregate,
// "username",
// "firstname",
// "lastname",
// "nickname",
// "displayname",
// language.German,
// domain.GenderUnspecified,
// "email@test.ch",
// true,
// ),
// ),
// ),
// ),
// },
// args: args{
// ctx: context.Background(),
// userID: "user1",
// resourceOwner: "org1",
// },
// res: res{
// err: caos_errs.IsNotFound,
// },
//},
{
name: "userid missing, invalid argument error",
fields: fields{
eventstore: eventstoreExpect(
t,
),
},
args: args{
ctx: context.Background(),
resourceOwner: "org1",
},
res: res{
err: caos_errs.IsErrorInvalidArgument,
},
},
{
name: "user not existing, precondition error",
fields: fields{
eventstore: eventstoreExpect(
t,
expectFilter(),
),
},
args: args{
ctx: context.Background(),
userID: "user1",
resourceOwner: "org1",
},
res: res{
err: caos_errs.IsPreconditionFailed,
},
},
{
name: "phone not existing, precondition error",
fields: fields{
eventstore: eventstoreExpect(
t,
expectFilter(
eventFromEventPusher(
user.NewHumanAddedEvent(context.Background(),
&user.NewAggregate("user1", "org1").Aggregate,
"username",
"firstname",
"lastname",
"nickname",
"displayname",
language.German,
domain.GenderUnspecified,
"email@test.ch",
true,
),
),
),
),
},
args: args{
ctx: context.Background(),
userID: "user1",
resourceOwner: "org1",
},
res: res{
err: caos_errs.IsNotFound,
},
},
{
name: "remove phone, ok",
fields: fields{

View File

@ -1020,6 +1020,92 @@ func TestCommandSide_AddHuman(t *testing.T) {
},
wantID: "user1",
},
}, {
name: "add human (with return code), ok",
fields: fields{
eventstore: expectEventstore(
expectFilter(),
expectFilter(
eventFromEventPusher(
org.NewDomainPolicyAddedEvent(context.Background(),
&user.NewAggregate("user1", "org1").Aggregate,
true,
true,
true,
),
),
),
expectFilter(
eventFromEventPusher(
org.NewPasswordComplexityPolicyAddedEvent(context.Background(),
&user.NewAggregate("user1", "org1").Aggregate,
1,
false,
false,
false,
false,
),
),
),
expectPush(
[]*repository.Event{
eventFromEventPusher(
newAddHumanEvent("$plain$x$password", false, "+41711234567"),
),
eventFromEventPusher(
user.NewHumanEmailVerifiedEvent(context.Background(),
&user.NewAggregate("user1", "org1").Aggregate),
),
eventFromEventPusher(
user.NewHumanPhoneCodeAddedEventV2(
context.Background(),
&userAgg.Aggregate,
&crypto.CryptoValue{
CryptoType: crypto.TypeEncryption,
Algorithm: "enc",
KeyID: "id",
Crypted: []byte("phoneCode"),
},
1*time.Hour,
true,
),
),
},
uniqueConstraintsFromEventConstraint(user.NewAddUsernameUniqueConstraint("username", "org1", true)),
),
),
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"),
userPasswordHasher: mockPasswordHasher("x"),
codeAlg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
newCode: mockCode("phoneCode", time.Hour),
},
args: args{
ctx: context.Background(),
orgID: "org1",
human: &AddHuman{
Username: "username",
Password: "password",
FirstName: "firstname",
LastName: "lastname",
Email: Email{
Address: "email@test.ch",
Verified: true,
},
Phone: Phone{
Number: "+41711234567",
ReturnCode: true,
},
PreferredLanguage: language.English,
},
secretGenerator: GetMockSecretGenerator(t),
allowInitMail: true,
},
res: res{
want: &domain.ObjectDetails{
ResourceOwner: "org1",
},
wantID: "user1",
},
},
{
name: "add human with metadata, ok",

View File

@ -57,7 +57,7 @@ func TestCommands_RegisterUserPasskey(t *testing.T) {
resourceOwner: "org1",
authenticator: domain.AuthenticatorAttachmentCrossPlattform,
},
wantErr: caos_errs.ThrowUnauthenticated(nil, "AUTH-Bohd2", "Errors.User.UserIDWrong"),
wantErr: caos_errs.ThrowPermissionDenied(nil, "AUTH-Bohd2", "Errors.User.UserIDWrong"),
},
{
name: "get human passwordless error",

View File

@ -0,0 +1,200 @@
package command
import (
"context"
"github.com/zitadel/logging"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/domain"
caos_errs "github.com/zitadel/zitadel/internal/errors"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/repository/user"
)
// ChangeUserPhone sets a user's phone number, generates a code
// and triggers a notification sms.
func (c *Commands) ChangeUserPhone(ctx context.Context, userID, resourceOwner, phone string, alg crypto.EncryptionAlgorithm) (*domain.Phone, error) {
return c.changeUserPhoneWithCode(ctx, userID, resourceOwner, phone, alg, false)
}
// ChangeUserPhoneReturnCode sets a user's phone number, generates a code and does not send a notification sms.
// The generated plain text code will be set in the returned Phone object.
func (c *Commands) ChangeUserPhoneReturnCode(ctx context.Context, userID, resourceOwner, phone string, alg crypto.EncryptionAlgorithm) (*domain.Phone, error) {
return c.changeUserPhoneWithCode(ctx, userID, resourceOwner, phone, alg, true)
}
// ChangeUserPhoneVerified sets a user's phone number and marks it is verified.
// No code is generated and no confirmation sms is send.
func (c *Commands) ChangeUserPhoneVerified(ctx context.Context, userID, resourceOwner, phone string) (*domain.Phone, error) {
cmd, err := c.NewUserPhoneEvents(ctx, userID, resourceOwner)
if err != nil {
return nil, err
}
if err = c.checkPermission(ctx, domain.PermissionUserWrite, cmd.aggregate.ResourceOwner, userID); err != nil {
return nil, err
}
if err = cmd.Change(ctx, domain.PhoneNumber(phone)); err != nil {
return nil, err
}
cmd.SetVerified(ctx)
return cmd.Push(ctx)
}
func (c *Commands) changeUserPhoneWithCode(ctx context.Context, userID, resourceOwner, phone string, alg crypto.EncryptionAlgorithm, returnCode bool) (*domain.Phone, error) {
config, err := secretGeneratorConfig(ctx, c.eventstore.Filter, domain.SecretGeneratorTypeVerifyPhoneCode)
if err != nil {
return nil, err
}
gen := crypto.NewEncryptionGenerator(*config, alg)
return c.changeUserPhoneWithGenerator(ctx, userID, resourceOwner, phone, gen, returnCode)
}
// changeUserPhoneWithGenerator set a user's phone number.
// returnCode controls if the plain text version of the code will be set in the return object.
// When the plain text code is returned, no notification sms will be send to the user.
func (c *Commands) changeUserPhoneWithGenerator(ctx context.Context, userID, resourceOwner, phone string, gen crypto.Generator, returnCode bool) (*domain.Phone, error) {
cmd, err := c.NewUserPhoneEvents(ctx, userID, resourceOwner)
if err != nil {
return nil, err
}
if authz.GetCtxData(ctx).UserID != userID {
if err = c.checkPermission(ctx, domain.PermissionUserWrite, cmd.aggregate.ResourceOwner, userID); err != nil {
return nil, err
}
}
if err = cmd.Change(ctx, domain.PhoneNumber(phone)); err != nil {
return nil, err
}
if err = cmd.AddGeneratedCode(ctx, gen, returnCode); err != nil {
return nil, err
}
return cmd.Push(ctx)
}
func (c *Commands) VerifyUserPhone(ctx context.Context, userID, resourceOwner, code string, alg crypto.EncryptionAlgorithm) (*domain.ObjectDetails, error) {
config, err := secretGeneratorConfig(ctx, c.eventstore.Filter, domain.SecretGeneratorTypeVerifyPhoneCode)
if err != nil {
return nil, err
}
gen := crypto.NewEncryptionGenerator(*config, alg)
return c.verifyUserPhoneWithGenerator(ctx, userID, resourceOwner, code, gen)
}
func (c *Commands) verifyUserPhoneWithGenerator(ctx context.Context, userID, resourceOwner, code string, gen crypto.Generator) (*domain.ObjectDetails, error) {
cmd, err := c.NewUserPhoneEvents(ctx, userID, resourceOwner)
if err != nil {
return nil, err
}
err = cmd.VerifyCode(ctx, code, gen)
if err != nil {
return nil, err
}
if _, err = cmd.Push(ctx); err != nil {
return nil, err
}
return writeModelToObjectDetails(&cmd.model.WriteModel), nil
}
// 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
plainCode *string
}
// NewUserPhoneEvents constructs a UserPhoneEvents with a Human Phone Write Model,
// filtered by userID and resourceOwner.
// If a model cannot be found, or it's state is invalid and error is returned.
func (c *Commands) NewUserPhoneEvents(ctx context.Context, userID, resourceOwner string) (*UserPhoneEvents, error) {
if userID == "" {
return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-xP292j", "Errors.User.Phone.IDMissing")
}
model, err := c.phoneWriteModelByID(ctx, userID, resourceOwner)
if err != nil {
return nil, err
}
if model.UserState == domain.UserStateUnspecified || model.UserState == domain.UserStateDeleted {
return nil, caos_errs.ThrowNotFound(nil, "COMMAND-ieJ2e", "Errors.User.Phone.NotFound")
}
if model.UserState == domain.UserStateInitial {
return nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-uz0Uu", "Errors.User.NotInitialised")
}
return &UserPhoneEvents{
eventstore: c.eventstore,
aggregate: UserAggregateFromWriteModel(&model.WriteModel),
model: model,
}, nil
}
// Change sets a new phone number.
// The generated event unsets any previously generated code and verified flag.
func (c *UserPhoneEvents) Change(ctx context.Context, phone domain.PhoneNumber) error {
phone, err := phone.Normalize()
if err != nil {
return err
}
event, hasChanged := c.model.NewChangedEvent(ctx, c.aggregate, phone)
if !hasChanged {
return caos_errs.ThrowPreconditionFailed(nil, "COMMAND-Uch5e", "Errors.User.Phone.NotChanged")
}
c.events = append(c.events, event)
return nil
}
// SetVerified sets the phone number to verified.
func (c *UserPhoneEvents) SetVerified(ctx context.Context) {
c.events = append(c.events, user.NewHumanPhoneVerifiedEvent(ctx, c.aggregate))
}
// 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)
if err != nil {
return err
}
c.events = append(c.events, user.NewHumanPhoneCodeAddedEventV2(ctx, c.aggregate, value, gen.Expiry(), returnCode))
if returnCode {
c.plainCode = &plain
}
return nil
}
func (c *UserPhoneEvents) VerifyCode(ctx context.Context, code string, gen crypto.Generator) error {
if code == "" {
return caos_errs.ThrowInvalidArgument(nil, "COMMAND-Fia4a", "Errors.User.Code.Empty")
}
err := crypto.VerifyCode(c.model.CodeCreationDate, c.model.CodeExpiry, c.model.Code, code, gen)
if err == nil {
c.events = append(c.events, user.NewHumanPhoneVerifiedEvent(ctx, c.aggregate))
return nil
}
_, err = c.eventstore.Push(ctx, user.NewHumanPhoneVerificationFailedEvent(ctx, c.aggregate))
logging.WithFields("id", "COMMAND-Zoo6b", "userID", c.aggregate.ID).OnError(err).Error("NewHumanPhoneVerificationFailedEvent push failed")
return caos_errs.ThrowInvalidArgument(err, "COMMAND-eis9R", "Errors.User.Code.Invalid")
}
// Push all events to the eventstore and Reduce them into the Model.
func (c *UserPhoneEvents) Push(ctx context.Context) (*domain.Phone, error) {
pushedEvents, err := c.eventstore.Push(ctx, c.events...)
if err != nil {
return nil, err
}
err = AppendAndReduce(c.model, pushedEvents...)
if err != nil {
return nil, err
}
phone := writeModelToPhone(c.model)
phone.PlainCode = c.plainCode
return phone, nil
}

View File

@ -0,0 +1,759 @@
package command
import (
"context"
"testing"
"time"
"github.com/golang/mock/gomock"
"github.com/muhlemmer/gu"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/text/language"
"github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/domain"
caos_errs "github.com/zitadel/zitadel/internal/errors"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/eventstore/repository"
"github.com/zitadel/zitadel/internal/eventstore/v1/models"
"github.com/zitadel/zitadel/internal/repository/instance"
"github.com/zitadel/zitadel/internal/repository/user"
)
func TestCommands_ChangeUserPhone(t *testing.T) {
type fields struct {
eventstore *eventstore.Eventstore
checkPermission domain.PermissionCheck
}
type args struct {
userID string
resourceOwner string
phone string
}
tests := []struct {
name string
fields fields
args args
wantErr error
}{
{
name: "missing permission",
fields: fields{
eventstore: eventstoreExpect(
t,
expectFilter(
eventFromEventPusher(
instance.NewSecretGeneratorAddedEvent(context.Background(),
&instance.NewAggregate("inst1").Aggregate,
domain.SecretGeneratorTypeVerifyPhoneCode,
12, time.Minute, true, true, true, true,
),
),
),
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
}(),
),
),
),
checkPermission: newMockPermissionCheckNotAllowed(),
},
args: args{
userID: "user1",
resourceOwner: "org1",
phone: "",
},
wantErr: caos_errs.ThrowPermissionDenied(nil, "AUTHZ-HKJD33", "Errors.PermissionDenied"),
},
{
name: "missing phone",
fields: fields{
eventstore: eventstoreExpect(
t,
expectFilter(
eventFromEventPusher(
instance.NewSecretGeneratorAddedEvent(context.Background(),
&instance.NewAggregate("inst1").Aggregate,
domain.SecretGeneratorTypeVerifyPhoneCode,
12, time.Minute, true, true, true, true,
),
),
),
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
}(),
),
),
),
checkPermission: newMockPermissionCheckAllowed(),
},
args: args{
userID: "user1",
resourceOwner: "org1",
phone: "",
},
wantErr: caos_errs.ThrowInvalidArgument(nil, "PHONE-Zt0NV", "Errors.User.Phone.Empty"),
},
{
name: "not changed",
fields: fields{
eventstore: eventstoreExpect(
t,
expectFilter(
eventFromEventPusher(
instance.NewSecretGeneratorAddedEvent(context.Background(),
&instance.NewAggregate("inst1").Aggregate,
domain.SecretGeneratorTypeVerifyPhoneCode,
12, time.Minute, true, true, true, true,
),
),
),
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
}(),
),
),
),
checkPermission: newMockPermissionCheckAllowed(),
},
args: args{
userID: "user1",
resourceOwner: "org1",
phone: "+41791234567",
},
wantErr: caos_errs.ThrowPreconditionFailed(nil, "COMMAND-Uch5e", "Errors.User.Phone.NotChanged"),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := &Commands{
eventstore: tt.fields.eventstore,
checkPermission: tt.fields.checkPermission,
}
_, err := c.ChangeUserPhone(context.Background(), tt.args.userID, tt.args.resourceOwner, tt.args.phone, crypto.CreateMockEncryptionAlg(gomock.NewController(t)))
require.ErrorIs(t, err, tt.wantErr)
// successful cases are tested in TestCommands_changeUserPhoneWithGenerator
})
}
}
func TestCommands_ChangeUserPhoneReturnCode(t *testing.T) {
type fields struct {
eventstore *eventstore.Eventstore
checkPermission domain.PermissionCheck
}
type args struct {
userID string
resourceOwner string
phone string
}
tests := []struct {
name string
fields fields
args args
wantErr error
}{
{
name: "missing permission",
fields: fields{
eventstore: eventstoreExpect(
t,
expectFilter(
eventFromEventPusher(
instance.NewSecretGeneratorAddedEvent(context.Background(),
&instance.NewAggregate("inst1").Aggregate,
domain.SecretGeneratorTypeVerifyPhoneCode,
12, time.Minute, true, true, true, true,
),
),
),
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
}(),
),
),
),
checkPermission: newMockPermissionCheckNotAllowed(),
},
args: args{
userID: "user1",
resourceOwner: "org1",
phone: "+41791234567",
},
wantErr: caos_errs.ThrowPermissionDenied(nil, "AUTHZ-HKJD33", "Errors.PermissionDenied"),
},
{
name: "missing phone",
fields: fields{
eventstore: eventstoreExpect(
t,
expectFilter(
eventFromEventPusher(
instance.NewSecretGeneratorAddedEvent(context.Background(),
&instance.NewAggregate("inst1").Aggregate,
domain.SecretGeneratorTypeVerifyEmailCode,
12, time.Minute, true, true, true, true,
),
),
),
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
}(),
),
),
),
checkPermission: newMockPermissionCheckAllowed(),
},
args: args{
userID: "user1",
resourceOwner: "org1",
phone: "",
},
wantErr: caos_errs.ThrowInvalidArgument(nil, "PHONE-Zt0NV", "Errors.User.Phone.Empty"),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := &Commands{
eventstore: tt.fields.eventstore,
checkPermission: tt.fields.checkPermission,
}
_, err := c.ChangeUserPhoneReturnCode(context.Background(), tt.args.userID, tt.args.resourceOwner, tt.args.phone, crypto.CreateMockEncryptionAlg(gomock.NewController(t)))
require.ErrorIs(t, err, tt.wantErr)
// successful cases are tested in TestCommands_changeUserPhoneWithGenerator
})
}
}
func TestCommands_ChangeUserPhoneVerified(t *testing.T) {
type fields struct {
eventstore *eventstore.Eventstore
checkPermission domain.PermissionCheck
}
type args struct {
userID string
resourceOwner string
phone string
}
tests := []struct {
name string
fields fields
args args
want *domain.Phone
wantErr error
}{
{
name: "missing userID",
fields: fields{
eventstore: eventstoreExpect(t),
checkPermission: newMockPermissionCheckNotAllowed(),
},
args: args{
userID: "",
resourceOwner: "org1",
phone: "+41791234567",
},
wantErr: caos_errs.ThrowInvalidArgument(nil, "COMMAND-xP292j", "Errors.User.Phone.IDMissing"),
},
{
name: "missing permission",
fields: fields{
eventstore: eventstoreExpect(
t,
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
}(),
),
),
),
checkPermission: newMockPermissionCheckNotAllowed(),
},
args: args{
userID: "user1",
resourceOwner: "org1",
phone: "+41791234567",
},
wantErr: caos_errs.ThrowPermissionDenied(nil, "AUTHZ-HKJD33", "Errors.PermissionDenied"),
},
{
name: "missing phone",
fields: fields{
eventstore: eventstoreExpect(
t,
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
}(),
),
),
),
checkPermission: newMockPermissionCheckAllowed(),
},
args: args{
userID: "user1",
resourceOwner: "org1",
phone: "",
},
wantErr: caos_errs.ThrowInvalidArgument(nil, "PHONE-Zt0NV", "Errors.User.Phone.Empty"),
},
{
name: "phone changed",
fields: fields{
eventstore: eventstoreExpect(
t,
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
}(),
),
),
expectPush(
[]*repository.Event{
eventFromEventPusher(
user.NewHumanPhoneChangedEvent(context.Background(),
&user.NewAggregate("user1", "org1").Aggregate,
"+41791234568",
),
),
eventFromEventPusher(
user.NewHumanPhoneVerifiedEvent(context.Background(),
&user.NewAggregate("user1", "org1").Aggregate,
),
),
},
),
),
checkPermission: newMockPermissionCheckAllowed(),
},
args: args{
userID: "user1",
resourceOwner: "org1",
phone: "+41791234568",
},
want: &domain.Phone{
ObjectRoot: models.ObjectRoot{
AggregateID: "user1",
ResourceOwner: "org1",
},
PhoneNumber: domain.PhoneNumber("+41791234568"),
IsPhoneVerified: true,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := &Commands{
eventstore: tt.fields.eventstore,
checkPermission: tt.fields.checkPermission,
}
got, err := c.ChangeUserPhoneVerified(context.Background(), tt.args.userID, tt.args.resourceOwner, tt.args.phone)
require.ErrorIs(t, err, tt.wantErr)
assert.Equal(t, got, tt.want)
})
}
}
func TestCommands_changeUserPhoneWithGenerator(t *testing.T) {
type fields struct {
eventstore *eventstore.Eventstore
checkPermission domain.PermissionCheck
}
type args struct {
userID string
resourceOwner string
phone string
returnCode bool
}
tests := []struct {
name string
fields fields
args args
want *domain.Phone
wantErr error
}{
{
name: "missing user",
fields: fields{
eventstore: eventstoreExpect(t),
},
args: args{
userID: "",
resourceOwner: "org1",
phone: "+41791234567",
returnCode: false,
},
wantErr: caos_errs.ThrowInvalidArgument(nil, "COMMAND-xP292j", "Errors.User.Phone.IDMissing"),
},
{
name: "missing permission",
fields: fields{
eventstore: eventstoreExpect(
t,
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
}(),
),
),
),
checkPermission: newMockPermissionCheckNotAllowed(),
},
args: args{
userID: "user1",
resourceOwner: "org1",
phone: "+41791234567",
returnCode: false,
},
wantErr: caos_errs.ThrowPermissionDenied(nil, "AUTHZ-HKJD33", "Errors.PermissionDenied"),
},
{
name: "missing phone",
fields: fields{
eventstore: eventstoreExpect(
t,
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
}(),
),
),
),
checkPermission: newMockPermissionCheckAllowed(),
},
args: args{
userID: "user1",
resourceOwner: "org1",
phone: "",
returnCode: false,
},
wantErr: caos_errs.ThrowInvalidArgument(nil, "PHONE-Zt0NV", "Errors.User.Phone.Empty"),
},
{
name: "not changed",
fields: fields{
eventstore: eventstoreExpect(
t,
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
}(),
),
),
),
checkPermission: newMockPermissionCheckAllowed(),
},
args: args{
userID: "user1",
resourceOwner: "org1",
phone: "+41791234567",
returnCode: false,
},
wantErr: caos_errs.ThrowPreconditionFailed(nil, "COMMAND-Uch5e", "Errors.User.Phone.NotChanged"),
},
{
name: "phone changed",
fields: fields{
eventstore: eventstoreExpect(
t,
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
}(),
),
),
expectPush(
[]*repository.Event{
eventFromEventPusher(
user.NewHumanPhoneChangedEvent(context.Background(),
&user.NewAggregate("user1", "org1").Aggregate,
"+41791234568",
),
),
eventFromEventPusher(
user.NewHumanPhoneCodeAddedEventV2(context.Background(),
&user.NewAggregate("user1", "org1").Aggregate,
&crypto.CryptoValue{
CryptoType: crypto.TypeEncryption,
Algorithm: "enc",
KeyID: "id",
Crypted: []byte("a"),
},
time.Hour*1,
false,
),
),
},
),
),
checkPermission: newMockPermissionCheckAllowed(),
},
args: args{
userID: "user1",
resourceOwner: "org1",
phone: "+41791234568",
returnCode: false,
},
want: &domain.Phone{
ObjectRoot: models.ObjectRoot{
AggregateID: "user1",
ResourceOwner: "org1",
},
PhoneNumber: "+41791234568",
IsPhoneVerified: false,
},
},
{
name: "phone changed, return code",
fields: fields{
eventstore: eventstoreExpect(
t,
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
}(),
),
),
expectPush(
[]*repository.Event{
eventFromEventPusher(
user.NewHumanPhoneChangedEvent(context.Background(),
&user.NewAggregate("user1", "org1").Aggregate,
"+41791234568",
),
),
eventFromEventPusher(
user.NewHumanPhoneCodeAddedEventV2(context.Background(),
&user.NewAggregate("user1", "org1").Aggregate,
&crypto.CryptoValue{
CryptoType: crypto.TypeEncryption,
Algorithm: "enc",
KeyID: "id",
Crypted: []byte("a"),
},
time.Hour*1,
true,
),
),
},
),
),
checkPermission: newMockPermissionCheckAllowed(),
},
args: args{
userID: "user1",
resourceOwner: "org1",
phone: "+41791234568",
returnCode: true,
},
want: &domain.Phone{
ObjectRoot: models.ObjectRoot{
AggregateID: "user1",
ResourceOwner: "org1",
},
PhoneNumber: "+41791234568",
IsPhoneVerified: false,
PlainCode: gu.Ptr("a"),
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := &Commands{
eventstore: tt.fields.eventstore,
checkPermission: tt.fields.checkPermission,
}
got, err := c.changeUserPhoneWithGenerator(context.Background(), tt.args.userID, tt.args.resourceOwner, tt.args.phone, GetMockSecretGenerator(t), tt.args.returnCode)
require.ErrorIs(t, err, tt.wantErr)
assert.Equal(t, got, tt.want)
})
}
}

View File

@ -29,5 +29,5 @@ func (c *Commands) CheckUserTOTP(ctx context.Context, userID, code, resourceOwne
if err := authz.UserIDInCTX(ctx, userID); err != nil {
return nil, err
}
return c.HumanCheckMFAOTPSetup(ctx, userID, code, "", resourceOwner)
return c.HumanCheckMFATOTPSetup(ctx, userID, code, "", resourceOwner)
}

View File

@ -45,7 +45,7 @@ func TestCommands_AddUserTOTP(t *testing.T) {
userID: "foo",
resourceowner: "org1",
},
wantErr: caos_errs.ThrowUnauthenticated(nil, "AUTH-Bohd2", "Errors.User.UserIDWrong"),
wantErr: caos_errs.ThrowPermissionDenied(nil, "AUTH-Bohd2", "Errors.User.UserIDWrong"),
},
{
name: "create otp error",
@ -191,7 +191,7 @@ func TestCommands_CheckUserTOTP(t *testing.T) {
ctx := authz.NewMockContext("", "org1", "user1")
cryptoAlg := crypto.CreateMockEncryptionAlg(gomock.NewController(t))
key, secret, err := domain.NewOTPKey("example.com", "user1", cryptoAlg)
key, secret, err := domain.NewTOTPKey("example.com", "user1", cryptoAlg)
require.NoError(t, err)
userAgg := &user.NewAggregate("user1", "org1").Aggregate
@ -218,7 +218,7 @@ func TestCommands_CheckUserTOTP(t *testing.T) {
args: args{
userID: "foo",
},
wantErr: caos_errs.ThrowUnauthenticated(nil, "AUTH-Bohd2", "Errors.User.UserIDWrong"),
wantErr: caos_errs.ThrowPermissionDenied(nil, "AUTH-Bohd2", "Errors.User.UserIDWrong"),
},
{
name: "success",

View File

@ -52,7 +52,7 @@ func TestCommands_RegisterUserU2F(t *testing.T) {
userID: "foo",
resourceOwner: "org1",
},
wantErr: caos_errs.ThrowUnauthenticated(nil, "AUTH-Bohd2", "Errors.User.UserIDWrong"),
wantErr: caos_errs.ThrowPermissionDenied(nil, "AUTH-Bohd2", "Errors.User.UserIDWrong"),
},
{
name: "get human passwordless error",

View File

@ -9,6 +9,7 @@ import (
"github.com/zitadel/passwap/argon2"
"github.com/zitadel/passwap/bcrypt"
"github.com/zitadel/passwap/md5"
"github.com/zitadel/passwap/pbkdf2"
"github.com/zitadel/passwap/scrypt"
"github.com/zitadel/passwap/verifier"
@ -38,6 +39,19 @@ const (
HashNameBcrypt HashName = "bcrypt" // hash and verify
HashNameMd5 HashName = "md5" // verify only, as hashing with md5 is insecure and deprecated
HashNameScrypt HashName = "scrypt" // hash and verify
HashNamePBKDF2 HashName = "pbkdf2" // hash and verify
)
type HashMode string
// HashMode defines a underlying [hash.Hash] implementation
// for algorithms like pbkdf2
const (
HashModeSHA1 HashMode = "sha1"
HashModeSHA224 HashMode = "sha224"
HashModeSHA256 HashMode = "sha256"
HashModeSHA384 HashMode = "sha384"
HashModeSHA512 HashMode = "sha512"
)
type PasswordHashConfig struct {
@ -85,6 +99,10 @@ var knowVerifiers = map[HashName]prefixVerifier{
prefixes: []string{scrypt.Prefix, scrypt.Prefix_Linux},
verifier: scrypt.Verifier,
},
HashNamePBKDF2: {
prefixes: []string{pbkdf2.Prefix},
verifier: pbkdf2.Verifier,
},
}
func (c *PasswordHashConfig) buildVerifiers() (verifiers []verifier.Verifier, prefixes []string, err error) {
@ -116,6 +134,8 @@ func (c *HasherConfig) buildHasher() (hasher passwap.Hasher, prefixes []string,
return c.bcrypt()
case HashNameScrypt:
return c.scrypt()
case HashNamePBKDF2:
return c.pbkdf2()
case "":
return nil, nil, fmt.Errorf("missing hasher algorithm")
case HashNameArgon2, HashNameMd5:
@ -207,3 +227,49 @@ func (c *HasherConfig) scrypt() (passwap.Hasher, []string, error) {
}
return scrypt.New(p), []string{scrypt.Prefix, scrypt.Prefix_Linux}, nil
}
func (c *HasherConfig) pbkdf2Params() (p pbkdf2.Params, _ HashMode, _ error) {
var dst = struct {
Rounds uint32 `mapstructure:"Rounds"`
Hash HashMode `mapstructure:"Hash"`
}{}
if err := c.decodeParams(&dst); err != nil {
return p, "", fmt.Errorf("decode pbkdf2 params: %w", err)
}
switch dst.Hash {
case HashModeSHA1:
p = pbkdf2.RecommendedSHA1Params
case HashModeSHA224:
p = pbkdf2.RecommendedSHA224Params
case HashModeSHA256:
p = pbkdf2.RecommendedSHA256Params
case HashModeSHA384:
p = pbkdf2.RecommendedSHA384Params
case HashModeSHA512:
p = pbkdf2.RecommendedSHA512Params
}
p.Rounds = dst.Rounds
return p, dst.Hash, nil
}
func (c *HasherConfig) pbkdf2() (passwap.Hasher, []string, error) {
p, hash, err := c.pbkdf2Params()
if err != nil {
return nil, nil, err
}
prefix := []string{pbkdf2.Prefix}
switch hash {
case HashModeSHA1:
return pbkdf2.NewSHA1(p), prefix, nil
case HashModeSHA224:
return pbkdf2.NewSHA224(p), prefix, nil
case HashModeSHA256:
return pbkdf2.NewSHA256(p), prefix, nil
case HashModeSHA384:
return pbkdf2.NewSHA384(p), prefix, nil
case HashModeSHA512:
return pbkdf2.NewSHA512(p), prefix, nil
default:
return nil, nil, fmt.Errorf("unsuppored pbkdf2 hash mode: %s", hash)
}
}

View File

@ -1,6 +1,9 @@
package crypto
import (
"crypto/sha1"
"crypto/sha256"
"crypto/sha512"
"testing"
"github.com/stretchr/testify/assert"
@ -8,6 +11,7 @@ import (
"github.com/zitadel/passwap/argon2"
"github.com/zitadel/passwap/bcrypt"
"github.com/zitadel/passwap/md5"
"github.com/zitadel/passwap/pbkdf2"
"github.com/zitadel/passwap/scrypt"
)
@ -238,6 +242,101 @@ func TestPasswordHashConfig_PasswordHasher(t *testing.T) {
},
wantPrefixes: []string{scrypt.Prefix, scrypt.Prefix_Linux, argon2.Prefix, bcrypt.Prefix, md5.Prefix},
},
{
name: "pbkdf2, parse error",
fields: fields{
Hasher: HasherConfig{
Algorithm: HashNamePBKDF2,
Params: map[string]any{
"cost": "bar",
},
},
},
wantErr: true,
},
{
name: "pbkdf2, hash mode error",
fields: fields{
Hasher: HasherConfig{
Algorithm: HashNamePBKDF2,
Params: map[string]any{
"Rounds": 12,
"Hash": "foo",
},
},
},
wantErr: true,
},
{
name: "pbkdf2, sha1",
fields: fields{
Hasher: HasherConfig{
Algorithm: HashNamePBKDF2,
Params: map[string]any{
"Rounds": 12,
"Hash": HashModeSHA1,
},
},
Verifiers: []HashName{HashNameArgon2, HashNameBcrypt, HashNameMd5},
},
wantPrefixes: []string{pbkdf2.Prefix, argon2.Prefix, bcrypt.Prefix, md5.Prefix},
},
{
name: "pbkdf2, sha224",
fields: fields{
Hasher: HasherConfig{
Algorithm: HashNamePBKDF2,
Params: map[string]any{
"Rounds": 12,
"Hash": HashModeSHA224,
},
},
Verifiers: []HashName{HashNameArgon2, HashNameBcrypt, HashNameMd5},
},
wantPrefixes: []string{pbkdf2.Prefix, argon2.Prefix, bcrypt.Prefix, md5.Prefix},
},
{
name: "pbkdf2, sha256",
fields: fields{
Hasher: HasherConfig{
Algorithm: HashNamePBKDF2,
Params: map[string]any{
"Rounds": 12,
"Hash": HashModeSHA256,
},
},
Verifiers: []HashName{HashNameArgon2, HashNameBcrypt, HashNameMd5},
},
wantPrefixes: []string{pbkdf2.Prefix, argon2.Prefix, bcrypt.Prefix, md5.Prefix},
},
{
name: "pbkdf2, sha384",
fields: fields{
Hasher: HasherConfig{
Algorithm: HashNamePBKDF2,
Params: map[string]any{
"Rounds": 12,
"Hash": HashModeSHA384,
},
},
Verifiers: []HashName{HashNameArgon2, HashNameBcrypt, HashNameMd5},
},
wantPrefixes: []string{pbkdf2.Prefix, argon2.Prefix, bcrypt.Prefix, md5.Prefix},
},
{
name: "pbkdf2, sha512",
fields: fields{
Hasher: HasherConfig{
Algorithm: HashNamePBKDF2,
Params: map[string]any{
"Rounds": 12,
"Hash": HashModeSHA512,
},
},
Verifiers: []HashName{HashNameArgon2, HashNameBcrypt, HashNameMd5},
},
wantPrefixes: []string{pbkdf2.Prefix, argon2.Prefix, bcrypt.Prefix, md5.Prefix},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
@ -484,3 +583,116 @@ func TestHasherConfig_scryptParams(t *testing.T) {
})
}
}
func TestHasherConfig_pbkdf2Params(t *testing.T) {
type fields struct {
Params map[string]any
}
tests := []struct {
name string
fields fields
wantP pbkdf2.Params
wantHash HashMode
wantErr bool
}{
{
name: "decode error",
fields: fields{
Params: map[string]any{
"foo": "bar",
},
},
wantErr: true,
},
{
name: "sha1",
fields: fields{
Params: map[string]any{
"Rounds": 12,
"Hash": "sha1",
},
},
wantP: pbkdf2.Params{
Rounds: 12,
KeyLen: sha1.Size,
SaltLen: 16,
},
wantHash: HashModeSHA1,
},
{
name: "sha224",
fields: fields{
Params: map[string]any{
"Rounds": 12,
"Hash": "sha224",
},
},
wantP: pbkdf2.Params{
Rounds: 12,
KeyLen: sha256.Size224,
SaltLen: 16,
},
wantHash: HashModeSHA224,
},
{
name: "sha256",
fields: fields{
Params: map[string]any{
"Rounds": 12,
"Hash": "sha256",
},
},
wantP: pbkdf2.Params{
Rounds: 12,
KeyLen: sha256.Size,
SaltLen: 16,
},
wantHash: HashModeSHA256,
},
{
name: "sha384",
fields: fields{
Params: map[string]any{
"Rounds": 12,
"Hash": "sha384",
},
},
wantP: pbkdf2.Params{
Rounds: 12,
KeyLen: sha512.Size384,
SaltLen: 16,
},
wantHash: HashModeSHA384,
},
{
name: "sha512",
fields: fields{
Params: map[string]any{
"Rounds": 12,
"Hash": "sha512",
},
},
wantP: pbkdf2.Params{
Rounds: 12,
KeyLen: sha512.Size,
SaltLen: 16,
},
wantHash: HashModeSHA512,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := &HasherConfig{
Params: tt.fields.Params,
}
gotP, gotHash, err := c.pbkdf2Params()
if tt.wantErr {
assert.Error(t, err)
return
}
require.NoError(t, err)
assert.Equal(t, tt.wantP, gotP)
assert.Equal(t, tt.wantHash, gotHash)
})
}
}

View File

@ -102,7 +102,7 @@ const (
type MFAType int
const (
MFATypeOTP MFAType = iota
MFATypeTOTP MFAType = iota
MFATypeU2F
MFATypeU2FUserVerification
)

View File

@ -4,20 +4,14 @@ type SecondFactorType int32
const (
SecondFactorTypeUnspecified SecondFactorType = iota
SecondFactorTypeOTP
SecondFactorTypeTOTP
SecondFactorTypeU2F
SecondFactorTypeOTPEmail
SecondFactorTypeOTPSMS
secondFactorCount
)
func SecondFactorTypes() []SecondFactorType {
types := make([]SecondFactorType, 0, secondFactorCount-1)
for i := SecondFactorTypeUnspecified + 1; i < secondFactorCount; i++ {
types = append(types, i)
}
return types
}
type MultiFactorType int32
const (
@ -27,14 +21,6 @@ const (
multiFactorCount
)
func MultiFactorTypes() []MultiFactorType {
types := make([]MultiFactorType, 0, multiFactorCount-1)
for i := MultiFactorTypeUnspecified + 1; i < multiFactorCount; i++ {
types = append(types, i)
}
return types
}
type FactorState int32
const (

View File

@ -3,20 +3,11 @@ package domain
import (
"github.com/pquerna/otp"
"github.com/pquerna/otp/totp"
"github.com/zitadel/zitadel/internal/crypto"
caos_errs "github.com/zitadel/zitadel/internal/errors"
es_models "github.com/zitadel/zitadel/internal/eventstore/v1/models"
)
type OTP struct {
es_models.ObjectRoot
Secret *crypto.CryptoValue
SecretString string
Url string
State MFAState
}
type TOTP struct {
*ObjectDetails
@ -24,7 +15,7 @@ type TOTP struct {
URI string
}
func NewOTPKey(issuer, accountName string, cryptoAlg crypto.EncryptionAlgorithm) (*otp.Key, *crypto.CryptoValue, error) {
func NewTOTPKey(issuer, accountName string, cryptoAlg crypto.EncryptionAlgorithm) (*otp.Key, *crypto.CryptoValue, error) {
key, err := totp.Generate(totp.GenerateOpts{Issuer: issuer, AccountName: accountName})
if err != nil {
return nil, nil, caos_errs.ThrowInternal(err, "TOTP-ieY3o", "Errors.Internal")
@ -36,7 +27,7 @@ func NewOTPKey(issuer, accountName string, cryptoAlg crypto.EncryptionAlgorithm)
return key, encryptedSecret, nil
}
func VerifyMFAOTP(code string, secret *crypto.CryptoValue, cryptoAlg crypto.EncryptionAlgorithm) error {
func VerifyTOTP(code string, secret *crypto.CryptoValue, cryptoAlg crypto.EncryptionAlgorithm) error {
decrypt, err := crypto.DecryptString(secret, cryptoAlg)
if err != nil {
return err

View File

@ -30,6 +30,8 @@ type Phone struct {
PhoneNumber PhoneNumber
IsPhoneVerified bool
// PlainCode is set by the command and can be used to return it to the caller (API)
PlainCode *string
}
type PhoneCode struct {

View File

@ -11,6 +11,8 @@ const (
SecretGeneratorTypePasswordResetCode
SecretGeneratorTypePasswordlessInitCode
SecretGeneratorTypeAppSecret
SecretGeneratorTypeOTPSMS
SecretGeneratorTypeOTPEmail
secretGeneratorTypeCount
)

View File

@ -48,11 +48,13 @@ type UserAuthMethodType int32
const (
UserAuthMethodTypeUnspecified UserAuthMethodType = iota
UserAuthMethodTypeOTP
UserAuthMethodTypeTOTP
UserAuthMethodTypeU2F
UserAuthMethodTypePasswordless
UserAuthMethodTypePassword
UserAuthMethodTypeIDP
UserAuthMethodTypeOTPSMS
UserAuthMethodTypeOTPEmail
userAuthMethodTypeCount
)
@ -67,15 +69,14 @@ func HasMFA(methods []UserAuthMethodType) bool {
var factors int
for _, method := range methods {
switch method {
case UserAuthMethodTypePassword:
factors++
case UserAuthMethodTypePasswordless:
return true
case UserAuthMethodTypeU2F:
factors++
case UserAuthMethodTypeOTP:
factors++
case UserAuthMethodTypeIDP:
case UserAuthMethodTypePassword,
UserAuthMethodTypeU2F,
UserAuthMethodTypeTOTP,
UserAuthMethodTypeOTPSMS,
UserAuthMethodTypeOTPEmail,
UserAuthMethodTypeIDP:
factors++
case UserAuthMethodTypeUnspecified,
userAuthMethodTypeCount:

View File

@ -315,6 +315,9 @@ func GenericEventMapper[T any, PT BaseEventSetter[T]](event *repository.Event) (
e := PT(new(T))
e.SetBaseEvent(BaseEventFromRepo(event))
if len(event.Data) == 0 {
return e, nil
}
err := json.Unmarshal(event.Data, e)
if err != nil {
return nil, errors.ThrowInternal(err, "V2-Thai6", "unable to unmarshal event")

View File

@ -106,8 +106,12 @@ func secondFactorsToDomain(types []domain.SecondFactorType) []domain.SecondFacto
switch secondfactorType {
case domain.SecondFactorTypeU2F:
secondfactors[i] = domain.SecondFactorTypeU2F
case domain.SecondFactorTypeOTP:
secondfactors[i] = domain.SecondFactorTypeOTP
case domain.SecondFactorTypeTOTP:
secondfactors[i] = domain.SecondFactorTypeTOTP
case domain.SecondFactorTypeOTPEmail:
secondfactors[i] = domain.SecondFactorTypeOTPEmail
case domain.SecondFactorTypeOTPSMS:
secondfactors[i] = domain.SecondFactorTypeOTPSMS
}
}
return secondfactors

View File

@ -91,6 +91,12 @@ func (s *Tester) CreateHumanUser(ctx context.Context) *user.AddHumanUserResponse
ReturnCode: &user.ReturnEmailVerificationCode{},
},
},
Phone: &user.SetHumanPhone{
Phone: "+41791234567",
Verification: &user.SetHumanPhone_ReturnCode{
ReturnCode: &user.ReturnPhoneVerificationCode{},
},
},
})
logging.OnError(err).Fatal("create human user")
return resp

View File

@ -248,7 +248,11 @@ func (s *Tester) WithInstanceAuthorization(ctx context.Context, u UserType, inst
if u == SystemUser {
s.ensureSystemUser()
}
return metadata.AppendToOutgoingContext(ctx, "Authorization", fmt.Sprintf("Bearer %s", s.Users.Get(instanceID, u).Token))
return s.WithAuthorizationToken(ctx, s.Users.Get(instanceID, u).Token)
}
func (s *Tester) WithAuthorizationToken(ctx context.Context, token string) context.Context {
return metadata.AppendToOutgoingContext(ctx, "Authorization", fmt.Sprintf("Bearer %s", token))
}
func (s *Tester) ensureSystemUser() {

View File

@ -182,6 +182,7 @@ func (u *userNotifier) reduceEmailCodeAdded(event eventstore.Event) (*handler.St
if !ok {
return nil, errors.ThrowInvalidArgumentf(nil, "HANDL-SWf3g", "reduce.wrong.event.type %s", user.HumanEmailCodeAddedType)
}
if e.CodeReturned {
return crdb.NewNoOpStatement(e), nil
}
@ -535,6 +536,9 @@ func (u *userNotifier) reducePhoneCodeAdded(event eventstore.Event) (*handler.St
if !ok {
return nil, errors.ThrowInvalidArgumentf(nil, "HANDL-He83g", "reduce.wrong.event.type %s", user.HumanPhoneCodeAddedType)
}
if e.CodeReturned {
return crdb.NewNoOpStatement(e), nil
}
ctx := HandlerContext(event.Aggregate())
alreadyHandled, err := u.checkIfCodeAlreadyHandledOrExpired(ctx, event, e.Expiry, nil,
user.UserV1PhoneCodeAddedType, user.UserV1PhoneCodeSentType,

View File

@ -129,7 +129,7 @@ func Test_LoginPolicyPrepares(t *testing.T) {
true,
true,
true,
database.EnumArray[domain.SecondFactorType]{domain.SecondFactorTypeOTP},
database.EnumArray[domain.SecondFactorType]{domain.SecondFactorTypeTOTP},
database.EnumArray[domain.MultiFactorType]{domain.MultiFactorTypeU2FWithPIN},
domain.PasswordlessTypeAllowed,
true,
@ -157,7 +157,7 @@ func Test_LoginPolicyPrepares(t *testing.T) {
AllowExternalIDPs: true,
ForceMFA: true,
ForceMFALocalOnly: true,
SecondFactors: database.EnumArray[domain.SecondFactorType]{domain.SecondFactorTypeOTP},
SecondFactors: database.EnumArray[domain.SecondFactorType]{domain.SecondFactorTypeTOTP},
MultiFactors: database.EnumArray[domain.MultiFactorType]{domain.MultiFactorTypeU2FWithPIN},
PasswordlessType: domain.PasswordlessTypeAllowed,
IsDefault: true,
@ -217,7 +217,7 @@ func Test_LoginPolicyPrepares(t *testing.T) {
regexp.QuoteMeta(prepareLoginPolicy2FAsStmt),
prepareLoginPolicy2FAsCols,
[]driver.Value{
database.EnumArray[domain.SecondFactorType]{domain.SecondFactorTypeOTP},
database.EnumArray[domain.SecondFactorType]{domain.SecondFactorTypeTOTP},
},
),
},
@ -225,7 +225,7 @@ func Test_LoginPolicyPrepares(t *testing.T) {
SearchResponse: SearchResponse{
Count: 1,
},
Factors: database.EnumArray[domain.SecondFactorType]{domain.SecondFactorTypeOTP},
Factors: database.EnumArray[domain.SecondFactorType]{domain.SecondFactorTypeTOTP},
},
},
{

View File

@ -115,11 +115,11 @@ func (p *loginPolicyProjection) reducers() []handler.AggregateReducer {
},
{
Event: org.LoginPolicySecondFactorAddedEventType,
Reduce: p.reduce2FAAdded,
Reduce: p.reduceSecondFactorAdded,
},
{
Event: org.LoginPolicySecondFactorRemovedEventType,
Reduce: p.reduce2FARemoved,
Reduce: p.reduceSecondFactorRemoved,
},
{
Event: org.OrgRemovedEventType,
@ -148,11 +148,11 @@ func (p *loginPolicyProjection) reducers() []handler.AggregateReducer {
},
{
Event: instance.LoginPolicySecondFactorAddedEventType,
Reduce: p.reduce2FAAdded,
Reduce: p.reduceSecondFactorAdded,
},
{
Event: instance.LoginPolicySecondFactorRemovedEventType,
Reduce: p.reduce2FARemoved,
Reduce: p.reduceSecondFactorRemoved,
},
{
Event: instance.InstanceRemovedEventType,
@ -345,7 +345,7 @@ func (p *loginPolicyProjection) reduceLoginPolicyRemoved(event eventstore.Event)
), nil
}
func (p *loginPolicyProjection) reduce2FAAdded(event eventstore.Event) (*handler.Statement, error) {
func (p *loginPolicyProjection) reduceSecondFactorAdded(event eventstore.Event) (*handler.Statement, error) {
var policyEvent policy.SecondFactorAddedEvent
switch e := event.(type) {
case *instance.LoginPolicySecondFactorAddedEvent:
@ -370,7 +370,7 @@ func (p *loginPolicyProjection) reduce2FAAdded(event eventstore.Event) (*handler
), nil
}
func (p *loginPolicyProjection) reduce2FARemoved(event eventstore.Event) (*handler.Statement, error) {
func (p *loginPolicyProjection) reduceSecondFactorRemoved(event eventstore.Event) (*handler.Statement, error) {
var policyEvent policy.SecondFactorRemovedEvent
switch e := event.(type) {
case *instance.LoginPolicySecondFactorRemovedEvent:

View File

@ -310,8 +310,8 @@ func TestLoginPolicyProjection_reduces(t *testing.T) {
},
},
{
name: "org reduce2FAAdded",
reduce: (&loginPolicyProjection{}).reduce2FAAdded,
name: "org reduceSecondFactorAdded",
reduce: (&loginPolicyProjection{}).reduceSecondFactorAdded,
args: args{
event: getEvent(testEvent(
repository.EventType(org.LoginPolicySecondFactorAddedEventType),
@ -342,8 +342,8 @@ func TestLoginPolicyProjection_reduces(t *testing.T) {
},
},
{
name: "org reduce2FARemoved",
reduce: (&loginPolicyProjection{}).reduce2FARemoved,
name: "org reduceSecondFactorRemoved",
reduce: (&loginPolicyProjection{}).reduceSecondFactorRemoved,
args: args{
event: getEvent(testEvent(
repository.EventType(org.LoginPolicySecondFactorRemovedEventType),
@ -558,8 +558,8 @@ func TestLoginPolicyProjection_reduces(t *testing.T) {
},
},
{
name: "instance reduce2FAAdded",
reduce: (&loginPolicyProjection{}).reduce2FAAdded,
name: "instance reduceSecondFactorAdded u2f",
reduce: (&loginPolicyProjection{}).reduceSecondFactorAdded,
args: args{
event: getEvent(testEvent(
repository.EventType(instance.LoginPolicySecondFactorAddedEventType),
@ -590,8 +590,8 @@ func TestLoginPolicyProjection_reduces(t *testing.T) {
},
},
{
name: "instance reduce2FARemoved",
reduce: (&loginPolicyProjection{}).reduce2FARemoved,
name: "instance reduceSecondFactorRemoved u2f",
reduce: (&loginPolicyProjection{}).reduceSecondFactorRemoved,
args: args{
event: getEvent(testEvent(
repository.EventType(instance.LoginPolicySecondFactorRemovedEventType),
@ -621,6 +621,70 @@ func TestLoginPolicyProjection_reduces(t *testing.T) {
},
},
},
{
name: "instance reduceSecondFactorAdded otp email",
reduce: (&loginPolicyProjection{}).reduceSecondFactorAdded,
args: args{
event: getEvent(testEvent(
repository.EventType(instance.LoginPolicySecondFactorAddedEventType),
instance.AggregateType,
[]byte(`{
"mfaType": 3
}`),
), instance.SecondFactorAddedEventMapper),
},
want: wantReduce{
aggregateType: eventstore.AggregateType("instance"),
sequence: 15,
previousSequence: 10,
executer: &testExecuter{
executions: []execution{
{
expectedStmt: "UPDATE projections.login_policies5 SET (change_date, sequence, second_factors) = ($1, $2, array_append(second_factors, $3)) WHERE (aggregate_id = $4) AND (instance_id = $5)",
expectedArgs: []interface{}{
anyArg{},
uint64(15),
domain.SecondFactorTypeOTPEmail,
"agg-id",
"instance-id",
},
},
},
},
},
},
{
name: "instance reduceSecondFactorRemoved otp email",
reduce: (&loginPolicyProjection{}).reduceSecondFactorRemoved,
args: args{
event: getEvent(testEvent(
repository.EventType(instance.LoginPolicySecondFactorRemovedEventType),
instance.AggregateType,
[]byte(`{
"mfaType": 3
}`),
), instance.SecondFactorRemovedEventMapper),
},
want: wantReduce{
aggregateType: eventstore.AggregateType("instance"),
sequence: 15,
previousSequence: 10,
executer: &testExecuter{
executions: []execution{
{
expectedStmt: "UPDATE projections.login_policies5 SET (change_date, sequence, second_factors) = ($1, $2, array_remove(second_factors, $3)) WHERE (aggregate_id = $4) AND (instance_id = $5)",
expectedArgs: []interface{}{
anyArg{},
uint64(15),
domain.SecondFactorTypeOTPEmail,
"agg-id",
"instance-id",
},
},
},
},
},
},
{
name: "org.reduceOwnerRemoved",
reduce: (&loginPolicyProjection{}).reduceOwnerRemoved,

View File

@ -89,6 +89,14 @@ func (p *userAuthMethodProjection) reducers() []handler.AggregateReducer {
Event: user.HumanMFAOTPVerifiedType,
Reduce: p.reduceActivateEvent,
},
{
Event: user.HumanOTPSMSAddedType,
Reduce: p.reduceAddAuthMethod,
},
{
Event: user.HumanOTPEmailAddedType,
Reduce: p.reduceAddAuthMethod,
},
{
Event: user.HumanPasswordlessTokenRemovedType,
Reduce: p.reduceRemoveAuthMethod,
@ -101,6 +109,14 @@ func (p *userAuthMethodProjection) reducers() []handler.AggregateReducer {
Event: user.HumanMFAOTPRemovedType,
Reduce: p.reduceRemoveAuthMethod,
},
{
Event: user.HumanOTPSMSRemovedType,
Reduce: p.reduceRemoveAuthMethod,
},
{
Event: user.HumanOTPEmailRemovedType,
Reduce: p.reduceRemoveAuthMethod,
},
},
},
{
@ -135,7 +151,7 @@ func (p *userAuthMethodProjection) reduceInitAuthMethod(event eventstore.Event)
methodType = domain.UserAuthMethodTypeU2F
tokenID = e.WebAuthNTokenID
case *user.HumanOTPAddedEvent:
methodType = domain.UserAuthMethodTypeOTP
methodType = domain.UserAuthMethodTypeTOTP
default:
return nil, errors.ThrowInvalidArgumentf(nil, "PROJE-f92f", "reduce.wrong.event.type %v", []eventstore.EventType{user.HumanPasswordlessTokenAddedType, user.HumanU2FTokenAddedType})
}
@ -178,7 +194,7 @@ func (p *userAuthMethodProjection) reduceActivateEvent(event eventstore.Event) (
tokenID = e.WebAuthNTokenID
name = e.WebAuthNTokenName
case *user.HumanOTPVerifiedEvent:
methodType = domain.UserAuthMethodTypeOTP
methodType = domain.UserAuthMethodTypeTOTP
default:
return nil, errors.ThrowInvalidArgumentf(nil, "PROJE-f92f", "reduce.wrong.event.type %v", []eventstore.EventType{user.HumanPasswordlessTokenAddedType, user.HumanU2FTokenAddedType})
@ -202,6 +218,34 @@ func (p *userAuthMethodProjection) reduceActivateEvent(event eventstore.Event) (
), nil
}
func (p *userAuthMethodProjection) reduceAddAuthMethod(event eventstore.Event) (*handler.Statement, error) {
var methodType domain.UserAuthMethodType
switch event.(type) {
case *user.HumanOTPSMSAddedEvent:
methodType = domain.UserAuthMethodTypeOTPSMS
case *user.HumanOTPEmailAddedEvent:
methodType = domain.UserAuthMethodTypeOTPEmail
default:
return nil, errors.ThrowInvalidArgumentf(nil, "PROJE-DS4g3", "reduce.wrong.event.type %v", []eventstore.EventType{user.HumanOTPSMSAddedType, user.HumanOTPEmailAddedType})
}
return crdb.NewCreateStatement(
event,
[]handler.Column{
handler.NewCol(UserAuthMethodTokenIDCol, ""),
handler.NewCol(UserAuthMethodCreationDateCol, event.CreationDate()),
handler.NewCol(UserAuthMethodChangeDateCol, event.CreationDate()),
handler.NewCol(UserAuthMethodResourceOwnerCol, event.Aggregate().ResourceOwner),
handler.NewCol(UserAuthMethodInstanceIDCol, event.Aggregate().InstanceID),
handler.NewCol(UserAuthMethodUserIDCol, event.Aggregate().ID),
handler.NewCol(UserAuthMethodSequenceCol, event.Sequence()),
handler.NewCol(UserAuthMethodStateCol, domain.MFAStateReady),
handler.NewCol(UserAuthMethodTypeCol, methodType),
handler.NewCol(UserAuthMethodNameCol, ""),
},
), nil
}
func (p *userAuthMethodProjection) reduceRemoveAuthMethod(event eventstore.Event) (*handler.Statement, error) {
var tokenID string
var methodType domain.UserAuthMethodType
@ -213,10 +257,17 @@ func (p *userAuthMethodProjection) reduceRemoveAuthMethod(event eventstore.Event
methodType = domain.UserAuthMethodTypeU2F
tokenID = e.WebAuthNTokenID
case *user.HumanOTPRemovedEvent:
methodType = domain.UserAuthMethodTypeOTP
methodType = domain.UserAuthMethodTypeTOTP
case *user.HumanOTPSMSRemovedEvent,
*user.HumanPhoneRemovedEvent:
methodType = domain.UserAuthMethodTypeOTPSMS
case *user.HumanOTPEmailRemovedEvent:
methodType = domain.UserAuthMethodTypeOTPEmail
default:
return nil, errors.ThrowInvalidArgumentf(nil, "PROJE-f92f", "reduce.wrong.event.type %v", []eventstore.EventType{user.HumanPasswordlessTokenAddedType, user.HumanU2FTokenAddedType})
return nil, errors.ThrowInvalidArgumentf(nil, "PROJE-f92f", "reduce.wrong.event.type %v",
[]eventstore.EventType{user.HumanPasswordlessTokenAddedType, user.HumanU2FTokenAddedType, user.HumanMFAOTPRemovedType,
user.HumanOTPSMSRemovedType, user.HumanPhoneRemovedType, user.HumanOTPEmailRemovedType})
}
conditions := []handler.Condition{
handler.NewCond(UserAuthMethodUserIDCol, event.Aggregate().ID),

View File

@ -98,7 +98,7 @@ func TestUserAuthMethodProjection_reduces(t *testing.T) {
},
},
{
name: "reduceAddedOTP",
name: "reduceAddedTOTP",
args: args{
event: getEvent(testEvent(
repository.EventType(user.HumanMFAOTPAddedType),
@ -125,7 +125,7 @@ func TestUserAuthMethodProjection_reduces(t *testing.T) {
"agg-id",
uint64(15),
domain.MFAStateNotReady,
domain.UserAuthMethodTypeOTP,
domain.UserAuthMethodTypeTOTP,
"",
},
},
@ -208,7 +208,7 @@ func TestUserAuthMethodProjection_reduces(t *testing.T) {
},
},
{
name: "reduceVerifiedOTP",
name: "reduceVerifiedTOTP",
args: args{
event: getEvent(testEvent(
repository.EventType(user.HumanMFAOTPVerifiedType),
@ -232,7 +232,7 @@ func TestUserAuthMethodProjection_reduces(t *testing.T) {
"",
domain.MFAStateReady,
"agg-id",
domain.UserAuthMethodTypeOTP,
domain.UserAuthMethodTypeTOTP,
"ro-id",
"",
"instance-id",
@ -242,6 +242,256 @@ func TestUserAuthMethodProjection_reduces(t *testing.T) {
},
},
},
{
name: "reduceAddedOTPSMS",
args: args{
event: getEvent(testEvent(
repository.EventType(user.HumanOTPSMSAddedType),
user.AggregateType,
nil,
), eventstore.GenericEventMapper[user.HumanOTPSMSAddedEvent]),
},
reduce: (&userAuthMethodProjection{}).reduceAddAuthMethod,
want: wantReduce{
aggregateType: user.AggregateType,
sequence: 15,
previousSequence: 10,
executer: &testExecuter{
executions: []execution{
{
expectedStmt: "INSERT INTO projections.user_auth_methods4 (token_id, creation_date, change_date, resource_owner, instance_id, user_id, sequence, state, method_type, name) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)",
expectedArgs: []interface{}{
"",
anyArg{},
anyArg{},
"ro-id",
"instance-id",
"agg-id",
uint64(15),
domain.MFAStateReady,
domain.UserAuthMethodTypeOTPSMS,
"",
},
},
},
},
},
},
{
name: "reduceAddedOTPEmail",
args: args{
event: getEvent(testEvent(
repository.EventType(user.HumanOTPEmailAddedType),
user.AggregateType,
nil,
), eventstore.GenericEventMapper[user.HumanOTPEmailAddedEvent]),
},
reduce: (&userAuthMethodProjection{}).reduceAddAuthMethod,
want: wantReduce{
aggregateType: user.AggregateType,
sequence: 15,
previousSequence: 10,
executer: &testExecuter{
executions: []execution{
{
expectedStmt: "INSERT INTO projections.user_auth_methods4 (token_id, creation_date, change_date, resource_owner, instance_id, user_id, sequence, state, method_type, name) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)",
expectedArgs: []interface{}{
"",
anyArg{},
anyArg{},
"ro-id",
"instance-id",
"agg-id",
uint64(15),
domain.MFAStateReady,
domain.UserAuthMethodTypeOTPEmail,
"",
},
},
},
},
},
},
{
name: "reduceRemoveOTPPasswordless",
args: args{
event: getEvent(testEvent(
repository.EventType(user.HumanPasswordlessTokenRemovedType),
user.AggregateType,
[]byte(`{
"webAuthNTokenId": "token-id"
}`),
), user.HumanPasswordlessRemovedEventMapper),
},
reduce: (&userAuthMethodProjection{}).reduceRemoveAuthMethod,
want: wantReduce{
aggregateType: user.AggregateType,
sequence: 15,
previousSequence: 10,
executer: &testExecuter{
executions: []execution{
{
expectedStmt: "DELETE FROM projections.user_auth_methods4 WHERE (user_id = $1) AND (method_type = $2) AND (resource_owner = $3) AND (instance_id = $4) AND (token_id = $5)",
expectedArgs: []interface{}{
"agg-id",
domain.UserAuthMethodTypePasswordless,
"ro-id",
"instance-id",
"token-id",
},
},
},
},
},
},
{
name: "reduceRemoveOTPU2F",
args: args{
event: getEvent(testEvent(
repository.EventType(user.HumanU2FTokenRemovedType),
user.AggregateType,
[]byte(`{
"webAuthNTokenId": "token-id"
}`),
), user.HumanU2FRemovedEventMapper),
},
reduce: (&userAuthMethodProjection{}).reduceRemoveAuthMethod,
want: wantReduce{
aggregateType: user.AggregateType,
sequence: 15,
previousSequence: 10,
executer: &testExecuter{
executions: []execution{
{
expectedStmt: "DELETE FROM projections.user_auth_methods4 WHERE (user_id = $1) AND (method_type = $2) AND (resource_owner = $3) AND (instance_id = $4) AND (token_id = $5)",
expectedArgs: []interface{}{
"agg-id",
domain.UserAuthMethodTypeU2F,
"ro-id",
"instance-id",
"token-id",
},
},
},
},
},
},
{
name: "reduceRemoveTOTP",
args: args{
event: getEvent(testEvent(
repository.EventType(user.HumanMFAOTPRemovedType),
user.AggregateType,
nil,
), user.HumanOTPRemovedEventMapper),
},
reduce: (&userAuthMethodProjection{}).reduceRemoveAuthMethod,
want: wantReduce{
aggregateType: user.AggregateType,
sequence: 15,
previousSequence: 10,
executer: &testExecuter{
executions: []execution{
{
expectedStmt: "DELETE FROM projections.user_auth_methods4 WHERE (user_id = $1) AND (method_type = $2) AND (resource_owner = $3) AND (instance_id = $4)",
expectedArgs: []interface{}{
"agg-id",
domain.UserAuthMethodTypeTOTP,
"ro-id",
"instance-id",
},
},
},
},
},
},
{
name: "reduceRemoveOTPSMS",
args: args{
event: getEvent(testEvent(
repository.EventType(user.HumanOTPSMSRemovedType),
user.AggregateType,
nil,
), eventstore.GenericEventMapper[user.HumanOTPSMSRemovedEvent]),
},
reduce: (&userAuthMethodProjection{}).reduceRemoveAuthMethod,
want: wantReduce{
aggregateType: user.AggregateType,
sequence: 15,
previousSequence: 10,
executer: &testExecuter{
executions: []execution{
{
expectedStmt: "DELETE FROM projections.user_auth_methods4 WHERE (user_id = $1) AND (method_type = $2) AND (resource_owner = $3) AND (instance_id = $4)",
expectedArgs: []interface{}{
"agg-id",
domain.UserAuthMethodTypeOTPSMS,
"ro-id",
"instance-id",
},
},
},
},
},
},
{
name: "reduceRemovePhone",
args: args{
event: getEvent(testEvent(
repository.EventType(user.HumanPhoneRemovedType),
user.AggregateType,
nil,
), user.HumanPhoneRemovedEventMapper),
},
reduce: (&userAuthMethodProjection{}).reduceRemoveAuthMethod,
want: wantReduce{
aggregateType: user.AggregateType,
sequence: 15,
previousSequence: 10,
executer: &testExecuter{
executions: []execution{
{
expectedStmt: "DELETE FROM projections.user_auth_methods4 WHERE (user_id = $1) AND (method_type = $2) AND (resource_owner = $3) AND (instance_id = $4)",
expectedArgs: []interface{}{
"agg-id",
domain.UserAuthMethodTypeOTPSMS,
"ro-id",
"instance-id",
},
},
},
},
},
},
{
name: "reduceRemoveOTPEmail",
args: args{
event: getEvent(testEvent(
repository.EventType(user.HumanOTPEmailRemovedType),
user.AggregateType,
nil,
), eventstore.GenericEventMapper[user.HumanOTPEmailRemovedEvent]),
},
reduce: (&userAuthMethodProjection{}).reduceRemoveAuthMethod,
want: wantReduce{
aggregateType: user.AggregateType,
sequence: 15,
previousSequence: 10,
executer: &testExecuter{
executions: []execution{
{
expectedStmt: "DELETE FROM projections.user_auth_methods4 WHERE (user_id = $1) AND (method_type = $2) AND (resource_owner = $3) AND (instance_id = $4)",
expectedArgs: []interface{}{
"agg-id",
domain.UserAuthMethodTypeOTPEmail,
"ro-id",
"instance-id",
},
},
},
},
},
},
{
name: "org reduceOwnerRemoved",
reduce: (&userAuthMethodProjection{}).reduceOwnerRemoved,

View File

@ -280,7 +280,7 @@ func Test_UserAuthMethodPrepares(t *testing.T) {
},
{
true,
domain.UserAuthMethodTypeOTP,
domain.UserAuthMethodTypeTOTP,
1,
},
},
@ -292,7 +292,7 @@ func Test_UserAuthMethodPrepares(t *testing.T) {
},
AuthMethodTypes: []domain.UserAuthMethodType{
domain.UserAuthMethodTypePasswordless,
domain.UserAuthMethodTypeOTP,
domain.UserAuthMethodTypeTOTP,
domain.UserAuthMethodTypePassword,
domain.UserAuthMethodTypeIDP,
},
@ -399,7 +399,7 @@ func Test_UserAuthMethodPrepares(t *testing.T) {
},
{
true,
domain.UserAuthMethodTypeOTP,
domain.UserAuthMethodTypeTOTP,
1,
true,
true,
@ -411,7 +411,7 @@ func Test_UserAuthMethodPrepares(t *testing.T) {
object: &testUserAuthMethodTypesRequired{
authMethods: []domain.UserAuthMethodType{
domain.UserAuthMethodTypePasswordless,
domain.UserAuthMethodTypeOTP,
domain.UserAuthMethodTypeTOTP,
domain.UserAuthMethodTypePassword,
domain.UserAuthMethodTypeIDP,
},

View File

@ -89,6 +89,14 @@ func RegisterEventMappers(es *eventstore.Eventstore) {
RegisterFilterEventMapper(AggregateType, HumanMFAOTPRemovedType, HumanOTPRemovedEventMapper).
RegisterFilterEventMapper(AggregateType, HumanMFAOTPCheckSucceededType, HumanOTPCheckSucceededEventMapper).
RegisterFilterEventMapper(AggregateType, HumanMFAOTPCheckFailedType, HumanOTPCheckFailedEventMapper).
RegisterFilterEventMapper(AggregateType, HumanOTPSMSAddedType, eventstore.GenericEventMapper[HumanOTPSMSAddedEvent]).
RegisterFilterEventMapper(AggregateType, HumanOTPSMSRemovedType, eventstore.GenericEventMapper[HumanOTPSMSRemovedEvent]).
RegisterFilterEventMapper(AggregateType, HumanOTPSMSCheckSucceededType, eventstore.GenericEventMapper[HumanOTPSMSCheckSucceededEvent]).
RegisterFilterEventMapper(AggregateType, HumanOTPSMSCheckFailedType, eventstore.GenericEventMapper[HumanOTPSMSCheckFailedEvent]).
RegisterFilterEventMapper(AggregateType, HumanOTPEmailAddedType, eventstore.GenericEventMapper[HumanOTPEmailAddedEvent]).
RegisterFilterEventMapper(AggregateType, HumanOTPEmailRemovedType, eventstore.GenericEventMapper[HumanOTPEmailRemovedEvent]).
RegisterFilterEventMapper(AggregateType, HumanOTPEmailCheckSucceededType, eventstore.GenericEventMapper[HumanOTPEmailCheckSucceededEvent]).
RegisterFilterEventMapper(AggregateType, HumanOTPEmailCheckFailedType, eventstore.GenericEventMapper[HumanOTPEmailCheckFailedEvent]).
RegisterFilterEventMapper(AggregateType, HumanU2FTokenAddedType, HumanU2FAddedEventMapper).
RegisterFilterEventMapper(AggregateType, HumanU2FTokenVerifiedType, HumanU2FVerifiedEventMapper).
RegisterFilterEventMapper(AggregateType, HumanU2FTokenSignCountChangedType, HumanU2FSignCountChangedEventMapper).

View File

@ -12,12 +12,22 @@ import (
)
const (
otpEventPrefix = mfaEventPrefix + "otp."
HumanMFAOTPAddedType = otpEventPrefix + "added"
HumanMFAOTPVerifiedType = otpEventPrefix + "verified"
HumanMFAOTPRemovedType = otpEventPrefix + "removed"
HumanMFAOTPCheckSucceededType = otpEventPrefix + "check.succeeded"
HumanMFAOTPCheckFailedType = otpEventPrefix + "check.failed"
otpEventPrefix = mfaEventPrefix + "otp."
HumanMFAOTPAddedType = otpEventPrefix + "added"
HumanMFAOTPVerifiedType = otpEventPrefix + "verified"
HumanMFAOTPRemovedType = otpEventPrefix + "removed"
HumanMFAOTPCheckSucceededType = otpEventPrefix + "check.succeeded"
HumanMFAOTPCheckFailedType = otpEventPrefix + "check.failed"
otpSMSEventPrefix = otpEventPrefix + "sms."
HumanOTPSMSAddedType = otpSMSEventPrefix + "added"
HumanOTPSMSRemovedType = otpSMSEventPrefix + "removed"
HumanOTPSMSCheckSucceededType = otpSMSEventPrefix + "check.succeeded"
HumanOTPSMSCheckFailedType = otpSMSEventPrefix + "check.failed"
otpEmailEventPrefix = otpEventPrefix + "email."
HumanOTPEmailAddedType = otpEmailEventPrefix + "added"
HumanOTPEmailRemovedType = otpEmailEventPrefix + "removed"
HumanOTPEmailCheckSucceededType = otpEmailEventPrefix + "check.succeeded"
HumanOTPEmailCheckFailedType = otpEmailEventPrefix + "check.failed"
)
type HumanOTPAddedEvent struct {
@ -202,3 +212,247 @@ func HumanOTPCheckFailedEventMapper(event *repository.Event) (eventstore.Event,
}
return otpAdded, nil
}
type HumanOTPSMSAddedEvent struct {
eventstore.BaseEvent `json:"-"`
}
func (e *HumanOTPSMSAddedEvent) Data() interface{} {
return nil
}
func (e *HumanOTPSMSAddedEvent) UniqueConstraints() []*eventstore.EventUniqueConstraint {
return nil
}
func (e *HumanOTPSMSAddedEvent) SetBaseEvent(event *eventstore.BaseEvent) {
e.BaseEvent = *event
}
func NewHumanOTPSMSAddedEvent(
ctx context.Context,
aggregate *eventstore.Aggregate,
) *HumanOTPSMSAddedEvent {
return &HumanOTPSMSAddedEvent{
BaseEvent: *eventstore.NewBaseEventForPush(
ctx,
aggregate,
HumanOTPSMSAddedType,
),
}
}
type HumanOTPSMSRemovedEvent struct {
eventstore.BaseEvent `json:"-"`
}
func (e *HumanOTPSMSRemovedEvent) Data() interface{} {
return nil
}
func (e *HumanOTPSMSRemovedEvent) UniqueConstraints() []*eventstore.EventUniqueConstraint {
return nil
}
func (e *HumanOTPSMSRemovedEvent) SetBaseEvent(event *eventstore.BaseEvent) {
e.BaseEvent = *event
}
func NewHumanOTPSMSRemovedEvent(
ctx context.Context,
aggregate *eventstore.Aggregate,
) *HumanOTPSMSRemovedEvent {
return &HumanOTPSMSRemovedEvent{
BaseEvent: *eventstore.NewBaseEventForPush(
ctx,
aggregate,
HumanOTPSMSRemovedType,
),
}
}
type HumanOTPSMSCheckSucceededEvent struct {
eventstore.BaseEvent `json:"-"`
*AuthRequestInfo
}
func (e *HumanOTPSMSCheckSucceededEvent) Data() interface{} {
return e
}
func (e *HumanOTPSMSCheckSucceededEvent) UniqueConstraints() []*eventstore.EventUniqueConstraint {
return nil
}
func (e *HumanOTPSMSCheckSucceededEvent) SetBaseEvent(event *eventstore.BaseEvent) {
e.BaseEvent = *event
}
func NewHumanOTPSMSCheckSucceededEvent(
ctx context.Context,
aggregate *eventstore.Aggregate,
info *AuthRequestInfo,
) *HumanOTPSMSCheckSucceededEvent {
return &HumanOTPSMSCheckSucceededEvent{
BaseEvent: *eventstore.NewBaseEventForPush(
ctx,
aggregate,
HumanOTPSMSCheckSucceededType,
),
AuthRequestInfo: info,
}
}
type HumanOTPSMSCheckFailedEvent struct {
eventstore.BaseEvent `json:"-"`
*AuthRequestInfo
}
func (e *HumanOTPSMSCheckFailedEvent) Data() interface{} {
return e
}
func (e *HumanOTPSMSCheckFailedEvent) UniqueConstraints() []*eventstore.EventUniqueConstraint {
return nil
}
func (e *HumanOTPSMSCheckFailedEvent) SetBaseEvent(event *eventstore.BaseEvent) {
e.BaseEvent = *event
}
func NewHumanOTPSMSCheckFailedEvent(
ctx context.Context,
aggregate *eventstore.Aggregate,
info *AuthRequestInfo,
) *HumanOTPSMSCheckFailedEvent {
return &HumanOTPSMSCheckFailedEvent{
BaseEvent: *eventstore.NewBaseEventForPush(
ctx,
aggregate,
HumanOTPSMSCheckFailedType,
),
AuthRequestInfo: info,
}
}
type HumanOTPEmailAddedEvent struct {
eventstore.BaseEvent `json:"-"`
}
func (e *HumanOTPEmailAddedEvent) Data() interface{} {
return nil
}
func (e *HumanOTPEmailAddedEvent) UniqueConstraints() []*eventstore.EventUniqueConstraint {
return nil
}
func (e *HumanOTPEmailAddedEvent) SetBaseEvent(event *eventstore.BaseEvent) {
e.BaseEvent = *event
}
func NewHumanOTPEmailAddedEvent(
ctx context.Context,
aggregate *eventstore.Aggregate,
) *HumanOTPEmailAddedEvent {
return &HumanOTPEmailAddedEvent{
BaseEvent: *eventstore.NewBaseEventForPush(
ctx,
aggregate,
HumanOTPEmailAddedType,
),
}
}
type HumanOTPEmailRemovedEvent struct {
eventstore.BaseEvent `json:"-"`
}
func (e *HumanOTPEmailRemovedEvent) Data() interface{} {
return nil
}
func (e *HumanOTPEmailRemovedEvent) UniqueConstraints() []*eventstore.EventUniqueConstraint {
return nil
}
func (e *HumanOTPEmailRemovedEvent) SetBaseEvent(event *eventstore.BaseEvent) {
e.BaseEvent = *event
}
func NewHumanOTPEmailRemovedEvent(
ctx context.Context,
aggregate *eventstore.Aggregate,
) *HumanOTPEmailRemovedEvent {
return &HumanOTPEmailRemovedEvent{
BaseEvent: *eventstore.NewBaseEventForPush(
ctx,
aggregate,
HumanOTPEmailRemovedType,
),
}
}
type HumanOTPEmailCheckSucceededEvent struct {
eventstore.BaseEvent `json:"-"`
*AuthRequestInfo
}
func (e *HumanOTPEmailCheckSucceededEvent) Data() interface{} {
return e
}
func (e *HumanOTPEmailCheckSucceededEvent) UniqueConstraints() []*eventstore.EventUniqueConstraint {
return nil
}
func (e *HumanOTPEmailCheckSucceededEvent) SetBaseEvent(event *eventstore.BaseEvent) {
e.BaseEvent = *event
}
func NewHumanOTPEmailCheckSucceededEvent(
ctx context.Context,
aggregate *eventstore.Aggregate,
info *AuthRequestInfo,
) *HumanOTPEmailCheckSucceededEvent {
return &HumanOTPEmailCheckSucceededEvent{
BaseEvent: *eventstore.NewBaseEventForPush(
ctx,
aggregate,
HumanOTPEmailCheckSucceededType,
),
AuthRequestInfo: info,
}
}
type HumanOTPEmailCheckFailedEvent struct {
eventstore.BaseEvent `json:"-"`
*AuthRequestInfo
}
func (e *HumanOTPEmailCheckFailedEvent) Data() interface{} {
return e
}
func (e *HumanOTPEmailCheckFailedEvent) UniqueConstraints() []*eventstore.EventUniqueConstraint {
return nil
}
func (e *HumanOTPEmailCheckFailedEvent) SetBaseEvent(event *eventstore.BaseEvent) {
e.BaseEvent = *event
}
func NewHumanOTPEmailCheckFailedEvent(
ctx context.Context,
aggregate *eventstore.Aggregate,
info *AuthRequestInfo,
) *HumanOTPEmailCheckFailedEvent {
return &HumanOTPEmailCheckFailedEvent{
BaseEvent: *eventstore.NewBaseEventForPush(
ctx,
aggregate,
HumanOTPEmailCheckFailedType,
),
AuthRequestInfo: info,
}
}

View File

@ -149,8 +149,9 @@ func HumanPhoneVerificationFailedEventMapper(event *repository.Event) (eventstor
type HumanPhoneCodeAddedEvent struct {
eventstore.BaseEvent `json:"-"`
Code *crypto.CryptoValue `json:"code,omitempty"`
Expiry time.Duration `json:"expiry,omitempty"`
Code *crypto.CryptoValue `json:"code,omitempty"`
Expiry time.Duration `json:"expiry,omitempty"`
CodeReturned bool `json:"code_returned,omitempty"`
}
func (e *HumanPhoneCodeAddedEvent) Data() interface{} {
@ -166,6 +167,15 @@ func NewHumanPhoneCodeAddedEvent(
aggregate *eventstore.Aggregate,
code *crypto.CryptoValue,
expiry time.Duration,
) *HumanPhoneCodeAddedEvent {
return NewHumanPhoneCodeAddedEventV2(ctx, aggregate, code, expiry, false)
}
func NewHumanPhoneCodeAddedEventV2(
ctx context.Context,
aggregate *eventstore.Aggregate,
code *crypto.CryptoValue,
expiry time.Duration,
codeReturned bool,
) *HumanPhoneCodeAddedEvent {
return &HumanPhoneCodeAddedEvent{
BaseEvent: *eventstore.NewBaseEventForPush(
@ -173,8 +183,9 @@ func NewHumanPhoneCodeAddedEvent(
aggregate,
HumanPhoneCodeAddedType,
),
Code: code,
Expiry: expiry,
Code: code,
Expiry: expiry,
CodeReturned: codeReturned,
}
}

Some files were not shown because too many files have changed in this diff Show More