Skip to content

Commit

Permalink
Merge pull request #1166 from govuk-one-login/pyic-7785
Browse files Browse the repository at this point in the history
PYIC-7785: Add option to use enqueue handler to just fetch oauth state
  • Loading branch information
MikeCollingwood authored Jan 9, 2025
2 parents bb6a376 + 6293ccc commit 02469c4
Show file tree
Hide file tree
Showing 7 changed files with 184 additions and 29 deletions.
71 changes: 69 additions & 2 deletions di-ipv-dcmaw-async-stub/deploy/template.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -243,7 +243,7 @@ Resources:
ParameterName: stubs/core/dcmaw-async/*
- KMSDecryptPolicy:
KeyId: !Ref DynamoDBKmsKey
- DynamoDBCrudPolicy:
- DynamoDBReadPolicy:
TableName: !Ref DcmawAsyncStubUserStateTable
AutoPublishAlias: live
Events:
Expand All @@ -263,6 +263,66 @@ Resources:
EntryPoints:
- src/handlers/managementEnqueueVcHandler.ts

# lambda to stub management DCMAW Async - cleanupDcmawState
ManagementCleanupSessionFunction:
Type: AWS::Serverless::Function
# checkov:skip=CKV_AWS_109: this requires a broad set of permissions
# checkov:skip=CKV_AWS_115: We do not have enough data to allocate the concurrent execution allowance per function.
# checkov:skip=CKV_AWS_116: Lambdas invoked via API Gateway do not support Dead Letter Queues.
# checkov:skip=CKV_AWS_173: doing it later
DependsOn:
- "ManagementCleanupSessionFunctionLogGroup"
Properties:
FunctionName: !Sub "managementCleanupSession-${Environment}"
CodeUri: "../lambdas"
Handler: managementCleanupSessionHandler.handler
Runtime: nodejs20.x
PackageType: Zip
Architectures:
- arm64
MemorySize: 2048
Tracing: Active
CodeSigningConfigArn: !If
- UseCodeSigning
- !Ref CodeSigningConfigArn
- !Ref AWS::NoValue
Environment:
# checkov:skip=CKV_AWS_173: These environment variables do not require encryption.
Variables:
DCMAW_ASYNC_PARAM_BASE_PATH: "/stubs/core/dcmaw-async/"
DCMAW_ASYNC_STUB_USER_STATE_TABLE_NAME: !Ref DcmawAsyncStubUserStateTable
VpcConfig:
SubnetIds:
- Fn::ImportValue: !Sub ${VpcStackName}-ProtectedSubnetIdA
- Fn::ImportValue: !Sub ${VpcStackName}-ProtectedSubnetIdB
SecurityGroupIds:
- !GetAtt DcmawAsyncLambdaSecurityGroup.GroupId
Policies:
- VPCAccessPolicy: { }
- SSMParameterReadPolicy:
ParameterName: stubs/core/dcmaw-async/*
- KMSDecryptPolicy:
KeyId: !Ref DynamoDBKmsKey
- DynamoDBWritePolicy:
TableName: !Ref DcmawAsyncStubUserStateTable
AutoPublishAlias: live
Events:
GetDcmawAsyncVc:
Type: Api
Properties:
RestApiId: !Ref RestApiGateway
Path: /management/cleanupDcmawState
Method: POST
Metadata:
# Manage esbuild properties
BuildMethod: esbuild
BuildProperties:
Minify: true
Target: "es2022"
Sourcemap: true # Enabling source maps will create the required NODE_OPTIONS environment variables on your lambda function during sam build
EntryPoints:
- src/handlers/managementCleanupSessionHandler.ts

# lambda to stub management DCMAW Async - enqueueError
ManagementEnqueueErrorFunction:
Type: AWS::Serverless::Function
Expand Down Expand Up @@ -408,6 +468,13 @@ Resources:
LogGroupName: !Sub "/aws/lambda/ManagementEnqueueVc-${Environment}"
KmsKeyId: !GetAtt LoggingKmsKey.Arn

ManagementCleanupSessionFunctionLogGroup:
Type: AWS::Logs::LogGroup
Properties:
RetentionInDays: 14
LogGroupName: !Sub "/aws/lambda/ManagementCleanupSession-${Environment}"
KmsKeyId: !GetAtt LoggingKmsKey.Arn

ManagementEnqueueErrorFunctionLogGroup:
Type: AWS::Logs::LogGroup
Properties:
Expand Down Expand Up @@ -626,4 +693,4 @@ Outputs:
Description: DCMAW Async API Gateway ID
Export:
Name: !Sub "DcmawAsyncRestApiGatewayID-${Environment}"
Value: !Ref RestApiGateway
Value: !Ref RestApiGateway
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,5 @@ export enum DocumentType {
export enum EvidenceType {
success = "success",
fail = "fail",
failWithCi = "failWithCi",
}
31 changes: 31 additions & 0 deletions di-ipv-dcmaw-async-stub/lambdas/src/domain/mockVc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,20 @@ export async function buildMockVc(

const testUserClaims = {
[TestUser.kennethD]: {
name: [
{
nameParts: [
{
value: "Kenneth",
type: "GivenName",
},
{
value: "Decerqueira",
type: "FamilyName",
},
],
},
],
birthDate: [
{
value: "1965-07-08",
Expand Down Expand Up @@ -99,5 +113,22 @@ const evidence = {
},
],
},
[EvidenceType.failWithCi]: {
type: "IdentityCheck",
strengthScore: 4,
validityScore: 0,
ci: ["D15"],
failedCheckDetails: [
{
checkMethod: "vcrypt",
identityCheckPolicy: "published",
activityFrom: null,
},
{
checkMethod: "bvr",
biometricVerificationProcessLevel: 3,
},
],
},
},
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { APIGatewayProxyEventV2, APIGatewayProxyResultV2 } from "aws-lambda";
import { buildApiResponse } from "../common/apiResponse";
import getErrorMessage from "../common/errorReporting";
import { deleteState } from "../services/userStateService";

export async function handler(
event: APIGatewayProxyEventV2,
): Promise<APIGatewayProxyResultV2> {
try {
if (event.body === undefined) {
return buildApiResponse({ errorMessage: "No request body" }, 400);
}

const requestBody = JSON.parse(event.body);

if (!requestBody.user_id) {
return buildApiResponse(
{ errorMessage: "Missing user_id in request body" },
400,
);
}

await deleteState(requestBody.user_id);

return buildApiResponse(
{
result: "success",
},
200,
);
} catch (error) {
return buildApiResponse(
{ errorMessage: "Unexpected error: " + getErrorMessage(error) },
500,
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import { APIGatewayProxyEventV2, APIGatewayProxyResultV2 } from "aws-lambda";
import { buildApiResponse } from "../common/apiResponse";
import getErrorMessage from "../common/errorReporting";
import { ManagementEnqueueErrorRequest } from "../domain/managementEnqueueRequest";
import { popState } from "../services/userStateService";
import getConfig from "../common/config";
import { getState } from "../services/userStateService";

export async function handler(
event: APIGatewayProxyEventV2,
Expand All @@ -20,7 +20,7 @@ export async function handler(
return buildApiResponse({ errorMessage: requestBody }, 400);
}

const state = await popState(requestBody.user_id);
const state = await getState(requestBody.user_id);

const queueMessage = {
sub: requestBody.user_id,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { buildApiResponse } from "../common/apiResponse";
import getErrorMessage from "../common/errorReporting";
import { buildMockVc } from "../domain/mockVc";
import { ManagementEnqueueVcRequest } from "../domain/managementEnqueueRequest";
import { popState } from "../services/userStateService";
import { getState } from "../services/userStateService";
import getConfig from "../common/config";

export async function handler(
Expand All @@ -17,17 +17,33 @@ export async function handler(
return buildApiResponse({ errorMessage: "No request body" }, 400);
}

const requestBody = parseRequest(event);
if (typeof requestBody === "string") {
return buildApiResponse({ errorMessage: requestBody }, 400);
const requestBody = JSON.parse(event.body);

if (!requestBody.test_user) {
// We do not want to produce a VC, only to return the oauth state to allows API tests to callback as the mobile
// app, which includes it in the callback endpoint as a query parameter.
const state = await getState(requestBody.user_id);
return buildApiResponse(
{
result: "success",
oauthState: state,
},
201,
);
}

const parsedBody = parseRequest(event.body);

if (typeof parsedBody === "string") {
return buildApiResponse({ errorMessage: parsedBody }, 400);
}

const vc = await buildMockVc(
requestBody.user_id,
requestBody.test_user,
requestBody.document_type,
requestBody.evidence_type,
requestBody.ci,
parsedBody.user_id,
parsedBody.test_user,
parsedBody.document_type,
parsedBody.evidence_type,
parsedBody.ci,
);

const signingKey = await importPKCS8(
Expand All @@ -38,10 +54,10 @@ export async function handler(
.setProtectedHeader({ alg: "ES256", typ: "JWT" })
.sign(signingKey);

const state = await popState(requestBody.user_id);
const state = await getState(parsedBody.user_id);

const queueMessage = {
sub: requestBody.user_id,
sub: parsedBody.user_id,
state,
"https://vocab.account.gov.uk/v1/credentialJWT": [signedJwt],
};
Expand All @@ -50,16 +66,17 @@ export async function handler(
method: "POST",
headers: { "x-api-key": config.queueStubApiKey },
body: JSON.stringify({
queueName: requestBody.queue_name ?? config.queueName,
queueName: parsedBody.queue_name ?? config.queueName,
queueEvent: queueMessage,
delaySeconds: requestBody.delay_seconds ?? 0,
delaySeconds: parsedBody.delay_seconds ?? 0,
}),
});

return buildApiResponse(
{
result: "success",
oauthState: state
// Returning state allows API tests to callback as the mobile app.
oauthState: state,
},
201,
);
Expand All @@ -71,14 +88,8 @@ export async function handler(
}
}

function parseRequest(
event: APIGatewayProxyEventV2,
): string | ManagementEnqueueVcRequest {
if (event.body === undefined) {
return "No request body";
}

const requestBody = JSON.parse(event.body);
function parseRequest(body: string): string | ManagementEnqueueVcRequest {
const requestBody = JSON.parse(body);

const mandatoryFields = [
"user_id",
Expand Down
14 changes: 11 additions & 3 deletions di-ipv-dcmaw-async-stub/lambdas/src/services/userStateService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@ export async function persistState(
await dynamoClient.updateItem(updateItemInput);
}

/** Gets state value and deletes record. */
export async function popState(userId: string): Promise<string | null> {
/** Gets state record. */
export async function getState(userId: string): Promise<string | null> {
const getItemInput: GetItemInput = {
TableName: userStateTableName,
Key: marshall({ userId }),
Expand All @@ -48,6 +48,14 @@ export async function popState(userId: string): Promise<string | null> {
if (userStateItem === null) {
throw new Error(`No state record found for user id ${userId}`);
}
await dynamoClient.deleteItem(getItemInput);
return userStateItem.state;
}

/** Deletes state record. */
export async function deleteState(userId: string): Promise<void> {
const getItemInput: GetItemInput = {
TableName: userStateTableName,
Key: marshall({ userId }),
};
await dynamoClient.deleteItem(getItemInput);
}

0 comments on commit 02469c4

Please sign in to comment.