Skip to content

Feature/347 replace passport azure ad with openid client#385

Open
ChrisS1512 wants to merge 6 commits intomasterfrom
feature/347-replace-passport-azure-ad-with-openid-client
Open

Feature/347 replace passport azure ad with openid client#385
ChrisS1512 wants to merge 6 commits intomasterfrom
feature/347-replace-passport-azure-ad-with-openid-client

Conversation

@ChrisS1512
Copy link
Copy Markdown
Contributor

@ChrisS1512 ChrisS1512 commented Feb 19, 2026

Jira link

#347

Change description

Replaced passport-azure-ad with openid client

Summary by CodeRabbit

  • Configuration Updates

    • Changed SSO configuration environment variable from SSO_IDENTITY_METADATA to SSO_ISSUER_URL.
    • Removed the SSO_ALLOW_HTTP_REDIRECT setting, simplifying SSO setup requirements.
  • Documentation

    • Added comprehensive migration documentation and rollout plan for authentication system updates.
  • Tests

    • Updated and expanded test coverage for authentication configuration and logout flows.

github-actions bot and others added 4 commits February 19, 2026 14:56
…ith openid-client

Created comprehensive technical plan for migrating from deprecated passport-azure-ad
library to openid-client for SSO authentication.

Plan includes:
- Custom Passport strategy wrapper around openid-client (minimal risk approach)
- Complete implementation details and file structure
- Comprehensive testing strategy (unit, integration, E2E)
- Zero-downtime deployment strategy
- 55-task implementation checklist organized in 10 phases

Key clarifications needed:
- Azure B2C implementation status
- SSO_ISSUER_URL format preference
- Token refresh strategy
- Deployment window availability
- Feature flag requirements for rollback

Files created:
- docs/tickets/347/ticket.md - Issue details and requirements
- docs/tickets/347/plan.md - Technical implementation plan
- docs/tickets/347/tasks.md - Implementation task checklist

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

coderabbitai bot commented Feb 19, 2026

Warning

Rate limit exceeded

@ChrisS1512 has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 16 minutes and 44 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📝 Walkthrough

Walkthrough

This PR migrates SSO authentication from the deprecated passport-azure-ad library to openid-client, replacing the SSO_IDENTITY_METADATA configuration with SSO_ISSUER_URL. The authentication flow is converted to asynchronous and includes comprehensive documentation of the migration strategy, implementation tasks, and test coverage plans.

Changes

Cohort / File(s) Summary
Workflow and CI/CD Configuration
.github/workflows/e2e.yml, .github/workflows/nightly.yml
Updated environment variable references from SSO_IDENTITY_METADATA to SSO_ISSUER_URL and removed explicit SSO_ALLOW_HTTP_REDIRECT setting.
Helm and Deployment Configuration
apps/web/helm/values.yaml, apps/web/helm/values.dev.yaml, apps/web/config/custom-environment-variables.json
Replaced SSO_IDENTITY_METADATA with SSO_ISSUER_URL in secret mappings and environment variable configuration; removed SSO_ALLOW_HTTP_REDIRECT from Node.js environment setup.
Package Dependencies
package.json, libs/auth/package.json
Replaced passport-azure-ad (4.3.5) with openid-client (6.3.3) and removed associated type definitions.
Core Authentication Configuration
libs/auth/src/config/sso-config.ts, libs/auth/src/config/sso-config.test.ts
Updated SsoConfig interface to use issuerUrl instead of identityMetadata; removed responseType and responseMode fields; added isSsoConfigured() function to validate configuration readiness.
Passport Strategy Integration
libs/auth/src/config/passport-config.ts, libs/auth/src/config/passport-config.test.ts
Converted configurePassport() to async function using openid-client discovery and strategy; introduced OIDC discovery flow, PKCE support, and token validation; updated test mocks to reflect new strategy structure.
Authentication Route Handlers
libs/auth/src/pages/logout/index.ts, libs/auth/src/pages/logout/index.test.ts, libs/auth/src/role-service/index.test.ts
Updated logout tenant extraction to use issuerUrl instead of identityMetadata; adjusted role-service tests to reflect simplified SSO configuration.
Application Startup
apps/web/src/app.ts
Modified app initialization to await asynchronous configurePassport() call.
Migration Documentation
docs/tickets/347/ticket.md, docs/tickets/347/plan.md, docs/tickets/347/review.md, docs/tickets/347/tasks.md
Comprehensive documentation outlining motivation, implementation strategy, detailed tasks across ten phases, architectural review, acceptance criteria, and deployment considerations for the authentication migration.

