Skip to content

Commit 00333b6

Browse files
committed
[#5864] Add MFA enrollment UI with self-contained QR rendering
This patch adds a "Two-Factor Authentication" card to the user profile page, providing the complete MFA lifecycle management UI, and adds client-side QR code generation. QR code rendering - qrcode library (v1.5.4, MIT license): The QR code for the TOTP provisioning URI is generated entirely client-side using a <canvas> element. No external services (Google Charts API, QR Server, etc.) are involved. This is a deliberate decision: academic tools aimed at digital preservation must not depend on third-party services subject to arbitrary policy changes, deprecation, or rate limits. The qrcode library is pure JavaScript with zero runtime dependencies. Profile page MFA card features: - Setup mode: generates secret via POST /mfa/setup, renders QR code from the otpauth:// provisioning URI on a canvas element - Enrollment confirmation: validates first TOTP code, displays recovery codes (shown once, never stored in plaintext client-side) - Status display: shows enabled badge + remaining recovery code count - Disable: requires current TOTP code (prevents unauthorized disable) - Regenerate recovery codes: requires current TOTP code Design decisions: 1. QR rendered in ngAfterViewChecked (not ngOnInit): The canvas element is inside an @if block. It only exists in the DOM after provisioningUri$ emits. AfterViewChecked ensures the canvas is available when we call QRCode.toCanvas(). 2. BehaviorSubjects (not NgRx) for profile component state: The profile MFA card is self-contained and does not need cross- component coordination. Local reactive state with BehaviorSubjects keeps it simple and testable without NgRx boilerplate. 3. All destructive operations require TOTP code: Disable and regenerate-codes both require a valid current code. This prevents a session-hijacker from removing MFA protection. 4. Custom theme compatibility: ProfilePageMfaFormComponent added to custom theme's imports array. New dependencies: - qrcode@1.5.4 (MIT) - client-side QR generation - @types/qrcode (devDep) - TypeScript type definitions i18n keys added: login.mfa.*, profile.mfa.* (25 keys total) Test plan: 1. Apply patch 2. Login, navigate to /profile => SUCCESS: "Two-Factor Authentication" card visible 3. Click setup, verify QR code renders (no network requests to external) 4. Scan with authenticator app, enter code => SUCCESS: Recovery codes displayed 5. Run: npm run lint => SUCCESS: 0 errors 6. Sign off :-D
1 parent 20d56d0 commit 00333b6

13 files changed

Lines changed: 587 additions & 24 deletions

package-lock.json

Lines changed: 185 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@
100100
"@ngrx/store": "^20.1.0",
101101
"@ngx-translate/core": "^17.0.0",
102102
"@nicky-lenaers/ngx-scroll-to": "^14.0.0",
103+
"@popperjs/core": "^2.11.8",
103104
"@terraformer/wkt": "^2.2.2",
104105
"altcha": "^2.3.0",
105106
"angulartics2": "^12.2.0",
@@ -147,6 +148,7 @@
147148
"nouislider": "^15.7.1",
148149
"orejime": "^2.3.3",
149150
"pem": "1.14.8",
151+
"qrcode": "^1.5.4",
150152
"reflect-metadata": "^0.2.2",
151153
"rxjs": "^7.8.0",
152154
"uuid": "^14.0.1",
@@ -179,6 +181,7 @@
179181
"@types/js-cookie": "3.0.6",
180182
"@types/lodash-es": "^4.17.12",
181183
"@types/node": "^20.19.43",
184+
"@types/qrcode": "^1.5.6",
182185
"@typescript-eslint/eslint-plugin": "^8.62.1",
183186
"@typescript-eslint/parser": "^8.62.1",
184187
"@typescript-eslint/rule-tester": "^8.62.1",

src/app/core/auth/auth.reducer.ts

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -72,10 +72,10 @@ export interface AuthState {
7272
idle: boolean;
7373

7474
// MFA state
75-
mfaRequired: boolean;
75+
mfaRequired?: boolean;
7676
mfaPendingToken?: AuthTokenInfo;
7777
mfaError?: string;
78-
mfaVerifying: boolean;
78+
mfaVerifying?: boolean;
7979

8080
}
8181

@@ -164,13 +164,6 @@ export function authReducer(state: any = initialState, action: AuthActions | Mfa
164164
});
165165

166166
case AuthActionTypes.AUTHENTICATE_SUCCESS:
167-
return Object.assign({}, state, {
168-
mfaRequired: false,
169-
mfaPendingToken: undefined,
170-
mfaError: undefined,
171-
mfaVerifying: false,
172-
});
173-
174167
case AuthActionTypes.LOG_OUT:
175168
return state;
176169

src/app/core/auth/mfa.effects.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
import { HttpResponse } from '@angular/common/http';
1+
import {
2+
HttpErrorResponse,
3+
HttpResponse,
4+
} from '@angular/common/http';
25
import { Injectable } from '@angular/core';
36
import {
47
Actions,
@@ -79,7 +82,8 @@ export class MfaEffects {
7982
return new MfaVerifySuccessAction(null);
8083
}),
8184
catchError((error: unknown) => {
82-
const message = error?.error?.error || 'mfa.verify.error';
85+
const err = error as HttpErrorResponse;
86+
const message = err?.error?.error || 'mfa.verify.error';
8387
return of(new MfaVerifyErrorAction(message));
8488
}),
8589
),

0 commit comments

Comments
 (0)