feat(console): u2f (#1080)

* fix user table count

* grpc ge

* move grpc

* u2f

* add u2f funcs

* rm local grpc, u2f dialog

* dialog u2f

* 2fa button

* mfa u2f credentialoptions

* decode base64 to bytearray, id, challenge

* u2f verify

* spinner, remove, attribute col

* delete mfa

* add forcemfa to policy

* add id to remove

* fix: add missing remove u2f in management

* user mgmt u2f delete, login policy

* rm log

* show attr in mgmt user mfa

* add missing id of mfa

* mfa table

* multifaktor for admin, org

* add secondfactor to gen component

* remove circular dependency

* lint

* revert identity prov

* add divider

* login policy lint

* Update console/src/app/modules/policies/login-policy/login-policy.component.html

* Update console/src/app/modules/policies/login-policy/login-policy.component.html

Co-authored-by: Maximilian Peintner <csaq7175@uibk.ac.at>
Co-authored-by: Livio Amstutz <livio.a@gmail.com>
This commit is contained in:
Max Peintner 2020-12-14 10:04:15 +01:00 committed by GitHub
parent cd44213e99
commit c6fed8ae86
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
33 changed files with 972 additions and 65 deletions

View File

@ -0,0 +1,19 @@
<h1 mat-dialog-title class="title"><span>{{data.title | translate}}</span></h1>
<div mat-dialog-content>
<p class="desc">{{data.desc | translate}}</p>
<cnsl-form-field class="form-field" label="Access Code" required="true">
<cnsl-label>{{'MFA.TYPE' | translate}}</cnsl-label>
<mat-select [(ngModel)]="newMfaType">
<mat-option *ngFor="let mfa of availableMfaTypes" [value]="mfa">
{{(data.componentType == LoginMethodComponentType.SecondFactor ? 'MFA.SECONDFACTORTYPES.': LoginMethodComponentType.MultiFactor ? 'MFA.MULTIFACTORTYPES.': '')+mfa | translate}}
</mat-option>
</mat-select>
</cnsl-form-field>
</div>
<div mat-dialog-actions class="action">
<button mat-button (click)="closeDialog()"><span translate>ACTIONS.CLOSE</span></button>
<button mat-raised-button class="ok-button" color="primary" (click)="closeDialogWithCode()"><span
translate>ACTIONS.OK</span>
</button>
</div>

View File

@ -0,0 +1,22 @@
.title {
font-size: 1.5rem;
}
.desc {
font-size: 14px;
color: var(--grey);
}
.form-field {
width: 100%;
}
.action {
display: flex;
justify-content: flex-end;
button {
margin-left: .5rem;
border-radius: .5rem;
}
}

View File

@ -0,0 +1,33 @@
import { Component, Inject } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { MultiFactorType as AdminMultiFactorType } from 'src/app/proto/generated/admin_pb';
import { MultiFactorType as MgmtMultiFactorType } from 'src/app/proto/generated/management_pb';
enum LoginMethodComponentType {
MultiFactor = 1,
SecondFactor = 2,
}
@Component({
selector: 'app-dialog-add-type',
templateUrl: './dialog-add-type.component.html',
styleUrls: ['./dialog-add-type.component.scss'],
})
export class DialogAddTypeComponent {
public LoginMethodComponentType: any = LoginMethodComponentType;
public newMfaType!: AdminMultiFactorType | MgmtMultiFactorType;
public availableMfaTypes: Array<AdminMultiFactorType | MgmtMultiFactorType> = [];
constructor(public dialogRef: MatDialogRef<DialogAddTypeComponent>,
@Inject(MAT_DIALOG_DATA) public data: any) {
this.availableMfaTypes = data.types;
}
public closeDialog(): void {
this.dialogRef.close();
}
public closeDialogWithCode(): void {
this.dialogRef.close(this.newMfaType);
}
}

View File

@ -0,0 +1,15 @@
<div class="sp_wrapper">
<mat-spinner diameter="30" *ngIf="loading$ | async"></mat-spinner>
</div>
<div class="mfa-list">
<div class="mfa" *ngFor="let mfa of mfas">
<button [disabled]="disabled" mat-icon-button (click)="removeMfa(mfa)" class="rm">
<mat-icon matTooltip="{{'ACTIONS.REMOVE' | translate}}">
remove_circle</mat-icon>
</button>
{{(componentType == LoginMethodComponentType.SecondFactor ? 'MFA.SECONDFACTORTYPES.': LoginMethodComponentType.MultiFactor ? 'MFA.MULTIFACTORTYPES.': '')+mfa | translate}}
</div>
<div class="mfa" (click)="addMfa()">
<mat-icon>add</mat-icon>
</div>
</div>

View File

@ -0,0 +1,42 @@
.t .sp_wrapper {
display: block;
}
.mfa-list {
display: flex;
flex-wrap: wrap;
margin: 0 -.5rem;
.mfa {
border: 1px solid var(--grey);
border-radius: .5rem;
display: grid;
align-items: center;
justify-content: center;
margin: .5rem;
padding: 10px;
cursor: pointer;
position: relative;
min-height: 70px;
min-width: 150px;
.rm {
position: absolute;
top: 0;
left: 0;
transform: translateX(-50%) translateY(-50%);
cursor: pointer;
&[disabled] {
display: none;
}
}
&:not(.disabled) {
&:hover {
background-color: #ffffff10;
}
}
}
}

View File

@ -0,0 +1,25 @@
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { MfaTableComponent } from './mfa-table.component';
describe('MfaTableComponent', () => {
let component: MfaTableComponent;
let fixture: ComponentFixture<MfaTableComponent>;
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [MfaTableComponent],
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(MfaTableComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,220 @@
import { Component, Input, OnInit, ViewChild } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { MatPaginator } from '@angular/material/paginator';
import { TranslateService } from '@ngx-translate/core';
import { BehaviorSubject, Observable } from 'rxjs';
import {
MultiFactor as AdminMultiFactor, MultiFactorType as AdminMultiFactorType,
SecondFactor as AdminSecondFactor, SecondFactorType as AdminSecondFactorType,
} from 'src/app/proto/generated/admin_pb';
import {
MultiFactor as MgmtMultiFactor, MultiFactorType as MgmtMultiFactorType,
SecondFactor as MgmtSecondFactor, SecondFactorType as MgmtSecondFactorType,
} from 'src/app/proto/generated/management_pb';
import { AdminService } from 'src/app/services/admin.service';
import { ManagementService } from 'src/app/services/mgmt.service';
import { ToastService } from 'src/app/services/toast.service';
import { PolicyComponentServiceType } from '../policies/policy-component-types.enum';
import { WarnDialogComponent } from '../warn-dialog/warn-dialog.component';
import { DialogAddTypeComponent } from './dialog-add-type/dialog-add-type.component';
export enum LoginMethodComponentType {
MultiFactor = 1,
SecondFactor = 2,
}
@Component({
selector: 'app-mfa-table',
templateUrl: './mfa-table.component.html',
styleUrls: ['./mfa-table.component.scss'],
})
export class MfaTableComponent implements OnInit {
public LoginMethodComponentType: any = LoginMethodComponentType;
@Input() componentType!: LoginMethodComponentType;
@Input() public serviceType!: PolicyComponentServiceType;
@Input() service!: AdminService | ManagementService;
@Input() disabled: boolean = false;
@ViewChild(MatPaginator) public paginator!: MatPaginator;
public mfas: Array<AdminMultiFactorType | MgmtMultiFactorType | MgmtSecondFactorType | AdminSecondFactorType> = [];
private loadingSubject: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
public loading$: Observable<boolean> = this.loadingSubject.asObservable();
public PolicyComponentServiceType: any = PolicyComponentServiceType;
constructor(public translate: TranslateService, private toast: ToastService, private dialog: MatDialog) { }
public ngOnInit(): void {
this.getData();
}
public removeMfa(type: MgmtMultiFactorType | AdminMultiFactorType | MgmtSecondFactorType | AdminSecondFactorType): void {
const dialogRef = this.dialog.open(WarnDialogComponent, {
data: {
confirmKey: 'ACTIONS.DELETE',
cancelKey: 'ACTIONS.CANCEL',
titleKey: 'MFA.DELETE.TITLE',
descriptionKey: 'MFA.DELETE.DESCRIPTION',
},
width: '400px',
});
dialogRef.afterClosed().subscribe(resp => {
if (resp) {
if (this.serviceType === PolicyComponentServiceType.MGMT) {
if (this.componentType === LoginMethodComponentType.MultiFactor) {
const req = new MgmtMultiFactor();
req.setMultiFactor(type as MgmtMultiFactorType);
(this.service as ManagementService).RemoveMultiFactorFromLoginPolicy(req).then(() => {
this.toast.showInfo('MFA.TOAST.DELETED', true);
this.refreshPageAfterTimout(2000);
});
} else if (this.componentType === LoginMethodComponentType.SecondFactor) {
const req = new MgmtSecondFactor();
req.setSecondFactor(type as MgmtSecondFactorType);
(this.service as ManagementService).RemoveSecondFactorFromLoginPolicy(req).then(() => {
this.toast.showInfo('MFA.TOAST.DELETED', true);
this.refreshPageAfterTimout(2000);
});
}
} else if (this.serviceType === PolicyComponentServiceType.ADMIN) {
if (this.componentType === LoginMethodComponentType.MultiFactor) {
const req = new AdminMultiFactor();
req.setMultiFactor(type as AdminMultiFactorType);
(this.service as AdminService).RemoveMultiFactorFromDefaultLoginPolicy(req).then(() => {
this.toast.showInfo('MFA.TOAST.DELETED', true);
this.refreshPageAfterTimout(2000);
});
} else if (this.componentType === LoginMethodComponentType.SecondFactor) {
const req = new AdminSecondFactor();
req.setSecondFactor(type as AdminSecondFactorType);
(this.service as AdminService).RemoveSecondFactorFromDefaultLoginPolicy(req).then(() => {
this.toast.showInfo('MFA.TOAST.DELETED', true);
this.refreshPageAfterTimout(2000);
});
}
}
}
});
}
public addMfa(): void {
let selection: any[] = [];
if (this.componentType === LoginMethodComponentType.MultiFactor) {
selection = this.serviceType === PolicyComponentServiceType.MGMT ?
[MgmtMultiFactorType.MULTIFACTORTYPE_U2F_WITH_PIN] :
this.serviceType === PolicyComponentServiceType.ADMIN ?
[AdminMultiFactorType.MULTIFACTORTYPE_U2F_WITH_PIN] :
[];
} else if (this.componentType === LoginMethodComponentType.SecondFactor) {
selection = this.serviceType === PolicyComponentServiceType.MGMT ?
[MgmtSecondFactorType.SECONDFACTORTYPE_U2F, MgmtSecondFactorType.SECONDFACTORTYPE_U2F] :
this.serviceType === PolicyComponentServiceType.ADMIN ?
[AdminSecondFactorType.SECONDFACTORTYPE_OTP, AdminSecondFactorType.SECONDFACTORTYPE_U2F] :
[];
}
const dialogRef = this.dialog.open(DialogAddTypeComponent, {
data: {
title: 'MFA.CREATE.TITLE',
desc: 'MFA.CREATE.DESCRIPTION',
componentType: this.componentType,
types: selection,
},
width: '400px',
});
dialogRef.afterClosed().subscribe((mfaType: AdminMultiFactorType | MgmtMultiFactorType |
AdminSecondFactorType | MgmtSecondFactorType) => {
if (mfaType) {
if (this.serviceType === PolicyComponentServiceType.MGMT) {
if (this.componentType === LoginMethodComponentType.MultiFactor) {
const req = new MgmtMultiFactor();
req.setMultiFactor(mfaType as MgmtMultiFactorType);
(this.service as ManagementService).AddMultiFactorToLoginPolicy(req).then(() => {
this.refreshPageAfterTimout(2000);
}).catch(error => {
this.toast.showError(error);
});
} else if (this.componentType === LoginMethodComponentType.SecondFactor) {
const req = new MgmtSecondFactor();
req.setSecondFactor(mfaType as MgmtSecondFactorType);
(this.service as ManagementService).AddSecondFactorToLoginPolicy(req).then(() => {
this.refreshPageAfterTimout(2000);
}).catch(error => {
this.toast.showError(error);
});
}
} else if (this.serviceType === PolicyComponentServiceType.ADMIN) {
if (this.componentType === LoginMethodComponentType.MultiFactor) {
const req = new AdminMultiFactor();
req.setMultiFactor(mfaType as AdminMultiFactorType);
(this.service as AdminService).addMultiFactorToDefaultLoginPolicy(req).then(() => {
this.refreshPageAfterTimout(2000);
}).catch(error => {
this.toast.showError(error);
});
} else if (this.componentType === LoginMethodComponentType.SecondFactor) {
const req = new AdminSecondFactor();
req.setSecondFactor(mfaType as AdminSecondFactorType);
(this.service as AdminService).AddSecondFactorToDefaultLoginPolicy(req).then(() => {
this.refreshPageAfterTimout(2000);
}).catch(error => {
this.toast.showError(error);
});
}
}
}
});
}
private async getData(): Promise<void> {
this.loadingSubject.next(true);
if (this.serviceType === PolicyComponentServiceType.MGMT) {
if (this.componentType === LoginMethodComponentType.MultiFactor) {
(this.service as ManagementService).GetLoginPolicyMultiFactors().then(resp => {
this.mfas = resp.toObject().multiFactorsList;
this.loadingSubject.next(false);
}).catch(error => {
this.toast.showError(error);
this.loadingSubject.next(false);
});
} else if (this.componentType === LoginMethodComponentType.SecondFactor) {
(this.service as ManagementService).GetLoginPolicySecondFactors().then(resp => {
this.mfas = resp.toObject().secondFactorsList;
this.loadingSubject.next(false);
}).catch(error => {
this.toast.showError(error);
this.loadingSubject.next(false);
});
}
} else if (this.serviceType === PolicyComponentServiceType.ADMIN) {
if (this.componentType === LoginMethodComponentType.MultiFactor) {
(this.service as AdminService).getDefaultLoginPolicyMultiFactors().then(resp => {
this.mfas = resp.toObject().multiFactorsList;
this.loadingSubject.next(false);
}).catch(error => {
this.toast.showError(error);
this.loadingSubject.next(false);
});
} else if (this.componentType === LoginMethodComponentType.SecondFactor) {
(this.service as AdminService).GetDefaultLoginPolicySecondFactors().then(resp => {
this.mfas = resp.toObject().secondFactorsList;
this.loadingSubject.next(false);
}).catch(error => {
this.toast.showError(error);
this.loadingSubject.next(false);
});
}
}
}
public refreshPageAfterTimout(to: number): void {
setTimeout(() => {
this.getData();
}, to);
}
}

View File

@ -0,0 +1,44 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatIconModule } from '@angular/material/icon';
import { MatPaginatorModule } from '@angular/material/paginator';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { MatTableModule } from '@angular/material/table';
import { MatTooltipModule } from '@angular/material/tooltip';
import { RouterModule } from '@angular/router';
import { TranslateModule } from '@ngx-translate/core';
import { HasRoleModule } from 'src/app/directives/has-role/has-role.module';
import { RefreshTableModule } from 'src/app/modules/refresh-table/refresh-table.module';
import { LocalizedDatePipeModule } from 'src/app/pipes/localized-date-pipe/localized-date-pipe.module';
import { TimestampToDatePipeModule } from 'src/app/pipes/timestamp-to-date-pipe/timestamp-to-date-pipe.module';
import { TruncatePipeModule } from 'src/app/pipes/truncate-pipe/truncate-pipe.module';
import { MfaTableComponent } from './mfa-table.component';
import { DialogAddTypeComponent } from './dialog-add-type/dialog-add-type.component';
import { InputModule } from '../input/input.module';
import { MatSelectModule } from '@angular/material/select';
@NgModule({
declarations: [MfaTableComponent, DialogAddTypeComponent],
imports: [
CommonModule,
FormsModule,
ReactiveFormsModule,
MatButtonModule,
MatIconModule,
InputModule,
MatSelectModule,
MatTooltipModule,
TranslateModule,
TimestampToDatePipeModule,
HasRoleModule,
MatProgressSpinnerModule,
],
exports: [
MfaTableComponent,
],
})
export class MfaTableModule { }

View File

@ -44,8 +44,34 @@
</mat-slide-toggle>
<p> {{'POLICY.DATA.ALLOWEXTERNALIDP_DESC' | translate}} </p>
</div>
<div class="row">
<mat-slide-toggle class="toggle" color="primary" [disabled]="disabled" ngDefaultControl
[(ngModel)]="loginData.forceMfa">
{{'POLICY.DATA.FORCEMFA' | translate}}
</mat-slide-toggle>
<p> {{'POLICY.DATA.FORCEMFA_DESC' | translate}} </p>
</div>
</div>
<button [disabled]="disabled" class="save-button" (click)="savePolicy()" color="primary" type="submit"
mat-raised-button>{{ 'ACTIONS.SAVE' | translate }}</button>
<div class="divider"></div>
<h3 class="subheader">{{ 'MFA.LIST.MULTIFACTORTITLE' | translate }}</h3>
<p class="subdesc">{{ 'MFA.LIST.MULTIFACTORDESCRIPTION' | translate }}</p>
<app-mfa-table [service]="service" [serviceType]="serviceType"
[componentType]="LoginMethodComponentType.MultiFactor"
[disabled]="([serviceType == PolicyComponentServiceType.ADMIN ? 'iam.policy.write' : serviceType == PolicyComponentServiceType.MGMT ? 'org.policy.write' : ''] | hasRole | async) == false">
</app-mfa-table>
<h3 class="subheader">{{ 'MFA.LIST.SECONDFACTORTITLE' | translate }}</h3>
<p class="subdesc">{{ 'MFA.LIST.SECONDFACTORDESCRIPTION' | translate }}</p>
<app-mfa-table [service]="service" [serviceType]="serviceType"
[componentType]="LoginMethodComponentType.SecondFactor"
[disabled]="([serviceType == PolicyComponentServiceType.ADMIN ? 'iam.policy.write' : serviceType == PolicyComponentServiceType.MGMT ? 'org.policy.write' : ''] | hasRole | async) == false">
</app-mfa-table>
<h3 class="subheader">{{'LOGINPOLICY.IDPS' | translate}}</h3>
<div class="idps">
@ -63,9 +89,6 @@
</div>
</div>
<button [disabled]="disabled" class="save-button" (click)="savePolicy()" color="primary" type="submit"
mat-raised-button>{{ 'ACTIONS.SAVE' | translate }}</button>
<ng-template appHasRole [appHasRole]="['org.idp.read']">
<h2>{{ 'IDP.LIST.TITLE' | translate }}</h2>
<p>{{ 'IDP.LIST.DESCRIPTION' | translate }}</p>
@ -73,4 +96,4 @@
[disabled]="([serviceType == PolicyComponentServiceType.ADMIN ? 'iam.idp.write' : serviceType == PolicyComponentServiceType.MGMT ? 'org.idp.write' : ''] | hasRole | async) == false">
</app-idp-table>
</ng-template>
</app-detail-layout>
</app-detail-layout>

View File

@ -37,6 +37,11 @@
width: 100%;
}
.subdesc {
color: var(--grey);
font-size: 14px;
}
.idps {
display: flex;
margin: 0 -.5rem;
@ -93,3 +98,10 @@
}
}
}
.divider {
width: 100%;
height: 1px;
background-color: var(--grey);
margin: 1rem 0;
}