Sequence Diagram

sequenceDiagram
    actor User
    participant App as Express App
    participant Passport as Passport
    participant OIDC as openid-client
    participant Provider as OIDC Provider
    participant Graph as Graph API

    User->>App: 1. Initiate login
    App->>OIDC: 2. Discover OIDC metadata<br/>(issuerUrl)
    OIDC->>Provider: 3. Fetch .well-known config
    Provider-->>OIDC: 4. Return endpoints & keys
    OIDC-->>App: 5. Return Client instance
    
    App->>Passport: 6. Generate auth URL<br/>(with PKCE challenge)
    Passport-->>User: 7. Redirect to Provider login
    
    User->>Provider: 8. Authenticate & consent
    Provider->>User: 9. Redirect with auth code
    
    User->>App: 10. Return with code & state
    App->>OIDC: 11. Exchange code for tokens<br/>(PKCE verifier)
    OIDC->>Provider: 12. Token endpoint request
    Provider-->>OIDC: 13. Return access & ID tokens
    OIDC-->>App: 14. Validate ID token signature
    
    App->>Graph: 15. Fetch user profile<br/>(if needed)
    Graph-->>App: 16. Return user data
    
    App->>Passport: 17. Verify callback with claims
    Passport-->>App: 18. Session established
    App-->>User: 19. Authenticated & redirected
Loading

Possibly related issues

Possibly related PRs

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarises the primary change—replacing passport-azure-ad with openid-client for SSO authentication—and is clear and specific.

✏️ 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/347-replace-passport-azure-ad-with-openid-client

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.

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: 9

🧹 Nitpick comments (10)
libs/auth/src/pages/logout/index.ts (1)

4-7: Minor: regex only matches lowercase hex in tenant ID.

The regex [a-f0-9-] will not match uppercase hex characters. Azure AD typically returns lowercase GUIDs in issuer URLs, so this is fine in practice, but if a configuration ever uses uppercase (e.g., ABCDEF12-...), extraction will silently fail and the user will be redirected to /session-logged-out instead of the Azure AD logout endpoint.

Consider using a case-insensitive flag or [a-fA-F0-9-] for robustness:

Suggested fix
 function extractTenantId(issuerUrl: string): string | null {
-  const match = issuerUrl.match(/\/([a-f0-9-]+)\/v2\.0/);
+  const match = issuerUrl.match(/\/([a-f0-9-]+)\/v2\.0/i);
   return match ? match[1] : null;
 }
libs/auth/src/config/passport-config.ts (1)

12-23: Non-exported helper precedes exported function.

Per coding guidelines, exported functions should come before non-exported helpers. initializePassport (non-exported) is defined before configurePassport (exported).

Suggested reorder

Move initializePassport below configurePassport, or alternatively inline the calls since it's only used twice.

As per coding guidelines: "constants outside function scope at top, exported functions next, other functions in usage order."

docs/tickets/347/plan.md (2)

130-141: Plan's SsoConfig interface is stale — includes responseType and responseMode that were removed in implementation.

The actual SsoConfig in libs/auth/src/config/sso-config.ts no longer includes these fields. Consider updating the plan to reflect the final implementation, or adding a note that the plan predates implementation.


412-428: Code example uses openid-client v5 API (Issuer.discover()).

The implementation uses the v6 API (client.discovery()). This sample would mislead anyone referencing the plan for future work. The actual version in use is 6.3.3, not 6.1.3 (line 183).

libs/auth/src/config/passport-config.test.ts (4)

