commit e2ef7cdd50bacb7bf2cc11be355acdd7a74572bd
parent 25187924f00ab7f7b627a793285e732f6457d9bb
Author: Florian Dold <dold@taler.net>
Date: Sat, 29 Aug 2026 19:25:32 +0200
taler-harness: test MFA when replacing verified email
Diffstat:
1 file changed, 82 insertions(+), 4 deletions(-)
diff --git a/packages/taler-harness/src/integrationtests/test-merchant-self-provision-activation.ts b/packages/taler-harness/src/integrationtests/test-merchant-self-provision-activation.ts
@@ -113,10 +113,7 @@ export async function runMerchantSelfProvisionActivationTest(
const completeSignup = await merchantClient.createInstanceSelfProvision(
instanceInfo,
{
- challengeIds: [
- emailChallenge.challenge_id,
- smsChallenge.challenge_id,
- ],
+ challengeIds: [emailChallenge.challenge_id, smsChallenge.challenge_id],
},
);
t.assertDeepEqual(completeSignup.type, "ok");
@@ -158,6 +155,87 @@ export async function runMerchantSelfProvisionActivationTest(
t.assertDeepEqual(details.phone_number, instanceInfo.phone_number);
t.assertDeepEqual(details.phone_validated, true);
+ // Replacing one validated contact must prove ownership of the replacement
+ // and retain one independent, previously validated factor. In particular,
+ // accepting only the new e-mail challenge would allow an authenticated
+ // attacker to replace the account's recovery address.
+ const changedEmail = "changed@taler.net";
+ const changedInstanceSettings = {
+ name: details.name,
+ email: changedEmail,
+ phone_number: details.phone_number,
+ website: details.website,
+ logo: details.logo,
+ address: details.address,
+ jurisdiction: details.jurisdiction,
+ use_stefan: details.use_stefan,
+ default_pay_delay: details.default_pay_delay,
+ default_refund_delay: details.default_refund_delay,
+ default_wire_transfer_delay: details.default_wire_transfer_delay,
+ default_wire_transfer_rounding_interval:
+ details.default_wire_transfer_rounding_interval,
+ };
+ const emailChangeChallenge = alternativeOrThrow(
+ await instanceApi.updateCurrentInstance(token, changedInstanceSettings),
+ HttpStatusCode.Accepted,
+ );
+ t.assertDeepEqual(emailChangeChallenge.combi_and, true);
+ t.assertDeepEqual(emailChangeChallenge.challenges.length, 2);
+ const newEmailChallenge = emailChangeChallenge.challenges.find(
+ (challenge) => challenge.tan_channel === TanChannel.EMAIL,
+ );
+ const oldPhoneChallenge = emailChangeChallenge.challenges.find(
+ (challenge) => challenge.tan_channel === TanChannel.SMS,
+ );
+ t.assertTrue(newEmailChallenge !== undefined);
+ t.assertTrue(oldPhoneChallenge !== undefined);
+
+ await doChallenge(
+ t,
+ instanceApi,
+ newEmailChallenge.challenge_id,
+ changedEmail,
+ mfaConfig.email.path,
+ );
+ const emailOnlyContinuation = alternativeOrThrow(
+ await instanceApi.updateCurrentInstance(token, changedInstanceSettings, {
+ challengeIds: [newEmailChallenge.challenge_id],
+ }),
+ HttpStatusCode.Accepted,
+ );
+ t.assertDeepEqual(emailOnlyContinuation.combi_and, true);
+ t.assertDeepEqual(emailOnlyContinuation.challenges.length, 2);
+ const detailsAfterEmailOnly = succeedOrThrow(
+ await instanceApi.getCurrentInstanceDetails(token),
+ );
+ t.assertDeepEqual(detailsAfterEmailOnly.email, instanceInfo.email);
+ const remainingPhoneChallenge = emailOnlyContinuation.challenges.find(
+ (challenge) => challenge.tan_channel === TanChannel.SMS,
+ );
+ t.assertTrue(remainingPhoneChallenge !== undefined);
+ await doChallenge(
+ t,
+ instanceApi,
+ remainingPhoneChallenge.challenge_id,
+ instanceInfo.phone_number,
+ mfaConfig.sms.path,
+ );
+ succeedOrThrow(
+ await instanceApi.updateCurrentInstance(token, changedInstanceSettings, {
+ challengeIds: emailOnlyContinuation.challenges.map(
+ (challenge) => challenge.challenge_id,
+ ),
+ }),
+ );
+ const changedDetails = succeedOrThrow(
+ await instanceApi.getCurrentInstanceDetails(token),
+ );
+ t.assertDeepEqual(changedDetails.email, changedEmail);
+ t.assertDeepEqual(changedDetails.email_validated, true);
+ t.assertDeepEqual(changedDetails.phone_number, instanceInfo.phone_number);
+ t.assertDeepEqual(changedDetails.phone_validated, true);
+ mfaConfig.email.address = changedEmail;
+
const firstAccount = succeedOrThrow(
await instanceApi.addBankAccount(token, {
payto_uri: getTestHarnessPaytoForLabel("account1"),