From cdfdc693411bbfae766972df2ef21273370f0f24 Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Fri, 13 Aug 2021 09:05:05 +0200 Subject: [PATCH] feat(console): advanced passwordless api (#2183) * feat: console add passwordless * fix: token name Co-authored-by: Livio Amstutz --- .../auth-passwordless.component.ts | 175 +++++++++--------- .../dialog-passwordless.component.html | 66 +++++++ .../dialog-passwordless.component.scss | 43 +++++ .../dialog-passwordless.component.ts | 125 +++++++++++++ .../auth-user-detail.component.html | 10 +- .../users/user-detail/user-detail.module.ts | 4 + .../passwordless/passwordless.component.html | 7 + .../passwordless/passwordless.component.scss | 20 +- .../passwordless/passwordless.component.ts | 120 ++++++------ .../user-detail/user-detail.component.html | 38 ++-- console/src/app/services/grpc-auth.service.ts | 14 ++ console/src/app/services/mgmt.service.ts | 8 + console/src/assets/i18n/de.json | 16 +- console/src/assets/i18n/en.json | 16 +- .../eventsourcing/eventstore/auth_request.go | 2 +- 15 files changed, 491 insertions(+), 173 deletions(-) create mode 100644 console/src/app/pages/users/user-detail/auth-user-detail/auth-passwordless/dialog-passwordless/dialog-passwordless.component.html create mode 100644 console/src/app/pages/users/user-detail/auth-user-detail/auth-passwordless/dialog-passwordless/dialog-passwordless.component.scss create mode 100644 console/src/app/pages/users/user-detail/auth-user-detail/auth-passwordless/dialog-passwordless/dialog-passwordless.component.ts diff --git a/console/src/app/pages/users/user-detail/auth-user-detail/auth-passwordless/auth-passwordless.component.ts b/console/src/app/pages/users/user-detail/auth-user-detail/auth-passwordless/auth-passwordless.component.ts index 1b2ad01f04..fa480f36e9 100644 --- a/console/src/app/pages/users/user-detail/auth-user-detail/auth-passwordless/auth-passwordless.component.ts +++ b/console/src/app/pages/users/user-detail/auth-user-detail/auth-passwordless/auth-passwordless.component.ts @@ -9,108 +9,109 @@ import { GrpcAuthService } from 'src/app/services/grpc-auth.service'; import { ToastService } from 'src/app/services/toast.service'; import { _base64ToArrayBuffer } from '../../u2f-util'; -import { DialogU2FComponent, U2FComponentDestination } from '../dialog-u2f/dialog-u2f.component'; +import { U2FComponentDestination } from '../dialog-u2f/dialog-u2f.component'; +import { DialogPasswordlessComponent } from './dialog-passwordless/dialog-passwordless.component'; export interface WebAuthNOptions { - challenge: string; - rp: { name: string, id: string; }; - user: { name: string, id: string, displayName: string; }; - pubKeyCredParams: any; - authenticatorSelection: { userVerification: string; }; - timeout: number; - attestation: string; + challenge: string; + rp: { name: string, id: string; }; + user: { name: string, id: string, displayName: string; }; + pubKeyCredParams: any; + authenticatorSelection: { userVerification: string; }; + timeout: number; + attestation: string; } @Component({ - selector: 'app-auth-passwordless', - templateUrl: './auth-passwordless.component.html', - styleUrls: ['./auth-passwordless.component.scss'], + selector: 'app-auth-passwordless', + templateUrl: './auth-passwordless.component.html', + styleUrls: ['./auth-passwordless.component.scss'], }) export class AuthPasswordlessComponent implements OnInit, OnDestroy { - public displayedColumns: string[] = ['name', 'state', 'actions']; - private loadingSubject: BehaviorSubject = new BehaviorSubject(false); - public loading$: Observable = this.loadingSubject.asObservable(); + public displayedColumns: string[] = ['name', 'state', 'actions']; + private loadingSubject: BehaviorSubject = new BehaviorSubject(false); + public loading$: Observable = this.loadingSubject.asObservable(); - @ViewChild(MatTable) public table!: MatTable; - @ViewChild(MatSort) public sort!: MatSort; - public dataSource!: MatTableDataSource; + @ViewChild(MatTable) public table!: MatTable; + @ViewChild(MatSort) public sort!: MatSort; + public dataSource!: MatTableDataSource; - public AuthFactorState: any = AuthFactorState; - public error: string = ''; + public AuthFactorState: any = AuthFactorState; + public error: string = ''; - constructor(private service: GrpcAuthService, - private toast: ToastService, - private dialog: MatDialog) { } + constructor(private service: GrpcAuthService, + private toast: ToastService, + private dialog: MatDialog) { } - public ngOnInit(): void { - this.getPasswordless(); - } + public ngOnInit(): void { + this.getPasswordless(); + } - public ngOnDestroy(): void { - this.loadingSubject.complete(); - } + public ngOnDestroy(): void { + this.loadingSubject.complete(); + } - public addPasswordless(): void { - this.service.addMyPasswordless().then((resp) => { - if (resp.key) { - const credOptions: CredentialCreationOptions = JSON.parse(atob(resp.key.publicKey as string)); + public addPasswordless(): void { + this.service.addMyPasswordless().then((resp) => { + if (resp.key) { + const credOptions: CredentialCreationOptions = JSON.parse(atob(resp.key.publicKey as string)); - if (credOptions.publicKey?.challenge) { - credOptions.publicKey.challenge = _base64ToArrayBuffer(credOptions.publicKey.challenge as any); - credOptions.publicKey.user.id = _base64ToArrayBuffer(credOptions.publicKey.user.id as any); - if (credOptions.publicKey.excludeCredentials) { - credOptions.publicKey.excludeCredentials.map(cred => { - cred.id = _base64ToArrayBuffer(cred.id as any); - return cred; - }); - } - const dialogRef = this.dialog.open(DialogU2FComponent, { - width: '400px', - data: { - credOptions, - type: U2FComponentDestination.PASSWORDLESS, - }, - }); - - dialogRef.afterClosed().subscribe(done => { - this.getPasswordless(); - }); - } - } - }, error => { - this.toast.showError(error); - }); - } - - public getPasswordless(): void { - this.service.listMyPasswordless().then(passwordless => { - this.dataSource = new MatTableDataSource(passwordless.resultList); - this.dataSource.sort = this.sort; - }).catch(error => { - this.error = error.message; - }); - } - - public deletePasswordless(id?: string): void { - const dialogRef = this.dialog.open(WarnDialogComponent, { - data: { - confirmKey: 'ACTIONS.DELETE', - cancelKey: 'ACTIONS.CANCEL', - titleKey: 'USER.PASSWORDLESS.DIALOG.DELETE_TITLE', - descriptionKey: 'USER.PASSWORDLESS.DIALOG.DELETE_DESCRIPTION', - }, + if (credOptions.publicKey?.challenge) { + credOptions.publicKey.challenge = _base64ToArrayBuffer(credOptions.publicKey.challenge as any); + credOptions.publicKey.user.id = _base64ToArrayBuffer(credOptions.publicKey.user.id as any); + if (credOptions.publicKey.excludeCredentials) { + credOptions.publicKey.excludeCredentials.map(cred => { + cred.id = _base64ToArrayBuffer(cred.id as any); + return cred; + }); + } + const dialogRef = this.dialog.open(DialogPasswordlessComponent, { width: '400px', - }); + data: { + credOptions, + type: U2FComponentDestination.PASSWORDLESS, + }, + }); - dialogRef.afterClosed().subscribe(resp => { - if (resp && id) { - this.service.removeMyPasswordless(id).then(() => { - this.toast.showInfo('USER.TOAST.PASSWORDLESSREMOVED', true); - this.getPasswordless(); - }).catch(error => { - this.toast.showError(error); - }); - } + dialogRef.afterClosed().subscribe(done => { + this.getPasswordless(); + }); + } + } + }, error => { + this.toast.showError(error); + }); + } + + public getPasswordless(): void { + this.service.listMyPasswordless().then(passwordless => { + this.dataSource = new MatTableDataSource(passwordless.resultList); + this.dataSource.sort = this.sort; + }).catch(error => { + this.error = error.message; + }); + } + + public deletePasswordless(id?: string): void { + const dialogRef = this.dialog.open(WarnDialogComponent, { + data: { + confirmKey: 'ACTIONS.DELETE', + cancelKey: 'ACTIONS.CANCEL', + titleKey: 'USER.PASSWORDLESS.DIALOG.DELETE_TITLE', + descriptionKey: 'USER.PASSWORDLESS.DIALOG.DELETE_DESCRIPTION', + }, + width: '400px', + }); + + dialogRef.afterClosed().subscribe(resp => { + if (resp && id) { + this.service.removeMyPasswordless(id).then(() => { + this.toast.showInfo('USER.TOAST.PASSWORDLESSREMOVED', true); + this.getPasswordless(); + }).catch(error => { + this.toast.showError(error); }); - } + } + }); + } } diff --git a/console/src/app/pages/users/user-detail/auth-user-detail/auth-passwordless/dialog-passwordless/dialog-passwordless.component.html b/console/src/app/pages/users/user-detail/auth-user-detail/auth-passwordless/dialog-passwordless/dialog-passwordless.component.html new file mode 100644 index 0000000000..c2acfc07c6 --- /dev/null +++ b/console/src/app/pages/users/user-detail/auth-user-detail/auth-passwordless/dialog-passwordless/dialog-passwordless.component.html @@ -0,0 +1,66 @@ +

{{'USER.PASSWORDLESS.DIALOG.ADD_TITLE' | translate}}

+
+
+

{{'USER.PASSWORDLESS.DIALOG.ADD_DESCRIPTION' | translate}}

+ +
+ +

{{'USER.PASSWORDLESS.DIALOG.NEW_DESCRIPTION' | translate}}

+
+ + + {{'USER.MFA.U2F_NAME' | translate}} + + + + + +

{{error}}

+ + +
+ +

{{'USER.PASSWORDLESS.DIALOG.SEND_DESCRIPTION' | translate}}

+
+ + + + +
+ +

{{'USER.PASSWORDLESS.DIALOG.QRCODE_DESCRIPTION' | translate}}

+
+ + + +
+ +
+ +

{{'USER.PASSWORDLESS.DIALOG.SENT' | translate}}

+
+ +
+ +

{{'USER.PASSWORDLESS.DIALOG.QRCODE_SCAN' | translate}}

+ +
+ +
+
+ + +
+
+ +
\ No newline at end of file diff --git a/console/src/app/pages/users/user-detail/auth-user-detail/auth-passwordless/dialog-passwordless/dialog-passwordless.component.scss b/console/src/app/pages/users/user-detail/auth-user-detail/auth-passwordless/dialog-passwordless/dialog-passwordless.component.scss new file mode 100644 index 0000000000..96b899a52d --- /dev/null +++ b/console/src/app/pages/users/user-detail/auth-user-detail/auth-passwordless/dialog-passwordless/dialog-passwordless.component.scss @@ -0,0 +1,43 @@ +.qrcode-wrapper { + display: flex; + align-content: center; + + .qrcode { + margin: 1rem auto; + } +} + +.form-field { + width: 100%; + margin: 0; +} + +.desc { + display: flex; + align-items: center; + padding-top: 1rem; + + i { + margin-right: 1rem; + } + + p { + color: var(--grey); + font-size: 14px; + } +} + +.error { + color: #f44336; + font-size: 14px; +} + +.action { + display: flex; + justify-content: flex-end; + + button { + margin-left: .5rem; + border-radius: .5rem; + } +} diff --git a/console/src/app/pages/users/user-detail/auth-user-detail/auth-passwordless/dialog-passwordless/dialog-passwordless.component.ts b/console/src/app/pages/users/user-detail/auth-user-detail/auth-passwordless/dialog-passwordless/dialog-passwordless.component.ts new file mode 100644 index 0000000000..d1994f7500 --- /dev/null +++ b/console/src/app/pages/users/user-detail/auth-user-detail/auth-passwordless/dialog-passwordless/dialog-passwordless.component.ts @@ -0,0 +1,125 @@ +import { Component, Inject } from '@angular/core'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { TranslateService } from '@ngx-translate/core'; +import { take } from 'rxjs/operators'; + +import { GrpcAuthService } from '../../../../../../services/grpc-auth.service'; +import { ToastService } from '../../../../../../services/toast.service'; +import { _arrayBufferToBase64 } from '../../u2f_util'; + +export enum U2FComponentDestination { + MFA = 'mfa', + PASSWORDLESS = 'passwordless', +} + +@Component({ + selector: 'app-dialog-passwordless', + templateUrl: './dialog-passwordless.component.html', + styleUrls: ['./dialog-passwordless.component.scss'], +}) +export class DialogPasswordlessComponent { + private type!: U2FComponentDestination; + public name: string = ''; + public error: string = ''; + public loading: boolean = false; + + public showSent: boolean = false; + public showQR: boolean = false; + public qrcodeLink: string = ''; + + constructor(public dialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) public data: { credOptions: any; type: U2FComponentDestination; }, + private service: GrpcAuthService, private translate: TranslateService, private toast: ToastService) { + this.type = data.type; + } + + public closeDialog(): void { + this.dialogRef.close(); + } + + public closeDialogWithCode(): void { + this.error = ''; + this.loading = true; + if (this.name && this.data.credOptions.publicKey) { + // this.data.credOptions.publicKey.rp.id = 'localhost'; + navigator.credentials.create(this.data.credOptions).then((resp) => { + if (resp && + (resp as any).response.attestationObject && + (resp as any).response.clientDataJSON && + (resp as any).rawId) { + + const attestationObject = (resp as any).response.attestationObject; + const clientDataJSON = (resp as any).response.clientDataJSON; + const rawId = (resp as any).rawId; + + const data = JSON.stringify({ + id: resp.id, + rawId: _arrayBufferToBase64(rawId), + type: resp.type, + response: { + attestationObject: _arrayBufferToBase64(attestationObject), + clientDataJSON: _arrayBufferToBase64(clientDataJSON), + }, + }); + + const base64 = btoa(data); + if (this.type === U2FComponentDestination.MFA) { + this.service.verifyMyMultiFactorU2F(base64, this.name).then(() => { + this.translate.get('USER.MFA.U2F_SUCCESS').pipe(take(1)).subscribe(msg => { + this.toast.showInfo(msg); + }); + this.dialogRef.close(true); + this.loading = false; + }).catch(error => { + this.loading = false; + this.toast.showError(error); + }); + } else if (this.type === U2FComponentDestination.PASSWORDLESS) { + this.service.verifyMyPasswordless(base64, this.name).then(() => { + this.translate.get('USER.PASSWORDLESS.U2F_SUCCESS').pipe(take(1)).subscribe(msg => { + this.toast.showInfo(msg); + }); + this.dialogRef.close(true); + this.loading = false; + }).catch(error => { + this.loading = false; + this.toast.showError(error); + }); + } + } else { + this.loading = false; + this.translate.get('USER.MFA.U2F_ERROR').pipe(take(1)).subscribe(msg => { + this.toast.showInfo(msg); + }); + this.dialogRef.close(true); + } + }).catch(error => { + this.loading = false; + this.error = error; + this.toast.showInfo(error.message); + }); + } + + } + + public sendMyPasswordlessLink(): void { + this.service.sendMyPasswordlessLink().then(() => { + this.toast.showInfo('USER.TOAST.PASSWORDLESSREGISTRATIONSENT'); + this.showSent = true; + }).catch(error => { + this.toast.showError(error); + }); + } + + public addMyPasswordlessLink(): void { + this.service.addMyPasswordlessLink().then((resp) => { + console.log(resp); + this.showQR = true; + + this.qrcodeLink = resp.link; + + }).catch(error => { + this.toast.showError(error); + }); + } +} diff --git a/console/src/app/pages/users/user-detail/auth-user-detail/auth-user-detail.component.html b/console/src/app/pages/users/user-detail/auth-user-detail/auth-user-detail.component.html index 8583345c71..be16af541a 100644 --- a/console/src/app/pages/users/user-detail/auth-user-detail/auth-user-detail.component.html +++ b/console/src/app/pages/users/user-detail/auth-user-detail/auth-user-detail.component.html @@ -33,11 +33,6 @@ - - - - +
diff --git a/console/src/app/pages/users/user-detail/user-detail/passwordless/passwordless.component.scss b/console/src/app/pages/users/user-detail/user-detail/passwordless/passwordless.component.scss index 0c317899c6..5cb25ad590 100644 --- a/console/src/app/pages/users/user-detail/user-detail/passwordless/passwordless.component.scss +++ b/console/src/app/pages/users/user-detail/user-detail/passwordless/passwordless.component.scss @@ -1,4 +1,3 @@ - .centered { display: flex; align-items: center; @@ -24,3 +23,22 @@ } } } + +.add-row { + display: flex; + margin: -.5rem; + flex-wrap: wrap; + justify-content: flex-end; + + .button { + margin: .5rem; + margin-top: 1rem; + display: flex; + align-items: center; + + .icon { + margin-left: .5rem; + margin-bottom: 2px; + } + } +} diff --git a/console/src/app/pages/users/user-detail/user-detail/passwordless/passwordless.component.ts b/console/src/app/pages/users/user-detail/user-detail/passwordless/passwordless.component.ts index 35dc13587b..870e8b6e15 100644 --- a/console/src/app/pages/users/user-detail/user-detail/passwordless/passwordless.component.ts +++ b/console/src/app/pages/users/user-detail/user-detail/passwordless/passwordless.component.ts @@ -9,74 +9,82 @@ import { ManagementService } from 'src/app/services/mgmt.service'; import { ToastService } from 'src/app/services/toast.service'; export interface WebAuthNOptions { - challenge: string; - rp: { name: string, id: string; }; - user: { name: string, id: string, displayName: string; }; - pubKeyCredParams: any; - authenticatorSelection: { userVerification: string; }; - timeout: number; - attestation: string; + challenge: string; + rp: { name: string, id: string; }; + user: { name: string, id: string, displayName: string; }; + pubKeyCredParams: any; + authenticatorSelection: { userVerification: string; }; + timeout: number; + attestation: string; } @Component({ - selector: 'app-passwordless', - templateUrl: './passwordless.component.html', - styleUrls: ['./passwordless.component.scss'], + selector: 'app-passwordless', + templateUrl: './passwordless.component.html', + styleUrls: ['./passwordless.component.scss'], }) export class PasswordlessComponent implements OnInit, OnDestroy { - @Input() private user!: User.AsObject; - public displayedColumns: string[] = ['name', 'state', 'actions']; - private loadingSubject: BehaviorSubject = new BehaviorSubject(false); - public loading$: Observable = this.loadingSubject.asObservable(); + @Input() private user!: User.AsObject; + public displayedColumns: string[] = ['name', 'state', 'actions']; + private loadingSubject: BehaviorSubject = new BehaviorSubject(false); + public loading$: Observable = this.loadingSubject.asObservable(); - @ViewChild(MatTable) public table!: MatTable; - @ViewChild(MatSort) public sort!: MatSort; - public dataSource!: MatTableDataSource; + @ViewChild(MatTable) public table!: MatTable; + @ViewChild(MatSort) public sort!: MatSort; + public dataSource!: MatTableDataSource; - public AuthFactorState: any = AuthFactorState; - public error: string = ''; + public AuthFactorState: any = AuthFactorState; + public error: string = ''; - constructor(private service: ManagementService, - private toast: ToastService, - private dialog: MatDialog) { } + constructor(private service: ManagementService, + private toast: ToastService, + private dialog: MatDialog) { } - public ngOnInit(): void { - this.getPasswordless(); - } + public ngOnInit(): void { + this.getPasswordless(); + } - public ngOnDestroy(): void { - this.loadingSubject.complete(); - } + public ngOnDestroy(): void { + this.loadingSubject.complete(); + } - public getPasswordless(): void { - this.service.listHumanPasswordless(this.user.id).then(passwordless => { - this.dataSource = new MatTableDataSource(passwordless.resultList); - this.dataSource.sort = this.sort; + public getPasswordless(): void { + this.service.listHumanPasswordless(this.user.id).then(passwordless => { + this.dataSource = new MatTableDataSource(passwordless.resultList); + this.dataSource.sort = this.sort; + }).catch(error => { + this.error = error.message; + }); + } + + public deletePasswordless(id?: string): void { + const dialogRef = this.dialog.open(WarnDialogComponent, { + data: { + confirmKey: 'ACTIONS.DELETE', + cancelKey: 'ACTIONS.CANCEL', + titleKey: 'USER.PASSWORDLESS.DIALOG.DELETE_TITLE', + descriptionKey: 'USER.PASSWORDLESS.DIALOG.DELETE_DESCRIPTION', + }, + width: '400px', + }); + + dialogRef.afterClosed().subscribe(resp => { + if (resp && id) { + this.service.removeHumanPasswordless(id, this.user.id).then(() => { + this.toast.showInfo('USER.TOAST.PASSWORDLESSREMOVED', true); + this.getPasswordless(); }).catch(error => { - this.error = error.message; + this.toast.showError(error); }); - } + } + }); + } - public deletePasswordless(id?: string): void { - const dialogRef = this.dialog.open(WarnDialogComponent, { - data: { - confirmKey: 'ACTIONS.DELETE', - cancelKey: 'ACTIONS.CANCEL', - titleKey: 'USER.PASSWORDLESS.DIALOG.DELETE_TITLE', - descriptionKey: 'USER.PASSWORDLESS.DIALOG.DELETE_DESCRIPTION', - }, - width: '400px', - }); - - dialogRef.afterClosed().subscribe(resp => { - if (resp && id) { - this.service.removeHumanPasswordless(id, this.user.id).then(() => { - this.toast.showInfo('USER.TOAST.PASSWORDLESSREMOVED', true); - this.getPasswordless(); - }).catch(error => { - this.toast.showError(error); - }); - } - }); - } + public sendPasswordlessRegistration(): void { + this.service.sendPasswordlessRegistration(this.user.id).then(() => { + this.toast.showInfo('USER.TOAST.PASSWORDLESSREGISTRATIONSENT'); + }).catch(error => { + this.toast.showError(error); + }); + } } diff --git a/console/src/app/pages/users/user-detail/user-detail/user-detail.component.html b/console/src/app/pages/users/user-detail/user-detail/user-detail.component.html index acbaffd164..9ab9f17526 100644 --- a/console/src/app/pages/users/user-detail/user-detail/user-detail.component.html +++ b/console/src/app/pages/users/user-detail/user-detail/user-detail.component.html @@ -55,6 +55,25 @@ + + + + + + + + @@ -72,25 +91,6 @@ - - - - - - - - diff --git a/console/src/app/services/grpc-auth.service.ts b/console/src/app/services/grpc-auth.service.ts index 637b1ef3d1..70c3504571 100644 --- a/console/src/app/services/grpc-auth.service.ts +++ b/console/src/app/services/grpc-auth.service.ts @@ -8,6 +8,8 @@ import { AddMyAuthFactorOTPResponse, AddMyAuthFactorU2FRequest, AddMyAuthFactorU2FResponse, + AddMyPasswordlessLinkRequest, + AddMyPasswordlessLinkResponse, AddMyPasswordlessRequest, AddMyPasswordlessResponse, GetMyEmailRequest, @@ -54,6 +56,8 @@ import { ResendMyEmailVerificationResponse, ResendMyPhoneVerificationRequest, ResendMyPhoneVerificationResponse, + SendMyPasswordlessLinkRequest, + SendMyPasswordlessLinkResponse, SetMyEmailRequest, SetMyEmailResponse, SetMyPhoneRequest, @@ -493,6 +497,16 @@ export class GrpcAuthService { ).then(resp => resp.toObject()); } + public sendMyPasswordlessLink(): Promise { + const req = new SendMyPasswordlessLinkRequest(); + return this.grpcService.auth.sendMyPasswordlessLink(req, null).then(resp => resp.toObject()); + } + + public addMyPasswordlessLink(): Promise { + const req = new AddMyPasswordlessLinkRequest(); + return this.grpcService.auth.addMyPasswordlessLink(req, null).then(resp => resp.toObject()); + } + public removeMyMultiFactorOTP(): Promise { return this.grpcService.auth.removeMyAuthFactorOTP( new RemoveMyAuthFactorOTPRequest(), null, diff --git a/console/src/app/services/mgmt.service.ts b/console/src/app/services/mgmt.service.ts index 23d75439e4..57406bbb4e 100644 --- a/console/src/app/services/mgmt.service.ts +++ b/console/src/app/services/mgmt.service.ts @@ -309,6 +309,8 @@ import { ResetPrivacyPolicyToDefaultRequest, ResetPrivacyPolicyToDefaultResponse, SendHumanResetPasswordNotificationRequest, + SendPasswordlessRegistrationRequest, + SendPasswordlessRegistrationResponse, SetCustomDomainClaimedMessageTextRequest, SetCustomDomainClaimedMessageTextResponse, SetCustomInitMessageTextRequest, @@ -598,6 +600,12 @@ export class ManagementService { return this.grpcService.mgmt.removeHumanPasswordless(req, null).then(resp => resp.toObject()); } + public sendPasswordlessRegistration(userId: string): Promise { + const req = new SendPasswordlessRegistrationRequest(); + req.setUserId(userId); + return this.grpcService.mgmt.sendPasswordlessRegistration(req, null).then(resp => resp.toObject()); + } + public listLoginPolicyMultiFactors(): Promise { const req = new ListLoginPolicyMultiFactorsRequest(); return this.grpcService.mgmt.listLoginPolicyMultiFactors(req, null).then(resp => resp.toObject()); diff --git a/console/src/assets/i18n/de.json b/console/src/assets/i18n/de.json index 5f00a9c63e..7dcf525ab1 100644 --- a/console/src/assets/i18n/de.json +++ b/console/src/assets/i18n/de.json @@ -203,6 +203,7 @@ "EMPTY": "Keine Einträge" }, "PASSWORDLESS": { + "SEND":"Registrierungslink senden", "TABLETYPE": "Typ", "TABLESTATE": "Status", "NAME": "Name", @@ -229,7 +230,17 @@ }, "DIALOG": { "DELETE_TITLE": "Passwordless entfernen", - "DELETE_DESCRIPTION": "Sie sind im Begriff eine Passwortlose Authentifizierungsmethode zu entfernen. Sind sie sicher?" + "DELETE_DESCRIPTION": "Sie sind im Begriff eine Passwortlose Authentifizierungsmethode zu entfernen. Sind sie sicher?", + "ADD_TITLE":"Passwortlose Authentifizierung", + "ADD_DESCRIPTION":"Wählen Sie eine der verfügbaren Optionen für das Erstellen einer passwordlosen Authentifizierungsmethode.", + "SEND_DESCRIPTION":"Senden Sie sich einen Registrierungslink an Ihre email adresse.", + "SEND":"Registrierungslink senden", + "SENT":"Die email wurde erfolgreich zugestellt. Kontrollieren Sie Ihr Postfach um mit dem Setup forzufahren.", + "QRCODE_DESCRIPTION":"QR-Code zum scannen mit einem anderen Gerät generieren.", + "QRCODE":"QR-Code generieren", + "QRCODE_SCAN":"Scannen Sie diesen QR Code um mit dem Setup auf Ihrem Gerät forzufahren.", + "NEW_DESCRIPTION":"Verwenden Sie dieses Gerät um Passwordless aufzusetzen.", + "NEW":"Hinzufügen" } }, "MFA": { @@ -464,7 +475,8 @@ "KEYADDED": "Schlüssel hinzugefügt!", "MACHINEADDED": "Service User erstellt!", "DELETED": "Benutzer erfolgreich gelöscht!", - "UNLOCKED":"Benutzer erfolgreich freigeschaltet!" + "UNLOCKED":"Benutzer erfolgreich freigeschaltet!", + "PASSWORDLESSREGISTRATIONSENT":"Link via email versendet." }, "MEMBERSHIPS": { "TITLE": "ZITADEL Manager-Rollen", diff --git a/console/src/assets/i18n/en.json b/console/src/assets/i18n/en.json index d2a646d34e..bc18091ba2 100644 --- a/console/src/assets/i18n/en.json +++ b/console/src/assets/i18n/en.json @@ -203,6 +203,7 @@ "EMPTY": "No entries" }, "PASSWORDLESS": { + "SEND":"Send registration link", "TABLETYPE": "Type", "TABLESTATE": "Status", "NAME": "Name", @@ -229,7 +230,17 @@ }, "DIALOG": { "DELETE_TITLE": "Remove Passwordless Authentication Method", - "DELETE_DESCRIPTION": "You are about to delete a passwordless Authentication method. Are you sure?" + "DELETE_DESCRIPTION": "You are about to delete a passwordless Authentication method. Are you sure?", + "ADD_TITLE":"Passwordless Authentication", + "ADD_DESCRIPTION":"Select one of the available options for creating a passwordless authentication method.", + "SEND_DESCRIPTION":"Send yourself a registration link to your email address.", + "SEND":"Send registration link", + "SENT":"The email was successfully delivered. Check your mailbox to continue with the setup.", + "QRCODE_DESCRIPTION":"Generate QR code for scanning with another device.", + "QRCODE":"Generate QR code", + "QRCODE_SCAN":"Scan this QR code to continue with the setup on your device.", + "NEW_DESCRIPTION":"Use this device to set up Passwordless.", + "NEW":"Add New" } }, "MFA": { @@ -464,7 +475,8 @@ "KEYADDED": "Key added!", "MACHINEADDED": "Service User created!", "DELETED": "User deleted successfully!", - "UNLOCKED":"User unlocked successfully!" + "UNLOCKED":"User unlocked successfully!", + "PASSWORDLESSREGISTRATIONSENT":"Registration Link sent successfully." }, "MEMBERSHIPS": { "TITLE": "ZITADEL Manager Roles", diff --git a/internal/auth/repository/eventsourcing/eventstore/auth_request.go b/internal/auth/repository/eventsourcing/eventstore/auth_request.go index e5c6d1bfdf..0ac5e6c4c7 100644 --- a/internal/auth/repository/eventsourcing/eventstore/auth_request.go +++ b/internal/auth/repository/eventsourcing/eventstore/auth_request.go @@ -342,7 +342,7 @@ func (repo *AuthRequestRepo) BeginPasswordlessInitCodeSetup(ctx context.Context, func (repo *AuthRequestRepo) VerifyPasswordlessInitCodeSetup(ctx context.Context, userID, resourceOwner, userAgentID, tokenName, codeID, verificationCode string, credentialData []byte) (err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - _, err = repo.Command.HumanPasswordlessSetupInitCode(ctx, userID, resourceOwner, userAgentID, tokenName, codeID, verificationCode, credentialData) + _, err = repo.Command.HumanPasswordlessSetupInitCode(ctx, userID, resourceOwner, tokenName, userAgentID, codeID, verificationCode, credentialData) return err }