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 }} |
@@ -21,7 +29,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 {