feat(console): advanced passwordless api (#2183)

* feat: console add passwordless

* fix: token name

Co-authored-by: Livio Amstutz <livio.a@gmail.com>
This commit is contained in:
Max Peintner 2021-08-13 09:05:05 +02:00 committed by GitHub
parent 999ac8d508
commit cdfdc69341
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 491 additions and 173 deletions

View File

@ -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<boolean> = new BehaviorSubject<boolean>(false);
public loading$: Observable<boolean> = this.loadingSubject.asObservable();
public displayedColumns: string[] = ['name', 'state', 'actions'];
private loadingSubject: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
public loading$: Observable<boolean> = this.loadingSubject.asObservable();
@ViewChild(MatTable) public table!: MatTable<WebAuthNToken.AsObject>;
@ViewChild(MatSort) public sort!: MatSort;
public dataSource!: MatTableDataSource<WebAuthNToken.AsObject>;
@ViewChild(MatTable) public table!: MatTable<WebAuthNToken.AsObject>;
@ViewChild(MatSort) public sort!: MatSort;
public dataSource!: MatTableDataSource<WebAuthNToken.AsObject>;
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);
});
}
}
});
}
}

View File

@ -0,0 +1,66 @@
<h1 mat-dialog-title>{{'USER.PASSWORDLESS.DIALOG.ADD_TITLE' | translate}}</h1>
<div mat-dialog-content>
<div *ngIf="!showSent && !showQR">
<p>{{'USER.PASSWORDLESS.DIALOG.ADD_DESCRIPTION' | translate}}</p>
<div class="desc">
<i class="icon las la-plus-circle"></i>
<p>{{'USER.PASSWORDLESS.DIALOG.NEW_DESCRIPTION' | translate}}</p>
</div>
<cnsl-form-field class="form-field" label="Name" required="true">
<cnsl-label>{{'USER.MFA.U2F_NAME' | translate}}</cnsl-label>
<input cnslInput [(ngModel)]="name" required (keydown.enter)="name ? closeDialogWithCode() : null" />
</cnsl-form-field>
<button [disabled]="!name" mat-raised-button class="ok-button" color="primary"
(click)="closeDialogWithCode()">{{'USER.PASSWORDLESS.DIALOG.NEW' | translate}}
</button>
<p class="error">{{error}}</p>
<div class="desc">
<i class="icon las la-paper-plane"></i>
<p>{{'USER.PASSWORDLESS.DIALOG.SEND_DESCRIPTION' | translate}}</p>
</div>
<button mat-raised-button class="send-button" color="primary"
(click)="sendMyPasswordlessLink()">{{'USER.PASSWORDLESS.DIALOG.SEND' | translate}}
</button>
<div class="desc">
<i class="icon las la-qrcode"></i>
<p>{{'USER.PASSWORDLESS.DIALOG.QRCODE_DESCRIPTION' | translate}}</p>
</div>
<button mat-raised-button class="qr-button" color="primary"
(click)="addMyPasswordlessLink()">{{'USER.PASSWORDLESS.DIALOG.QRCODE' | translate}}
</button>
</div>
<div *ngIf="showSent">
<button mat-icon-button (click)="showSent = false">
<i class="las la-arrow-left"></i>
</button>
<p>{{'USER.PASSWORDLESS.DIALOG.SENT' | translate}}</p>
</div>
<div *ngIf="showQR">
<button mat-icon-button (click)="showQR = false">
<i class="las la-arrow-left"></i>
</button>
<p>{{'USER.PASSWORDLESS.DIALOG.QRCODE_SCAN' | translate}}</p>
<div class="qrcode-wrapper">
<qrcode *ngIf="qrcodeLink" class="qrcode" [qrdata]="qrcodeLink" [width]="150" [errorCorrectionLevel]="'M'"></qrcode>
</div>
</div>
<mat-spinner diameter="30" *ngIf="loading"></mat-spinner>
</div>
<div mat-dialog-actions class="action">
<button mat-button (click)="closeDialog()">{{'ACTIONS.CLOSE' | translate}}</button>
</div>

View File

@ -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;
}
}

View File

@ -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<DialogPasswordlessComponent>,
@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);
});
}
}

View File

