Skip to content

Commit 40cea53

Browse files
committed
feat(profile): enforce MFA
* added functionality to enforce MFA
1 parent 879b32c commit 40cea53

File tree

6 files changed

+164
-45
lines changed

6 files changed

+164
-45
lines changed
Lines changed: 36 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,39 @@
1-
<div class="mb-5" *ngIf="displayImageBlock">
2-
<h1 class="page-subtitle">{{'AUTHENTICATION.TITLE' | customTranslate | translate}}</h1>
3-
<p>{{'AUTHENTICATION.ANTI_PHISHING_INFO' | customTranslate | translate}}</p>
4-
<div *ngIf="imageSrc && imageSrc.length">
5-
<img [src]="imageSrc" alt="" class="img-size" />
1+
<div [hidden]="loadingMfa || loadingImg">
2+
<div class="mb-5" *ngIf="displayImageBlock">
3+
<h1 class="page-subtitle">{{'AUTHENTICATION.TITLE' | customTranslate | translate}}</h1>
4+
<p>{{'AUTHENTICATION.ANTI_PHISHING_INFO' | customTranslate | translate}}</p>
5+
<div *ngIf="imageSrc && imageSrc.length">
6+
<img [src]="imageSrc" alt="" class="img-size" />
7+
</div>
8+
<button (click)="onAddImg()" class="m-1 action-button" color="accent" mat-flat-button>
9+
{{'AUTHENTICATION.NEW_IMG' | customTranslate | translate}}
10+
</button>
11+
<button
12+
(click)="onDeleteImg()"
13+
class="m-1"
14+
color="warn"
15+
[disabled]="!imgAtt || !imgAtt.value"
16+
mat-flat-button>
17+
{{'AUTHENTICATION.DELETE_IMG' | customTranslate | translate}}
18+
</button>
619
</div>
7-
<button (click)="onAddImg()" class="m-1 action-button" color="accent" mat-flat-button>
8-
{{'AUTHENTICATION.NEW_IMG' | customTranslate | translate}}
9-
</button>
10-
<button
11-
(click)="onDeleteImg()"
12-
class="m-1"
13-
color="warn"
14-
[disabled]="!imgAtt || !imgAtt.value"
15-
mat-flat-button>
16-
{{'AUTHENTICATION.DELETE_IMG' | customTranslate | translate}}
20+
21+
<h1 class="page-subtitle">{{'AUTHENTICATION.MFA' | customTranslate | translate}}</h1>
22+
<span
23+
[matTooltip]="'AUTHENTICATION.MFA_DISABLED' | customTranslate | translate"
24+
[matTooltipDisabled]="mfaAvailable"
25+
matTooltipPosition="right">
26+
<mat-slide-toggle
27+
[disabled]="!mfaAvailable"
28+
#toggle
29+
color="primary"
30+
>{{'AUTHENTICATION.MFA_TOGGLE' | customTranslate | translate}}</mat-slide-toggle
31+
>
32+
</span>
33+
34+
<br />
35+
<button mat-flat-button class="mt-3" (click)="redirectToMfa()" color="accent">
36+
{{'AUTHENTICATION.MFA_INFO'|translate}}
1737
</button>
1838
</div>
19-
20-
<h1 class="page-subtitle">{{'AUTHENTICATION.MFA' | customTranslate | translate}}</h1>
21-
<span
22-
>{{'AUTHENTICATION.MFA_INFO'|translate}}<a [href]="mfaUrl">{{mfaUrl}}</a></span
23-
>
39+
<mat-spinner class="ml-auto mr-auto" *ngIf="loadingMfa || loadingImg"></mat-spinner>

apps/user-profile/src/app/pages/settings-page/settings-authorization/settings-authentication.component.ts

Lines changed: 116 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,40 @@
1-
import { Component, OnInit } from '@angular/core';
1+
import { AfterViewInit, Component, OnInit, ViewChild } from '@angular/core';
22
import { MatDialog } from '@angular/material/dialog';
33
import { getDefaultDialogConfig } from '@perun-web-apps/perun/utils';
44
import { AddAuthImgDialogComponent } from '../../../components/dialogs/add-auth-img-dialog/add-auth-img-dialog.component';
55
import { Attribute, AttributesManagerService } from '@perun-web-apps/perun/openapi';
6-
import { StoreService } from '@perun-web-apps/perun/services';
6+
import { AuthService, StoreService } from '@perun-web-apps/perun/services';
77
import { RemoveStringValueDialogComponent } from '../../../components/dialogs/remove-string-value-dialog/remove-string-value-dialog.component';
88
import { TranslateService } from '@ngx-translate/core';
9+
import { OAuthService } from 'angular-oauth2-oidc';
10+
import { MatSlideToggle } from '@angular/material/slide-toggle';
911

