Skip to content

Gate MFA enrollment on email OTP#1573

Merged
riderx merged 4 commits intomainfrom
riderx/account-2fa-lock
Feb 4, 2026
Merged

Gate MFA enrollment on email OTP#1573
riderx merged 4 commits intomainfrom
riderx/account-2fa-lock

Conversation

@riderx
Copy link
Copy Markdown
Member

@riderx riderx commented Feb 4, 2026

Summary (AI generated)

  • Guard auth trigger creation to avoid auth schema permission failures in CI
  • Keep email OTP MFA gating migration intact where auth privileges exist

Test plan (AI generated)

  • bun lint
  • Run CI (Supabase migrations should no longer fail on auth schema permissions)

Screenshots (AI generated)

  • N/A

Checklist (AI generated)

  • My code follows the code style of this project and passes\n .
  • My change requires a change to the documentation.
  • I have updated the documentation\n accordingly.
  • My change has adequate E2E test coverage.
  • I have tested my code manually, and I have provided steps how to reproduce\n my tests

Generated with AI

Copilot AI review requested due to automatic review settings February 4, 2026 02:41
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Feb 4, 2026

Warning

Rate limit exceeded

@riderx has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 15 minutes and 0 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📝 Walkthrough

Walkthrough

Adds an email OTP verification flow for 2FA: localization keys, UI/state and backend RPCs/types, a new user_security table with RLS and trigger enforcement to require recent email verification before MFA enrollment.

Changes

Cohort / File(s) Summary
Translations (multiple locales)
messages/en.json, messages/de.json, messages/es.json, messages/fr.json, messages/hi.json, messages/id.json, messages/it.json, messages/ja.json, messages/ko.json, messages/pl.json, messages/pt-br.json, messages/ru.json, messages/tr.json, messages/vi.json, messages/zh-cn.json
Added 10 new email-otp-* translation keys across locales for the email OTP / 2FA verification UI (title, description, prompts, status, actions, and "verified until" text).
Account settings UI & logic
src/pages/settings/account/index.vue
Integrated email OTP MFA flow: new reactive state (otpVerificationCode, otpVerifiedAt, timers, loading flags), load/send/verify functions, enforced OTP check in MFA enable/disable paths, UI elements for sending/entering codes, lifecycle timer cleanup, and routing/setup adjustments.
DB migrations & types
supabase/migrations/20260204103000_mfa_email_otp_guard.sql, src/types/supabase.types.ts
Added public.user_security table with email_otp_verified_at, singleton public.security_settings, RLS policies, public.record_email_otp_verified() and public.is_recent_email_otp_verified() functions, and auth.enforce_email_otp_for_mfa() trigger on auth.mfa_factors. Updated TypeScript DB types to include new table and record_email_otp_verified function.

Sequence Diagram

sequenceDiagram
    participant User as User
    participant Vue as Vue Component
    participant Supabase as Supabase Backend
    participant Email as Email Service

    User->>Vue: Open account settings
    Vue->>Supabase: loadOtpVerification()
    Supabase-->>Vue: Return otpVerifiedAt
    Vue->>User: Display OTP status

    User->>Vue: Click "Send verification code"
    Vue->>Supabase: sendOtpVerification()
    Supabase->>Email: Send OTP to user's email
    Email-->>User: Deliver OTP
    Supabase-->>Vue: Acknowledge sent

    User->>Vue: Enter OTP code
    Vue->>Supabase: verifyOtpForMfa(code)
    Supabase->>Supabase: Validate code & record email_otp_verified_at
    Supabase-->>Vue: Verification success
    Vue->>User: Show verified status

    User->>Vue: Enable MFA
    Vue->>Supabase: handleMfa() (includes OTP check)
    Supabase->>Supabase: auth.enforce_email_otp_for_mfa() verifies recent OTP
    alt OTP recent
        Supabase-->>Vue: MFA enabled
        Vue->>User: Success
    else OTP expired
        Supabase-->>Vue: Reject enable request
        Vue->>User: Prompt to reverify OTP
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested labels

enhancement

Poem

