Skip to content

feat: Add Login UI MFA Flow#142

Open
matiasperrone-exo wants to merge 5 commits into
feat/mfa---usercontroller-mfa-integration-device-trust-cookie-management-audit-wiring-and-2fa-rate-limiting---cu-86ba2zc6pfrom
feat/mfa---login-ui-flow
Open

feat: Add Login UI MFA Flow#142
matiasperrone-exo wants to merge 5 commits into
feat/mfa---usercontroller-mfa-integration-device-trust-cookie-management-audit-wiring-and-2fa-rate-limiting---cu-86ba2zc6pfrom
feat/mfa---login-ui-flow

Conversation

@matiasperrone-exo

@matiasperrone-exo matiasperrone-exo commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

Task:

Ref: https://app.clickup.com/t/9014802374/86ba2zp3q

Changes

Preview

image

Overview

Frontend-only change adding a multi-factor authentication (MFA) flow to the React login UI. The monolithic login.js was refactored — inline form components were extracted into dedicated files, and two new MFA screens (verification code + recovery code) were added.

Highlights

  • New MFA flow: after a successful password step, the backend can signal mfa_required, which transitions the UI into a two-factor email-OTP challenge instead of completing login.
  • Redirect-aware AJAX: a new postRawRequestFull helper captures the final URL after server-side redirects (xhr.responseURL) so the SPA can navigate the top window to the post-login destination.
  • Component extraction: all inline form definitions in login.js were moved into resources/js/login/components/, slimming and reorganizing the main file.

New Files

Actions & constants

  • resources/js/login/constants.jsHTTP_CODES, LOGIN_FLOW, MFA_METHODS, FLOW, MFA_ERROR_CODE, and defaults (OTP length 6, TTL 300, default method email_otp).

Login form components (resources/js/login/components/)

  • two_factor_form.js — MFA email-OTP entry with expiry countdown, 30s resend cooldown, "Trust this device for 30 days", links to recovery code / cancel.
  • recovery_code_form.js — recovery code entry screen with back-to-OTP and cancel.
  • email_input_form.js — extracted email entry form.
  • password_input_form.js — extracted password form (incl. failed-attempt / account-lock messaging).
  • otp_input_form.js — extracted single-use code form.
  • email_error_actions.js, existing_account_actions.js, help_links.js, otp_help_links.js, third_party_identity_providers.js — extracted helper/link components.

Modified Files

  • resources/js/base_actions.js — added postRawRequestFull (returns { response, status, finalUrl }).
  • resources/js/login/actions.js — added verify2FA, resend2FA, verifyRecoveryCode, and authenticateWithPassword (uses the redirect-aware helper).
  • resources/js/login/login.js — major refactor:
    • Imports extracted components instead of defining them inline.
    • Added MFA state (mfaMethod, trustDevice, twoFactorCode, recoveryCode, otpLength, otpLifetime, codeVersion) and handlers (onVerify2FA, onResend2FA,
      onVerifyRecovery, onCancel2FA, onUseRecovery, onBackToOtp).
    • Password auth now goes through authenticateWithPassword; on mfa_required it switches to the MFA flow, otherwise redirects via finalUrl.
    • Render logic gated by derived flow flags (showTwoFactorForm, showRecoveryForm, isPasswordFlow, isOtpFlow, showDefaultFlow).
  • resources/js/login/login.module.scss — new styles: .info_message, .countdown, .trust_device_row, .disabled_link.
  • resources/views/auth/login.blade.php — wired new endpoints/config: verify2faAction, resend2faAction, recovery2faAction, mfaMethod, plus session-driven otpLength /
    otpLifetime; exposes VERIFY_2FA_ENDPOINT, RESEND_2FA_ENDPOINT, RECOVERY_2FA_ENDPOINT, FORM_ACTION_ENDPOINT on window.
  • package.json — updated serve script flag (--https--server-type https) for the newer webpack-dev-server.

User's GOAL

Current state

The login React component supports password and passwordless OTP flows. It has no MFA verification screen after password authentication.

Target state

The login UI supports a third flow state, '2fa'. When the backend returns mfa_required, the UI renders an MFA verification step with OTP input, trust-device checkbox, resend action, recovery-code link, and relevant error handling.

TASKS

  • refactor all reactjs components from resources/js/login/login.js and move then to a new folder under resources/js/login/components
    ( each component should have its own file )
  • Add '2fa' flow state to login component.
  • Detect error_code=mfa_required in login response.
  • Store username, otp_length, otp_lifetime, and method from response.
  • Render informational message: "A verification code has been sent to your email".
  • Reuse existing OTP input component.
  • Render "Trust this device for 30 days" checkbox, unchecked by default.
  • Implement OTP submit to POST /auth/login/2fa/verify with:
    • otp_value
    • method='email_otp'
    • trust_device
  • Implement resend to POST /auth/login/2fa/resend with method='email_otp'.
  • Handle resend success with user feedback and cooldown/countdown.
  • Handle 429 response with rate-limit message.
  • Implement Cancel action to return to password screen and clear client-side MFA state.
  • Add "Use a recovery code instead" link that transitions to the recovery-code login mode implemented in Ticket 12A.
  • Add countdown timer based on otp_lifetime.
  • Hide "Use a different method" in Phase I.
  • Ensure CSRF token is included in all POST requests.

ACCEPTANCE CRITERIA

  • mfa_required response transitions UI to 2FA screen.
  • OTP input length follows otp_length from server.
  • Valid OTP completes login and follows normal redirect/OIDC flow.
  • Invalid OTP shows inline error and remains on 2FA screen.
  • mfa_session_expired shows message and returns user to password flow.
  • mfa_rate_limit shows "Too many attempts" or approved product copy.
  • Resend calls correct endpoint and shows feedback.
  • Cancel returns to password flow.
  • Existing password and passwordless OTP flows are unaffected.

DEVELOPMENT NOTES

Key files:

  • Modified: resources/js/login/login.js
  • Possibly modified: resources/views/auth/login.blade.php
  • Existing: react-otp-input or equivalent OTP component

