Skip to content

Conversation

@ashwini-mv
Copy link
Contributor

@ashwini-mv ashwini-mv commented Nov 20, 2025

Problem:
The requireRole() middleware was always redirecting unauthenticated users to /sign-in, regardless of whether SSO was configured. This caused admin dashboard pages (/admin-dashboard, /system-admin-dashboard) to redirect to the account selection page instead of Azure AD SSO login, breaking the SSO authentication flow.

Solution:
Updated both requireRole() and requireAuth() middlewares to check:

  1. If the requested URL is an admin page
  2. If SSO is configured (ENABLE_SSO=true)
  3. Redirect to /login (Azure AD) for admin pages when SSO enabled
  4. Redirect to /sign-in (account selection) for non-admin pages or when SSO disabled

Changes:

  • Added SSO-aware redirect logic to requireRole() and requireAuth()
  • Added unit tests for new authentication redirect behavior
  • Added requireAuth() middleware to account-home page
  • Added CFT IDAM authentication setup to account-home E2E tests
  • Consolidated SSO and CFT IDAM test files for better organization

Jira link

https://tools.hmcts.net/jira/browse/VIBE-240

Testing done

yes

Checklist

  • commit messages are meaningful and follow good commit message guidelines
  • README and other documentation has been updated / added (if needed)
  • tests have been updated / new tests has been added (if needed)
  • Does this PR introduce a breaking change

Summary by CodeRabbit

  • Tests
    • Reorganized E2E coverage: removed obsolete suites, added a comprehensive SSO suite, expanded CFT flows (session lifecycle, concurrent sessions, parameter preservation, error paths) and added unit tests for unauthenticated-redirect logic.
  • Features
    • Account pages now require authentication and use a unified unauthenticated-redirect flow that routes admin pages to SSO and other pages to sign-in.
  • Bug Fixes
    • Improved preservation of original destination, query parameters and return URLs across authentication redirects.

✏️ Tip: You can customize this high-level summary in your review settings.

**Problem:**
The requireRole() middleware was always redirecting unauthenticated users
to /sign-in, regardless of whether SSO was configured. This caused admin
dashboard pages (/admin-dashboard, /system-admin-dashboard) to redirect
to the account selection page instead of Azure AD SSO login, breaking the
SSO authentication flow.

**Solution:**
Updated both requireRole() and requireAuth() middlewares to check:
1. If the requested URL is an admin page
2. If SSO is configured (ENABLE_SSO=true)
3. Redirect to /login (Azure AD) for admin pages when SSO enabled
4. Redirect to /sign-in (account selection) for non-admin pages or when
   SSO disabled

**Changes:**
- Added SSO-aware redirect logic to requireRole() and requireAuth()
- Added unit tests for new authentication redirect behavior
- Added requireAuth() middleware to account-home page
- Added CFT IDAM authentication setup to account-home E2E tests
- Consolidated SSO and CFT IDAM test files for better organization

**Test Results:**
✅ All 247 E2E tests passing
- SSO tests: 7/7 ✅
- CFT IDAM tests: 10/10 ✅
- Account Home tests: 42/42 ✅ (previously 0/42 ❌)
- All other tests: 188/188 ✅

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
@coderabbitai
Copy link

coderabbitai bot commented Nov 20, 2025

Walkthrough

Introduces a centralized redirect helper for unauthenticated requests and integrates it into authentication/authorization middleware and tests, requires auth on account-home, removes several legacy E2E test files while adding consolidated SSO/CFT suites, and adds/uses new E2E login helpers.

Changes