94-114: Incomplete config validation tests only check empty-string values.

The production code guards with !ssoConfig.issuerUrl || !ssoConfig.clientId || !ssoConfig.clientSecret, which treats both "" and undefined as falsy. The tests only exercise the empty-string path. Consider adding a case with undefined to confirm the guard covers both, especially since environment variables can genuinely be absent (not just blank).


139-145: Discovery failure test validates current (throwing) behaviour — be aware this will need updating.

The review document (review.md) flags unhandled discovery failure as a critical issue and proposes a graceful fallback. Once that fix lands, this test's assertion (rejects.toThrow) will need to change to expect configurePassport to succeed without registering a strategy. Worth noting with a // TODO or addressing in the same PR.


295-309: Missing access token test — verify the assertion matches the error constructor.

Line 307 uses expect.objectContaining({ message: "No access token received from identity provider" }). This works because new Error(msg) sets .message, but it's slightly fragile — if the production code ever changes to a custom error class, this assertion could silently pass or fail in unexpected ways. The current form is acceptable, though matching with expect(new Error("No access token received from identity provider")) would be more explicit.


128-136: Import STRATEGY_NAME constant in the test file and use it instead of the hardcoded string.

The test hardcodes "azuread-openidconnect" while production uses the STRATEGY_NAME constant. This creates a maintenance risk—if the constant is ever changed, the test will not catch the discrepancy. Using the constant in the test ensures a single source of truth.

docs/tickets/347/review.md (1)

1-7: Useful review document overall, but needs a factual accuracy pass.

The positive feedback and architectural observations are sound. However, several sections reference code states that don't match what's actually in the PR (see other comments). A quick reconciliation pass against the final test file and production code would make this a reliable reference for the team.

docs/tickets/347/tasks.md (1)

51-103: Phases 5–10 remain largely unchecked — ensure these are tracked before merge.

Integration tests, E2E tests, environment configuration, manual testing, and deployment preparation are all incomplete. This is expected for an initial review, but merging to master with these outstanding (particularly Phase 7: environment variable configuration and Phase 10: deployment runbook) carries risk.

SSO_CLIENT_ID: ${{ secrets.SSO_CLIENT_ID }}
SSO_CLIENT_SECRET: ${{ secrets.SSO_CLIENT_SECRET }}
SSO_IDENTITY_METADATA: ${{ secrets.SSO_ISSUER_URL }}
SSO_ISSUER_URL: ${{ secrets.SSO_ISSUER_URL }}
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 examine the e2e.yml workflow file to understand the context
cat -n .github/workflows/e2e.yml | head -100

Repository: hmcts/cath-service

Length of output: 4404


🏁 Script executed:

# Check if there are any other references to SSO secrets in the codebase
rg "SSO_ISSUER_URL|SSO_IDENTITY_METADATA" -A 2 -B 2

Repository: hmcts/cath-service

Length of output: 13279


🏁 Script executed:

# Look for documentation about required secrets or environment setup
fd -type f \( -name "README*" -o -name "*.md" \) | xargs grep -l "secret\|SSO" 2>/dev/null | head -10

Repository: hmcts/cath-service

Length of output: 232


Confirm SSO_ISSUER_URL GitHub Actions secret is provisioned.