🐇 I hopped through inboxes, tiny and spry,
Sent sparkled codes that fluttered to the sky,
Guarding MFA with carrot-powered cheer,
OTPs secure the burrow far and near. 🥕🔐

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Description check ⚠️ Warning The PR description is incomplete and lacks critical sections required by the repository template. The provided description appears to be AI-generated summary text rather than the actual author's PR description with all required sections. The author must provide a complete PR description including: (1) A clear, detailed Summary explaining the purpose of the email OTP gating feature and its interaction with MFA enrollment; (2) A comprehensive Test plan with step-by-step instructions to verify the OTP verification flow, MFA enrollment logic, and migration success; (3) Screenshots or videos demonstrating the new OTP verification UI in account settings; (4) Confirmation of all checklist items, particularly E2E test coverage and manual testing steps for the complex OTP/MFA flow.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Gate MFA enrollment on email OTP' directly summarizes the primary change: restricting MFA enrollment until email OTP verification is completed.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch riderx/account-2fa-lock

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 474d4a99ac

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +81 to +85
IF TG_OP = 'INSERT' THEN
otp_ok := public.is_recent_email_otp_verified(NEW.user_id);
IF NOT otp_ok THEN
RAISE EXCEPTION 'email otp verification required for mfa enrollment';
END IF;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve backward compatibility for MFA enroll

