mirror of
https://github.com/zitadel/zitadel.git
synced 2025-01-06 13:07:52 +00:00
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:
parent
999ac8d508
commit
cdfdc69341
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
@ -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>
|
||||
|
@ -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: [
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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,
|
||||
|
@ -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());
|
||||
|
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user