fix(console): u2f, mfa, loginpolicy, auth and mgmt passwordless, clockskew, userinfo within idtoken (#1108)

* fix 2fa,mfa config, self management

* u2f enable when otp

* passwordless grpc auth

* clockskew, passwordless container, util class

* passwordless, i18n

* passwordless auth and mgmt

* lint ts

* chore(deps-dev): bump ts-node from 9.1.0 to 9.1.1 in /console (#1089)

Bumps [ts-node](https://github.com/TypeStrong/ts-node) from 9.1.0 to 9.1.1.
- [Release notes](https://github.com/TypeStrong/ts-node/releases)
- [Commits](https://github.com/TypeStrong/ts-node/compare/v9.1.0...v9.1.1)

Signed-off-by: dependabot[bot] <[email protected]>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* chore(deps-dev): bump @angular/cli from 11.0.3 to 11.0.4 in /console (#1094)

Bumps [@angular/cli](https://github.com/angular/angular-cli) from 11.0.3 to 11.0.4.
- [Release notes](https://github.com/angular/angular-cli/releases)
- [Commits](https://github.com/angular/angular-cli/compare/v11.0.3...v11.0.4)

Signed-off-by: dependabot[bot] <[email protected]>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Max Peintner <[email protected]>

* chore(deps): bump uuid from 8.3.1 to 8.3.2 in /console (#1098)

Bumps [uuid](https://github.com/uuidjs/uuid) from 8.3.1 to 8.3.2.
- [Release notes](https://github.com/uuidjs/uuid/releases)
- [Changelog](https://github.com/uuidjs/uuid/blob/master/CHANGELOG.md)
- [Commits](https://github.com/uuidjs/uuid/compare/v8.3.1...v8.3.2)

Signed-off-by: dependabot[bot] <[email protected]>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Max Peintner <[email protected]>

* chore(deps-dev): bump @angular/language-service in /console (#1099)

Bumps [@angular/language-service](https://github.com/angular/angular/tree/HEAD/packages/language-service) from 11.0.3 to 11.0.4.
- [Release notes](https://github.com/angular/angular/releases)
- [Changelog](https://github.com/angular/angular/blob/master/CHANGELOG.md)
- [Commits](https://github.com/angular/angular/commits/11.0.4/packages/language-service)

Signed-off-by: dependabot[bot] <[email protected]>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* chore(deps-dev): bump @types/node from 14.14.10 to 14.14.13 in /console (#1100)

Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 14.14.10 to 14.14.13.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

Signed-off-by: dependabot[bot] <[email protected]>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* chore(deps-dev): bump @angular-devkit/build-angular in /console (#1088)

Bumps [@angular-devkit/build-angular](https://github.com/angular/angular-cli) from 0.1100.3 to 0.1100.4.
- [Release notes](https://github.com/angular/angular-cli/releases)
- [Commits](https://github.com/angular/angular-cli/commits)

Signed-off-by: dependabot[bot] <[email protected]>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Max Peintner <[email protected]>

* fix: replace regex check for projectid (#1064)

* update lock

* fix app detail

* logs

* fix login policy update

* fix error message

* decode excluded cred id

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Livio Amstutz <[email protected]>
This commit is contained in:
Max Peintner
2020-12-16 16:34:12 +01:00
committed by GitHub
co-authored by dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Livio Amstutz
parent 71df1bcd0e
commit 6aa0588fe0
34 changed files with 1261 additions and 423 deletions
@@ -127,6 +127,22 @@
<span>{{'APP.OIDC.IDTOKENROLEASSERTION_DESCRIPTION' | translate}}</span>
</cnsl-info-section>
<mat-checkbox class="full-width" style="margin-bottom: .5rem"
formControlName="idTokenUserinfoAssertion" color="primary">
{{'APP.OIDC.IDTOKENUSERINFOASSERTION' | translate}}</mat-checkbox>
<cnsl-info-section class="full-width desc">
<span>{{'APP.OIDC.IDTOKENUSERINFOASSERTION_DESCRIPTION' | translate}}</span>
</cnsl-info-section>
<p class="clockskew-title">ClockSkew</p>
<mat-slider color="primary" formControlName="clockSkewSeconds" class="clockskew-slider" thumbLabel
[displayWith]="formatClockSkewLabel" tickInterval=".1" min="0" [step]="1" max="5">
</mat-slider>
<cnsl-info-section class="full-width desc">
<span>{{'APP.OIDC.CLOCKSKEW' | translate}}</span>
</cnsl-info-section>
<div class="divider"></div>
<p class="full-width section-title">{{'APP.OIDC.REDIRECTSECTIONTITLE' | translate}}</p>
@@ -128,6 +128,17 @@
margin-right: .5rem;
}
.clockskew-title {
font-size: 14px;
color: var(--grey);
margin: 1rem .5rem 0 .5rem;
}
.clockskew-slider {
width: 100%;
margin: 0 .5rem;
}
.desc {
color: var(--grey);
font-size: 14px;
@@ -6,6 +6,7 @@ import { MatButtonToggleChange } from '@angular/material/button-toggle';
import { MatDialog } from '@angular/material/dialog';
import { ActivatedRoute, Params } from '@angular/router';
import { TranslateService } from '@ngx-translate/core';
import { Duration } from 'google-protobuf/google/protobuf/duration_pb';
import { Subscription } from 'rxjs';
import { take } from 'rxjs/operators';
import { ChangeType } from 'src/app/modules/changes/changes.component';
@@ -15,6 +16,7 @@ import {
OIDCApplicationType,
OIDCAuthMethodType,
OIDCConfig,
OIDCConfigUpdate,
OIDCGrantType,
OIDCResponseType,
OIDCTokenType,
@@ -116,9 +118,15 @@ export class AppDetailComponent implements OnInit, OnDestroy {
accessTokenType: [{ value: '', disabled: true }],
accessTokenRoleAssertion: [{ value: false, disabled: true }],
idTokenRoleAssertion: [{ value: false, disabled: true }],
idTokenUserinfoAssertion: [{ value: false, disabled: true }],
clockSkewSeconds: [{ value: 0, disabled: true }],
});
}
public formatClockSkewLabel(seconds: number): string {
return seconds + 's';
}
public ngOnInit(): void {
this.subscription = this.route.params.subscribe(params => this.getData(params));
}
@@ -132,11 +140,12 @@ export class AppDetailComponent implements OnInit, OnDestroy {
this.mgmtService.GetIam().then(iam => {
this.isZitadel = iam.toObject().iamProjectId === this.projectId;
});
this.authService.isAllowed(['project.app.write$', 'project.app.write:' + id]).pipe(take(1)).subscribe((allowed) => {
this.authService.isAllowed(['project.app.write$', 'project.app.write:' + projectid]).pipe(take(1)).subscribe((allowed) => {
this.canWrite = allowed;
this.mgmtService.GetApplicationById(projectid, id).then(app => {
this.app = app.toObject();
this.appNameForm.patchValue(this.app);
console.log(this.app);
if (allowed) {
this.appNameForm.enable();
this.appForm.enable();
@@ -150,6 +159,11 @@ export class AppDetailComponent implements OnInit, OnDestroy {
if (this.app.oidcConfig?.postLogoutRedirectUrisList) {
this.postLogoutRedirectUrisList = this.app.oidcConfig.postLogoutRedirectUrisList;
}
if (this.app.oidcConfig?.clockSkew) {
const inSecs = this.app.oidcConfig?.clockSkew.seconds + this.app.oidcConfig?.clockSkew.nanos / 100000;
console.log(inSecs);
this.appForm.controls['clockSkewSeconds'].setValue(inSecs);
}
if (this.app.oidcConfig) {
this.appForm.patchValue(this.app.oidcConfig);
}
@@ -159,8 +173,6 @@ export class AppDetailComponent implements OnInit, OnDestroy {
this.errorMessage = error.message;
});
});
this.docs = (await this.mgmtService.GetZitadelDocs()).toObject();
}
@@ -247,9 +259,32 @@ export class AppDetailComponent implements OnInit, OnDestroy {
this.app.oidcConfig.accessTokenType = this.accessTokenType?.value;
this.app.oidcConfig.accessTokenRoleAssertion = this.accessTokenRoleAssertion?.value;
this.app.oidcConfig.idTokenRoleAssertion = this.idTokenRoleAssertion?.value;
this.app.oidcConfig.idTokenUserinfoAssertion = this.idTokenUserinfoAssertion?.value;
const req = new OIDCConfigUpdate();
req.setProjectId(this.projectId);
req.setApplicationId(this.app.id);
req.setRedirectUrisList(this.app.oidcConfig.redirectUrisList);
req.setResponseTypesList(this.app.oidcConfig.responseTypesList);
req.setAuthMethodType(this.app.oidcConfig.authMethodType);
req.setPostLogoutRedirectUrisList(this.app.oidcConfig.postLogoutRedirectUrisList);
req.setGrantTypesList(this.app.oidcConfig.grantTypesList);
req.setApplicationType(this.app.oidcConfig.applicationType);
req.setDevMode(this.app.oidcConfig.devMode);
req.setAccessTokenType(this.app.oidcConfig.accessTokenType);
req.setAccessTokenRoleAssertion(this.app.oidcConfig.accessTokenRoleAssertion);
req.setIdTokenRoleAssertion(this.app.oidcConfig.idTokenRoleAssertion);
req.setIdTokenUserinfoAssertion(this.app.oidcConfig.idTokenUserinfoAssertion);
if (this.clockSkewSeconds?.value) {
const dur = new Duration();
dur.setSeconds(Math.floor(this.clockSkewSeconds?.value));
dur.setNanos((Math.floor(this.clockSkewSeconds?.value % 1) * 10000));
req.setClockSkew(dur);
}
console.log(req.toObject());
this.mgmtService
.UpdateOIDCAppConfig(this.projectId, this.app.id, this.app.oidcConfig)
.UpdateOIDCAppConfig(req)
.then(() => {
this.toast.showInfo('APP.TOAST.OIDCUPDATED', true);
})
@@ -319,4 +354,12 @@ export class AppDetailComponent implements OnInit, OnDestroy {
public get accessTokenRoleAssertion(): AbstractControl | null {
return this.appForm.get('accessTokenRoleAssertion');
}
public get idTokenUserinfoAssertion(): AbstractControl | null {
return this.appForm.get('idTokenUserinfoAssertion');
}
public get clockSkewSeconds(): AbstractControl | null {
return this.appForm.get('clockSkewSeconds');
}
}
@@ -13,6 +13,7 @@ import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { MatRadioModule } from '@angular/material/radio';
import { MatSelectModule } from '@angular/material/select';
import { MatSlideToggleModule } from '@angular/material/slide-toggle';
import { MatSliderModule } from '@angular/material/slider';
import { MatStepperModule } from '@angular/material/stepper';
import { MatTooltipModule } from '@angular/material/tooltip';
import { TranslateModule } from '@ngx-translate/core';
@@ -61,6 +62,7 @@ import { AppsRoutingModule } from './apps-routing.module';
MatSlideToggleModule,
InputModule,
MetaLayoutModule,
MatSliderModule,
ChangesModule,
InfoSectionModule,
],
@@ -0,0 +1,50 @@
<app-card title="{{'USER.PASSWORDLESS.TITLE' | translate}}"
description="{{'USER.PASSWORDLESS.DESCRIPTION' | translate}}">
<app-refresh-table [loading]="loading$ | async" (refreshed)="getPasswordless()"
[dataSize]="dataSource?.data?.length">
<table class="table" mat-table [dataSource]="dataSource">
<ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef> {{ 'USER.PASSWORDLESS.NAME' | translate }} </th>
<td mat-cell *matCellDef="let mfa"><span *ngIf="mfa?.name" class="centered">
{{ mfa?.name }}
</span>
</td>
</ng-container>
<ng-container matColumnDef="state">
<th mat-header-cell *matHeaderCellDef> {{ 'USER.PASSWORDLESS.TABLESTATE' | translate }} </th>
<td mat-cell *matCellDef="let mfa"><span class="centered">
{{'USER.PASSWORDLESS.STATE.'+ mfa.state | translate}}
<i matTooltip="verified" *ngIf="mfa.state === MFAState.MFASTATE_READY"
class="verified las la-check-circle"></i>
</span>
</td>
</ng-container>
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef> {{ 'USER.PASSWORDLESS.TABLEACTIONS' | translate }} </th>
<td mat-cell *matCellDef="let mfa">
<button matTooltip="{{'ACTIONS.REMOVE' | translate}}" color="warn" mat-icon-button
(click)="deletePasswordless(mfa.id)">
<i class="las la-trash"></i>
</button>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr class="highlight" mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>
</app-refresh-table>
<div class="add-row">
<button class="button" (click)="addPasswordless()" mat-stroked-button color="primary"
matTooltip="{{'ACTIONS.NEW' | translate}}">
<i class="las la-fingerprint"></i>
{{'USER.PASSWORDLESS.U2F' | translate}}
</button>
</div>
<div class="table-wrapper">
<div class="spinner-container" *ngIf="loading$ | async">
<mat-spinner diameter="50"></mat-spinner>
</div>
</div>
</app-card>
@@ -0,0 +1,41 @@
.add-row {
display: flex;
margin: -.5rem;
flex-wrap: wrap;
.button {
margin: .5rem;
margin-top: 1rem;
.icon {
margin-right: .5rem;
}
}
}
.centered {
display: flex;
align-items: center;
i {
margin-left: 1rem;
color: var(--color-main);
}
}
.table {
width: 100%;
td,
th {
&:first-child {
padding-left: 0;
padding-right: 1rem;
}
&:last-child {
padding-right: 0;
}
}
}
@@ -0,0 +1,25 @@
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { AuthPasswordlessComponent } from './auth-passwordless.component';
describe('AuthPasswordlessComponent', () => {
let component: AuthPasswordlessComponent;
let fixture: ComponentFixture<AuthPasswordlessComponent>;
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [AuthPasswordlessComponent],
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(AuthPasswordlessComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,114 @@
import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
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 { MFAState, WebAuthNResponse, WebAuthNToken } 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 { _base64ToArrayBuffer } from '../../u2f-util';
import { DialogU2FComponent, U2FComponentDestination } from '../dialog-u2f/dialog-u2f.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;
}
@Component({
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();
@ViewChild(MatTable) public table!: MatTable<WebAuthNToken.AsObject>;
@ViewChild(MatSort) public sort!: MatSort;
public dataSource!: MatTableDataSource<WebAuthNToken.AsObject>;
public MFAState: any = MFAState;
public error: string = '';
constructor(private service: GrpcAuthService,
private toast: ToastService,
private dialog: MatDialog) { }
public ngOnInit(): void {
this.getPasswordless();
}
public ngOnDestroy(): void {
this.loadingSubject.complete();
}
public addPasswordless(): void {
this.service.AddMyPasswordless().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,
type: U2FComponentDestination.PASSWORDLESS,
},
});
dialogRef.afterClosed().subscribe(done => {
if (done) {
this.getPasswordless();
} else {
this.getPasswordless();
}
});
}
}, error => {
this.toast.showError(error);
});
}
public getPasswordless(): void {
this.service.GetMyPasswordless().then(passwordless => {
this.dataSource = new MatTableDataSource(passwordless.toObject().tokensList);
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);
});
}
});
}
}
@@ -51,6 +51,8 @@
</app-contact>
</app-card>
<app-auth-passwordless *ngIf="user" #mfaComponent></app-auth-passwordless>
<app-auth-user-mfa *ngIf="user" #mfaComponent></app-auth-user-mfa>
<app-card *ngIf="user?.id" title="{{ 'GRANTS.USER.TITLE' | translate }}"
@@ -8,8 +8,8 @@
<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 }}
<td mat-cell *matCellDef="let mfa"><span *ngIf="mfa?.attribute" class="centered">
{{ mfa?.attribute }}
</span>
</td>
</ng-container>
@@ -43,7 +43,7 @@
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"
<button class="button" (click)="addU2F()" mat-stroked-button color="primary"
matTooltip="{{'ACTIONS.NEW' | translate}}">
<i class="las la-fingerprint"></i>
{{'USER.MFA.U2F' | translate}}
@@ -8,19 +8,10 @@ import { MfaOtpResponse, MFAState, MfaType, MultiFactor, WebAuthNResponse } from
import { GrpcAuthService } from 'src/app/services/grpc-auth.service';
import { ToastService } from 'src/app/services/toast.service';
import { _base64ToArrayBuffer } from '../../u2f-util';
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; };
@@ -50,6 +41,7 @@ export class AuthUserMfaComponent implements OnInit, OnDestroy {
public error: string = '';
public otpAvailable: boolean = false;
constructor(private service: GrpcAuthService,
private toast: ToastService,
private dialog: MatDialog) { }
@@ -82,10 +74,6 @@ export class AuthUserMfaComponent implements OnInit, OnDestroy {
});
}
public verifyU2f(): void {
}
public addU2F(): void {
this.service.AddMyMfaU2F().then((u2fresp) => {
const webauthn: WebAuthNResponse.AsObject = u2fresp.toObject();
@@ -94,6 +82,9 @@ export class AuthUserMfaComponent implements OnInit, OnDestroy {
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));
}
const dialogRef = this.dialog.open(DialogU2FComponent, {
width: '400px',
data: {
@@ -17,19 +17,27 @@ export function _arrayBufferToBase64(buffer: any): string {
.replace(/=/g, '');
}
export enum U2FComponentDestination {
MFA = 'mfa',
PASSWORDLESS = 'passwordless',
}
@Component({
selector: 'app-dialog-u2f',
templateUrl: './dialog-u2f.component.html',
styleUrls: ['./dialog-u2f.component.scss'],
})
export class DialogU2FComponent {
private type!: U2FComponentDestination;
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) { }
@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();
@@ -61,16 +69,29 @@ export class DialogU2FComponent {
});
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);
if (this.type === U2FComponentDestination.MFA) {
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);
});
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 => {
@@ -0,0 +1,10 @@
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;
}
@@ -29,6 +29,7 @@ import { HasRolePipeModule } from 'src/app/pipes/has-role-pipe/has-role-pipe.mod
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 { AuthPasswordlessComponent } from './auth-user-detail/auth-passwordless/auth-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';
@@ -47,6 +48,7 @@ import { ShowKeyDialogModule } from './machine-keys/show-key-dialog/show-key-dia
import { MembershipsComponent } from './memberships/memberships.component';
import { PasswordComponent } from './password/password.component';
import { UserDetailRoutingModule } from './user-detail-routing.module';
import { PasswordlessComponent } from './user-detail/passwordless/passwordless.component';
import { UserDetailComponent } from './user-detail/user-detail.component';
import { UserMfaComponent } from './user-detail/user-mfa/user-mfa.component';
@@ -57,7 +59,9 @@ import { UserMfaComponent } from './user-detail/user-mfa/user-mfa.component';
DialogOtpComponent,
EditDialogComponent,
AuthUserMfaComponent,
AuthPasswordlessComponent,
UserMfaComponent,
PasswordlessComponent,
ThemeSettingComponent,
PasswordComponent,
CodeDialogComponent,
@@ -0,0 +1,43 @@
<app-card title="{{'USER.PASSWORDLESS.TITLE' | translate}}"
description="{{'USER.PASSWORDLESS.DESCRIPTION' | translate}}">
<app-refresh-table [loading]="loading$ | async" (refreshed)="getPasswordless()"
[dataSize]="dataSource?.data?.length">
<table class="table" mat-table [dataSource]="dataSource">
<ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef> {{ 'USER.PASSWORDLESS.NAME' | translate }} </th>
<td mat-cell *matCellDef="let mfa"><span *ngIf="mfa?.name" class="centered">
{{ mfa?.name }}
</span>
</td>
</ng-container>
<ng-container matColumnDef="state">
<th mat-header-cell *matHeaderCellDef> {{ 'USER.PASSWORDLESS.TABLESTATE' | translate }} </th>
<td mat-cell *matCellDef="let mfa"><span class="centered">
{{'USER.PASSWORDLESS.STATE.'+ mfa.state | translate}}
<i matTooltip="verified" *ngIf="mfa.state === MFAState.MFASTATE_READY"
class="verified las la-check-circle"></i>
</span>
</td>
</ng-container>
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef> {{ 'USER.PASSWORDLESS.TABLEACTIONS' | translate }} </th>
<td mat-cell *matCellDef="let mfa">
<button matTooltip="{{'ACTIONS.REMOVE' | translate}}" color="warn" mat-icon-button
(click)="deletePasswordless(mfa.id)">
<i class="las la-trash"></i>
</button>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr class="highlight" mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>
</app-refresh-table>
<div class="table-wrapper">
<div class="spinner-container" *ngIf="loading$ | async">
<mat-spinner diameter="50"></mat-spinner>
</div>
</div>
</app-card>
@@ -0,0 +1,26 @@
.centered {
display: flex;
align-items: center;
i {
margin-left: 1rem;
color: var(--color-main);
}
}
.table {
width: 100%;
td,
th {
&:first-child {
padding-left: 0;
padding-right: 1rem;
}
&:last-child {
padding-right: 0;
}
}
}
@@ -0,0 +1,25 @@
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { AuthPasswordlessComponent } from './auth-passwordless.component';
describe('AuthPasswordlessComponent', () => {
let component: AuthPasswordlessComponent;
let fixture: ComponentFixture<AuthPasswordlessComponent>;
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [AuthPasswordlessComponent],
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(AuthPasswordlessComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,84 @@
import { Component, Input, OnDestroy, OnInit, ViewChild } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
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 { MFAState, WebAuthNToken } from 'src/app/proto/generated/auth_pb';
import { UserView } from 'src/app/proto/generated/management_pb';
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;
}
@Component({
selector: 'app-passwordless',
templateUrl: './passwordless.component.html',
styleUrls: ['./passwordless.component.scss'],
})
export class PasswordlessComponent implements OnInit, OnDestroy {
@Input() private user!: UserView.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>;
public MFAState: any = MFAState;
public error: string = '';
constructor(private service: ManagementService,
private toast: ToastService,
private dialog: MatDialog) { }
public ngOnInit(): void {
this.getPasswordless();
}
public ngOnDestroy(): void {
this.loadingSubject.complete();
}
public getPasswordless(): void {
this.service.GetPasswordless(this.user.id).then(passwordless => {
console.log(passwordless.toObject().tokensList);
this.dataSource = new MatTableDataSource(passwordless.toObject().tokensList);
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.RemovePasswordless(id, this.user.id).then(() => {
this.toast.showInfo('USER.TOAST.PASSWORDLESSREMOVED', true);
this.getPasswordless();
}).catch(error => {
this.toast.showError(error);
});
}
});
}
}
@@ -84,6 +84,8 @@
</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>
<app-card *ngIf="user?.id" title="{{ 'GRANTS.USER.TITLE' | translate }}"
@@ -8,8 +8,8 @@
<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 }}
<td mat-cell *matCellDef="let mfa"><span *ngIf="mfa?.attribute" class="centered">
{{ mfa.attribute }}
</span>
</td>
</ng-container>
@@ -47,6 +47,7 @@ export class UserMfaComponent implements OnInit, OnDestroy {
public getMFAs(): void {
this.mgmtUserService.getUserMfas(this.user.id).then(mfas => {
console.log(mfas.toObject().mfasList);
this.dataSource = new MatTableDataSource(mfas.toObject().mfasList);
this.dataSource.sort = this.sort;
}).catch(error => {
@@ -81,7 +82,8 @@ export class UserMfaComponent implements OnInit, OnDestroy {
this.toast.showError(error);
});
} else if (type === MfaType.MFATYPE_U2F && id) {
this.mgmtUserService.RemoveMfaU2F(id).then(() => {
console.log(this.user.id, id);
this.mgmtUserService.RemoveMfaU2F(this.user.id, id).then(() => {
this.toast.showInfo('USER.TOAST.U2FREMOVED', true);
const index = this.dataSource.data.findIndex(mfa => mfa.type === type);