The workflow references ${{ secrets.SSO_ISSUER_URL }} on line 64. The authentication configuration requires this secret and will fail during initialisation if it is missing or empty. Verify the secret exists in the repository settings and contains the correct Azure AD v2.0 issuer URL (format: https://login.microsoftonline.com/{tenant}/v2.0).

Comment on lines 73 to +74
// Initialize Passport for Azure AD authentication
configurePassport(app);
await configurePassport(app);
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

Stale comment still references "Azure AD".

The inline comment no longer reflects the implementation.

✏️ Proposed fix
-  // Initialize Passport for Azure AD authentication
+  // Initialize Passport with OpenID Connect strategy
   await configurePassport(app);
📝 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
// Initialize Passport for Azure AD authentication
configurePassport(app);
await configurePassport(app);
// Initialize Passport with OpenID Connect strategy
await configurePassport(app);


// Initialize Passport for Azure AD authentication
configurePassport(app);
await configurePassport(app);
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:

rg -n --type=ts -g '!**/node_modules/**' 'createApp' -A5 -B2

Repository: hmcts/cath-service

Length of output: 12979


🏁 Script executed:

# Find the main entrypoint file in apps/web
fd -t f -e ts -e js 'index|main|server|start' apps/web/src/ | head -20

Repository: hmcts/cath-service

Length of output: 279


🏁 Script executed:

# Check the structure of apps/web/src
ls -la apps/web/src/

Repository: hmcts/cath-service

Length of output: 581


🏁 Script executed:

cat -n apps/web/src/server.ts

Repository: hmcts/cath-service

Length of output: 2551


🏁 Script executed:

cat -n apps/web/src/app.ts | head -80

Repository: hmcts/cath-service

Length of output: 4260


🏁 Script executed:

cat -n apps/api/src/server.ts | head -30

Repository: hmcts/cath-service

Length of output: 1244


Wrap startServer() with error handling to gracefully handle OIDC discovery failures.

configurePassport performs async OIDC discovery at startup. If the network call fails, createApp() will reject and cause an unhandled promise rejection, crashing the process without logs. The web server's top-level await has no error handling (unlike the API server which uses try-catch). Add structured error handling around the startup sequence to catch and log failures, then exit cleanly.

Comment on lines +15 to +16
- [x] Create `libs/auth/src/openid-connect-strategy/` directory
- [x] Create `libs/auth/src/openid-connect-strategy/openid-connect-strategy.ts` with base strategy class
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

Phase 2 tasks 1–2 are misleadingly marked as complete.

These items say a custom openid-connect-strategy/ directory and base strategy class were created, but the actual implementation uses the built-in openid-client/passport Strategy — no custom strategy exists. The remaining Phase 2 items (lines 18–22) correctly note "(delegated to openid-client/passport built-in Strategy)"; these two should be updated similarly or struck through to avoid confusion.

Comment on lines +36 to +37
- [x] Create `libs/auth/src/openid-connect-strategy/openid-connect-strategy.test.ts`
- [x] Write tests for OIDC discovery success and failure
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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for openid-connect-strategy files
fd "openid-connect-strategy" --type f

Repository: hmcts/cath-service

Length of output: 44


🏁 Script executed:

#!/bin/bash
# Check the content of the tasks.md file to understand the context
cat -n docs/tickets/347/tasks.md | head -50

Repository: hmcts/cath-service

Length of output: 3500


Phase 4 tasks reference files that do not exist in the codebase.

Lines 36–37 reference openid-connect-strategy.test.ts and related OIDC tests, yet these files are not present in the repository. Although Phase 2 tasks also claim to have created the base strategy files, none of these files exist. These task items should not be marked complete ([x]) until the actual files are created and tests are written.


CaTH AI is currently using library passport-azure-ad which will be deprecated soon. Instead of using passport-azure-ad, we need to use openid-client in our application.

SSO_ISSUER_URL will be used from keyvault and Github secrets which has been added already.
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

Minor text corrections.

"keyvault" → "Key Vault" (Azure product name), "Github" → "GitHub" (proper noun).

🧰 Tools
🪛 LanguageTool

[uncategorized] ~14-~14: The official name of this software platform is spelled with a capital “H”.
Context: ...SUER_URL will be used from keyvault and Github secrets which has been added already. ...

(GITHUB)


[uncategorized] ~14-~14: Possible missing comma found.
Context: ...L will be used from keyvault and Github secrets which has been added already. **Accept...

(AI_HYDRA_LEO_MISSING_COMMA)

Comment on lines +25 to +39
export async function configurePassport(app: Express): Promise<void> {
const disableSso = process.env.NODE_ENV === "development" && !process.env.ENABLE_SSO;

if (disableSso) {
initializePassport(app);
return;
}

const ssoConfig = getSsoConfig();

// Check if SSO configuration is complete (e.g., for E2E tests or environments without SSO setup)
if (!ssoConfig.identityMetadata || !ssoConfig.clientId || !ssoConfig.clientSecret) {
// Initialize passport with minimal configuration (no OIDC strategy)
app.use(passport.initialize());
app.use(passport.session());

// Simple serialization
passport.serializeUser((user, done) => {
done(null, user);
});

passport.deserializeUser((user: Express.User, done) => {
done(null, user);
});

return;
if (!ssoConfig.issuerUrl || !ssoConfig.clientId || !ssoConfig.clientSecret) {
throw new Error("SSO configuration is incomplete. Required: SSO_ISSUER_URL, SSO_CLIENT_ID, SSO_CLIENT_SECRET");
}

// Initialize passport
app.use(passport.initialize());
app.use(passport.session());
passport.use(
new OIDCStrategy(
{
identityMetadata: ssoConfig.identityMetadata,
clientID: ssoConfig.clientId,
clientSecret: ssoConfig.clientSecret,
redirectUrl: ssoConfig.redirectUri,
responseType: ssoConfig.responseType,
responseMode: ssoConfig.responseMode,
scope: ssoConfig.scope,
passReqToCallback: false,
validateIssuer: true,
clockSkew: 300
},
verifyOidcCallback
)
const oidcConfig = await client.discovery(new URL(ssoConfig.issuerUrl), ssoConfig.clientId, ssoConfig.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 | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

fd "app.ts" --type f -exec grep -l "configurePassport" {} \;

Repository: hmcts/cath-service

Length of output: 314


🏁 Script executed:

fd "plan.md" --type f

Repository: hmcts/cath-service

Length of output: 566


🏁 Script executed:

git ls-files | grep -E "(app\.ts|plan\.md)" | head -20

Repository: hmcts/cath-service

Length of output: 634


🏁 Script executed:

grep -n "configurePassport" apps/api/src/app.ts apps/web/src/app.ts 2>/dev/null

Repository: hmcts/cath-service

Length of output: 253


🏁 Script executed:

rg "OIDC discovery|discovery fails" docs/tickets --type md

Repository: hmcts/cath-service

Length of output: 1865


🏁 Script executed:

cat -n libs/auth/src/config/passport-config.ts | head -50

Repository: hmcts/cath-service

Length of output: 1976


Add error handling for OIDC discovery failure on line 39.

Line 39 client.discovery() lacks a try/catch block. According to plan.md, when discovery fails, the expected behaviour is to "Log error, return 503 on /login attempts," not fail app startup. Wrap discovery in try/catch, log the error, and initialize passport without the OIDC strategy to allow graceful degradation.

Comment on lines +126 to +136
describe("isSsoConfigured", () => {
let originalEnv: NodeJS.ProcessEnv;

beforeEach(() => {
originalEnv = { ...process.env };
vi.resetModules();
});

afterEach(() => {
process.env = originalEnv;
});
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

isSsoConfigured beforeEach doesn't scrub SSO_ISSUER_URL.

Unlike the getSsoConfig suite, the beforeEach here omits deleting SSO_ISSUER_URL, SSO_CLIENT_ID, and SSO_CLIENT_SECRET. The test on line 146 that asserts isSsoConfigured() returns false when issuerUrl is missing is therefore fragile in any environment where SSO_ISSUER_URL is already set (e.g., a developer's local shell).

🛡️ Proposed fix
  beforeEach(() => {
    originalEnv = { ...process.env };
+   delete process.env.SSO_ISSUER_URL;
+   delete process.env.SSO_CLIENT_ID;
+   delete process.env.SSO_CLIENT_SECRET;
+   delete process.env.NODE_ENV;
+   delete process.env.ENABLE_SSO;
    vi.resetModules();
  });

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Feb 19, 2026

🎭 Playwright E2E Test Results

235 tests   235 ✅  21m 51s ⏱️
 32 suites    0 💤
  1 files      0 ❌

Results for commit 19b9279.

♻️ This comment has been updated with latest results.

@sonarqubecloud
Copy link
Copy Markdown

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