diff --git a/console/src/app/modules/mfa-table/dialog-add-type/dialog-add-type.component.html b/console/src/app/modules/mfa-table/dialog-add-type/dialog-add-type.component.html new file mode 100644 index 0000000000..f77383e17f --- /dev/null +++ b/console/src/app/modules/mfa-table/dialog-add-type/dialog-add-type.component.html @@ -0,0 +1,19 @@ +

{{data.title | translate}}

+
+

{{data.desc | translate}}

+ + + {{'MFA.TYPE' | translate}} + + + {{(data.componentType == LoginMethodComponentType.SecondFactor ? 'MFA.SECONDFACTORTYPES.': LoginMethodComponentType.MultiFactor ? 'MFA.MULTIFACTORTYPES.': '')+mfa | translate}} + + + +
+
+ + +
\ No newline at end of file diff --git a/console/src/app/modules/mfa-table/dialog-add-type/dialog-add-type.component.scss b/console/src/app/modules/mfa-table/dialog-add-type/dialog-add-type.component.scss new file mode 100644 index 0000000000..2a10cfe140 --- /dev/null +++ b/console/src/app/modules/mfa-table/dialog-add-type/dialog-add-type.component.scss @@ -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; + } +} diff --git a/console/src/app/modules/mfa-table/dialog-add-type/dialog-add-type.component.ts b/console/src/app/modules/mfa-table/dialog-add-type/dialog-add-type.component.ts new file mode 100644 index 0000000000..befb157abf --- /dev/null +++ b/console/src/app/modules/mfa-table/dialog-add-type/dialog-add-type.component.ts @@ -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 = []; + constructor(public dialogRef: MatDialogRef, + @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); + } +} diff --git a/console/src/app/modules/mfa-table/mfa-table.component.html b/console/src/app/modules/mfa-table/mfa-table.component.html new file mode 100644 index 0000000000..3d45e4c2f5 --- /dev/null +++ b/console/src/app/modules/mfa-table/mfa-table.component.html @@ -0,0 +1,15 @@ +
+ +
+
+
+ + {{(componentType == LoginMethodComponentType.SecondFactor ? 'MFA.SECONDFACTORTYPES.': LoginMethodComponentType.MultiFactor ? 'MFA.MULTIFACTORTYPES.': '')+mfa | translate}} +
+
+ add +
+
\ No newline at end of file diff --git a/console/src/app/modules/mfa-table/mfa-table.component.scss b/console/src/app/modules/mfa-table/mfa-table.component.scss new file mode 100644 index 0000000000..f0fe7e80a7 --- /dev/null +++ b/console/src/app/modules/mfa-table/mfa-table.component.scss @@ -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; + } + } + } +} diff --git a/console/src/app/modules/mfa-table/mfa-table.component.spec.ts b/console/src/app/modules/mfa-table/mfa-table.component.spec.ts new file mode 100644 index 0000000000..24992ccaff --- /dev/null +++ b/console/src/app/modules/mfa-table/mfa-table.component.spec.ts @@ -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; + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + declarations: [MfaTableComponent], + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(MfaTableComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/console/src/app/modules/mfa-table/mfa-table.component.ts b/console/src/app/modules/mfa-table/mfa-table.component.ts new file mode 100644 index 0000000000..401e90af48 --- /dev/null +++ b/console/src/app/modules/mfa-table/mfa-table.component.ts @@ -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 = []; + + private loadingSubject: BehaviorSubject = new BehaviorSubject(false); + public loading$: Observable = 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 { + 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); + } +} diff --git a/console/src/app/modules/mfa-table/mfa-table.module.ts b/console/src/app/modules/mfa-table/mfa-table.module.ts new file mode 100644 index 0000000000..8db0c2cf9d --- /dev/null +++ b/console/src/app/modules/mfa-table/mfa-table.module.ts @@ -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 { } diff --git a/console/src/app/modules/policies/login-policy/login-policy.component.html b/console/src/app/modules/policies/login-policy/login-policy.component.html index dcd843ab65..90fca4d406 100644 --- a/console/src/app/modules/policies/login-policy/login-policy.component.html +++ b/console/src/app/modules/policies/login-policy/login-policy.component.html @@ -44,8 +44,34 @@