1012
@Component({
1113
selector: 'perun-web-apps-settings-authentication',
1214
templateUrl: './settings-authentication.component.html',
1315
styleUrls: ['./settings-authentication.component.scss'],
1416
})
15-
export class SettingsAuthenticationComponent implements OnInit {
17+
export class SettingsAuthenticationComponent implements OnInit, AfterViewInit {
18+
@ViewChild('toggle') toggle: MatSlideToggle;
19+
1620
removeDialogTitle: string;
1721
imgAtt: Attribute;
1822
imageSrc = '';
1923
removeDialogDescription: string;
2024
mfaUrl = '';
2125
displayImageBlock: boolean;
26+
mfaAvailable = false;
27+
mfaApiUrl = '';
28+
loadingMfa = false;
29+
loadingImg = false;
2230

2331
constructor(
2432
private dialog: MatDialog,
2533
private attributesManagerService: AttributesManagerService,
2634
private store: StoreService,
27-
private translate: TranslateService
35+
private translate: TranslateService,
36+
private oauthService: OAuthService,
37+
private authService: AuthService
2838
) {
2939
translate
3040
.get('AUTHENTICATION.DELETE_IMG_DIALOG_TITLE')
@@ -34,7 +44,15 @@ export class SettingsAuthenticationComponent implements OnInit {
3444
.subscribe((res) => (this.removeDialogDescription = res));
3545
}
3646

47+
ngAfterViewInit(): void {
48+
this.toggle.change.subscribe((change) => {
49+
this.reAuthenticate(change.checked);
50+
});
51+
}
52+
3753
ngOnInit(): void {
54+
this.loadingMfa = true;
55+
this.loadingImg = true;
3856
this.translate.onLangChange.subscribe(() => {
3957
this.translate
4058
.get('AUTHENTICATION.DELETE_IMG_DIALOG_TITLE')
@@ -45,9 +63,58 @@ export class SettingsAuthenticationComponent implements OnInit {
4563
this.mfaUrl = this.store.get('mfa', 'url_' + this.translate.currentLang);
4664
});
4765
this.mfaUrl = this.store.get('mfa', 'url_' + this.translate.currentLang);
66+
this.mfaApiUrl = this.store.get('mfa', 'api_url');
67+
fetch(this.mfaApiUrl + 'mfaAvailable', {
68+
method: 'GET',
69+
headers: { Authorization: 'Bearer ' + this.oauthService.getIdToken() },
70+
})
71+
.then((response) => response.text())
72+
.then((responseText) => {
73+
this.mfaAvailable = responseText === 'true';
74+
if (this.mfaAvailable) {
75+
this.loadMfa();
76+
}
77+
})
78+
.catch((e) => {
79+
console.error(e);
80+
this.loadingMfa = false;
81+
});
82+
4883
this.loadImage();
4984
}
5085

86+
private loadMfa(): void {
87+
const mfaRoute = sessionStorage.getItem('mfa_route');
88+
if (mfaRoute) {
89+
const enforceMfa = sessionStorage.getItem('enforce_mfa');
90+
this.enableMfa(enforceMfa === 'true')
91+
.then((res) => {
92+
if (res.ok && enforceMfa === 'true') {
93+
this.toggle.toggle();
94+
}
95+
this.loadingMfa = false;
96+
})
97+
.catch((e) => {
98+
console.error(e);
99+
this.loadingMfa = false;
100+
});
101+
} else {
102+
const enforceMfaAttributeName = this.store.get('mfa', 'enforce_mfa_attribute');
103+
this.attributesManagerService
104+
.getUserAttributeByName(this.store.getPerunPrincipal().userId, enforceMfaAttributeName)
105+
.subscribe((attr) => {
106+
if (attr.value) {
107+
this.toggle.toggle();
108+
}
109+
this.loadingMfa = false;
110+
});
111+
}
112+
if (sessionStorage.getItem('mfa_route')) {
113+
sessionStorage.removeItem('enforce_mfa');
114+
sessionStorage.removeItem('mfa_route');
115+
}
116+
}
117+
51118
onAddImg() {
52119
const config = getDefaultDialogConfig();
53120
config.width = '500px';
@@ -62,13 +129,29 @@ export class SettingsAuthenticationComponent implements OnInit {
62129
});
63130
}
64131

65-
// private transformTextToImg(text: string) {
66-
// const canvas = document.createElement('canvas');
67-
// const context = canvas.getContext('2d');
68-
// context.font = "100px Calibri";
69-
// context.fillText(text, 1, 70);
70-
// return canvas.toDataURL('image/png');
71-
// }
132+
reAuthenticate(enforceMfa: boolean): void {
133+
sessionStorage.setItem('enforce_mfa', enforceMfa.toString());
134+
sessionStorage.setItem('mfa_route', '/profile/settings/auth');
135+
localStorage.removeItem('refresh_token');
136+
this.oauthService.logOut(true);
137+
sessionStorage.setItem('auth:redirect', location.pathname);
138+
sessionStorage.setItem('auth:queryParams', location.search.substring(1));
139+
this.authService.loadConfigData();
140+
this.oauthService.loadDiscoveryDocumentAndLogin();
141+
}
142+
143+
enableMfa(value: boolean): Promise<Response> {
144+
const idToken = this.oauthService.getIdToken();
145+
const path = `mfaEnforced`;
146+
const url = `${this.mfaApiUrl}${path}`;
147+
const body = `value=${value}`;
148+
149+
return fetch(url, {
150+
method: 'PUT',
151+
body: body,
152+
headers: { Authorization: `Bearer ${idToken}` },
153+
});
154+
}
72155

73156
onDeleteImg() {
74157
const config = getDefaultDialogConfig();
@@ -95,17 +178,28 @@ export class SettingsAuthenticationComponent implements OnInit {
95178
this.displayImageBlock = this.store.get('mfa', 'enable_security_image');
96179
this.attributesManagerService
97180
.getUserAttributeByName(this.store.getPerunPrincipal().userId, imgAttributeName)
98-
.subscribe((attr) => {
99-
if (!attr) {
100-
this.attributesManagerService
101-
.getAttributeDefinitionByName(imgAttributeName)
102-
.subscribe((att) => {
103-
this.imgAtt = att as Attribute;
104-
});
105-
} else {
106-
this.imgAtt = attr;
107-
this.imageSrc = this.imgAtt.value as unknown as string;
181+
.subscribe(
182+
(attr) => {
183+
if (!attr) {
184+
this.attributesManagerService
185+
.getAttributeDefinitionByName(imgAttributeName)
186+
.subscribe((att) => {
187+
this.imgAtt = att as Attribute;
188+
});
189+
} else {
190+
this.imgAtt = attr;
191+
this.imageSrc = this.imgAtt.value as unknown as string;
192+
}
193+
this.loadingImg = false;
194+
},
195+
(e) => {
196+
console.error(e);
197+
this.loadingImg = false;
108198
}
109-
});
199+
);
200+
}
201+
202+
redirectToMfa(): void {
203+
window.open(this.mfaUrl, '_blank');
110204
}
111205
}

apps/user-profile/src/assets/config/defaultConfig.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@
7070
"api_url": "https://id.muni.cz/mfaapi/",
7171
"enable_security_image": true,
7272
"security_image_attribute": "urn:perun:user:attribute-def:def:securityImage:mu",
73+
"enforce_mfa_attribute": "urn:perun:user:attribute-def:def:mfaEnforced:mu",
7374
"url_en": "https://mfa.aai.muni.cz/",
7475
"url_cs": "https://mfa.aai.muni.cz/"
7576
},

apps/user-profile/src/assets/i18n/cs.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,12 +136,14 @@
136136
"AUTHENTICATION": {
137137
"TITLE": "Bezpečnostní obrázek",
138138
"MFA": "Vícefázové ověření",
139+
"MFA_TOGGLE": "Zapnout vícefázové ověření pro všechny služby",
140+
"MFA_DISABLED": "Potřebujete mít alespoň jeden aktivní MFA token.",
139141
"NEW_IMG": "Nový obrázek",
140142
"DELETE_IMG": "Vymazat obrázek",
141143
"ANTI_PHISHING_INFO": "Tento bezpečnostní obrázek se vám ukáže před tím, než zadáte heslo, ujistíte se tak, že se nepřihlašujete na podvrženou stránku",
142144
"DELETE_IMG_DIALOG_TITLE": "Vymazat proti-phishingový obrázek",
143145
"DELETE_IMG_DIALOG_DESC": "Váš bezpečnostní obrázek bude odstraněn a nebude použit během autentizace.",
144-
"MFA_INFO": "Pro správu prostředků vícefázového ověření (MFA) navštivte "
146+
"MFA_INFO": "Spravovat moje prostředky vícefázového ověření (MFA)"
145147
},
146148
"DIALOGS": {
147149
"CHANGE_EMAIL": {

apps/user-profile/src/assets/i18n/en.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,12 +136,14 @@
136136
"AUTHENTICATION": {
137137
"TITLE": "Security image",
138138
"MFA": "Multi-factor authentication",
139+
"MFA_TOGGLE": "Turn on multi-factor authentication for all services",
140+
"MFA_DISABLED": "You need to have at least one active MFA token.",
139141
"NEW_IMG": "New image",
140142
"DELETE_IMG": "Delete image",
141143
"ANTI_PHISHING_INFO": "You will be shown this security image before you enter your password so you will know that you are visiting the right site",
142144
"DELETE_IMG_DIALOG_TITLE": "Delete anti-phishing image",
143145
"DELETE_IMG_DIALOG_DESC": "Your security image will be deleted and will not be used during authentication process.",
144-
"MFA_INFO": "To manage MFA tokens go to "
146+
"MFA_INFO": "Manage my MFA tokens"
145147
},
146148
"DIALOGS": {
147149
"CHANGE_EMAIL": {

libs/perun/services/src/lib/auth.service.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,11 @@ export class AuthService {
4444
) {
4545
customQueryParams['prompt'] = 'consent';
4646
}
47-
47+
if (sessionStorage.getItem('mfa_route')) {
48+
customQueryParams['acr_values'] = 'https://refeds.org/profile/mfa';
49+
customQueryParams['prompt'] = 'login';
50+
customQueryParams['max_age'] = '0';
51+
}
4852
return {
4953
requestAccessToken: true,
5054
issuer: this.store.get('oidc_client', 'oauth_authority'),

0 commit comments

Comments
 (0)