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('/'); } },