This trigger rejects any auth.mfa_factors insert/update unless there is a recent user_security.email_otp_verified_at row. The only code that writes user_security in this change is the new settings UI flow, so existing clients (mobile apps, scripts, or older web builds) that call supabase.auth.mfa.enroll() directly will now fail with email otp verification required for mfa enrollment. That’s a backward-incompatible change to the public MFA enrollment API and will block users on older clients from enabling 2FA unless you add a compatibility path (e.g., allow legacy clients or provide a backend endpoint that performs the OTP verification/write).

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@src/pages/settings/account/index.vue`:
- Around line 44-71: The computed otpVerificationValid is stale because it only
depends on otpVerifiedUntil.value; add a small reactive clock ref (e.g., nowRef)
that updates every X seconds with dayjs() and include nowRef.value in
otpVerificationValid so the computed re-evaluates as time passes; create the
interval in the component setup and clear it on unmount (use onUnmounted) to
avoid leaks, and ensure otpVerificationStatus and otpVerifiedUntilLabel keep
using otpVerificationValid/otpVerifiedUntil so the UI reflects expiry correctly.
- Around line 871-897: Replace the plain HTML buttons and input used for OTP
with DaisyUI components while preserving existing bindings and behavior: swap
the two <button> elements that call sendOtpVerification and verifyOtpForMfa (and
use otpSending, otpVerificationLoading, otpVerificationCode for :class and
:disabled) to use d-btn with the same :class, :disabled, `@click` and text
(t('email-otp-send-code') and t('verify')), and replace the input bound to
otpVerificationCode with a d-input preserving v-model="otpVerificationCode",
inputmode="numeric", :placeholder="t('verification-code')", class styling, and
`@keydown.enter.prevent`="verifyOtpForMfa"; ensure disabled styling and loading
states map to DaisyUI props/classes so behavior and accessibility remain
identical.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds an email OTP “step-up” verification that must be completed (and remain fresh for 1 hour) before a user can enroll TOTP-based MFA.

Changes:

  • Adds a public.user_security table plus helper function and an auth.mfa_factors trigger to block MFA enrollment unless a recent email OTP verification is recorded.
  • Updates the account settings UI to send/verify an email OTP and gate the “Enable 2FA” action based on verification freshness.
  • Extends i18n strings and updates generated Supabase DB types to include user_security.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 4 comments.

File Description
supabase/migrations/20260204103000_mfa_email_otp_guard.sql Adds user_security + helper function + trigger enforcing “recent email OTP required” for MFA factor creation/verification.
src/types/supabase.types.ts Adds user_security table typing to the generated Supabase Database types.
src/pages/settings/account/index.vue Adds UI/workflow to send & verify email OTP, store verification time, and block MFA enrollment unless verification is still valid.
messages/en.json Adds new English i18n keys for the email OTP verification UI.

Comment on lines +23 to +32
"email-otp-2fa-title": "Email verification for 2FA",
"email-otp-2fa-description": "Verify your email with a one-time code before enabling 2FA. Verification expires after 1 hour.",
"email-otp-code-required": "Enter the verification code",
"email-otp-expired": "Verification expired, please verify again",
"email-otp-not-verified": "Not verified",
"email-otp-required": "Verify your email before enabling 2FA",
"email-otp-send-code": "Send verification code",
"email-otp-sent": "Verification code sent",
"email-otp-verified": "Email verified for 2FA",
"email-otp-verified-until": "Verified until {time}",
Copy link

Copilot AI Feb 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New i18n keys were added only in en.json. Since src/modules/i18n.ts doesn’t configure a fallbackLocale, other locales will render the raw key (and emit missing-key warnings) when this UI is used.

Add these keys to the other messages/*.json locale files (or configure a fallback strategy) to avoid regressions for non-English users.

Copilot uses AI. Check for mistakes.
Comment on lines 611 to 614
const { error: unregisterError } = await supabase.auth.mfa.unenroll({ factorId: data.id })
if (error)
console.error('Cannot unregister MFA', unregisterError)
}
Copy link

Copilot AI Feb 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both cancel paths in handleMfa check if (error) (from the earlier enroll call) instead of checking unregisterError, so MFA unenroll failures won’t be surfaced correctly when the user cancels the setup flow.

Copilot uses AI. Check for mistakes.
Comment on lines +697 to +698
email_otp_verified_at: now,
updated_at: now,
Copy link

Copilot AI Feb 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This writes email_otp_verified_at using the client’s clock (new Date().toISOString()). Besides clock skew, it also makes the “verified for 1 hour” window dependent on a client-supplied timestamp.

Prefer setting the verification timestamp on the database side (e.g., via a SECURITY DEFINER RPC or trigger that uses now()), and have the client call that instead of sending the timestamp value.

Suggested change
email_otp_verified_at: now,
updated_at: now,

Copilot uses AI. Check for mistakes.
Comment on lines +29 to +40
CREATE POLICY "Users can insert own security status"
ON "public"."user_security"
FOR INSERT
TO authenticated
WITH CHECK (user_id = (select auth.uid()));

CREATE POLICY "Users can update own security status"
ON "public"."user_security"
FOR UPDATE
TO authenticated
USING (user_id = (select auth.uid()))
WITH CHECK (user_id = (select auth.uid()));
Copy link

Copilot AI Feb 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The user_security RLS policies currently allow any authenticated user to insert and update their own email_otp_verified_at value, even though this column is used by auth.enforce_email_otp_for_mfa to gate MFA enrollment. An attacker who has an authenticated session (e.g., via stolen token) can simply create or update their user_security row and set email_otp_verified_at to a recent timestamp, thereby bypassing the intended email OTP verification requirement for registering or verifying MFA factors. To fix this, prevent direct client-controlled writes to email_otp_verified_at (e.g., restrict INSERT/UPDATE on this column to trusted backend logic or a secure RPC that only sets it after a real OTP check).

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 12

🤖 Fix all issues with AI agents
In `@messages/hi.json`:
- Around line 23-32: Translate the new email-otp JSON keys into Hindi: replace
the values for "email-otp-2fa-title", "email-otp-2fa-description",
"email-otp-code-required", "email-otp-expired", "email-otp-not-verified",
"email-otp-required", "email-otp-send-code", "email-otp-sent",
"email-otp-verified", and "email-otp-verified-until" with appropriate Hindi
translations while keeping the placeholder "{time}" exactly as-is in
"email-otp-verified-until"; ensure tone matches existing Hindi entries and do
not change the keys or any placeholder formatting.

In `@messages/id.json`:
- Around line 23-32: Translate the new email-OTP message values in the id locale
for the keys "email-otp-2fa-title", "email-otp-2fa-description",
"email-otp-code-required", "email-otp-expired", "email-otp-not-verified",
"email-otp-required", "email-otp-send-code", "email-otp-sent",
"email-otp-verified", and "email-otp-verified-until" into Indonesian (replace
the current English strings), keeping the placeholder "{time}" exactly as-is in
"email-otp-verified-until" and preserving punctuation/capitalization conventions
consistent with the rest of the id file.

In `@messages/it.json`:
- Around line 23-32: Translate the new email-otp i18n keys into Italian: replace
values for "email-otp-2fa-title", "email-otp-2fa-description",
"email-otp-code-required", "email-otp-expired", "email-otp-not-verified",
"email-otp-required", "email-otp-send-code", "email-otp-sent",
"email-otp-verified", and "email-otp-verified-until" with appropriate Italian
text (keeping the existing keys unchanged) and ensure the placeholder "{time}"
remains intact in "email-otp-verified-until".

In `@messages/ja.json`:
- Around line 23-32: Several new localization keys (email-otp-2fa-title,
email-otp-2fa-description, email-otp-code-required, email-otp-expired,
email-otp-not-verified, email-otp-required, email-otp-send-code, email-otp-sent,
email-otp-verified, email-otp-verified-until) are still in English; replace each
English value with the appropriate Japanese translation, preserving placeholders
like {time} and JSON string formatting, and follow the file's existing
translation style and UTF-8 encoding to ensure consistency for Japanese users.

In `@messages/ko.json`:
- Around line 23-32: The listed "email-otp-*" message values are still in
English and must be replaced with Korean translations; update each key
("email-otp-2fa-title", "email-otp-2fa-description", "email-otp-code-required",
"email-otp-expired", "email-otp-not-verified", "email-otp-required",
"email-otp-send-code", "email-otp-sent", "email-otp-verified",
"email-otp-verified-until") with proper Korean text, preserving punctuation and
any placeholders (e.g., keep "{time}" exactly as-is for
"email-otp-verified-until"), and ensure translations match the tone/length of
other entries in the file.

In `@messages/pl.json`:
- Around line 23-32: Replace the English values for the listed localization keys
with Polish translations: update "email-otp-2fa-title",
"email-otp-2fa-description", "email-otp-code-required", "email-otp-expired",
"email-otp-not-verified", "email-otp-required", "email-otp-send-code",
"email-otp-sent", "email-otp-verified", and "email-otp-verified-until" to Polish
equivalents (e.g., "Weryfikacja e-mail dla 2FA", "Zweryfikuj swój e‑mail przy
użyciu jednorazowego kodu przed włączeniem 2FA. Weryfikacja wygasa po 1
godzinie.", "Wprowadź kod weryfikacyjny", "Weryfikacja wygasła, zweryfikuj
ponownie", "Niezweryfikowano", "Zweryfikuj swój e‑mail przed włączeniem 2FA",
"Wyślij kod weryfikacyjny", "Kod weryfikacyjny wysłany", "E‑mail zweryfikowany
dla 2FA", and "Zweryfikowano do {time}"), and ensure the {time} placeholder in
"email-otp-verified-until" is preserved exactly.

In `@messages/ru.json`:
- Around line 23-32: The locale file has ten email OTP keys still in English;
replace the English values with correct Russian translations for
"email-otp-2fa-title", "email-otp-2fa-description", "email-otp-code-required",
"email-otp-expired", "email-otp-not-verified", "email-otp-required",
"email-otp-send-code", "email-otp-sent", "email-otp-verified", and
"email-otp-verified-until" so the Russian locale is consistent (ensure
placeholders like {time} are preserved exactly).

In `@messages/tr.json`:
- Around line 23-32: Replace the English values for the email OTP localization
keys with Turkish translations: update "email-otp-2fa-title",
"email-otp-2fa-description", "email-otp-code-required", "email-otp-expired",
"email-otp-not-verified", "email-otp-required", "email-otp-send-code",
"email-otp-sent", "email-otp-verified", and "email-otp-verified-until" in
messages/tr.json to their proper Turkish equivalents while preserving
placeholder {time} in "email-otp-verified-until" and keeping the keys unchanged.

In `@messages/vi.json`:
- Around line 23-32: Replace the English values for the email OTP locale keys
with proper Vietnamese translations: "email-otp-2fa-title",
"email-otp-2fa-description", "email-otp-code-required", "email-otp-expired",
"email-otp-not-verified", "email-otp-required", "email-otp-send-code",
"email-otp-sent", "email-otp-verified", and "email-otp-verified-until"; update
their string values to correct Vietnamese phrases (including preserving the
placeholder {time} in "email-otp-verified-until") so the email OTP verification
flow displays Vietnamese text consistently.

In `@messages/zh-cn.json`:
- Around line 23-32: The listed locale entries (email-otp-2fa-title,
email-otp-2fa-description, email-otp-code-required, email-otp-expired,
email-otp-not-verified, email-otp-required, email-otp-send-code, email-otp-sent,
email-otp-verified, email-otp-verified-until) are still in English; replace each
value with the correct Simplified Chinese translation (preserving placeholders
like {time} exactly), e.g. translate the title/description, prompts and status
messages into Chinese, update the JSON values for those keys
(email-otp-verified-until must keep "{time}" unchanged), and run the locale/JSON
validation to ensure there are no syntax or placeholder errors.

In `@src/pages/settings/account/index.vue`:
- Around line 912-918: Replace the bespoke Tailwind class binding on the MFA
button with DaisyUI button classes so it matches other buttons; specifically,
change the element that has `@click`="handleMfa" to use the base classes "d-btn
d-btn-outline d-btn-sm" and map the dynamic states: when mfaEnabled is false use
a success/emerald DaisyUI variant (e.g., apply the success/emerald styling
class), when mfaEnabled is true use an error/rose variant, and when !mfaEnabled
&& !otpVerificationValid add the disabled styling (opacity/cursor) and keep
:disabled="!mfaEnabled && !otpVerificationValid"; preserve the
`@click`="handleMfa" handler and ensure the conditional class logic is expressed
using the same reactive properties (mfaEnabled, otpVerificationValid) but with
DaisyUI variant classes instead of the current bespoke border/ring classes.

In `@supabase/migrations/20260204103000_mfa_email_otp_guard.sql`:
- Around line 111-163: The trigger function auth.enforce_email_otp_for_mfa
currently skips OTP enforcement for users with user_created_at < enforced_at
(via the enforced_at check from
public.security_settings.mfa_email_otp_enforced_at), which allows pre‑existing
users to bypass OTP; update the function to remove or tighten that bypass by
eliminating the user_created_at < enforced_at conditional (or change the logic
to only bypass when enforcement is explicitly disabled in
public.security_settings), so that both INSERT and UPDATE flows always call
public.is_recent_email_otp_verified and raise the exception if otp_ok is false;
keep the trigger trg_enforce_email_otp_for_mfa and auth.mfa_factors target
unchanged.
🧹 Nitpick comments (3)
messages/pt-br.json (1)

23-32: Translate the new OTP strings to pt‑BR (currently English).

These values are user‑visible and should match the locale; leaving them in English creates a language mismatch in the UI.

💬 Suggested pt‑BR translations
-  "email-otp-2fa-title": "Email verification for 2FA",
-  "email-otp-2fa-description": "Verify your email with a one-time code before enabling 2FA. Verification expires after 1 hour.",
-  "email-otp-code-required": "Enter the verification code",
-  "email-otp-expired": "Verification expired, please verify again",
-  "email-otp-not-verified": "Not verified",
-  "email-otp-required": "Verify your email before enabling 2FA",
-  "email-otp-send-code": "Send verification code",
-  "email-otp-sent": "Verification code sent",
-  "email-otp-verified": "Email verified for 2FA",
-  "email-otp-verified-until": "Verified until {time}",
+  "email-otp-2fa-title": "Verificação de e-mail para 2FA",
+  "email-otp-2fa-description": "Verifique seu e-mail com um código de uso único antes de ativar o 2FA. A verificação expira após 1 hora.",
+  "email-otp-code-required": "Insira o código de verificação",
+  "email-otp-expired": "Verificação expirada, verifique novamente",
+  "email-otp-not-verified": "Não verificado",
+  "email-otp-required": "Verifique seu e-mail antes de ativar o 2FA",
+  "email-otp-send-code": "Enviar código de verificação",
+  "email-otp-sent": "Código de verificação enviado",
+  "email-otp-verified": "E-mail verificado para 2FA",
+  "email-otp-verified-until": "Verificado até {time}",
messages/fr.json (1)

23-32: Translate the new OTP strings to French (currently English).

These UI strings should be localized for the French locale.

💬 Suggested fr translations
-  "email-otp-2fa-title": "Email verification for 2FA",
-  "email-otp-2fa-description": "Verify your email with a one-time code before enabling 2FA. Verification expires after 1 hour.",
-  "email-otp-code-required": "Enter the verification code",
-  "email-otp-expired": "Verification expired, please verify again",
-  "email-otp-not-verified": "Not verified",
-  "email-otp-required": "Verify your email before enabling 2FA",
-  "email-otp-send-code": "Send verification code",
-  "email-otp-sent": "Verification code sent",
-  "email-otp-verified": "Email verified for 2FA",
-  "email-otp-verified-until": "Verified until {time}",
+  "email-otp-2fa-title": "Vérification de l’e-mail pour le 2FA",
+  "email-otp-2fa-description": "Vérifiez votre e-mail avec un code à usage unique avant d’activer le 2FA. La vérification expire après 1 heure.",
+  "email-otp-code-required": "Saisissez le code de vérification",
+  "email-otp-expired": "Vérification expirée, veuillez vérifier à nouveau",
+  "email-otp-not-verified": "Non vérifié",
+  "email-otp-required": "Vérifiez votre e-mail avant d’activer le 2FA",
+  "email-otp-send-code": "Envoyer le code de vérification",
+  "email-otp-sent": "Code de vérification envoyé",
+  "email-otp-verified": "E-mail vérifié pour le 2FA",
+  "email-otp-verified-until": "Vérifié jusqu’à {time}",
messages/de.json (1)

23-32: Translate the new OTP strings to German (currently English).

These values should be localized for the German locale to keep the UI consistent.

💬 Suggested de translations
-  "email-otp-2fa-title": "Email verification for 2FA",
-  "email-otp-2fa-description": "Verify your email with a one-time code before enabling 2FA. Verification expires after 1 hour.",
-  "email-otp-code-required": "Enter the verification code",
-  "email-otp-expired": "Verification expired, please verify again",
-  "email-otp-not-verified": "Not verified",
-  "email-otp-required": "Verify your email before enabling 2FA",
-  "email-otp-send-code": "Send verification code",
-  "email-otp-sent": "Verification code sent",
-  "email-otp-verified": "Email verified for 2FA",
-  "email-otp-verified-until": "Verified until {time}",
+  "email-otp-2fa-title": "E-Mail-Verifizierung für 2FA",
+  "email-otp-2fa-description": "Verifiziere deine E-Mail mit einem Einmalcode, bevor du 2FA aktivierst. Die Verifizierung läuft nach 1 Stunde ab.",
+  "email-otp-code-required": "Bestätigungscode eingeben",
+  "email-otp-expired": "Verifizierung abgelaufen, bitte erneut verifizieren",
+  "email-otp-not-verified": "Nicht verifiziert",
+  "email-otp-required": "E-Mail vor dem Aktivieren von 2FA verifizieren",
+  "email-otp-send-code": "Bestätigungscode senden",
+  "email-otp-sent": "Bestätigungscode gesendet",
+  "email-otp-verified": "E-Mail für 2FA verifiziert",
+  "email-otp-verified-until": "Verifiziert bis {time}",

Comment on lines +111 to +163
CREATE OR REPLACE FUNCTION "auth"."enforce_email_otp_for_mfa"() RETURNS trigger
LANGUAGE "plpgsql" SECURITY DEFINER
SET "search_path" TO ''
AS $$
DECLARE
otp_ok boolean;
enforced_at timestamptz;
user_created_at timestamptz;
BEGIN
SELECT public.security_settings.mfa_email_otp_enforced_at
INTO enforced_at
FROM public.security_settings
WHERE public.security_settings.id = true;

IF enforced_at IS NOT NULL THEN
SELECT auth.users.created_at
INTO user_created_at
FROM auth.users
WHERE auth.users.id = NEW.user_id;

IF user_created_at IS NOT NULL AND user_created_at < enforced_at THEN
RETURN NEW;
END IF;
END IF;

IF TG_OP = 'INSERT' THEN
otp_ok := public.is_recent_email_otp_verified(NEW.user_id);
IF NOT otp_ok THEN
RAISE EXCEPTION 'email otp verification required for mfa enrollment';
END IF;
RETURN NEW;
END IF;

IF TG_OP = 'UPDATE'
AND (NEW.status IS DISTINCT FROM OLD.status)
AND NEW.status = 'verified' THEN
otp_ok := public.is_recent_email_otp_verified(NEW.user_id);
IF NOT otp_ok THEN
RAISE EXCEPTION 'email otp verification required for mfa enrollment';
END IF;
END IF;

RETURN NEW;
END;
$$;

ALTER FUNCTION "auth"."enforce_email_otp_for_mfa"() OWNER TO "postgres";

DROP TRIGGER IF EXISTS "trg_enforce_email_otp_for_mfa" ON auth.mfa_factors;
CREATE TRIGGER "trg_enforce_email_otp_for_mfa"
BEFORE INSERT OR UPDATE ON auth.mfa_factors
FOR EACH ROW
EXECUTE FUNCTION auth.enforce_email_otp_for_mfa();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Potential OTP bypass for pre‑existing users.

The cutoff on user_created_at < enforced_at lets all pre‑enforcement users enroll/verify MFA via API without recent OTP, even though the UI blocks it. If the goal is to gate MFA enrollment for all users, remove this bypass (or only bypass when enforcement is explicitly disabled).

🔒 Suggested enforcement adjustment
 DECLARE
     otp_ok boolean;
     enforced_at timestamptz;
-    user_created_at timestamptz;
 BEGIN
     SELECT public.security_settings.mfa_email_otp_enforced_at
     INTO enforced_at
     FROM public.security_settings
     WHERE public.security_settings.id = true;
 
-    IF enforced_at IS NOT NULL THEN
-        SELECT auth.users.created_at
-        INTO user_created_at
-        FROM auth.users
-        WHERE auth.users.id = NEW.user_id;
-
-        IF user_created_at IS NOT NULL AND user_created_at < enforced_at THEN
-            RETURN NEW;
-        END IF;
-    END IF;
+    IF enforced_at IS NULL THEN
+        RETURN NEW;
+    END IF;
🤖 Prompt for AI Agents
In `@supabase/migrations/20260204103000_mfa_email_otp_guard.sql` around lines 111
- 163, The trigger function auth.enforce_email_otp_for_mfa currently skips OTP
enforcement for users with user_created_at < enforced_at (via the enforced_at
check from public.security_settings.mfa_email_otp_enforced_at), which allows
pre‑existing users to bypass OTP; update the function to remove or tighten that
bypass by eliminating the user_created_at < enforced_at conditional (or change
the logic to only bypass when enforcement is explicitly disabled in
public.security_settings), so that both INSERT and UPDATE flows always call
public.is_recent_email_otp_verified and raise the exception if otp_ok is false;
keep the trigger trg_enforce_email_otp_for_mfa and auth.mfa_factors target
unchanged.

@riderx riderx force-pushed the riderx/account-2fa-lock branch from a86260c to bf2b469 Compare February 4, 2026 04:16
@riderx riderx force-pushed the riderx/account-2fa-lock branch from ec6ef91 to b2be395 Compare February 4, 2026 04:26
@sonarqubecloud
Copy link
Copy Markdown

sonarqubecloud bot commented Feb 4, 2026

@riderx riderx merged commit 5922c4d into main Feb 4, 2026
11 checks passed
@riderx riderx deleted the riderx/account-2fa-lock branch February 4, 2026 04:33
@riderx
Copy link
Copy Markdown
Member Author

riderx commented Feb 4, 2026

/tip @testingpene1010 $250 please have look in our production if you confirm that fixed for you

@algora-pbc
Copy link
Copy Markdown

algora-pbc bot commented Feb 4, 2026

🎉🎈 @testingpene1010 has been awarded $250 by Capgo! 🎈🎊

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants