Skip to content

Add magic code verification for card provisioning default contact method change#90004

Draft
MelvinBot wants to merge 2 commits into
mainfrom
claude-requireMagicCodeForCardProvisioning
Draft

Add magic code verification for card provisioning default contact method change#90004
MelvinBot wants to merge 2 commits into
mainfrom
claude-requireMagicCodeForCardProvisioning

Conversation

@MelvinBot
Copy link
Copy Markdown
Contributor

@MelvinBot MelvinBot commented May 8, 2026

Explanation of Change

Both the company card and Expensify card provisioning flows previously called setContactMethodAsDefault() directly when a user submitted a validated secondary email — no magic code verification was required. A bad actor who planted their email as a validated secondary contact method could exploit this to silently promote it to primary, intercepting all future magic codes.

This PR adds a magic code verification step (sent to the current primary login via requestValidateCodeAction()) before allowing the default contact method change. Two new RHP pages are introduced — one for company cards and one for Expensify cards — that render ValidateCodeActionContent and only call setContactMethodAsDefault(email, validateCode) after the user enters the code.

Both the "already validated email" branch (from the add-work-email pages) and the "newly validated email" branch (from the verify-work-account pages) now converge on the new confirmation page before the default contact method is changed. When the submitted email already matches the current primary, the magic code step is skipped since no default-login change is needed.

Fixed Issues

$ #89121
PROPOSAL: #89121 (comment)

Tests

// TODO: The human co-author must fill out the tests you ran before marking this PR as "ready for review"
// Please describe what tests you performed that validates your changed worked.

  • Verify that no errors appear in the JS console

Offline tests

// TODO: The human co-author must fill out the offline tests before marking this PR as "ready for review"

QA Steps

// TODO: These must be filled out, or the issue title must include "[No QA]."

  • Verify that no errors appear in the JS console

