Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
602000c
Add technical planning for VIBE-142: CaTH Sign In - B2C
github-actions[bot] Jan 13, 2026
e409376
Merge remote-tracking branch 'origin/master' into feature/VIBE-142-ca…
alao-daniel Jan 29, 2026
661544c
Merge remote-tracking branch 'origin/master' into feature/VIBE-142-ca…
alao-daniel Jan 29, 2026
ec381e1
Merge branch 'master' of https://github.com/hmcts/cath-service into f…
alao-daniel Feb 2, 2026
b5aac04
implement b2c cath sign in
alao-daniel Feb 5, 2026
d99d33f
fix tests
alao-daniel Feb 5, 2026
35c632c
fix build
alao-daniel Feb 5, 2026
4e3c695
remove duplicate VIBE-142 folder
alao-daniel Feb 11, 2026
725ca0a
update B2C flow for CaTH sign in only
alao-daniel Feb 11, 2026
6165a12
fix build
alao-daniel Feb 11, 2026
9032d0e
Merge master into feature/VIBE-142-cath-sign-in-b2c
alao-daniel Feb 13, 2026
7a591f8
fix: update qs to 6.14.2 to resolve GHSA-w7fw-mjwx-w883
alao-daniel Feb 13, 2026
2d80bf7
Merge branch 'master' into feature/VIBE-142-cath-sign-in-b2c
junaidiqbalmoj Feb 16, 2026
1872272
fix b2c subscription and password functionality
alao-daniel Feb 17, 2026
87d6704
update session-expired template
alao-daniel Feb 17, 2026
4a36573
add locale to session timeout
alao-daniel Feb 17, 2026
bcf8d16
fix build
alao-daniel Feb 17, 2026
23f04d5
remove back link from sign out page
alao-daniel Mar 5, 2026
601b14e
Merge branch 'master' into feature/VIBE-142-cath-sign-in-b2c
alao-daniel Apr 14, 2026
693b516
fix sonarqube false positive issue
alao-daniel Apr 14, 2026
0eba3a1
Merge branch 'master' into feature/VIBE-142-cath-sign-in-b2c
alao-daniel Apr 15, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion apps/web/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,20 @@ REDIS_URL=redis://localhost:6380
# - SSO_SYSTEM_ADMIN_GROUP_ID
# - SSO_INTERNAL_ADMIN_CTSC_GROUP_ID
# - SSO_INTERNAL_ADMIN_LOCAL_GROUP_ID

#
# Allow HTTP redirect for SSO (false for HTTPS, true only if using HTTP)
SSO_ALLOW_HTTP_REDIRECT=false

# Azure B2C Configuration (for CaTH verified users - using PIP staging custom domain)
# B2C secrets are loaded from Azure Key Vault automatically:
# - B2C_CLIENT_ID
# - B2C_CLIENT_SECRET
#
# Non-secret config must be set here for local development
ENABLE_B2C=true
B2C_CUSTOM_DOMAIN=
B2C_CUSTOM_DOMAIN_PATH=

# Application Insights (Optional)
APPLICATION_INSIGHTS_CONNECTION_STRING=InstrumentationKey=your-instrumentation-key;IngestionEndpoint=https://dc.services.visualstudio.com/

Expand Down
8 changes: 8 additions & 0 deletions apps/web/helm/values.dev.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ nodejs:
BASE_URL: 'https://{{ .Values.nodejs.ingressHost }}'
SSO_ALLOW_HTTP_REDIRECT: 'false'
CFT_IDAM_URL: 'https://idam-web-public.aat.platform.hmcts.net'
ENABLE_B2C: 'true'
B2C_CUSTOM_DOMAIN: 'sign-in.pip-frontend.staging.platform.hmcts.net'
B2C_CUSTOM_DOMAIN_PATH: 'pip-frontend.staging.platform.hmcts.net'
keyVaults:
# Local development uses pip-ss-kv-stg with -dev suffixed secrets
pip-ss-kv-stg:
Expand All @@ -33,3 +36,8 @@ nodejs:
# CFT IDAM secret for local development
- name: cft-idam-client-secret
alias: CFT_IDAM_CLIENT_SECRET
# B2C secrets for verified user sign-in (using PIP staging frontend app registration)
- name: auto-pip-stg-pip-frontend-stg-id
alias: B2C_CLIENT_ID
- name: auto-pip-stg-pip-frontend-stg-pwd
alias: B2C_CLIENT_SECRET
10 changes: 7 additions & 3 deletions apps/web/src/app.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,14 @@ vi.mock("config", () => ({
}));