{{'POLICY.DATA.ALLOWEXTERNALIDP_DESC' | translate}}

+
+ + {{'POLICY.DATA.FORCEMFA' | translate}} + +

{{'POLICY.DATA.FORCEMFA_DESC' | translate}}

+
+ + +
+ +

{{ 'MFA.LIST.MULTIFACTORTITLE' | translate }}

+

{{ 'MFA.LIST.MULTIFACTORDESCRIPTION' | translate }}

+ + + +

{{ 'MFA.LIST.SECONDFACTORTITLE' | translate }}

+

{{ 'MFA.LIST.SECONDFACTORDESCRIPTION' | translate }}

+ + +

{{'LOGINPOLICY.IDPS' | translate}}

@@ -63,9 +89,6 @@
- -

{{ 'IDP.LIST.TITLE' | translate }}

{{ 'IDP.LIST.DESCRIPTION' | translate }}

@@ -73,4 +96,4 @@ [disabled]="([serviceType == PolicyComponentServiceType.ADMIN ? 'iam.idp.write' : serviceType == PolicyComponentServiceType.MGMT ? 'org.idp.write' : ''] | hasRole | async) == false">
- \ No newline at end of file + diff --git a/console/src/app/modules/policies/login-policy/login-policy.component.scss b/console/src/app/modules/policies/login-policy/login-policy.component.scss index ea690233c2..5d2b27fbc2 100644 --- a/console/src/app/modules/policies/login-policy/login-policy.component.scss +++ b/console/src/app/modules/policies/login-policy/login-policy.component.scss @@ -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; +} diff --git a/console/src/app/modules/policies/login-policy/login-policy.component.ts b/console/src/app/modules/policies/login-policy/login-policy.component.ts index 119a251433..69d1be4667 100644 --- a/console/src/app/modules/policies/login-policy/login-policy.component.ts +++ b/console/src/app/modules/policies/login-policy/login-policy.component.ts @@ -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); } } diff --git a/console/src/app/modules/policies/login-policy/login-policy.module.ts b/console/src/app/modules/policies/login-policy/login-policy.module.ts index 8ea3fe5a67..65cfe1cbf6 100644 --- a/console/src/app/modules/policies/login-policy/login-policy.module.ts +++ b/console/src/app/modules/policies/login-policy/login-policy.module.ts @@ -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, ], }) diff --git a/console/src/app/pages/users/user-detail/auth-user-detail/auth-user-mfa/auth-user-mfa.component.html b/console/src/app/pages/users/user-detail/auth-user-detail/auth-user-mfa/auth-user-mfa.component.html index 9f2b0c7ecd..d3299fc4a3 100644 --- a/console/src/app/pages/users/user-detail/auth-user-detail/auth-user-mfa/auth-user-mfa.component.html +++ b/console/src/app/pages/users/user-detail/auth-user-detail/auth-user-mfa/auth-user-mfa.component.html @@ -1,11 +1,19 @@ - + + + + + + @@ -35,6 +43,11 @@ matTooltip="{{'ACTIONS.NEW' | translate}}"> {{'USER.MFA.OTP' | translate}} +
diff --git a/console/src/app/pages/users/user-detail/auth-user-detail/auth-user-mfa/auth-user-mfa.component.scss b/console/src/app/pages/users/user-detail/auth-user-detail/auth-user-mfa/auth-user-mfa.component.scss index 411f391e2f..3f9facae7c 100644 --- a/console/src/app/pages/users/user-detail/auth-user-detail/auth-user-mfa/auth-user-mfa.component.scss +++ b/console/src/app/pages/users/user-detail/auth-user-detail/auth-user-mfa/auth-user-mfa.component.scss @@ -2,6 +2,7 @@ .add-row { display: flex; margin: -.5rem; + flex-wrap: wrap; .button { margin: .5rem; diff --git a/console/src/app/pages/users/user-detail/auth-user-detail/auth-user-mfa/auth-user-mfa.component.ts b/console/src/app/pages/users/user-detail/auth-user-detail/auth-user-mfa/auth-user-mfa.component.ts index ea9baa7253..052dd46a14 100644 --- a/console/src/app/pages/users/user-detail/auth-user-detail/auth-user-mfa/auth-user-mfa.component.ts +++ b/console/src/app/pages/users/user-detail/auth-user-detail/auth-user-mfa/auth-user-mfa.component.ts @@ -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 = new BehaviorSubject(false); public loading$: Observable = 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 { } }); } + + } diff --git a/console/src/app/pages/users/user-detail/auth-user-detail/dialog-otp/dialog-otp.component.html b/console/src/app/pages/users/user-detail/auth-user-detail/dialog-otp/dialog-otp.component.html index 9a4eb065e1..0d5aa2426b 100644 --- a/console/src/app/pages/users/user-detail/auth-user-detail/dialog-otp/dialog-otp.component.html +++ b/console/src/app/pages/users/user-detail/auth-user-detail/dialog-otp/dialog-otp.component.html @@ -1,6 +1,6 @@