PR Author Checklist

  • I linked the correct issue in the ### Fixed Issues section above
  • I wrote clear testing steps that cover the changes made in this PR
    • I added steps for local testing in the Tests section
    • I added steps for the expected offline behavior in the Offline steps section
    • I added steps for Staging and/or Production testing in the QA steps section
    • I added steps to cover failure scenarios (i.e. verify an input displays the correct error message if the entered data is not correct)
    • I turned off my network connection and tested it while offline to ensure it matches the expected behavior (i.e. verify the default avatar icon is displayed if app is offline)
    • I tested this PR with a High Traffic account against the staging or production API to ensure there are no regressions (e.g. long loading states that impact usability).
  • I included screenshots or videos for tests on all platforms
  • I ran the tests on all platforms & verified they passed on:
    • Android: Native
    • Android: mWeb Chrome
    • iOS: Native
    • iOS: mWeb Safari
    • MacOS: Chrome / Safari
  • I verified there are no console errors (if there's a console error not related to the PR, report it or open an issue for it to be fixed)
  • I followed proper code patterns (see Reviewing the code)
    • I verified that any callback methods that were added or modified are named for what the method does and never what callback they handle (i.e. toggleReport and not onIconClick)
    • I verified that comments were added to code that is not self explanatory
    • I verified that any new or modified comments were clear, correct English, and explained "why" the code was doing something instead of only explaining "what" the code was doing.
    • I verified any copy / text shown in the product is localized by adding it to src/languages/* files and using the translation method
      • If any non-english text was added/modified, I used JaimeGPT to get English > Spanish translation. I then posted it in #expensify-open-source and it was approved by an internal Expensify engineer. Link to Slack message:
    • I verified all numbers, amounts, dates and phone numbers shown in the product are using the localization methods
    • I verified any copy / text that was added to the app is grammatically correct in English. It adheres to proper capitalization guidelines (note: only the first word of header/labels should be capitalized), and is either coming verbatim from figma or has been approved by marketing (in order to get marketing approval, ask the Bug Zero team member to add the Waiting for copy label to the issue)
    • I verified proper file naming conventions were followed for any new files or renamed files. All non-platform specific files are named after what they export and are not named "index.js". All platform-specific files are named for the platform the code supports as outlined in the README.
    • I verified the JSDocs style guidelines (in STYLE.md) were followed
  • If a new code pattern is added I verified it was agreed to be used by multiple Expensify engineers
  • I followed the guidelines as stated in the Review Guidelines
  • I tested other components that can be impacted by my changes (i.e. if the PR modifies a shared library or component like Avatar, I verified the components using Avatar are working as expected)
  • I verified all code is DRY (the PR doesn't include any logic written more than once, with the exception of tests)
  • I verified any variables that can be defined as constants (ie. in CONST.ts or at the top of the file that uses the constant) are defined as such
  • I verified that if a function's arguments changed that all usages have also been updated correctly
  • If any new file was added I verified that:
    • The file has a description of what it does and/or why is needed at the top of the file if the code is not self explanatory
  • If a new CSS style is added I verified that:
    • A similar style doesn't already exist
    • The style can't be created with an existing StyleUtils function (i.e. StyleUtils.getBackgroundAndBorderStyle(theme.componentBG))
  • If new assets were added or existing ones were modified, I verified that:
    • The assets are optimized and compressed (for SVG files, run npm run compress-svg)
    • The assets load correctly across all supported platforms.
  • If the PR modifies code that runs when editing or sending messages, I tested and verified there is no unexpected behavior for all supported markdown - URLs, single line code, code blocks, quotes, headings, bold, strikethrough, and italic.
  • If the PR modifies a generic component, I tested and verified that those changes do not break usages of that component in the rest of the App (i.e. if a shared library or component like Avatar is modified, I verified that Avatar is working as expected in all cases)
  • If the PR modifies a component related to any of the existing Storybook stories, I tested and verified all stories for that component are still working as expected.
  • If the PR modifies a component or page that can be accessed by a direct deeplink, I verified that the code functions as expected when the deeplink is used - from a logged in and logged out account.
  • If the PR modifies the UI (e.g. new buttons, new UI components, changing the padding/spacing/sizing, moving components, etc) or modifies the form input styles:
    • I verified that all the inputs inside a form are aligned with each other.
    • I added Design label and/or tagged @Expensify/design so the design team can review the changes.
  • If a new page is added, I verified it's using the ScrollView component to make it scrollable when more elements are added to the page.
  • I added unit tests for any new feature or bug fix in this PR to help automatically prevent regressions in this user flow.
  • If the main branch was merged into this PR after a review, I tested again and verified the outcome was still expected according to the Test steps.

Screenshots/Videos

Android: Native
Android: mWeb Chrome
iOS: Native
iOS: mWeb Safari
MacOS: Chrome / Safari
- [ ] I verified that similar component doesn't exist in the codebase - [ ] I verified that all props are defined accurately and each prop has a `/** comment above it */` - [ ] I verified that each file is named correctly - [ ] I verified that each component has a clear name that is non-ambiguous and the purpose of the component can be inferred from the name alone - [ ] I verified that the only data being stored in component state is data necessary for rendering and nothing else - [ ] In component if we are not using the full Onyx data that we loaded, I've added the proper selector in order to ensure the component only re-renders when the data it is using changes - [ ] For Class Components, any internal methods passed to components event handlers are bound to `this` properly so there are no scoping issues (i.e. for `onClick={this.submit}` the method `this.submit` should be bound to `this` in the constructor) - [ ] I verified that component internal methods bound to `this` are necessary to be bound (i.e. avoid `this.submit = this.submit.bind(this);` if `this.submit` is never passed to a component event handler like `onClick`) - [ ] I verified that all JSX used for rendering exists in the render method - [ ] I verified that each component has the minimum amount of code necessary for its purpose, and it is broken down into smaller components in order to separate concerns and functions

Screenshots/Videosundefined

… card provisioning

Both the company card and Expensify card provisioning flows previously called
setContactMethodAsDefault() directly when a validated email was submitted,
allowing a default contact method change without any verification. This adds
a magic code challenge step (sent to the current primary login) before
allowing the change, closing the account takeover vector described in the
linked issue.

For the unvalidated email branch, the existing verify-work-email pages now
navigate to the new confirmation page after the email is validated, instead
of directly linking the card feed.

When the submitted email already matches the current primary contact method,
the magic code step is skipped since no default-login change is needed.

Co-authored-by: Shubham Agrawal <shubham1206agra@users.noreply.github.com>
@OSBotify
Copy link
Copy Markdown
Contributor

OSBotify commented May 8, 2026

🦜 Polyglot Parrot! 🦜

Squawk! Looks like you added some shiny new English strings. Allow me to parrot them back to you in other tongues:

View the translation diff
diff --git a/src/languages/de.ts b/src/languages/de.ts
index c73eab89..e55f49aa 100644
--- a/src/languages/de.ts
+++ b/src/languages/de.ts
@@ -5198,8 +5198,9 @@ _Für ausführlichere Anweisungen [besuchen Sie unsere Hilfeseite](${CONST.NETSU
             fromOtherWorkspaces: 'Aus anderen Workspaces',
             addWorkEmail: 'Fügen Sie Ihre Arbeits-E-Mail hinzu',
             addWorkEmailDescription: 'Bitte fügen Sie Ihre Arbeits-E-Mail hinzu, um vorhandene Feeds aus anderen Workspaces zu nutzen.',
-            confirmDefaultContactMethod: "Let's verify it's you",
-            enterMagicCodeDefaultContactMethod: (primaryLogin: string) => `Please enter the magic code sent to ${primaryLogin} to confirm changing your default contact method.`,
+            confirmDefaultContactMethod: 'Lass uns überprüfen, ob du es bist',
+            enterMagicCodeDefaultContactMethod: (primaryLogin: string) =>
+                `Bitte gib den magischen Code ein, der an ${primaryLogin} gesendet wurde, um die Änderung deiner Standard-Kontaktmethode zu bestätigen.`,
             error: {
                 workspaceFeedsCouldNotBeLoadedTitle: 'Kartendaten konnten nicht geladen werden',
                 workspaceFeedsCouldNotBeLoadedMessage:
diff --git a/src/languages/fr.ts b/src/languages/fr.ts
index d1f33469..10f896da 100644
--- a/src/languages/fr.ts
+++ b/src/languages/fr.ts
@@ -5208,8 +5208,9 @@ _Pour des instructions plus détaillées, [visitez notre site d’aide](${CONST.
             fromOtherWorkspaces: "D'autres espaces de travail",
             addWorkEmail: 'Ajoutez votre adresse e-mail professionnelle',
             addWorkEmailDescription: "Veuillez ajouter votre e-mail professionnel pour utiliser les flux existants d'autres espaces de travail.",
-            confirmDefaultContactMethod: "Let's verify it's you",
-            enterMagicCodeDefaultContactMethod: (primaryLogin: string) => `Please enter the magic code sent to ${primaryLogin} to confirm changing your default contact method.`,
+            confirmDefaultContactMethod: 'Vérifions que c’est bien vous',
+            enterMagicCodeDefaultContactMethod: (primaryLogin: string) =>
+                `Veuillez saisir le code magique envoyé à ${primaryLogin} pour confirmer la modification de votre méthode de contact par défaut.`,
             error: {
                 workspaceFeedsCouldNotBeLoadedTitle: 'Impossible de charger les flux de cartes',
                 workspaceFeedsCouldNotBeLoadedMessage:
diff --git a/src/languages/it.ts b/src/languages/it.ts
index e56dbe1b..65ad2d05 100644
--- a/src/languages/it.ts
+++ b/src/languages/it.ts
@@ -5183,8 +5183,9 @@ _Per istruzioni più dettagliate, [visita il nostro sito di assistenza](${CONST.
             fromOtherWorkspaces: 'Da altri spazi di lavoro',
             addWorkEmail: 'Aggiungi la tua email di lavoro',
             addWorkEmailDescription: 'Aggiungi la tua email di lavoro per utilizzare i feed esistenti di altri spazi di lavoro.',
-            confirmDefaultContactMethod: "Let's verify it's you",
-            enterMagicCodeDefaultContactMethod: (primaryLogin: string) => `Please enter the magic code sent to ${primaryLogin} to confirm changing your default contact method.`,
+            confirmDefaultContactMethod: 'Verifichiamo che tu sia tu',
+            enterMagicCodeDefaultContactMethod: (primaryLogin: string) =>
+                `Inserisci il codice magico inviato a ${primaryLogin} per confermare la modifica del tuo metodo di contatto predefinito.`,
             error: {
                 workspaceFeedsCouldNotBeLoadedTitle: 'Impossibile caricare i feed della carta',
                 workspaceFeedsCouldNotBeLoadedMessage:
diff --git a/src/languages/ja.ts b/src/languages/ja.ts
index bade5697..f0a02aa2 100644
--- a/src/languages/ja.ts
+++ b/src/languages/ja.ts
@@ -5137,8 +5137,8 @@ _詳しい手順については、[ヘルプサイトをご覧ください](${CO
             fromOtherWorkspaces: '他のワークスペースから',
             addWorkEmail: 'あなたの勤務用メールアドレスを追加',
             addWorkEmailDescription: '他のワークスペースの既存フィードを使用するには、勤務用メールアドレスを追加してください。',
-            confirmDefaultContactMethod: "Let's verify it's you",
-            enterMagicCodeDefaultContactMethod: (primaryLogin: string) => `Please enter the magic code sent to ${primaryLogin} to confirm changing your default contact method.`,
+            confirmDefaultContactMethod: 'ご本人確認を行います',
+            enterMagicCodeDefaultContactMethod: (primaryLogin: string) => `デフォルトの連絡方法を変更するために、${primaryLogin} に送信されたマジックコードを入力してください。`,
             error: {
                 workspaceFeedsCouldNotBeLoadedTitle: 'カードフィードを読み込めませんでした',
                 workspaceFeedsCouldNotBeLoadedMessage: 'ワークスペースカードフィードの読み込み中にエラーが発生しました。もう一度お試しいただくか、管理者にお問い合わせください。',
diff --git a/src/languages/nl.ts b/src/languages/nl.ts
index 7439c9e7..6bb5b774 100644
--- a/src/languages/nl.ts
+++ b/src/languages/nl.ts
@@ -5175,8 +5175,9 @@ _Voor meer gedetailleerde instructies, [bezoek onze help-site](${CONST.NETSUITE_
             fromOtherWorkspaces: 'Uit andere workspaces',
             addWorkEmail: 'Voeg je werkmail toe',
             addWorkEmailDescription: 'Voeg je werkmail toe om bestaande feeds van andere workspaces te gebruiken.',
-            confirmDefaultContactMethod: "Let's verify it's you",
-            enterMagicCodeDefaultContactMethod: (primaryLogin: string) => `Please enter the magic code sent to ${primaryLogin} to confirm changing your default contact method.`,
+            confirmDefaultContactMethod: 'Laten we verifiëren dat jij het bent',
+            enterMagicCodeDefaultContactMethod: (primaryLogin: string) =>
+                `Voer de magische code in die naar ${primaryLogin} is gestuurd om het wijzigen van je standaardcontactmethode te bevestigen.`,
             error: {
                 workspaceFeedsCouldNotBeLoadedTitle: 'Kaartfeeds konden niet worden geladen',
                 workspaceFeedsCouldNotBeLoadedMessage: 'Er is een fout opgetreden bij het laden van workspacekaartfeeds. Probeer het opnieuw of neem contact op met je beheerder.',
diff --git a/src/languages/pl.ts b/src/languages/pl.ts
index 1c3cff84..b87e5f3a 100644
--- a/src/languages/pl.ts
+++ b/src/languages/pl.ts
@@ -5167,8 +5167,8 @@ _Aby uzyskać bardziej szczegółowe instrukcje, [odwiedź naszą stronę pomocy
             fromOtherWorkspaces: 'Z innych przestrzeni roboczych',
             addWorkEmail: 'Dodaj swój służbowy e-mail',
             addWorkEmailDescription: 'Dodaj swój służbowy e-mail, aby korzystać z istniejących kanałów z innych przestrzeni roboczych.',
-            confirmDefaultContactMethod: "Let's verify it's you",
-            enterMagicCodeDefaultContactMethod: (primaryLogin: string) => `Please enter the magic code sent to ${primaryLogin} to confirm changing your default contact method.`,
+            confirmDefaultContactMethod: 'Zweryfikujmy, czy to na pewno Ty',
+            enterMagicCodeDefaultContactMethod: (primaryLogin: string) => `Wpisz magiczny kod wysłany na ${primaryLogin}, aby potwierdzić zmianę domyślnej metody kontaktu.`,
             error: {
                 workspaceFeedsCouldNotBeLoadedTitle: 'Nie można wczytać kanałów kart',
                 workspaceFeedsCouldNotBeLoadedMessage: 'Wystąpił błąd podczas ładowania kanałów kart w przestrzeni roboczej. Spróbuj ponownie lub skontaktuj się ze swoim administratorem.',
diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts
index a0d35b8e..0c9acf13 100644
--- a/src/languages/pt-BR.ts
+++ b/src/languages/pt-BR.ts
@@ -5169,8 +5169,8 @@ _Para instruções mais detalhadas, [visite nossa central de ajuda](${CONST.NETS
             fromOtherWorkspaces: 'De outros workspaces',
             addWorkEmail: 'Adicione seu e-mail de trabalho',
             addWorkEmailDescription: 'Adicione seu e-mail de trabalho para usar os feeds existentes de outros workspaces.',
-            confirmDefaultContactMethod: "Let's verify it's you",
-            enterMagicCodeDefaultContactMethod: (primaryLogin: string) => `Please enter the magic code sent to ${primaryLogin} to confirm changing your default contact method.`,
+            confirmDefaultContactMethod: 'Vamos verificar se é você',
+            enterMagicCodeDefaultContactMethod: (primaryLogin: string) => `Insira o código mágico enviado para ${primaryLogin} para confirmar a alteração do seu método de contato padrão.`,
             error: {
                 workspaceFeedsCouldNotBeLoadedTitle: 'Não foi possível carregar os feeds do cartão',
                 workspaceFeedsCouldNotBeLoadedMessage: 'Ocorreu um erro ao carregar os feeds de cartões do workspace. Tente novamente ou contate o seu administrador.',
diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts
index 014a7d56..81d2d25a 100644
--- a/src/languages/zh-hans.ts
+++ b/src/languages/zh-hans.ts
@@ -5053,8 +5053,8 @@ _如需更详细的说明,请[访问我们的帮助网站](${CONST.NETSUITE_IM
             fromOtherWorkspaces: '来自其他工作区',
             addWorkEmail: '添加您的工作邮箱',
             addWorkEmailDescription: '请添加您的工作邮箱以使用其他工作区的现有流水。',
-            confirmDefaultContactMethod: "Let's verify it's you",
-            enterMagicCodeDefaultContactMethod: (primaryLogin: string) => `Please enter the magic code sent to ${primaryLogin} to confirm changing your default contact method.`,
+            confirmDefaultContactMethod: '让我们验证一下你的身份',
+            enterMagicCodeDefaultContactMethod: (primaryLogin: string) => `请输入发送到 ${primaryLogin} 的魔法验证码,以确认更改您的默认联系方式。`,
             error: {
                 workspaceFeedsCouldNotBeLoadedTitle: '无法加载卡片流水',
                 workspaceFeedsCouldNotBeLoadedMessage: '加载工作区卡片动态时发生错误。请重试或联系管理员。',

Note

You can apply these changes to your branch by copying the patch to your clipboard, then running pbpaste | git apply 😉

View workflow run

@codecov
Copy link
Copy Markdown

codecov Bot commented May 8, 2026

Codecov Report

❌ Looks like you've decreased code coverage for some files. Please write tests to increase, or at least maintain, the existing level of code coverage. See our documentation here for how to interpret this table.

Files with missing lines Coverage Δ
src/SCREENS.ts 100.00% <ø> (ø)
...gation/linkingConfig/RELATIONS/WORKSPACE_TO_RHP.ts 100.00% <ø> (ø)
src/libs/Navigation/linkingConfig/config.ts 76.92% <ø> (ø)
src/ROUTES.ts 18.18% <0.00%> (-0.04%) ⬇️
...gation/AppNavigator/ModalStackNavigators/index.tsx 6.78% <0.00%> (-0.02%) ⬇️
...ifyCard/WorkspaceExpensifyCardAddWorkEmailPage.tsx 0.00% <0.00%> (ø)
...rd/WorkspaceExpensifyCardVerifyWorkAccountPage.tsx 0.00% <0.00%> (ø)
...ce/companyCards/WorkspaceVerifyWorkAccountPage.tsx 0.00% <0.00%> (ø)
...panyCards/WorkspaceCompanyCardAddWorkEmailPage.tsx 0.00% <0.00%> (ø)
...ceExpensifyCardConfirmDefaultContactMethodPage.tsx 0.00% <0.00%> (ø)
... and 1 more
... and 11 files with indirect coverage changes

@tgolen
Copy link
Copy Markdown
Contributor

tgolen commented May 11, 2026

@shubham1206agra are you helping MelvinBot take this one across the finish line?

@shubham1206agra
Copy link
Copy Markdown
Contributor

@MelvinBot I think you forgot to implement #89121 (comment)

@MelvinBot
Copy link
Copy Markdown
Contributor Author

Hi shubham1206agra, I reviewed the PR changes against the proposal and all 5 implementation items appear to be covered:

  1. New confirmation pages — Both WorkspaceCompanyCardConfirmDefaultContactMethodPage and WorkspaceExpensifyCardConfirmDefaultContactMethodPage render ValidateCodeActionContent, call requestValidateCodeAction() on mount, and call setContactMethodAsDefault(email, validateCode) on submit before linking the feed.

  2. VerifyWorkAccount pages updated — Both pages now navigate to the confirmation page when isWorkEmailValidated becomes true (instead of directly calling linkCardFeedToPolicy).

  3. AddWorkEmail pages updated — The validated-existing-login branch navigates to the confirmation page instead of calling setContactMethodAsDefault directly.

  4. Skip when already primary — Both AddWorkEmail pages check if the submitted email matches the current primary and skip the magic code step if so.

  5. Route/screen registration — New routes, screens, linking config, and navigator entries are all present.

Could you clarify what you think is missing? Happy to make any adjustments needed.

@shubham1206agra
Copy link
Copy Markdown
Contributor

@MelvinBot If the isWorkEmailValidated is false, what will happen? Will I see the validation flow once or twice?

@MelvinBot
Copy link
Copy Markdown
Contributor Author

Twice. When isWorkEmailValidated is false, the user sees two separate magic code screens:

  1. Email validationWorkspaceVerifyWorkAccountPage sends a code to the new work email via resendValidateCode() and calls getAccessiblePolicies(validateCode) on submit. This validates the email itself.

  2. Identity confirmation — Once the email is validated, the useEffect navigates to the confirmation page, which sends a second code to the current primary login via requestValidateCodeAction() and calls setContactMethodAsDefault(email, validateCode) on submit. This verifies the user's identity before changing the default contact method.

This is intentional per the proposal — the first code proves you own the new email, the second code proves you own the current primary (preventing a bad actor from silently promoting a planted email).

@shubham1206agra
Copy link
Copy Markdown
Contributor

@MelvinBot Merge main please

…deForCardProvisioning

Co-authored-by: Shubham Agrawal <shubham1206agra@users.noreply.github.com>
@MelvinBot
Copy link
Copy Markdown
Contributor Author

✅ Merged main into this branch successfully.

@shubham1206agra
Copy link
Copy Markdown
Contributor

@MelvinBot Why am I seeing the code to the same email when I try to enter unverified work email?

@MelvinBot
Copy link
Copy Markdown
Contributor Author

The ADD_WORK_EMAIL backend command updates account.primaryLogin to the newly entered email before the confirmation page loads. So when the confirmation page calls requestValidateCodeAction() (which sends RESEND_VALIDATE_CODE with null params, letting the backend pick the recipient from the session's primary login), the code goes to the new work email — not the original primary.

The confirmation page also displays the updated primaryLogin as the recipient at line 72, confirming the UI shows the "same email."

The fix: The confirmation page needs to send the magic code to the original primary login (before AddWorkEmail changed it), not rely on account.primaryLogin. You could either:

  1. Pass the original primary login as a route param from the AddWorkEmail page before calling AddWorkEmail, or
  2. Store the original primary login before the AddWorkEmail call and use it on the confirmation page instead of reading from account.primaryLogin

The same issue applies to both the company card and Expensify card confirmation pages.

@tgolen
Copy link
Copy Markdown
Contributor

tgolen commented May 14, 2026

which sends RESEND_VALIDATE_CODE with null params, letting the backend pick the recipient from the session's primary login

I think this should be fixed on the frontend. Rather then sending null, we should send the proper email address that we want the code to be sent to.

I wouldn't advise changing the backend for this because it is used for many different flows.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants