From 031f7dcd0cd55e185ba6103bc7d6f89bbd289320 Mon Sep 17 00:00:00 2001 From: OpenShift Cherrypick Robot Date: Tue, 11 Nov 2025 23:59:35 +0100 Subject: [PATCH] [redhat-3.16] fix(web): redirect to username confirmation page after LDAP login (PROJQUAY-9735) (#4501) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fix(web): redirect to username confirmation page after LDAP login (PROJQUAY-9735) When logging in with LDAP authentication, the new React UI was skipping the username confirmation page and going directly to the Organizations page. This fix ensures that users with the confirm_username prompt are redirected to the /updateuser page after successful login. Changes: - Modified Signin.tsx to check for user prompts after successful login - Added redirect to /updateuser if prompts exist - Enhanced UpdateUser.tsx to honor quay.redirectAfterLoad for external logins - Added Cypress e2e tests for username confirmation flow 🤖 Generated with [Claude Code](https://claude.com/claude-code) Signed-off-by: Brady Pratt Co-authored-by: Brady Pratt Co-authored-by: Claude --- web/cypress/e2e/signin.cy.ts | 72 ++++++++++++++++++++++++ web/src/routes/Signin/Signin.tsx | 26 +++++++++ web/src/routes/UpdateUser/UpdateUser.tsx | 32 +++++++++++ 3 files changed, 130 insertions(+) diff --git a/web/cypress/e2e/signin.cy.ts b/web/cypress/e2e/signin.cy.ts index 93cb09833..1c5075d10 100644 --- a/web/cypress/e2e/signin.cy.ts +++ b/web/cypress/e2e/signin.cy.ts @@ -234,6 +234,78 @@ describe('Signin page', () => { // Should not redirect cy.url().should('include', '/signin'); }); + + it('Redirects to username confirmation page when user has prompts', () => { + // Mock successful login + setupSuccessfulSignin(); + + // Mock user API to return user with confirm_username prompt + cy.intercept('GET', '/api/v1/user/', { + statusCode: 200, + body: { + anonymous: false, + username: 'test_ldap_user', + email: 'test@example.com', + verified: true, + prompts: ['confirm_username'], + organizations: [], + logins: [ + { + service: 'ldap', + service_identifier: 'test_ldap_user', + metadata: { + service_username: 'test_ldap_user', + }, + }, + ], + }, + }).as('getUserWithPrompt'); + + // Fill and submit login form + cy.get('#pf-login-username-id').type('test_ldap_user'); + cy.get('#pf-login-password-id').type('password'); + cy.get('button[type=submit]').click(); + + // Wait for signin and user fetch + cy.wait('@signinSuccess'); + cy.wait('@getCsrfToken'); + cy.wait('@getUserWithPrompt'); + + // Should redirect to updateuser page for username confirmation + cy.url().should('include', '/updateuser'); + }); + + it('Redirects to organization page when user has no prompts', () => { + // Mock successful login + setupSuccessfulSignin(); + + // Mock user API to return user without prompts + cy.intercept('GET', '/api/v1/user/', { + statusCode: 200, + body: { + anonymous: false, + username: 'user1', + email: 'user1@example.com', + verified: true, + prompts: [], + organizations: [], + logins: [], + }, + }).as('getUserNoPrompt'); + + // Fill and submit login form + cy.get('#pf-login-username-id').type('user1'); + cy.get('#pf-login-password-id').type('password'); + cy.get('button[type=submit]').click(); + + // Wait for signin and user fetch + cy.wait('@signinSuccess'); + cy.wait('@getCsrfToken'); + cy.wait('@getUserNoPrompt'); + + // Should redirect to organization page + cy.url().should('include', '/organization'); + }); }); describe('Forgot Password functionality', () => { diff --git a/web/src/routes/Signin/Signin.tsx b/web/src/routes/Signin/Signin.tsx index abecad36a..26137f16f 100644 --- a/web/src/routes/Signin/Signin.tsx +++ b/web/src/routes/Signin/Signin.tsx @@ -26,6 +26,8 @@ import {useExternalLogins} from 'src/hooks/UseExternalLogins'; import {useExternalLoginAuth} from 'src/hooks/UseExternalLoginAuth'; import {ExternalLoginButton} from 'src/components/ExternalLoginButton'; import {LoginPageLayout} from 'src/components/LoginPageLayout'; +import {useQueryClient} from '@tanstack/react-query'; +import {fetchUser} from 'src/resources/UserResource'; type ViewType = 'signin' | 'forgotPassword'; @@ -48,6 +50,7 @@ export function Signin() { const navigate = useNavigate(); const quayConfig = useQuayConfig(); const [searchParams] = useSearchParams(); + const queryClient = useQueryClient(); const { requestRecovery, isLoading: sendingRecovery, @@ -170,6 +173,29 @@ export function Signin() { setAuthState((old) => ({...old, isSignedIn: true, username: username})); await getCsrfToken(); GlobalAuthState.isLoggedIn = true; + + // Fetch fresh user data to check for prompts + let user; + try { + user = await queryClient.fetchQuery(['user'], fetchUser); + } catch (fetchErr) { + // If fetching user fails, show error and stop navigation + setErr( + addDisplayError( + 'Login successful but failed to load user data. Please refresh the page.', + fetchErr, + ), + ); + return; + } + + // If user has prompts (e.g., confirm_username), redirect to updateuser + if (user.prompts && user.prompts.length > 0) { + navigate('/updateuser'); + return; + } + + // Otherwise, redirect to the intended destination const redirectUrl = searchParams.get('redirect_url'); if (redirectUrl) { window.location.href = redirectUrl; diff --git a/web/src/routes/UpdateUser/UpdateUser.tsx b/web/src/routes/UpdateUser/UpdateUser.tsx index 9b3616d6a..a5b6a0878 100644 --- a/web/src/routes/UpdateUser/UpdateUser.tsx +++ b/web/src/routes/UpdateUser/UpdateUser.tsx @@ -47,6 +47,38 @@ export default function UpdateUser() { if (updatedUser?.prompts?.length) { setIsUpdating(false); } else { + // Check for stored redirect URL (set by external login flow) + const redirectUrl = localStorage.getItem('quay.redirectAfterLoad'); + localStorage.removeItem('quay.redirectAfterLoad'); + + if (redirectUrl) { + // Validate redirect URL to prevent open redirect vulnerability + try { + // Allow relative paths (starting with /) + if (redirectUrl.startsWith('/')) { + window.location.href = redirectUrl; + return; + } + + // For absolute URLs, validate they are same-origin + const url = new URL(redirectUrl); + if (url.origin === window.location.origin) { + window.location.href = redirectUrl; + return; + } + + // Invalid URL - fall through to default navigation + console.warn( + 'Ignoring redirect URL from different origin:', + redirectUrl, + ); + } catch (err) { + // Invalid URL format - fall through to default navigation + console.warn('Invalid redirect URL format:', redirectUrl, err); + } + } + + // Default navigation if no valid redirect URL navigate('/'); } },