{{'USER.MFA.OTP_DIALOG_TITLE' | translate}}

-

{{'USER.MFA.OTP_DIALOG_DESCRIPTION' | translate}}

+

{{'USER.MFA.OTP_DIALOG_DESCRIPTION' | translate}}

@@ -15,4 +15,4 @@ -
\ No newline at end of file +
diff --git a/console/src/app/pages/users/user-detail/auth-user-detail/dialog-u2f/dialog-u2f.component.html b/console/src/app/pages/users/user-detail/auth-user-detail/dialog-u2f/dialog-u2f.component.html new file mode 100644 index 0000000000..b649faae0c --- /dev/null +++ b/console/src/app/pages/users/user-detail/auth-user-detail/dialog-u2f/dialog-u2f.component.html @@ -0,0 +1,18 @@ +

{{'USER.MFA.U2F_DIALOG_TITLE' | translate}}

+
+

{{'USER.MFA.U2F_DIALOG_DESCRIPTION' | translate}}

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

{{error}}

+
+
+ + +
diff --git a/console/src/app/pages/users/user-detail/auth-user-detail/dialog-u2f/dialog-u2f.component.scss b/console/src/app/pages/users/user-detail/auth-user-detail/dialog-u2f/dialog-u2f.component.scss new file mode 100644 index 0000000000..94688b0fb8 --- /dev/null +++ b/console/src/app/pages/users/user-detail/auth-user-detail/dialog-u2f/dialog-u2f.component.scss @@ -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; + } +} diff --git a/console/src/app/pages/users/user-detail/auth-user-detail/dialog-u2f/dialog-u2f.component.ts b/console/src/app/pages/users/user-detail/auth-user-detail/dialog-u2f/dialog-u2f.component.ts new file mode 100644 index 0000000000..5ca1afb86c --- /dev/null +++ b/console/src/app/pages/users/user-detail/auth-user-detail/dialog-u2f/dialog-u2f.component.ts @@ -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, + @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); + }); + } + + } +} diff --git a/console/src/app/pages/users/user-detail/user-detail.module.ts b/console/src/app/pages/users/user-detail/user-detail.module.ts index a217c0e7b8..f33c87fc5b 100644 --- a/console/src/app/pages/users/user-detail/user-detail.module.ts +++ b/console/src/app/pages/users/user-detail/user-detail.module.ts @@ -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, diff --git a/console/src/app/pages/users/user-detail/user-detail/user-mfa/user-mfa.component.html b/console/src/app/pages/users/user-detail/user-detail/user-mfa/user-mfa.component.html index eb234e45d8..8af7f0a87c 100644 --- a/console/src/app/pages/users/user-detail/user-detail/user-mfa/user-mfa.component.html +++ b/console/src/app/pages/users/user-detail/user-detail/user-mfa/user-mfa.component.html @@ -1,11 +1,19 @@ - +
{{ 'USER.MFA.TABLETYPE' | translate }} {{'USER.MFA.TYPE.'+ mfa.type | translate}} {{ 'USER.MFA.ATTRIBUTE' | translate }} + {{ mfa.attr }} + + {{ 'USER.MFA.TABLESTATE' | translate }} @@ -20,7 +28,7 @@ {{ 'USER.MFA.TABLEACTIONS' | translate }}
+ + + + + diff --git a/console/src/app/pages/users/user-detail/user-detail/user-mfa/user-mfa.component.ts b/console/src/app/pages/users/user-detail/user-detail/user-mfa/user-mfa.component.ts index e91c170d77..58e25f23cf 100644 --- a/console/src/app/pages/users/user-detail/user-detail/user-mfa/user-mfa.component.ts +++ b/console/src/app/pages/users/user-detail/user-detail/user-mfa/user-mfa.component.ts @@ -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 = new BehaviorSubject([]); private loadingSubject: BehaviorSubject = new BehaviorSubject(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); }); diff --git a/console/src/app/pages/users/user-list/user-table/user-table.component.html b/console/src/app/pages/users/user-list/user-table/user-table.component.html index a508c5d858..74d0cad53e 100644 --- a/console/src/app/pages/users/user-list/user-table/user-table.component.html +++ b/console/src/app/pages/users/user-list/user-table/user-table.component.html @@ -1,4 +1,4 @@ - diff --git a/console/src/app/services/admin.service.ts b/console/src/app/services/admin.service.ts index adad72a275..4b14b81f3c 100644 --- a/console/src/app/services/admin.service.ts +++ b/console/src/app/services/admin.service.ts @@ -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 { + const req = new Empty(); + return this.grpcService.admin.getDefaultLoginPolicyMultiFactors(req); + } + + public addMultiFactorToDefaultLoginPolicy(req: MultiFactor): Promise { + return this.grpcService.admin.addMultiFactorToDefaultLoginPolicy(req); + } + + public RemoveMultiFactorFromDefaultLoginPolicy(req: MultiFactor): Promise { + return this.grpcService.admin.removeMultiFactorFromDefaultLoginPolicy(req); + } + + public GetDefaultLoginPolicySecondFactors(): Promise { + const req = new Empty(); + return this.grpcService.admin.getDefaultLoginPolicySecondFactors(req); + } + + public AddSecondFactorToDefaultLoginPolicy(req: SecondFactor): Promise { + return this.grpcService.admin.addSecondFactorToDefaultLoginPolicy(req); + } + + public RemoveSecondFactorFromDefaultLoginPolicy(req: SecondFactor): Promise { + return this.grpcService.admin.removeSecondFactorFromDefaultLoginPolicy(req); + } + public GetIamMemberRoles(): Promise { const req = new Empty(); return this.grpcService.admin.getIamMemberRoles(req); diff --git a/console/src/app/services/grpc-auth.service.ts b/console/src/app/services/grpc-auth.service.ts index 67d5f4def4..5d54ddc061 100644 --- a/console/src/app/services/grpc-auth.service.ts +++ b/console/src/app/services/grpc-auth.service.ts @@ -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 { + return this.grpcService.auth.addMyMfaU2F( + new Empty(), + ); + } + + public RemoveMyMfaU2F(id: string): Promise { + const req = new WebAuthNTokenID(); + req.setId(id); + return this.grpcService.auth.removeMyMfaU2F(req); + } + + public VerifyMyMfaU2F(credential: string, tokenname: string): Promise { + const req = new VerifyWebAuthN(); + req.setPublicKeyCredential(credential); + req.setTokenName(tokenname); + + return this.grpcService.auth.verifyMyMfaU2F( + req, + ); + } + public RemoveMfaOTP(): Promise { return this.grpcService.auth.removeMfaOTP( - new Empty(), + new Empty(), ); } diff --git a/console/src/app/services/mgmt.service.ts b/console/src/app/services/mgmt.service.ts index 31fef046a0..d2ce0fbb32 100644 --- a/console/src/app/services/mgmt.service.ts +++ b/console/src/app/services/mgmt.service.ts @@ -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 { + const req = new Empty(); + return this.grpcService.mgmt.getLoginPolicyMultiFactors(req); + } + + public AddMultiFactorToLoginPolicy(req: MultiFactor): Promise { + return this.grpcService.mgmt.addMultiFactorToLoginPolicy(req); + } + + public RemoveMultiFactorFromLoginPolicy(req: MultiFactor): Promise { + return this.grpcService.mgmt.removeMultiFactorFromLoginPolicy(req); + } + + public GetLoginPolicySecondFactors(): Promise { + const req = new Empty(); + return this.grpcService.mgmt.getLoginPolicySecondFactors(req); + } + + public AddSecondFactorToLoginPolicy(req: SecondFactor): Promise { + return this.grpcService.mgmt.addSecondFactorToLoginPolicy(req); + } + + public RemoveSecondFactorFromLoginPolicy(req: SecondFactor): Promise { + return this.grpcService.mgmt.removeSecondFactorFromLoginPolicy(req); + } + public GetLoginPolicy(): Promise { 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 { + const req = new WebAuthNTokenID(); + req.setId(id); + return this.grpcService.mgmt.removeMfaU2F(req); + } + public SaveUserProfile( id: string, firstName?: string, diff --git a/console/src/assets/i18n/de.json b/console/src/assets/i18n/de.json index a003ec328b..81d6e41d33 100644 --- a/console/src/assets/i18n/de.json +++ b/console/src/assets/i18n/de.json @@ -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", diff --git a/console/src/assets/i18n/en.json b/console/src/assets/i18n/en.json index b4f01ba663..c2ab9c5a97 100644 --- a/console/src/assets/i18n/en.json +++ b/console/src/assets/i18n/en.json @@ -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", diff --git a/internal/api/grpc/management/user.go b/internal/api/grpc/management/user.go index 031503118c..a4d9ad2cd0 100644 --- a/internal/api/grpc/management/user.go +++ b/internal/api/grpc/management/user.go @@ -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) diff --git a/internal/api/grpc/management/user_converter.go b/internal/api/grpc/management/user_converter.go index 645a07c0ea..6b1dfe07eb 100644 --- a/internal/api/grpc/management/user_converter.go +++ b/internal/api/grpc/management/user_converter.go @@ -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, } } diff --git a/internal/management/repository/eventsourcing/eventstore/user.go b/internal/management/repository/eventsourcing/eventstore/user.go index a9c77c84b8..08e5ced20f 100644 --- a/internal/management/repository/eventsourcing/eventstore/user.go +++ b/internal/management/repository/eventsourcing/eventstore/user.go @@ -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) { diff --git a/internal/management/repository/user.go b/internal/management/repository/user.go index 92a2364c11..b1a23581d9 100644 --- a/internal/management/repository/user.go +++ b/internal/management/repository/user.go @@ -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 diff --git a/pkg/grpc/management/proto/management.proto b/pkg/grpc/management/proto/management.proto index cd02b8f79e..e385818c93 100644 --- a/pkg/grpc/management/proto/management.proto +++ b/pkg/grpc/management/proto/management.proto @@ -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 {
{{ 'USER.MFA.TABLETYPE' | translate }} {{'USER.MFA.TYPE.'+ mfa.type | translate}} {{ 'USER.MFA.ATTRIBUTE' | translate }} + {{ mfa.attr }} + + {{ 'USER.MFA.TABLESTATE' | translate }} @@ -21,7 +29,7 @@ {{ 'USER.MFA.TABLEACTIONS' | translate }}