Revamping authenticator app login

# Authenticator App Visibility & UX Improvements

Issue: [#7950 - Informing user about authenticator app](https://github.com/neetozone/neeto-auth-web/issues/7950)

---

## UI Changes

### 1. Post-Login Interstitial Page

After a user successfully logs in via email OTP, a full-page interstitial promotes the authenticator app.

- Title: "Skip the wait next time"

- Description: "Set up an authenticator app like Google Authenticator or 1Password for instant, secure sign-ins. No more waiting for login codes."

- Primary button: "Set up authenticator app" — sets a cross-subdomain cookie and redirects; after OTT login completes on the org subdomain, the Dashboard reads the cookie and redirects to Profile > Authenticator app

- Secondary button: "Maybe later" — proceeds to dashboard

- Snooze: A cross-subdomain cookie is set when the interstitial is shown, suppressing it for 7 days

- Only shows when:

- The feature flag is enabled

- The user hasn't already set up their authenticator app

- The 7-day snooze cookie has expired

Files: Auth/Login/AuthenticatorAppPromotion.jsx (new), Auth/Login/Otp.jsx (modified), Dashboard/index.jsx (modified)

---

### 2. Authenticator App Login Screen — "Other ways to sign in"

The authenticator app OTP login screen now shows alternative login methods below the code input.

Before: Only showed "Don't have access to the authenticator app? Use backup code"

After: Shows:

- "Don't have access to the authenticator app? Use backup code" (existing, kept as text link)

- Divider line

- "Other ways to sign in" heading

- "Login with email OTP" button (with EmailSent icon, AuthenticationButton style)

- "Login with Google" button (with Google icon, only if Google login is enabled)

The same alternative methods are shown on the backup code screen.

Files: Auth/Login/AuthenticatorAppOtp/AuthenticatorAppOtpCode.jsx, Auth/Login/AuthenticatorAppOtp/BackupCode.jsx, Auth/Login/AuthenticatorAppOtp.jsx

---

### 3. Admin Authentication Settings Page Redesign

Before: Flat toggles with no icons, separated by border-top lines. Authenticator app was nested under Email login with indentation. No visual hierarchy.

After:

- Each auth method has an icon (Google logo, Email icon, Apple logo)

- Methods separated by dividers with consistent padding

- Order: Google, Email OTP, Apple (Apple moved to last)

- Authenticator app section removed from admin page (it's now a user-level feature, not workspace-controlled)

- Scrollbar gutter stabilized to prevent horizontal jumps

File: AdminPanel/Authentication/Form.jsx

---

### 4. Google Login Settings — Checkbox & Cleaner Layout

Before: Domain restriction fields always visible with verbose titles.

After:

- Checkbox: "Restrict login to specific domains" — unchecked by default, hides input fields

- When checked: reveals allowed domains input and exception emails input

- Auto-expands if domains or emails already exist

- Titles shortened: "Restrict login to specific domains", "Exception"

- Descriptions moved below inputs as subtexts

- Scrollbar gutter fix prevents horizontal jump when checkbox is toggled

Files: AdminPanel/Authentication/GoogleLogin.jsx, stylesheets/layout/common.css

---

### 5. Profile > Authenticator App Page (New)

A new page in user profile settings where any user can set up or disable their authenticator app.

Nav item: "Authenticator app" with Security icon

Card layout using CardLayout component with:

- Title: "Authenticator app login" with status Tag inline (Enabled/Disabled)

- Description: Feature description

- Footer: "Set up" or "Disable" button (left), status tag (right in title)

- When email login disabled: Warning Callout with different messages for owners (with link to Authentication settings) and non-owners (contact workspace owner)

Setup flow: Inline on the same page (state-based). Custom stepper with labels below circles. Shows QR code, code verification, and backup codes.

File: Dashboard/Account/AuthenticatorApp.jsx (new)

---

### 6. Help Popovers

- Profile > Authenticator app page: Header has a help icon that opens a popover with description and "View help article" link

- Admin Authentication page: Help icon inline with the authenticator app title (removed along with the section)

- Help URL: Points to https://neetoauthhelp.neetokb.com/articles/how-to-set-up-and-use-authenticator-app-login

File: constants/helpUrls.js

---

### 7. Text & Copy Changes

| Location | Before | After |

|----------|--------|-------|

| Email login label | "Email login" | "Email OTP login" |

| Email login description | (none) | "Users will receive a one-time login code via email to sign in." |

| Authenticator app description | Long 2-sentence description | "Log in instantly and securely using Google Authenticator, 1Password, or any other authenticator app. No more waiting for OTP." |

| Disable login confirmation | "You are disabling {{loginType}} login. This can't be undone." | "Users will no longer be able to log in using {{loginType}}." |

| Disable email login confirmation | Same generic message | "Users will not be able to login with Email OTP and Authenticator app login." |

| Google allowed domains title | "Want to allow login only from some certain domains?" | "Restrict login to specific domains" |

| Google allowed domains description | Long sentence above input | Short subtext below input |

| Google exception description | Long sentence above input | Short subtext below input |

| Email input help text | "We will send a login code to this email." | Removed (from login, signup, and consumer login) |

---

### 8. Removed Components

- AdminPanel/Authentication/AuthenticatorAppOtpSetup/index.jsx (modal) — removed, unused

- AdminPanel/Authentication/AuthenticatorAppOtpSetup/Page.jsx — removed, replaced by inline setup in profile page

- AdminPanel/Authentication/AuthenticatorAppOtpSetup/Content.jsx — removed, replaced by inline setup in profile page

- AdminPanel/Authentication/AuthenticatorAppOtpSetup/FormDirtyTracker.js — removed, only used by Content.jsx

- Admin panel route for /admin/admin-panel/general/authentication/authenticator-app-otp — removed

---

## Functional Changes

### 1. Authenticator App Is Now a User-Level Feature

Before: The admin authentication page had a toggle for authenticator app that was confusingly tied to the admin's personal authenticator_app_otp_enabled user field. Regular users couldn't set up their authenticator app. The admin toggle both enabled the workspace feature AND triggered personal setup.

After:

- No workspace-level toggle — authenticator app is always available when the feature flag is enabled and email login is on

- Any user can set up their authenticator app from Profile > Authenticator app

- The only dependency is email login being enabled for the workspace

- The login flow (`generate_otp`) only checks user.authenticator_app_otp_enabled? — no org-level check

Files: sessions_controller.rb, Dashboard/Account/AuthenticatorApp.jsx (new), Dashboard/Account/index.jsx, Dashboard/Account/utils.js, routes.js

---

### 2. Email OTP Fallback on Authenticator Login Screen

Before: Users with authenticator app enabled could only log in with their authenticator code or backup codes. No fallback if they didn't have access to their authenticator app.

After: The authenticator login screen offers email OTP and Google login as fallbacks via "Other ways to sign in" section.

Files: Auth/Login/AuthenticatorAppOtp.jsx, Auth/Login/AuthenticatorAppOtp/AuthenticatorAppOtpCode.jsx, Auth/Login/AuthenticatorAppOtp/BackupCode.jsx

---

### 3. Post-Login Interstitial with Cross-Subdomain Cookie

Before: No promotion of authenticator app feature.

After: After email OTP login, if the user hasn't set up authenticator app and hasn't seen the interstitial in the last 7 days, a full-page interstitial is shown. Uses cross-subdomain cookies (not localStorage) since the OTP login happens on app subdomain and the dashboard loads on the org subdomain.

- Snooze cookie: authenticator_app_promo_snoozed (7 days, cross-subdomain)

- Setup redirect cookie: authenticator_app_setup_redirect (120 seconds, cross-subdomain)

Files: Auth/Login/Otp.jsx, Auth/Login/AuthenticatorAppPromotion.jsx, Dashboard/index.jsx

---

### 4. Disable Confirmation Messages

Before: Generic "This can't be undone" for all login methods.

After:

- Generic: "Users will no longer be able to log in using {{type}}."

- Email-specific: "Users will not be able to login with Email OTP and Authenticator app login."

- Authenticator disable (profile): "You will be switched back to email OTP for login."

- Removed "you can re-enable anytime" — obvious with a toggle

---

### 5. Organization-Level Column (Added but Unused)

A migration added authenticator_app_otp_enabled to the organizations table. However, we subsequently decided authenticator app should be a user-level feature (always available when email login is enabled). The column exists in the database but is not referenced in any code — it's not in permitted params, serializers, or any checks.

File: db/migrate/20260319035309_add_authenticator_app_otp_enabled_to_organizations.rb

---

### 6. Dev Bypass for Testing

Added a development-only bypass in the authenticator app login flow: entering 999999 as the authenticator code bypasses TOTP validation in local/development environments.

File: sessions_controller.rbbypass_authenticator_app_otp? method

Note: This must be removed before production deployment.

---

### 7. Permission-Based Access Control

The profile Authenticator app page checks globalProps.permissions (specifically MANAGE_ORGANIZATION_SETTINGS_PERMISSION) to determine if the user can access Authentication settings. This follows the app-wide convention of using role-based permissions rather than isOwner.

- Owners/admins: See warning callout with link to Authentication settings when email login is disabled

- Non-owners: See warning callout telling them to contact their workspace owner




# Issues Found in NeetoAuth Authenticator App Implementation

During the work on [#7950 - Informing user about authenticator app](https://github.com/neetozone/neeto-auth-web/issues/7950), we identified the following problems in the existing system through competitor analysis (Google, Microsoft, GitHub, Slack, Auth0, Okta) and hands-on testing.

---

## 1. Discoverability — Users Don't Know the Feature Exists

Problem: The authenticator app login feature existed but had zero visibility. Users logging in via email OTP had no indication that a faster alternative was available. There was no promotion in the login flow, on the OTP verification screen, or in the OTP email.

How competitors handle it: Microsoft shows a post-login interstitial ("registration campaign") nudging users to set up the authenticator app after every SMS/email login, with a configurable snooze period. Google shows security recommendation cards in account settings.

What we did: Added a post-login interstitial page that appears after successful email OTP login. The interstitial is snoozed for 7 days using a cross-subdomain cookie (required because OTP login happens on the app subdomain while the dashboard loads on the org subdomain). The snooze starts when the interstitial is shown, not when the user dismisses it.

---

## 2. No Fallback — Users Locked Out Without Authenticator App

Problem: Once a user enabled their authenticator app, it became the ONLY way to log in (besides backup codes). If the user was on a different device, lost their phone, or didn't have their authenticator app available, they were effectively locked out.

How competitors handle it: Google shows "Try another way" with a list of all configured methods. Microsoft shows "Sign in another way". GitHub shows text links for recovery codes and passkeys. All major products provide fallbacks.

What we did: Added "Other ways to sign in" section on the authenticator app login screen with "Login with email OTP" (sends OTP via existing resend API) and "Login with Google" (if enabled for the org) as alternatives.

---

## 3. No Workspace-Level vs User-Level Separation

Problem: There was no clear separation between workspace policy and user setup. The admin authentication page toggle was using the admin's personal authenticator_app_otp_enabled field as a proxy for a workspace-level setting. This caused: the form being always dirty after page load, the "Save changes" button being permanently active, and enabling the toggle navigating the admin away to personal setup.

How competitors handle it: Google, GitHub, Microsoft, and Slack all separate admin policy (enable/require the feature) from user setup (each user configures their own authenticator). In most products, the authenticator app is always available as a user choice — the admin can enforce 2FA but can't prevent users from setting it up.

What we did: Removed the authenticator app toggle from the admin page entirely. The authenticator app is now always available as a user-level feature when email login is enabled. Each user manages their authenticator from Profile > Authenticator app. The only dependency is email login being enabled for the workspace.

---

## 4. Regular Users Couldn't Set Up Authenticator App

Problem: The authenticator app setup flow only existed inside the admin panel. Regular users (non-admins) had no way to set up their authenticator app.

How competitors handle it: In every major product, users set up their authenticator from their own profile/security settings — never from the admin console.

What we did: Created a new "Authenticator app" page in user profile settings. Any user can set up or disable their authenticator app from here, with a full inline setup flow (QR code, code verification, backup codes).

---

## 5. Admin Toggle Behavior Was Confusing

Problem: When an admin toggled the authenticator app switch ON, it immediately navigated them away from the authentication settings page to the authenticator app setup flow. If they had other unsaved changes, those were lost. The BlockNavigation prompt appeared because the form was dirty.

What we did: Removed the authenticator toggle from the admin page. The feature is user-level, not workspace-controlled. The admin page now only controls Google login, Email OTP login, and Apple login.

---

## 6. Disable Confirmation Messages Were Incorrect

Problem: The disable confirmation dialog said "This can't be undone" when disabling any login method. This is incorrect — all login methods can be re-enabled. Additionally, disabling email login silently disabled the authenticator app toggle without any warning.

What we did: Changed messages to accurately describe the impact. Generic: "Users will no longer be able to log in using {{type}}." Email-specific: "Users will not be able to login with Email OTP and Authenticator app login." Removed "you can re-enable anytime" — obvious with a toggle.

---

## 7. Authentication Settings Page Had Poor Visual Design

Problem: The authentication settings page had no visual hierarchy. All login methods were flat toggles separated by thin border lines. No icons, no descriptions. The authenticator app was indented under email login, making it look like a sub-feature.

What we did: Added icons for each auth method. Each method visually separated with dividers. Added descriptions. Reordered: Google, Email OTP, Apple. Google's domain restriction fields hidden behind a checkbox to reduce visual noise. Scrollbar gutter fix prevents horizontal layout jumps.

---

## 8. Google Login Settings Were Verbose

Problem: The domain restriction section had verbose titles and descriptions that were always visible, taking up excessive vertical space even when the feature wasn't used.

What we did: Added a checkbox "Restrict login to specific domains" — unchecked by default, hiding the input fields. Shortened titles and moved descriptions below inputs as subtexts. Auto-expands if domains/emails already exist.

---

## 9. Login Flow Didn't Account for Authenticator Users When Disabling Email

Problem: No warning about the impact on authenticator app users when disabling email login. The authenticator app login depends on email login being enabled (the generate_otp flow is the entry point).

What we did: Added a specific confirmation message when disabling email login: "Users will not be able to login with Email OTP and Authenticator app login." The profile Authenticator app page shows a warning callout when email login is disabled, with a link to Authentication settings for owners and a "contact workspace owner" message for non-owners.

---

## 10. Unnecessary Help Text on Login/Signup Forms

Problem: "We will send a login code to this email" was shown on login, signup, and consumer login forms. This text is self-evident and inaccurate for users with authenticator app (they won't receive an email code).

What we did: Removed the help text from all three forms (Login, Signup, Consumer Login).

---

## 11. API Response Missing Fields / Stale Form State

Problem: The organization serializer (`_organization.json.jbuilder`) didn't include new fields, causing the Formik form to stay dirty after saving (form value didn't match the refetched initial value), keeping the "Save changes" button permanently active.

What we did: Initially added authenticator_app_otp_enabled to the jbuilder partial. Later removed it when we decided authenticator app is a user-level feature. The form now only tracks Google, Email, and Apple login settings.

---

## 12. Confusing Terminology

Problem: Throughout development, we used inconsistent terms — "Turn off" vs "Disable", "Active" vs "Enabled", "Security" vs "Authenticator app", "Not set up" vs "Disabled". This created confusion in code (variable names, translation keys, test IDs not matching the displayed text).

What we did: Standardized on: "Enable/Disable" for toggling, "Enabled/Disabled" for status tags. Renamed all variables, translation keys, test IDs, file names, and component names to match. The profile page file is AuthenticatorApp.jsx, the route is /admin/my/profile/authenticator-app, translation namespace is authenticatorApp.profile.*.

---

## 13. BackupCodes Component Prop Mismatch

Problem: The BackupCodes component expected a codes prop but was being passed backupCodes via spread syntax. This caused the backup codes to not render, leading to a crash after OTP verification in the setup flow.

What we did: Fixed the prop to pass codes={backupCodes} explicitly.

---

## 14. Cross-Subdomain State Sharing

Problem: Multiple attempts to pass state between the app subdomain (OTP login) and the org subdomain (dashboard) failed because localStorage is per-subdomain. URL-based approaches (`redirect_uri` manipulation) didn't work due to the OTT URL format using && double ampersands.

What we did: Used cross-subdomain cookies with domain=.{rootDomain} for both the interstitial snooze (7-day expiry) and the setup redirect (120-second expiry). Cookies are readable across all subdomains under the same root domain.

---

## 15. Owner vs Permission-Based Access Check

Problem: Initially used globalProps.user.isOwner to determine if a user could access Authentication settings. But in local development, has_owner_access? always returns true (`Rails.env.local?`), making it impossible to test the non-owner experience locally.

What we did: Switched to globalProps.permissions.includes(MANAGE_ORGANIZATION_SETTINGS_PERMISSION) — the same permission-based approach used across the entire app for route protection and access control. This correctly reflects the user's actual role in all environments.