@ -33,11 +33,6 @@
</app-detail-form>
</app-card>
<app-card *ngIf="user && user.human && user.id" title="{{ 'USER.EXTERNALIDP.TITLE' | translate }}"
description="{{ 'USER.EXTERNALIDP.DESC' | translate }}">
<app-external-idps [userId]="user.id" [service]="userService"></app-external-idps>
</app-card>
<app-card *ngIf="user" title="{{ 'USER.LOGINMETHODS.TITLE' | translate }}"
description="{{ 'USER.LOGINMETHODS.DESCRIPTION' | translate }}">
<button card-actions mat-icon-button (click)="refreshUser()" matTooltip="{{'ACTIONS.REFRESH' | translate}}">
@ -50,6 +45,11 @@
</app-contact>
</app-card>
<app-card *ngIf="user && user.human && user.id" title="{{ 'USER.EXTERNALIDP.TITLE' | translate }}"
description="{{ 'USER.EXTERNALIDP.DESC' | translate }}">
<app-external-idps [userId]="user.id" [service]="userService"></app-external-idps>
</app-card>
<app-auth-passwordless *ngIf="user" #mfaComponent></app-auth-passwordless>
<app-auth-user-mfa *ngIf="user" #mfaComponent></app-auth-user-mfa>

View File

@ -33,6 +33,9 @@ import { TimestampToDatePipeModule } from 'src/app/pipes/timestamp-to-date-pipe/
import { AuthFactorDialogComponent } from './auth-user-detail/auth-factor-dialog/auth-factor-dialog.component';
import { AuthPasswordlessComponent } from './auth-user-detail/auth-passwordless/auth-passwordless.component';
import {
DialogPasswordlessComponent,
} from './auth-user-detail/auth-passwordless/dialog-passwordless/dialog-passwordless.component';
import { AuthUserDetailComponent } from './auth-user-detail/auth-user-detail.component';
import { AuthUserMfaComponent } from './auth-user-detail/auth-user-mfa/auth-user-mfa.component';
import { CodeDialogComponent } from './auth-user-detail/code-dialog/code-dialog.component';
@ -68,6 +71,7 @@ import { UserMfaComponent } from './user-detail/user-mfa/user-mfa.component';
ContactComponent,
ResendEmailDialogComponent,
DialogU2FComponent,
DialogPasswordlessComponent,
AuthFactorDialogComponent,
],
imports: [

View File

@ -35,6 +35,13 @@
<tr class="highlight" mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>
</app-refresh-table>
<div class="add-row">
<button class="button" (click)="sendPasswordlessRegistration()" mat-raised-button color="primary"
matTooltip="{{'ACTIONS.NEW' | translate}}">
{{'USER.PASSWORDLESS.SEND' | translate}}
<i class="icon las la-paper-plane"></i>
</button>
</div>
<div class="table-wrapper">
<div class="spinner-container" *ngIf="loading$ | async">
<mat-spinner diameter="50"></mat-spinner>

View File

@ -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;
}
}
}

View File

@ -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<boolean> = new BehaviorSubject<boolean>(false);
public loading$: Observable<boolean> = this.loadingSubject.asObservable();
@Input() private user!: User.AsObject;
public displayedColumns: string[] = ['name', 'state', 'actions'];
private loadingSubject: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
public loading$: Observable<boolean> = this.loadingSubject.asObservable();
@ViewChild(MatTable) public table!: MatTable<WebAuthNToken.AsObject>;
@ViewChild(MatSort) public sort!: MatSort;
public dataSource!: MatTableDataSource<WebAuthNToken.AsObject>;
@ViewChild(MatTable) public table!: MatTable<WebAuthNToken.AsObject>;
@ViewChild(MatSort) public sort!: MatSort;
public dataSource!: MatTableDataSource<WebAuthNToken.AsObject>;
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);
});
}
}

View File

