Skip to content

Commit 42f5342

Browse files
authored
Merge branch 'v2' into form-level-analytics
2 parents 94fafad + d242c8c commit 42f5342

8 files changed

Lines changed: 1812 additions & 7 deletions

File tree

model/src/data-model/types.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,4 +222,7 @@ export type FormDefinition = {
222222
toggle?: boolean | string | undefined;
223223
retryTimeoutSeconds?: number | undefined;
224224
analytics?: Analytics;
225+
webhookHmacSharedKey?: string | undefined;
226+
fullStartPage?: string | undefined;
227+
serviceName?: string | undefined;
225228
};

model/src/schema/schema.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -354,6 +354,9 @@ export const Schema = joi
354354
toggleRedirect: joi.string().optional(),
355355
retryTimeoutSeconds: joi.number().optional(),
356356
analytics: analyticsSchema.optional(),
357+
webhookHmacSharedKey: joi.string().optional(),
358+
fullStartPage: joi.string().optional(),
359+
serviceName: joi.string().optional(),
357360
});
358361

359362
/**

runner/src/server/forms/kls-test-form.json

Lines changed: 1743 additions & 0 deletions
Large diffs are not rendered by default.

runner/src/server/plugins/views.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -71,9 +71,9 @@ export default {
7171
appVersion: pkg.version,
7272
assetPath: "/assets",
7373
cookiesPolicy: request?.state?.cookies_policy,
74-
serviceName: capitalize(config.serviceName),
74+
serviceName: capitalize(request.server?.app?.forms?.[request.params?.id]?.def?.serviceName || config.serviceName),
7575
feedbackLink: config.feedbackLink,
76-
pageTitle: config.serviceName + " - GOV.UK",
76+
pageTitle: (request.server?.app?.forms?.[request.params?.id]?.def?.serviceName || config.serviceName) + " - GOV.UK",
7777
analyticsAccount: config.analyticsAccount,
7878
gtmId1: analytics.gtmId1 || "",
7979
gtmId2: analytics.gtmId2 || "",
@@ -83,13 +83,14 @@ export default {
8383
BROWSER_REFRESH_URL: config.browserRefreshUrl,
8484
sessionTimeout: config.sessionTimeout,
8585
skipTimeoutWarning: false,
86-
serviceStartPage: config.serviceStartPage || "#",
86+
serviceStartPage: request.server?.app?.forms?.[request.params?.id]?.def?.fullStartPage || config.serviceName || "#",
8787
privacyPolicyUrl: config.privacyPolicyUrl || "/help/privacy",
8888
phaseTag: config.phaseTag,
8989
navigation: request?.auth.isAuthenticated
9090
? [{ text: "Sign out", href: "/logout" }]
9191
: null,
9292
};
9393
},
94+
9495
},
9596
};

runner/src/server/services/statusService.ts

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { HapiRequest, HapiServer } from "../types";
2+
import { createHmacRaw } from "../utils/hmac";
23
import {
34
CacheService,
45
NotifyService,
@@ -130,6 +131,29 @@ export class StatusService {
130131

131132
let newReference;
132133

134+
/**
135+
* If the OPTIONAL config contains webhookHmacSharedKey, then we send HMAC Auth headers
136+
* This is used to confirm ONLY X-Gov's backend is sending data to our API
137+
* Everyone else will be Rejected
138+
*/
139+
const id = request.params?.id;
140+
const forms = request.server?.app?.forms;
141+
const model = id && forms?.[id];
142+
const hmacKey = model?.def?.webhookHmacSharedKey;
143+
let customSecurityHeaders: Record<string, string> = {};
144+
145+
if (hmacKey) {
146+
const [hmacSignature, requestTime, hmacExpiryTime] = await createHmacRaw(
147+
request.yar.id,
148+
hmacKey
149+
);
150+
customSecurityHeaders = {
151+
"X-Request-ID": request.yar.id.toString(),
152+
"X-HMAC-Signature": hmacSignature.toString(),
153+
"X-HMAC-Time": requestTime.toString(),
154+
};
155+
}
156+
133157
if (callback) {
134158
this.logger.info(
135159
["StatusService", "outputRequests"],
@@ -153,7 +177,8 @@ export class StatusService {
153177
firstWebhook.outputData.url,
154178
{ ...formData },
155179
"POST",
156-
firstWebhook.outputData.sendAdditionalPayMetadata
180+
firstWebhook.outputData.sendAdditionalPayMetadata,
181+
customSecurityHeaders
157182
);
158183
await this.cacheService.mergeState(request, {
159184
reference: newReference,
@@ -178,7 +203,8 @@ export class StatusService {
178203
...formData,
179204
},
180205
"POST",
181-
sendAdditionalPayMetadata
206+
sendAdditionalPayMetadata,
207+
customSecurityHeaders
182208
)
183209
),
184210
];

runner/src/server/services/webhookService.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,20 +27,26 @@ export class WebhookService {
2727
url: string,
2828
data: object,
2929
method: "POST" | "PUT" = "POST",
30-
sendAdditionalPayMetadata: boolean = false
30+
sendAdditionalPayMetadata: boolean = false,
31+
authHeaders?: Record<string, string>
3132
) {
3233
// Commented out due to potential for logging PII
3334
// this.logger.info(
3435
// ["WebhookService", "postRequest body"],
3536
// JSON.stringify(data)
3637
// );
38+
3739
let request = method === "POST" ? post : put;
3840
try {
3941
if (!sendAdditionalPayMetadata) {
4042
delete data?.metadata?.pay;
4143
}
4244
const { payload } = await request(url, {
4345
...DEFAULT_OPTIONS,
46+
headers: {
47+
...DEFAULT_OPTIONS.headers,
48+
...(authHeaders || {}),
49+
},
4450
payload: JSON.stringify(data),
4551
});
4652

runner/src/server/utils/hmac.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,29 @@ export async function createHmac(email: string, hmacKey: string) {
8080
}
8181
}
8282

83+
/**
84+
* Similar to the above but returns raw epoch timestamps,
85+
* making it preferable for cryptographic logic.
86+
* The other function may benefit from refactoring
87+
* to separate display logic from core logic. */
88+
export async function createHmacRaw(message: string, hmacKey: string) {
89+
try {
90+
const currentTimestamp = Math.floor(Date.now() / 1000);
91+
const dataToHash = message + currentTimestamp;
92+
const hmac = crypto
93+
.createHmac("sha256", hmacKey)
94+
.update(dataToHash)
95+
.digest("hex");
96+
97+
const expiryTimestamp = currentTimestamp + TIME_THRESHOLD;
98+
99+
return [hmac, currentTimestamp, expiryTimestamp];
100+
} catch (error) {
101+
console.error("Error creating HMAC (raw):", error);
102+
throw error;
103+
}
104+
}
105+
83106
/**
84107
* Validates an HMAC signature
85108
*/

runner/src/server/views/timeout.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ <h1 class="govuk-heading-l">Your application has timed out</h1>
1414
We have reset your application because you did not do anything for {{ sessionTimeout / 60000 }} minutes. We did this
1515
to keep your information secure.
1616
</p>
17-
<a href="{{ serviceStartPage }}" class="govuk-button">Start application again</a>
17+
<a href="{{ startPage }}" class="govuk-button">Start application again</a>
1818
</div>
1919
</div>
2020
</div>

0 commit comments

Comments
 (0)