View File

@ -22,6 +22,7 @@ import { ToastService } from 'src/app/services/toast.service';
import { PolicyComponentServiceType } from '../policy-component-types.enum';
import { AddIdpDialogComponent } from './add-idp-dialog/add-idp-dialog.component';
import { LoginMethodComponentType } from 'src/app/modules/mfa-table/mfa-table.component';
@Component({
selector: 'app-login-policy',
@ -29,6 +30,7 @@ import { AddIdpDialogComponent } from './add-idp-dialog/add-idp-dialog.component
styleUrls: ['./login-policy.component.scss'],
})
export class LoginPolicyComponent implements OnDestroy {
public LoginMethodComponentType: any = LoginMethodComponentType;
public loginData!: LoginPolicyView.AsObject | DefaultLoginPolicyView.AsObject;
private sub: Subscription = new Subscription();
@ -112,6 +114,8 @@ export class LoginPolicyComponent implements OnDestroy {
mgmtreq.setAllowExternalIdp(this.loginData.allowExternalIdp);
mgmtreq.setAllowRegister(this.loginData.allowRegister);
mgmtreq.setAllowUsernamePassword(this.loginData.allowUsernamePassword);
mgmtreq.setForceMfa(this.loginData.forceMfa);
// console.log(mgmtreq.toObject());
if ((this.loginData as LoginPolicyView.AsObject).pb_default) {
return (this.service as ManagementService).CreateLoginPolicy(mgmtreq);
} else {
@ -122,6 +126,9 @@ export class LoginPolicyComponent implements OnDestroy {
adminreq.setAllowExternalIdp(this.loginData.allowExternalIdp);
adminreq.setAllowRegister(this.loginData.allowRegister);
adminreq.setAllowUsernamePassword(this.loginData.allowUsernamePassword);
adminreq.setForceMfa(this.loginData.forceMfa);
// console.log(adminreq.toObject());
return (this.service as AdminService).UpdateDefaultLoginPolicy(adminreq);
}
}

View File

@ -17,6 +17,7 @@ import { HasRolePipeModule } from 'src/app/pipes/has-role-pipe/has-role-pipe.mod
import { AddIdpDialogModule } from './add-idp-dialog/add-idp-dialog.module';
import { LoginPolicyRoutingModule } from './login-policy-routing.module';
import { LoginPolicyComponent } from './login-policy.component';
import { MfaTableModule } from 'src/app/modules/mfa-table/mfa-table.module';
@NgModule({
declarations: [LoginPolicyComponent],
@ -36,6 +37,7 @@ import { LoginPolicyComponent } from './login-policy.component';
DetailLayoutModule,
AddIdpDialogModule,
IdpTableModule,
MfaTableModule,
MatProgressSpinnerModule,
],
})

View File

@ -1,11 +1,19 @@
<app-card title="{{'USER.MFA.TITLE' | translate}}" description="{{'USER.MFA.DESCRIPTION' | translate}}">
<app-refresh-table [loading]="loading$ | async" (refreshed)="getOTP()" [dataSize]="dataSource?.data?.length">
<app-refresh-table [loading]="loading$ | async" (refreshed)="getMFAs()" [dataSize]="dataSource?.data?.length">
<table class="table" mat-table [dataSource]="dataSource">
<ng-container matColumnDef="type">
<th mat-header-cell *matHeaderCellDef> {{ 'USER.MFA.TABLETYPE' | translate }} </th>
<td mat-cell *matCellDef="let mfa"> {{'USER.MFA.TYPE.'+ mfa.type | translate}} </td>
</ng-container>
<ng-container matColumnDef="attr">
<th mat-header-cell *matHeaderCellDef> {{ 'USER.MFA.ATTRIBUTE' | translate }} </th>
<td mat-cell *matCellDef="let mfa"><span *ngIf="mfa?.attr" class="centered">
{{ mfa.attr }}
</span>
</td>
</ng-container>
<ng-container matColumnDef="state">
<th mat-header-cell *matHeaderCellDef> {{ 'USER.MFA.TABLESTATE' | translate }} </th>
<td mat-cell *matCellDef="let mfa"><span class="centered">
@ -20,7 +28,7 @@
<th mat-header-cell *matHeaderCellDef> {{ 'USER.MFA.TABLEACTIONS' | translate }} </th>
<td mat-cell *matCellDef="let mfa">
<button matTooltip="{{'ACTIONS.REMOVE' | translate}}" color="warn" mat-icon-button
(click)="deleteMFA(mfa.type)">
(click)="deleteMFA(mfa.type, mfa.id)">
<i class="las la-trash"></i>
</button>
</td>
@ -35,6 +43,11 @@
matTooltip="{{'ACTIONS.NEW' | translate}}">
<mat-icon class="icon" svgIcon="mdi_radar"></mat-icon>{{'USER.MFA.OTP' | translate}}
</button>
<button class="button" *ngIf="otpAvailable" (click)="addU2F()" mat-stroked-button color="primary"
matTooltip="{{'ACTIONS.NEW' | translate}}">
<i class="las la-fingerprint"></i>
{{'USER.MFA.U2F' | translate}}
</button>
</div>
<div class="table-wrapper">
<div class="spinner-container" *ngIf="loading$ | async">

View File

@ -2,6 +2,7 @@
.add-row {
display: flex;
margin: -.5rem;
flex-wrap: wrap;
.button {
margin: .5rem;

View File

@ -4,11 +4,32 @@ import { MatSort } from '@angular/material/sort';
import { MatTable, MatTableDataSource } from '@angular/material/table';
import { BehaviorSubject, Observable } from 'rxjs';
import { WarnDialogComponent } from 'src/app/modules/warn-dialog/warn-dialog.component';
import { MfaOtpResponse, MFAState, MfaType, MultiFactor } from 'src/app/proto/generated/auth_pb';
import { MfaOtpResponse, MFAState, MfaType, MultiFactor, WebAuthNResponse } from 'src/app/proto/generated/auth_pb';
import { GrpcAuthService } from 'src/app/services/grpc-auth.service';
import { ToastService } from 'src/app/services/toast.service';
import { DialogOtpComponent } from '../dialog-otp/dialog-otp.component';
import { DialogU2FComponent } from '../dialog-u2f/dialog-u2f.component';
export function _base64ToArrayBuffer(base64: string): any {
const binaryString = atob(base64);
const len = binaryString.length;
const bytes = new Uint8Array(len);
for (let i = 0; i < len; i++) {
bytes[i] = binaryString.charCodeAt(i);
}
return bytes.buffer;
}
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;
}
@Component({
selector: 'app-auth-user-mfa',
@ -16,7 +37,7 @@ import { DialogOtpComponent } from '../dialog-otp/dialog-otp.component';
styleUrls: ['./auth-user-mfa.component.scss'],
})
export class AuthUserMfaComponent implements OnInit, OnDestroy {
public displayedColumns: string[] = ['type', 'state', 'actions'];
public displayedColumns: string[] = ['type', 'attr', 'state', 'actions'];
private loadingSubject: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
public loading$: Observable<boolean> = this.loadingSubject.asObservable();
@ -29,10 +50,12 @@ export class AuthUserMfaComponent implements OnInit, OnDestroy {
public error: string = '';
public otpAvailable: boolean = false;
constructor(private service: GrpcAuthService, private toast: ToastService, private dialog: MatDialog) { }
constructor(private service: GrpcAuthService,
private toast: ToastService,
private dialog: MatDialog) { }
public ngOnInit(): void {
this.getOTP();
this.getMFAs();
}
public ngOnDestroy(): void {
@ -50,7 +73,7 @@ export class AuthUserMfaComponent implements OnInit, OnDestroy {
dialogRef.afterClosed().subscribe((code) => {
if (code) {
this.service.VerifyMfaOTP(code).then(() => {
this.getOTP();
this.getMFAs();
});
}
});
@ -59,7 +82,40 @@ export class AuthUserMfaComponent implements OnInit, OnDestroy {
});
}
public getOTP(): void {
public verifyU2f(): void {
}
public addU2F(): void {
this.service.AddMyMfaU2F().then((u2fresp) => {
const webauthn: WebAuthNResponse.AsObject = u2fresp.toObject();
const credOptions: CredentialCreationOptions = JSON.parse(atob(webauthn.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);
const dialogRef = this.dialog.open(DialogU2FComponent, {
width: '400px',
data: {
credOptions,
},
});
dialogRef.afterClosed().subscribe(done => {
if (done) {
this.getMFAs();
} else {
this.getMFAs();
}
});
}
}, error => {
this.toast.showError(error);
});
}
public getMFAs(): void {
this.service.GetMyMfas().then(mfas => {
this.dataSource = new MatTableDataSource(mfas.toObject().mfasList);
this.dataSource.sort = this.sort;
@ -73,13 +129,13 @@ export class AuthUserMfaComponent implements OnInit, OnDestroy {
});
}
public deleteMFA(type: MfaType): void {
public deleteMFA(type: MfaType, id?: string): void {
const dialogRef = this.dialog.open(WarnDialogComponent, {
data: {
confirmKey: 'ACTIONS.DELETE',
cancelKey: 'ACTIONS.CANCEL',
titleKey: 'USER.MFA.DIALOG.OTP_DELETE_TITLE',
descriptionKey: 'USER.MFA.DIALOG.OTP_DELETE_DESCRIPTION',
titleKey: 'USER.MFA.DIALOG.MFA_DELETE_TITLE',
descriptionKey: 'USER.MFA.DIALOG.MFA_DELETE_DESCRIPTION',
},
width: '400px',
});
@ -94,7 +150,19 @@ export class AuthUserMfaComponent implements OnInit, OnDestroy {
if (index > -1) {
this.dataSource.data.splice(index, 1);
}
this.getOTP();
this.getMFAs();
}).catch(error => {
this.toast.showError(error);
});
} else if (type === MfaType.MFATYPE_U2F && id) {
this.service.RemoveMyMfaU2F(id).then(() => {
this.toast.showInfo('USER.TOAST.U2FREMOVED', true);
const index = this.dataSource.data.findIndex(mfa => mfa.type === type);
if (index > -1) {
this.dataSource.data.splice(index, 1);
}
this.getMFAs();
}).catch(error => {
this.toast.showError(error);
});
@ -102,4 +170,6 @@ export class AuthUserMfaComponent implements OnInit, OnDestroy {
}
});
}
}

View File

@ -1,6 +1,6 @@
<h1 mat-dialog-title>{{'USER.MFA.OTP_DIALOG_TITLE' | translate}}</h1>
<div mat-dialog-content>
<p translate>{{'USER.MFA.OTP_DIALOG_DESCRIPTION' | translate}}</p>
<p>{{'USER.MFA.OTP_DIALOG_DESCRIPTION' | translate}}</p>
<div class="qrcode-wrapper">
<qrcode *ngIf="data" class="qrcode" [qrdata]="data" [width]="150" [errorCorrectionLevel]="'M'"></qrcode>
</div>
@ -15,4 +15,4 @@
<button mat-raised-button class="ok-button" color="primary" (click)="closeDialogWithCode()"><span
translate>ACTIONS.OK</span>
</button>
</div>
</div>

View File

@ -0,0 +1,18 @@
<h1 mat-dialog-title>{{'USER.MFA.U2F_DIALOG_TITLE' | translate}}</h1>
<div mat-dialog-content>
<p>{{'USER.MFA.U2F_DIALOG_DESCRIPTION' | translate}}</p>
<cnsl-form-field class="form-field" label="Name" required="true">
<cnsl-label>{{'USER.MFA.U2F_NAME' | translate}}</cnsl-label>
<input cnslInput [(ngModel)]="name" required/>
</cnsl-form-field>
<mat-spinner diameter="30" *ngIf="loading"></mat-spinner>
<p class="error">{{error}}</p>
</div>
<div mat-dialog-actions class="action">
<button mat-button (click)="closeDialog()">{{'ACTIONS.CLOSE' | translate}}</button>
<button [disabled]="!name" mat-raised-button class="ok-button" color="primary" (click)="closeDialogWithCode()">{{'ACTIONS.VERIFY' | translate}}
</button>
</div>

View File

@ -0,0 +1,27 @@
.qrcode-wrapper {
display: flex;
align-content: center;
.qrcode {
margin: 1rem auto;
}
}
.form-field {
width: 100%;
}
.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,89 @@
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 'src/app/services/grpc-auth.service';
import { ToastService } from 'src/app/services/toast.service';
export function _arrayBufferToBase64(buffer: any): string {
let binary = '';
const bytes = new Uint8Array(buffer);
const len = bytes.byteLength;
for (let i = 0; i < len; i++) {
binary += String.fromCharCode(bytes[i]);
}
return btoa(binary).replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=/g, '');
}
@Component({
selector: 'app-dialog-u2f',
templateUrl: './dialog-u2f.component.html',
styleUrls: ['./dialog-u2f.component.scss'],
})
export class DialogU2FComponent {
public name: string = '';
public error: string = '';
public loading: boolean = false;
constructor(public dialogRef: MatDialogRef<DialogU2FComponent>,
@Inject(MAT_DIALOG_DATA) public data: { credOptions: any; },
private service: GrpcAuthService, private translate: TranslateService, private toast: ToastService) { }
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);
this.service.VerifyMyMfaU2F(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 {
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);
});
}
}
}

