Signup Flow
Last Updated: 2026-03-06 Source: https://github.com/co-cddo/ndx Captured SHA:
b846188
Executive Summary
The NDX Signup Flow is a self-service user registration system that provisions accounts for UK local government employees in the Innovation Sandbox's IAM Identity Center. The system validates email domains against the ukps-domains allowlist (filtered to local authority entries), creates users cross-account via STS role assumption, and provides operator alerting through AWS Chatbot to Slack. The entire flow is fronted by CloudFront with OAC-signed Lambda Function URL invocations, CSRF protection, WAF rate limiting, and structured JSON logging with PII redaction.
End-to-End Signup Flow
Lambda Handler Architecture
File: repos/ndx/infra-signup/lib/lambda/signup/handler.ts (463 lines)
The handler exposes three endpoints behind the /signup-api/ path prefix:
| Method | Path | Purpose | Story |
|---|---|---|---|
| GET | /signup-api/health | Infrastructure verification | 1.2 |
| GET | /signup-api/domains | Fetch allowed domain list | 1.3 |
| POST | /signup-api/signup | Create user account | 1.4 |
Request Validation Chain
The signup endpoint applies a strict validation pipeline before any business logic:
- Timing delay (50-150ms random): Prevents timing-based information leakage
- Body size check: Rejects requests exceeding 10KB
- Content-Type validation: Requires
application/json - CSRF validation: Requires
X-NDX-Request: signup-formheader (ADR-045) - JSON parsing: Prototype pollution defense (rejects
__proto__keys) - Required fields:
firstName,lastName,email,domain - Name validation: Max 100 characters, forbidden character regex from shared
@ndx/signup-types - Email validation: Max 254 characters (RFC 5321), rejects
+aliases - Email normalization: Lowercase, ASCII-only (Unicode homoglyph defense), strip
+suffix
Security Headers
All Lambda responses include:
Content-Security-Policy: default-src 'none'
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
Referrer-Policy: strict-origin-when-cross-origin
Strict-Transport-Security: max-age=31536000; includeSubDomains
X-Request-ID: {correlationId}
Structured Logging
All log entries use JSON format with level, message, correlationId, and domain (never the full email). PII is never written to logs per NFR22.
Domain Service
File: repos/ndx/infra-signup/lib/lambda/signup/domain-service.ts (248 lines)
Fetches and caches the UK public sector domain allowlist from GitHub.
Data Source
https://raw.githubusercontent.com/govuk-digital-backbone/ukps-domains/main/data/user_domains.json
The response follows this structure:
{
"version": "...",
"domains": [
{
"domain_pattern": "birmingham.gov.uk",
"organisation_type_id": "local_authority",
"notes": "Local authority: Birmingham City",
"source": "..."
}
]
}
Filtering and Transformation
Only domains with organisation_type_id === "local_authority" are included. Each entry is transformed to a DomainInfo object with the organisation name extracted from the notes field (e.g., "Local authority: Birmingham City" becomes "Birmingham City").
Caching Strategy (ADR-044)
- Module-level cache persists across warm Lambda invocations
- 5-minute TTL per ADR-044 and NFR12
- 3-second fetch timeout to prevent Lambda hangs
- Graceful fallback to stale cache on GitHub unavailability (NFR18, NFR21)
Identity Store Service
File: repos/ndx/infra-signup/lib/lambda/signup/identity-store-service.ts (371 lines)
Manages user creation in AWS IAM Identity Center via cross-account STS role assumption.
Cross-Account Access Pattern
Credential caching: STS credentials are cached at module level with a 5-minute expiry buffer. The IdentitystoreClient is reused across invocations and only recreated when credentials refresh.
Default region: us-west-2 (configurable via AWS_REGION environment variable).
User Creation Sequence
ListUserswith email filter to check existence- If exists: return 409 Conflict with redirect to
/login CreateUserwithUserName(email),DisplayName,Name(given/family), andEmailsCreateGroupMembershipto add user to the NDX Users group- Race condition handling:
ConflictExceptionfromCreateUserreturns 409
Note: IAM Identity Center's "Send email OTP for users created from API" setting handles password setup. The Lambda does not send a welcome email directly.
Cross-Account IAM Role
File: repos/ndx/infra-signup/isb-cross-account-role.yaml
Deployed to the ISB account (955063685555) via CloudFormation.
Trust Policy:
- Principal: The Lambda execution role ARN in the NDX account
- Condition: External ID
ndx-signup-external-id
Permissions (scoped per ADR-043):
| Action | Resource Scope |
|---|---|
identitystore:CreateUser | Identity store ARN |
identitystore:ListUsers | Identity store ARN + user/* |
identitystore:DescribeUser | Identity store ARN + user/* |
identitystore:CreateGroupMembership | Identity store ARN + user/* + specific group ARN |
Signup Infrastructure Stack
File: repos/ndx/infra-signup/lib/signup-stack.ts (371 lines)
Lambda Function
| Property | Value |
|---|---|
| Runtime | Node.js 20 |
| Memory | 256 MB |
| Timeout | 30 seconds |
| Function Name | ndx-signup |
| Tracing | X-Ray Active |
| Bundling | esbuild, minified, sourcemaps |
| Log Retention | 90 days (THREE_MONTHS) |
| Log Group | /aws/lambda/ndx-signup |
Environment Variables:
IDENTITY_STORE_ID: IAM Identity Center store IDGROUP_ID: NDX Users group IDCROSS_ACCOUNT_ROLE_ARN: Role in ISB accountENVIRONMENT: prod/testLOG_LEVEL: INFO (prod) or DEBUG (test)NODE_OPTIONS:--enable-source-maps
Function URL + CloudFront OAC
The Lambda Function URL uses AWS_IAM auth type. CloudFront signs requests using SigV4 via Origin Access Control. Both lambda:InvokeFunctionUrl and lambda:InvokeFunction permissions are granted to the CloudFront service principal, scoped to the distribution ARN.
Operator Alerting (Story 3.1)
EventBridge Rule (ndx-signup-createuser-alert):
- Source:
aws.sso-directory - Detail Type:
AWS API Call via CloudTrail - Event:
CreateUserfromsso-directory.amazonaws.com - Target: SNS topic
ndx-signup-alerts
SNS Topic (ndx-signup-alerts):
- Cross-account resource policy allows AWS Chatbot in NDX account (568672915267) to subscribe
- Chatbot forwards formatted notifications to configured Slack channel
WAF Rate Limiting (Story 3.2)
See 30-ndx-website.md for WAF stack details. The WAF is deployed to us-east-1 and scoped to /signup-api/signup with a limit of 10 requests per 5-minute window per IP, returning a 429 JSON response.
Shared Types
Package: @ndx/signup-types (referenced via path mapping, ADR-048)
Shared between frontend signup form and backend Lambda to ensure consistent validation:
SignupRequestinterface:firstName,lastName,email,domainSignupErrorCodeenum:INVALID_EMAIL,DOMAIN_NOT_ALLOWED,USER_EXISTS,CSRF_INVALID,INVALID_CONTENT_TYPE,SERVER_ERRORERROR_MESSAGESmappingFORBIDDEN_NAME_CHARSregexDomainInfointerface:domain,orgName
Error Responses
| Status | Code | Condition |
|---|---|---|
| 200 | {success: true} | User created successfully |
| 400 | REQUEST_TOO_LARGE | Body exceeds 10KB |
| 400 | INVALID_CONTENT_TYPE | Not application/json or malformed JSON |
| 400 | INVALID_EMAIL | Missing fields, name too long, bad chars, email too long, + alias |
| 403 | CSRF_INVALID | Missing or invalid X-NDX-Request header |
| 403 | DOMAIN_NOT_ALLOWED | Email domain not in local authority allowlist |
| 404 | NOT_FOUND | Unknown endpoint |
| 409 | USER_EXISTS | Account already registered (includes redirectUrl: /login) |
| 429 | RATE_LIMITED | WAF rate limit exceeded |
| 500 | SERVER_ERROR | Identity Store or internal failure |
| 503 | SERVICE_UNAVAILABLE | Domain service or GitHub unavailable |
CI/CD Pipeline
The signup infrastructure is deployed via the infra.yaml workflow:
- Unit Tests (
signup-infra-unit-tests): Jest tests oninfra-signup/changes - Signup CDK Deploy (
signup-cdk-deploy): Deploys Lambda to NDX account via OIDC roleGitHubActions-NDX-InfraDeploy - ISB Cross-Account Role Deploy (
isb-cross-account-role-deploy): Deploys IAM role to ISB account via OIDC roleGitHubActions-ISB-InfraDeploy
All jobs use step-security/harden-runner with egress audit and pinned action SHAs.
Related Documentation
- 30-ndx-website.md - Main website architecture (CloudFront, S3, WAF)
- 32-scenarios-microsite.md - Scenarios microsite
- 10-isb-core-architecture.md - ISB core with IAM Identity Center
- 00-repo-inventory.md - Repository overview
Source Files Referenced
| File Path | Purpose | Lines |
|---|---|---|
repos/ndx/infra-signup/lib/lambda/signup/handler.ts | Lambda handler with routing and validation | 463 |
repos/ndx/infra-signup/lib/lambda/signup/domain-service.ts | Domain allowlist fetching and caching | 248 |
repos/ndx/infra-signup/lib/lambda/signup/identity-store-service.ts | Cross-account IAM Identity Center client | 371 |
repos/ndx/infra-signup/lib/lambda/signup/services.ts | Shared domain logic (normalize, validate) | 103 |
repos/ndx/infra-signup/lib/signup-stack.ts | CDK stack definition | 371 |
repos/ndx/infra-signup/isb-cross-account-role.yaml | Cross-account IAM role (CloudFormation) | 80 |
repos/ndx/infra-signup/isb-github-actions-role.yaml | GitHub Actions OIDC role for ISB | ~100 |
repos/ndx/infra/lib/waf-stack.ts | WAF rate limiting for signup API | 182 |
repos/ndx/.github/workflows/infra.yaml | CI/CD pipeline for signup infrastructure | 431 |
Generated from source analysis. See 00-repo-inventory.md for full inventory.