@ -55,6 +55,25 @@
</app-detail-form>
</app-card>
<app-card *ngIf="user.human" title="{{ 'USER.LOGINMETHODS.TITLE' | translate }}"
description="{{ 'USER.LOGINMETHODS.DESCRIPTION' | translate }}">
<button card-actions mat-icon-button (click)="refreshUser()" matTooltip="{{'ACTIONS.REFRESH' | translate}}">
<mat-icon>refresh</mat-icon>
</button>
<app-contact disablePhoneCode="true"
[canWrite]="(['user.write:' + user?.id, 'user.write$'] | hasRole | async)" *ngIf="user?.human"
[human]="user.human" (editType)="openEditDialog($event)" (deletedPhone)="deletePhone()"
(resendEmailVerification)="resendEmailVerification()"
(resendPhoneVerification)="resendPhoneVerification()">
<button pwdAction [disabled]="(canWrite$ | async) == false" (click)="sendSetPasswordNotification()"
mat-stroked-button color="primary" *ngIf="user.state === UserState.USER_STATE_INITIAL">{{
'USER.PASSWORD.RESENDNOTIFICATION' | translate }}</button>
<button emailAction class="resendemail" *ngIf="user.state == UserState.USER_STATE_INITIAL"
mat-stroked-button color="primary" (click)="resendInitEmail()">{{'USER.RESENDINITIALEMAIL' |
translate}}</button>
</app-contact>
</app-card>
<app-card *ngIf="user && user.human && user.id" title="{{ 'USER.EXTERNALIDP.TITLE' | translate }}"
description="{{ 'USER.EXTERNALIDP.DESC' | translate }}">
<app-external-idps [userId]="user.id" [service]="mgmtUserService"></app-external-idps>
@ -72,25 +91,6 @@
</app-card>
</ng-template>
<app-card *ngIf="user.human" title="{{ 'USER.LOGINMETHODS.TITLE' | translate }}"
description="{{ 'USER.LOGINMETHODS.DESCRIPTION' | translate }}">
<button card-actions mat-icon-button (click)="refreshUser()" matTooltip="{{'ACTIONS.REFRESH' | translate}}">
<mat-icon>refresh</mat-icon>
</button>
<app-contact disablePhoneCode="true"
[canWrite]="(['user.write:' + user?.id, 'user.write$'] | hasRole | async)" *ngIf="user?.human"
[human]="user.human" (editType)="openEditDialog($event)" (deletedPhone)="deletePhone()"
(resendEmailVerification)="resendEmailVerification()"
(resendPhoneVerification)="resendPhoneVerification()">
<button pwdAction [disabled]="(canWrite$ | async) == false" (click)="sendSetPasswordNotification()"
mat-stroked-button color="primary" *ngIf="user.state === UserState.USER_STATE_INITIAL">{{
'USER.PASSWORD.RESENDNOTIFICATION' | translate }}</button>
<button emailAction class="resendemail" *ngIf="user.state == UserState.USER_STATE_INITIAL"
mat-stroked-button color="primary" (click)="resendInitEmail()">{{'USER.RESENDINITIALEMAIL' |
translate}}</button>
</app-contact>
</app-card>
<app-passwordless *ngIf="user && user.human" [user]="user"></app-passwordless>
<app-user-mfa *ngIf="user && user.human" [user]="user"></app-user-mfa>

View File

@ -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<SendMyPasswordlessLinkResponse.AsObject> {
const req = new SendMyPasswordlessLinkRequest();
return this.grpcService.auth.sendMyPasswordlessLink(req, null).then(resp => resp.toObject());
}
public addMyPasswordlessLink(): Promise<AddMyPasswordlessLinkResponse.AsObject> {
const req = new AddMyPasswordlessLinkRequest();
return this.grpcService.auth.addMyPasswordlessLink(req, null).then(resp => resp.toObject());
}
public removeMyMultiFactorOTP(): Promise<RemoveMyAuthFactorOTPResponse.AsObject> {
return this.grpcService.auth.removeMyAuthFactorOTP(
new RemoveMyAuthFactorOTPRequest(), null,

View File

@ -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<SendPasswordlessRegistrationResponse.AsObject> {
const req = new SendPasswordlessRegistrationRequest();
req.setUserId(userId);
return this.grpcService.mgmt.sendPasswordlessRegistration(req, null).then(resp => resp.toObject());
}
public listLoginPolicyMultiFactors(): Promise<ListLoginPolicyMultiFactorsResponse.AsObject> {
const req = new ListLoginPolicyMultiFactorsRequest();
return this.grpcService.mgmt.listLoginPolicyMultiFactors(req, null).then(resp => resp.toObject());

View File

@ -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",

View File

@ -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",

View File

@ -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
}