Cohort / File(s) Summary
Redirect helper & tests
libs/auth/src/middleware/redirect-helpers.ts, libs/auth/src/middleware/redirect-helpers.test.ts
New redirectUnauthenticated(req,res) that saves req.session.returnTo, detects admin routes, uses isSsoConfigured() and redirects to /login (admin+SSO) or /sign-in; comprehensive unit tests for URL/query/hash/trailing-slash behaviors.
Middleware integration & tests
libs/auth/src/middleware/authenticate.ts, libs/auth/src/middleware/authenticate.test.ts, libs/auth/src/middleware/authorise.ts, libs/auth/src/middleware/authorise.test.ts
Replaced inline redirect logic with redirectUnauthenticated calls; updated documentation/comments and tests to mock/assert the helper; adjusted some expected redirect targets (e.g., /sign-in).
Require auth on account-home
libs/verified-pages/src/pages/account-home/index.ts, libs/verified-pages/src/pages/account-home/index.test.ts
Added requireAuth() to GET middleware chain; updated test mock for @hmcts/auth to expose requireAuth as a no-op middleware.
E2E: account-home pre-login
e2e-tests/tests/account-home.spec.ts
Added beforeEach to perform HMCTS/CFT IDAM login via new loginWithCftIdam helper; removed explicit page.goto("/account-home") in tests; imported helper.
E2E: CFT IDAM suites
e2e-tests/tests/cft-idam/cft-idam.spec.ts, deleted e2e-tests/tests/cft-idam/cft-idam-session.spec.ts
Expanded main CFT IDAM suite to cover language/query-preservation, lifecycle (redirect back, session persistence, logout), error handling and concurrent sessions; removed separate session test file.
E2E: SSO consolidation
e2e-tests/tests/sso/sso.spec.ts, deleted e2e-tests/tests/sso/sso-login.spec.ts, e2e-tests/tests/sso/sso-authorisation.spec.ts, e2e-tests/tests/sso/sso-session.spec.ts
Added consolidated sso.spec.ts covering role-based access, session lifecycle, concurrent sessions and rejected flows; removed older separate SSO suites.
E2E helpers & imports
e2e-tests/.../utils/*, e2e-tests/tests/account-home.spec.ts
Tests now import and use shared helpers (loginWithCftIdam, loginWithSSO, assertAuthenticated, assertNotAuthenticated) and updated imports across E2E suites; several legacy files removed.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant Browser
    participant App
    participant RedirectHelper as redirectUnauthenticated
    participant SSOConfig as isSsoConfigured()

    User->>Browser: Request protected URL (e.g. /admin-dashboard or /account-home)
    Browser->>App: GET request
    App->>RedirectHelper: redirectUnauthenticated(req, res)
    RedirectHelper->>App: save req.session.returnTo
    RedirectHelper->>SSOConfig: isSsoConfigured()
    alt Admin page AND SSO configured
        RedirectHelper->>Browser: redirect to /login
        Note right of Browser: SSO (Azure AD) flow continues
    else Not admin OR SSO not configured
        RedirectHelper->>Browser: redirect to /sign-in
        Note right of Browser: local CFT IDAM flow continues
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

  • Areas needing attention:
    • URL detection logic in libs/auth/src/middleware/redirect-helpers.ts (correct prefix detection vs substring, handling of queries/hashes).
    • Updated unit tests that mock/expect redirectUnauthenticated in authenticate/authorise tests.
    • Confirm E2E removals vs consolidated suites maintain coverage and environment guards.
    • Multi-context E2E tests and reliability of new helpers (loginWithCftIdam, loginWithSSO).

Suggested reviewers

  • KianKwa

Poem

🐰 I hopped through routes and saved returnTo bright,

Sent admins to login, users to sign‑in light,
Tests now mock my helper, sessions kept in view,
E2E flows stitched, concurrent hops anew,
A little rabbit cheer — auth patched through and through. 🥕

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly summarizes the main change—adding SSO redirect logic to the requireRole middleware. It aligns with the primary objectives of updating authentication redirect behavior to be SSO-aware.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch VIBE-240

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

Copy link

@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

🧹 Nitpick comments (2)
e2e-tests/tests/sso/sso.spec.ts (1)

25-63: Consider test.each pattern to reduce duplication.

The Local Admin and CTSC Admin tests follow identical patterns. While test isolation is valuable in e2e testing, you could reduce duplication using Playwright's test.each pattern:

+  const adminRoles = [
+    { role: 'Local Admin', emailEnv: 'SSO_TEST_LOCAL_ADMIN_EMAIL', passwordEnv: 'SSO_TEST_LOCAL_ADMIN_PASSWORD' },
+    { role: 'CTSC Admin', emailEnv: 'SSO_TEST_CTSC_ADMIN_EMAIL', passwordEnv: 'SSO_TEST_CTSC_ADMIN_PASSWORD' }
+  ];
+
+  for (const { role, emailEnv, passwordEnv } of adminRoles) {
+    test(`${role} can login via SSO and access admin dashboard only`, async ({ page }) => {
+      await page.goto('/admin-dashboard');
+      await expect(page).toHaveURL(/login.microsoftonline.com/);
+      await loginWithSSO(page, process.env[emailEnv]!, process.env[passwordEnv]!);
+      await expect(page).toHaveURL('/admin-dashboard');
+      await assertAuthenticated(page);
+
+      await page.goto('/system-admin-dashboard');
+      await expect(page).toHaveURL('/admin-dashboard');
+
+      await page.goto('/account-home');
+      await expect(page).toHaveURL('/admin-dashboard');
+    });
+  }
e2e-tests/tests/cft-idam/cft-idam.spec.ts (1)

61-106: Consider splitting this test into focused scenarios.

This test validates three distinct concerns:

  1. Language parameter preservation through CFT IDAM flow (lines 62-77)
  2. Query parameter preservation in CFT login (lines 79-83)
  3. Welsh content display on rejection page (lines 85-105)

For better test isolation, maintainability, and clearer failure messages, consider splitting into separate tests:

test('Language parameter is preserved through CFT IDAM redirect', async ({ page }) => {
  await page.goto('/sign-in?lng=cy');
  const heading = page.getByRole('heading', { name: /sut hoffech chi fewngofnodi/i });
  await expect(heading).toBeVisible();
  
  const hmctsRadio = page.getByRole('radio', { name: /gyda chyfrif myhmcts/i });
  await hmctsRadio.check();
  const continueButton = page.getByRole('button', { name: /parhau/i });
  await continueButton.click();
  
  const currentUrl = page.url();
  expect(currentUrl).toMatch(/lng=cy|ui_locales=cy/);
});

test('Query parameters are preserved in CFT login redirect', async ({ page }) => {
  await page.goto('/cft-login?lng=cy&test=value');
  await page.waitForLoadState('networkidle'); // More explicit than timeout
  const currentUrl = page.url();
  expect(currentUrl).toMatch(/lng=cy|ui_locales=cy/);
});

test('CFT rejected page displays Welsh content correctly', async ({ page }) => {
  await page.goto('/cft-rejected?lng=cy');
  // ... rest of Welsh content tests
});

Also, consider replacing await page.waitForTimeout(2000) on line 81 with await page.waitForLoadState('networkidle') for more reliable synchronization.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1406e34 and e2a8b3b.

📒 Files selected for processing (11)
  • e2e-tests/tests/account-home.spec.ts (1 hunks)
  • e2e-tests/tests/cft-idam/cft-idam-session.spec.ts (0 hunks)
  • e2e-tests/tests/cft-idam/cft-idam.spec.ts (5 hunks)
  • e2e-tests/tests/sso/sso-authorisation.spec.ts (0 hunks)
  • e2e-tests/tests/sso/sso-login.spec.ts (0 hunks)
  • e2e-tests/tests/sso/sso-session.spec.ts (0 hunks)
  • e2e-tests/tests/sso/sso.spec.ts (1 hunks)
  • libs/auth/src/middleware/authenticate.test.ts (3 hunks)
  • libs/auth/src/middleware/authenticate.ts (2 hunks)
  • libs/auth/src/middleware/authorise.ts (3 hunks)
  • libs/verified-pages/src/pages/account-home/index.ts (2 hunks)
💤 Files with no reviewable changes (4)
  • e2e-tests/tests/sso/sso-authorisation.spec.ts
  • e2e-tests/tests/cft-idam/cft-idam-session.spec.ts
  • e2e-tests/tests/sso/sso-session.spec.ts
  • e2e-tests/tests/sso/sso-login.spec.ts
🧰 Additional context used
🧬 Code graph analysis (6)
libs/verified-pages/src/pages/account-home/index.ts (2)
libs/auth/src/middleware/authenticate.ts (1)
  • requireAuth (11-31)
libs/auth/src/middleware/authorise.ts (1)
  • blockUserAccess (58-77)
e2e-tests/tests/cft-idam/cft-idam.spec.ts (1)
e2e-tests/utils/cft-idam-helpers.ts (1)
  • loginWithCftIdam (10-41)
libs/auth/src/middleware/authenticate.ts (1)
libs/auth/src/index.ts (1)
  • isSsoConfigured (4-4)
e2e-tests/tests/sso/sso.spec.ts (1)
e2e-tests/utils/sso-helpers.ts (1)
  • loginWithSSO (10-46)
e2e-tests/tests/account-home.spec.ts (1)
e2e-tests/utils/cft-idam-helpers.ts (1)
  • loginWithCftIdam (10-41)
libs/auth/src/middleware/authenticate.test.ts (1)
libs/auth/src/middleware/authenticate.ts (1)
  • requireAuth (11-31)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: E2E Tests
🔇 Additional comments (13)
e2e-tests/tests/sso/sso.spec.ts (4)

1-23: LGTM! Comprehensive System Admin SSO flow test.

The test correctly validates the System Admin authentication flow, dashboard access permissions, and the expected redirect from account-home to admin-dashboard for SSO users.


65-81: LGTM! Proper validation of edge cases.

The tests correctly validate the SSO rejection flow for users without roles and the redirect behavior for unauthenticated users.


84-107: LGTM! Session lifecycle validation is thorough.

The test properly validates session persistence across navigation, reload, and logout. The regex-based logout link selector (text=/logout|sign out/i) provides flexibility for localization but assumes the link text follows this pattern.


109-138: LGTM! Excellent concurrent session validation.

The test properly validates that multiple concurrent sessions from the same user are handled correctly, with appropriate context isolation and cleanup.

libs/verified-pages/src/pages/account-home/index.ts (1)

1-18: LGTM! Proper authentication gate added to account-home.

The addition of requireAuth() as the first middleware correctly protects the account-home page, ensuring users are authenticated before proceeding to role-based access control via blockUserAccess(). The middleware order is correct:

  1. requireAuth() - Ensures authentication, redirects to /sign-in if unauthenticated
  2. blockUserAccess() - Redirects SSO admin users to their admin dashboard
  3. getHandler - Renders the page for authorized users
libs/auth/src/middleware/authorise.ts (1)

46-48: LGTM! Correct redirect for users without roles.

The change to redirect users with no roles to /sign-in instead of /login is correct. This allows users to potentially select a different authentication method at the account selection page.

libs/auth/src/middleware/authenticate.ts (1)

11-30: LGTM! SSO-aware authentication redirect logic properly implemented.

The middleware now correctly routes unauthenticated users based on SSO configuration and page type:

  • Admin pages with SSO enabled → /login (Azure AD)
  • All other cases → /sign-in (account selection)

The req.session.returnTo is properly preserved to redirect users back to their intended destination after authentication.

Note: There's a related comment on authorise.ts regarding code duplication of this redirect logic.

e2e-tests/tests/account-home.spec.ts (1)

6-34: LGTM! Proper pre-authentication setup for protected page tests.

The beforeEach hook correctly authenticates users before each test, which is necessary since account-home now requires authentication via the requireAuth() middleware. The authentication flow is comprehensive and uses the shared loginWithCftIdam helper for consistency.

Minor note: The first test (lines 31-34) correctly acknowledges that the page is already loaded from beforeEach, but many subsequent tests still call await page.goto("/account-home"). While redundant, this doesn't affect test correctness and may improve test independence.

libs/auth/src/middleware/authenticate.test.ts (1)

1-133: LGTM! Comprehensive test coverage for SSO-aware authentication.

The test suite now properly covers all authentication redirect scenarios:

  • Authenticated users continue without redirect ✓
  • Admin pages with SSO enabled redirect to /login
  • Admin pages without SSO redirect to /sign-in
  • Non-admin pages redirect to /sign-in regardless of SSO configuration ✓

All tests correctly verify that req.session.returnTo is preserved for post-authentication redirection. The mock setup with beforeEach cleanup ensures test isolation.

e2e-tests/tests/cft-idam/cft-idam.spec.ts (4)

108-125: LGTM! Proper error handling validation.

The test correctly validates error scenarios in the CFT IDAM authorization flow, including missing and invalid authorization codes. The verification that protected resources redirect unauthenticated users to sign-in is also appropriate.


166-202: LGTM! Comprehensive session lifecycle validation.

This test excellently validates the complete CFT IDAM session lifecycle from initial unauthenticated state through login, navigation, reload, and logout. The clear test phase comments and consistent use of helper functions make the test easy to understand and maintain.


204-244: LGTM! Proper concurrent session validation for CFT IDAM.

The test correctly validates that multiple concurrent sessions from the same user work independently with CFT IDAM authentication. The test properly isolates contexts, verifies authentication state, and cleans up resources.


248-305: LGTM! Thorough validation of CFT rejected page.

Both tests properly validate the rejected page:

  1. Content display and accessibility compliance
  2. Keyboard navigation functionality

The keyboard navigation test (lines 279-305) is particularly well-implemented, with a robust tab-until-found approach that handles variable page structure.

@github-actions
Copy link
Contributor

github-actions bot commented Nov 20, 2025

🎭 Playwright E2E Test Results

265 tests   265 ✅  17m 3s ⏱️
 16 suites    0 💤
  1 files      0 ❌

Results for commit 9963248.

♻️ This comment has been updated with latest results.

ashwini-mv and others added 2 commits November 20, 2025 17:30
**Problem:**
The SSO-aware redirect logic was duplicated in both authenticate.ts and
authorise.ts middlewares, increasing maintenance burden and risk of
inconsistencies.

**Solution:**
- Created new redirect-helpers.ts with redirectUnauthenticated() function
- Updated both authenticate.ts and authorise.ts to use the helper
- Simplified middleware tests to mock the helper function directly
- Added comprehensive unit tests for the redirect helper

**Changes:**
- **libs/auth/src/middleware/redirect-helpers.ts**: New helper function with
  SSO-aware redirect logic
- **libs/auth/src/middleware/redirect-helpers.test.ts**: 11 comprehensive tests
  covering all redirect scenarios
- **libs/auth/src/middleware/authenticate.ts**: Refactored to use helper
- **libs/auth/src/middleware/authenticate.test.ts**: Simplified tests to mock
  helper
- **libs/auth/src/middleware/authorise.ts**: Refactored to use helper
- **libs/auth/src/middleware/authorise.test.ts**: Simplified tests to mock
  helper

**Benefits:**
- DRY: Single source of truth for redirect logic
- Easier maintenance: Changes only need to be made in one place
- Better testability: Redirect logic can be tested independently
- Consistent behavior: Both middlewares use identical redirect logic

**Test Results:**
✅ All 170 auth unit tests passing (including 11 new redirect helper tests)
✅ All 7 SSO E2E tests passing
✅ All 42 account-home E2E tests passing

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
  Replace permissive assertion that accepts both success and error states
  with explicit IDAM redirect check. The previous test would pass even when
  CFT IDAM was misconfigured, making it impossible to detect failures.

  Changes:
  - Use waitForLoadState('networkidle') instead of arbitrary timeout
  - Assert redirect to IDAM URL instead of accepting error pages
  - Update test name to reflect what is actually being validated
Copy link

@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: 1

🧹 Nitpick comments (2)
e2e-tests/tests/cft-idam/cft-idam.spec.ts (2)

76-76: Use const instead of let for immutable variables.

Both currentUrl declarations are only assigned once in their respective scopes and never reassigned, so they should be declared with const.

Apply this diff:

-    let currentUrl = page.url();
+    const currentUrl = page.url();
     expect(currentUrl).toMatch(/lng=cy|ui_locales=cy/);

     // Test 2: CFT login preserves query parameters in redirect
     await page.goto('/cft-login?lng=cy&test=value');
     await page.waitForLoadState('networkidle');
-    currentUrl = page.url();
+    const currentUrl = page.url();
     expect(currentUrl).toMatch(/lng=cy|ui_locales=cy/);

Also applies to: 82-82


117-117: Use const instead of let for immutable URL variables.

The url variable is only assigned once in each test segment (Test 2 and Test 3) and never reassigned, so it should be declared with const.

Apply this diff:

     // Test 2: Authorization flow handles missing code parameter
     await page.goto('/cft-login/return');
     await expect(page).toHaveURL(/sign-in.*error/);
-    let url = new URL(page.url());
+    const url = new URL(page.url());
     expect(url.searchParams.get('error')).toBeTruthy();

     // Test 3: Authorization flow handles invalid code parameter
     await page.goto('/cft-login/return?code=invalid-code-12345');
     await expect(page).toHaveURL(/sign-in.*error/);
-    url = new URL(page.url());
+    const url = new URL(page.url());
     expect(url.searchParams.get('error')).toBeTruthy();

Also applies to: 123-123

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e2a8b3b and 36659cf.

📒 Files selected for processing (7)
  • e2e-tests/tests/cft-idam/cft-idam.spec.ts (5 hunks)
  • libs/auth/src/middleware/authenticate.test.ts (2 hunks)
  • libs/auth/src/middleware/authenticate.ts (2 hunks)
  • libs/auth/src/middleware/authorise.test.ts (4 hunks)
  • libs/auth/src/middleware/authorise.ts (3 hunks)
  • libs/auth/src/middleware/redirect-helpers.test.ts (1 hunks)
  • libs/auth/src/middleware/redirect-helpers.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • libs/auth/src/middleware/authenticate.ts
🧰 Additional context used
🧬 Code graph analysis (5)
libs/auth/src/middleware/redirect-helpers.test.ts (1)
libs/auth/src/middleware/redirect-helpers.ts (1)
  • redirectUnauthenticated (13-30)
libs/auth/src/middleware/authorise.test.ts (3)
libs/auth/src/middleware/authorise.ts (1)
  • requireRole (12-39)
libs/account/src/roles.ts (1)
  • USER_ROLES (1-5)
libs/auth/src/middleware/redirect-helpers.ts (1)
  • redirectUnauthenticated (13-30)
libs/auth/src/middleware/authenticate.test.ts (2)
libs/auth/src/middleware/redirect-helpers.ts (1)
  • redirectUnauthenticated (13-30)
libs/auth/src/middleware/authenticate.ts (1)
  • requireAuth (11-19)
libs/auth/src/middleware/authorise.ts (1)
libs/auth/src/middleware/redirect-helpers.ts (1)
  • redirectUnauthenticated (13-30)
e2e-tests/tests/cft-idam/cft-idam.spec.ts (1)
e2e-tests/utils/cft-idam-helpers.ts (1)
  • loginWithCftIdam (10-41)
🔇 Additional comments (16)
libs/auth/src/middleware/authorise.test.ts (3)

6-15: LGTM! Clean mock setup.

The mock hoisting and beforeEach hook follow vitest best practices, ensuring proper test isolation.


34-67: LGTM! Tests properly verify the delegated redirect behavior.

Both tests correctly assert that redirectUnauthenticated is called for unauthenticated scenarios and that the middleware chain is not continued.


136-157: LGTM! Redirect target updated correctly.

The test correctly expects /sign-in for users without roles, aligning with the centralized redirect logic.

libs/auth/src/middleware/authorise.ts (2)

4-17: LGTM! Code duplication successfully eliminated.

The refactoring addresses the past review feedback by delegating to the centralized redirectUnauthenticated helper. This improves maintainability and ensures consistent redirect behavior across the codebase.


35-37: LGTM! Fallback redirect correctly updated.

Users without roles are now redirected to /sign-in (account selection), which is consistent with the centralized redirect logic for non-admin pages.

libs/auth/src/middleware/redirect-helpers.ts (1)

1-30: LGTM! Excellent refactoring that centralizes redirect logic.

The redirectUnauthenticated helper successfully consolidates the SSO-aware redirect behavior:

  • Clear JSDoc documentation
  • Proper returnTo preservation
  • Correct admin page detection using startsWith
  • Appropriate SSO configuration check
  • Clean early return pattern

This addresses the code duplication issue noted in past reviews and improves maintainability.

libs/auth/src/middleware/authenticate.test.ts (2)

5-14: LGTM! Proper mock setup.

The mock hoisting and beforeEach hook follow vitest best practices for test isolation.


16-44: LGTM! Tests properly verify the middleware delegation.

Both test cases correctly verify:

  • Authenticated users continue through the middleware chain
  • Unauthenticated users are redirected via redirectUnauthenticated
libs/auth/src/middleware/redirect-helpers.test.ts (3)

16-95: LGTM! Comprehensive test coverage for SSO-enabled scenarios.

The tests thoroughly verify:

  • Admin pages redirect to /login when SSO is configured
  • Non-admin pages redirect to /sign-in regardless of SSO
  • Query parameters are preserved in returnTo
  • Both redirect target and returnTo are correctly set

97-146: LGTM! Proper coverage for SSO-disabled scenarios.

The tests correctly verify that all pages redirect to /sign-in when SSO is not configured, maintaining consistent behavior for the account selection flow.


148-199: LGTM! Excellent edge case coverage.

The tests verify important edge cases:

  • Trailing slashes are handled correctly
  • Hash fragments are preserved in returnTo
  • Substring matches don't incorrectly trigger admin page detection (e.g., /not-admin-dashboard)

This prevents potential bugs from URL variations.

e2e-tests/tests/cft-idam/cft-idam.spec.ts (5)

127-148: LGTM! Well-structured destination preservation test.

This test properly validates the complete flow of accessing a protected resource, being redirected to sign-in, completing authentication, and being redirected back to the original destination. The test structure is clear and assertions are appropriate.


150-157: LGTM! Past review feedback properly addressed.

The test now uses a strict assertion that only accepts successful IDAM redirects, replacing the previously permissive logic that accepted both success and error states. The addition of waitForLoadState('networkidle') provides more explicit synchronization. This directly addresses the past review comment.


160-196: LGTM! Comprehensive session lifecycle test.

This test provides excellent E2E coverage of the complete authentication lifecycle, from initial unauthenticated access through login, session persistence across navigation and reload, to logout. The clear Test 1-5 labels make the test easy to follow and maintain.


198-238: LGTM! Proper concurrent session testing.

This test correctly uses separate browser contexts to validate concurrent session handling, properly cleans up resources by closing contexts, and verifies that both sessions remain independent and valid. The test follows Playwright best practices for multi-context scenarios.


273-299: LGTM! Good accessibility testing.

This test provides valuable keyboard navigation coverage for the CFT rejected page. The loop-and-try approach to finding the focusable link is appropriate given the variable page structure, and the test properly verifies both focus state and Enter-key navigation.

Replace permissive CFT login endpoint assertion that accepts both
success and error states with explicit IDAM redirect check. Replace
arbitrary timeout with targeted wait for navigation completion.

Changes:
- Use waitForLoadState('networkidle') for CFT login endpoint test
- Assert redirect to IDAM URL instead of accepting error pages
- Replace waitForTimeout with waitForURL for query parameter test
- Add assertion for test parameter preservation
- Fix formatting in redirect-helpers.ts for linting compliance

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Copy link

@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: 1

🧹 Nitpick comments (4)
e2e-tests/tests/cft-idam/cft-idam.spec.ts (4)

86-106: Consider moving Welsh rejected page validation to dedicated test.

Test 3 validates Welsh content on the CFT rejected page, which is outside the scope of "Language and query parameters are preserved through CFT IDAM flow." This makes the test harder to maintain and understand.

Consider moving this validation to a new test in the "CFT IDAM Rejected Page" describe block:

test('CFT rejected page displays Welsh content correctly', async ({ page }) => {
  await page.goto('/cft-rejected?lng=cy');
  
  // Verify language toggle shows English
  const languageToggle = page.locator('.language');
  await expect(languageToggle).toContainText('English');
  
  // Check for Welsh content
  const welshHeading = page.locator('h1');
  await expect(welshHeading).toBeVisible();
  
  // Check return link still works
  const signInLink = page.getByRole('link', { name: /yn ôl.*fewngofnodi|return.*sign/i });
  await expect(signInLink).toHaveAttribute('href', '/sign-in');
  
  // Run accessibility checks in Welsh
  const accessibilityScanResults = await new AxeBuilder({ page })
    .withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa', 'wcag22aa'])
    .disableRules(['target-size', 'link-name'])
    .analyze();
  expect(accessibilityScanResults.violations).toEqual([]);
});

This improves test organization and makes it easier to identify failures.


161-197: Consider splitting multi-step lifecycle test for easier debugging.

This test validates 5 distinct behaviors in a single test case: unauthenticated redirect, login, navigation persistence, reload persistence, and logout. While this provides good end-to-end coverage, a failure in any step prevents subsequent steps from running, making it harder to identify multiple issues.

Consider splitting into focused tests while maintaining one comprehensive test:

test('Complete session lifecycle: login, navigation, reload, and logout', async ({ page }) => {
  // Keep existing comprehensive test as-is
  // ...
});

test('Authenticated session persists across navigation', async ({ page }) => {
  // Abbreviated: login, then test navigation only
});

test('Authenticated session persists after page reload', async ({ page }) => {
  // Abbreviated: login, then test reload only
});

test('User can logout successfully', async ({ page }) => {
  // Abbreviated: login, then test logout only
});

This allows independent validation of each behavior while keeping the comprehensive lifecycle test for integration verification.


282-291: Retry loop could be fragile if page structure changes.

The test uses a trial-and-error approach with up to 10 tab presses to find the sign-in link. While this simulates user behavior, it may become flaky if interactive elements are added or reordered.

Consider a more deterministic approach:

// Find all focusable elements
const focusableElements = await page.locator('a, button, input, [tabindex]:not([tabindex="-1"])').all();

// Tab to each and check for sign-in link
for (let i = 0; i < focusableElements.length; i++) {
  await page.keyboard.press('Tab');
  const signInLink = page.getByRole('link', { name: /return to sign in page/i });
  if (await signInLink.evaluate(el => el === document.activeElement)) {
    break;
  }
}

// Verify link is focused
const signInLink = page.getByRole('link', { name: /return to sign in page/i });
await expect(signInLink).toBeFocused();

Alternatively, keep the current implementation but reduce the iteration limit to match expected tab order.


110-113: ---

Consider consolidating redundant protected resource redirect tests.

Lines 110–113 and 162–165 test identical behavior: accessing /account-home without authentication redirects to sign-in. Both execute page.goto('/account-home'), check the same URL redirect, and call assertNotAuthenticated(page). While defense-in-depth testing has value, this specific assertion appears duplicated across two test suites. Consider whether a single authoritative test suffices, or document the intentional overlap if multiple layers are required.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 36659cf and 343d848.

📒 Files selected for processing (2)
  • e2e-tests/tests/cft-idam/cft-idam.spec.ts (5 hunks)
  • libs/auth/src/middleware/redirect-helpers.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • libs/auth/src/middleware/redirect-helpers.ts
🧰 Additional context used
🧬 Code graph analysis (1)
e2e-tests/tests/cft-idam/cft-idam.spec.ts (1)
e2e-tests/utils/cft-idam-helpers.ts (1)
  • loginWithCftIdam (10-41)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: E2E Tests
🔇 Additional comments (2)
e2e-tests/tests/cft-idam/cft-idam.spec.ts (2)

151-157: Past review comment successfully addressed.

The test now uses waitForLoadState('networkidle') and a strict URL assertion that requires a redirect to the CFT IDAM host, eliminating the permissive dual-pass condition from the previous implementation.


199-239: Good concurrent session test implementation.

The test properly uses separate browser contexts to simulate multiple concurrent sessions and validates that both remain authenticated independently. This provides good coverage of session isolation.

ashwini-mv and others added 2 commits November 20, 2025 18:19
The test mock for @hmcts/auth was incomplete, missing the requireAuth
export which caused 15 tests to fail. This was a pre-existing issue
discovered when running unit tests for ticket validation.

Changes:
- Add requireAuth mock that returns middleware calling next()
- Update comment to reflect all three mocked functions

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Replace waitForURL with ambiguous regex that could match the initial
page with waitForLoadState('networkidle') to ensure redirect completes
before assertions run.

The previous regex /idam-web-public\.aat\.platform\.hmcts\.net|cft-login/
could match the initial cft-login page before the redirect happened,
causing the test to proceed prematurely.

Changes:
- Use waitForLoadState('networkidle') instead of waitForURL regex
- Ensures all network activity and redirects complete before assertions
- Prevents race condition where test could pass/fail intermittently

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Copy link

@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: 0

🧹 Nitpick comments (2)
libs/verified-pages/src/pages/account-home/index.test.ts (2)

4-19: LGTM! Mock implementation is correct and consistent.

The requireAuth mock follows the same pattern as blockUserAccess and correctly returns a no-op middleware for testing purposes. This allows the tests to focus on the handler logic without authentication concerns.

Optionally, you could improve type safety by replacing any types with proper Express types:

-  requireAuth: vi.fn(() => (_req: any, _res: any, next: any) => next()),
-  blockUserAccess: vi.fn(() => (_req: any, _res: any, next: any) => next())
+  requireAuth: vi.fn(() => (_req: Request, _res: Response, next: NextFunction) => next()),
+  blockUserAccess: vi.fn(() => (_req: Request, _res: Response, next: NextFunction) => next())

You would also need to import NextFunction from express:

-import type { Request, Response } from "express";
+import type { Request, Response, NextFunction } from "express";

42-250: Consider adding middleware chain verification tests.

The existing tests appropriately focus on handler behavior. However, you could optionally add a test to verify the middleware chain structure and order.

Example test:

it("should have correct middleware chain order", async () => {
  const { GET } = await import("./index.js");
  
  expect(Array.isArray(GET)).toBe(true);
  expect(GET).toHaveLength(3); // requireAuth, blockUserAccess, handler
  
  // Verify middlewares are functions
  expect(typeof GET[0]).toBe("function");
  expect(typeof GET[1]).toBe("function");
  expect(typeof GET[2]).toBe("function");
});

This ensures the middleware chain structure remains correct if refactoring occurs.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 343d848 and ecb6f0b.

📒 Files selected for processing (1)
  • libs/verified-pages/src/pages/account-home/index.test.ts (2 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: SonarQube Analysis
  • GitHub Check: E2E Tests

Copy link

@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: 0

🧹 Nitpick comments (2)
e2e-tests/tests/cft-idam/cft-idam.spec.ts (2)

84-84: Consider using .toContain() for literal string matching.

For checking the presence of the literal string test=value, using .toContain('test=value') is more direct and consistent with the assertion style on line 83 than wrapping it in a regex with .toMatch().

Apply this diff:

-    expect(currentUrl).toMatch(/test=value/);
+    expect(currentUrl).toContain('test=value');

282-291: Consider a more robust approach to keyboard navigation testing.

The loop with try-catch to find the focused element could be fragile if the page structure changes. While testing actual tab navigation is valuable, the current approach:

  • Silently continues on timeout with an empty catch block
  • Assumes the link is within 10 tab stops
  • Makes debugging harder if the test fails

Consider one of these alternatives:

Option 1 (if testing actual tab order is essential):

     // Find and focus on the sign-in link
     let focused = false;
     for (let i = 0; i < 10 && !focused; i++) {
       await page.keyboard.press('Tab');
       const signInLink = page.getByRole('link', { name: /return to sign in page/i });
       try {
         await expect(signInLink).toBeFocused({ timeout: 100 });
         focused = true;
       } catch {
-        // Continue tabbing
+        // Continue tabbing if not focused yet
       }
     }
+    
+    // Ensure we actually found the focused element
+    expect(focused).toBe(true);

Option 2 (if direct focus testing is acceptable):

     // Directly focus the sign-in link and verify keyboard interaction
     const signInLink = page.getByRole('link', { name: /return to sign in page/i });
     await signInLink.focus();
     await expect(signInLink).toBeFocused();
     
     // Press Enter to navigate
     await page.keyboard.press('Enter');
     await expect(page).toHaveURL('/sign-in');
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ecb6f0b and d1d3979.

📒 Files selected for processing (1)
  • e2e-tests/tests/cft-idam/cft-idam.spec.ts (5 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
e2e-tests/tests/cft-idam/cft-idam.spec.ts (1)
e2e-tests/utils/cft-idam-helpers.ts (1)
  • loginWithCftIdam (10-41)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: E2E Tests
🔇 Additional comments (6)
e2e-tests/tests/cft-idam/cft-idam.spec.ts (6)

109-126: LGTM!

The test effectively validates error handling for protected resource access and invalid authorization flows with clear assertions.


128-149: LGTM!

This test properly validates that the authentication flow preserves and redirects users back to their originally requested protected resource.


151-158: LGTM!

The test now correctly uses a strict URL assertion and proper wait condition, addressing previous review feedback.


160-197: LGTM!

The comprehensive session lifecycle test appropriately bundles related scenarios in sequence, validating authentication state from initial access through logout.


199-239: LGTM!

The concurrent sessions test properly validates multi-context authentication and correctly manages browser context lifecycle with cleanup.


243-272: LGTM!

The test thoroughly validates content and accessibility of the CFT rejected page with appropriate accessibility rule exclusions that are documented.

Remove incorrect expectation that arbitrary query parameters are
preserved through CFT IDAM redirect. The implementation only preserves
the language parameter (lng -> ui_locales), which is the correct behavior.

Changes:
- Remove test=value parameter from test URL
- Remove assertion checking for test=value in redirected URL
- Simplify comment to reflect actual behavior
- Update assertion to only check for ui_locales=cy

The test now correctly validates that only the language parameter is
preserved during the IDAM redirect flow.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
@sonarqubecloud
Copy link

Copy link

@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: 0

🧹 Nitpick comments (1)
e2e-tests/tests/cft-idam/cft-idam.spec.ts (1)

79-84: Clarify test intent and fix assertion inconsistency.

Test 2 navigates to /cft-login?lng=cy and checks the URL after network idle, but it doesn't complete the IDAM login flow (unlike the loginWithCftIdam helper which waits for the full redirect). It's unclear what this test is validating:

  1. If testing the redirect URL parameter transformation, the test should explicitly wait for the IDAM redirect: await page.waitForURL(/idam-web-public\.aat\.platform\.hmcts\.net/)
  2. The assertion at line 83 only checks for ui_locales=cy, but Test 1 (line 77) checks for both lng=cy and ui_locales=cy. For consistency, line 83 should also check that the original lng parameter is preserved or transformed.

Consider this refactor to make the test intent clearer:

-    // Test 2: CFT login preserves language parameter in redirect
+    // Test 2: CFT login transforms language parameter for IDAM redirect
     await page.goto('/cft-login?lng=cy');
-    await page.waitForLoadState('networkidle');
+    await page.waitForURL(/idam-web-public\.aat\.platform\.hmcts\.net/);
     currentUrl = page.url();
-    expect(currentUrl).toMatch(/ui_locales=cy/);
+    expect(currentUrl).toMatch(/lng=cy|ui_locales=cy/);
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d1d3979 and 9963248.

📒 Files selected for processing (1)
  • e2e-tests/tests/cft-idam/cft-idam.spec.ts (5 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
e2e-tests/tests/cft-idam/cft-idam.spec.ts (1)
e2e-tests/utils/cft-idam-helpers.ts (1)
  • loginWithCftIdam (10-41)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: SonarQube Analysis
  • GitHub Check: E2E Tests
🔇 Additional comments (7)
e2e-tests/tests/cft-idam/cft-idam.spec.ts (7)

108-125: LGTM!

The test effectively consolidates three authorization error scenarios with clear assertions. The error parameter validation and redirect checks are appropriate.


127-148: LGTM!

Excellent test coverage for redirect preservation after login. This ensures users are returned to their originally requested protected resource, which is critical for user experience.


150-157: LGTM!

The test now correctly asserts a strict redirect to the CFT IDAM host with proper network idle wait, addressing previous review concerns.


160-196: LGTM!

Comprehensive session lifecycle test with excellent coverage of authentication persistence across navigations and page reloads. The clear test structure with numbered comments makes it easy to understand each scenario.


198-238: LGTM!

Well-structured test for concurrent session handling. Properly creates isolated browser contexts, verifies both sessions remain authenticated (confirming the system allows multiple concurrent sessions), and includes proper cleanup.


242-271: LGTM!

Thorough validation of the rejected page content and accessibility. The accessibility scan with documented rule exclusions is appropriate, and the content checks cover all key elements.


273-299: LGTM!

The keyboard navigation test uses a pragmatic approach with a tabbing loop to handle variable element order. The 10-iteration limit is reasonable for this simple page, and the 100ms timeout on focus assertions helps prevent flakiness.

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.

2 participants