Gotchas:

  • The backend may return mfa_required through login_strategy->errorLogin(), which may be represented as flash data or JSON depending on flow. Follow existing login error handling.
  • Keep this as a state in the existing login component, not a separate page.
  • Trust-device checkbox must default to unchecked.

Risks:

  • Risk: React flow does not receive enough data from backend. Mitigation: verify mfa_required response includes username, method, otp_length, otp_lifetime.

Out of scope:

Recovery-code profile management, device-management UI, method selector for future MFA methods.

Summary by CodeRabbit

  • New Features

    • Added two-factor authentication (2FA) with one-time password (OTP) verification.
    • Added recovery code verification option.
    • Added "trust this device" functionality to streamline future logins.
    • Added support for third-party identity providers for social login.
    • Added CAPTCHA integration for enhanced security.
    • Improved login flow with better error messaging and code expiry countdowns.
  • Bug Fixes

    • Improved session expiration handling during authentication.
  • Chores

    • Updated HTTPS server configuration.

@coderabbitai

coderabbitai Bot commented Jun 22, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ac68b20d-08b3-4813-95e1-34237da5dae0

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/mfa---login-ui-flow

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.

@matiasperrone-exo matiasperrone-exo self-assigned this Jun 22, 2026
@github-actions

Copy link
Copy Markdown

📘 OpenAPI / Swagger preview

➡️ https://OpenStackweb.github.io/openstackid/openapi/pr-142/

This page is automatically updated on each push to this PR.

@github-actions

Copy link
Copy Markdown

📘 OpenAPI / Swagger preview

➡️ https://OpenStackweb.github.io/openstackid/openapi/pr-142/

This page is automatically updated on each push to this PR.

@matiasperrone-exo matiasperrone-exo force-pushed the feat/mfa---login-ui-flow branch from fc5deae to 3481ac9 Compare June 23, 2026 14:13
@github-actions

Copy link
Copy Markdown

📘 OpenAPI / Swagger preview

➡️ https://OpenStackweb.github.io/openstackid/openapi/pr-142/

This page is automatically updated on each push to this PR.

