taler-typescript-core

Wallet core logic and WebUIs for various components
Log | Files | Refs | Submodules | README | LICENSE

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:
Mpackages/taler-harness/src/integrationtests/test-merchant-self-provision-activation.ts | 86+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----
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"),