vi.mock("@hmcts/auth", () => ({
configurePassport: vi.fn(),
authNavigationMiddleware: vi.fn(() => vi.fn()),
ssoCallbackHandler: vi.fn(),
cftCallbackHandler: vi.fn()
b2cCallbackHandler: vi.fn(),
b2cCallbackPostHandler: vi.fn(),
b2cForgotPasswordHandler: vi.fn(),
cftCallbackHandler: vi.fn(),
configurePassport: vi.fn(),
sessionTimeoutMiddleware: vi.fn((_req: any, _res: any, next: any) => next()),
ssoCallbackHandler: vi.fn()
}));

vi.mock("@hmcts/admin-pages/config", () => ({
Expand Down
26 changes: 24 additions & 2 deletions apps/web/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,16 @@ import { fileURLToPath } from "node:url";
import "@hmcts/web-core"; // Import for Express type augmentation
import { fileUploadRoutes as adminFileUploadRoutes, moduleRoot as adminModuleRoot, pageRoutes as adminRoutes } from "@hmcts/admin-pages/config";
import { moduleRoot as adminCourtModuleRoot, pageRoutes as adminCourtRoutes } from "@hmcts/administrative-court-daily-cause-list/config";
import { authNavigationMiddleware, cftCallbackHandler, configurePassport, ssoCallbackHandler } from "@hmcts/auth";
import {
authNavigationMiddleware,
b2cCallbackHandler,
b2cCallbackPostHandler,
b2cForgotPasswordHandler,
cftCallbackHandler,
configurePassport,
sessionTimeoutMiddleware,
ssoCallbackHandler
} from "@hmcts/auth";
import { moduleRoot as authModuleRoot, pageRoutes as authRoutes } from "@hmcts/auth/config";
import {
moduleRoot as careStandardsTribunalModuleRoot,
Expand Down Expand Up @@ -66,7 +75,9 @@ export async function createApp(): Promise<Express> {
app.use(configureNonce());
app.use(
configureHelmet({
cftIdamUrl: process.env.CFT_IDAM_URL
cftIdamUrl: process.env.CFT_IDAM_URL,
b2cCustomDomain: process.env.B2C_CUSTOM_DOMAIN,
b2cTenantName: process.env.B2C_TENANT_NAME
})
);
app.use(expressSessionRedis({ redisConnection: await getRedisClient() }));
Expand Down Expand Up @@ -114,12 +125,23 @@ export async function createApp(): Promise<Express> {
// Add authentication state to navigation (AFTER all other middleware is set up)
app.use(authNavigationMiddleware());

// Session timeout tracking for authenticated users
app.use(sessionTimeoutMiddleware);

// Manual route registration for SSO callback (maintains /sso/return URL for external SSO config)
app.get("/sso/return", ssoCallbackHandler);

// Manual route registration for CFT callback (maintains /cft-login/return URL for external CFT IDAM config)
app.get("/cft-login/return", cftCallbackHandler);

// Manual route registration for B2C callback (maintains /login/return URL for Azure B2C config)
// Supports both GET (response_mode=query) and POST (response_mode=form_post)
app.get("/login/return", b2cCallbackHandler);
app.post("/login/return", b2cCallbackPostHandler);

// Manual route registration for B2C password reset
app.get("/b2c-forgot-password", b2cForgotPasswordHandler);

// Register location autocomplete routes (no prefix - frontend expects /locations)
app.use(await createSimpleRouter(locationApiRoutes));

Expand Down
1 change: 1 addition & 0 deletions apps/web/src/assets/css/index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
@use "@hmcts/admin-pages/src/assets/css/index.scss" as admin;
@use "@hmcts/system-admin-pages/src/assets/css/dashboard.scss";
@use "@hmcts/verified-pages/src/assets/css/verified-pages.scss" as verified;
@use "@hmcts/auth/src/assets/css/session-timeout.scss";

.govuk-service-navigation__navigation-end {
margin-left: auto !important;
Expand Down
1 change: 1 addition & 0 deletions apps/web/src/assets/js/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { initListTypeSensitivity } from "@hmcts/admin-pages/src/assets/js/list-type-sensitivity.js";
import "@hmcts/auth/src/assets/js/session-timeout.js";
import cookieManager from "@hmcts/cookie-manager";
import { initTableSearch } from "@hmcts/list-types-common/src/assets/js/table-search.js";
import { initBackToTop } from "@hmcts/web-core/src/assets/js/back-to-top.js";
Expand Down
138 changes: 138 additions & 0 deletions docs/tickets/229/plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
# Technical Plan: Azure B2C Sign-In

## Technical Approach

### Azure B2C Integration (Non-Prod Instance)
- **Use Non-Prod B2C instance** for integration as per technical criteria
- Add B2C OIDC strategy alongside existing SSO (passport-azure-ad)
- Three B2C user flows (policies): HMCTS, Common Platform, CaTH
- Pass `ui_locales` parameter for Welsh language support in B2C flows
- Handle B2C callbacks with authorization code exchange
- Store user profile in existing session (already Redis-backed via web-core)

### Environment Variables (Non-Prod B2C)
Add to `.env.example`:
```bash
# Azure B2C Configuration (Non-Prod)
B2C_TENANT_NAME=<non-prod-tenant-name>
B2C_TENANT_ID=<non-prod-tenant-id>
B2C_CLIENT_ID=<client-id>
B2C_CLIENT_SECRET=<client-secret>
B2C_POLICY_HMCTS=B2C_1A_HMCTS_SignIn
B2C_POLICY_COMMON_PLATFORM=B2C_1A_CommonPlatform_SignIn
B2C_POLICY_CATH=B2C_1A_CaTH_SignIn

# Session Timeout (milliseconds)
SESSION_TIMEOUT_WARNING_MS=1500000 # 25 minutes
SESSION_TIMEOUT_LOGOUT_MS=1800000 # 30 minutes
```

### Session Management
- Extend existing Redis session with inactivity tracking
- Server-side: Track `lastActivity` timestamp, calculate timeout
- Client-side: JavaScript countdown timer, show modal warning
- Timeout thresholds: warning at 25 mins, logout at 30 mins (configurable)

### Authentication Flow
1. User clicks "Sign in" → `/sign-in` (options page)
2. User selects provider → redirect to B2C with policy + language
3. B2C authentication → callback to `/b2c/callback`
4. Exchange code for tokens → create session → redirect to dashboard
5. Inactivity timer running on all authenticated pages
6. Warning modal → user can continue or be logged out

## Implementation

### New Files in libs/auth/

**Configuration:**
- `src/config/b2c-config.ts` - B2C tenant, policies, redirect URIs (similar to sso-config.ts)

**Business Logic:**
- `src/b2c/strategy.ts` - Passport B2C OIDC strategy setup
- `src/b2c/verify-callback.ts` - User profile extraction from B2C token
- `src/session/timeout-tracker.ts` - Session activity tracking service

**Middleware:**
- `src/middleware/session-timeout.ts` - Track activity, inject timeout data

**Pages:**
- `src/pages/sign-in/index.ts` - Sign-in options controller (GET/POST)
- `src/pages/sign-in/index.njk` - Radio buttons for 3 providers
- `src/pages/sign-in/en.ts` - English content
- `src/pages/sign-in/cy.ts` - Welsh content
- `src/pages/b2c-callback/index.ts` - B2C OAuth callback handler
- `src/pages/session-expired/index.ts` - Timeout expiry page
- `src/pages/session-expired/index.njk`
- `src/pages/session-expired/en.ts`
- `src/pages/session-expired/cy.ts`

**Assets:**
- `src/assets/js/session-timeout.ts` - Client-side countdown timer + modal

### Files to Modify

**libs/auth/:**
- `src/config/passport-config.ts` - Add B2C strategy alongside SSO
- `src/index.ts` - Export new B2C handlers and middleware
- `package.json` - Add build:nunjucks script for new pages

**apps/web/:**
- `src/app.ts` - Register session timeout middleware, mount B2C routes
- `vite.config.ts` - Include auth assets

**Root:**
- `.env.example` - Document B2C environment variables

### Key Exports (libs/auth/src/index.ts)
```typescript
export { sessionTimeoutMiddleware } from "./middleware/session-timeout.js";
export { GET as b2cCallbackHandler } from "./pages/b2c-callback/index.js";
export { isBtoCConfigured } from "./config/b2c-config.js";
```

## Error Handling

### B2C Errors
- Invalid authorization code → Show error summary on sign-in page
- Network failures → Retry with exponential backoff, fallback error page
- Invalid policy → Log error, redirect to sign-in with message

### Session Errors
- Redis connection failure → Fallback to in-memory sessions (dev only)
- Session not found → Redirect to `/session-expired` page
- Activity tracking failure → Log warning, continue (graceful degradation)

### User Input Errors
- No provider selected → Show GOV.UK error summary component
- Invalid callback state parameter → Log security event, reject with 403

## CLARIFICATIONS NEEDED

1. **Non-Prod B2C Tenant Configuration**
- Need Non-Prod B2C tenant name, tenant ID, client ID, and client secret
- Are the 3 user flow policies already created in Non-Prod B2C?
- What are the exact policy names (e.g., B2C_1A_HMCTS_SignIn)?
- Are Welsh translations configured in B2C user flows (ui_locales support)?
- What redirect URI is registered in Non-Prod B2C? (e.g., `https://localhost:8080/b2c/callback`)

2. **Session Timeout Values**
- Confirm timeout thresholds (warning: 25 mins, logout: 30 mins)?
- Should timeout apply to all users or only verified users?

3. **Sign-In Provider Differences**
- Do the 3 providers return different claims/profile structures?
- Should all 3 providers use the same B2C tenant or separate ones?
- Are error messages different for HMCTS vs CaTH/Common Platform?

4. **Dashboard Redirect**
- After sign-in, redirect to `/dashboard` or different page per role?
- Does dashboard already exist in verified-pages module?

5. **Forgotten Password Flow**
- Is this handled entirely by B2C user flows (built-in)?
- Or do we need custom pages for verification code entry?

6. **Sign-Out Behavior**
- Should sign-out call B2C logout endpoint or just destroy local session?
- Where to redirect after sign-out (home page `/` or custom page)?
86 changes: 86 additions & 0 deletions docs/tickets/229/tasks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# Implementation Tasks

## Prerequisites (Non-Prod B2C)
- [ ] Obtain Non-Prod B2C tenant name, tenant ID, client ID, and client secret
- [ ] Confirm user flow policy names for HMCTS, Common Platform, CaTH
- [ ] Register redirect URI in Non-Prod B2C (e.g., `https://localhost:8080/b2c/callback`)

## B2C Configuration and Strategy
- [x] Create `libs/auth/src/config/b2c-config.ts` with Non-Prod tenant, policies, redirect URIs
- [ ] Create `libs/auth/src/b2c/strategy.ts` with Passport B2C OIDC strategy (deferred - using manual token exchange)
- [ ] Create `libs/auth/src/b2c/verify-callback.ts` for user profile extraction (integrated into callback handler)
- [ ] Update `libs/auth/src/config/passport-config.ts` to register B2C strategy (not needed - manual flow)
- [x] Add B2C environment variables to `.env.example`

## Sign-In Options Page
- [x] Create `libs/auth/src/pages/sign-in/index.ts` controller with GET/POST handlers
- [x] Create `libs/auth/src/pages/sign-in/index.njk` with radio buttons for 3 providers
- [x] Create `libs/auth/src/pages/sign-in/en.ts` with English content
- [x] Create `libs/auth/src/pages/sign-in/cy.ts` with Welsh content
- [x] Add validation for provider selection
- [x] Add error summary component for validation errors

## B2C Callback Handler
- [x] Create `libs/auth/src/pages/b2c-callback/index.ts` OAuth callback controller
- [x] Handle authorization code exchange
- [x] Create session with user profile
- [x] Handle B2C errors (invalid code, network failures, invalid policy)
- [x] Redirect to dashboard after successful sign-in

## Session Timeout
- [x] Create `libs/auth/src/session/timeout-tracker.ts` service
- [x] Create `libs/auth/src/middleware/session-timeout.ts` middleware
- [x] Track `lastActivity` timestamp in session
- [x] Create `libs/auth/src/pages/session-expired/index.ts` controller
- [x] Create `libs/auth/src/pages/session-expired/index.njk` template
- [x] Create `libs/auth/src/pages/session-expired/en.ts` content
- [x] Create `libs/auth/src/pages/session-expired/cy.ts` content
- [x] Create `libs/auth/src/assets/js/session-timeout.ts` client-side timer
- [x] Add countdown modal warning at 25 mins
- [x] Auto-logout at 30 mins

## Welsh Language Support
- [x] Pass `ui_locales` parameter to B2C flows based on session language
- [x] Test Welsh content on sign-in page (implemented in controller tests)
- [x] Test Welsh content on session expired page (implemented)
- [ ] Verify B2C flows display Welsh when `ui_locales=cy` is passed (requires Non-Prod B2C setup)

## Sign-Out Functionality
- [x] Update sign-out handler to call B2C logout endpoint
- [x] Destroy local session on sign-out
- [x] Redirect to appropriate page after sign-out

## Forgot Password Functionality
- [x] Add `policyPasswordReset` to B2C config interface
- [x] Create `libs/auth/src/pages/b2c-forgot-password/index.ts` handler
- [x] Export handler in `libs/auth/src/index.ts`
- [x] Register `/b2c-forgot-password` route in `apps/web/src/app.ts`
- [x] Add unit tests (b2c-forgot-password/index.test.ts - 5 tests)

## Module Registration
- [x] Add `build:nunjucks` script to `libs/auth/package.json` (already exists)
- [x] Export B2C handlers and middleware in `libs/auth/src/index.ts`
- [x] Register session timeout middleware in `apps/web/src/app.ts`
- [x] Include auth assets in web app (JS in index.ts, CSS in index.scss, exports in package.json)

## Unit Tests
- [x] Test B2C strategy configuration (b2c-config.test.ts - 9 tests)
- [x] Test verify callback profile extraction (integrated into callback handler)
- [x] Test session timeout tracker service (timeout-tracker.test.ts - 13 tests)
- [x] Test session timeout middleware (session-timeout.test.ts - 6 tests)
- [x] Test sign-in page controller (GET/POST) (index.test.ts - 9 tests)
- [x] Test B2C callback handler (b2c-callback/index.test.ts - 23 tests)
- [x] Test session expired page controller (session-expired/index.test.ts - 5 tests)
- [x] Test validation errors on sign-in page (included in controller tests)

## E2E Tests
- [x] Test complete sign-in journey with HMCTS provider (sign-in.spec.ts - redirects to CFT IDAM)
- [x] Test complete sign-in journey with Common Platform provider (sign-in.spec.ts - redirects to home)
- [x] Test complete sign-in journey with CaTH provider (sign-in.spec.ts - redirects to B2C with Welsh locale support)
- [x] Test validation error when no provider selected (sign-in.spec.ts)
- [x] Test Welsh language on sign-in flow (sign-in.spec.ts - multiple Welsh tests)
- [ ] Test session timeout warning modal (requires authenticated session - manual testing)
- [ ] Test session timeout auto-logout (requires authenticated session - manual testing)
- [x] Test sign-out journey (sign-out.spec.ts - session-logged-out page)
- [x] Test accessibility on sign-in page (sign-in.spec.ts - axe-core checks)
- [x] Test accessibility on session expired page (session-expired.spec.ts - axe-core checks)
Loading
Loading