View File

@ -33,6 +33,7 @@ import { AuthUserDetailComponent } from './auth-user-detail/auth-user-detail.com
import { AuthUserMfaComponent } from './auth-user-detail/auth-user-mfa/auth-user-mfa.component';
import { CodeDialogComponent } from './auth-user-detail/code-dialog/code-dialog.component';
import { DialogOtpComponent } from './auth-user-detail/dialog-otp/dialog-otp.component';
import { DialogU2FComponent } from './auth-user-detail/dialog-u2f/dialog-u2f.component';
import { EditDialogComponent } from './auth-user-detail/edit-dialog/edit-dialog.component';
import { ResendEmailDialogComponent } from './auth-user-detail/resend-email-dialog/resend-email-dialog.component';
import { ThemeSettingComponent } from './auth-user-detail/theme-setting/theme-setting.component';
@ -65,6 +66,7 @@ import { UserMfaComponent } from './user-detail/user-mfa/user-mfa.component';
ExternalIdpsComponent,
ContactComponent,
ResendEmailDialogComponent,
DialogU2FComponent,
],
imports: [
UserDetailRoutingModule,

View File

@ -1,11 +1,19 @@
<app-card title="{{'USER.MFA.TITLE' | translate}}" description="{{'USER.MFA.DESCRIPTION' | translate}}">
<app-refresh-table [loading]="loading$ | async" (refreshed)="getOTP()" [dataSize]="dataSource?.data?.length">
<app-refresh-table [loading]="loading$ | async" (refreshed)="getMFAs()" [dataSize]="dataSource?.data?.length">
<table class="table" mat-table [dataSource]="dataSource">
<ng-container matColumnDef="type">
<th mat-header-cell *matHeaderCellDef> {{ 'USER.MFA.TABLETYPE' | translate }} </th>
<td mat-cell *matCellDef="let mfa"> {{'USER.MFA.TYPE.'+ mfa.type | translate}} </td>
</ng-container>
<ng-container matColumnDef="attr">
<th mat-header-cell *matHeaderCellDef> {{ 'USER.MFA.ATTRIBUTE' | translate }} </th>
<td mat-cell *matCellDef="let mfa"><span *ngIf="mfa?.attr" class="centered">
{{ mfa.attr }}
</span>
</td>
</ng-container>
<ng-container matColumnDef="state">
<th mat-header-cell *matHeaderCellDef> {{ 'USER.MFA.TABLESTATE' | translate }} </th>
<td mat-cell *matCellDef="let mfa">
@ -21,7 +29,7 @@
<th mat-header-cell *matHeaderCellDef> {{ 'USER.MFA.TABLEACTIONS' | translate }} </th>
<td mat-cell *matCellDef="let mfa">
<button matTooltip="{{'ACTIONS.REMOVE' | translate}}" color="warn" mat-icon-button
(click)="deleteMFA(mfa.type)">
(click)="deleteMFA(mfa.type, mfa.id)">
<i class="las la-trash"></i>
</button>
</td>

View File

@ -20,7 +20,7 @@ export interface MFAItem {
styleUrls: ['./user-mfa.component.scss'],
})
export class UserMfaComponent implements OnInit, OnDestroy {
public displayedColumns: string[] = ['type', 'state', 'actions'];
public displayedColumns: string[] = ['type', 'attr', 'state', 'actions'];
@Input() private user!: UserView.AsObject;
public mfaSubject: BehaviorSubject<UserMultiFactor.AsObject[]> = new BehaviorSubject<UserMultiFactor.AsObject[]>([]);
private loadingSubject: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
@ -37,7 +37,7 @@ export class UserMfaComponent implements OnInit, OnDestroy {
constructor(private mgmtUserService: ManagementService, private dialog: MatDialog, private toast: ToastService) { }
public ngOnInit(): void {
this.getOTP();
this.getMFAs();
}
public ngOnDestroy(): void {
@ -45,7 +45,7 @@ export class UserMfaComponent implements OnInit, OnDestroy {
this.loadingSubject.complete();
}
public getOTP(): void {
public getMFAs(): void {
this.mgmtUserService.getUserMfas(this.user.id).then(mfas => {
this.dataSource = new MatTableDataSource(mfas.toObject().mfasList);
this.dataSource.sort = this.sort;
@ -54,13 +54,14 @@ export class UserMfaComponent implements OnInit, OnDestroy {
});
}
public deleteMFA(type: MfaType): void {
public deleteMFA(type: MfaType, id?: string): void {
console.log(type, id);
const dialogRef = this.dialog.open(WarnDialogComponent, {
data: {
confirmKey: 'ACTIONS.DELETE',
cancelKey: 'ACTIONS.CANCEL',
titleKey: 'USER.MFA.DIALOG.OTP_DELETE_TITLE',
descriptionKey: 'USER.MFA.DIALOG.OTP_DELETE_DESCRIPTION',
titleKey: 'USER.MFA.DIALOG.MFA_DELETE_TITLE',
descriptionKey: 'USER.MFA.DIALOG.MFA_DELETE_DESCRIPTION',
},
width: '400px',
});
@ -75,7 +76,19 @@ export class UserMfaComponent implements OnInit, OnDestroy {
if (index > -1) {
this.dataSource.data.splice(index, 1);
}
this.getOTP();
this.getMFAs();
}).catch(error => {
this.toast.showError(error);
});
} else if (type === MfaType.MFATYPE_U2F && id) {
this.mgmtUserService.RemoveMfaU2F(id).then(() => {
this.toast.showInfo('USER.TOAST.U2FREMOVED', true);
const index = this.dataSource.data.findIndex(mfa => mfa.type === type);
if (index > -1) {
this.dataSource.data.splice(index, 1);
}
this.getMFAs();
}).catch(error => {
this.toast.showError(error);
});

View File

@ -1,4 +1,4 @@
<app-refresh-table [loading]="loading$ | async" (refreshed)="refreshPage()" [dataSize]="dataSource.data.length"
<app-refresh-table [loading]="loading$ | async" (refreshed)="refreshPage()" [dataSize]="userResult?.totalResult"
[timestamp]="userResult?.viewTimestamp" [selection]="selection"
[emitRefreshOnPreviousRoutes]="refreshOnPreviousRoutes">
<cnsl-form-field @appearfade *ngIf="userSearchKey != undefined" actions class="filtername">

View File

@ -36,6 +36,8 @@ import {
IdpSearchRequest,
IdpSearchResponse,
IdpView,
MultiFactor,
MultiFactorsResult,
OidcIdpConfig,
OidcIdpConfigCreate,
OidcIdpConfigUpdate,
@ -46,6 +48,8 @@ import {
OrgSetUpRequest,
OrgSetUpResponse,
RemoveIamMemberRequest,
SecondFactor,
SecondFactorsResult,
ViewID,
Views,
} from '../proto/generated/admin_pb';
@ -73,6 +77,32 @@ export class AdminService {
return this.grpcService.admin.setUpOrg(req);
}
public getDefaultLoginPolicyMultiFactors(): Promise<MultiFactorsResult> {
const req = new Empty();
return this.grpcService.admin.getDefaultLoginPolicyMultiFactors(req);
}
public addMultiFactorToDefaultLoginPolicy(req: MultiFactor): Promise<MultiFactor> {
return this.grpcService.admin.addMultiFactorToDefaultLoginPolicy(req);
}
public RemoveMultiFactorFromDefaultLoginPolicy(req: MultiFactor): Promise<Empty> {
return this.grpcService.admin.removeMultiFactorFromDefaultLoginPolicy(req);
}
public GetDefaultLoginPolicySecondFactors(): Promise<SecondFactorsResult> {
const req = new Empty();
return this.grpcService.admin.getDefaultLoginPolicySecondFactors(req);
}
public AddSecondFactorToDefaultLoginPolicy(req: SecondFactor): Promise<SecondFactor> {
return this.grpcService.admin.addSecondFactorToDefaultLoginPolicy(req);
}
public RemoveSecondFactorFromDefaultLoginPolicy(req: SecondFactor): Promise<Empty> {
return this.grpcService.admin.removeSecondFactorFromDefaultLoginPolicy(req);
}
public GetIamMemberRoles(): Promise<IamMemberRoles> {
const req = new Empty();
return this.grpcService.admin.getIamMemberRoles(req);

View File

@ -5,34 +5,37 @@ import { BehaviorSubject, from, merge, Observable, of, Subject } from 'rxjs';
import { catchError, filter, finalize, first, map, mergeMap, switchMap, take, timeout } from 'rxjs/operators';
import {
Changes,
ChangesRequest,
ExternalIDPRemoveRequest,
ExternalIDPSearchRequest,
ExternalIDPSearchResponse,
Gender,
MfaOtpResponse,
MultiFactors,
MyPermissions,
MyProjectOrgSearchQuery,
MyProjectOrgSearchRequest,
MyProjectOrgSearchResponse,
Org,
PasswordChange,
PasswordComplexityPolicy,
UpdateUserAddressRequest,
UpdateUserEmailRequest,
UpdateUserPhoneRequest,
UpdateUserProfileRequest,
UserAddress,
UserEmail,
UserPhone,
UserProfile,
UserProfileView,
UserSessionViews,
UserView,
VerifyMfaOtp,
VerifyUserPhoneRequest,
Changes,
ChangesRequest,
ExternalIDPRemoveRequest,
ExternalIDPSearchRequest,
ExternalIDPSearchResponse,
Gender,
MfaOtpResponse,
MultiFactors,
MyPermissions,
MyProjectOrgSearchQuery,
MyProjectOrgSearchRequest,
MyProjectOrgSearchResponse,
Org,
PasswordChange,
PasswordComplexityPolicy,
UpdateUserAddressRequest,
UpdateUserEmailRequest,
UpdateUserPhoneRequest,
UpdateUserProfileRequest,
UserAddress,
UserEmail,
UserPhone,
UserProfile,
UserProfileView,
UserSessionViews,
UserView,
VerifyMfaOtp,
VerifyUserPhoneRequest,
VerifyWebAuthN,
WebAuthNResponse,
WebAuthNTokenID,
} from '../proto/generated/auth_pb';
import { GrpcService } from './grpc.service';
import { StorageKey, StorageService } from './storage.service';
@ -328,9 +331,31 @@ export class GrpcAuthService {
);
}
public AddMyMfaU2F(): Promise<WebAuthNResponse> {
return this.grpcService.auth.addMyMfaU2F(
new Empty(),
);
}
public RemoveMyMfaU2F(id: string): Promise<Empty> {
const req = new WebAuthNTokenID();
req.setId(id);
return this.grpcService.auth.removeMyMfaU2F(req);
}
public VerifyMyMfaU2F(credential: string, tokenname: string): Promise<Empty> {
const req = new VerifyWebAuthN();
req.setPublicKeyCredential(credential);
req.setTokenName(tokenname);
return this.grpcService.auth.verifyMyMfaU2F(
req,
);
}
public RemoveMfaOTP(): Promise<Empty> {
return this.grpcService.auth.removeMfaOTP(
new Empty(),
new Empty(),
);
}

View File

@ -2,6 +2,7 @@ import { Injectable } from '@angular/core';
import { Empty } from 'google-protobuf/google/protobuf/empty_pb';
import { Timestamp } from 'google-protobuf/google/protobuf/timestamp_pb';
import { BehaviorSubject } from 'rxjs';
import { MultiFactorsResult } from '../proto/generated/admin_pb';
import {
AddMachineKeyRequest,
@ -50,6 +51,7 @@ import {
MachineKeySearchResponse,
MachineKeyType,
MachineResponse,
MultiFactor,
NotificationType,
OIDCApplicationCreate,
OIDCConfig,
@ -122,6 +124,8 @@ import {
ProjectView,
RemoveOrgDomainRequest,
RemoveOrgMemberRequest,
SecondFactor,
SecondFactorsResult,
SetPasswordNotificationRequest,
UpdateMachineRequest,
UpdateUserAddressRequest,
@ -152,6 +156,7 @@ import {
UserSearchResponse,
UserView,
ValidateOrgDomainRequest,
WebAuthNTokenID,
ZitadelDocs,
} from '../proto/generated/management_pb';
import { GrpcService } from './grpc.service';
@ -185,6 +190,32 @@ export class ManagementService {
return this.grpcService.mgmt.searchIdps(req);
}
public GetLoginPolicyMultiFactors(): Promise<MultiFactorsResult> {
const req = new Empty();
return this.grpcService.mgmt.getLoginPolicyMultiFactors(req);
}
public AddMultiFactorToLoginPolicy(req: MultiFactor): Promise<MultiFactor> {
return this.grpcService.mgmt.addMultiFactorToLoginPolicy(req);
}
public RemoveMultiFactorFromLoginPolicy(req: MultiFactor): Promise<Empty> {
return this.grpcService.mgmt.removeMultiFactorFromLoginPolicy(req);
}
public GetLoginPolicySecondFactors(): Promise<SecondFactorsResult> {
const req = new Empty();
return this.grpcService.mgmt.getLoginPolicySecondFactors(req);
}
public AddSecondFactorToLoginPolicy(req: SecondFactor): Promise<SecondFactor> {
return this.grpcService.mgmt.addSecondFactorToLoginPolicy(req);
}
public RemoveSecondFactorFromLoginPolicy(req: SecondFactor): Promise<Empty> {
return this.grpcService.mgmt.removeSecondFactorFromLoginPolicy(req);
}
public GetLoginPolicy(): Promise<LoginPolicyView> {
const req = new Empty();
return this.grpcService.mgmt.getLoginPolicy(req);
@ -683,6 +714,12 @@ export class ManagementService {
return this.grpcService.mgmt.removeMfaOTP(req);
}
public RemoveMfaU2F(id: string): Promise<Empty> {
const req = new WebAuthNTokenID();
req.setId(id);
return this.grpcService.mgmt.removeMfaU2F(req);
}
public SaveUserProfile(
id: string,
firstName?: string,

View File

@ -155,6 +155,7 @@
"MFA": {
"TABLETYPE":"Typ",
"TABLESTATE":"Status",
"ATTRIBUTE":"Attribut",
"TABLEACTIONS":"Aktionen",
"TITLE": "Multifaktor-Authentisierung",
"DESCRIPTION": "Füge einen zusätzlichen Faktor hinzu, um Dein Konto optimal zu schützen.",
@ -162,6 +163,12 @@
"OTP": "OTP konfigurieren",
"OTP_DIALOG_TITLE": "OTP hinzufügen",
"OTP_DIALOG_DESCRIPTION": "Scanne den QR-Code mit einer Authenticator App und verifiziere den erhaltenen Code, um OTP zu aktivieren.",
"U2F":"U2F hinzufügen",
"U2F_DIALOG_TITLE": "U2F hinzufügen",
"U2F_DIALOG_DESCRIPTION": "Gib einen Namen für den von dir verwendeten Universellen Multifaktor an.",
"U2F_SUCCESS":"U2F erfolgreich erstellt!",
"U2F_ERROR":"Ein Fehler ist aufgetreten!",
"U2F_NAME":"U2F Name",
"TYPE": {
"0":"Keine MFA definiert",
"1":"OTP",
@ -174,8 +181,8 @@
"3": "Gelöscht"
},
"DIALOG": {
"OTP_DELETE_TITLE":"OTP Faktor entfernen",
"OTP_DELETE_DESCRIPTION":"Sie sind im Begriff OTP als Zweitfaktormethode zu entfernen. Sind sie sicher?"
"MFA_DELETE_TITLE":"Zweiten Faktor entfernen",
"MFA_DELETE_DESCRIPTION":"Sie sind im Begriff eine Zweitfaktormethode zu entfernen. Sind sie sicher?"
}
},
"EXTERNALIDP": {
@ -345,6 +352,7 @@
"PHONEVERIFICATIONSENT":"Bestätigungscode per Telefonnummer gesendet.",
"EMAILVERIFICATIONSENT":"Bestätigungscode per E-Mail gesendet.",
"OTPREMOVED":"OTP entfernt.",
"U2FREMOVED":"U2F entfernt.",
"INITIALPASSWORDSET":"Initiales Passwort gesetzt.",
"PASSWORDNOTIFICATIONSENT":"Passwortänderung mittgeteilt.",
"PASSWORDCHANGED":"Passwort geändert.",
@ -551,7 +559,9 @@
"ALLOWREGISTER":"Registrieren erlaubt",
"ALLOWUSERNAMEPASSWORD_DESC":"Der konventionelle Login mit Benutzername und Passwort wird erlaubt.",
"ALLOWEXTERNALIDP_DESC":"Der Login wird für die darunter liegenden Identity Provider erlaubt.",
"ALLOWREGISTER_DESC":"Ist die Option gewählt, erscheint im Login ein zusätzlicher Schritt zum Registrieren eines Benutzers."
"ALLOWREGISTER_DESC":"Ist die Option gewählt, erscheint im Login ein zusätzlicher Schritt zum Registrieren eines Benutzers.",
"FORCEMFA":"Mfa erzwingen",
"FORCEMFA_DESC":"Ist die Option gewählt, müssen Benutzer einen zweiten Faktor für den Login verwenden."
},
"RESET":"Richtlinie zurücksetzen",
"CREATECUSTOM":"Benutzerdefinierte Richtlinie erstellen",
@ -763,7 +773,7 @@
},
"IDP":{
"LIST": {
"TITLE":"Identitäts Providers",
"TITLE":"Identitäts Provider",
"DESCRIPTION":"Definieren Sie hier Ihre zusätzlichen Idps, die sie für die Authentifizierung in Ihren Organisationen verwenden können."
},
"CREATE": {
@ -826,6 +836,37 @@
"DELETED":"Idp erfolgreich gelöscht!"
}
},
"MFA":{
"LIST": {
"MULTIFACTORTITLE":"Multifaktoren",
"MULTIFACTORDESCRIPTION":"Definieren Sie hier Ihre Multifaktoren, die sie für die Authentifizierung verwenden können.",
"SECONDFACTORTITLE":"Zweitfaktoren",
"SECONDFACTORDESCRIPTION":"Definieren Sie hier Ihre Zweitfaktoren, die sie für die Authentifizierung verwenden können."
},
"CREATE": {
"TITLE":"Neuer Faktor",
"DESCRIPTION":"Definieren Sie hier den gewünschten Typ."
},
"DELETE": {
"TITLE":"Faktor löschen",
"DESCRIPTION":"Sie sind im Begriff einen Faktor zu löschen. Die daruch hervorgerufenen Änderungen sind unwiederruflich. Wollen Sie dies wirklich tun?"
},
"TOAST": {
"ADDED":"Erfolgreich hinzugefügt.",
"SAVED": "Erfolgreich gespeichert.",
"DELETED":"Mfa erfolgreich gelöscht!"
},
"TYPE":"Typ",
"MULTIFACTORTYPES": {
"0":"Unknown",
"1":"U2F with Pin"
},
"SECONDFACTORTYPES": {
"0":"Unknown",
"1":"OTP",
"2":"U2F"
}
},
"LOGINPOLICY": {
"CREATE": {
"TITLE":"Login Policy",

View File

@ -155,6 +155,7 @@
"MFA": {
"TABLETYPE":"Type",
"TABLESTATE":"Status",
"ATTRIBUTE":"Attribut",
"TABLEACTIONS":"Actions",
"TITLE": "Multifactor Authentication",
"DESCRIPTION": "Add a second factor to ensure optimal security for your account.",
@ -162,6 +163,12 @@
"OTP": "Configure OTP",
"OTP_DIALOG_TITLE": "Add OTP",
"OTP_DIALOG_DESCRIPTION": "Scan the QR code with an authenticator app and enter the code below to verify and activate the OTP method.",
"U2F":"Add U2F",
"U2F_DIALOG_TITLE": "Verify U2F",
"U2F_DIALOG_DESCRIPTION": "Enter a name for your used universal Multifactor.",
"U2F_SUCCESS":"U2F created successfully!",
"U2F_ERROR":"An error during U2F setup occurred!",
"U2F_NAME":"U2F Name",
"TYPE": {
"0": "No MFA defined",
"1": "OTP",
@ -174,8 +181,8 @@
"3": "Deleted"
},
"DIALOG": {
"OTP_DELETE_TITLE":"Remove OTP Factor",
"OTP_DELETE_DESCRIPTION":"You are about to delete OTP as second factor. Are you sure?"
"MFA_DELETE_TITLE":"Remove Secondfactor",
"MFA_DELETE_DESCRIPTION":"You are about to delete a second factor. Are you sure?"
}
},
"EXTERNALIDP": {
@ -345,6 +352,7 @@
"PHONEVERIFICATIONSENT":"Phone verification code sent.",
"EMAILVERIFICATIONSENT":"E-mail verification code sent.",
"OTPREMOVED":"OTP removed.",
"U2FREMOVED":"U2F removed.",
"INITIALPASSWORDSET":"Initial password set.",
"PASSWORDNOTIFICATIONSENT":"Password change notification sent.",
"PASSWORDCHANGED":"Password changed successfully.",
@ -551,7 +559,9 @@
"ALLOWREGISTER":"Register allowed",
"ALLOWUSERNAMEPASSWORD_DESC":"The conventional login with user name and password is allowed.",
"ALLOWEXTERNALIDP_DESC":"The login is allowed for the underlying identity providers",
"ALLOWREGISTER_DESC":"If the option is selected, an additional step for registering a user appears in the login."
"ALLOWREGISTER_DESC":"If the option is selected, an additional step for registering a user appears in the login.",
"FORCEMFA":"Force MFA",
"FORCEMFA_DESC":"If the option is selected, users have to configure a second factor for login."
},
"RESET":"Reset Policy",
"CREATECUSTOM":"Create Custom Policy",
@ -826,6 +836,37 @@
"DELETED":"Idp removed successfully!"
}
},
"MFA":{
"LIST": {
"MULTIFACTORTITLE":"Multifactors",
"MULTIFACTORDESCRIPTION":"Define your Multifactors for Authentication here.",
"SECONDFACTORTITLE":"Secondfactors",
"SECONDFACTORDESCRIPTION":"Define your Secondfactors for Authentication here."
},
"CREATE": {
"TITLE":"New Factor",
"DESCRIPTION":"Select your new Factor type."
},
"DELETE": {
"TITLE":"Delete Factor",
"DESCRIPTION":"You are about to delete a Factor from Login Policy. Are you sure?"
},
"TOAST": {
"ADDED":"Added successfully.",
"SAVED": "Saved successfully.",
"DELETED":"Removed successfully"
},
"TYPE":"Type",
"MULTIFACTORTYPES": {
"0":"Unknown",
"1":"U2F with Pin"
},
"SECONDFACTORTYPES": {
"0":"Unknown",
"1":"OTP",
"2":"U2F"
}
},
"LOGINPOLICY": {
"CREATE": {
"TITLE":"Login Policy",

View File

@ -226,6 +226,11 @@ func (s *Server) RemoveMfaOTP(ctx context.Context, userID *management.UserID) (*
return &empty.Empty{}, err
}
func (s *Server) RemoveMfaU2F(ctx context.Context, webAuthNTokenID *management.WebAuthNTokenID) (*empty.Empty, error) {
err := s.user.RemoveU2F(ctx, webAuthNTokenID.UserId, webAuthNTokenID.Id)
return &empty.Empty{}, err
}
func (s *Server) SearchUserMemberships(ctx context.Context, in *management.UserMembershipSearchRequest) (*management.UserMembershipSearchResponse, error) {
request := userMembershipSearchRequestsToModel(in)
request.AppendUserIDQuery(in.UserId)

View File

@ -2,14 +2,15 @@ package management
import (
"encoding/json"
"github.com/caos/logging"
"github.com/caos/zitadel/internal/model"
"github.com/golang/protobuf/ptypes"
"golang.org/x/text/language"
"google.golang.org/protobuf/encoding/protojson"
"google.golang.org/protobuf/types/known/structpb"
"github.com/caos/zitadel/internal/eventstore/models"
"github.com/caos/zitadel/internal/model"
usr_model "github.com/caos/zitadel/internal/user/model"
"github.com/caos/zitadel/pkg/grpc/management"
"github.com/caos/zitadel/pkg/grpc/message"
@ -504,6 +505,7 @@ func mfaFromModel(mfa *usr_model.MultiFactor) *management.UserMultiFactor {
State: mfaStateFromModel(mfa.State),
Type: mfaTypeFromModel(mfa.Type),
Attribute: mfa.Attribute,
Id: mfa.ID,
}
}

View File

@ -231,6 +231,10 @@ func (repo *UserRepo) RemoveOTP(ctx context.Context, userID string) error {
return repo.UserEvents.RemoveOTP(ctx, userID)
}
func (repo *UserRepo) RemoveU2F(ctx context.Context, userID, webAuthNTokenID string) error {
return repo.UserEvents.RemoveU2FToken(ctx, userID, webAuthNTokenID)
}
func (repo *UserRepo) SetOneTimePassword(ctx context.Context, password *usr_model.Password) (*usr_model.Password, error) {
policy, err := repo.View.PasswordComplexityPolicyByAggregateID(authz.GetCtxData(ctx).OrgID)
if err != nil && caos_errs.IsNotFound(err) {

View File

@ -32,6 +32,7 @@ type UserRepository interface {
UserMFAs(ctx context.Context, userID string) ([]*model.MultiFactor, error)
RemoveOTP(ctx context.Context, userID string) error
RemoveU2F(ctx context.Context, userID, webAuthNTokenID string) error
SearchExternalIDPs(ctx context.Context, request *model.ExternalIDPSearchRequest) (*model.ExternalIDPSearchResponse, error)
RemoveExternalIDP(ctx context.Context, externalIDP *model.ExternalIDP) error

View File

@ -399,6 +399,16 @@ service ManagementService {
};
}
rpc RemoveMfaU2F(WebAuthNTokenID) returns (google.protobuf.Empty) {
option (google.api.http) = {
delete: "/users/{user_id}/mfas/u2f/{id}"
};
option (caos.zitadel.utils.v1.auth_option) = {
permission: "user.write"
};
}
// Sends an Notification (Email/SMS) with a password reset Link
rpc SendSetPasswordNotification(SetPasswordNotificationRequest) returns (google.protobuf.Empty) {
option (google.api.http) = {
@ -1646,6 +1656,11 @@ message UserID {
string id = 1 [(validate.rules).string.min_len = 1];
}
message WebAuthNTokenID {
string user_id = 1 [(validate.rules).string.min_len = 1];
string id = 2 [(validate.rules).string.min_len = 1];
}
message LoginName {
string login_name = 1 [(validate.rules).string.min_len = 1];
}
@ -2030,6 +2045,7 @@ message UserMultiFactor {
MfaType type = 1;
MFAState state = 2;
string attribute = 3;
string id = 4;
}
enum MfaType {