@matiasperrone-exo matiasperrone-exo marked this pull request as ready for review June 23, 2026 14:28
@matiasperrone-exo matiasperrone-exo self-assigned this Jun 23, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 16

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@resources/js/base_actions.js`:
- Around line 102-106: The issue is that params are unconditionally being added
to the URL query string via the url.query(params) call, which exposes sensitive
POST data like passwords, OTPs, and captchas in URLs and logs. Modify the code
to only add params to the query string for non-POST requests. Check the HTTP
method of the request before calling url.query(params) and only apply it when
the method is not POST, ensuring sensitive data in POST requests stays in the
request body and is never exposed in the URL.

In `@resources/js/login/components/email_error_actions.js`:
- Around line 22-42: The two Button components in this block are not respecting
the disableInput state, allowing users to trigger actions even when the UI
should be locked. Add the disabled prop set to disableInput on both buttons: the
button with onClick={emitOtpAction} and the button with
href={createAccountAction}. This will prevent users from clicking these recovery
action buttons when the input is disabled.

In `@resources/js/login/components/email_input_form.js`:
- Around line 51-55: Remove the use of dangerouslySetInnerHTML on the error
message element in the email error rendering section. Instead of using
dangerouslySetInnerHTML with the emailError property, render the emailError text
directly as a child of the paragraph element to prevent potential XSS
vulnerabilities. This applies to the conditional block where emailError is not
empty and the error paragraph is being displayed.

In `@resources/js/login/components/existing_account_actions.js`:
- Around line 34-37: The `Link` component with `disabled={disableInput}` prop
does not actually prevent navigation in MUI v4 when using the default anchor
element. To fix this, add an onClick handler to the Link component that prevents
the default navigation behavior when disableInput is true. The onClick handler
should call preventDefault() on the event whenever disableInput is true,
effectively blocking the navigation while the input is locked.

In `@resources/js/login/components/help_links.js`:
- Around line 18-21: The URL construction for forgotPasswordActionHref does not
account for existing query parameters in the forgotPasswordAction base URL. When
appending the email parameter, the code always uses `?email=...`, which will
create invalid URLs if forgotPasswordAction already contains a query string.
Modify the conditional block where forgotPasswordActionHref is assigned (when
userName exists) to check if forgotPasswordAction already contains a `?`
character. If it does, use `&` to append the email parameter; otherwise use `?`.
This ensures proper URL formatting regardless of whether the base URL already
has query parameters.

In `@resources/js/login/components/otp_input_form.js`:
- Around line 55-58: The error label paragraph element in the OTP input form is
using dangerouslySetInnerHTML to render the otpError variable, which creates an
XSS security vulnerability. Remove the dangerouslySetInnerHTML prop and instead
render the otpError directly as text content of the paragraph element so that
any HTML-like characters in the error message are displayed as plain text rather
than being interpreted as executable HTML.
- Line 1: Remove the useMemo hook to prevent stale captcha visibility state.
First, remove useMemo from the import statement at the top of the
otp_input_form.js file. Then, locate where useMemo is being used in the
component (likely wrapping captcha visibility logic) and unwrap it by removing
the useMemo function call while keeping the code it was wrapping. This ensures
the captcha visibility logic runs on every render and properly reflects the
current state of dependencies like loginAttempts, rather than relying on a stale
memoized result based only on the function reference.
- Line 49: The `hasErrored` prop being passed to the OtpInput component is not
supported in react-otp-input v3.1.1 and was removed in v3.0.0+. Remove the
`hasErrored={!otpError}` prop from the OtpInput component and instead use the
`renderInput` prop to handle error state styling. Pass the otpError state to the
custom input component through the `renderInput` prop so that the error styling
can be conditionally applied within the input component based on the parent
component's otpError state.

In `@resources/js/login/components/password_input_form.js`:
- Around line 85-88: The paragraph element with className styles.error_label is
rendering passwordError as HTML using dangerouslySetInnerHTML, which creates an
XSS vulnerability. Remove the dangerouslySetInnerHTML prop and instead render
passwordError directly as text content within the paragraph element to safely
display the error message without executing any embedded HTML or scripts.

In `@resources/js/login/components/recovery_code_form.js`:
- Around line 22-25: The handleBack function defined in the recovery_code_form
component is not connected to any UI element, so users cannot interact with it
to go back to the OTP screen. Add a button element in the form's JSX render that
calls the handleBack function on click. This button should be placed in a
logical location in the recovery form UI, likely near the form submission or as
a navigation element. Note that the same issue applies to the code around lines
65-72, so ensure both instances have the corresponding UI elements added to
enable the back navigation functionality.
- Around line 53-55: The recoveryError is being rendered as raw HTML using
dangerouslySetInnerHTML, which creates an XSS vulnerability if the error content
contains user-controlled or backend text. Replace the dangerouslySetInnerHTML
prop with direct text rendering by removing the
dangerouslySetInnerHTML={{__html: recoveryError}} attribute and instead render
recoveryError directly as plain text inside the paragraph element with the
error_label className. This will automatically escape any HTML special
characters and prevent potential XSS attacks.

In `@resources/js/login/components/two_factor_form.js`:
- Around line 91-93: The otpError variable is being rendered with
dangerouslySetInnerHTML which creates an XSS vulnerability since the value comes
from server responses. Remove the dangerouslySetInnerHTML attribute from the
error message paragraph element in the two_factor_form component and instead
render otpError as plain text content. If HTML formatting is required for the
error message, sanitize otpError using a proper HTML sanitization library before
using dangerouslySetInnerHTML.

In `@resources/js/login/login.js`:
- Around line 435-463: Add an early guard clause at the beginning of the
onVerify2FA method to prevent duplicate verification requests. Before processing
any validation logic or making the verify2FA API call, check if
this.state.disableInput is already true, and if so, return early from the
method. This will prevent rapid successive clicks or Enter key events from
triggering multiple concurrent API calls. Apply the same guard pattern to the
other verify handler mentioned in the comment (around lines 524-547).
- Around line 183-202: In the handleAuthenticateValidation method, the password
validation block that checks if user_password is empty is being executed
unconditionally for all auth flows. This causes the OTP flow validation to fail
even when a valid OTP code is provided, because the password check comes after
the OTP check and always requires a password. Wrap the password validation block
in a conditional statement that only executes when the auth flow is NOT
FLOW.OTP, so that OTP flow validation can complete successfully without
requiring a password field.
- Around line 220-241: The `default` case in the switch statement declares
`const redirect` without block scope, creating scope and TDZ issues. Wrap the
entire contents of the `default` case (from the `const redirect` declaration
through the closing of the if-else block that checks the redirect condition)
with curly braces to create proper block scope and isolate the variable
declaration from other switch cases.

In `@resources/views/auth/login.blade.php`:
- Around line 102-107: The line setting window.RESET_2FA_ENDPOINT is referencing
config.reset2faAction which is not defined in the template's configuration
object, causing the window variable to become undefined. Verify the template's
PHP configuration section where the config object is initialized and either add
the missing reset2faAction property with the appropriate endpoint URL, or
correct the reference to use the correct existing config key name that
corresponds to the reset 2FA endpoint.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: dd2029b0-3aab-49c1-b3e1-2abdc066795e

📥 Commits

Reviewing files that changed from the base of the PR and between 5fc47a4 and 3481ac9.

📒 Files selected for processing (17)
  • package.json
  • resources/js/base_actions.js
  • resources/js/login/actions.js
  • resources/js/login/components/email_error_actions.js
  • resources/js/login/components/email_input_form.js
  • resources/js/login/components/existing_account_actions.js
  • resources/js/login/components/help_links.js
  • resources/js/login/components/otp_help_links.js
  • resources/js/login/components/otp_input_form.js
  • resources/js/login/components/password_input_form.js
  • resources/js/login/components/recovery_code_form.js
  • resources/js/login/components/third_party_identity_providers.js
  • resources/js/login/components/two_factor_form.js
  • resources/js/login/constants.js
  • resources/js/login/login.js
  • resources/js/login/login.module.scss
  • resources/views/auth/login.blade.php

Comment thread resources/js/base_actions.js Outdated
Comment on lines +102 to +106
if (!isObjectEmpty(params))
url = url.query(params);

let key = url.toString();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Avoid putting POST payloads into the URL query string.

At Line 102-103, params are copied into url.query(...) before POST. With authenticateWithPassword in resources/js/login/actions.js (Line 60-62), this can expose password/OTP/captcha values via URL/query logging and request-key tracking. Keep MFA/login payloads in the request body only.

🔧 Proposed fix
 export const postRawRequestFull = (endpoint) => (params, headers = {}) => {
-    let url = URI(endpoint);
-
-    if (!isObjectEmpty(params))
-        url = url.query(params);
-
-    let key = url.toString();
+    const url = URI(endpoint);
+    const key = url.toString();

     cancel(key);

     let req = http.post(url.toString());
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@resources/js/base_actions.js` around lines 102 - 106, The issue is that
params are unconditionally being added to the URL query string via the
url.query(params) call, which exposes sensitive POST data like passwords, OTPs,
and captchas in URLs and logs. Modify the code to only add params to the query
string for non-POST requests. Check the HTTP method of the request before
calling url.query(params) and only apply it when the method is not POST,
ensuring sensitive data in POST requests stays in the request body and is never
exposed in the URL.

Comment on lines +22 to +42
<Button
variant="contained"
onClick={emitOtpAction}
type="button"
className={styles.secondary_btn}
color="primary"
>
Email me a one time use code
</Button>
</Grid>
<Grid item>
<Button
variant="contained"
href={createAccountAction}
type="button"
target="_self"
className={styles.secondary_btn}
color="primary"
>
Register and set a password
</Button>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Disable all recovery actions while input is locked.

Two buttons in this block ignore disableInput, so users can still trigger actions while the UI is in a disabled state.

Suggested fix
         <Grid item>
           <Button
             variant="contained"
             onClick={emitOtpAction}
+            disabled={disableInput}
             type="button"
             className={styles.secondary_btn}
             color="primary"
           >
             Email me a one time use code
           </Button>
         </Grid>
         <Grid item>
           <Button
             variant="contained"
             href={createAccountAction}
