Skip to content

VIBE-142 - CaTH B2C Sign In#338

Open
alao-daniel wants to merge 18 commits intomasterfrom
feature/VIBE-142-cath-sign-in-b2c
Open

VIBE-142 - CaTH B2C Sign In#338
alao-daniel wants to merge 18 commits intomasterfrom
feature/VIBE-142-cath-sign-in-b2c

Conversation

@alao-daniel
Copy link
Copy Markdown
Contributor

@alao-daniel alao-daniel commented Feb 5, 2026

Jira link

[VIBE-142] CaTH Sign In - B2C
#229

Change description

Implement CaTH B2C sign in

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

  • New Features

    • Azure B2C sign‑in integration with multiple providers, B2C login/callback/forgot‑password flows, and B2C logout handling
    • Session timeout management with warning modal, countdown and client/server activity tracking
    • Welsh language support for session‑expired and sign‑out pages
    • Password reset via B2C
  • Tests

    • Extensive unit and end‑to‑end tests for sign‑in, sign‑out, session expiry, accessibility and keyboard navigation
  • Chores

    • Updated dependency resolutions for security
  • Documentation

    • Added B2C implementation plan and task docs

github-actions bot and others added 5 commits January 13, 2026 17:19
- Created specification document detailing Azure B2C authentication with three provider options, session management, and timeout handling
- Created implementation plan with phased approach covering B2C integration, OAuth/OIDC flows, Redis session storage, and client-side timeout tracking
- Created detailed task list with clear dependencies on Azure B2C configuration and testing requirements
- Downloaded JIRA attachment for reference

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Feb 5, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds Azure B2C sign-in (non‑prod) with login, callback, forgot‑password and logout handlers; server/client session timeout tracking and UI; B2C configuration utilities; route and helmet integrations; i18n for session-expired; tests, docs and env/helm updates.

Changes

