Skip to content

[HOTE-1082] feat: create-session-login-lambda#415

Draft
billwirz1 wants to merge 1 commit intomainfrom
feature/hote-1082/login-lambda
Draft

[HOTE-1082] feat: create-session-login-lambda#415
billwirz1 wants to merge 1 commit intomainfrom
feature/hote-1082/login-lambda

Conversation

@billwirz1
Copy link
Copy Markdown
Contributor

Description

Context

The login Lambda currently exchanges the NHS Login authorisation code for tokens and sets JWT cookies, but it does not persist a session record to Aurora. This ticket reworks the (or creates a new) login Lambda so that, at the point of successful NHS Login authentication, a new session row is written to the session table with all required fields. Downstream tickets (token refresh, reuse detection, logout) all depend on this record existing.

A new Goose migration is not required — migration 000008_create_session_table.sql already defines the schema. We should validate this schema and implement against it.

We should rewrite or create a new login lambda to handle this.

Type of changes

  • Refactoring (non-breaking change)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would change existing functionality)
  • Bug fix (non-breaking change which fixes an issue)

Checklist

  • I am familiar with the contributing guidelines
  • I have followed the code style of the project
  • I have added tests to cover my changes
  • I have updated the documentation accordingly
  • This PR is a result of pair or mob programming

Sensitive Information Declaration

To ensure the utmost confidentiality and protect your and others privacy, we kindly ask you to NOT including PII (Personal Identifiable Information) / PID (Personal Identifiable Data) or any other sensitive data in this PR (Pull Request) and the codebase changes. We will remove any PR that do contain any sensitive information. We really appreciate your cooperation in this matter.

  • I confirm that neither PII/PID nor sensitive data are included in this PR and the codebase changes.

Co-authored-by: Copilot <copilot@github.com>
Copilot AI review requested due to automatic review settings April 27, 2026 14:03
@github-actions
Copy link
Copy Markdown

UI Coverage Report

Lines Statements Branches Functions
Coverage: 96%
96.32% (5927/6153) 88.4% (724/819) 88.44% (222/251)

@github-actions
Copy link
Copy Markdown

Lambdas Coverage Report

Lines Statements Branches Functions
Coverage: 97%
97.68% (2194/2246) 90.42% (633/700) 96.56% (366/379)

@sonarqubecloud
Copy link
Copy Markdown

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds a new preview-only session-login-lambda that performs NHS Login callback handling, persists a session record to Aurora via SessionDbClient, and issues isolated preview cookies; also adds supporting local infra and Postman tooling.