+            disabled={disableInput}
             type="button"
             target="_self"
             className={styles.secondary_btn}
             color="primary"
           >
             Register and set a password
           </Button>
         </Grid>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<Button
variant="contained"
onClick={emitOtpAction}
type="button"
className={styles.secondary_btn}
color="primary"
>
Email me a one time use code
</Button>
</Grid>
<Grid item>
<Button
variant="contained"
href={createAccountAction}
type="button"
target="_self"
className={styles.secondary_btn}
color="primary"
>
Register and set a password
</Button>
<Button
variant="contained"
onClick={emitOtpAction}
disabled={disableInput}
type="button"
className={styles.secondary_btn}
color="primary"
>
Email me a one time use code
</Button>
</Grid>
<Grid item>
<Button
variant="contained"
href={createAccountAction}
disabled={disableInput}
type="button"
target="_self"
className={styles.secondary_btn}
color="primary"
>
Register and set a password
</Button>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@resources/js/login/components/email_error_actions.js` around lines 22 - 42,
The two Button components in this block are not respecting the disableInput
state, allowing users to trigger actions even when the UI should be locked. Add
the disabled prop set to disableInput on both buttons: the button with
onClick={emitOtpAction} and the button with href={createAccountAction}. This
will prevent users from clicking these recovery action buttons when the input is
disabled.

Comment on lines +51 to +55
{emailError != "" && (
<p
className={styles.error_label}
dangerouslySetInnerHTML={{ __html: emailError }}
></p>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Avoid rendering raw HTML for email errors.

dangerouslySetInnerHTML on error content can introduce XSS if any part of the message is attacker-influenced.

Suggested fix
       {emailError != "" && (
-        <p
-          className={styles.error_label}
-          dangerouslySetInnerHTML={{ __html: emailError }}
-        ></p>
+        <p className={styles.error_label}>{emailError}</p>
       )}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{emailError != "" && (
<p
className={styles.error_label}
dangerouslySetInnerHTML={{ __html: emailError }}
></p>
{emailError != "" && (
<p className={styles.error_label}>{emailError}</p>
)}
🧰 Tools
🪛 ast-grep (0.44.0)

[warning] 53-53: Do not assign unsanitized HTML to dangerouslySetInnerHTML; wrap with a sanitizer (e.g. DOMPurify.sanitize)
Context: dangerouslySetInnerHTML={{ __html: emailError }}
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting').

(react-dangerously-inner-html)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@resources/js/login/components/email_input_form.js` around lines 51 - 55,
Remove the use of dangerouslySetInnerHTML on the error message element in the
email error rendering section. Instead of using dangerouslySetInnerHTML with the
emailError property, render the emailError text directly as a child of the
paragraph element to prevent potential XSS vulnerabilities. This applies to the
conditional block where emailError is not empty and the error paragraph is being
displayed.

Source: Linters/SAST tools

Comment on lines +34 to +37
<Link
disabled={disableInput}
href={forgotPasswordActionHref}
target="_self"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, let's find and check the package.json to determine MUI version
cd /
fd -t f "package.json" -x head -50 {} \; 2>/dev/null | head -100

Repository: OpenStackweb/openstackid

Length of output: 2974


🏁 Script executed:

# Find the specific file mentioned in the review
fd -t f "existing_account_actions.js"

Repository: OpenStackweb/openstackid

Length of output: 220


🏁 Script executed:

# Check for package.json at repo root
cat package.json 2>/dev/null | grep -A5 -B5 "`@material-ui`"

Repository: OpenStackweb/openstackid

Length of output: 698


🏁 Script executed:

# Read the existing_account_actions.js file to see the full context
cat -n resources/js/login/components/existing_account_actions.js

Repository: OpenStackweb/openstackid

Length of output: 1678


🏁 Script executed:

# Search for other usages of Link component in the codebase to understand pattern
rg "from.*`@material-ui/core`" resources/js/login/components/existing_account_actions.js -A 10

Repository: OpenStackweb/openstackid

Length of output: 506


🏁 Script executed:

# Check what disableInput is and how it's defined
rg "disableInput" resources/js/login/components/existing_account_actions.js -B 2 -A 2

Repository: OpenStackweb/openstackid

Length of output: 585


🌐 Web query:

Material-UI v4 Link component disabled prop behavior

💡 Result:

In Material-UI (MUI) v4, the Link component does not have a built-in "disabled" state or default styling for a disabled appearance [1][2]. While you can pass a disabled prop to the Link component—as it is built on top of the Typography component and supports spreading arbitrary props—the Link component itself does not natively interpret or style this prop [3][1][4]. Consequently, simply adding the disabled prop to a Link will not prevent user interaction, nor will it change the component's appearance [1][2]. To achieve a disabled effect in MUI v4, you must implement custom logic and styling [1]: 1. Accessibility and Interaction: If you need to stop interaction, you should manually disable the link's functionality (e.g., by preventing the onClick handler) and manage its focusability, such as by setting tabIndex={-1} [1]. For accessibility, it is recommended to add the aria-disabled="true" attribute [1]. 2. Styling: Since there is no default "disabled" class, you need to apply custom CSS to visually indicate that the link is disabled (e.g., changing the text color, setting cursor to default or not-allowed, and disabling text-decoration) [1]. 3. Using component="button": If you change the underlying element to a button using component="button", the standard HTML disabled attribute will function; however, you will still need to define custom styles to achieve the desired disabled visual appearance, as MUI v4 does not provide default disabled styling for the Link component even when rendered as a button [1][2].

Citations:


🌐 Web query:

MUI v4.11 Link API disabled attribute navigation

💡 Result:

In MUI v4.11, setting the disabled prop on MuiLink only prevents navigation when the rendered root element is a button (component="button"); otherwise it won’t reliably stop link navigation because the underlying element is an . What the code shows (v4.11.4): - MuiLink defaults component="a" (anchor). [1] - The Link implementation applies a “button” class only when component === 'button'. [1] So for disabled navigation you must do one of these: 1) Render it as a button and use disabled Use component="button". This makes disabled behave like a real disabled button: the browser won’t trigger navigation for href-based routing and MUI applies disabled semantics/styles for the button variant. - Example pattern: ... [2] 2) Keep it as an anchor () and block navigation yourself If you must use href with the default component="a", disabled does not inherently stop the default anchor click. A workaround is to intercept the click and call preventDefault when you want “disabled navigation”. (This is essentially the pattern described as a working workaround.) [2] 3) For accessibility If you implement “disabled-like” behavior via preventDefault, also add aria-disabled and remove from tab order as needed (as suggested in the workaround). [2] Practical takeaway - If you’re using MuiLink for navigation and want a true “disabled” state in MUI v4.11: set component="button" (and provide onClick only if needed) rather than relying on disabled while keeping it as an . [2][1] - If you keep href and the element: you need an onClick handler to preventDefault; disabled alone won’t be sufficient. [2]

Citations:


Link is not actually disabled here.

Passing disabled to MUI v4 Link does not prevent navigation when using the default component="a", so this action can still fire when input is locked.

Suggested fix
       <Grid item xs={12}>
         <Link
-          disabled={disableInput}
           href={forgotPasswordActionHref}
+          onClick={disableInput ? (ev) => ev.preventDefault() : undefined}
+          aria-disabled={disableInput}
+          className={disableInput ? styles.disabled_link : undefined}
           target="_self"
           variant="body2"
         >
           Reset your password
         </Link>
       </Grid>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@resources/js/login/components/existing_account_actions.js` around lines 34 -
37, The `Link` component with `disabled={disableInput}` prop does not actually
prevent navigation in MUI v4 when using the default anchor element. To fix this,
add an onClick handler to the Link component that prevents the default
navigation behavior when disableInput is true. The onClick handler should call
preventDefault() on the event whenever disableInput is true, effectively
blocking the navigation while the input is locked.

Comment on lines +18 to +21
let forgotPasswordActionHref = forgotPasswordAction;
if (userName) {
forgotPasswordActionHref = `${forgotPasswordAction}?email=${encodeURIComponent(userName)}`;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Handle existing query params when appending email.

Current construction always uses ?email=..., which breaks when forgotPasswordAction already contains query parameters.

Suggested fix
     let forgotPasswordActionHref = forgotPasswordAction;
     if (userName) {
-      forgotPasswordActionHref = `${forgotPasswordAction}?email=${encodeURIComponent(userName)}`;
+      const separator = forgotPasswordAction.includes("?") ? "&" : "?";
+      forgotPasswordActionHref = `${forgotPasswordAction}${separator}email=${encodeURIComponent(userName)}`;
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let forgotPasswordActionHref = forgotPasswordAction;
if (userName) {
forgotPasswordActionHref = `${forgotPasswordAction}?email=${encodeURIComponent(userName)}`;
}
let forgotPasswordActionHref = forgotPasswordAction;
if (userName) {
const separator = forgotPasswordAction.includes("?") ? "&" : "?";
forgotPasswordActionHref = `${forgotPasswordAction}${separator}email=${encodeURIComponent(userName)}`;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@resources/js/login/components/help_links.js` around lines 18 - 21, The URL
construction for forgotPasswordActionHref does not account for existing query
parameters in the forgotPasswordAction base URL. When appending the email
parameter, the code always uses `?email=...`, which will create invalid URLs if
forgotPasswordAction already contains a query string. Modify the conditional
block where forgotPasswordActionHref is assigned (when userName exists) to check
if forgotPasswordAction already contains a `?` character. If it does, use `&` to
append the email parameter; otherwise use `?`. This ensures proper URL
formatting regardless of whether the base URL already has query parameters.

