Skip to content

Commit 611e206

Browse files
updating with v2
2 parents b5de725 + 9630787 commit 611e206

30 files changed

Lines changed: 3958 additions & 3422 deletions

.eslintrc.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,12 @@
4848
{
4949
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
5050
"rules": {
51-
"prettier/prettier": ["error"],
51+
"prettier/prettier": [
52+
"error",
53+
{
54+
"endOfLine": "auto"
55+
}
56+
],
5257
"tsdoc/syntax": "warn"
5358
}
5459
}

.nvmrc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
12
1+
20

designer/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@
124124
"jsdom": "^19.0.0",
125125
"lcov-result-merger": "^3.1.0",
126126
"mini-css-extract-plugin": "^0.11.2",
127-
"msw": "^1.3.2",
127+
"msw": "^1.3.5",
128128
"nodemon": "^2.0.20",
129129
"nunjucks": "^3.2.3",
130130
"postcss": "^8.2.4",
@@ -142,7 +142,7 @@
142142
"sinon": "^13.0.1",
143143
"standard": "16.0.4",
144144
"ts-node-dev": "^1.1.8",
145-
"typescript": "4.9.5",
145+
"typescript": "^5.x",
146146
"webpack": "^4.44.2",
147147
"webpack-bundle-analyzer": "^4.3.0",
148148
"webpack-cli": "^3.3.12",
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Microsoft Authentication Layer (MSAL) APIM Authentication Support
2+
3+
The `MsalAuthorizer` service supports MSAL authentication to communicate securely with APIM-hosted API services.
4+
5+
## Setup
6+
7+
To use MSAL Authentication you will need to configure the following environment variables:
8+
9+
| Variable name | Definition | Example |
10+
| --------------- | ---------------------------------------------- | ---------------------------------- |
11+
| `APIM_BASE_URL` | The base URL of the APIM hosted service | `https://apim.example.com` |
12+
| `TENANT_ID` | Azure AD tenant ID for MSAL authentication | `tenant-id` |
13+
| `CLIENT_ID` | Azure AD client ID for MSAL authentication | `client-id` |
14+
| `CLIENT_SECRET` | Azure AD client secret for MSAL authentication | `client-secret` |
15+
| `SCOPES` | MSAL scopes required to access the service | `["https://example.com/.default"]` |
16+
17+
## Multi-tenant support
18+
19+
Multi-tenancy is supported for each form IoC registration. Each form registers its own APIM hosted service dependencies with their respective `MsalAuthorizerConfig`, which includes its own MSAL credentials. Through this approach there is no shared state between forms or service instances on a form, so forms on the same deployed server can authenticate against different tenants independently.
20+
21+
## Token lifecycle
22+
23+
Authentication is handled by `MsalAuthorizer`, which uses the MSAL package's `acquireTokenByClientCredential`. MSAL caches tokens internally and reuses them until expiry (or at least near expiry), so there is no network call to the identity provider on every invocation. Token refresh is also handled automatically by MSAL, so no additional retry or refresh logic is needed.
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
# Secure Form Submissions
2+
3+
This feature allows the user to achieve secure form submissions through the addition of security headers
4+
5+
## Form Configuration
6+
7+
The configuration inherits the `MsalAuthorizerConfig`
8+
Refer to the [`MSAL Authentication documentation`](/docs/runner/msal-apim-authentication.md) for information about `tenantId`, `clientId`, `clientSecret` and `scopes`
9+
10+
```json
11+
"secureFormSubmissionConfig": {
12+
"tenantId": "${env_form_submission_tenant_id}",
13+
"clientId": "${env_form_submission_client_id}",
14+
"clientSecret": "${env_form_submission_client_secret}",
15+
"scopes": ["${env_form_submission_scope1}", "${env_form_submission_scope2}"],
16+
"useAwsWafUserAgentWorkaround": boolean
17+
}
18+
```
19+
20+
> **Note: useAwsWafUserAgentWorkaround**
21+
> AWS WAF requires User-Agent to be present as part of auth, setting this option to tru will make the functioality provide one to prevent 403
22+
23+
## How to Update env variables
24+
25+
Environment variables can be added or updated by following the guide at <https://ukhsa.atlassian.net/wiki/spaces/IDT/pages/294420993/XGO-007+Adding+New+Environment+Variables>
26+
27+
## How it works
28+
29+
### The following is a top-down view
30+
31+
The [`FormSecurityService`](/runner/src/server/services/formSecurityService.ts) supports our different security mechanisms.
32+
33+
- For the [`webhookHmacSharedKey`](/runner/src/server/services/formSecurityService.ts#L31) approach it generates Hmac and sets HMAC specific headers.
34+
35+
- For the [`APIM`](/runner/src/server/services/formSecurityService.ts#L49) approach it uses server Dependency Injection to register named instances of [`SecureFormSubmissionService`](/runner/src/server/services/secureFormSubmissionService.ts#L137) which exposes a `getAuthHeader` method that returns an object in the form of `{ Authorization: "Bearer <token>"}`.
36+
Registration into DI is done on [`server initialization`](/runner/src/server/index.ts#L191) for forms declaring the `secureFormSubmissionConfig`
37+
38+
The `StatusService` [`invokes`](/runner/src/server/services/statusService.ts#L137) `FormSecurityService.getSecurityHeaders` method which returns headers based on what approach the form use. It then passes those headers to the `WebhookService`
39+
40+
The `WebhookService` accepts [`additionalHeaders?: Record<string, string>`](/runner/src/server/services/webhookService.ts#L32) as part of it's postRequest method, which it then includes when it makes the request to the form configured webhook output.
41+
42+
### Implementation Details: DI service registration
43+
44+
Per form SecureFormSubmissionService registration
45+
46+
```typescript
47+
for (const form of enginePlugin.options.configs) {
48+
const formId = form.id;
49+
50+
if (form.configuration.secureFormSubmissionConfig) {
51+
const instanceName = getSecureFormSubmissionServiceInstance(formId);
52+
53+
const namedService = new SecureFormSubmissionService(
54+
form.configuration.secureFormSubmissionConfig
55+
);
56+
57+
await server.registerService(
58+
Schmervice.withName(instanceName, namedService)
59+
);
60+
}
61+
}
62+
```
63+
64+
### Implementation Details: DI service resolution
65+
66+
Once registered, the service can be resolved from the DI using the below.
67+
Notice we have to do a dynamic resolution - since services are dynamically registered based on what forms are served, there is no knowledge of what services exists and in turn there's no way to statically type the access.
68+
69+
```typescript
70+
const instanceName = getSecureFormSubmissionServiceInstance(formId);
71+
if (instanceName) {
72+
const services = request.services([])
73+
74+
const serviceInstance = services[instanceName] as SecureFormSubmissionService;
75+
if (serviceInstance) {
76+
const headers = await serviceInstance.getAuthHeader();
77+
...
78+
}
79+
....
80+
}
81+
```

model/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@
5959
"nunjucks": "^3.2.3",
6060
"path": "0.12.7",
6161
"ts-jest": "^29.1.1",
62-
"typescript": "4.9.5",
62+
"typescript": "^5.x",
6363
"wreck": "14.2.0"
6464
}
6565
}

model/src/data-model/types.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,18 @@ export type Analytics = {
196196
matomoUrl: string;
197197
};
198198

199+
export interface MsalAuthorizerConfig {
200+
tenantId: string;
201+
clientId: string;
202+
clientSecret: string;
203+
scopes: string[];
204+
}
205+
206+
export interface SecureFormSubmissionConfig extends MsalAuthorizerConfig {
207+
/* Empty for now */
208+
useAwsWafUserAgentWorkaround?: boolean;
209+
}
210+
199211
/**
200212
* `FormDefinition` is a typescript representation of `Schema`
201213
*/
@@ -232,4 +244,5 @@ export type FormDefinition = {
232244
serviceName?: string | undefined;
233245
confirmationSessionTimeout: number | undefined;
234246
returnTo?: boolean | undefined;
247+
secureFormSubmissionConfig: SecureFormSubmissionConfig;
235248
};

model/src/schema/schema.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -322,6 +322,19 @@ const exitSchema = joi.object().keys({
322322
format: joi.string().allow("STATE", "WEBHOOK"),
323323
});
324324

325+
const msalAuthorizeConfigSchema = joi.object().keys({
326+
tenantId: joi.string().required(),
327+
clientId: joi.string().required(),
328+
clientSecret: joi.string().required(),
329+
scopes: joi.array().items(joi.string()).min(1).required(),
330+
});
331+
332+
const secureFormSubmissionConfig = msalAuthorizeConfigSchema.concat(
333+
joi.object().keys({
334+
useAwsWafUserAgentWorkaround: joi.bool().optional(),
335+
})
336+
);
337+
325338
export const Schema = joi
326339
.object()
327340
.required()
@@ -365,6 +378,7 @@ export const Schema = joi
365378
serviceName: joi.string().optional(),
366379
confirmationSessionTimeout: joi.number().optional(),
367380
returnTo: joi.boolean().optional(),
381+
secureFormSubmissionConfig: secureFormSubmissionConfig.optional(),
368382
});
369383

370384
/**

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,8 +61,8 @@
6161
"lint-staged": "^10.4.2",
6262
"magic-string": "^0.25.7",
6363
"prettier": "2.1.2",
64-
"typedoc": "~0.23.17",
65-
"typescript": "4.9.5"
64+
"typedoc": "~0.28.x",
65+
"typescript": "^5.x"
6666
},
6767
"dependencies": {
6868
"@babel/runtime": "^7.21.0",

queue-model/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
"eslint-plugin-import": "^2.25.4",
3434
"eslint-plugin-tsdoc": "^0.2.14",
3535
"prisma": "6.12.0",
36-
"typescript": "4.9.5"
36+
"typescript": "^5.x"
3737
},
3838
"dependencies": {
3939
"@prisma/client": "6.12.0"

0 commit comments

Comments
 (0)