Cohort / File(s) Summary
B2C Config & Exports
libs/auth/src/config/b2c-config.ts, libs/auth/src/config/b2c-config.test.ts, libs/auth/src/index.ts, libs/auth/src/config.ts
Adds B2C configuration helpers and exports (getB2cConfig, isB2cConfigured, getB2cBaseUrl/Authority). Exposes assets path and re-exports new B2C handlers/utilities. Tests added for config.
B2C Pages & Handlers
libs/auth/src/pages/b2c-login/index.ts, libs/auth/src/pages/b2c-login/index.test.ts, libs/auth/src/pages/b2c-callback/index.ts, libs/auth/src/pages/b2c-callback/index.test.ts, libs/auth/src/pages/b2c-forgot-password/index.ts, libs/auth/src/pages/b2c-forgot-password/index.test.ts
Implements B2C login redirect, callback (code exchange, user provisioning, session login) and forgot-password flow with state/nonce and locale handling. Comprehensive unit tests added.
Session Timeout Utilities & Middleware
libs/auth/src/session/timeout-tracker.ts, libs/auth/src/session/timeout-tracker.test.ts, libs/auth/src/middleware/session-timeout.ts, libs/auth/src/middleware/session-timeout.test.ts
Adds timeout utilities (getTimeoutConfig, isSessionExpired/Approaching, updateLastActivity, getTimeUntilExpiry) and Express middleware enforcing inactivity, exposing sessionTimeoutMs. Tests cover behavior and edge cases.
Client Session Timeout Assets
libs/auth/src/assets/js/session-timeout.ts, libs/auth/src/assets/css/session-timeout.scss, libs/auth/package.json, apps/web/src/assets/js/index.ts, apps/web/src/assets/css/index.scss
Adds client-side timeout script and styles, exposes them via package.json exports, and imports them in the web app entry points.
Session Expired Page & I18n
libs/auth/src/pages/session-expired/index.ts, libs/auth/src/pages/session-expired/index.test.ts, libs/auth/src/pages/session-expired/en.ts, libs/auth/src/pages/session-expired/cy.ts, libs/auth/src/pages/session-expired/index.njk, e2e-tests/tests/session-expired.spec.ts
Adds session-expired page, English/Welsh translations, template and tests (unit and e2e accessibility/keyboard/screen‑reader checks).
Logout Flow
libs/auth/src/pages/logout/index.ts, libs/auth/src/pages/logout/index.test.ts
Adds B2C logout path producing B2C logout URL with post-logout redirect; retains existing logout flows and centralises post-logout redirect helper.
App Integration & Routing
apps/web/src/app.ts, apps/web/src/app.test.ts, libs/public-pages/src/pages/sign-in/index.ts, libs/public-pages/src/pages/sign-in/index.test.ts
Registers sessionTimeoutMiddleware and new B2C routes (GET/POST /login/return, GET /b2c-forgot-password); updates sign-in flow to redirect CaTH to /b2c-login with locale. Tests adjusted.
Helmet / CSP Changes
libs/web-core/src/middleware/helmet/helmet-middleware.ts
Adds b2cTenantName and b2cCustomDomain options and includes computed B2C URL in form-action CSP sources.
Env, Helm & CI Test Flags
apps/web/.env.example, apps/web/helm/values.dev.yaml, e2e-tests/playwright.config.ts
Adds ENABLE_B2C, B2C_CUSTOM_DOMAIN, B2C_CUSTOM_DOMAIN_PATH env vars and secret mappings; enables ENABLE_B2C in e2e webServer commands.
E2E Tests
e2e-tests/tests/sign-in.spec.ts, e2e-tests/tests/sign-out.spec.ts
Updates sign-in e2e to assert B2C redirect and Welsh locale preservation; adds sign-out tests verifying session-logged-out panel, accessibility and redirects.
Types & Model Update
libs/auth/src/user-profile.ts, libs/account/src/repository/model.ts, libs/auth/tsconfig.json
Extends session typings (b2cProvider, b2cLocale, lastActivity), adds PI_AAD to User.userProvenance, and excludes assets from auth tsconfig compilation.
Docs & Planning
docs/tickets/229/*
Adds ticket, plan and tasks documenting non‑prod B2C integration, session timeout design, files to add and testing checklist.
Dependency Resolutions
package.json
Updates package resolutions (tar 7.5.7) and adds entries for @isaacs/brace-expansion, lodash, axios.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant Browser
    participant Frontend as PIP Frontend
    participant B2C as Azure B2C
    participant DB as Database
    participant SessionStore as Session Store

    User->>Browser: Visit /sign-in, choose CaTH
    Browser->>Frontend: POST /sign-in (accountType=cath)
    Frontend->>Browser: Redirect to /b2c-login?lng=en
    Browser->>Frontend: GET /b2c-login
    Frontend->>Frontend: generate state/nonce, store session b2cProvider/b2cLocale
    Frontend->>Browser: Redirect to B2C authorize URL
    Browser->>B2C: Authorize request (state, nonce, policy)
    B2C->>Browser: Prompt credentials
    Browser->>B2C: Submit credentials
    B2C->>Browser: Redirect to /login/return?code=...&state=...
    Browser->>Frontend: GET /login/return
    Frontend->>Frontend: validate state, exchange code for tokens
    Frontend->>B2C: POST token endpoint (code, client creds)
    B2C->>Frontend: Return id_token, access_token
    Frontend->>Frontend: extract profile, create/update user
    Frontend->>DB: persist user
    Frontend->>SessionStore: req.login() and set lastActivity
    Frontend->>Browser: Redirect to /account-home?lng=en

    Note over Browser,Frontend: Session Timeout Flow
    Browser->>Frontend: (inactive 25min) requests show sessionTimeoutMs
    Frontend->>Browser: include sessionTimeoutMs in responses
    Browser->>Browser: initSessionTimeout() -> show modal countdown
    User->>Browser: Click "Continue"
    Browser->>Frontend: POST /api/extend-session
    Frontend->>SessionStore: update lastActivity
    Frontend->>Browser: success, modal dismissed
    Browser->>Browser: (if countdown reaches logout) redirect to /session-expired
    Browser->>Frontend: GET /session-expired
    Frontend->>Browser: Render session-expired page
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 3
✅ 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 'VIBE-142 - CaTH B2C Sign In' directly reflects the main change: implementing Azure B2C authentication for CaTH account sign-in, which is the primary feature across the changeset.
Docstring Coverage ✅ Passed Docstring coverage is 81.25% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/VIBE-142-cath-sign-in-b2c

Tip

Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs).
Share your feedback on Discord.


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 and usage tips.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Feb 5, 2026

🎭 Playwright E2E Test Results

260 tests   260 ✅  25m 28s ⏱️
 36 suites    0 💤
  1 files      0 ❌

Results for commit 23f04d5.

♻️ This comment has been updated with latest results.

Copy link
Copy Markdown
Contributor

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

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

🟡 Minor comments (14)
e2e-tests/tests/sign-out.spec.ts-56-65 (1)

56-65: ⚠️ Potential issue | 🟡 Minor

Replace waitForTimeout with waitForURL.

Fixed delays are flaky. Use Playwright's built-in URL assertion which auto-retries.

Proposed fix
       // Visit logout endpoint without being authenticated
       await page.goto("/logout");

       // Should redirect to session-logged-out page
-      await page.waitForTimeout(1000);
-      await expect(page).toHaveURL(/session-logged-out/);
+      await expect(page).toHaveURL(/session-logged-out/);
libs/auth/src/pages/b2c-forgot-password/index.ts-23-28 (1)

23-28: ⚠️ Potential issue | 🟡 Minor

Potential undefined values in URL parameters.

If b2cConfig.clientId is undefined (e.g., misconfiguration that passes isB2cConfigured() check), the URL will contain "undefined". Consider adding defensive validation or ensuring getB2cConfig() returns validated values when isB2cConfigured() is true.

libs/auth/src/assets/js/session-timeout.ts-162-175 (1)

162-175: ⚠️ Potential issue | 🟡 Minor

handleContinue doesn't verify response success.

The .then() callback executes regardless of HTTP status. A 4xx/5xx response would still reset timers rather than triggering logout.

🛠️ Proposed fix
   fetch("/api/extend-session", {
     method: "POST",
     headers: {
       "Content-Type": "application/json"
     }
   })
-    .then(() => {
+    .then((response) => {
+      if (!response.ok) {
+        throw new Error(`HTTP ${response.status}`);
+      }
       resetTimers();
     })
libs/auth/src/pages/b2c-callback/index.test.ts-13-16 (1)

13-16: ⚠️ Potential issue | 🟡 Minor

Remove any type annotations to maintain strict typing in tests.

mockSession: any at line 15 and Record<string, any> at line 390 bypass type safety and can hide shape regressions. The session mock should be typed using express-session types, and the JWT payload should use Record<string, unknown>.

Suggested changes
+import type { Session } from "express-session";
+
-  let mockSession: any;
+  type MockSession = Partial<Session> & {
+    id: string;
+    b2cProvider: string;
+    returnTo: string;
+    b2cLocale: string;
+    save: (callback: (err?: Error | null) => void) => void;
+  };
+  let mockSession: MockSession;

-function createMockIdToken(payload: Record<string, any>): string {
+function createMockIdToken(payload: Record<string, unknown>): string {

Note: Line 229 also uses callback: any which should be typed as (err?: Error | null) => void.

docs/tickets/229/ticket.md-16-35 (1)

16-35: ⚠️ Potential issue | 🟡 Minor

Standardise “sign in”/“sign‑in” wording

Several sentences use “sign into”, “sign in”, and “log in” interchangeably. Please standardise to “sign in to” (verb), “sign‑in” (noun/adjective), and “log in” (verb) for consistency in user‑facing text.

Also applies to: 39-74

docs/tickets/229/tasks.md-82-83 (1)

82-83: ⚠️ Potential issue | 🟡 Minor

Minor wording tweak for test checklist

Consider “requires an authenticated session” for clarity.

Proposed edit
- - [ ] Test session timeout warning modal (requires authenticated session - manual testing)
- - [ ] Test session timeout auto-logout (requires authenticated session - manual testing)
+ - [ ] Test session timeout warning modal (requires an authenticated session - manual testing)
+ - [ ] Test session timeout auto-logout (requires an authenticated session - manual testing)
libs/auth/src/config/b2c-config.test.ts-92-98 (1)

92-98: ⚠️ Potential issue | 🟡 Minor

Make authority‑URL tests deterministic when custom domain env vars are set

getB2cAuthorityUrl relies on getB2cBaseUrl, which prefers B2C_CUSTOM_DOMAIN/B2C_CUSTOM_DOMAIN_PATH. If those are set in the environment, these expectations will fail. Clear or set them explicitly in this describe block.

Suggested fix
   describe("getB2cAuthorityUrl", () => {
     beforeEach(() => {
       process.env.B2C_TENANT_NAME = "test-tenant";
       process.env.B2C_POLICY_HMCTS = "B2C_1A_HMCTS";
       process.env.B2C_POLICY_COMMON_PLATFORM = "B2C_1A_CP";
       process.env.B2C_POLICY_CATH = "B2C_1A_CaTH";
+      delete process.env.B2C_CUSTOM_DOMAIN;
+      delete process.env.B2C_CUSTOM_DOMAIN_PATH;
     });
docs/tickets/229/plan.md-17-21 (1)

17-21: ⚠️ Potential issue | 🟡 Minor

Ensure environment placeholders are clearly non-secret.

Please confirm these are placeholders and not real values. Consider using obvious dummy tokens (e.g., <redacted>), so scanners don’t flag them as potential secrets.

docs/tickets/VIBE-142/plan.md-204-208 (1)

204-208: ⚠️ Potential issue | 🟡 Minor

Use “sign in” as a verb.

Line 207 reads “Redirect to sign-in if not authenticated”; as a verb it should be “sign in”.

Suggested change
-   - Redirect to sign-in if not authenticated
+   - Redirect to sign in if not authenticated
docs/tickets/VIBE-142/specification.md-5-11 (1)

5-11: ⚠️ Potential issue | 🟡 Minor

Use “sign in to” instead of “sign into”.

This is the standard phrasing for authentication.

Suggested change
-These users must sign into CaTH using their verified credentials before gaining access to their accounts.
+These users must sign in to CaTH using their verified credentials before gaining access to their accounts.
@@
-**I want to** sign into CaTH
+**I want to** sign in to CaTH
docs/tickets/229/plan.md-88-92 (1)

88-92: ⚠️ Potential issue | 🟡 Minor

Fix the export name typo.

The snippet shows isBtoCConfigured, but the actual helper is isB2cConfigured.

Suggested change
-export { isBtoCConfigured } from "./config/b2c-config.js";
+export { isB2cConfigured } from "./config/b2c-config.js";
docs/tickets/VIBE-142/plan.md-71-85 (1)

71-85: ⚠️ Potential issue | 🟡 Minor

Add a language identifier to the fenced code block.

This resolves the Markdown lint warning (MD040) and improves readability.

Suggested change
-   ```
+   ```env
    AZURE_B2C_TENANT_NAME=your-tenant
    AZURE_B2C_TENANT_ID=your-tenant-id
    ...
-   ```
+   ```
e2e-tests/tests/sign-in.spec.ts-112-116 (1)

112-116: ⚠️ Potential issue | 🟡 Minor

Replace fixed waits with toHaveURL() to reduce test flakiness.

Lines 113 and 130 use hard waitForTimeout(1000) which is brittle. Playwright's toHaveURL() auto-waits for navigation with a 30-second default timeout, eliminating timing brittleness.

For line 113–115, replace:

await page.waitForTimeout(1000);
const currentUrl = page.url();
expect(currentUrl).toMatch(/pip-frontend\.staging\.platform\.hmcts\.net|b2c-login/);

with:

await expect(page).toHaveURL(/pip-frontend\.staging\.platform\.hmcts\.net|b2c-login/);

For line 129–132, apply the same pattern while preserving the conditional Welsh locale check.

libs/auth/src/pages/b2c-callback/index.ts-1-4 (1)

1-4: ⚠️ Potential issue | 🟡 Minor

Add @hmcts/cloud-native-platform to the runtime dependencies in libs/auth/package.json.

The import on line 2 will fail at runtime since this package is not declared as a dependency. Add "@hmcts/cloud-native-platform": "workspace:*" to align with other workspace packages.

🧹 Nitpick comments (12)
libs/auth/src/assets/css/session-timeout.scss (1)

5-33: Consider using GOV.UK Design System colour variables.

The modal styling is functional and accessible. However, the hardcoded colours (#ffffff, #0b0c0c, rgba(0, 0, 0, 0.5)) could be replaced with GOV.UK Design System colour variables for consistency across the service.

e2e-tests/tests/sign-out.spec.ts (3)

9-19: Consider using semantic selectors per coding guidelines.

The test uses CSS class selectors (.govuk-panel--confirmation). Per coding guidelines, prefer getByRole() first, then getByText(). For a panel, consider getByRole('region') or getByText() for the heading.

Example using semantic selectors
-      const panel = page.locator(".govuk-panel--confirmation");
-      await expect(panel).toBeVisible();
-
-      // Check panel title
-      const panelTitle = panel.locator(".govuk-panel__title");
-      await expect(panelTitle).toContainText(/you have been signed out/i);
+      // Check for signed out heading
+      const heading = page.getByRole("heading", { level: 1 });
+      await expect(heading).toContainText(/you have been signed out/i);

21-29: Consider inlining accessibility checks with the journey test.

As per coding guidelines, E2E tests should use AxeBuilder to test accessibility inline within journey tests, not as separate tests. This could be combined with the panel display test above.


67-83: This test largely duplicates the panel display test.

Lines 71-77 repeat the same panel visibility checks from lines 12-18. Consider consolidating these assertions into a single comprehensive test per the coding guidelines for minimising test count.

libs/auth/src/assets/js/session-timeout.ts (2)

52-62: Static analysis false positive — innerHTML is safe here.

The static analysis flagged this as XSS-vulnerable, but the content is a static string literal with no user input interpolation. This is safe.

However, the hardcoded English text ("You will soon be signed out, due to inactivity") does not support Welsh. Per coding guidelines, every page must support both English and Welsh.

Consider reading locale from document.documentElement.lang and providing translated strings, or injecting the modal HTML server-side via templates.


48-74: Modal lacks accessibility features.

The warning modal should:

  • Have role="dialog" and aria-modal="true"
  • Include aria-labelledby pointing to the heading
  • Trap keyboard focus while visible
  • Return focus to the triggering element when dismissed
♿ Proposed accessibility improvements
   modal.id = "session-timeout-modal";
   modal.className = "session-timeout-modal";
+  modal.setAttribute("role", "dialog");
+  modal.setAttribute("aria-modal", "true");
+  modal.setAttribute("aria-labelledby", "session-timeout-heading");
   modal.style.display = "none";
   modal.innerHTML = `
     <div class="session-timeout-overlay"></div>
     <div class="session-timeout-content govuk-!-padding-4">
-      <h2 class="govuk-heading-m">You will soon be signed out, due to inactivity</h2>
+      <h2 id="session-timeout-heading" class="govuk-heading-m">You will soon be signed out, due to inactivity</h2>
libs/auth/src/middleware/session-timeout.ts (1)

8-12: Lift and rename the public route list to a module-level constant.

This keeps constants top-level and in SCREAMING_SNAKE_CASE, and avoids re-allocating the array per request.

Proposed refactor
 import type { NextFunction, Request, Response } from "express";
 import { getTimeUntilExpiry, isSessionExpired, updateLastActivity } from "../session/timeout-tracker.js";
 
+const PUBLIC_ROUTES = ["/", "/sign-in", "/session-expired", "/logout", "/health"];
+
 /**
  * Middleware to track session inactivity and enforce timeout
  * Checks if session has expired and updates last activity timestamp
  */
 export function sessionTimeoutMiddleware(req: Request, res: Response, next: NextFunction) {
   // Skip timeout tracking for public routes
-  const publicRoutes = ["/", "/sign-in", "/session-expired", "/logout", "/health"];
-  if (publicRoutes.includes(req.path)) {
+  if (PUBLIC_ROUTES.includes(req.path)) {
     next();
     return;
   }

As per coding guidelines: Constants should use SCREAMING_SNAKE_CASE (e.g., MAX_FILE_SIZE, DEFAULT_TIMEOUT). Module ordering: constants outside function scope at top.

libs/auth/src/pages/session-expired/index.test.ts (1)

65-77: Content verification tests are reasonable for translation regression.

These tests verify exact content strings, which serves as a safeguard against accidental translation changes. Consider whether these assertions duplicate the source of truth in en.ts/cy.ts unnecessarily.

e2e-tests/tests/sign-in.spec.ts (1)

118-136: Consider folding the Welsh redirect assertion into the main CaTH journey test.

This adds another micro-test for the same journey. To keep the suite lean, merge the Welsh locale assertion into the CaTH flow test and keep accessibility checks inline there. Based on learnings: Applies to e2e-tests/**/*.spec.ts : E2E tests in Playwright should minimize test count with one test per complete user journey, including validations, Welsh translations, and accessibility checks inline rather than in separate tests.

e2e-tests/tests/session-expired.spec.ts (1)

8-113: Consolidate the micro-tests into a single journey-style test with inline Axe checks.

This file splits the same journey into multiple small tests (content, accessibility, Welsh, keyboard, screen reader). Please merge these into one or two full-journey tests with Axe inline to align with the Playwright e2e guidance. Based on learnings: Applies to e2e-tests/**/*.spec.ts : E2E tests in Playwright should minimize test count with one test per complete user journey, including validations, Welsh translations, and accessibility checks inline rather than in separate tests.

libs/auth/src/pages/b2c-callback/index.ts (1)

24-33: Type the callback parameters and alias snake_case fields.

Using any and error_description here weakens type-safety and breaks camelCase conventions; a small params type and alias keeps strictness without much noise.

♻️ Suggested refactor
+type B2cCallbackParams = {
+  code?: string | string[];
+  state?: string | string[];
+  error?: string | string[];
+  error_description?: string | string[];
+};
+
-async function handleCallback(req: Request, res: Response, params: any) {
-  const { code, state, error, error_description } = params;
+async function handleCallback(req: Request, res: Response, params: B2cCallbackParams) {
+  const { code, state, error, error_description: errorDescription } = params;
-    if (error_description?.includes("AADB2C90118")) {
+    if (errorDescription?.includes("AADB2C90118")) {
       const locale = req.session.b2cLocale || "en";
       return res.redirect(`/b2c-forgot-password?lng=${locale}`);
     }

-    console.error(`B2C authentication error: ${error} - ${error_description}`);
+    console.error(`B2C authentication error: ${error} - ${errorDescription}`);
As per coding guidelines: TypeScript variables should use camelCase (e.g., `userId`, `caseDetails`, `documentId`). Use TypeScript strict mode. Avoid `any` without justification.
libs/auth/src/config/b2c-config.ts (1)

3-18: Move B2cConfig to the bottom to match module ordering.

Keeping interfaces/types after functions matches the agreed module layout and makes exports easier to scan.
As per coding guidelines: Module ordering: constants outside function scope at top, exported functions next, other functions in usage order, interfaces and types at bottom.

Comment on lines +67 to +75
export function isB2cConfigured(): boolean {
// Check if B2C should be disabled for local development
if (process.env.NODE_ENV === "development" && !process.env.ENABLE_B2C) {
return false;
}

const b2cConfig = getB2cConfig();
return !!(b2cConfig.clientId && b2cConfig.clientSecret);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Tighten the config completeness check.

isB2cConfigured only checks clientId/secret; missing tenantName/tenantId yields invalid base URLs and confusing failures. Treat empty values as not configured.

✅ Suggested fix
-  return !!(b2cConfig.clientId && b2cConfig.clientSecret);
+  return !!(
+    b2cConfig.clientId &&
+    b2cConfig.clientSecret &&
+    b2cConfig.tenantName &&
+    b2cConfig.tenantId
+  );
📝 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
export function isB2cConfigured(): boolean {
// Check if B2C should be disabled for local development
if (process.env.NODE_ENV === "development" && !process.env.ENABLE_B2C) {
return false;
}
const b2cConfig = getB2cConfig();
return !!(b2cConfig.clientId && b2cConfig.clientSecret);
}
export function isB2cConfigured(): boolean {
// Check if B2C should be disabled for local development
if (process.env.NODE_ENV === "development" && !process.env.ENABLE_B2C) {
return false;
}
const b2cConfig = getB2cConfig();
return !!(
b2cConfig.clientId &&
b2cConfig.clientSecret &&
b2cConfig.tenantName &&
b2cConfig.tenantId
);
}

Comment on lines +90 to +94
trackException(error as Error, {
area: "B2C callback",
userEmail: userProfile.email,
userId: userProfile.id
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, locate and examine the file in question
cat -n libs/auth/src/pages/b2c-callback/index.ts | head -100

Repository: hmcts/cath-service

Length of output: 4225


🏁 Script executed:

# Find the trackException function definition
rg -n "trackException" --type ts -A 5 -B 2

Repository: hmcts/cath-service

Length of output: 32671


🏁 Script executed:

# Search for correlation ID or tracing ID patterns
rg -n "correlationId|tracingId|requestId|traceId" --type ts

Repository: hmcts/cath-service

Length of output: 44


🏁 Script executed:

# Check telemetry/logger infrastructure
fd -e ts -e js | grep -E "(telemetry|logger|tracking)" | head -20

Repository: hmcts/cath-service

Length of output: 44


Remove user identifiers from telemetry.

userEmail and userId are PII. Since these are not required for diagnosing the database error, remove them and rely on the implicit request context and session information already tracked by Application Insights.

Suggested change
-      trackException(error as Error, {
-        area: "B2C callback",
-        userEmail: userProfile.email,
-        userId: userProfile.id
-      });
+      trackException(error as Error, {
+        area: "B2C callback"
+      });
📝 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
trackException(error as Error, {
area: "B2C callback",
userEmail: userProfile.email,
userId: userProfile.id
});
trackException(error as Error, {
area: "B2C callback"
});

Comment on lines +98 to +104
// Get return URL and locale from session
const returnTo = req.session.returnTo || "/account-home";
const locale = req.session.b2cLocale;

// Build redirect URL with locale if Welsh
const redirectUrl = locale === "cy" ? `${returnTo}?lng=cy` : returnTo;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Find where returnTo is set in the session across the codebase
rg -n "returnTo\s*=" -g '*.ts' --context 3

Repository: hmcts/cath-service

Length of output: 3337


🏁 Script executed:

# Check the full context of the file under review
wc -l libs/auth/src/pages/b2c-callback/index.ts

Repository: hmcts/cath-service

Length of output: 107


🏁 Script executed:

# Read the file to understand the complete context
head -120 libs/auth/src/pages/b2c-callback/index.ts | tail -50

Repository: hmcts/cath-service

Length of output: 1708


🏁 Script executed:

# Search for session middleware and returnTo initialization
rg -n "session\." -g '*.ts' -A 2 | grep -E "(returnTo|b2cLocale)" -B 2 -A 2

Repository: hmcts/cath-service

Length of output: 6481


🏁 Script executed:

# Check the full redirect-helpers.ts file for validation logic
cat -n libs/auth/src/middleware/redirect-helpers.ts

Repository: hmcts/cath-service

Length of output: 1294


🏁 Script executed:

# Check the b2c-callback test file for existing test coverage on returnTo
grep -n "returnTo" libs/auth/src/pages/b2c-callback/index.test.ts -A 3 -B 3

Repository: hmcts/cath-service

Length of output: 822


Sanitise returnTo and fix query string handling.

The code has two issues:

  1. returnTo originates from req.originalUrl (user-controlled) with no validation. This enables open redirect attacks via protocol-relative URLs or path traversal.
  2. The query string append breaks when returnTo already contains ? (e.g., /admin-dashboard?foo=bar), creating a malformed URL.

Validate that returnTo is a relative path, and use conditional ? vs & separator:

Proposed fix
-    const returnTo = req.session.returnTo || "/account-home";
-    const locale = req.session.b2cLocale;
-
-    const redirectUrl = locale === "cy" ? `${returnTo}?lng=cy` : returnTo;
+    const returnTo = req.session.returnTo || "/account-home";
+    const safeReturnTo =
+      returnTo.startsWith("/") && !returnTo.startsWith("//") ? returnTo : "/account-home";
+    const locale = req.session.b2cLocale;
+    const separator = safeReturnTo.includes("?") ? "&" : "?";
+    const redirectUrl = locale === "cy" ? `${safeReturnTo}${separator}lng=cy` : safeReturnTo;
📝 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
// Get return URL and locale from session
const returnTo = req.session.returnTo || "/account-home";
const locale = req.session.b2cLocale;
// Build redirect URL with locale if Welsh
const redirectUrl = locale === "cy" ? `${returnTo}?lng=cy` : returnTo;
// Get return URL and locale from session
const returnTo = req.session.returnTo || "/account-home";
const safeReturnTo =
returnTo.startsWith("/") && !returnTo.startsWith("//") ? returnTo : "/account-home";
const locale = req.session.b2cLocale;
const separator = safeReturnTo.includes("?") ? "&" : "?";
const redirectUrl = locale === "cy" ? `${safeReturnTo}${separator}lng=cy` : safeReturnTo;

Comment on lines +173 to +179
const response = await fetch(tokenUrl, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded"
},
body: params.toString()
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n libs/auth/src/pages/b2c-callback/index.ts | head -200 | tail -50

Repository: hmcts/cath-service

Length of output: 1779


🏁 Script executed:

cat -n libs/auth/src/pages/b2c-callback/index.ts | head -160 | tail -40

Repository: hmcts/cath-service

Length of output: 1441


🏁 Script executed:

# Check for any global fetch timeout or request timeout handling
rg -t ts "AbortController|setTimeout.*fetch|timeout.*fetch" libs/auth/src/

# Check for any Express middleware that sets request timeout
rg -t ts "req\.setTimeout|requestTimeout|timeout:" libs/auth/src/ | head -20

Repository: hmcts/cath-service

Length of output: 44


🏁 Script executed:

# Check the parent POST handler signature and first lines of file for imports
cat -n libs/auth/src/pages/b2c-callback/index.ts | head -130

Repository: hmcts/cath-service

Length of output: 5372


Add timeout protection to the B2C token exchange fetch call.

The fetch() call lacks a timeout, allowing hung B2C endpoints to indefinitely tie up request threads. Implement AbortController with a 10-second timeout and clear the timer in a finally block to ensure reliable timeout handling.

Proposed fix
-  const response = await fetch(tokenUrl, {
-    method: "POST",
-    headers: {
-      "Content-Type": "application/x-www-form-urlencoded"
-    },
-    body: params.toString()
-  });
+  const controller = new AbortController();
+  const timeoutId = setTimeout(() => controller.abort(), 10000);
+  let response: Response;
+  try {
+    response = await fetch(tokenUrl, {
+      method: "POST",
+      headers: {
+        "Content-Type": "application/x-www-form-urlencoded"
+      },
+      body: params.toString(),
+      signal: controller.signal
+    });
+  } finally {
+    clearTimeout(timeoutId);
+  }

Comment on lines +193 to +208
function extractUserProfile(idToken: string, _provider: string): UserProfile {
// Decode JWT (basic implementation - in production use a JWT library)
const parts = idToken.split(".");
if (parts.length !== 3) {
throw new Error("Invalid ID token format");
}

const payload = JSON.parse(Buffer.from(parts[1], "base64").toString("utf-8"));

return {
id: payload.sub || payload.oid,
email: payload.email || payload.emails?.[0] || payload.signInNames?.emailAddress,
displayName: payload.name || payload.given_name || payload.family_name || "Unknown",
role: "VERIFIED",
provenance: "B2C"
};
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# First, let's read the complete file to understand the full context
wc -l libs/auth/src/pages/b2c-callback/index.ts

Repository: hmcts/cath-service

Length of output: 107


🏁 Script executed:

# Read the entire file to see the complete implementation
cat -n libs/auth/src/pages/b2c-callback/index.ts

Repository: hmcts/cath-service

Length of output: 8166


🏁 Script executed:

# Search for where extractUserProfile is called
rg "extractUserProfile" --type ts

Repository: hmcts/cath-service

Length of output: 295


🏁 Script executed:

# Search for JWT verification in the codebase
rg "jwtVerify|verifyToken|jwt.verify|jose" --type ts

Repository: hmcts/cath-service

Length of output: 123


🏁 Script executed:

# Check what JWT/auth libraries are available
find . -name "package.json" -type f | head -5 | xargs grep -l "jose\|jsonwebtoken\|jwt"

Repository: hmcts/cath-service

Length of output: 46


🏁 Script executed:

# Check if there's middleware applied to this endpoint
rg "b2c-callback|oauth" --type ts libs/auth/src | grep -i "middleware\|route\|app\."

Repository: hmcts/cath-service

Length of output: 44


🏁 Script executed:

# Check package.json files for JWT libraries
cat libs/auth/package.json | grep -A 5 -B 5 "jose\|jsonwebtoken\|jwt" || echo "Checking root package.json..."

Repository: hmcts/cath-service

Length of output: 91


🏁 Script executed:

# Check root package.json
cat package.json | grep -A 5 -B 5 "jose\|jsonwebtoken\|jwt" 2>/dev/null || echo "No match in root"

Repository: hmcts/cath-service

Length of output: 716


🏁 Script executed:

# Search for any other JWT verification in the auth library
rg "jwt\.|verify\(" libs/auth/src --type ts -A 2

Repository: hmcts/cath-service

Length of output: 44


🏁 Script executed:

# Check the oauth-middleware to understand the context
head -50 libs/api/src/middleware/oauth-middleware.ts

Repository: hmcts/cath-service

Length of output: 1569


Verify ID token and validate required claims.

The code decodes the JWT without verifying the signature, issuer, audience, or expiry. Although the token is obtained directly from Azure B2C via HTTPS, claims should still be validated before use. The function also silently allows missing required claims (sub, email) which would result in undefined user identifiers.

Use jsonwebtoken with jwks-rsa (already in dependencies) to verify the token against B2C's public key set and validate required claims exist before provisioning the user profile. Alternatively, add jose as a dependency and configure token verification with issuer, audience, and claims checks.

Comment on lines +19 to +30
// Build authorization URL with policy as query param
const b2cBaseUrl = getB2cBaseUrl();
const authUrl = new URL(`${b2cBaseUrl}/oauth2/v2.0/authorize`);
authUrl.searchParams.set("p", b2cConfig.policyCath);
authUrl.searchParams.set("client_id", b2cConfig.clientId);
authUrl.searchParams.set("redirect_uri", b2cConfig.redirectUri);
authUrl.searchParams.set("response_type", b2cConfig.responseType);
authUrl.searchParams.set("response_mode", b2cConfig.responseMode);
authUrl.searchParams.set("scope", b2cConfig.scope.join(" "));
authUrl.searchParams.set("ui_locales", uiLocale);
authUrl.searchParams.set("state", generateState(req));
authUrl.searchParams.set("nonce", generateNonce());
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Persist and validate the OAuth state value.

The state value is generated and sent but not stored for callback validation. Please persist it in the session and verify/clear it in the callback handler to ensure CSRF protection.

Suggested change
-  authUrl.searchParams.set("state", generateState(req));
+  const state = generateState(req);
+  req.session.b2cState = state;
+  authUrl.searchParams.set("state", state);
📝 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
// Build authorization URL with policy as query param
const b2cBaseUrl = getB2cBaseUrl();
const authUrl = new URL(`${b2cBaseUrl}/oauth2/v2.0/authorize`);
authUrl.searchParams.set("p", b2cConfig.policyCath);
authUrl.searchParams.set("client_id", b2cConfig.clientId);
authUrl.searchParams.set("redirect_uri", b2cConfig.redirectUri);
authUrl.searchParams.set("response_type", b2cConfig.responseType);
authUrl.searchParams.set("response_mode", b2cConfig.responseMode);
authUrl.searchParams.set("scope", b2cConfig.scope.join(" "));
authUrl.searchParams.set("ui_locales", uiLocale);
authUrl.searchParams.set("state", generateState(req));
authUrl.searchParams.set("nonce", generateNonce());
// Build authorization URL with policy as query param
const b2cBaseUrl = getB2cBaseUrl();
const authUrl = new URL(`${b2cBaseUrl}/oauth2/v2.0/authorize`);
authUrl.searchParams.set("p", b2cConfig.policyCath);
authUrl.searchParams.set("client_id", b2cConfig.clientId);
authUrl.searchParams.set("redirect_uri", b2cConfig.redirectUri);
authUrl.searchParams.set("response_type", b2cConfig.responseType);
authUrl.searchParams.set("response_mode", b2cConfig.responseMode);
authUrl.searchParams.set("scope", b2cConfig.scope.join(" "));
authUrl.searchParams.set("ui_locales", uiLocale);
const state = generateState(req);
req.session.b2cState = state;
authUrl.searchParams.set("state", state);
authUrl.searchParams.set("nonce", generateNonce());

Comment on lines +9 to +13
export function getTimeoutConfig() {
return {
warningMs: Number.parseInt(process.env.SESSION_TIMEOUT_WARNING_MS || String(DEFAULT_WARNING_MS), 10),
logoutMs: Number.parseInt(process.env.SESSION_TIMEOUT_LOGOUT_MS || String(DEFAULT_LOGOUT_MS), 10)
};
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Guard against NaN env values to avoid disabling timeouts.

If the env values are empty or non-numeric, parseInt returns NaN, and expiry checks can silently stop working. Please fallback to defaults when parsing fails.

Proposed fix
 export function getTimeoutConfig() {
-  return {
-    warningMs: Number.parseInt(process.env.SESSION_TIMEOUT_WARNING_MS || String(DEFAULT_WARNING_MS), 10),
-    logoutMs: Number.parseInt(process.env.SESSION_TIMEOUT_LOGOUT_MS || String(DEFAULT_LOGOUT_MS), 10)
-  };
+  const warningEnv = Number(process.env.SESSION_TIMEOUT_WARNING_MS);
+  const logoutEnv = Number(process.env.SESSION_TIMEOUT_LOGOUT_MS);
+  return {
+    warningMs: Number.isFinite(warningEnv) ? warningEnv : DEFAULT_WARNING_MS,
+    logoutMs: Number.isFinite(logoutEnv) ? logoutEnv : DEFAULT_LOGOUT_MS
+  };
 }

Copy link
Copy Markdown
Contributor

@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 (1)
libs/auth/src/pages/b2c-forgot-password/index.ts (1)

8-35: Page controller doesn’t render bilingual content

This handler only redirects and never renders an en/cy content object. If it must live under libs/*/src/pages, consider rendering a minimal bilingual view (or relocate this to a non-page handler) to keep page conventions consistent. As per coding guidelines, “libs//src/pages/**/.ts: Page controllers should export GET and/or POST functions with type annotations (req: Request, res: Response) and render views with content objects. Every page must support both English and Welsh by providing en and cy content objects to the renderer.”

Comment on lines +40 to +46
function generateState(req: Request): string {
const sessionId = req.session.id || "unknown";
const timestamp = Date.now();
const random = randomBytes(16).toString("hex");

return Buffer.from(`${sessionId}:${timestamp}:${random}`).toString("base64");
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's locate and examine the file
find . -type f -name "index.ts" -path "*/b2c-forgot-password/*"

Repository: hmcts/cath-service

Length of output: 112


🏁 Script executed:

# Get the full file content to understand context
cat -n libs/auth/src/pages/b2c-forgot-password/index.ts | head -100

Repository: hmcts/cath-service

Length of output: 2444


🏁 Script executed:

# Search for state validation in the codebase
rg "b2cState|state.*validat|validat.*state" -A 3 -B 3 libs/auth/

Repository: hmcts/cath-service

Length of output: 44


🏁 Script executed:

# Check for callback handlers related to this OAuth flow
rg "b2c.*callback|oauth.*callback" -i libs/auth/ -A 5

Repository: hmcts/cath-service

Length of output: 5906


🏁 Script executed:

# Examine the b2c-callback handler to see state validation
cat -n libs/auth/src/pages/b2c-callback/index.ts | head -80

Repository: hmcts/cath-service

Length of output: 3473


🏁 Script executed:

# Look for the state validation logic in detail
rg "state.*==|state.*validat|validateState" libs/auth/src/pages/b2c-callback/ -A 5 -B 2

Repository: hmcts/cath-service

Length of output: 640


🏁 Script executed:

# Check the test file to understand expected state format
cat -n libs/auth/src/pages/b2c-callback/index.test.ts | head -150

Repository: hmcts/cath-service

Length of output: 5925


Store state token server-side instead of embedding session ID in OAuth state parameter

The session ID is exposed in the redirect URL and the state parameter lacks server-side validation storage. This leaks session identifiers and deviates from OAuth2 best practices. Replace with a random token persisted in the session for callback validation.

Suggested fix
 function generateState(req: Request): string {
-  const sessionId = req.session.id || "unknown";
-  const timestamp = Date.now();
-  const random = randomBytes(16).toString("hex");
-
-  return Buffer.from(`${sessionId}:${timestamp}:${random}`).toString("base64");
+  const state = randomBytes(32).toString("base64url");
+  req.session.b2cState = state;
+  return state;
 }
📝 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
function generateState(req: Request): string {
const sessionId = req.session.id || "unknown";
const timestamp = Date.now();
const random = randomBytes(16).toString("hex");
return Buffer.from(`${sessionId}:${timestamp}:${random}`).toString("base64");
}
function generateState(req: Request): string {
const state = randomBytes(32).toString("base64url");
req.session.b2cState = state;
return state;
}

@@ -0,0 +1,151 @@
# #229: [VIBE-142] CaTH Sign In - B2C
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

remove VIBE-142 folder. Its a duplicate of folder 229

const b2cConfig = getB2cConfig();
let policyName: string;

switch (policy) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

this switch statement is bit strange. We do not use b2c config for policyHmcts and policyCommonPlatform. hmcts is CFT IDAM and already working.

try {
await createOrUpdateUser({
email: userProfile.email,
userProvenance: "B2C_IDAM",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

There will be no B2C_IDAM, it will be PI_AAD

// Get policy based on provider
let policy: string;
switch (provider) {
case "hmcts":
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

same as I mentioned above, this switch is not looking correct to me, we do not have b2c config for hmcts and common platform

Copy link
Copy Markdown
Contributor

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

🧹 Nitpick comments (6)
libs/account/src/repository/model.ts (1)

1-8: Consider extracting duplicated union types into shared type aliases.

The role union ("VERIFIED" | "LOCAL_ADMIN" | …) is duplicated on lines 7 and 11. Similarly, userProvenance could benefit from a named type. This is pre-existing, so just a nudge for a future tidy-up.

♻️ Example extraction
+export type UserProvenance = "SSO" | "CFT_IDAM" | "CRIME_IDAM" | "B2C_IDAM" | "PI_AAD";
+export type UserRole = "VERIFIED" | "LOCAL_ADMIN" | "CTSC_ADMIN" | "SYSTEM_ADMIN";
+
 export interface User {
   email: string;
   firstName?: string;
   surname?: string;
-  userProvenance: "SSO" | "CFT_IDAM" | "CRIME_IDAM" | "B2C_IDAM" | "PI_AAD";
+  userProvenance: UserProvenance;
   userProvenanceId: string;
-  role: "VERIFIED" | "LOCAL_ADMIN" | "CTSC_ADMIN" | "SYSTEM_ADMIN";
+  role: UserRole;
 }
 
 export interface UpdateUserInput {
-  role?: "VERIFIED" | "LOCAL_ADMIN" | "CTSC_ADMIN" | "SYSTEM_ADMIN";
+  role?: UserRole;
   lastSignedInDate?: Date;
 }
libs/auth/src/config/b2c-config.ts (2)

22-33: getConfigValue treats empty string env vars as unset.

if (envValue) is falsy for "", so an explicitly set empty env var (e.g. B2C_CUSTOM_DOMAIN="") falls through to config.get. This is probably fine for this use-case but worth being aware of — if deliberate clearing via env is ever needed, this won't work.


1-16: Interface placement and inline defaults.

Per coding guidelines, interfaces and types should be declared at the bottom of the module. Also, the default policy strings ("B2C_1_SignInUserFlow", "B2C_1A_PASSWORD_RESET") could be extracted as SCREAMING_SNAKE_CASE constants at the top for clarity, per guidelines.

libs/auth/src/config/b2c-config.test.ts (1)

86-95: No test coverage for getB2cBaseUrl.

getB2cBaseUrl has two branches (custom domain vs. default tenant URL) but neither is tested. Consider adding tests for both paths.

libs/auth/src/pages/b2c-callback/index.ts (2)

24-24: params: any bypasses strict typing.

The handleCallback parameter is untyped. Consider using Record<string, unknown> or a narrower interface to satisfy TypeScript strict mode and the project's any avoidance guideline.

Proposed fix
-async function handleCallback(req: Request, res: Response, params: any) {
+async function handleCallback(req: Request, res: Response, params: Record<string, unknown>) {

As per coding guidelines: "Avoid any without justification."


143-177: exchangeCodeForTokens return type is unvalidated.

response.json() returns any. The caller then accesses .id_token without confirming the property exists. A missing or malformed response from B2C would surface as an opaque error in extractUserProfile rather than a clear token-exchange failure.

Consider adding a runtime check on the response shape.

Comment on lines +57 to +63
it("should return false when B2C is not configured", () => {
delete process.env.B2C_TENANT_NAME;
delete process.env.B2C_CLIENT_ID;
delete process.env.B2C_CLIENT_SECRET;

expect(isB2cConfigured()).toBe(false);
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Test may pass for the wrong reason when NODE_ENV is "development".

If the test runner sets NODE_ENV=development (common with vitest), isB2cConfigured() returns false due to the dev-mode guard — not because of missing credentials. Set NODE_ENV explicitly to avoid a false positive.

Proposed fix
     it("should return false when B2C is not configured", () => {
+      process.env.NODE_ENV = "production";
       delete process.env.B2C_TENANT_NAME;
       delete process.env.B2C_CLIENT_ID;
       delete process.env.B2C_CLIENT_SECRET;
📝 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
it("should return false when B2C is not configured", () => {
delete process.env.B2C_TENANT_NAME;
delete process.env.B2C_CLIENT_ID;
delete process.env.B2C_CLIENT_SECRET;
expect(isB2cConfigured()).toBe(false);
});
it("should return false when B2C is not configured", () => {
process.env.NODE_ENV = "production";
delete process.env.B2C_TENANT_NAME;
delete process.env.B2C_CLIENT_ID;
delete process.env.B2C_CLIENT_SECRET;
expect(isB2cConfigured()).toBe(false);
});

Comment on lines +77 to +85
export function getB2cBaseUrl(): string {
const b2cConfig = getB2cConfig();

if (b2cConfig.customDomain && b2cConfig.customDomainPath) {
return `https://${b2cConfig.customDomain}/${b2cConfig.customDomainPath}`;
}

return `https://${b2cConfig.tenantName}.b2clogin.com/${b2cConfig.tenantName}.onmicrosoft.com`;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

getB2cBaseUrl silently produces invalid URLs when tenantName is empty.

When neither custom domain nor tenantName is configured, this returns https://.b2clogin.com/.onmicrosoft.com. Consider guarding or throwing to surface the misconfiguration early.

Proposed fix
+  if (!b2cConfig.tenantName) {
+    throw new Error("B2C tenantName is required but not configured");
+  }
+
   return `https://${b2cConfig.tenantName}.b2clogin.com/${b2cConfig.tenantName}.onmicrosoft.com`;
📝 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
export function getB2cBaseUrl(): string {
const b2cConfig = getB2cConfig();
if (b2cConfig.customDomain && b2cConfig.customDomainPath) {
return `https://${b2cConfig.customDomain}/${b2cConfig.customDomainPath}`;
}
return `https://${b2cConfig.tenantName}.b2clogin.com/${b2cConfig.tenantName}.onmicrosoft.com`;
}
export function getB2cBaseUrl(): string {
const b2cConfig = getB2cConfig();
if (b2cConfig.customDomain && b2cConfig.customDomainPath) {
return `https://${b2cConfig.customDomain}/${b2cConfig.customDomainPath}`;
}
if (!b2cConfig.tenantName) {
throw new Error("B2C tenantName is required but not configured");
}
return `https://${b2cConfig.tenantName}.b2clogin.com/${b2cConfig.tenantName}.onmicrosoft.com`;
}

Comment on lines +58 to +66
try {
// Verify state matches session
const stateData = Buffer.from(state, "base64").toString("utf-8");
const [sessionId] = stateData.split(":");

if (sessionId !== req.session.id) {
console.error("B2C callback state mismatch");
return res.status(403).send("Invalid request: state mismatch");
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

State decoding lacks error handling for malformed input.

If state is not valid base64 or the decoded string doesn't contain :, Buffer.from may produce garbage and the split won't yield a meaningful sessionId. Wrap in try/catch to return a clear 403.

Proposed fix
   try {
-    // Verify state matches session
-    const stateData = Buffer.from(state, "base64").toString("utf-8");
-    const [sessionId] = stateData.split(":");
+    let sessionId: string;
+    try {
+      const stateData = Buffer.from(state, "base64url").toString("utf-8");
+      [sessionId] = stateData.split(":");
+    } catch {
+      console.error("B2C callback: failed to decode state parameter");
+      return res.status(403).send("Invalid request: malformed state");
+    }
📝 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
try {
// Verify state matches session
const stateData = Buffer.from(state, "base64").toString("utf-8");
const [sessionId] = stateData.split(":");
if (sessionId !== req.session.id) {
console.error("B2C callback state mismatch");
return res.status(403).send("Invalid request: state mismatch");
}
try {
let sessionId: string;
try {
const stateData = Buffer.from(state, "base64url").toString("utf-8");
[sessionId] = stateData.split(":");
} catch {
console.error("B2C callback: failed to decode state parameter");
return res.status(403).send("Invalid request: malformed state");
}
if (sessionId !== req.session.id) {
console.error("B2C callback state mismatch");
return res.status(403).send("Invalid request: state mismatch");
}

alao-daniel and others added 8 commits February 11, 2026 16:32
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Resolved conflict in apps/web/src/app.ts by keeping both:
- B2C auth imports from this branch (b2cCallbackHandler, sessionTimeoutMiddleware, etc.)
- Administrative court module import from master

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

Co-Authored-By: Claude <noreply@anthropic.com>
@ChrisS1512 ChrisS1512 linked an issue Feb 18, 2026 that may be closed by this pull request
@sonarqubecloud
Copy link
Copy Markdown

sonarqubecloud bot commented Mar 5, 2026

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.

[VIBE-142] CaTH Sign In - B2C

3 participants