Comment on lines +91 to +93
{otpError &&
<p className={styles.error_label} dangerouslySetInnerHTML={{__html: otpError}}></p>
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Sanitize otpError before injecting HTML.

otpError is rendered through dangerouslySetInnerHTML, and this value can come from server response text. That creates an XSS sink on the login surface.

🔧 Suggested fix
-            {otpError &&
-                <p className={styles.error_label} dangerouslySetInnerHTML={{__html: otpError}}></p>
-            }
+            {otpError && <p className={styles.error_label}>{otpError}</p>}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{otpError &&
<p className={styles.error_label} dangerouslySetInnerHTML={{__html: otpError}}></p>
}
{otpError && <p className={styles.error_label}>{otpError}</p>}
🧰 Tools
🪛 ast-grep (0.44.0)

[warning] 91-91: Do not assign unsanitized HTML to dangerouslySetInnerHTML; wrap with a sanitizer (e.g. DOMPurify.sanitize)
Context: dangerouslySetInnerHTML={{__html: otpError}}
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting').

(react-dangerously-inner-html)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@resources/js/login/components/two_factor_form.js` around lines 91 - 93, The
otpError variable is being rendered with dangerouslySetInnerHTML which creates
an XSS vulnerability since the value comes from server responses. Remove the
dangerouslySetInnerHTML attribute from the error message paragraph element in
the two_factor_form component and instead render otpError as plain text content.
If HTML formatting is required for the error message, sanitize otpError using a
proper HTML sanitization library before using dangerouslySetInnerHTML.

Source: Linters/SAST tools

Comment on lines +183 to +202
handleAuthenticateValidation() {
if (this.state.authFlow === FLOW.OTP) {
if (this.state.otpCode == "") {
this.setState({
...this.state,
disableInput: false,
errors: { ...this.state.errors, otp: "Single-use code is empty" },
});
return false;
}
}

return (
<Grid container spacing={1} style={{marginTop: "30px"}}>
<Grid item xs={12}>
<Button variant="contained"
onClick={emitOtpAction}
type="button"
disabled={disableInput}
className={styles.secondary_btn}
color="primary">
Sign in by emailing me a single-use code
</Button>
</Grid>
<Grid item xs={12}>
<Link
disabled={disableInput}
href={forgotPasswordAction} target="_self" variant="body2">
Reset your password
</Link>
</Grid>
</Grid>
);
}

const ThirdPartyIdentityProviders = ({ thirdPartyProviders, formAction, disableInput, allowNativeAuth }) => {
return (
<>
{allowNativeAuth && <DividerWithText>or</DividerWithText>}
{
thirdPartyProviders.map((provider) => {
const verbiage = `${handleThirdPartyProvidersVerbiage(provider.name)} with ${provider.label}`;
return (
<Button
disabled={disableInput}
key={provider.name}
variant="contained"
className={styles.third_party_idp_button + ` ${provider.name}`}
color="primary"
target="_self"
title={verbiage}
href={`${formAction}/${provider.name}`}>
{verbiage}
</Button>
);
})
}
<p>If you have a login, you may still choose to use a social login with <b>the same email address</b> to
access your account.</p>
</>
);
}

const otp_flow = 'otp';
const password_flow = 'password';
if (this.state.user_password == "") {
this.setState({
...this.state,
disableInput: false,
errors: { ...this.state.errors, password: "Password is empty" },
});
return false;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

OTP flow is incorrectly blocked by password validation.

After OTP validation, the method still requires user_password for all flows. In FLOW.OTP, this rejects valid OTP submissions with “Password is empty”.

🔧 Suggested fix
-    if (this.state.user_password == "") {
+    if (
+      this.state.authFlow === FLOW.PASSWORD &&
+      this.state.user_password === ""
+    ) {
       this.setState({
         ...this.state,
         disableInput: false,
         errors: { ...this.state.errors, password: "Password is empty" },
       });
       return false;
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
handleAuthenticateValidation() {
if (this.state.authFlow === FLOW.OTP) {
if (this.state.otpCode == "") {
this.setState({
...this.state,
disableInput: false,
errors: { ...this.state.errors, otp: "Single-use code is empty" },
});
return false;
}
}
return (
<Grid container spacing={1} style={{marginTop: "30px"}}>
<Grid item xs={12}>
<Button variant="contained"
onClick={emitOtpAction}
type="button"
disabled={disableInput}
className={styles.secondary_btn}
color="primary">
Sign in by emailing me a single-use code
</Button>
</Grid>
<Grid item xs={12}>
<Link
disabled={disableInput}
href={forgotPasswordAction} target="_self" variant="body2">
Reset your password
</Link>
</Grid>
</Grid>
);
}
const ThirdPartyIdentityProviders = ({ thirdPartyProviders, formAction, disableInput, allowNativeAuth }) => {
return (
<>
{allowNativeAuth && <DividerWithText>or</DividerWithText>}
{
thirdPartyProviders.map((provider) => {
const verbiage = `${handleThirdPartyProvidersVerbiage(provider.name)} with ${provider.label}`;
return (
<Button
disabled={disableInput}
key={provider.name}
variant="contained"
className={styles.third_party_idp_button + ` ${provider.name}`}
color="primary"
target="_self"
title={verbiage}
href={`${formAction}/${provider.name}`}>
{verbiage}
</Button>
);
})
}
<p>If you have a login, you may still choose to use a social login with <b>the same email address</b> to
access your account.</p>
</>
);
}
const otp_flow = 'otp';
const password_flow = 'password';
if (this.state.user_password == "") {
this.setState({
...this.state,
disableInput: false,
errors: { ...this.state.errors, password: "Password is empty" },
});
return false;
}
handleAuthenticateValidation() {
if (this.state.authFlow === FLOW.OTP) {
if (this.state.otpCode == "") {
this.setState({
...this.state,
disableInput: false,
errors: { ...this.state.errors, otp: "Single-use code is empty" },
});
return false;
}
}
if (
this.state.authFlow === FLOW.PASSWORD &&
this.state.user_password === ""
) {
this.setState({
...this.state,
disableInput: false,
errors: { ...this.state.errors, password: "Password is empty" },
});
return false;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@resources/js/login/login.js` around lines 183 - 202, In the
handleAuthenticateValidation method, the password validation block that checks
if user_password is empty is being executed unconditionally for all auth flows.
This causes the OTP flow validation to fail even when a valid OTP code is
provided, because the password check comes after the OTP check and always
requires a password. Wrap the password validation block in a conditional
statement that only executes when the auth flow is NOT FLOW.OTP, so that OTP
flow validation can complete successfully without requiring a password field.

Comment on lines +220 to +241
switch (error_code) {
case MFA_ERROR_CODE.MFA_CHALLENGE_REQUIRED:
this.setState((prevState) => ({
...prevState,
authFlow: FLOW.MFA,
disableInput: false,
otpLength: otp_length ?? prevState.otpLength,
otpLifetime: otp_lifetime ?? prevState.otpLifetime,
}));
break;
default:
const redirect = finalUrl && status === HTTP_CODES.OK;
if (redirect) {
window.location.href = finalUrl;
} else {
this.showAlert("Oops... Something went wrong!", "error");
this.setState((prevState) => ({
...prevState,
disableInput: false,
authFlow: props.flow,
allowNativeAuth: props.allowNativeAuth,
showInfoBanner: props.showInfoBanner,
infoBannerContent: props.infoBannerContent,
}

if (props.authError != '' && !this.state.user_fullname) {
this.state.user_fullname = props.userName;
}));
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, find the file and verify its existence
fd -t f login.js | head -20

Repository: OpenStackweb/openstackid

Length of output: 216


🏁 Script executed:

# Check the specific lines in the file
if [ -f "resources/js/login/login.js" ]; then
  echo "=== File found, checking lines 220-241 ==="
  sed -n '220,241p' "resources/js/login/login.js"
else
  echo "=== File not found at that location, searching ==="
  find . -name "login.js" -type f 2>/dev/null | head -10
fi

Repository: OpenStackweb/openstackid

Length of output: 914


🏁 Script executed:

# Get context around lines 220-241 to understand the function
if [ -f "resources/js/login/login.js" ]; then
  echo "=== Extended context (lines 210-250) ==="
  sed -n '210,250p' "resources/js/login/login.js"
fi

Repository: OpenStackweb/openstackid

Length of output: 1488


🏁 Script executed:

# Check if Biome or ESLint is configured and what rules apply
fd -t f -e json -e js -e yaml \
  | xargs grep -l "noSwitchDeclarations\|eslintrc\|biome.json" 2>/dev/null \
  | head -10

Repository: OpenStackweb/openstackid

Length of output: 162


🏁 Script executed:

# Check for linter configuration files
fd -t f \( -name "biome.json" -o -name ".eslintrc*" -o -name "eslint.config.*" \) | head -10

Repository: OpenStackweb/openstackid

Length of output: 393


🏁 Script executed:

# Check package.json for eslint/biome dependencies
if [ -f "package.json" ]; then
  echo "=== Checking for linter packages in package.json ==="
  cat package.json | grep -A 5 -B 5 '"devDependencies"\|"eslint\|biome"' | head -30
fi

Repository: OpenStackweb/openstackid

Length of output: 766


🏁 Script executed:

# Verify the JavaScript/React syntax is correct by examining the file structure
ast-grep outline resources/js/login/login.js --view expanded | grep -A 20 "handleAuthenticatePasswordOk"

Repository: OpenStackweb/openstackid

Length of output: 869


🏁 Script executed:

# Search for all config files more comprehensively
find . -maxdepth 2 -type f \( -name "*eslint*" -o -name "biome.json" -o -name ".prettierrc*" \) 2>/dev/null

Repository: OpenStackweb/openstackid

Length of output: 162


🏁 Script executed:

# Check the entire package.json to see all dependencies
if [ -f "package.json" ]; then
  wc -l package.json
  cat package.json | jq '.devDependencies | keys[]' 2>/dev/null | grep -i "eslint\|biome\|lint" | head -20
fi

Repository: OpenStackweb/openstackid

Length of output: 179


🏁 Script executed:

# Verify the code pattern is actually problematic by checking what rules would flag it
# This is a well-known pattern that both ESLint and Biome flag
# Let's also check if there are any other switch statements in the codebase to compare
rg "switch\s*\(" resources/js/login/login.js -A 15 | head -50

Repository: OpenStackweb/openstackid

Length of output: 739


🏁 Script executed:

# Check for any CI/CD configuration that might enforce linting
find . -maxdepth 2 -type f \( -name ".github" -o -name ".gitlab-ci.yml" -o -name "*.yml" -o -name "*.yaml" \) | head -20

Repository: OpenStackweb/openstackid

Length of output: 208


🏁 Script executed:

# Look for any GitHub Actions workflow files
find .github/workflows -type f 2>/dev/null | head -10

Repository: OpenStackweb/openstackid

Length of output: 318


🏁 Script executed:

# Check if there's a .github directory with any workflow files
if [ -d ".github" ]; then
  find .github -type f | head -20
fi

Repository: OpenStackweb/openstackid

Length of output: 318


🏁 Script executed:

# Search the entire repo for any eslint or biome configuration files more thoroughly
find . -name "*eslint*" -o -name "biome.json" -o -name ".prettierrc*" 2>/dev/null | grep -v node_modules

Repository: OpenStackweb/openstackid

Length of output: 397


🏁 Script executed:

# Let's check the complete package.json for linting tools
cat package.json | jq '.devDependencies' 2>/dev/null | head -30

Repository: OpenStackweb/openstackid

Length of output: 1161


🏁 Script executed:

# Check if there are any other similar patterns in switch statements in the codebase
rg "switch\s*\(" --type js -A 20 | rg "^\s*(case|default):" -A 3 | head -40

Repository: OpenStackweb/openstackid

Length of output: 162


🏁 Script executed:

# Verify that the suggested fix (wrapping in braces) doesn't break anything
# by checking the structure of the fixed code
cat > /tmp/test-fix.js << 'EOF'
// Original code structure test
switch (error_code) {
  case MFA_ERROR_CODE.MFA_CHALLENGE_REQUIRED:
    this.setState((prevState) => ({
      ...prevState,
      authFlow: FLOW.MFA,
      disableInput: false,
      otpLength: otp_length ?? prevState.otpLength,
      otpLifetime: otp_lifetime ?? prevState.otpLifetime,
    }));
    break;
  default: {
    const redirect = finalUrl && status === HTTP_CODES.OK;
    if (redirect) {
      window.location.href = finalUrl;
    } else {
      this.showAlert("Oops... Something went wrong!", "error");
      this.setState((prevState) => ({
        ...prevState,
        disableInput: false,
      }));
    }
  }
}
EOF
# Verify syntax is valid
node --check /tmp/test-fix.js && echo "✓ Suggested fix is syntactically valid"

Repository: OpenStackweb/openstackid

Length of output: 201


Wrap the default switch clause in braces.

The default clause declares const redirect directly without block scope, creating scope and TDZ (Temporal Dead Zone) issues. Wrapping it in braces isolates the declaration and prevents potential bugs.

🔧 Suggested fix
       case MFA_ERROR_CODE.MFA_CHALLENGE_REQUIRED:
         this.setState((prevState) => ({
           ...prevState,
           authFlow: FLOW.MFA,
           disableInput: false,
           otpLength: otp_length ?? prevState.otpLength,
           otpLifetime: otp_lifetime ?? prevState.otpLifetime,
         }));
         break;
-      default:
+      default: {
         const redirect = finalUrl && status === HTTP_CODES.OK;
         if (redirect) {
           window.location.href = finalUrl;
         } else {
           this.showAlert("Oops... Something went wrong!", "error");
           this.setState((prevState) => ({
             ...prevState,
             disableInput: false,
           }));
         }
+      }
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
switch (error_code) {
case MFA_ERROR_CODE.MFA_CHALLENGE_REQUIRED:
this.setState((prevState) => ({
...prevState,
authFlow: FLOW.MFA,
disableInput: false,
otpLength: otp_length ?? prevState.otpLength,
otpLifetime: otp_lifetime ?? prevState.otpLifetime,
}));
break;
default:
const redirect = finalUrl && status === HTTP_CODES.OK;
if (redirect) {
window.location.href = finalUrl;
} else {
this.showAlert("Oops... Something went wrong!", "error");
this.setState((prevState) => ({
...prevState,
disableInput: false,
authFlow: props.flow,
allowNativeAuth: props.allowNativeAuth,
showInfoBanner: props.showInfoBanner,
infoBannerContent: props.infoBannerContent,
}
if (props.authError != '' && !this.state.user_fullname) {
this.state.user_fullname = props.userName;
}));
}
}
switch (error_code) {
case MFA_ERROR_CODE.MFA_CHALLENGE_REQUIRED:
this.setState((prevState) => ({
...prevState,
authFlow: FLOW.MFA,
disableInput: false,
otpLength: otp_length ?? prevState.otpLength,
otpLifetime: otp_lifetime ?? prevState.otpLifetime,
}));
break;
default: {
const redirect = finalUrl && status === HTTP_CODES.OK;
if (redirect) {
window.location.href = finalUrl;
} else {
this.showAlert("Oops... Something went wrong!", "error");
this.setState((prevState) => ({
...prevState,
disableInput: false,
}));
}
}
}
🧰 Tools
🪛 Biome (2.5.0)

[error] 231-231: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

(lint/correctness/noSwitchDeclarations)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@resources/js/login/login.js` around lines 220 - 241, The `default` case in
the switch statement declares `const redirect` without block scope, creating
scope and TDZ issues. Wrap the entire contents of the `default` case (from the
`const redirect` declaration through the closing of the if-else block that
checks the redirect condition) with curly braces to create proper block scope
and isolate the variable declaration from other switch cases.

Source: Linters/SAST tools

Comment on lines +435 to +463
onVerify2FA() {
const { twoFactorCode, trustDevice, mfaMethod } = this.state;
if (twoFactorCode === "") {
this.setState({
...this.state,
errors: {
...this.state.errors,
twofactor: "Verification code is empty",
},
});
return;
}
this.setState({
...this.state,
disableInput: true,
errors: { ...this.state.errors, twofactor: "" },
});

verify2FA(twoFactorCode, mfaMethod, trustDevice, this.props.token).then(
(payload) => {
// Success: the backend redirected (302) and the XHR followed it; navigate the top
// window to the final destination to resume the normal redirect / OIDC flow.
window.location.href = payload.finalUrl || window.location.href;
},
(error) => {
this.handleMfaError(error, "twofactor");
},
);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Add an early guard to prevent duplicate verify requests.

Both verify handlers can be invoked while disableInput is already true, which can trigger duplicate 2FA/recovery API calls (e.g., rapid submit/Enter events).

🔧 Suggested fix
   onVerify2FA() {
+    if (this.state.disableInput) return;
     const { twoFactorCode, trustDevice, mfaMethod } = this.state;
     if (twoFactorCode === "") {
       this.setState({
@@
   onVerifyRecovery() {
+    if (this.state.disableInput) return;
     const { recoveryCode } = this.state;
     if (recoveryCode === "") {
       this.setState({

Also applies to: 524-547

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@resources/js/login/login.js` around lines 435 - 463, Add an early guard
clause at the beginning of the onVerify2FA method to prevent duplicate
verification requests. Before processing any validation logic or making the
verify2FA API call, check if this.state.disableInput is already true, and if so,
return early from the method. This will prevent rapid successive clicks or Enter
key events from triggering multiple concurrent API calls. Apply the same guard
pattern to the other verify handler mentioned in the comment (around lines
524-547).

Comment on lines +102 to +107
window.VERIFY_2FA_ENDPOINT = config.verify2faAction;
window.RESEND_2FA_ENDPOINT = config.resend2faAction;
window.CANCEL_LOGIN_ENDPOINT = config.cancelLogin;
window.RECOVERY_2FA_ENDPOINT = config.recovery2faAction;
window.RESET_2FA_ENDPOINT = config.reset2faAction;
window.FORM_ACTION_ENDPOINT = config.formAction;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

RESET_2FA_ENDPOINT is wired to an undefined config key.

config.reset2faAction is never defined in this template, so window.RESET_2FA_ENDPOINT becomes undefined.

🔧 Suggested fix
-        window.RESET_2FA_ENDPOINT = config.reset2faAction;
+        window.RESET_2FA_ENDPOINT = config.cancelLogin;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
window.VERIFY_2FA_ENDPOINT = config.verify2faAction;
window.RESEND_2FA_ENDPOINT = config.resend2faAction;
window.CANCEL_LOGIN_ENDPOINT = config.cancelLogin;
window.RECOVERY_2FA_ENDPOINT = config.recovery2faAction;
window.RESET_2FA_ENDPOINT = config.reset2faAction;
window.FORM_ACTION_ENDPOINT = config.formAction;
window.VERIFY_2FA_ENDPOINT = config.verify2faAction;
window.RESEND_2FA_ENDPOINT = config.resend2faAction;
window.CANCEL_LOGIN_ENDPOINT = config.cancelLogin;
window.RECOVERY_2FA_ENDPOINT = config.recovery2faAction;
window.RESET_2FA_ENDPOINT = config.cancelLogin;
window.FORM_ACTION_ENDPOINT = config.formAction;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@resources/views/auth/login.blade.php` around lines 102 - 107, The line
setting window.RESET_2FA_ENDPOINT is referencing config.reset2faAction which is
not defined in the template's configuration object, causing the window variable
to become undefined. Verify the template's PHP configuration section where the
config object is initialized and either add the missing reset2faAction property
with the appropriate endpoint URL, or correct the reference to use the correct
existing config key name that corresponds to the reset 2FA endpoint.

@github-actions

Copy link
Copy Markdown

📘 OpenAPI / Swagger preview

➡️ https://OpenStackweb.github.io/openstackid/openapi/pr-142/

This page is automatically updated on each push to this PR.

@github-actions

Copy link
Copy Markdown

📘 OpenAPI / Swagger preview

➡️ https://OpenStackweb.github.io/openstackid/openapi/pr-142/

This page is automatically updated on each push to this PR.

@github-actions

Copy link
Copy Markdown

📘 OpenAPI / Swagger preview

➡️ https://OpenStackweb.github.io/openstackid/openapi/pr-142/

This page is automatically updated on each push to this PR.

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.

1 participant