Skip to content

Commit 622d428

Browse files
committed
WIP for #976
1 parent 6c90390 commit 622d428

5 files changed

Lines changed: 114 additions & 56 deletions

File tree

myconext-gui/src/locale/en.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -437,7 +437,10 @@ const en = {
437437
copy: "Copy the code",
438438
copied: "Copied",
439439
continue: "My code is safe. Continue",
440-
leaveConfirmation: "Are you sure you want to leave the page? Changes will not be saved."
440+
leaveConfirmation: "Are you sure you want to leave the page? Changes will not be saved.",
441+
finalizedRegistration: "Existing recovery method",
442+
finalizedRegistrationExplanation: "You are not allowed to register a phone number, as you already have an existing backup code as recovery method. You can change your recovery-method ",
443+
finalizedRegistrationHere: "here."
441444
},
442445
phoneVerification: {
443446
header: "Add a recovery phone number",

myconext-gui/src/locale/nl.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -437,7 +437,10 @@ const nl = {
437437
copy: "Kopieer de code",
438438
copied: "Gekopieerd",
439439
continue: "Mijn code is veilig. Doorgaan",
440-
leaveConfirmation: "Weet je zeker dat je deze pagina wilt verlaten? Wijzigingen worden niet opgeslagen."
440+
leaveConfirmation: "Weet je zeker dat je deze pagina wilt verlaten? Wijzigingen worden niet opgeslagen.",
441+
finalizedRegistration: "Bestaande herstelmethode",
442+
finalizedRegistrationExplanation: "Je mag geen telefoonnummer registreren, omdat je al een bestaande back-upcode als herstelmethode hebt. Wijzig je herstelmethode ",
443+
finalizedRegistrationHere: "hier."
441444
},
442445
phoneVerification: {
443446
header: "Voeg een hersteltelefoonnummer toe",

myconext-gui/src/routes/tiqr/PhoneVerification.svelte

Lines changed: 46 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,17 @@
88
import {navigate} from "svelte-routing";
99
import Modal from "../../components/Modal.svelte";
1010
11+
const RECOVERY_CODE = "recovery-code";
12+
1113
export let change = false;
1214
1315
let initial = true;
1416
let phoneNumber = "";
1517
let showSpinner = false;
1618
let rateLimited = false;
1719
let showRateLimitedModal = false;
20+
let finalizedRegistration = false;
21+
let recoveryCode = sessionStorage.getItem(RECOVERY_CODE);
1822
1923
$: allowedNext = validPhoneNumber(phoneNumber);
2024
$: phoneNumberIncorrect = !initial && !validPhoneNumber(phoneNumber);
@@ -27,9 +31,13 @@
2731
const promise = change ? reTextPhoneNumber : textPhoneNumber;
2832
promise(phoneNumber.replaceAll(" ", "").replaceAll("-", ""))
2933
.then(() => navigate(`${change ? "change-" : ""}phone-confirmation`))
30-
.catch(() => {
31-
rateLimited = true;
32-
showRateLimitedModal = true;
34+
.catch(e => {
35+
if (e.status === 406) {
36+
finalizedRegistration = true;
37+
} else {
38+
rateLimited = true;
39+
showRateLimitedModal = true;
40+
}
3341
showSpinner = false;
3442
})
3543
}
@@ -105,32 +113,42 @@
105113
{/if}
106114
<div class="phone-verification">
107115
<div class="inner-container">
108-
109-
<h2 class="header">{I18n.t("PhoneVerification.Header.COPY")}</h2>
110-
<p class="explanation">{I18n.t("phoneVerification.info")}</p>
111-
<p class="methods">{I18n.t("PhoneVerification.Text.COPY")}</p>
112-
113-
<input class:error={phoneNumberIncorrect}
114-
type="tel"
115-
id="password-field"
116-
spellcheck="false"
117-
on:keydown={handleEnter}
118-
placeholder={I18n.t("PhoneVerification.PlaceHolder.COPY")}
119-
use:init
120-
bind:value={phoneNumber}>
121-
{#if phoneNumberIncorrect}
122-
<div class="error">
123-
<span class="svg">{@html critical}</span>
124-
<span>{I18n.t("phoneVerification.phoneIncorrect")}</span>
125-
</div>
116+
{#if finalizedRegistration}
117+
<h2 class="header">{I18n.t("recovery.finalizedRegistration")}</h2>
118+
<p class="explanation">{I18n.t("recovery.finalizedRegistrationExplanation")}
119+
<a href="/backup-codes" on:click|preventDefault|stopPropagation={() => navigate("/backup-codes")}>
120+
{I18n.t("recovery.finalizedRegistrationHere")}
121+
</a>
122+
</p>
123+
{#if recoveryCode}
124+
//TODO recovery code show + translation
125+
{/if}
126+
{:else}
127+
<h2 class="header">{I18n.t("PhoneVerification.Header.COPY")}</h2>
128+
<p class="explanation">{I18n.t("phoneVerification.info")}</p>
129+
<p class="methods">{I18n.t("PhoneVerification.Text.COPY")}</p>
130+
131+
<input class:error={phoneNumberIncorrect}
132+
type="tel"
133+
id="password-field"
134+
spellcheck="false"
135+
on:keydown={handleEnter}
136+
placeholder={I18n.t("PhoneVerification.PlaceHolder.COPY")}
137+
use:init
138+
bind:value={phoneNumber}>
139+
{#if phoneNumberIncorrect}
140+
<div class="error">
141+
<span class="svg">{@html critical}</span>
142+
<span>{I18n.t("phoneVerification.phoneIncorrect")}</span>
143+
</div>
144+
{/if}
145+
146+
<Button href="/next"
147+
larger={true}
148+
disabled={showSpinner || !allowedNext}
149+
label={I18n.t("PhoneVerification.Verify.COPY")}
150+
onClick={next}/>
126151
{/if}
127-
128-
<Button href="/next"
129-
larger={true}
130-
disabled={showSpinner || !allowedNext}
131-
label={I18n.t("PhoneVerification.Verify.COPY")}
132-
onClick={next}/>
133-
134152
</div>
135153
</div>
136154

myconext-gui/src/routes/tiqr/Recovery.svelte

Lines changed: 37 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,19 @@
44
import backupIcon from "../../icons/redesign/backup-code.svg?raw";
55
import LoginOption from "../../components/LoginOption.svelte";
66
import {navigate} from "svelte-routing";
7+
import {user} from "../../stores/user";
8+
import {onMount} from "svelte";
79
810
export let change = false;
911
12+
let finalizedRegistration = false;
13+
14+
onMount(() => {
15+
if ($user.registration?.status === "FINALIZED") {
16+
finalizedRegistration = true;
17+
}
18+
})
19+
1020
const phoneNumber = () => {
1121
navigate(`/${change ? "change-" : ""}phone-verification`);
1222
}
@@ -46,24 +56,32 @@
4656
</style>
4757
<div class="recovery">
4858
<div class="inner-container">
49-
50-
<h2 class="header">{I18n.t(`recovery.${change ? "changeHeader" : "header"}`)}</h2>
51-
<p class="explanation">{I18n.t(`recovery.${change ? "changeInfo" : "info"}`)}</p>
52-
<p class="methods">{I18n.t("Recovery.Methods.COPY")}</p>
53-
<div class="phone-number">
54-
<LoginOption icon={phoneIcon}
55-
label={I18n.t("Recovery.PhoneNumber.COPY")}
56-
subLabel={I18n.t("Recovery.PhoneNumberInfo.COPY")}
57-
action={phoneNumber}
58-
index={1}
59-
preferred={true}/>
60-
</div>
61-
<div class="other-account">
62-
<LoginOption icon={backupIcon}
63-
label={I18n.t("Recovery.BackupCode.COPY")}
64-
subLabel={I18n.t("Recovery.BackupCodeInfo.COPY")}
65-
action={backUpCode}
66-
index={2}/>
67-
</div>
59+
{#if !change && finalizedRegistration}
60+
<h2 class="header">{I18n.t("recovery.finalizedRegistration")}</h2>
61+
<p class="explanation">{I18n.t("recovery.finalizedRegistrationExplanation")}
62+
<a href="/backup-codes" on:click|preventDefault|stopPropagation={() => navigate("/backup-codes")}>
63+
{I18n.t("recovery.finalizedRegistrationHere")}
64+
</a>
65+
</p>
66+
{:else}
67+
<h2 class="header">{I18n.t(`recovery.${change ? "changeHeader" : "header"}`)}</h2>
68+
<p class="explanation">{I18n.t(`recovery.${change ? "changeInfo" : "info"}`)}</p>
69+
<p class="methods">{I18n.t("Recovery.Methods.COPY")}</p>
70+
<div class="phone-number">
71+
<LoginOption icon={phoneIcon}
72+
label={I18n.t("Recovery.PhoneNumber.COPY")}
73+
subLabel={I18n.t("Recovery.PhoneNumberInfo.COPY")}
74+
action={phoneNumber}
75+
index={1}
76+
preferred={true}/>
77+
</div>
78+
<div class="other-account">
79+
<LoginOption icon={backupIcon}
80+
label={I18n.t("Recovery.BackupCode.COPY")}
81+
subLabel={I18n.t("Recovery.BackupCodeInfo.COPY")}
82+
action={backUpCode}
83+
index={2}/>
84+
</div>
85+
{/if}
6886
</div>
6987
</div>

myconext-server/src/main/java/myconext/tiqr/TiqrController.java

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import jakarta.validation.Valid;
1010
import myconext.exceptions.ExpiredAuthenticationException;
1111
import myconext.exceptions.ForbiddenException;
12+
import myconext.exceptions.NotAcceptableException;
1213
import myconext.exceptions.UserNotFoundException;
1314
import myconext.manage.Manage;
1415
import myconext.model.SamlAuthenticationRequest;
@@ -231,7 +232,7 @@ public ResponseEntity<GeneratedBackupCode> generateBackupCode(
231232
private ResponseEntity<GeneratedBackupCode> doGenerateBackupCode(User user, boolean regenerateSpFlow) throws TiqrException {
232233
if (!regenerateSpFlow) {
233234
Registration registration = registrationRepository.findRegistrationByUserId(user.getId()).orElseThrow(IllegalArgumentException::new);
234-
if (!registration.getStatus().equals(RegistrationStatus.INITIALIZED)) {
235+
if (registration.getStatus().equals(RegistrationStatus.FINALIZED)) {
235236
throw new ForbiddenException("Forbidden backup code, wrong status: " + registration.getStatus());
236237
}
237238
}
@@ -260,7 +261,7 @@ public ResponseEntity<FinishEnrollment> sendPhoneCodeForSp(HttpServletRequest re
260261
@Valid @RequestBody PhoneCode phoneCode) {
261262
User user = userFromAuthentication(authentication);
262263
String phoneNumber = phoneCode.getPhoneNumber();
263-
return doSendPhoneCode(user, phoneNumber, false, request);
264+
return doSendPhoneCode(user, phoneNumber, false, false, request);
264265
}
265266

266267
@Operation(summary = "Send new phone code", description = "Send a new verification code to mobile phone for a finished authentication")
@@ -275,7 +276,7 @@ public ResponseEntity<FinishEnrollment> resendPhoneCodeForSp(HttpServletRequest
275276
}
276277
User user = userFromAuthentication(secAuthentication);
277278
String phoneNumber = phoneCode.getPhoneNumber();
278-
return doSendPhoneCode(user, phoneNumber, true, request);
279+
return doSendPhoneCode(user, phoneNumber, true, true, request);
279280
}
280281

281282
@PostMapping("/send-phone-code")
@@ -285,10 +286,23 @@ public ResponseEntity<FinishEnrollment> sendPhoneCode(HttpServletRequest request
285286
@RequestBody Map<String, String> requestBody) {
286287
User user = getUserFromAuthenticationRequest(hash);
287288
String phoneNumber = requestBody.get("phoneNumber");
288-
return doSendPhoneCode(user, phoneNumber, false, request);
289-
}
289+
return doSendPhoneCode(user, phoneNumber, false, false, request);
290+
}
291+
292+
private ResponseEntity<FinishEnrollment> doSendPhoneCode(User user,
293+
String phoneNumber,
294+
boolean regenerateSpFlow,
295+
boolean deactivationFlow,
296+
HttpServletRequest request) {
297+
Registration registration = registrationRepository.findRegistrationByUserId(user.getId()).orElseThrow(IllegalArgumentException::new);
298+
//When the 1st factor is compromised, then it is not allowed to reset MFA
299+
if (deactivationFlow && !registration.getStatus().equals(RegistrationStatus.FINALIZED)) {
300+
throw new NotAcceptableException("Not allowed to resend phone code, registration is not finalized");
301+
}
302+
if (!deactivationFlow && registration.getStatus().equals(RegistrationStatus.FINALIZED)) {
303+
throw new NotAcceptableException("Not allowed to send phone code, registration is already finalized");
304+
}
290305

291-
private ResponseEntity<FinishEnrollment> doSendPhoneCode(User user, String phoneNumber, boolean regenerateSpFlow, HttpServletRequest request) {
292306
rateLimitEnforcer.checkSendSMSRateLimit(user);
293307

294308
LOG.info(String.format("Sending SMS for user %s to number %s", user.getEmail(), phoneNumber));
@@ -300,6 +314,8 @@ private ResponseEntity<FinishEnrollment> doSendPhoneCode(User user, String phone
300314
Map<String, Object> surfSecureId = user.getSurfSecureId();
301315
surfSecureId.put(PHONE_VERIFICATION_CODE, phoneVerification);
302316
surfSecureId.remove(RATE_LIMIT);
317+
//Can't have both
318+
surfSecureId.remove(RECOVERY_CODE);
303319

304320
if (regenerateSpFlow) {
305321
surfSecureId.put(NEW_UNVERIFIED_PHONE_NUMBER, phoneNumber);
@@ -603,7 +619,7 @@ public ResponseEntity<FinishEnrollment> sendDeactivationPhoneCodeForSp(HttpServl
603619
if (!StringUtils.hasText(phoneNumber)) {
604620
throw new ForbiddenException("Forbidden empty phone number");
605621
}
606-
return doSendPhoneCode(user, phoneNumber, false, request);
622+
return doSendPhoneCode(user, phoneNumber, false, true, request);
607623
}
608624

609625
@Operation(summary = "De-activate the app", description = "De-activate the eduID app for the current user")

0 commit comments

Comments
 (0)