Changes:

  • Introduces lambdas/src/session-login-lambda/* (handler, DI init, service layer, schemas, cookie helpers) with Jest unit coverage.
  • Extends NhsLoginService to surface verified ID token iss and aud claims for downstream session persistence.
  • Adds local infra wiring (session-preview/login route), plus Postman collection/environment and a root package.json Newman script to exercise the flow.

Reviewed changes

Copilot reviewed 16 out of 17 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
scripts/config/gitleaks.toml Ignores UI .pnpm-store to reduce false-positive secret scans.
postman/session_login_preview_wiremock.postman_collection.json Adds a collection to exercise the preview session login flow against LocalStack + WireMock.
postman/session_login_preview_wiremock.local.postman_environment.json Provides a local Postman environment for the collection.
postman/README.md Documents how to run the Postman collection manually and via CLI.
package.json Adds a Newman-based script to run the Postman collection with Terraform-derived API Gateway values.
local-environment/infra/main.tf Wires a new session_login_lambda module and exposes it under session-preview/login.
lambdas/src/session-login-lambda/session-login-service.ts Implements session creation + persistence + token signing after successful NHS Login.
lambdas/src/session-login-lambda/session-login-service.test.ts Unit tests for the session login service behavior and failure modes.
lambdas/src/session-login-lambda/schemas.ts Zod schema for the callback request body (code).
lambdas/src/session-login-lambda/init.ts DI wiring for HTTP/JWKS/NHS services, DB client, and session token service.
lambdas/src/session-login-lambda/init.test.ts Unit tests for DI wiring, singleton protection, and rejection retry behavior.
lambdas/src/session-login-lambda/index.ts Lambda handler that validates input, calls service, sets preview cookies, and echoes correlation ID.
lambdas/src/session-login-lambda/index.test.ts Handler tests for validation, cookie output, and status code mapping.
lambdas/src/session-login-lambda/cookies.ts Helpers to build isolated preview cookie strings and paths.
lambdas/src/lib/login/nhs-login-service.ts Adds idTokenIssuer and idTokenAudience to the callback result.
lambdas/src/lib/login/nhs-login-service.test.ts Updates tests to assert the newly-returned issuer/audience fields.
.gitignore Ignores .pnpm-store at repo root.

Comment on lines +62 to +75
throw new Error(
"AUTH_COOKIE_PRIVATE_KEYS_SECRET_NAME must be either a non-JSON private key string or a JSON object containing a non-empty 'key' entry.",
);
} catch (error) {
if (!(error instanceof SyntaxError)) {
throw error;
}
}

if (secretValue.trim().length === 0) {
throw new Error("AUTH_COOKIE_PRIVATE_KEYS_SECRET_NAME secret value must not be empty.");
}

return secretValue;
Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

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

parseAuthCookiePrivateKey error messages refer to AUTH_COOKIE_PRIVATE_KEYS_SECRET_NAME as if it were the secret value, which is confusing when debugging (the env var is a secret name, not the secret contents).
Reword these errors to distinguish between the secret name env var and the retrieved secret value (e.g. "AUTH_COOKIE_PRIVATE_KEYS_SECRET_NAME secret value must...").

Copilot uses AI. Check for mistakes.
Comment on lines +142 to +147
"const body = pm.response.json();",
"pm.test(\"User info payload is returned\", function () {",
" pm.expect(body.sub).to.eql(\"test-sub-123\");",
" pm.expect(body.nhs_number).to.eql(\"9912003071\");",
" pm.expect(body.gp_registration_details.gp_ods_code).to.eql(\"Y12345\");",
"});",
Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

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

This Postman collection uses a 10-digit nhs_number value in assertions; even in test tooling, we should avoid using data that could be mistaken for real patient identifiers.
Replace it with a clearly synthetic/non-production marker value and/or add an explicit note that all identifiers are fictitious test data.

Copilot uses AI. Check for mistakes.
Comment on lines +166 to +169
if (typeof audience !== "string") {
return undefined;
}

Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

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

normalizeAudience rejects the JWT aud claim when it is an array, but per JWT/OIDC the aud claim is allowed to be string | string[], so this can cause valid NHS Login tokens to fail with ID_TOKEN_AUD_INVALID and block login.
Accept string[] audiences (e.g., if length===1 use the only entry; if multiple, select the expected clientId if present, otherwise fail with a clear error).

Suggested change
if (typeof audience !== "string") {
return undefined;
}
if (typeof audience === "string") {
return this.normalizeAudienceValue(audience);
}
if (!Array.isArray(audience)) {
return undefined;
}
const normalizedAudiences = audience
.map((value) => this.normalizeAudienceValue(value))
.filter((value): value is string => value !== undefined);
if (normalizedAudiences.length === 0) {
return undefined;
}
const uniqueAudiences = [...new Set(normalizedAudiences)];
if (uniqueAudiences.length === 1) {
return uniqueAudiences[0];
}
throw new Error("ID token contains multiple audiences and cannot be normalized");
}
private normalizeAudienceValue(audience: string): string | undefined {

Copilot uses AI. Check for mistakes.
Comment on lines +10 to +20
export function buildPreviewAccessCookie(token: string, sameSite: string, secure: boolean): string {
return `${PREVIEW_SESSION_ACCESS_COOKIE_NAME}=${token}; HttpOnly; Path=${PREVIEW_SESSION_ACCESS_COOKIE_PATH}; SameSite=${sameSite};${secureAttribute(secure)}`;
}

export function buildPreviewRefreshCookie(
token: string,
sameSite: string,
secure: boolean,
): string {
return `${PREVIEW_SESSION_REFRESH_COOKIE_NAME}=${token}; HttpOnly; Path=${PREVIEW_SESSION_REFRESH_COOKIE_PATH}; SameSite=${sameSite};${secureAttribute(secure)}`;
}
Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

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

sameSite is interpolated directly into Set-Cookie without validation, so an unexpected env value can produce invalid cookies or (in worst cases) response-splitting if it contains control characters.
Validate AUTH_COOKIE_SAME_SITE to an allowed set (e.g. Strict/Lax/None) and enforce Secure when SameSite=None before building cookies.

Copilot uses AI. Check for mistakes.
}));

jest.mock("../lib/login/nhs-token-verifier", () => ({
NhsTokenVerifier: jest.fn().mockImplementation(() => ({ verifyIdToken: jest.fn() })),
Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

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

The init unit test mocks NhsTokenVerifier with a verifyIdToken method, but the real verifier interface used by NhsLoginService is verifyToken; this mismatch can hide wiring issues and makes the mock misleading.
Update the mock to expose verifyToken (or return a shape matching INhsTokenVerifier).

Suggested change
NhsTokenVerifier: jest.fn().mockImplementation(() => ({ verifyIdToken: jest.fn() })),
NhsTokenVerifier: jest.fn().mockImplementation(() => ({ verifyToken: jest.fn() })),

Copilot uses AI. Check for mistakes.
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