Repository: govuk-one-login/mobile-id-check-async
Pull request #469: DCMAW-11475: Add session validation to TxmaEvents Lambda
Author: login: jhumbert-dd
Created: 2025-03-10T09:30:37Z | Merged: 2025-03-21T17:57:31Z

<!-- Include the Jira ticket number in square brackets as prefix, eg `[DCMAW-XXXX] PR Title` -->
​DCMAW-11475

### What changed
- Adds `getSession` method to `DynamoDbAdapter` to retrieve and validate a session. Validation checks the session:
  - exists
  - is in the `ASYNC_BIOMETRIC_TOKEN_ISSUED` state
  - is less than or equal to `60` minutes old.
- Updates existing validation
  - Removes validation from `sessionAttributes` type guards.
  - Adds `validateSession` method to existing Dynamo operations for:
    - `BiometricTokenIssued.ts`
    - `BiometricSessionFinished.ts`
  - Refactors `sessionAttributes` type guards to reduce duplication


### Why did it change
This is part of the work to add new TxMA events to track the billable transactions during a user’s biometric verification journey.




Diff:
diff --git a/backend-api/infra/functions/txmaEvent.yaml b/backend-api/infra/functions/txmaEvent.yaml
index 343a0b5b3..d2b6a62ce 100644
--- a/backend-api/infra/functions/txmaEvent.yaml
+++ b/backend-api/infra/functions/txmaEvent.yaml
@@ -39,10 +39,10 @@ Resources:
       RetentionInDays: 30
       LogGroupName: !Sub /aws/lambda/${AWS::StackName}-txma-event
 
-  AsyncTxmaEventSubscriptionFilter:    
+  AsyncTxmaEventSubscriptionFilter:
     Type: AWS::Logs::SubscriptionFilter
     Properties:
-      DestinationArn: 
+      DestinationArn:
         !FindInMap [CslsConfiguration, !Ref Environment, CSLSEGRESS]
       FilterPattern: ""
       LogGroupName: !Ref AsyncTxmaEventLogGroup
@@ -93,3 +93,10 @@ Resources:
                   - ec2:CreateNetworkInterface
                   - ec2:DeleteNetworkInterface
                 Resource: '*'
+        - PolicyName: AsyncTxmaEventFunctionDynamodbPolicy
+          PolicyDocument:
+            Statement:
+              - Effect: Allow
+                Action:
+                  - "dynamodb:GetItem"
+                Resource: !GetAtt SessionsTable.Arn
diff --git a/backend-api/openApiSpecs/async-public-spec.yaml b/backend-api/openApiSpecs/async-public-spec.yaml
index b8a1d3e71..1d679569e 100644
--- a/backend-api/openApiSpecs/async-public-spec.yaml
+++ b/backend-api/openApiSpecs/async-public-spec.yaml
@@ -595,6 +595,40 @@ paths:
             application/json:
               schema:
                 $ref: '#/components/schemas/WriteTxmaEventResponse'
+        '401':
+          description: Unauthorized
+          headers:
+            Access-Control-Allow-Origin:
+              schema:
+                type: string
+            Cache-Control:
+              schema:
+                type: string
+            Content-Type:
+              schema:
+                type: string
+            Strict-Transport-Security:
+              schema:
+                type: string
+            X-Content-Type-Options:
+              schema:
+                type: string
+            X-Frame-Options:
+              schema:
+                type: string
+            Content-Security-Policy:
+              schema:
+                type: string
+            Feature-Policy:
+              schema:
+                type: string
+            Referrer-Policy:
+              schema:
+                type: string
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/WriteTxmaEventResponse'
         '501':
           description: Not Implemented
           headers:
@@ -649,6 +683,16 @@ paths:
               method.response.header.X-Frame-Options: "'DENY'"
             responseTemplates:
               text/plain: Bad Request
+          "401":
+            statusCode: "401"
+            responseParameters:
+              method.response.header.Cache-Control: "'no-store'"
+              method.response.header.Content-Type: "'application/json'"
+              method.response.header.Strict-Transport-Security: "'max-age=31536000'"
+              method.response.header.X-Content-Type-Options: "'nosniff'"
+              method.response.header.X-Frame-Options: "'DENY'"
+            responseTemplates:
+              text/plain: Unauthorized
           "501":
             statusCode: "501"
             responseParameters:
diff --git a/backend-api/src/functions/adapters/aws/dynamo/dynamoDbAdapter.test.ts b/backend-api/src/functions/adapters/aws/dynamo/dynamoDbAdapter.test.ts
index ec025c07c..3da4a7f9a 100644
--- a/backend-api/src/functions/adapters/aws/dynamo/dynamoDbAdapter.test.ts
+++ b/backend-api/src/functions/adapters/aws/dynamo/dynamoDbAdapter.test.ts
@@ -1,29 +1,38 @@
-import { expect } from "@jest/globals";
-import "dotenv/config";
-import "../../../../../tests/testUtils/matchers";
-import { DynamoDbAdapter } from "./dynamoDbAdapter";
-import { BiometricTokenIssued } from "../../../common/session/updateOperations/BiometricTokenIssued/BiometricTokenIssued";
-import {
-  SessionRegistry,
-  UpdateSessionError,
-  SessionUpdateFailed,
-  SessionUpdated,
-} from "../../../common/session/SessionRegistry";
 import {
   ConditionalCheckFailedException,
   DynamoDBClient,
+  GetItemCommand,
   ReturnValue,
   ReturnValuesOnConditionCheckFailure,
   UpdateItemCommand,
 } from "@aws-sdk/client-dynamodb";
+import { marshall } from "@aws-sdk/util-dynamodb";
+import { expect } from "@jest/globals";
 import { mockClient } from "aws-sdk-client-mock";
-import { errorResult, Result, successResult } from "../../../utils/result";
+import "../../../../../tests/testUtils/matchers";
+import { GetSessionOperation } from "../../../common/session/getOperations/GetSessionOperation";
+import { GetSessionBiometricTokenIssued } from "../../../common/session/getOperations/TxmaEvent/GetSessionBiometricTokenIssued";
+import { SessionState } from "../../../common/session/session";
+import {
+  GetSessionError,
+  GetSessionFailed,
+  SessionRegistry,
+  SessionUpdated,
+  SessionUpdateFailed,
+  UpdateSessionError,
+} from "../../../common/session/SessionRegistry";
+import { BiometricTokenIssued } from "../../../common/session/updateOperations/BiometricTokenIssued/BiometricTokenIssued";
 import { UpdateSessionOperation } from "../../../common/session/updateOperations/UpdateSessionOperation";
-import { marshall } from "@aws-sdk/util-dynamodb";
 import {
+  invalidBiometricTokenIssuedSessionAttributesWrongSessionState,
+  invalidBiometricTokenIssuedSessionAttributeTypes,
+  mockSessionId,
+  NOW_IN_MILLISECONDS,
   validBaseSessionAttributes,
   validBiometricTokenIssuedSessionAttributes,
 } from "../../../testUtils/unitTestData";
+import { errorResult, Result, successResult } from "../../../utils/result";
+import { DynamoDbAdapter } from "./dynamoDbAdapter";
 
 const mockDynamoDbClient = mockClient(DynamoDBClient);
 
@@ -33,10 +42,18 @@ let consoleDebugSpy: jest.SpyInstance;
 
 describe("DynamoDbAdapter", () => {
   beforeEach(() => {
+    jest.useFakeTimers();
+    jest.setSystemTime(NOW_IN_MILLISECONDS);
     sessionRegistry = new DynamoDbAdapter("mock_table_name");
     consoleErrorSpy = jest.spyOn(console, "error");
     consoleDebugSpy = jest.spyOn(console, "debug");
   });
+
+  afterEach(() => {
+    jest.resetAllMocks();
+    jest.useRealTimers();
+  });
+
   describe("updateSession", () => {
     let result: Result<SessionUpdated, SessionUpdateFailed>;
 
@@ -218,10 +235,13 @@ describe("DynamoDbAdapter", () => {
               updateOperation.getDynamoDbExpressionAttributeValues(),
           };
           mockDynamoDbClient
-            .onAnyCommand() // default
-            .rejects("Did not receive expected input")
-            .on(UpdateItemCommand, expectedUpdateItemCommandInput, true) // match to expected input
-            .resolves({ Attributes: {} });
+            .on(UpdateItemCommand, expectedUpdateItemCommandInput) // match to expected input
+            .resolves({
+              Attributes: marshall(
+                invalidBiometricTokenIssuedSessionAttributeTypes,
+              ),
+            });
+
           result = await sessionRegistry.updateSession(
             "mock_session_id",
             updateOperation,
@@ -231,6 +251,8 @@ describe("DynamoDbAdapter", () => {
         it("Logs the failure", () => {
           expect(consoleErrorSpy).toHaveBeenCalledWithLogFields({
             messageCode: "MOBILE_ASYNC_UPDATE_SESSION_UNEXPECTED_FAILURE",
+            error:
+              "Could not parse valid session attributes after successful update command",
             data: {
               updateExpression: updateOperation.getDynamoDbUpdateExpression(),
               conditionExpression:
@@ -293,4 +315,180 @@ describe("DynamoDbAdapter", () => {
       });
     });
   });
+
+  describe("getSession", () => {
+    const getOperation: GetSessionOperation =
+      new GetSessionBiometricTokenIssued();
+    let result: Result<void, GetSessionFailed>;
+
+    describe("On every attempt", () => {
+      beforeEach(async () => {
+        mockDynamoDbClient.on(GetItemCommand).resolves({});
+        await sessionRegistry.getSession(mockSessionId, getOperation);
+      });
+
+      it("Logs the attempt", () => {
+        expect(consoleDebugSpy).toHaveBeenCalledWithLogFields({
+          messageCode: "MOBILE_ASYNC_GET_SESSION_ATTEMPT",
+          data: {
+            sessionId: mockSessionId,
+            getItemCommandInput: getOperation.getDynamoDbGetCommandInput({
+              tableName: "mock_table_name",
+              keyValue: mockSessionId,
+            }),
+          },
+        });
+      });
+    });
+
+    describe("Given there is an unexpected error retrieving the session", () => {
+      beforeEach(async () => {
+        mockDynamoDbClient.on(GetItemCommand).rejects("mock_error");
+        result = await sessionRegistry.getSession(mockSessionId, getOperation);
+      });
+
+      it("Logs the failure", () => {
+        expect(consoleErrorSpy).toHaveBeenCalledWithLogFields({
+          messageCode: "MOBILE_ASYNC_GET_SESSION_UNEXPECTED_FAILURE",
+        });
+
+        const { name, message } = JSON.parse(
+          consoleErrorSpy.mock.calls[0][0],
+        ).error;
+        expect(name).toEqual("Error");
+        expect(message).toEqual("mock_error");
+      });
+
+      it("Returns failure with server error", () => {
+        expect(result).toEqual(
+          errorResult({
+            errorType: GetSessionError.INTERNAL_SERVER_ERROR,
+          }),
+        );
+      });
+    });
+
+    describe("Given the session was not found", () => {
+      beforeEach(async () => {
+        mockDynamoDbClient.on(GetItemCommand).resolves({});
+        result = await sessionRegistry.getSession(mockSessionId, getOperation);
+      });
+
+      it("Logs the failure", () => {
+        expect(consoleErrorSpy).toHaveBeenCalledWithLogFields({
+          messageCode: "MOBILE_ASYNC_GET_SESSION_SESSION_NOT_FOUND",
+        });
+      });
+
+      it("Returns failure with a client error", () => {
+        expect(result).toEqual(
+          errorResult({
+            errorType: GetSessionError.CLIENT_ERROR,
+          }),
+        );
+      });
+    });
+
+    describe("Given the session was found", () => {
+      describe("Given invalid session attribute types were returned in the response", () => {
+        beforeEach(async () => {
+          mockDynamoDbClient.on(GetItemCommand).resolves({
+            Item: marshall(invalidBiometricTokenIssuedSessionAttributeTypes),
+          });
+          result = await sessionRegistry.getSession(
+            mockSessionId,
+            getOperation,
+          );
+        });
+
+        it("Logs the failure", () => {
+          expect(consoleErrorSpy).toHaveBeenCalledWithLogFields({
+            messageCode: "MOBILE_ASYNC_GET_SESSION_UNEXPECTED_FAILURE",
+            error:
+              "Session attributes missing or contains invalid attribute types",
+            sessionAttributes: invalidBiometricTokenIssuedSessionAttributeTypes,
+          });
+        });
+
+        it("Returns failure with a server error", () => {
+          expect(result).toEqual(
+            errorResult({
+              errorType: GetSessionError.INTERNAL_SERVER_ERROR,
+            }),
+          );
+        });
+      });
+
+      describe("Given an invalid session was returned in the response (e.g. session in the wrong state)", () => {
+        const invalidBiometricTokenIssuedSessionAttributesWrongSessionStateItem =
+          {
+            Item: marshall(
+              invalidBiometricTokenIssuedSessionAttributesWrongSessionState,
+            ),
+          };
+
+        beforeEach(async () => {
+          mockDynamoDbClient
+            .on(GetItemCommand)
+            .resolves(
+              invalidBiometricTokenIssuedSessionAttributesWrongSessionStateItem,
+            );
+          result = await sessionRegistry.getSession(
+            mockSessionId,
+            getOperation,
+          );
+        });
+
+        it("Logs the failure", () => {
+          expect(consoleErrorSpy).toHaveBeenCalledWithLogFields({
+            messageCode: "MOBILE_ASYNC_GET_SESSION_SESSION_INVALID",
+            invalidAttributes: [
+              { sessionState: SessionState.AUTH_SESSION_CREATED },
+            ],
+            sessionAttributes:
+              invalidBiometricTokenIssuedSessionAttributesWrongSessionState,
+          });
+        });
+
+        it("Returns failure with a client error", () => {
+          expect(result).toStrictEqual(
+            errorResult({
+              errorType: GetSessionError.CLIENT_ERROR,
+              data: {
+                invalidAttributes: [
+                  { sessionState: SessionState.AUTH_SESSION_CREATED },
+                ],
+                sessionAttributes:
+                  invalidBiometricTokenIssuedSessionAttributesWrongSessionState,
+              },
+            }),
+          );
+        });
+      });
+
+      describe("Given a valid session was returned in the response", () => {
+        beforeEach(async () => {
+          mockDynamoDbClient.on(GetItemCommand).resolves({
+            Item: marshall(validBiometricTokenIssuedSessionAttributes),
+          });
+          result = await sessionRegistry.getSession(
+            mockSessionId,
+            getOperation,
+          );
+        });
+
+        it("Logs the success", () => {
+          expect(consoleDebugSpy).toHaveBeenCalledWithLogFields({
+            messageCode: "MOBILE_ASYNC_GET_SESSION_SUCCESS",
+          });
+        });
+
+        it("Returns success with session", () => {
+          expect(result).toEqual(
+            successResult(validBiometricTokenIssuedSessionAttributes),
+          );
+        });
+      });
+    });
+  });
 });
diff --git a/backend-api/src/functions/adapters/aws/dynamo/dynamoDbAdapter.ts b/backend-api/src/functions/adapters/aws/dynamo/dynamoDbAdapter.ts
index 25e7f2ac4..7c98b1615 100644
--- a/backend-api/src/functions/adapters/aws/dynamo/dynamoDbAdapter.ts
+++ b/backend-api/src/functions/adapters/aws/dynamo/dynamoDbAdapter.ts
@@ -1,6 +1,7 @@
 import {
   ConditionalCheckFailedException,
   DynamoDBClient,
+  GetItemCommand,
   PutItemCommand,
   PutItemCommandInput,
   QueryCommand,
@@ -10,31 +11,42 @@ import {
   ReturnValuesOnConditionCheckFailure,
   UpdateItemCommand,
 } from "@aws-sdk/client-dynamodb";
-import { CreateSessionAttributes } from "../../../services/session/sessionService";
-import { NodeHttpHandler } from "@smithy/node-http-handler";
+import { NodeHttpHandler } from "@aws-sdk/node-http-handler";
 import {
-  marshall,
   NativeAttributeValue,
+  marshall,
   unmarshall,
 } from "@aws-sdk/util-dynamodb";
-import { UpdateSessionOperation } from "../../../common/session/updateOperations/UpdateSessionOperation";
+import { logger } from "../../../common/logging/logger";
+import { LogMessage } from "../../../common/logging/LogMessage";
+import { GetSessionOperation } from "../../../common/session/getOperations/GetSessionOperation";
 import {
-  errorResult,
-  FailureWithValue,
-  Result,
-  successResult,
-} from "../../../utils/result";
+  SessionAttributes,
+  SessionState,
+} from "../../../common/session/session";
 import {
+  GetSessionError,
+  GetSessionErrorSessionNotFound,
+  GetSessionFailed,
+  GetSessionInternalServerError,
+  GetSessionSessionInvalidErrorData,
+  GetSessionValidateSessionErrorData,
+  InvalidSessionAttributeTypes,
   SessionRegistry,
-  SessionUpdated,
   SessionUpdateFailed,
   SessionUpdateFailedInternalServerError,
-  UpdateExpressionDataToLog,
+  SessionUpdated,
+  UpdateOperationDataToLog,
   UpdateSessionError,
 } from "../../../common/session/SessionRegistry";
-import { logger } from "../../../common/logging/logger";
-import { LogMessage } from "../../../common/logging/LogMessage";
-import { SessionState } from "../../../common/session/session";
+import { UpdateSessionOperation } from "../../../common/session/updateOperations/UpdateSessionOperation";
+import { CreateSessionAttributes } from "../../../services/session/sessionService";
+import {
+  FailureWithValue,
+  Result,
+  errorResult,
+  successResult,
+} from "../../../utils/result";
 
 export type DatabaseRecord = Record<string, NativeAttributeValue>;
 
@@ -131,6 +143,64 @@ export class DynamoDbAdapter implements SessionRegistry {
     }
   }
 
+  async getSession(
+    sessionId: string,
+    getOperation: GetSessionOperation,
+  ): Promise<Result<SessionAttributes, GetSessionFailed>> {
+    const getItemCommandInput = getOperation.getDynamoDbGetCommandInput({
+      tableName: this.tableName,
+      keyValue: sessionId,
+    });
+
+    let response;
+    try {
+      logger.debug(LogMessage.GET_SESSION_ATTEMPT, {
+        data: { sessionId, getItemCommandInput },
+      });
+
+      response = await this.dynamoDbClient.send(
+        new GetItemCommand(getItemCommandInput),
+      );
+    } catch (error: unknown) {
+      return this.handleGetSessionInternalServerError({
+        error,
+      });
+    }
+
+    const responseItem = response.Item;
+    if (responseItem == null) {
+      return this.handleGetSessionNotFoundError();
+    }
+
+    const getSessionAttributesResult =
+      getOperation.getSessionAttributesFromDynamoDbItem(responseItem);
+    if (getSessionAttributesResult.isError) {
+      const { sessionAttributes } = getSessionAttributesResult.value;
+      return this.handleGetSessionInternalServerError({
+        error: "Session attributes missing or contains invalid attribute types",
+        sessionAttributes,
+      });
+    }
+    const sessionAttributes = getSessionAttributesResult.value;
+
+    const { sessionState, createdAt } = sessionAttributes;
+    const validateSessionResult = getOperation.validateSession({
+      sessionState,
+      createdAt,
+    });
+
+    if (validateSessionResult.isError) {
+      const { invalidAttributes } = validateSessionResult.value;
+      return this.handleGetSessionInvalidError({
+        invalidAttributes,
+        sessionAttributes,
+      });
+    }
+
+    logger.debug(LogMessage.GET_SESSION_SUCCESS);
+    return successResult(sessionAttributes);
+  }
+
   async updateSession(
     sessionId: string,
     updateOperation: UpdateSessionOperation,
@@ -225,14 +295,56 @@ export class DynamoDbAdapter implements SessionRegistry {
 
   private handleUpdateSessionInternalServerError(
     error: unknown,
-    updateExpressionDataToLog: UpdateExpressionDataToLog,
+    data: UpdateOperationDataToLog,
   ): FailureWithValue<SessionUpdateFailedInternalServerError> {
     logger.error(LogMessage.UPDATE_SESSION_UNEXPECTED_FAILURE, {
-      error: error,
-      data: updateExpressionDataToLog,
+      error,
+      data,
     });
     return errorResult({
       errorType: UpdateSessionError.INTERNAL_SERVER_ERROR,
     });
   }
+
+  private handleGetSessionInternalServerError({
+    error,
+    sessionAttributes,
+  }: {
+    error: unknown;
+    sessionAttributes?: InvalidSessionAttributeTypes;
+  }): FailureWithValue<GetSessionInternalServerError> {
+    logger.error(LogMessage.GET_SESSION_UNEXPECTED_FAILURE, {
+      error,
+      sessionAttributes,
+    });
+    return errorResult({
+      errorType: GetSessionError.INTERNAL_SERVER_ERROR,
+    });
+  }
+
+  private handleGetSessionNotFoundError(): FailureWithValue<GetSessionErrorSessionNotFound> {
+    logger.error(LogMessage.GET_SESSION_SESSION_NOT_FOUND);
+
+    return errorResult({
+      errorType: GetSessionError.CLIENT_ERROR,
+    });
+  }
+
+  private handleGetSessionInvalidError({
+    invalidAttributes,
+    sessionAttributes,
+  }: GetSessionValidateSessionErrorData): FailureWithValue<GetSessionSessionInvalidErrorData> {
+    logger.error(LogMessage.GET_SESSION_SESSION_INVALID, {
+      invalidAttributes,
+      sessionAttributes,
+    });
+
+    return errorResult({
+      errorType: GetSessionError.CLIENT_ERROR,
+      data: {
+        invalidAttributes,
+        sessionAttributes,
+      },
+    });
+  }
 }
diff --git a/backend-api/src/functions/asyncBiometricToken/asyncBiometricTokenHandler.test.ts b/backend-api/src/functions/asyncBiometricToken/asyncBiometricTokenHandler.test.ts
index 3e143e809..bd447feba 100644
--- a/backend-api/src/functions/asyncBiometricToken/asyncBiometricTokenHandler.test.ts
+++ b/backend-api/src/functions/asyncBiometricToken/asyncBiometricTokenHandler.test.ts
@@ -1,26 +1,28 @@
 import { expect } from "@jest/globals";
-import "../../../tests/testUtils/matchers";
-import "dotenv/config";
 import { APIGatewayProxyResult, Context } from "aws-lambda";
+import "../../../tests/testUtils/matchers";
+import { logger } from "../common/logging/logger";
+import {
+  SessionRegistry,
+  UpdateSessionError,
+} from "../common/session/SessionRegistry";
+import { BiometricTokenIssued } from "../common/session/updateOperations/BiometricTokenIssued/BiometricTokenIssued";
 import { buildLambdaContext } from "../testUtils/mockContext";
 import { buildRequest } from "../testUtils/mockRequest";
-import { lambdaHandlerConstructor } from "./asyncBiometricTokenHandler";
-import { IAsyncBiometricTokenDependencies } from "./handlerDependencies";
 import {
   expectedSecurityHeaders,
-  mockSessionId,
-  mockInertSessionRegistry,
   mockInertEventService,
+  mockInertSessionRegistry,
+  mockSessionId,
+  mockSuccessfulEventService,
+  mockWriteBiometricTokenIssuedEventSuccessResult,
+  mockWriteGenericEventSuccessResult,
+  validBiometricTokenIssuedSessionAttributes,
+  validBiometricTokenIssuedSessionAttributesMobileApp,
 } from "../testUtils/unitTestData";
-import { logger } from "../common/logging/logger";
-import {
-  emptyFailure,
-  emptySuccess,
-  errorResult,
-  successResult,
-} from "../utils/result";
-import { UpdateSessionError } from "../common/session/SessionRegistry";
-import { BiometricTokenIssued } from "../common/session/updateOperations/BiometricTokenIssued/BiometricTokenIssued";
+import { emptyFailure, errorResult, successResult } from "../utils/result";
+import { lambdaHandlerConstructor } from "./asyncBiometricTokenHandler";
+import { IAsyncBiometricTokenDependencies } from "./handlerDependencies";
 
 jest.mock("crypto", () => ({
   ...jest.requireActual("crypto"),
@@ -53,43 +55,6 @@ describe("Async Biometric Token", () => {
     .fn()
     .mockResolvedValue(successResult("mockBiometricToken"));
 
-  const validSessionAttributes = {
-    clientId: "mockClientId",
-    govukSigninJourneyId: "mockGovukSigninJourneyId",
-    createdAt: 12345,
-    issuer: "mockIssuer",
-    sessionId: mockSessionId,
-    sessionState: "mockSessionState",
-    clientState: "mockClientState",
-    subjectIdentifier: "mockSubjectIdentifier",
-    timeToLive: 12345,
-    documentType: "NFC_PASSPORT",
-    opaqueId: "mockOpaqueId",
-    redirectUri: "https://www.mockRedirectUri.com",
-  };
-
-  const mockSuccessfulSessionRegistry = {
-    ...mockInertSessionRegistry,
-    updateSession: jest
-      .fn()
-      .mockResolvedValue(successResult({ attributes: validSessionAttributes })),
-  };
-
-  const mockWriteGenericEventSuccessResult = jest
-    .fn()
-    .mockResolvedValue(emptySuccess());
-
-  const mockWriteBiometricTokenIssuedEventSuccessResult = jest
-    .fn()
-    .mockResolvedValue(emptySuccess());
-
-  const mockSuccessfulEventService = {
-    ...mockInertEventService,
-    writeGenericEvent: mockWriteGenericEventSuccessResult,
-    writeBiometricTokenIssuedEvent:
-      mockWriteBiometricTokenIssuedEventSuccessResult,
-  };
-
   beforeEach(() => {
     dependencies = {
       env: {
@@ -105,7 +70,7 @@ describe("Async Biometric Token", () => {
       },
       getSecrets: mockGetSecretsSuccess,
       getBiometricToken: mockGetBiometricTokenSuccess,
-      getSessionRegistry: () => mockSuccessfulSessionRegistry,
+      getSessionRegistry: () => mockBiometricTokenSessionRegistrySuccess,
       getEventService: () => mockSuccessfulEventService,
     };
     context = buildLambdaContext();
@@ -351,6 +316,7 @@ describe("Async Biometric Token", () => {
               context,
             );
           });
+
           it("Logs the error", async () => {
             expect(consoleErrorSpy).toHaveBeenCalledWithLogFields({
               messageCode: "MOBILE_ASYNC_ERROR_WRITING_AUDIT_EVENT",
@@ -404,7 +370,7 @@ describe("Async Biometric Token", () => {
             updateSession: jest.fn().mockResolvedValue(
               errorResult({
                 errorType: UpdateSessionError.CONDITIONAL_CHECK_FAILURE,
-                attributes: validSessionAttributes,
+                attributes: validBiometricTokenIssuedSessionAttributesMobileApp,
               }),
             ),
           });
@@ -595,7 +561,9 @@ describe("Async Biometric Token", () => {
     });
 
     it("Passes correct arguments to update session", () => {
-      expect(mockSuccessfulSessionRegistry.updateSession).toHaveBeenCalledWith(
+      expect(
+        mockBiometricTokenSessionRegistrySuccess.updateSession,
+      ).toHaveBeenCalledWith(
         mockSessionId,
         new BiometricTokenIssued("NFC_PASSPORT", "mock_opaque_id"),
       );
@@ -673,3 +641,17 @@ describe("Async Biometric Token", () => {
     });
   });
 });
+
+const mockBiometricTokenSessionRegistrySuccess: SessionRegistry = {
+  updateSession: jest.fn().mockResolvedValue(
+    successResult({
+      attributes: validBiometricTokenIssuedSessionAttributesMobileApp,
+    }),
+  ),
+
+  getSession: jest.fn().mockResolvedValue(
+    successResult({
+      attributes: validBiometricTokenIssuedSessionAttributes,
+    }),
+  ),
+};
diff --git a/backend-api/src/functions/asyncFinishBiometricSession/asyncFinishBiometricSessionHandler.test.ts b/backend-api/src/functions/asyncFinishBiometricSession/asyncFinishBiometricSessionHandler.test.ts
index 1c64c9c54..58ef1c859 100644
--- a/backend-api/src/functions/asyncFinishBiometricSession/asyncFinishBiometricSessionHandler.test.ts
+++ b/backend-api/src/functions/asyncFinishBiometricSession/asyncFinishBiometricSessionHandler.test.ts
@@ -14,6 +14,7 @@ import {
   mockInertSessionRegistry,
   mockInertEventService,
   validBiometricSessionFinishedAttributes,
+  validBiometricSessionFinishedAttributesMobileApp,
 } from "../testUtils/unitTestData";
 import {
   emptySuccess,
@@ -62,10 +63,7 @@ describe("Async Finish Biometric Session", () => {
 
   const mockSessionUpdateSuccess = jest.fn().mockResolvedValue(
     successResult({
-      attributes: {
-        ...validBiometricSessionFinishedAttributes,
-        redirectUri: "www.mockRedirectUri.com",
-      },
+      attributes: validBiometricSessionFinishedAttributesMobileApp,
     }),
   );
 
@@ -601,7 +599,7 @@ describe("Async Finish Biometric Session", () => {
         govukSigninJourneyId: "mockGovukSigninJourneyId",
         componentId: "mockIssuer",
         transactionId: "f32432a9-0965-4da9-8a2c-a98a79349d4a",
-        redirect_uri: "www.mockRedirectUri.com",
+        redirect_uri: "https://www.mockRedirectUri.com",
         suspected_fraud_signal: undefined,
         getNowInMilliseconds: Date.now,
         ipAddress: "1.1.1.1",
diff --git a/backend-api/src/functions/asyncTxmaEvent/asyncTxmaEventHandler.test.ts b/backend-api/src/functions/asyncTxmaEvent/asyncTxmaEventHandler.test.ts
index 885f6d046..95b26b514 100644
--- a/backend-api/src/functions/asyncTxmaEvent/asyncTxmaEventHandler.test.ts
+++ b/backend-api/src/functions/asyncTxmaEvent/asyncTxmaEventHandler.test.ts
@@ -2,12 +2,21 @@ import { expect } from "@jest/globals";
 import { APIGatewayProxyResult, Context } from "aws-lambda";
 import "../../../tests/testUtils/matchers";
 import { logger } from "../common/logging/logger";
+import {
+  GetSessionError,
+  SessionRegistry,
+} from "../common/session/SessionRegistry";
 import { buildLambdaContext } from "../testUtils/mockContext";
 import { buildRequest } from "../testUtils/mockRequest";
 import {
   expectedSecurityHeaders,
+  mockInertSessionRegistry,
   mockSessionId,
+  mockSuccessfulEventService,
+  NOW_IN_MILLISECONDS,
+  validBiometricTokenIssuedSessionAttributes,
 } from "../testUtils/unitTestData";
+import { errorResult, successResult } from "../utils/result";
 import { lambdaHandlerConstructor } from "./asyncTxmaEventHandler";
 import { IAsyncTxmaEventDependencies } from "./handlerDependencies";
 
@@ -19,14 +28,25 @@ describe("Async TxMA Event", () => {
   let result: APIGatewayProxyResult;
 
   beforeEach(() => {
+    jest.useFakeTimers();
+    jest.setSystemTime(NOW_IN_MILLISECONDS);
     dependencies = {
-      env: {},
+      env: {
+        SESSION_TABLE_NAME: "mockTableName",
+      },
+      getSessionRegistry: () => mockTxmaEventSessionRegistrySuccess,
+      getEventService: () => mockSuccessfulEventService,
     };
     context = buildLambdaContext();
     consoleInfoSpy = jest.spyOn(console, "info");
     consoleErrorSpy = jest.spyOn(console, "error");
   });
 
+  afterEach(() => {
+    jest.useRealTimers();
+    jest.restoreAllMocks();
+  });
+
   describe("On every invocation", () => {
     beforeEach(async () => {
       result = await lambdaHandlerConstructor(
@@ -39,6 +59,7 @@ describe("Async TxMA Event", () => {
     it("Adds context and version to log attributes and logs STARTED message", () => {
       expect(consoleInfoSpy).toHaveBeenCalledWithLogFields({
         messageCode: "MOBILE_ASYNC_TXMA_EVENT_STARTED",
+        functionVersion: "1",
         function_arn: "arn:12345", // example field to verify that context has been added
       });
     });
@@ -56,6 +77,40 @@ describe("Async TxMA Event", () => {
     });
   });
 
+  describe("Config validation", () => {
+    describe.each(["SESSION_TABLE_NAME"])(
+      "Given %s environment variable is missing",
+      (envVar: string) => {
+        beforeEach(async () => {
+          delete dependencies.env[envVar];
+          result = await lambdaHandlerConstructor(
+            dependencies,
+            validRequest,
+            context,
+          );
+        });
+        it("returns 500 Internal server error", () => {
+          expect(result).toStrictEqual({
+            statusCode: 500,
+            body: JSON.stringify({
+              error: "server_error",
+              error_description: "Internal Server Error",
+            }),
+            headers: expectedSecurityHeaders,
+          });
+        });
+        it("logs INVALID_CONFIG", () => {
+          expect(consoleErrorSpy).toHaveBeenCalledWithLogFields({
+            messageCode: "MOBILE_ASYNC_TXMA_EVENT_INVALID_CONFIG",
+            data: {
+              missingEnvironmentVariables: [envVar],
+            },
+          });
+        });
+      },
+    );
+  });
+
   describe("Request body validation", () => {
     describe("Given request body is invalid", () => {
       beforeEach(async () => {
@@ -90,8 +145,101 @@ describe("Async TxMA Event", () => {
     });
   });
 
+  describe("Retrieving a session", () => {
+    describe("Given there is an unexpected error retrieving the session", () => {
+      beforeEach(async () => {
+        dependencies.getSessionRegistry = () => ({
+          ...mockInertSessionRegistry,
+          getSession: jest.fn().mockResolvedValue(
+            errorResult({
+              errorType: GetSessionError.INTERNAL_SERVER_ERROR,
+            }),
+          ),
+        });
+        result = await lambdaHandlerConstructor(
+          dependencies,
+          validRequest,
+          context,
+        );
+      });
+
+      it("Returns 500 Internal server error", async () => {
+        expect(result).toStrictEqual({
+          statusCode: 500,
+          body: JSON.stringify({
+            error: "server_error",
+            error_description: "Internal Server Error",
+          }),
+          headers: expectedSecurityHeaders,
+        });
+      });
+    });
+
+    describe("Given failure is due to client error", () => {
+      describe("Given session was not found", () => {
+        beforeEach(async () => {
+          dependencies.getSessionRegistry = () => ({
+            ...mockInertSessionRegistry,
+            getSession: jest.fn().mockResolvedValue(
+              errorResult({
+                errorType: GetSessionError.CLIENT_ERROR,
+              }),
+            ),
+          });
+          result = await lambdaHandlerConstructor(
+            dependencies,
+            validRequest,
+            context,
+          );
+        });
+
+        it("Returns 401 Unauthorized", () => {
+          expect(result).toStrictEqual({
+            statusCode: 401,
+            body: JSON.stringify({
+              error: "invalid_session",
+              error_description: "Session does not exist or in incorrect state",
+            }),
+            headers: expectedSecurityHeaders,
+          });
+        });
+      });
+
+      describe("Given the session is invalid", () => {
+        beforeEach(async () => {
+          dependencies.getSessionRegistry = () => ({
+            ...mockInertSessionRegistry,
+            getSession: jest.fn().mockResolvedValue(
+              errorResult({
+                errorType: GetSessionError.CLIENT_ERROR,
+              }),
+            ),
+          });
+          result = await lambdaHandlerConstructor(
+            dependencies,
+            validRequest,
+            context,
+          );
+        });
+
+        it("Returns 401 Unauthorized", () => {
+          expect(result).toStrictEqual({
+            statusCode: 401,
+            body: JSON.stringify({
+              error: "invalid_session",
+              error_description: "Session does not exist or in incorrect state",
+            }),
+            headers: expectedSecurityHeaders,
+          });
+        });
+      });
+    });
+  });
+
   describe("Given a valid request is made", () => {
     beforeEach(async () => {
+      dependencies.getSessionRegistry = () =>
+        mockTxmaEventSessionRegistrySuccess;
       result = await lambdaHandlerConstructor(
         dependencies,
         validRequest,
@@ -121,3 +269,12 @@ const validRequest = buildRequest({
     eventName: "DCMAW_ASYNC_HYBRID_BILLING_STARTED",
   }),
 });
+
+export const mockTxmaEventSessionRegistrySuccess: SessionRegistry = {
+  ...mockInertSessionRegistry,
+  getSession: jest.fn().mockResolvedValue(
+    successResult({
+      attributes: validBiometricTokenIssuedSessionAttributes,
+    }),
+  ),
+};
diff --git a/backend-api/src/functions/asyncTxmaEvent/asyncTxmaEventHandler.ts b/backend-api/src/functions/asyncTxmaEvent/asyncTxmaEventHandler.ts
index 97bb3736f..ba006c491 100644
--- a/backend-api/src/functions/asyncTxmaEvent/asyncTxmaEventHandler.ts
+++ b/backend-api/src/functions/asyncTxmaEvent/asyncTxmaEventHandler.ts
@@ -6,24 +6,38 @@ import {
 import {
   badRequestResponse,
   notImplementedResponse,
+  serverErrorResponse,
+  unauthorizedResponse,
 } from "../common/lambdaResponses";
 import { logger } from "../common/logging/logger";
 import { LogMessage } from "../common/logging/LogMessage";
 import { setupLogger } from "../common/logging/setupLogger";
+import { GetSessionBiometricTokenIssued } from "../common/session/getOperations/TxmaEvent/GetSessionBiometricTokenIssued";
+import {
+  GetSessionError,
+  GetSessionFailed,
+} from "../common/session/SessionRegistry";
 import {
   IAsyncTxmaEventDependencies,
   runtimeDependencies,
 } from "./handlerDependencies";
+import { getTxmaEventConfig } from "./txmaEventConfig";
 import { validateRequestBody } from "./validateRequestBody/validateRequestBody";
 
 export async function lambdaHandlerConstructor(
-  _dependencies: IAsyncTxmaEventDependencies,
+  dependencies: IAsyncTxmaEventDependencies,
   event: APIGatewayProxyEvent,
   context: Context,
 ): Promise<APIGatewayProxyResult> {
   setupLogger(context);
   logger.info(LogMessage.TXMA_EVENT_STARTED);
 
+  const configResult = getTxmaEventConfig(dependencies.env);
+  if (configResult.isError) {
+    return serverErrorResponse;
+  }
+  const config = configResult.value;
+
   const validateRequestBodyResult = validateRequestBody(event.body);
   if (validateRequestBodyResult.isError) {
     const { errorMessage } = validateRequestBodyResult.value;
@@ -32,9 +46,22 @@ export async function lambdaHandlerConstructor(
     });
     return badRequestResponse("invalid_request", errorMessage);
   }
+  const { sessionId } = validateRequestBodyResult.value;
 
-  logger.info(LogMessage.TXMA_EVENT_COMPLETED);
+  const sessionRegistry = dependencies.getSessionRegistry(
+    config.SESSION_TABLE_NAME,
+  );
+  const getSessionResult = await sessionRegistry.getSession(
+    sessionId,
+    new GetSessionBiometricTokenIssued(),
+  );
+  if (getSessionResult.isError) {
+    return handleGetSessionError({
+      errorData: getSessionResult.value,
+    });
+  }
 
+  logger.info(LogMessage.TXMA_EVENT_COMPLETED);
   return notImplementedResponse;
 }
 
@@ -42,3 +69,18 @@ export const lambdaHandler = lambdaHandlerConstructor.bind(
   null,
   runtimeDependencies,
 );
+
+async function handleGetSessionError({
+  errorData,
+}: {
+  errorData: GetSessionFailed;
+}): Promise<APIGatewayProxyResult> {
+  if (errorData.errorType === GetSessionError.CLIENT_ERROR) {
+    return unauthorizedResponse(
+      "invalid_session",
+      "Session does not exist or in incorrect state",
+    );
+  }
+
+  return serverErrorResponse;
+}
diff --git a/backend-api/src/functions/asyncTxmaEvent/handlerDependencies.ts b/backend-api/src/functions/asyncTxmaEvent/handlerDependencies.ts
index 1c9bc5dcf..b812fc91e 100644
--- a/backend-api/src/functions/asyncTxmaEvent/handlerDependencies.ts
+++ b/backend-api/src/functions/asyncTxmaEvent/handlerDependencies.ts
@@ -1,7 +1,16 @@
+import { DynamoDbAdapter } from "../adapters/aws/dynamo/dynamoDbAdapter";
+import { SessionRegistry } from "../common/session/SessionRegistry";
+import { EventService } from "../services/events/eventService";
+import { IEventService } from "../services/events/types";
+
 export type IAsyncTxmaEventDependencies = {
   env: NodeJS.ProcessEnv;
+  getSessionRegistry: (tableName: string) => SessionRegistry;
+  getEventService: (sqsQueue: string) => IEventService;
 };
 
 export const runtimeDependencies: IAsyncTxmaEventDependencies = {
   env: process.env,
+  getSessionRegistry: (tableName: string) => new DynamoDbAdapter(tableName),
+  getEventService: (sqsQueue: string) => new EventService(sqsQueue),
 };
diff --git a/backend-api/src/functions/asyncTxmaEvent/txmaEventConfig.ts b/backend-api/src/functions/asyncTxmaEvent/txmaEventConfig.ts
new file mode 100644
index 000000000..e3e707dc0
--- /dev/null
+++ b/backend-api/src/functions/asyncTxmaEvent/txmaEventConfig.ts
@@ -0,0 +1,29 @@
+import {
+  Config,
+  getRequiredEnvironmentVariables,
+  MissingEnvVarError,
+} from "../common/config/environment";
+import { logger } from "../common/logging/logger";
+import { LogMessage } from "../common/logging/LogMessage";
+import { Result } from "../utils/result";
+
+const REQUIRED_ENVIRONMENT_VARIABLES = ["SESSION_TABLE_NAME"] as const;
+
+export type TxmaEventConfig = Config<
+  (typeof REQUIRED_ENVIRONMENT_VARIABLES)[number]
+>;
+
+export function getTxmaEventConfig(
+  env: NodeJS.ProcessEnv,
+): Result<TxmaEventConfig, MissingEnvVarError> {
+  const envVarsResult = getRequiredEnvironmentVariables(
+    env,
+    REQUIRED_ENVIRONMENT_VARIABLES,
+  );
+  if (envVarsResult.isError) {
+    logger.error(LogMessage.TXMA_EVENT_INVALID_CONFIG, {
+      data: { missingEnvironmentVariables: envVarsResult.value.missingEnvVars },
+    });
+  }
+  return envVarsResult;
+}
diff --git a/backend-api/src/functions/common/logging/LogMessage.ts b/backend-api/src/functions/common/logging/LogMessage.ts
index 2284763b1..5887d55aa 100644
--- a/backend-api/src/functions/common/logging/LogMessage.ts
+++ b/backend-api/src/functions/common/logging/LogMessage.ts
@@ -37,6 +37,31 @@ export class LogMessage implements LogAttributes {
     "Error occurred while attempting to retrieve active session by subject ID.",
   );
 
+  static readonly GET_SESSION_ATTEMPT = new LogMessage(
+    "MOBILE_ASYNC_GET_SESSION_ATTEMPT",
+    "Attempting to retrieve session by session ID from DynamoDB.",
+  );
+
+  static readonly GET_SESSION_UNEXPECTED_FAILURE = new LogMessage(
+    "MOBILE_ASYNC_GET_SESSION_UNEXPECTED_FAILURE",
+    "An unexpected failure occurred while trying to retrieve the user session from DynamoDB.",
+  );
+
+  static readonly GET_SESSION_SESSION_NOT_FOUND = new LogMessage(
+    "MOBILE_ASYNC_GET_SESSION_SESSION_NOT_FOUND",
+    "No session found when attempting to get the user session from DynamoDB.",
+  );
+
+  static readonly GET_SESSION_SESSION_INVALID = new LogMessage(
+    "MOBILE_ASYNC_GET_SESSION_SESSION_INVALID",
+    "The session retrieved from DynamoDB is not valid.",
+  );
+
+  static readonly GET_SESSION_SUCCESS = new LogMessage(
+    "MOBILE_ASYNC_GET_SESSION_SUCCESS",
+    "Successfully retrieved user session from DynamoDB.",
+  );
+
   static readonly UPDATE_SESSION_ATTEMPT = new LogMessage(
     "MOBILE_ASYNC_UPDATE_SESSION_ATTEMPT",
     "Attempting to update user session in DynamoDB.",
@@ -256,10 +281,18 @@ export class LogMessage implements LogAttributes {
     "MOBILE_ASYNC_TXMA_EVENT_STARTED",
     "Lambda handler processing has started.",
   );
+  static readonly TXMA_EVENT_INVALID_CONFIG = new LogMessage(
+    "MOBILE_ASYNC_TXMA_EVENT_INVALID_CONFIG",
+    "One or more required environment variables were missing or invalid.",
+  );
   static readonly TXMA_EVENT_REQUEST_BODY_INVALID = new LogMessage(
     "MOBILE_ASYNC_TXMA_EVENT_REQUEST_BODY_INVALID",
     "The incoming request body was missing or invalid.",
   );
+  static readonly TXMA_EVENT_SESSION_INVALID = new LogMessage(
+    "MOBILE_ASYNC_TXMA_EVENT_SESSION_INVALID",
+    "The retrieved session is not valid",
+  );
   static readonly TXMA_EVENT_COMPLETED = new LogMessage(
     "MOBILE_ASYNC_TXMA_EVENT_COMPLETED",
     "Lambda handler processing has completed successfully.",
diff --git a/backend-api/src/functions/common/session/SessionRegistry.ts b/backend-api/src/functions/common/session/SessionRegistry.ts
index 7bf224d7a..cc9a2c024 100644
--- a/backend-api/src/functions/common/session/SessionRegistry.ts
+++ b/backend-api/src/functions/common/session/SessionRegistry.ts
@@ -1,23 +1,38 @@
-import { SessionAttributes } from "./session";
 import { Result } from "../../utils/result";
+import { GetSessionOperation } from "./getOperations/GetSessionOperation";
+import { SessionAttributes, SessionState } from "./session";
 import { UpdateSessionOperation } from "./updateOperations/UpdateSessionOperation";
+
 export interface SessionRegistry {
   updateSession(
     sessionId: string,
     updateOperation: UpdateSessionOperation,
   ): Promise<Result<SessionUpdated, SessionUpdateFailed>>;
+
+  getSession(
+    sessionId: string,
+    getOperation: GetSessionOperation,
+  ): Promise<Result<SessionAttributes, GetSessionFailed>>;
 }
 
+// Update session
+
 export interface SessionUpdated {
   attributes: SessionAttributes;
 }
 
+export enum UpdateSessionError {
+  INTERNAL_SERVER_ERROR = "INTERNAL_SERVER_ERROR",
+  CONDITIONAL_CHECK_FAILURE = "CONDITIONAL_CHECK_FAILURE",
+  SESSION_NOT_FOUND = "SESSION_NOT_FOUND",
+}
+
 export type SessionUpdateFailed =
   | SessionUpdateFailedConditionalCheckFailure
   | SessionUpdateFailedInternalServerError
   | SessionUpdateFailedSessionNotFound;
 
-export interface SessionUpdateFailedConditionalCheckFailure {
+interface SessionUpdateFailedConditionalCheckFailure {
   errorType: UpdateSessionError.CONDITIONAL_CHECK_FAILURE;
   attributes: SessionAttributes;
 }
@@ -26,17 +41,90 @@ export interface SessionUpdateFailedInternalServerError {
   errorType: UpdateSessionError.INTERNAL_SERVER_ERROR;
 }
 
-export interface SessionUpdateFailedSessionNotFound {
+interface SessionUpdateFailedSessionNotFound {
   errorType: UpdateSessionError.SESSION_NOT_FOUND;
 }
 
-export enum UpdateSessionError {
-  INTERNAL_SERVER_ERROR = "INTERNAL_SERVER_ERROR",
-  CONDITIONAL_CHECK_FAILURE = "CONDITIONAL_CHECK_FAILURE",
-  SESSION_NOT_FOUND = "SESSION_NOT_FOUND",
-}
+export type UpdateOperationDataToLog =
+  | UpdateExpressionDataToLog
+  | UpdateSessionValidateSessionErrorData;
 
-export interface UpdateExpressionDataToLog {
+interface UpdateExpressionDataToLog {
   updateExpression: string;
   conditionExpression: string | undefined;
 }
+
+export interface UpdateSessionValidateSessionErrorData {
+  invalidAttribute:
+    | UpdateSessionInvalidSessionAttribute
+    | BiometricSessionFinishedInvalidSessionAttribute;
+}
+
+interface UpdateSessionInvalidSessionAttribute {
+  sessionState?: Exclude<SessionState, SessionState.BIOMETRIC_TOKEN_ISSUED>;
+  createdAt?: number;
+}
+
+export interface BiometricSessionFinishedValidateSessionErrorData {
+  invalidAttribute: BiometricSessionFinishedInvalidSessionAttribute;
+}
+
+interface BiometricSessionFinishedInvalidSessionAttribute {
+  sessionState?: Exclude<SessionState, SessionState.BIOMETRIC_SESSION_FINISHED>;
+  createdAt?: number;
+}
+
+// Get session
+
+export enum GetSessionError {
+  INTERNAL_SERVER_ERROR = "INTERNAL_SERVER_ERROR",
+  CLIENT_ERROR = "CLIENT_ERROR",
+}
+
+export type GetSessionFailed =
+  | GetSessionInternalServerError
+  | GetSessionClientError;
+
+export interface GetSessionInternalServerError {
+  errorType: GetSessionError.INTERNAL_SERVER_ERROR;
+}
+
+export interface GetSessionClientError {
+  errorType: GetSessionError.CLIENT_ERROR;
+}
+
+export interface GetSessionErrorSessionNotFound {
+  errorType: GetSessionError.CLIENT_ERROR;
+}
+
+export interface GetSessionSessionInvalidErrorData {
+  errorType: GetSessionError.CLIENT_ERROR;
+  data: GetSessionValidateSessionErrorData;
+}
+
+export interface GetSessionValidateSessionErrorData {
+  invalidAttributes?: ValidateSessionInvalidAttributes[];
+  sessionAttributes?: InvalidSessionAttributeTypes;
+}
+
+export interface ValidateSessionErrorInvalidAttributesData {
+  invalidAttributes: ValidateSessionInvalidAttributes[];
+}
+
+export interface ValidateSessionErrorInvalidAttributeTypeData {
+  sessionAttributes: InvalidSessionAttributeTypes;
+}
+
+export interface ValidateSessionInvalidAttributes {
+  sessionState?: Exclude<SessionState, SessionState.BIOMETRIC_TOKEN_ISSUED>;
+  createdAt?: number;
+}
+
+export type InvalidSessionAttributeTypes = Partial<SessionAttributes>;
+
+// Shared
+
+export interface ValidateSessionAttributes {
+  sessionState: SessionState;
+  createdAt: number;
+}
diff --git a/backend-api/src/functions/common/session/getOperations/GetSessionOperation.ts b/backend-api/src/functions/common/session/getOperations/GetSessionOperation.ts
new file mode 100644
index 000000000..ce1be83c9
--- /dev/null
+++ b/backend-api/src/functions/common/session/getOperations/GetSessionOperation.ts
@@ -0,0 +1,26 @@
+import { AttributeValue, GetItemCommandInput } from "@aws-sdk/client-dynamodb";
+import { Result } from "../../../utils/result";
+import { SessionAttributes } from "../session";
+import {
+  ValidateSessionAttributes,
+  ValidateSessionErrorInvalidAttributesData,
+  ValidateSessionErrorInvalidAttributeTypeData,
+} from "../SessionRegistry";
+
+export interface GetSessionOperation {
+  getDynamoDbGetCommandInput({
+    tableName,
+    keyValue,
+  }: {
+    tableName: string;
+    keyValue: string;
+  }): GetItemCommandInput;
+
+  getSessionAttributesFromDynamoDbItem(
+    item: Record<string, AttributeValue> | undefined,
+  ): Result<SessionAttributes, ValidateSessionErrorInvalidAttributeTypeData>;
+
+  validateSession(
+    attributes: ValidateSessionAttributes,
+  ): Result<void, ValidateSessionErrorInvalidAttributesData>;
+}
diff --git a/backend-api/src/functions/common/session/getOperations/TxmaEvent/GetSessionBiometricTokenIssued.test.ts b/backend-api/src/functions/common/session/getOperations/TxmaEvent/GetSessionBiometricTokenIssued.test.ts
new file mode 100644
index 000000000..97c972688
--- /dev/null
+++ b/backend-api/src/functions/common/session/getOperations/TxmaEvent/GetSessionBiometricTokenIssued.test.ts
@@ -0,0 +1,157 @@
+import { marshall, unmarshall } from "@aws-sdk/util-dynamodb";
+import {
+  invalidCreatedAt,
+  mockSessionId,
+  NOW_IN_MILLISECONDS,
+  validBiometricTokenIssuedSessionAttributes,
+  validCreatedAt,
+} from "../../../../testUtils/unitTestData";
+import {
+  emptySuccess,
+  errorResult,
+  successResult,
+} from "../../../../utils/result";
+import { SessionState } from "../../session";
+import { GetSessionBiometricTokenIssued } from "./GetSessionBiometricTokenIssued";
+
+describe("TxMA event get session operation", () => {
+  let getSessionOperation: GetSessionBiometricTokenIssued;
+
+  beforeEach(() => {
+    jest.useFakeTimers();
+    jest.setSystemTime(NOW_IN_MILLISECONDS);
+    getSessionOperation = new GetSessionBiometricTokenIssued();
+  });
+
+  describe("When I request the DynamoDB command input", () => {
+    it("Returns the appropriate GetItemCommand input", () => {
+      const result = getSessionOperation.getDynamoDbGetCommandInput({
+        tableName: "mock_table_name",
+        keyValue: mockSessionId,
+      });
+      expect(result).toEqual({
+        TableName: "mock_table_name",
+        Key: { sessionId: { S: mockSessionId } },
+      });
+    });
+  });
+
+  describe("When I request the getSessionAttributesFromDynamoDbItem", () => {
+    describe("Given a session attributes item was provided that does not include all BiometricTokenIssuedSessionAttributes properties", () => {
+      it("Returns an emptyFailure", () => {
+        const result = getSessionOperation.getSessionAttributesFromDynamoDbItem(
+          marshall({
+            sessionId: mockSessionId,
+            sessionState: SessionState.BIOMETRIC_TOKEN_ISSUED,
+          }),
+        );
+
+        expect(result).toEqual(
+          errorResult({
+            sessionAttributes: {
+              sessionId: mockSessionId,
+              sessionState: SessionState.BIOMETRIC_TOKEN_ISSUED,
+            },
+          }),
+        );
+      });
+    });
+
+    describe("Given a valid BiometricTokenIssuedSessionAttributes item was provided", () => {
+      const validBiometricTokenIssuedSessionAttributesItem = marshall(
+        validBiometricTokenIssuedSessionAttributes,
+      );
+
+      it("Returns successResult with BiometricTokenIssuedSessionAttributes session attributes", () => {
+        const result = getSessionOperation.getSessionAttributesFromDynamoDbItem(
+          marshall(validBiometricTokenIssuedSessionAttributes),
+        );
+
+        expect(result).toEqual(
+          successResult(
+            unmarshall(validBiometricTokenIssuedSessionAttributesItem),
+          ),
+        );
+      });
+    });
+  });
+
+  describe("Session attribute validation", () => {
+    describe("Given the session is in the wrong state - sessionState = AUTH_SESSION_CREATED", () => {
+      const invalidSessionAttributesWrongSessionState = {
+        createdAt: validCreatedAt,
+        sessionState: SessionState.AUTH_SESSION_CREATED,
+      };
+
+      it("Returns an error result with the invalid attribute", () => {
+        const result = getSessionOperation.validateSession(
+          invalidSessionAttributesWrongSessionState,
+        );
+
+        expect(result).toStrictEqual(
+          errorResult({
+            invalidAttributes: [
+              { sessionState: SessionState.AUTH_SESSION_CREATED },
+            ],
+          }),
+        );
+      });
+    });
+
+    describe("Given the session is too old", () => {
+      const invalidSessionAttributesSessionTooOld = {
+        sessionState: SessionState.BIOMETRIC_TOKEN_ISSUED,
+        createdAt: invalidCreatedAt,
+      };
+
+      it("Returns an error result with the invalid attribute", () => {
+        const result = getSessionOperation.validateSession(
+          invalidSessionAttributesSessionTooOld,
+        );
+
+        expect(result).toStrictEqual(
+          errorResult({
+            invalidAttributes: [{ createdAt: invalidCreatedAt }],
+          }),
+        );
+      });
+    });
+
+    describe("Given the session has multiple invalid attributes", () => {
+      const invalidSessionAttributesSessionTooOld = {
+        sessionState: SessionState.AUTH_SESSION_CREATED,
+        createdAt: invalidCreatedAt,
+      };
+
+      it("Returns an error result with all invalid attributes", () => {
+        const result = getSessionOperation.validateSession(
+          invalidSessionAttributesSessionTooOld,
+        );
+
+        expect(result).toStrictEqual(
+          errorResult({
+            invalidAttributes: [
+              { sessionState: SessionState.AUTH_SESSION_CREATED },
+              { createdAt: invalidCreatedAt },
+            ],
+          }),
+        );
+      });
+    });
+
+    describe("Given the session is valid", () => {
+      const validSessionAttributes = {
+        sessionState: SessionState.BIOMETRIC_TOKEN_ISSUED,
+        createdAt: validBiometricTokenIssuedSessionAttributes.createdAt,
+      };
+
+      it("Returns an empty success result", () => {
+        const result = getSessionOperation.validateSession(
+          validSessionAttributes,
+        );
+
+        expect(result).toEqual(emptySuccess());
+      });
+    });
+  });
+});
diff --git a/backend-api/src/functions/common/session/getOperations/TxmaEvent/GetSessionBiometricTokenIssued.ts b/backend-api/src/functions/common/session/getOperations/TxmaEvent/GetSessionBiometricTokenIssued.ts
new file mode 100644
index 000000000..92514e037
--- /dev/null
+++ b/backend-api/src/functions/common/session/getOperations/TxmaEvent/GetSessionBiometricTokenIssued.ts
@@ -0,0 +1,53 @@
+import { AttributeValue, GetItemCommandInput } from "@aws-sdk/client-dynamodb";
+import { marshall } from "@aws-sdk/util-dynamodb";
+import { emptySuccess, errorResult, Result } from "../../../../utils/result";
+import { isOlderThan60Minutes } from "../../../../utils/utils";
+import { SessionAttributes, SessionState } from "../../session";
+import { getTxmaEventBiometricTokenIssuedSessionAttributes } from "../../sessionAttributes/sessionAttributes";
+import {
+  ValidateSessionAttributes,
+  ValidateSessionErrorInvalidAttributesData,
+  ValidateSessionErrorInvalidAttributeTypeData,
+  ValidateSessionInvalidAttributes,
+} from "../../SessionRegistry";
+import { GetSessionOperation } from "../GetSessionOperation";
+
+export class GetSessionBiometricTokenIssued implements GetSessionOperation {
+  getDynamoDbGetCommandInput({
+    tableName,
+    keyValue,
+  }: {
+    tableName: string;
+    keyValue: string;
+  }): GetItemCommandInput {
+    return {
+      TableName: tableName,
+      Key: { sessionId: marshall(keyValue) },
+    };
+  }
+
+  getSessionAttributesFromDynamoDbItem(
+    item: Record<string, AttributeValue>,
+  ): Result<SessionAttributes, ValidateSessionErrorInvalidAttributeTypeData> {
+    return getTxmaEventBiometricTokenIssuedSessionAttributes(item);
+  }
+
+  validateSession(
+    attributes: ValidateSessionAttributes,
+  ): Result<void, ValidateSessionErrorInvalidAttributesData> {
+    const { sessionState, createdAt } = attributes;
+    const invalidAttributes: ValidateSessionInvalidAttributes[] = [];
+
+    if (!sessionState || sessionState !== SessionState.BIOMETRIC_TOKEN_ISSUED) {
+      invalidAttributes.push({ sessionState });
+    }
+
+    if (!createdAt || isOlderThan60Minutes(createdAt)) {
+      invalidAttributes.push({ createdAt });
+    }
+
+    if (invalidAttributes.length > 0) return errorResult({ invalidAttributes });
+
+    return emptySuccess();
+  }
+}
diff --git a/backend-api/src/functions/common/session/session.ts b/backend-api/src/functions/common/session/session.ts
index 067c4c3b3..cd01ffb4b 100644
--- a/backend-api/src/functions/common/session/session.ts
+++ b/backend-api/src/functions/common/session/session.ts
@@ -13,12 +13,12 @@ export type SessionAttributes =
 
 export interface BaseSessionAttributes {
   clientId: string;
-  govukSigninJourneyId: string;
+  clientState: string;
   createdAt: number;
+  govukSigninJourneyId: string;
   issuer: string;
   sessionId: string;
   sessionState: SessionState;
-  clientState: string;
   subjectIdentifier: string;
   timeToLive: number;
   redirectUri?: string;
@@ -26,15 +26,15 @@ export interface BaseSessionAttributes {
 
 export interface BiometricTokenIssuedSessionAttributes
   extends BaseSessionAttributes {
-  sessionState: SessionState.BIOMETRIC_TOKEN_ISSUED;
   documentType: DocumentType;
   opaqueId: string;
+  sessionState: SessionState.BIOMETRIC_TOKEN_ISSUED;
 }
 
 export interface BiometricSessionFinishedAttributes
   extends BaseSessionAttributes {
-  sessionState: SessionState.BIOMETRIC_SESSION_FINISHED;
   documentType: DocumentType;
   opaqueId: string;
+  sessionState: SessionState.BIOMETRIC_SESSION_FINISHED;
   biometricSessionId: string;
 }
diff --git a/backend-api/src/functions/common/session/updateOperations/sessionAttributes/sessionAttributes.test.ts b/backend-api/src/functions/common/session/sessionAttributes/sessionAttributes.test.ts
similarity index 93%
rename from backend-api/src/functions/common/session/updateOperations/sessionAttributes/sessionAttributes.test.ts
rename to backend-api/src/functions/common/session/sessionAttributes/sessionAttributes.test.ts
index 8d632a7e9..ef89e8b50 100644
--- a/backend-api/src/functions/common/session/updateOperations/sessionAttributes/sessionAttributes.test.ts
+++ b/backend-api/src/functions/common/session/sessionAttributes/sessionAttributes.test.ts
@@ -1,19 +1,25 @@
+import { AttributeValue } from "@aws-sdk/client-dynamodb";
 import { marshall, unmarshall } from "@aws-sdk/util-dynamodb";
+import {
+  NOW_IN_MILLISECONDS,
+  validBaseSessionAttributes,
+  validBiometricSessionFinishedAttributes,
+  validBiometricTokenIssuedSessionAttributes,
+} from "../../../testUtils/unitTestData";
+import { emptyFailure, successResult } from "../../../utils/result";
+import { SessionAttributes } from "../session";
 import {
   getBaseSessionAttributes,
   getBiometricSessionFinishedSessionAttributes,
   getBiometricTokenIssuedSessionAttributes,
 } from "./sessionAttributes";
-import { emptyFailure, successResult } from "../../../../utils/result";
-import { SessionAttributes, SessionState } from "../../session";
-import { AttributeValue } from "@aws-sdk/client-dynamodb";
-import {
-  validBaseSessionAttributes,
-  validBiometricSessionFinishedAttributes,
-  validBiometricTokenIssuedSessionAttributes,
-} from "../../../../testUtils/unitTestData";
 
 describe("Session attributes", () => {
+  beforeEach(() => {
+    jest.useFakeTimers();
+    jest.setSystemTime(NOW_IN_MILLISECONDS);
+  });
+
   interface TestScenario {
     scenario: string;
     attributes: Record<string, AttributeValue> | undefined;
@@ -200,15 +206,6 @@ describe("Session attributes", () => {
         ...givenAnyCommonSessionAttributeIsInvalid(
           validBiometricTokenIssuedSessionAttributes,
         ),
-        {
-          scenario: "Given sessionState is not ASYNC_BIOMETRIC_TOKEN_ISSUED",
-          attributes: buildSessionAttributes(
-            validBiometricTokenIssuedSessionAttributes,
-            {
-              sessionState: SessionState.AUTH_SESSION_CREATED,
-            },
-          ),
-        },
         {
           scenario: "Given documentType is missing",
           attributes: buildSessionAttributes(
@@ -288,15 +285,6 @@ describe("Session attributes", () => {
         ...givenAnyCommonSessionAttributeIsInvalid(
           validBiometricSessionFinishedAttributes,
         ),
-        {
-          scenario: "Given sessionState is not BIOMETRIC_SESSION_FINISHED",
-          attributes: buildSessionAttributes(
-            validBiometricSessionFinishedAttributes,
-            {
-              sessionState: SessionState.AUTH_SESSION_CREATED,
-            },
-          ),
-        },
         {
           scenario: "Given documentType is missing",
           attributes: buildSessionAttributes(
diff --git a/backend-api/src/functions/common/session/updateOperations/sessionAttributes/sessionAttributes.ts b/backend-api/src/functions/common/session/sessionAttributes/sessionAttributes.ts
similarity index 63%
rename from backend-api/src/functions/common/session/updateOperations/sessionAttributes/sessionAttributes.ts
rename to backend-api/src/functions/common/session/sessionAttributes/sessionAttributes.ts
index 7fce38260..7337e867b 100644
--- a/backend-api/src/functions/common/session/updateOperations/sessionAttributes/sessionAttributes.ts
+++ b/backend-api/src/functions/common/session/sessionAttributes/sessionAttributes.ts
@@ -1,12 +1,17 @@
 import { AttributeValue } from "@aws-sdk/client-dynamodb";
 import { NativeAttributeValue, unmarshall } from "@aws-sdk/util-dynamodb";
-import { Result, emptyFailure, successResult } from "../../../../utils/result";
+import {
+  emptyFailure,
+  errorResult,
+  Result,
+  successResult,
+} from "../../../utils/result";
 import {
   BaseSessionAttributes,
   BiometricSessionFinishedAttributes,
   BiometricTokenIssuedSessionAttributes,
-  SessionState,
-} from "../../session";
+} from "../session";
+import { ValidateSessionErrorInvalidAttributeTypeData } from "../SessionRegistry";
 
 export const getBaseSessionAttributes = (
   item: Record<string, AttributeValue> | undefined,
@@ -19,16 +24,23 @@ export const getBaseSessionAttributes = (
   return successResult(sessionAttributes);
 };
 
-export const isBaseSessionAttributes = (
+const isBaseSessionAttributes = (
   item: Record<string, NativeAttributeValue>,
 ): item is BaseSessionAttributes => {
+  if (!isCommonSessionAttributes(item)) return false;
+  return true;
+};
+
+const isCommonSessionAttributes = (
+  item: Record<string, NativeAttributeValue>,
+): boolean => {
   if (typeof item.clientId !== "string") return false;
-  if (typeof item.govukSigninJourneyId !== "string") return false;
+  if (typeof item.clientState !== "string") return false;
   if (typeof item.createdAt !== "number") return false;
+  if (typeof item.govukSigninJourneyId !== "string") return false;
   if (typeof item.issuer !== "string") return false;
   if (typeof item.sessionId !== "string") return false;
   if (typeof item.sessionState !== "string") return false;
-  if (typeof item.clientState !== "string") return false;
   if (typeof item.subjectIdentifier !== "string") return false;
   if (typeof item.timeToLive !== "number") return false;
   if ("redirectUri" in item && typeof item.redirectUri !== "string") {
@@ -49,6 +61,31 @@ export const getBiometricTokenIssuedSessionAttributes = (
   return successResult(sessionAttributes);
 };
 
+export const getTxmaEventBiometricTokenIssuedSessionAttributes = (
+  item: Record<string, AttributeValue>,
+): Result<
+  BiometricTokenIssuedSessionAttributes,
+  ValidateSessionErrorInvalidAttributeTypeData
+> => {
+  const sessionAttributes: Record<string, unknown> = unmarshall(item);
+  if (!isBiometricTokenIssuedSessionAttributes(sessionAttributes)) {
+    return errorResult({
+      sessionAttributes,
+    });
+  }
+
+  return successResult(sessionAttributes);
+};
+
+const isBiometricTokenIssuedSessionAttributes = (
+  item: Record<string, NativeAttributeValue>,
+): item is BiometricTokenIssuedSessionAttributes => {
+  if (!isCommonSessionAttributes(item)) return false;
+  if (typeof item.documentType !== "string") return false;
+  if (typeof item.opaqueId !== "string") return false;
+  return true;
+};
+
 export const getBiometricSessionFinishedSessionAttributes = (
   item: Record<string, AttributeValue> | undefined,
 ): Result<BiometricSessionFinishedAttributes, void> => {
@@ -61,44 +98,12 @@ export const getBiometricSessionFinishedSessionAttributes = (
   return successResult(sessionAttributes);
 };
 
-export const isBiometricTokenIssuedSessionAttributes = (
-  item: Record<string, NativeAttributeValue>,
-): item is BiometricTokenIssuedSessionAttributes => {
-  if (typeof item.clientId !== "string") return false;
-  if (typeof item.govukSigninJourneyId !== "string") return false;
-  if (typeof item.createdAt !== "number") return false;
-  if (typeof item.issuer !== "string") return false;
-  if (typeof item.sessionId !== "string") return false;
-  if (item.sessionState !== SessionState.BIOMETRIC_TOKEN_ISSUED) return false;
-  if (typeof item.clientState !== "string") return false;
-  if (typeof item.subjectIdentifier !== "string") return false;
-  if (typeof item.timeToLive !== "number") return false;
-  if (typeof item.documentType !== "string") return false;
-  if (typeof item.opaqueId !== "string") return false;
-  if ("redirectUri" in item && typeof item.redirectUri !== "string") {
-    return false;
-  }
-  return true;
-};
-
-export const isBiometricSessionFinishedSessionAttributes = (
+const isBiometricSessionFinishedSessionAttributes = (
   item: Record<string, NativeAttributeValue>,
 ): item is BiometricSessionFinishedAttributes => {
-  if (typeof item.clientId !== "string") return false;
-  if (typeof item.govukSigninJourneyId !== "string") return false;
-  if (typeof item.createdAt !== "number") return false;
-  if (typeof item.issuer !== "string") return false;
-  if (typeof item.sessionId !== "string") return false;
-  if (item.sessionState !== SessionState.BIOMETRIC_SESSION_FINISHED)
-    return false;
-  if (typeof item.clientState !== "string") return false;
-  if (typeof item.subjectIdentifier !== "string") return false;
-  if (typeof item.timeToLive !== "number") return false;
+  if (!isCommonSessionAttributes(item)) return false;
   if (typeof item.documentType !== "string") return false;
   if (typeof item.opaqueId !== "string") return false;
   if (typeof item.biometricSessionId !== "string") return false;
-  if ("redirectUri" in item && typeof item.redirectUri !== "string") {
-    return false;
-  }
   return true;
 };
diff --git a/backend-api/src/functions/common/session/updateOperations/BiometricSessionFinished/BiometricSessionFinished.test.ts b/backend-api/src/functions/common/session/updateOperations/BiometricSessionFinished/BiometricSessionFinished.test.ts
index 17c29284c..1ef2fb851 100644
--- a/backend-api/src/functions/common/session/updateOperations/BiometricSessionFinished/BiometricSessionFinished.test.ts
+++ b/backend-api/src/functions/common/session/updateOperations/BiometricSessionFinished/BiometricSessionFinished.test.ts
@@ -1,22 +1,23 @@
-import { BiometricSessionFinished } from "./BiometricSessionFinished";
-import { SessionState } from "../../session";
-import { emptyFailure, successResult } from "../../../../utils/result";
 import { marshall, unmarshall } from "@aws-sdk/util-dynamodb";
 import {
+  NOW_IN_MILLISECONDS,
   validBiometricSessionFinishedAttributes,
   validBiometricTokenIssuedSessionAttributes,
 } from "../../../../testUtils/unitTestData";
+import { emptyFailure, successResult } from "../../../../utils/result";
+import { SessionState } from "../../session";
+import { BiometricSessionFinished } from "./BiometricSessionFinished";
 
 describe("BiometricSessionFinished", () => {
   let biometricSessionFinished: BiometricSessionFinished;
   const mockBiometricSessionId = "mock-biometric-session-id";
 
   beforeEach(() => {
+    jest.useFakeTimers();
+    jest.setSystemTime(NOW_IN_MILLISECONDS);
     biometricSessionFinished = new BiometricSessionFinished(
       mockBiometricSessionId,
     );
-    jest.useFakeTimers();
-    jest.setSystemTime(new Date(2025, 1, 19));
   });
 
   afterEach(() => {
diff --git a/backend-api/src/functions/common/session/updateOperations/BiometricSessionFinished/BiometricSessionFinished.ts b/backend-api/src/functions/common/session/updateOperations/BiometricSessionFinished/BiometricSessionFinished.ts
index 103fd7476..f7c0fc27c 100644
--- a/backend-api/src/functions/common/session/updateOperations/BiometricSessionFinished/BiometricSessionFinished.ts
+++ b/backend-api/src/functions/common/session/updateOperations/BiometricSessionFinished/BiometricSessionFinished.ts
@@ -1,11 +1,11 @@
-import { UpdateSessionOperation } from "../UpdateSessionOperation";
-import { SessionState, SessionAttributes } from "../../session";
 import { AttributeValue } from "@aws-sdk/client-dynamodb";
+import { Result } from "../../../../utils/result";
+import { SessionAttributes, SessionState } from "../../session";
 import {
   getBiometricSessionFinishedSessionAttributes,
   getBiometricTokenIssuedSessionAttributes,
-} from "../sessionAttributes/sessionAttributes";
-import { Result } from "../../../../utils/result";
+} from "../../sessionAttributes/sessionAttributes";
+import { UpdateSessionOperation } from "../UpdateSessionOperation";
 
 export class BiometricSessionFinished implements UpdateSessionOperation {
   constructor(private readonly biometricSessionId: string) {}
diff --git a/backend-api/src/functions/common/session/updateOperations/BiometricTokenIssued/BiometricTokenIssued.test.ts b/backend-api/src/functions/common/session/updateOperations/BiometricTokenIssued/BiometricTokenIssued.test.ts
index bb49a750f..6054154d5 100644
--- a/backend-api/src/functions/common/session/updateOperations/BiometricTokenIssued/BiometricTokenIssued.test.ts
+++ b/backend-api/src/functions/common/session/updateOperations/BiometricTokenIssued/BiometricTokenIssued.test.ts
@@ -1,22 +1,29 @@
-import { BiometricTokenIssued } from "./BiometricTokenIssued";
-import { SessionState } from "../../session";
-import { emptyFailure, successResult } from "../../../../utils/result";
 import { marshall, unmarshall } from "@aws-sdk/util-dynamodb";
 import {
+  NOW_IN_MILLISECONDS,
   validBaseSessionAttributes,
   validBiometricTokenIssuedSessionAttributes,
 } from "../../../../testUtils/unitTestData";
+import { emptyFailure, successResult } from "../../../../utils/result";
+import { SessionState } from "../../session";
+import { BiometricTokenIssued } from "./BiometricTokenIssued";
 
 describe("BiometricTokenIssued", () => {
   let biometricTokenIssued: BiometricTokenIssued;
 
   beforeEach(() => {
+    jest.useFakeTimers();
+    jest.setSystemTime(NOW_IN_MILLISECONDS);
     biometricTokenIssued = new BiometricTokenIssued(
       "NFC_PASSPORT",
       "mockOpaqueId",
     );
   });
 
+  afterEach(() => {
+    jest.useRealTimers();
+  });
+
   describe("When I request the DynamoDB UpdateExpression", () => {
     it("Returns the appropriate UpdateExpression string", () => {
       const result = biometricTokenIssued.getDynamoDbUpdateExpression();
diff --git a/backend-api/src/functions/common/session/updateOperations/BiometricTokenIssued/BiometricTokenIssued.ts b/backend-api/src/functions/common/session/updateOperations/BiometricTokenIssued/BiometricTokenIssued.ts
index 9f7d7cbed..5b3405a86 100644
--- a/backend-api/src/functions/common/session/updateOperations/BiometricTokenIssued/BiometricTokenIssued.ts
+++ b/backend-api/src/functions/common/session/updateOperations/BiometricTokenIssued/BiometricTokenIssued.ts
@@ -1,11 +1,11 @@
-import { UpdateSessionOperation } from "../UpdateSessionOperation";
+import { AttributeValue } from "@aws-sdk/client-dynamodb";
 import { DocumentType } from "../../../../types/document";
 import { SessionState } from "../../session";
-import { AttributeValue } from "@aws-sdk/client-dynamodb";
 import {
   getBaseSessionAttributes,
   getBiometricTokenIssuedSessionAttributes,
-} from "../sessionAttributes/sessionAttributes";
+} from "../../sessionAttributes/sessionAttributes";
+import { UpdateSessionOperation } from "../UpdateSessionOperation";
 
 export class BiometricTokenIssued implements UpdateSessionOperation {
   constructor(
diff --git a/backend-api/src/functions/testUtils/unitTestData.ts b/backend-api/src/functions/testUtils/unitTestData.ts
index a2c0bb670..5114b74d2 100644
--- a/backend-api/src/functions/testUtils/unitTestData.ts
+++ b/backend-api/src/functions/testUtils/unitTestData.ts
@@ -1,6 +1,7 @@
 import { SessionState } from "../common/session/session";
 import { SessionRegistry } from "../common/session/SessionRegistry";
 import { IEventService } from "../services/events/types";
+import { emptySuccess } from "../utils/result";
 
 export const mockSessionId = "58f4281d-d988-49ce-9586-6ef70a2be0b4";
 export const mockBiometricSessionId = "f32432a9-0965-4da9-8a2c-a98a79349d4a";
@@ -15,29 +16,13 @@ export const expectedSecurityHeaders = {
 };
 
 export const NOW_IN_MILLISECONDS: number = 1704110400000; // 2024-01-01 12:00:00.000
-
-export const mockInertSessionRegistry: SessionRegistry = {
-  updateSession: jest.fn(() => {
-    throw new Error("Not implemented");
-  }),
-};
-
-export const mockInertEventService: IEventService = {
-  writeGenericEvent: jest.fn(() => {
-    throw new Error("Not implemented");
-  }),
-  writeCredentialTokenIssuedEvent: jest.fn(() => {
-    throw new Error("Not implemented");
-  }),
-  writeBiometricTokenIssuedEvent: jest.fn(() => {
-    throw new Error("Not implemented");
-  }),
-};
+export const validCreatedAt: number = 1704106860000; // 2024-01-01 11:01:00.000
+export const invalidCreatedAt: number = 1704106740000; // 2024-01-01 10:59:00.000
 
 export const validBaseSessionAttributes = {
   clientId: "mockClientId",
   govukSigninJourneyId: "mockGovukSigninJourneyId",
-  createdAt: 12345,
+  createdAt: validCreatedAt,
   issuer: "mockIssuer",
   sessionId: mockSessionId,
   sessionState: SessionState.AUTH_SESSION_CREATED,
@@ -46,6 +31,18 @@ export const validBaseSessionAttributes = {
   timeToLive: 12345,
 };
 
+export const invalidBaseSessionAttributeTypes = {
+  clientId: "mockClientId",
+  govukSigninJourneyId: "mockGovukSigninJourneyId",
+  createdAt: validCreatedAt,
+  issuer: 12345, // Invalid type
+  sessionId: mockSessionId,
+  sessionState: SessionState.AUTH_SESSION_CREATED,
+  clientState: "mockClientState",
+  subjectIdentifier: "mockSubjectIdentifier",
+  timeToLive: "12345", // Invalid type
+};
+
 export const validBiometricTokenIssuedSessionAttributes = {
   ...validBaseSessionAttributes,
   sessionState: SessionState.BIOMETRIC_TOKEN_ISSUED,
@@ -53,8 +50,66 @@ export const validBiometricTokenIssuedSessionAttributes = {
   opaqueId: "mockOpaqueId",
 };
 
+export const validBiometricTokenIssuedSessionAttributesMobileApp = {
+  ...validBiometricTokenIssuedSessionAttributes,
+  redirectUri: "https://www.mockRedirectUri.com",
+};
+
+export const invalidBiometricTokenIssuedSessionAttributesWrongSessionState = {
+  ...validBiometricTokenIssuedSessionAttributes,
+  sessionState: SessionState.AUTH_SESSION_CREATED,
+};
+
+export const invalidBiometricTokenIssuedSessionAttributeTypes = {
+  ...validBiometricTokenIssuedSessionAttributes,
+  issuer: invalidBaseSessionAttributeTypes.issuer,
+  timeToLive: invalidBaseSessionAttributeTypes.timeToLive,
+};
+
 export const validBiometricSessionFinishedAttributes = {
   ...validBiometricTokenIssuedSessionAttributes,
   sessionState: SessionState.BIOMETRIC_SESSION_FINISHED,
   biometricSessionId: mockBiometricSessionId,
 };
+
+export const validBiometricSessionFinishedAttributesMobileApp = {
+  ...validBiometricSessionFinishedAttributes,
+  redirectUri: "https://www.mockRedirectUri.com",
+};
+
+export const mockInertSessionRegistry: SessionRegistry = {
+  updateSession: jest.fn(() => {
+    throw new Error("Not implemented");
+  }),
+
+  getSession: jest.fn(() => {
+    throw new Error("Not implemented");
+  }),
+};
+
+export const mockInertEventService: IEventService = {
+  writeGenericEvent: jest.fn(() => {
+    throw new Error("Not implemented");
+  }),
+  writeCredentialTokenIssuedEvent: jest.fn(() => {
+    throw new Error("Not implemented");
+  }),
+  writeBiometricTokenIssuedEvent: jest.fn(() => {
+    throw new Error("Not implemented");
+  }),
+};
+
+export const mockWriteGenericEventSuccessResult = jest
+  .fn()
+  .mockResolvedValue(emptySuccess());
+
+export const mockWriteBiometricTokenIssuedEventSuccessResult = jest
+  .fn()
+  .mockResolvedValue(emptySuccess());
+
+export const mockSuccessfulEventService = {
+  ...mockInertEventService,
+  writeGenericEvent: mockWriteGenericEventSuccessResult,
+  writeBiometricTokenIssuedEvent:
+    mockWriteBiometricTokenIssuedEventSuccessResult,
+};
diff --git a/backend-api/src/functions/utils/utils.ts b/backend-api/src/functions/utils/utils.ts
index 133c71d05..d019ef4ff 100644
--- a/backend-api/src/functions/utils/utils.ts
+++ b/backend-api/src/functions/utils/utils.ts
@@ -7,3 +7,9 @@ export function isValidUUIDv4(uuid: string): boolean {
     /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$/;
   return uuidRegex.test(uuid);
 }
+
+export function isOlderThan60Minutes(createdAtInMilliseconds: number) {
+  const SIXTY_MINUTES_IN_MILLISECONDS = 3600000;
+  const validFrom = Date.now() - SIXTY_MINUTES_IN_MILLISECONDS;
+  return createdAtInMilliseconds < validFrom;
+}
diff --git a/backend-api/template.yaml b/backend-api/template.yaml
index d3df8984f..f5b35754a 100644
--- a/backend-api/template.yaml
+++ b/backend-api/template.yaml
@@ -3331,6 +3331,13 @@ Resources:
                 Resource: '*'
             Version: "2012-10-17"
           PolicyName: VpcPolicy
+        - PolicyDocument:
+            Statement:
+              - Action:
+                  - dynamodb:GetItem
+                Effect: Allow
+                Resource: !GetAtt SessionsTable.Arn
+          PolicyName: AsyncTxmaEventFunctionDynamodbPolicy
       RoleName: !Sub ${AWS::StackName}-txma-event-lambda
 
   AsyncTxmaEventLogGroup:
diff --git a/backend-api/tests/api-tests/biometricToken.test.ts b/backend-api/tests/api-tests/biometricToken.test.ts
index 492194a83..053015d2b 100644
--- a/backend-api/tests/api-tests/biometricToken.test.ts
+++ b/backend-api/tests/api-tests/biometricToken.test.ts
@@ -98,7 +98,7 @@ describe("POST /async/biometricToken", () => {
         "/async/biometricToken",
         requestBody,
       );
-    }, 15000);
+    }, 25000);
 
     it("Returns an error and 401 status code", () => {
       expect(biometricTokenResponse.status).toBe(401);
diff --git a/backend-api/tests/api-tests/txmaEvent.test.ts b/backend-api/tests/api-tests/txmaEvent.test.ts
index b85167dfd..080c9fb57 100644
--- a/backend-api/tests/api-tests/txmaEvent.test.ts
+++ b/backend-api/tests/api-tests/txmaEvent.test.ts
@@ -1,6 +1,13 @@
 import { AxiosResponse } from "axios";
 import { SESSIONS_API_INSTANCE } from "./utils/apiInstance";
 import { expectedSecurityHeaders, mockSessionId } from "./utils/apiTestData";
+import {
+  createSessionForSub,
+  getActiveSessionIdFromSub,
+  issueBiometricToken,
+  pollForEvents,
+} from "./utils/apiTestHelpers";
+import { randomUUID } from "crypto";
 
 describe("POST /async/txmaEvent", () => {
   describe("Given request body is invalid", () => {
@@ -20,6 +27,7 @@ describe("POST /async/txmaEvent", () => {
 
     it("Returns an error and 400 status code", async () => {
       expect(response.status).toBe(400);
+      expect(response.statusText).toBe("Bad Request");
       expect(response.data).toStrictEqual({
         error: "invalid_request",
         error_description:
@@ -31,12 +39,14 @@ describe("POST /async/txmaEvent", () => {
     });
   });
 
-  describe("Given there is a valid request", () => {
+  describe("Given the session is not valid", () => {
     let response: AxiosResponse;
+    let sessionId: string;
 
     beforeAll(async () => {
+      sessionId = mockSessionId;
       const requestBody = {
-        sessionId: mockSessionId,
+        sessionId,
         eventName: "DCMAW_ASYNC_HYBRID_BILLING_STARTED",
       };
 
@@ -44,10 +54,55 @@ describe("POST /async/txmaEvent", () => {
         "/async/txmaEvent",
         requestBody,
       );
+    });
 
-      console.log("RESPONSE >>>>>", response);
+    it("Returns an error and 401 status code", async () => {
+      expect(response.status).toBe(401);
+      expect(response.statusText).toBe("Unauthorized");
+      expect(response.data).toStrictEqual({
+        error: "invalid_session",
+        error_description: "Session does not exist or in incorrect state",
+      });
+      expect(response.headers).toEqual(
+        expect.objectContaining(expectedSecurityHeaders),
+      );
     });
 
+    it("Writes an event with the correct event_name", async () => {
+      const eventsResponse = await pollForEvents({
+        partitionKey: `SESSION#${sessionId}`,
+        sortKeyPrefix: `TXMA#EVENT_NAME#DCMAW_ASYNC_CRI_4XXERROR`,
+        numberOfEvents: 1,
+      });
+
+      expect(eventsResponse[0].event).toEqual(
+        expect.objectContaining({
+          event_name: "DCMAW_ASYNC_CRI_4XXERROR",
+        }),
+      );
+    }, 40000);
+  });
+
+  describe("Given the request is valid", () => {
+    let sessionId: string | null;
+    let response: AxiosResponse;
+
+    beforeAll(async () => {
+      const sub = randomUUID();
+      await createSessionForSub(sub);
+      sessionId = await getActiveSessionIdFromSub(sub);
+      await issueBiometricToken(sessionId);
+
+      const requestBody = {
+        sessionId,
+        eventName: "DCMAW_ASYNC_HYBRID_BILLING_STARTED",
+      };
+      response = await SESSIONS_API_INSTANCE.post(
+        "/async/txmaEvent",
+        requestBody,
+      );
+    }, 30000);
+
     it("Returns 501 Not Implemented response", () => {
       expect(response.status).toBe(501);
       expect(response.data).toStrictEqual({


Reviews:
user: login: jmooney-dd
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-10T13:20:13Z
user: login: jmooney-dd
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-10T13:23:34Z
user: login: jmooney-dd
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-10T13:26:30Z
user: login: jmooney-dd
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-10T13:33:51Z
user: login: jmooney-dd
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-10T13:34:42Z
user: login: jmooney-dd
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-10T13:37:30Z
user: login: jmooney-dd
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-10T13:38:12Z
user: login: jhumbert-dd
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-10T14:00:04Z
user: login: jhumbert-dd
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-10T14:10:19Z
user: login: jhumbert-dd
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-10T14:12:42Z
user: login: jhumbert-dd
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-10T14:16:04Z
user: login: jhumbert-dd
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-10T14:20:38Z
user: login: jhumbert-dd
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-10T14:31:59Z
user: login: jmooney-dd
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-10T14:40:54Z
user: login: jmooney-dd
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-10T15:17:25Z
user: login: jmooney-dd
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-10T17:35:49Z
user: login: jhumbert-dd
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-10T18:11:49Z
user: login: jhumbert-dd
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-10T18:14:51Z
user: login: jhumbert-dd
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-10T18:16:08Z
user: login: jhumbert-dd
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-12T15:58:57Z
user: login: jhumbert-dd
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-12T16:08:38Z
user: login: jmooney-dd
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-12T16:16:43Z
user: login: jmooney-dd
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-12T16:17:39Z
user: login: jmooney-dd
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-12T16:19:54Z
user: login: jmooney-dd
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-12T16:21:52Z
user: login: jmooney-dd
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-12T16:22:57Z
user: login: jmooney-dd
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-12T16:25:49Z
user: login: jmooney-dd
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-12T16:26:59Z
user: login: jmooney-dd
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-12T16:28:32Z
user: login: jmooney-dd
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-12T16:28:43Z
user: login: jhumbert-dd
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-12T16:31:12Z
user: login: jhumbert-dd
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-12T16:33:15Z
user: login: jmooney-dd
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-12T16:37:01Z
user: login: jmooney-dd
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-12T17:06:35Z
user: login: jmooney-dd
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-12T17:06:53Z
user: login: jhumbert-dd
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-12T17:09:02Z
user: login: jhumbert-dd
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-12T17:09:13Z
user: login: jhumbert-dd
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-12T17:12:24Z
user: login: jhumbert-dd
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-12T17:12:53Z
user: login: jhumbert-dd
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-12T17:12:59Z
user: login: jhumbert-dd
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-12T17:24:53Z
user: login: kikidawson-gds
state: DISMISSED
author_association: CONTRIBUTOR
submitted_at: 2025-03-13T10:37:20Z
user: login: jmooney-dd
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-13T10:41:14Z
user: login: jmooney-dd
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-13T10:41:58Z
user: login: jmooney-dd
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-13T10:45:48Z
user: login: jmooney-dd
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-13T10:49:05Z
user: login: jhumbert-dd
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-13T10:50:34Z
user: login: jmooney-dd
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-13T10:52:20Z
user: login: jhumbert-dd
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-13T10:54:53Z
user: login: jmooney-dd
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-13T10:57:17Z
user: login: jmooney-dd
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-13T10:58:12Z
user: login: jmooney-dd
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-13T10:59:31Z
user: login: jhumbert-dd
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-13T14:48:26Z
user: login: jhumbert-dd
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-13T15:02:23Z
user: login: jhumbert-dd
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-13T15:05:53Z
user: login: jhumbert-dd
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-13T15:14:25Z
user: login: jmooney-dd
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-14T09:48:18Z
user: login: jhumbert-dd
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-17T14:51:18Z
user: login: jhumbert-dd
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-17T15:06:19Z
user: login: jhumbert-dd
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-17T15:17:12Z
user: login: jhumbert-dd
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-17T16:28:15Z
user: login: jhumbert-dd
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-17T16:33:09Z
user: login: jhumbert-dd
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-17T16:46:37Z
user: login: sandymay-dd
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-17T22:06:17Z
user: login: sandymay-dd
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-17T22:16:31Z
user: login: jhumbert-dd
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-18T09:18:12Z
user: login: jhumbert-dd
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-18T09:21:36Z
user: login: jhumbert-dd
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-18T11:14:49Z
user: login: jhumbert-dd
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-19T15:52:21Z
user: login: jhumbert-dd
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-19T16:01:47Z
user: login: jhumbert-dd
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-19T16:44:15Z
user: login: jhumbert-dd
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-19T16:45:15Z
user: login: jhumbert-dd
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-19T16:45:41Z
user: login: jhumbert-dd
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-19T16:51:27Z
user: login: jhumbert-dd
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-19T17:02:02Z
user: login: jhumbert-dd
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-19T20:40:55Z
user: login: jhumbert-dd
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-19T20:49:03Z
user: login: jhumbert-dd
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T09:21:47Z
user: login: jhumbert-dd
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T09:51:12Z
user: login: chris-cooksley-gds
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T10:41:33Z
user: login: chris-cooksley-gds
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T11:06:22Z
user: login: chris-cooksley-gds
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T11:16:45Z
user: login: chris-cooksley-gds
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T11:22:18Z
user: login: chris-cooksley-gds
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T11:23:24Z
user: login: chris-cooksley-gds
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T11:25:14Z
user: login: jhumbert-dd
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T12:55:30Z
user: login: jhumbert-dd
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T12:58:46Z
user: login: jhumbert-dd
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T13:16:09Z
user: login: jhumbert-dd
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T13:16:16Z
user: login: chris-cooksley-gds
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T13:55:54Z
user: login: chris-cooksley-gds
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T15:25:36Z
user: login: chris-cooksley-gds
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T15:28:25Z
user: login: chris-cooksley-gds
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T15:33:15Z
user: login: chris-cooksley-gds
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T15:38:35Z
user: login: chris-cooksley-gds
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T15:42:39Z
user: login: chris-cooksley-gds
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T15:48:09Z
user: login: chris-cooksley-gds
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T15:50:34Z
user: login: chris-cooksley-gds
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T15:57:32Z
user: login: chris-cooksley-gds
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T15:59:59Z
user: login: jhumbert-dd
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T16:03:16Z
user: login: chris-cooksley-gds
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T16:09:17Z
user: login: chris-cooksley-gds
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T16:11:23Z
user: login: chris-cooksley-gds
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T16:12:41Z
user: login: chris-cooksley-gds
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T16:13:54Z
user: login: chris-cooksley-gds
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T16:14:52Z
user: login: sandymay-dd
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T16:23:35Z
user: login: chris-cooksley-gds
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T16:33:09Z
user: login: jhumbert-dd
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T16:44:48Z
user: login: jhumbert-dd
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T17:02:33Z
user: login: jhumbert-dd
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T17:06:40Z
user: login: jhumbert-dd
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T17:07:10Z
user: login: jhumbert-dd
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T17:08:59Z
user: login: jhumbert-dd
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T17:10:23Z
user: login: jhumbert-dd
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-21T09:25:29Z
user: login: jhumbert-dd
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-21T09:26:15Z
user: login: jhumbert-dd
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-21T09:26:25Z
user: login: jhumbert-dd
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-21T09:52:41Z
user: login: jhumbert-dd
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-21T10:09:12Z
user: login: jhumbert-dd
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-21T10:14:01Z
user: login: jhumbert-dd
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-21T10:57:03Z
user: login: jhumbert-dd
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-21T11:06:00Z
user: login: jhumbert-dd
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-21T11:09:02Z
user: login: jhumbert-dd
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-21T11:25:32Z
user: login: jhumbert-dd
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-21T11:53:45Z
user: login: jhumbert-dd
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-21T12:11:16Z
user: login: jhumbert-dd
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-21T12:35:33Z
user: login: jhumbert-dd
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-21T12:40:04Z
user: login: jhumbert-dd
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-21T12:44:56Z
user: login: jhumbert-dd
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-21T12:51:02Z
user: login: sandymay-dd
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-21T12:58:54Z
user: login: sandymay-dd
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-21T12:59:17Z
user: login: jmooney-dd
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-21T13:52:44Z
user: login: jhumbert-dd
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-21T14:03:10Z
user: login: jmooney-dd
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-21T14:03:36Z
user: login: jmooney-dd
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-21T14:04:32Z
user: login: jmooney-dd
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-21T14:05:10Z
user: login: jmooney-dd
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-21T14:05:18Z
user: login: jmooney-dd
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-21T14:07:31Z
user: login: jmooney-dd
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-21T14:08:46Z
user: login: jhumbert-dd
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-21T14:09:07Z
user: login: jmooney-dd
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-21T14:16:19Z
user: login: jmooney-dd
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-21T14:17:43Z
user: login: jhumbert-dd
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-21T14:19:43Z
user: login: jmooney-dd
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-21T14:23:34Z
user: login: jmooney-dd
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-21T14:28:57Z
user: login: jhumbert-dd
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-21T14:30:07Z
user: login: jhumbert-dd
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-21T14:33:11Z
user: login: jmooney-dd
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-21T16:14:23Z
user: login: jmooney-dd
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-21T16:17:09Z
user: login: jhumbert-dd
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-21T16:27:51Z
user: login: jhumbert-dd
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-21T16:28:01Z
user: login: jhumbert-dd
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-21T16:58:18Z
user: login: jhumbert-dd
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-21T16:58:28Z
user: login: jhumbert-dd
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-21T17:02:42Z
user: login: sandymay-dd
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-21T17:14:01Z
user: login: sandymay-dd
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-21T17:22:01Z
user: login: jhumbert-dd
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-21T17:42:02Z
user: login: sandymay-dd
body: Approved the last few commits having taken over James' review.
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-21T17:55:39Z

Review comments:
diff_hunk: @@ -131,6 +141,55 @@ export class DynamoDbAdapter implements SessionRegistry {
     }
   }
 
+  async getSession(
+    sessionId: string,
+  ): Promise<Result<void, SessionRetrievalFailed>> {
+    logger.debug(LogMessage.GET_SESSION_ATTEMPT);
path: backend-api/src/functions/adapters/dynamoDbAdapter.ts
user: login: jmooney-dd
body: suggestion: can we move this to within the try block? I think this is typically how we do this elsewhere for network attempt stuff
created_at: 2025-03-10T13:20:12Z
author_association: CONTRIBUTOR
diff_hunk: @@ -235,4 +306,28 @@ export class DynamoDbAdapter implements SessionRegistry {
       errorType: UpdateSessionError.INTERNAL_SERVER_ERROR,
     });
   }
+
+  private handleGetSessionInternalServerError(
+    error: unknown,
+  ): FailureWithValue<SessionRetrievalFailedInternalServerError> {
+    logger.error(LogMessage.GET_SESSION_UNEXPECTED_FAILURE, {
+      data: { error },
+    });
+    return errorResult({
+      errorType: GetSessionError.INTERNAL_SERVER_ERROR,
+    });
+  }
+
+  private getSessionAttributesFromDynamoDbItem(
+    item: Record<string, AttributeValue> | undefined,
+    options?: {
+      operationFailed: boolean;
+    },
+  ): Result<SessionAttributes, void> {
+    if (options?.operationFailed) {
+      return getBaseSessionAttributes(item);
+    } else {
+      return getBiometricTokenIssuedSessionAttributes(item);
+    }
path: backend-api/src/functions/adapters/dynamoDbAdapter.ts
user: login: jmooney-dd
body: thought: should this decision making be done in the dynamo adapter or should we pass something through to it, like we do with the update function, to decide which type of session attributes we get back?
created_at: 2025-03-10T13:23:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -32,6 +48,26 @@ export async function lambdaHandlerConstructor(
     });
     return badRequestResponse("invalid_request", errorMessage);
   }
+  const { sessionId } = validateRequestBodyResult.value;
+
+  const sessionRegistry = dependencies.getSessionRegistry(
+    config.SESSION_TABLE_NAME,
+  );
+  const getSessionResult = await sessionRegistry.getSession(sessionId);
+  const eventService = dependencies.getEventService(config.TXMA_SQS);
+  const eventData = {
+    sessionId,
+    issuer: config.ISSUER,
+    ipAddress: getIpAddress(event),
+    txmaAuditEncoded: getHeader(event.headers, "Txma-Audit-Encoded"),
+  };
path: backend-api/src/functions/asyncTxmaEvent/asyncTxmaEventHandler.ts
user: login: jmooney-dd
body: ```suggestion
  const { ipAddress, txmaAuditEncoded } = getAuditData(event);
  const eventData = {
    sessionId,
    issuer: config.ISSUER,
    ipAddress,
    txmaAuditEncoded,
  };
```

I wouldn't commit from here as they'll be import changes
created_at: 2025-03-10T13:26:30Z
author_association: CONTRIBUTOR
diff_hunk: @@ -131,6 +141,55 @@ export class DynamoDbAdapter implements SessionRegistry {
     }
   }
 
+  async getSession(
+    sessionId: string,
+  ): Promise<Result<void, SessionRetrievalFailed>> {
+    logger.debug(LogMessage.GET_SESSION_ATTEMPT);
+
+    let response;
+    try {
+      response = await this.dynamoDbClient.send(
+        new GetItemCommand({
+          TableName: this.tableName,
+          Key: marshall({ sessionId }),
+        }),
+      );
+    } catch (error) {
+      return this.handleGetSessionInternalServerError(error);
+    }
+
+    const responseItem = response.Item;
+    if (responseItem == null) {
+      logger.error(LogMessage.GET_SESSION_SESSION_NOT_FOUND);
+
+      return errorResult({
+        errorType: GetSessionError.SESSION_NOT_FOUND,
+      });
+    }
+
+    const getSessionAttributesResult =
+      this.getSessionAttributesFromDynamoDbItem(responseItem);
+    if (getSessionAttributesResult.isError) {
+      return this.handleGetSessionInternalServerError(
+        "Could not parse valid session attributes after successful update command",
+      );
+    }
+    const sessionAttributes = getSessionAttributesResult.value;
+
+    const { createdAt } = sessionAttributes;
+    if (this.isOlderThan60minutes(createdAt)) {
+      logger.error(LogMessage.GET_SESSION_SESSION_TOO_OLD);
+
+      return errorResult({
+        errorType: GetSessionError.SESSION_NOT_FOUND,
+      });
+    }
+
+    logger.error(LogMessage.GET_SESSION_SUCCESS);
path: backend-api/src/functions/adapters/dynamoDbAdapter.ts
user: login: jmooney-dd
body: issue: this is not an error
created_at: 2025-03-10T13:33:50Z
author_association: CONTRIBUTOR
diff_hunk: @@ -131,6 +141,55 @@ export class DynamoDbAdapter implements SessionRegistry {
     }
   }
 
+  async getSession(
+    sessionId: string,
+  ): Promise<Result<void, SessionRetrievalFailed>> {
+    logger.debug(LogMessage.GET_SESSION_ATTEMPT);
+
+    let response;
+    try {
+      response = await this.dynamoDbClient.send(
+        new GetItemCommand({
+          TableName: this.tableName,
+          Key: marshall({ sessionId }),
+        }),
+      );
+    } catch (error) {
+      return this.handleGetSessionInternalServerError(error);
+    }
+
+    const responseItem = response.Item;
+    if (responseItem == null) {
+      logger.error(LogMessage.GET_SESSION_SESSION_NOT_FOUND);
+
+      return errorResult({
+        errorType: GetSessionError.SESSION_NOT_FOUND,
+      });
+    }
+
+    const getSessionAttributesResult =
+      this.getSessionAttributesFromDynamoDbItem(responseItem);
+    if (getSessionAttributesResult.isError) {
+      return this.handleGetSessionInternalServerError(
+        "Could not parse valid session attributes after successful update command",
+      );
+    }
+    const sessionAttributes = getSessionAttributesResult.value;
+
+    const { createdAt } = sessionAttributes;
+    if (this.isOlderThan60minutes(createdAt)) {
+      logger.error(LogMessage.GET_SESSION_SESSION_TOO_OLD);
+
+      return errorResult({
+        errorType: GetSessionError.SESSION_NOT_FOUND,
+      });
+    }
+
+    logger.error(LogMessage.GET_SESSION_SUCCESS);
+
+    return emptySuccess();
path: backend-api/src/functions/adapters/dynamoDbAdapter.ts
user: login: jmooney-dd
body: issue: Do we not also need to check the state before we return success? At the moment I can only see we have checked createdAt
created_at: 2025-03-10T13:34:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -177,11 +236,10 @@ export class DynamoDbAdapter implements SessionRegistry {
         const getSessionAttributesOptions = {
           operationFailed: true,
         };
-        const getAttributesResult =
-          updateOperation.getSessionAttributesFromDynamoDbItem(
-            error.Item,
-            getSessionAttributesOptions,
-          );
+        const getAttributesResult = this.getSessionAttributesFromDynamoDbItem(
path: backend-api/src/functions/adapters/dynamoDbAdapter.ts
user: login: jmooney-dd
body: question: why are we changing this, should this not come from the updateOperation?
created_at: 2025-03-10T13:37:30Z
author_association: CONTRIBUTOR
diff_hunk: @@ -205,8 +263,9 @@ export class DynamoDbAdapter implements SessionRegistry {
       }
     }
 
-    const getAttributesResult =
-      updateOperation.getSessionAttributesFromDynamoDbItem(response.Attributes);
+    const getAttributesResult = this.getSessionAttributesFromDynamoDbItem(
path: backend-api/src/functions/adapters/dynamoDbAdapter.ts
user: login: jmooney-dd
body: question: why are we changing this, should this not come from the updateOperation?
created_at: 2025-03-10T13:38:11Z
author_association: CONTRIBUTOR
diff_hunk: @@ -131,6 +141,55 @@ export class DynamoDbAdapter implements SessionRegistry {
     }
   }
 
+  async getSession(
+    sessionId: string,
+  ): Promise<Result<void, SessionRetrievalFailed>> {
+    logger.debug(LogMessage.GET_SESSION_ATTEMPT);
path: backend-api/src/functions/adapters/dynamoDbAdapter.ts
user: login: jhumbert-dd
body: Ok will do. What's the benefit of this being in a try block out of curiosity?
created_at: 2025-03-10T14:00:04Z
author_association: CONTRIBUTOR
diff_hunk: @@ -131,6 +141,55 @@ export class DynamoDbAdapter implements SessionRegistry {
     }
   }
 
+  async getSession(
+    sessionId: string,
+  ): Promise<Result<void, SessionRetrievalFailed>> {
+    logger.debug(LogMessage.GET_SESSION_ATTEMPT);
+
+    let response;
+    try {
+      response = await this.dynamoDbClient.send(
+        new GetItemCommand({
+          TableName: this.tableName,
+          Key: marshall({ sessionId }),
+        }),
+      );
+    } catch (error) {
+      return this.handleGetSessionInternalServerError(error);
+    }
+
+    const responseItem = response.Item;
+    if (responseItem == null) {
+      logger.error(LogMessage.GET_SESSION_SESSION_NOT_FOUND);
+
+      return errorResult({
+        errorType: GetSessionError.SESSION_NOT_FOUND,
+      });
+    }
+
+    const getSessionAttributesResult =
+      this.getSessionAttributesFromDynamoDbItem(responseItem);
+    if (getSessionAttributesResult.isError) {
+      return this.handleGetSessionInternalServerError(
+        "Could not parse valid session attributes after successful update command",
+      );
+    }
+    const sessionAttributes = getSessionAttributesResult.value;
+
+    const { createdAt } = sessionAttributes;
+    if (this.isOlderThan60minutes(createdAt)) {
+      logger.error(LogMessage.GET_SESSION_SESSION_TOO_OLD);
+
+      return errorResult({
+        errorType: GetSessionError.SESSION_NOT_FOUND,
+      });
+    }
+
+    logger.error(LogMessage.GET_SESSION_SUCCESS);
+
+    return emptySuccess();
path: backend-api/src/functions/adapters/dynamoDbAdapter.ts
user: login: jhumbert-dd
body: That's done in the `getSessionAttributesFromDynamoDbItem` function
created_at: 2025-03-10T14:10:19Z
author_association: CONTRIBUTOR
diff_hunk: @@ -177,11 +236,10 @@ export class DynamoDbAdapter implements SessionRegistry {
         const getSessionAttributesOptions = {
           operationFailed: true,
         };
-        const getAttributesResult =
-          updateOperation.getSessionAttributesFromDynamoDbItem(
-            error.Item,
-            getSessionAttributesOptions,
-          );
+        const getAttributesResult = this.getSessionAttributesFromDynamoDbItem(
path: backend-api/src/functions/adapters/dynamoDbAdapter.ts
user: login: jhumbert-dd
body: I wanted to make this a common function as this is now not an exclusive `updateOperation`. I think this might need to be part of a bigger refactor that might be too big to include in the changes for this PR
created_at: 2025-03-10T14:12:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -205,8 +263,9 @@ export class DynamoDbAdapter implements SessionRegistry {
       }
     }
 
-    const getAttributesResult =
-      updateOperation.getSessionAttributesFromDynamoDbItem(response.Attributes);
+    const getAttributesResult = this.getSessionAttributesFromDynamoDbItem(
path: backend-api/src/functions/adapters/dynamoDbAdapter.ts
user: login: jhumbert-dd
body: The reasoning for this is the same as this [comment](https://github.com/govuk-one-login/mobile-id-check-async/pull/469#discussion_r1987371714)
created_at: 2025-03-10T14:16:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -131,6 +141,55 @@ export class DynamoDbAdapter implements SessionRegistry {
     }
   }
 
+  async getSession(
+    sessionId: string,
+  ): Promise<Result<void, SessionRetrievalFailed>> {
+    logger.debug(LogMessage.GET_SESSION_ATTEMPT);
+
+    let response;
+    try {
+      response = await this.dynamoDbClient.send(
+        new GetItemCommand({
+          TableName: this.tableName,
+          Key: marshall({ sessionId }),
+        }),
+      );
+    } catch (error) {
+      return this.handleGetSessionInternalServerError(error);
+    }
+
+    const responseItem = response.Item;
+    if (responseItem == null) {
+      logger.error(LogMessage.GET_SESSION_SESSION_NOT_FOUND);
+
+      return errorResult({
+        errorType: GetSessionError.SESSION_NOT_FOUND,
+      });
+    }
+
+    const getSessionAttributesResult =
+      this.getSessionAttributesFromDynamoDbItem(responseItem);
+    if (getSessionAttributesResult.isError) {
+      return this.handleGetSessionInternalServerError(
+        "Could not parse valid session attributes after successful update command",
+      );
+    }
+    const sessionAttributes = getSessionAttributesResult.value;
+
+    const { createdAt } = sessionAttributes;
+    if (this.isOlderThan60minutes(createdAt)) {
+      logger.error(LogMessage.GET_SESSION_SESSION_TOO_OLD);
+
+      return errorResult({
+        errorType: GetSessionError.SESSION_NOT_FOUND,
+      });
+    }
+
+    logger.error(LogMessage.GET_SESSION_SUCCESS);
path: backend-api/src/functions/adapters/dynamoDbAdapter.ts
user: login: jhumbert-dd
body: updated
created_at: 2025-03-10T14:20:38Z
author_association: CONTRIBUTOR
diff_hunk: @@ -32,6 +48,26 @@ export async function lambdaHandlerConstructor(
     });
     return badRequestResponse("invalid_request", errorMessage);
   }
+  const { sessionId } = validateRequestBodyResult.value;
+
+  const sessionRegistry = dependencies.getSessionRegistry(
+    config.SESSION_TABLE_NAME,
+  );
+  const getSessionResult = await sessionRegistry.getSession(sessionId);
+  const eventService = dependencies.getEventService(config.TXMA_SQS);
+  const eventData = {
+    sessionId,
+    issuer: config.ISSUER,
+    ipAddress: getIpAddress(event),
+    txmaAuditEncoded: getHeader(event.headers, "Txma-Audit-Encoded"),
+  };
path: backend-api/src/functions/asyncTxmaEvent/asyncTxmaEventHandler.ts
user: login: jhumbert-dd
body: Nice! Updated
created_at: 2025-03-10T14:31:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -177,11 +236,10 @@ export class DynamoDbAdapter implements SessionRegistry {
         const getSessionAttributesOptions = {
           operationFailed: true,
         };
-        const getAttributesResult =
-          updateOperation.getSessionAttributesFromDynamoDbItem(
-            error.Item,
-            getSessionAttributesOptions,
-          );
+        const getAttributesResult = this.getSessionAttributesFromDynamoDbItem(
path: backend-api/src/functions/adapters/dynamoDbAdapter.ts
user: login: jmooney-dd
body: Your function does this:
```
    if (options?.operationFailed) {
      return getBaseSessionAttributes(item);
    } else {
      return getBiometricTokenIssuedSessionAttributes(item);
    }
```
    
But different lambdas use updateSession and ask for different shapes of attributes to what is above
created_at: 2025-03-10T14:40:54Z
author_association: CONTRIBUTOR
diff_hunk: @@ -131,6 +141,55 @@ export class DynamoDbAdapter implements SessionRegistry {
     }
   }
 
+  async getSession(
+    sessionId: string,
+  ): Promise<Result<void, SessionRetrievalFailed>> {
+    logger.debug(LogMessage.GET_SESSION_ATTEMPT);
path: backend-api/src/functions/adapters/dynamoDbAdapter.ts
user: login: jmooney-dd
body: None functionally. Discussed it with Chris and we preferred it being in the try block as it resembles it properly being the attempt 🤷 
created_at: 2025-03-10T15:17:25Z
author_association: CONTRIBUTOR
diff_hunk: @@ -131,6 +142,55 @@ export class DynamoDbAdapter implements SessionRegistry {
     }
   }
 
+  async getSession(
+    sessionId: string,
+  ): Promise<Result<void, SessionRetrievalFailed>> {
+    let response;
+    try {
+      logger.debug(LogMessage.GET_SESSION_ATTEMPT);
+
+      response = await this.dynamoDbClient.send(
+        new GetItemCommand({
+          TableName: this.tableName,
+          Key: marshall({ sessionId }),
+        }),
+      );
+    } catch (error) {
+      return this.handleGetSessionInternalServerError(error);
+    }
+
+    const responseItem = response.Item;
+    if (responseItem == null) {
+      logger.error(LogMessage.GET_SESSION_SESSION_NOT_FOUND);
+
+      return errorResult({
+        errorType: GetSessionError.SESSION_NOT_FOUND,
+      });
+    }
+
+    const getSessionAttributesResult =
+      this.getSessionAttributesFromDynamoDbItem(responseItem);
+    if (getSessionAttributesResult.isError) {
+      return this.handleGetSessionNotFoundError(
+        "Could not parse valid session attributes after successful get command",
+      );
+    }
+    const sessionAttributes = getSessionAttributesResult.value;
+
+    const { createdAt } = sessionAttributes;
+    if (this.isOlderThan60minutes(createdAt)) {
path: backend-api/src/functions/adapters/dynamoDbAdapter.ts
user: login: jmooney-dd
body: question: did you consider using a query command so you don't have to do this validation here? That or use getItem command as you've done here to simply get an item and leave the validation for the lambda?
created_at: 2025-03-10T17:35:49Z
author_association: CONTRIBUTOR
diff_hunk: @@ -235,4 +306,28 @@ export class DynamoDbAdapter implements SessionRegistry {
       errorType: UpdateSessionError.INTERNAL_SERVER_ERROR,
     });
   }
+
+  private handleGetSessionInternalServerError(
+    error: unknown,
+  ): FailureWithValue<SessionRetrievalFailedInternalServerError> {
+    logger.error(LogMessage.GET_SESSION_UNEXPECTED_FAILURE, {
+      data: { error },
+    });
+    return errorResult({
+      errorType: GetSessionError.INTERNAL_SERVER_ERROR,
+    });
+  }
+
+  private getSessionAttributesFromDynamoDbItem(
+    item: Record<string, AttributeValue> | undefined,
+    options?: {
+      operationFailed: boolean;
+    },
+  ): Result<SessionAttributes, void> {
+    if (options?.operationFailed) {
+      return getBaseSessionAttributes(item);
+    } else {
+      return getBiometricTokenIssuedSessionAttributes(item);
+    }
path: backend-api/src/functions/adapters/dynamoDbAdapter.ts
user: login: jhumbert-dd
body: Updated to use the same pattern as the `updateSession` method
created_at: 2025-03-10T18:11:49Z
author_association: CONTRIBUTOR
diff_hunk: @@ -177,11 +236,10 @@ export class DynamoDbAdapter implements SessionRegistry {
         const getSessionAttributesOptions = {
           operationFailed: true,
         };
-        const getAttributesResult =
-          updateOperation.getSessionAttributesFromDynamoDbItem(
-            error.Item,
-            getSessionAttributesOptions,
-          );
+        const getAttributesResult = this.getSessionAttributesFromDynamoDbItem(
path: backend-api/src/functions/adapters/dynamoDbAdapter.ts
user: login: jhumbert-dd
body: Updated to use the same pattern as the `updateSession` method after discussion
created_at: 2025-03-10T18:14:51Z
author_association: CONTRIBUTOR
diff_hunk: @@ -205,8 +263,9 @@ export class DynamoDbAdapter implements SessionRegistry {
       }
     }
 
-    const getAttributesResult =
-      updateOperation.getSessionAttributesFromDynamoDbItem(response.Attributes);
+    const getAttributesResult = this.getSessionAttributesFromDynamoDbItem(
path: backend-api/src/functions/adapters/dynamoDbAdapter.ts
user: login: jhumbert-dd
body: Updated
created_at: 2025-03-10T18:16:08Z
author_association: CONTRIBUTOR
diff_hunk: @@ -131,6 +142,55 @@ export class DynamoDbAdapter implements SessionRegistry {
     }
   }
 
+  async getSession(
+    sessionId: string,
+  ): Promise<Result<void, SessionRetrievalFailed>> {
+    let response;
+    try {
+      logger.debug(LogMessage.GET_SESSION_ATTEMPT);
+
+      response = await this.dynamoDbClient.send(
+        new GetItemCommand({
+          TableName: this.tableName,
+          Key: marshall({ sessionId }),
+        }),
+      );
+    } catch (error) {
+      return this.handleGetSessionInternalServerError(error);
+    }
+
+    const responseItem = response.Item;
+    if (responseItem == null) {
+      logger.error(LogMessage.GET_SESSION_SESSION_NOT_FOUND);
+
+      return errorResult({
+        errorType: GetSessionError.SESSION_NOT_FOUND,
+      });
+    }
+
+    const getSessionAttributesResult =
+      this.getSessionAttributesFromDynamoDbItem(responseItem);
+    if (getSessionAttributesResult.isError) {
+      return this.handleGetSessionNotFoundError(
+        "Could not parse valid session attributes after successful get command",
+      );
+    }
+    const sessionAttributes = getSessionAttributesResult.value;
+
+    const { createdAt } = sessionAttributes;
+    if (this.isOlderThan60minutes(createdAt)) {
path: backend-api/src/functions/adapters/dynamoDbAdapter.ts
user: login: jhumbert-dd
body: Moved to validation to the Lambda after discussion.
created_at: 2025-03-12T15:58:57Z
author_association: CONTRIBUTOR
diff_hunk: @@ -90,8 +145,281 @@ describe("Async TxMA Event", () => {
     });
   });
 
+  describe("Retrieving a session", () => {
+    describe("Given there is an error retrieving the session", () => {
+      beforeEach(async () => {
+        dependencies.getSessionRegistry = () => ({
+          ...mockInertSessionRegistry,
+          getSession: jest.fn().mockResolvedValue(
+            errorResult({
+              errorType: GetSessionError.INTERNAL_SERVER_ERROR,
+            }),
+          ),
+        });
+        result = await lambdaHandlerConstructor(
+          dependencies,
+          validRequest,
+          context,
+        );
+      });
+
+      describe("Given DCMAW_ASYNC_CRI_5XXERROR event fails to write to TxMA", () => {
+        beforeEach(async () => {
+          dependencies.getEventService = () => ({
+            ...mockInertEventService,
+            writeGenericEvent: jest.fn().mockResolvedValue(
+              errorResult({
+                errorMessage: "mockError",
+              }),
+            ),
+          });
+
+          result = await lambdaHandlerConstructor(
+            dependencies,
+            validRequest,
+            context,
+          );
+        });
+
+        it("Logs the error", async () => {
+          expect(consoleErrorSpy).toHaveBeenCalledWithLogFields({
+            messageCode: "MOBILE_ASYNC_ERROR_WRITING_AUDIT_EVENT",
+            data: {
+              auditEventName: "DCMAW_ASYNC_CRI_5XXERROR",
+            },
+          });
+        });
+
+        it("Returns 500 Internal server error", async () => {
+          expect(result).toStrictEqual({
+            statusCode: 500,
+            body: JSON.stringify({
+              error: "server_error",
+              error_description: "Internal Server Error",
+            }),
+            headers: expectedSecurityHeaders,
+          });
+        });
+      });
+
+      describe("Given DCMAW_ASYNC_CRI_5XXERROR event successfully to write to TxMA", () => {
+        it("Writes DCMAW_ASYNC_CRI_5XXERROR event to TxMA", () => {
+          expect(mockWriteGenericEventSuccessResult).toBeCalledWith({
+            eventName: "DCMAW_ASYNC_CRI_5XXERROR",
+            componentId: "mockIssuer",
+            getNowInMilliseconds: Date.now,
+            govukSigninJourneyId: undefined,
+            sessionId: mockSessionId,
+            sub: undefined,
+            ipAddress: "1.1.1.1",
+            txmaAuditEncoded: "mockTxmaAuditEncodedHeader",
+            redirect_uri: undefined,
+            suspected_fraud_signal: undefined,
+          });
+        });
+
+        it("Returns 500 Internal server error", async () => {
+          expect(result).toStrictEqual({
+            statusCode: 500,
+            body: JSON.stringify({
+              error: "server_error",
+              error_description: "Internal Server Error",
+            }),
+            headers: expectedSecurityHeaders,
+          });
+        });
+      });
+    });
+
+    describe("Given failure is due to client error", () => {
+      describe("Given session was not found", () => {
+        beforeEach(async () => {
+          dependencies.getSessionRegistry = () => ({
+            ...mockInertSessionRegistry,
+            getSession: jest.fn().mockResolvedValue(
+              errorResult({
+                errorType: GetSessionError.SESSION_NOT_FOUND,
+              }),
+            ),
+          });
+          result = await lambdaHandlerConstructor(
+            dependencies,
+            validRequest,
+            context,
+          );
+        });
+
+        describe("Given DCMAW_ASYNC_CRI_4XXERROR event fails to write to TxMA", () => {
+          beforeEach(async () => {
+            dependencies.getEventService = () => ({
+              ...mockInertEventService,
+              writeGenericEvent: jest.fn().mockResolvedValue(
+                errorResult({
+                  errorMessage: "mockError",
+                }),
+              ),
+            });
+
+            result = await lambdaHandlerConstructor(
+              dependencies,
+              validRequest,
+              context,
+            );
+          });
+
+          it("Logs the error", async () => {
+            expect(consoleErrorSpy).toHaveBeenCalledWithLogFields({
+              messageCode: "MOBILE_ASYNC_ERROR_WRITING_AUDIT_EVENT",
+              data: {
+                auditEventName: "DCMAW_ASYNC_CRI_4XXERROR",
+              },
+            });
+          });
+
+          it("Returns 500 Internal Server Error ", async () => {
+            expect(result).toStrictEqual({
+              statusCode: 500,
+              body: JSON.stringify({
+                error: "server_error",
+                error_description: "Internal Server Error",
+              }),
+              headers: expectedSecurityHeaders,
+            });
+          });
+        });
+
+        it("Writes DCMAW_ASYNC_CRI_4XXERROR event to TxMA", () => {
+          expect(mockWriteGenericEventSuccessResult).toBeCalledWith({
+            eventName: "DCMAW_ASYNC_CRI_4XXERROR",
+            componentId: "mockIssuer",
+            getNowInMilliseconds: Date.now,
+            govukSigninJourneyId: undefined,
+            sessionId: mockSessionId,
+            sub: undefined,
+            ipAddress: "1.1.1.1",
+            txmaAuditEncoded: "mockTxmaAuditEncodedHeader",
+            redirect_uri: undefined,
+            suspected_fraud_signal: undefined,
+          });
+        });
+
+        it("Returns 401 Unauthorized", () => {
+          expect(result).toStrictEqual({
+            statusCode: 401,
+            body: JSON.stringify({
+              error: "invalid_session",
+              error_description: "Session does not exist or in incorrect state",
+            }),
+            headers: expectedSecurityHeaders,
+          });
+        });
+      });
+    });
+  });
+
+  describe("Session validation", () => {
+    describe("Given the session is more than 60 minutes old", () => {
+      beforeEach(async () => {
+        jest.useFakeTimers();
+        jest.setSystemTime(NOW_IN_MILLISECONDS);
+        dependencies.getSessionRegistry = () => ({
+          ...mockSuccessfulSessionRegistry,
+          getSession: jest.fn().mockResolvedValue(
+            successResult({
+              ...validBaseSessionAttributes,
+              sessionState: SessionState.BIOMETRIC_TOKEN_ISSUED,
+              createdAt: 1704106740000,
+            }),
+          ),
+        });
+        result = await lambdaHandlerConstructor(
+          dependencies,
+          validRequest,
+          context,
+        );
+      });
+
+      describe("Given DCMAW_ASYNC_CRI_4XXERROR event fails to write to TxMA", () => {
+        beforeEach(async () => {
+          dependencies.getEventService = () => ({
+            ...mockInertEventService,
+            writeGenericEvent: jest.fn().mockResolvedValue(
+              errorResult({
+                errorMessage: "mockError",
+              }),
+            ),
+          });
+
+          result = await lambdaHandlerConstructor(
+            dependencies,
+            validRequest,
+            context,
+          );
+        });
+
+        it("Logs the error", async () => {
+          expect(consoleErrorSpy).toHaveBeenCalledWithLogFields({
+            messageCode: "MOBILE_ASYNC_ERROR_WRITING_AUDIT_EVENT",
+            data: {
+              auditEventName: "DCMAW_ASYNC_CRI_4XXERROR",
+            },
+          });
+        });
+
+        it("Returns 500 Internal Server Error ", async () => {
+          expect(result).toStrictEqual({
+            statusCode: 500,
+            body: JSON.stringify({
+              error: "server_error",
+              error_description: "Internal Server Error",
+            }),
+            headers: expectedSecurityHeaders,
+          });
+        });
+      });
+
+      it("Writes DCMAW_ASYNC_CRI_4XXERROR event to TxMA", () => {
+        expect(mockWriteGenericEventSuccessResult).toBeCalledWith({
+          eventName: "DCMAW_ASYNC_CRI_4XXERROR",
+          componentId: "mockIssuer",
+          getNowInMilliseconds: expect.anything(),
path: backend-api/src/functions/asyncTxmaEvent/asyncTxmaEventHandler.test.ts
user: login: jhumbert-dd
body: For some reason, using `Date.now` for this value does not match when using jest `useFakeTimers` and/or `setSystemTime`. Is there any other way to not use this workaround?
created_at: 2025-03-12T16:08:38Z
author_association: CONTRIBUTOR
diff_hunk: @@ -44,9 +49,46 @@ describe("POST /async/txmaEvent", () => {
         "/async/txmaEvent",
         requestBody,
       );
+    });
 
-      console.log("RESPONSE >>>>>", response);
+    it("Returns an error and 401 status code", async () => {
+      expect(response.status).toBe(401);
+      expect(response.data).toStrictEqual({
+        error: "invalid_session",
+        error_description: "Session does not exist or in incorrect state",
+      });
+      expect(response.headers).toEqual(
+        expect.objectContaining(expectedSecurityHeaders),
+      );
     });
+  });
+
+  describe("Given the request is valid", () => {
+    let sessionId: string | null;
+    let response: AxiosResponse;
+
+    beforeAll(async () => {
+      const sub = randomUUID();
+      await createSessionForSub(sub);
+      sessionId = await getActiveSessionIdFromSub(sub);
+      if (!sessionId) throw new Error("Failed to get valid session ID");
path: backend-api/tests/api-tests/txmaEvent.test.ts
user: login: jmooney-dd
body: ```suggestion
    let sessionId: string;
    let response: AxiosResponse;

    beforeAll(async () => {
      const sub = randomUUID();
      await createSessionForSub(sub);
      sessionId = await getActiveSessionIdFromSub(sub);
```
created_at: 2025-03-12T16:16:43Z
author_association: CONTRIBUTOR
diff_hunk: @@ -44,9 +49,46 @@ describe("POST /async/txmaEvent", () => {
         "/async/txmaEvent",
         requestBody,
       );
+    });
 
-      console.log("RESPONSE >>>>>", response);
+    it("Returns an error and 401 status code", async () => {
+      expect(response.status).toBe(401);
+      expect(response.data).toStrictEqual({
+        error: "invalid_session",
+        error_description: "Session does not exist or in incorrect state",
+      });
+      expect(response.headers).toEqual(
+        expect.objectContaining(expectedSecurityHeaders),
+      );
     });
+  });
+
+  describe("Given the request is valid", () => {
+    let sessionId: string | null;
+    let response: AxiosResponse;
+
+    beforeAll(async () => {
+      const sub = randomUUID();
+      await createSessionForSub(sub);
+      sessionId = await getActiveSessionIdFromSub(sub);
+      if (!sessionId) throw new Error("Failed to get valid session ID");
path: backend-api/tests/api-tests/txmaEvent.test.ts
user: login: jmooney-dd
body: getActiveSessionIdFromSub checks if sessionId is null and only returns a string. No need to handle an error here
created_at: 2025-03-12T16:17:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,24 @@
+import { AttributeValue } from "@aws-sdk/client-dynamodb";
+import { marshall } from "@aws-sdk/util-dynamodb";
+import { Result } from "../../../../utils/result";
+import { SessionAttributes } from "../../session";
+import { getBiometricTokenIssuedSessionAttributes } from "../../updateOperations/sessionAttributes/sessionAttributes";
+import { GetSessionOperation } from "../GetSessionOperation";
+
+export class TxMAEvent implements GetSessionOperation {
+  private readonly sessionId: string;
+
+  constructor({ sessionId }: { sessionId: string }) {
+    this.sessionId = sessionId;
+  }
+
+  getDynamoDbKeyExpression() {
+    return { sessionId: marshall(this.sessionId) };
+  }
path: backend-api/src/functions/common/session/getOperations/TxmaEvent/TxMAEvent.ts
user: login: jmooney-dd
body: nitpick (non-blocking): we could just pass sessionId to the getSession func, I don't necessarily think it needs it's own func here
created_at: 2025-03-12T16:19:53Z
author_association: CONTRIBUTOR
path: backend-api/src/functions/asyncTxmaEvent/asyncTxmaEventHandler.ts
user: login: jmooney-dd
body: issue: there are console.logs still in this file, please can we remove
created_at: 2025-03-12T16:21:52Z
author_association: CONTRIBUTOR
path: backend-api/generate_env_file.sh
user: login: jmooney-dd
body: question: why are we changing this file?
created_at: 2025-03-12T16:22:56Z
author_association: CONTRIBUTOR
diff_hunk: @@ -293,4 +304,129 @@ describe("DynamoDbAdapter", () => {
       });
     });
   });
+
+  describe("getSession", () => {
+    const getOperation: GetSessionOperation = new TxMAEvent({
+      sessionId: mockSessionId,
+    });
+    let result: Result<void, SessionRetrievalFailed>;
+
+    describe("On every attempt", () => {
+      beforeEach(async () => {
+        mockDynamoDbClient.on(GetItemCommand).resolves({});
+        await sessionRegistry.getSession(getOperation);
+      });
+
+      it("Logs the attempt", () => {
+        expect(consoleDebugSpy).toHaveBeenCalledWithLogFields({
+          messageCode: "MOBILE_ASYNC_GET_SESSION_ATTEMPT",
+        });
+      });
+    });
+
+    describe("Given there is an unexpected error retrieving the session", () => {
+      beforeEach(async () => {
+        mockDynamoDbClient.on(GetItemCommand).rejects("mock_error");
+        result = await sessionRegistry.getSession(getOperation);
+      });
+
+      it("Logs the failure", () => {
+        expect(consoleErrorSpy).toHaveBeenCalledWithLogFields({
+          messageCode: "MOBILE_ASYNC_GET_SESSION_UNEXPECTED_FAILURE",
+        });
+      });
+
+      it("Returns failure with server error", () => {
+        expect(result).toEqual(
+          errorResult({
+            errorType: GetSessionError.INTERNAL_SERVER_ERROR,
+          }),
+        );
+      });
+    });
+
+    describe("Given session was not found", () => {
+      beforeEach(async () => {
+        mockDynamoDbClient.on(GetItemCommand).resolves({});
+        result = await sessionRegistry.getSession(getOperation);
+      });
+
+      it("Logs the failure", () => {
+        expect(consoleErrorSpy).toHaveBeenCalledWithLogFields({
+          messageCode: "MOBILE_ASYNC_GET_SESSION_SESSION_NOT_FOUND",
+        });
+      });
+
+      it("Returns failure with an invalid session error", () => {
+        expect(result).toEqual(
+          errorResult({
+            errorType: GetSessionError.SESSION_NOT_FOUND,
+          }),
+        );
+      });
+    });
+
+    describe("Given session was found", () => {
+      describe("Given invalid session attributes were returned in response", () => {
path: backend-api/src/functions/adapters/aws/dynamo/dynamoDbAdapter.test.ts
user: login: jmooney-dd
body: question: is this not the same test as the session not found test [here](https://github.com/govuk-one-login/mobile-id-check-async/pull/469/files#diff-21922729a9b2f03203c4d24babee8687788eb5574f12ed141f79a70b7ec9670cR348)?
created_at: 2025-03-12T16:25:49Z
author_association: CONTRIBUTOR
diff_hunk: @@ -293,4 +304,129 @@ describe("DynamoDbAdapter", () => {
       });
     });
   });
+
+  describe("getSession", () => {
+    const getOperation: GetSessionOperation = new TxMAEvent({
+      sessionId: mockSessionId,
+    });
+    let result: Result<void, SessionRetrievalFailed>;
+
+    describe("On every attempt", () => {
+      beforeEach(async () => {
+        mockDynamoDbClient.on(GetItemCommand).resolves({});
+        await sessionRegistry.getSession(getOperation);
+      });
+
+      it("Logs the attempt", () => {
+        expect(consoleDebugSpy).toHaveBeenCalledWithLogFields({
+          messageCode: "MOBILE_ASYNC_GET_SESSION_ATTEMPT",
+        });
+      });
+    });
+
+    describe("Given there is an unexpected error retrieving the session", () => {
+      beforeEach(async () => {
+        mockDynamoDbClient.on(GetItemCommand).rejects("mock_error");
+        result = await sessionRegistry.getSession(getOperation);
+      });
+
+      it("Logs the failure", () => {
+        expect(consoleErrorSpy).toHaveBeenCalledWithLogFields({
+          messageCode: "MOBILE_ASYNC_GET_SESSION_UNEXPECTED_FAILURE",
+        });
+      });
+
+      it("Returns failure with server error", () => {
+        expect(result).toEqual(
+          errorResult({
+            errorType: GetSessionError.INTERNAL_SERVER_ERROR,
+          }),
+        );
+      });
+    });
+
+    describe("Given session was not found", () => {
+      beforeEach(async () => {
+        mockDynamoDbClient.on(GetItemCommand).resolves({});
+        result = await sessionRegistry.getSession(getOperation);
+      });
+
+      it("Logs the failure", () => {
+        expect(consoleErrorSpy).toHaveBeenCalledWithLogFields({
+          messageCode: "MOBILE_ASYNC_GET_SESSION_SESSION_NOT_FOUND",
+        });
+      });
+
+      it("Returns failure with an invalid session error", () => {
+        expect(result).toEqual(
+          errorResult({
+            errorType: GetSessionError.SESSION_NOT_FOUND,
+          }),
+        );
+      });
+    });
+
+    describe("Given session was found", () => {
+      describe("Given invalid session attributes were returned in response", () => {
+        beforeEach(async () => {
+          mockDynamoDbClient.on(GetItemCommand).resolves({
+            Item: marshall({
+              clientId: "mockClientId",
+              govukSigninJourneyId: "mockGovukSigninJourneyId",
+              createdAt: 12345,
+              issuer: "mockIssuer",
+              sessionId: mockSessionId,
+              sessionState: SessionState.AUTH_SESSION_CREATED,
+              clientState: "mockClientState",
+              subjectIdentifier: "mockSubjectIdentifier",
+              timeToLive: 12345,
+            }),
+          });
+          result = await sessionRegistry.getSession(getOperation);
+        });
+
+        it("Logs the failure", () => {
+          expect(consoleErrorSpy).toHaveBeenCalledWithLogFields({
+            messageCode: "MOBILE_ASYNC_GET_SESSION_SESSION_NOT_FOUND",
+          });
+        });
+
+        it("Returns failure with server error", () => {
+          expect(result).toEqual(
+            errorResult({
+              errorType: GetSessionError.SESSION_NOT_FOUND,
+            }),
+          );
+        });
+      });
+
+      describe("Given valid session attributes were returned in response", () => {
+        const validBiometricTokenIssuedSessionAttributesItem = marshall({
+          ...validBiometricTokenIssuedSessionAttributes,
+          createdAt: 1704106860000, // 2024-01-01T11:01:00.000Z
+        });
+
+        beforeEach(async () => {
+          mockDynamoDbClient.on(GetItemCommand).resolves({
+            Item: validBiometricTokenIssuedSessionAttributesItem,
+          });
+          result = await sessionRegistry.getSession(getOperation);
+        });
+
+        it("Logs the failure", () => {
path: backend-api/src/functions/adapters/aws/dynamo/dynamoDbAdapter.test.ts
user: login: jmooney-dd
body: ```suggestion
        it("Logs the success", () => {
```

or something similar
created_at: 2025-03-12T16:26:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -56,6 +77,40 @@ describe("Async TxMA Event", () => {
     });
   });
 
+  describe("Config validation", () => {
+    describe.each(["SESSION_TABLE_NAME"])(
+      "Given %s environment variable is missing",
+      (envVar: string) => {
+        beforeEach(async () => {
+          delete dependencies.env[envVar];
+          result = await lambdaHandlerConstructor(
+            dependencies,
+            validRequest,
+            context,
+          );
+        });
+        it("returns 500 Internal server error", async () => {
path: backend-api/src/functions/asyncTxmaEvent/asyncTxmaEventHandler.test.ts
user: login: jmooney-dd
body: ```suggestion
        it("returns 500 Internal server error", () => {
```
created_at: 2025-03-12T16:28:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -56,6 +77,40 @@ describe("Async TxMA Event", () => {
     });
   });
 
+  describe("Config validation", () => {
+    describe.each(["SESSION_TABLE_NAME"])(
+      "Given %s environment variable is missing",
+      (envVar: string) => {
+        beforeEach(async () => {
+          delete dependencies.env[envVar];
+          result = await lambdaHandlerConstructor(
+            dependencies,
+            validRequest,
+            context,
+          );
+        });
+        it("returns 500 Internal server error", async () => {
+          expect(result).toStrictEqual({
+            statusCode: 500,
+            body: JSON.stringify({
+              error: "server_error",
+              error_description: "Internal Server Error",
+            }),
+            headers: expectedSecurityHeaders,
+          });
+        });
+        it("logs INVALID_CONFIG", async () => {
path: backend-api/src/functions/asyncTxmaEvent/asyncTxmaEventHandler.test.ts
user: login: jmooney-dd
body: ```suggestion
        it("logs INVALID_CONFIG", () => {
```
created_at: 2025-03-12T16:28:42Z
author_association: CONTRIBUTOR
path: backend-api/generate_env_file.sh
user: login: jhumbert-dd
body: Ah yes, I will revert this.
created_at: 2025-03-12T16:31:11Z
author_association: CONTRIBUTOR
path: backend-api/generate_env_file.sh
user: login: jhumbert-dd
body: Reverted
created_at: 2025-03-12T16:33:14Z
author_association: CONTRIBUTOR
diff_hunk: @@ -90,8 +145,281 @@ describe("Async TxMA Event", () => {
     });
   });
 
+  describe("Retrieving a session", () => {
+    describe("Given there is an error retrieving the session", () => {
+      beforeEach(async () => {
+        dependencies.getSessionRegistry = () => ({
+          ...mockInertSessionRegistry,
+          getSession: jest.fn().mockResolvedValue(
+            errorResult({
+              errorType: GetSessionError.INTERNAL_SERVER_ERROR,
+            }),
+          ),
+        });
+        result = await lambdaHandlerConstructor(
+          dependencies,
+          validRequest,
+          context,
+        );
+      });
+
+      describe("Given DCMAW_ASYNC_CRI_5XXERROR event fails to write to TxMA", () => {
+        beforeEach(async () => {
+          dependencies.getEventService = () => ({
+            ...mockInertEventService,
+            writeGenericEvent: jest.fn().mockResolvedValue(
+              errorResult({
+                errorMessage: "mockError",
+              }),
+            ),
+          });
+
+          result = await lambdaHandlerConstructor(
+            dependencies,
+            validRequest,
+            context,
+          );
+        });
+
+        it("Logs the error", async () => {
+          expect(consoleErrorSpy).toHaveBeenCalledWithLogFields({
+            messageCode: "MOBILE_ASYNC_ERROR_WRITING_AUDIT_EVENT",
+            data: {
+              auditEventName: "DCMAW_ASYNC_CRI_5XXERROR",
+            },
+          });
+        });
+
+        it("Returns 500 Internal server error", async () => {
+          expect(result).toStrictEqual({
+            statusCode: 500,
+            body: JSON.stringify({
+              error: "server_error",
+              error_description: "Internal Server Error",
+            }),
+            headers: expectedSecurityHeaders,
+          });
+        });
+      });
+
+      describe("Given DCMAW_ASYNC_CRI_5XXERROR event successfully to write to TxMA", () => {
+        it("Writes DCMAW_ASYNC_CRI_5XXERROR event to TxMA", () => {
+          expect(mockWriteGenericEventSuccessResult).toBeCalledWith({
+            eventName: "DCMAW_ASYNC_CRI_5XXERROR",
+            componentId: "mockIssuer",
+            getNowInMilliseconds: Date.now,
+            govukSigninJourneyId: undefined,
+            sessionId: mockSessionId,
+            sub: undefined,
+            ipAddress: "1.1.1.1",
+            txmaAuditEncoded: "mockTxmaAuditEncodedHeader",
+            redirect_uri: undefined,
+            suspected_fraud_signal: undefined,
+          });
+        });
+
+        it("Returns 500 Internal server error", async () => {
+          expect(result).toStrictEqual({
+            statusCode: 500,
+            body: JSON.stringify({
+              error: "server_error",
+              error_description: "Internal Server Error",
+            }),
+            headers: expectedSecurityHeaders,
+          });
+        });
+      });
+    });
+
+    describe("Given failure is due to client error", () => {
+      describe("Given session was not found", () => {
+        beforeEach(async () => {
+          dependencies.getSessionRegistry = () => ({
+            ...mockInertSessionRegistry,
+            getSession: jest.fn().mockResolvedValue(
+              errorResult({
+                errorType: GetSessionError.SESSION_NOT_FOUND,
+              }),
+            ),
+          });
+          result = await lambdaHandlerConstructor(
+            dependencies,
+            validRequest,
+            context,
+          );
+        });
+
+        describe("Given DCMAW_ASYNC_CRI_4XXERROR event fails to write to TxMA", () => {
+          beforeEach(async () => {
+            dependencies.getEventService = () => ({
+              ...mockInertEventService,
+              writeGenericEvent: jest.fn().mockResolvedValue(
+                errorResult({
+                  errorMessage: "mockError",
+                }),
+              ),
+            });
+
+            result = await lambdaHandlerConstructor(
+              dependencies,
+              validRequest,
+              context,
+            );
+          });
+
+          it("Logs the error", async () => {
+            expect(consoleErrorSpy).toHaveBeenCalledWithLogFields({
+              messageCode: "MOBILE_ASYNC_ERROR_WRITING_AUDIT_EVENT",
+              data: {
+                auditEventName: "DCMAW_ASYNC_CRI_4XXERROR",
+              },
+            });
+          });
+
+          it("Returns 500 Internal Server Error ", async () => {
+            expect(result).toStrictEqual({
+              statusCode: 500,
+              body: JSON.stringify({
+                error: "server_error",
+                error_description: "Internal Server Error",
+              }),
+              headers: expectedSecurityHeaders,
+            });
+          });
+        });
+
+        it("Writes DCMAW_ASYNC_CRI_4XXERROR event to TxMA", () => {
+          expect(mockWriteGenericEventSuccessResult).toBeCalledWith({
+            eventName: "DCMAW_ASYNC_CRI_4XXERROR",
+            componentId: "mockIssuer",
+            getNowInMilliseconds: Date.now,
+            govukSigninJourneyId: undefined,
+            sessionId: mockSessionId,
+            sub: undefined,
+            ipAddress: "1.1.1.1",
+            txmaAuditEncoded: "mockTxmaAuditEncodedHeader",
+            redirect_uri: undefined,
+            suspected_fraud_signal: undefined,
+          });
+        });
+
+        it("Returns 401 Unauthorized", () => {
+          expect(result).toStrictEqual({
+            statusCode: 401,
+            body: JSON.stringify({
+              error: "invalid_session",
+              error_description: "Session does not exist or in incorrect state",
+            }),
+            headers: expectedSecurityHeaders,
+          });
+        });
+      });
+    });
+  });
+
+  describe("Session validation", () => {
+    describe("Given the session is more than 60 minutes old", () => {
+      beforeEach(async () => {
+        jest.useFakeTimers();
+        jest.setSystemTime(NOW_IN_MILLISECONDS);
+        dependencies.getSessionRegistry = () => ({
+          ...mockSuccessfulSessionRegistry,
+          getSession: jest.fn().mockResolvedValue(
+            successResult({
+              ...validBaseSessionAttributes,
+              sessionState: SessionState.BIOMETRIC_TOKEN_ISSUED,
+              createdAt: 1704106740000,
+            }),
+          ),
+        });
+        result = await lambdaHandlerConstructor(
+          dependencies,
+          validRequest,
+          context,
+        );
+      });
+
+      describe("Given DCMAW_ASYNC_CRI_4XXERROR event fails to write to TxMA", () => {
+        beforeEach(async () => {
+          dependencies.getEventService = () => ({
+            ...mockInertEventService,
+            writeGenericEvent: jest.fn().mockResolvedValue(
+              errorResult({
+                errorMessage: "mockError",
+              }),
+            ),
+          });
+
+          result = await lambdaHandlerConstructor(
+            dependencies,
+            validRequest,
+            context,
+          );
+        });
+
+        it("Logs the error", async () => {
+          expect(consoleErrorSpy).toHaveBeenCalledWithLogFields({
+            messageCode: "MOBILE_ASYNC_ERROR_WRITING_AUDIT_EVENT",
+            data: {
+              auditEventName: "DCMAW_ASYNC_CRI_4XXERROR",
+            },
+          });
+        });
+
+        it("Returns 500 Internal Server Error ", async () => {
+          expect(result).toStrictEqual({
+            statusCode: 500,
+            body: JSON.stringify({
+              error: "server_error",
+              error_description: "Internal Server Error",
+            }),
+            headers: expectedSecurityHeaders,
+          });
+        });
+      });
+
+      it("Writes DCMAW_ASYNC_CRI_4XXERROR event to TxMA", () => {
+        expect(mockWriteGenericEventSuccessResult).toBeCalledWith({
+          eventName: "DCMAW_ASYNC_CRI_4XXERROR",
+          componentId: "mockIssuer",
+          getNowInMilliseconds: expect.anything(),
path: backend-api/src/functions/asyncTxmaEvent/asyncTxmaEventHandler.test.ts
user: login: jmooney-dd
body: In biometricSessionFinished lambda unit tests we use fake timers but still call this with Date.now, having a look at what's different...
created_at: 2025-03-12T16:37:01Z
author_association: CONTRIBUTOR
diff_hunk: @@ -293,4 +304,129 @@ describe("DynamoDbAdapter", () => {
       });
     });
   });
+
+  describe("getSession", () => {
+    const getOperation: GetSessionOperation = new TxMAEvent({
+      sessionId: mockSessionId,
+    });
+    let result: Result<void, SessionRetrievalFailed>;
+
+    describe("On every attempt", () => {
+      beforeEach(async () => {
+        mockDynamoDbClient.on(GetItemCommand).resolves({});
+        await sessionRegistry.getSession(getOperation);
+      });
+
+      it("Logs the attempt", () => {
+        expect(consoleDebugSpy).toHaveBeenCalledWithLogFields({
+          messageCode: "MOBILE_ASYNC_GET_SESSION_ATTEMPT",
+        });
+      });
+    });
+
+    describe("Given there is an unexpected error retrieving the session", () => {
+      beforeEach(async () => {
+        mockDynamoDbClient.on(GetItemCommand).rejects("mock_error");
+        result = await sessionRegistry.getSession(getOperation);
+      });
+
+      it("Logs the failure", () => {
+        expect(consoleErrorSpy).toHaveBeenCalledWithLogFields({
+          messageCode: "MOBILE_ASYNC_GET_SESSION_UNEXPECTED_FAILURE",
+        });
+      });
+
+      it("Returns failure with server error", () => {
+        expect(result).toEqual(
+          errorResult({
+            errorType: GetSessionError.INTERNAL_SERVER_ERROR,
+          }),
+        );
+      });
+    });
+
+    describe("Given session was not found", () => {
+      beforeEach(async () => {
+        mockDynamoDbClient.on(GetItemCommand).resolves({});
+        result = await sessionRegistry.getSession(getOperation);
+      });
+
+      it("Logs the failure", () => {
+        expect(consoleErrorSpy).toHaveBeenCalledWithLogFields({
+          messageCode: "MOBILE_ASYNC_GET_SESSION_SESSION_NOT_FOUND",
+        });
+      });
+
+      it("Returns failure with an invalid session error", () => {
+        expect(result).toEqual(
+          errorResult({
+            errorType: GetSessionError.SESSION_NOT_FOUND,
+          }),
+        );
+      });
+    });
+
+    describe("Given session was found", () => {
+      describe("Given invalid session attributes were returned in response", () => {
path: backend-api/src/functions/adapters/aws/dynamo/dynamoDbAdapter.test.ts
user: login: jmooney-dd
body: Discussed, I thought this was the lambda file, ignore me!
created_at: 2025-03-12T17:06:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -90,8 +145,281 @@ describe("Async TxMA Event", () => {
     });
   });
 
+  describe("Retrieving a session", () => {
+    describe("Given there is an error retrieving the session", () => {
+      beforeEach(async () => {
+        dependencies.getSessionRegistry = () => ({
+          ...mockInertSessionRegistry,
+          getSession: jest.fn().mockResolvedValue(
+            errorResult({
+              errorType: GetSessionError.INTERNAL_SERVER_ERROR,
+            }),
+          ),
+        });
+        result = await lambdaHandlerConstructor(
+          dependencies,
+          validRequest,
+          context,
+        );
+      });
+
+      describe("Given DCMAW_ASYNC_CRI_5XXERROR event fails to write to TxMA", () => {
+        beforeEach(async () => {
+          dependencies.getEventService = () => ({
+            ...mockInertEventService,
+            writeGenericEvent: jest.fn().mockResolvedValue(
+              errorResult({
+                errorMessage: "mockError",
+              }),
+            ),
+          });
+
+          result = await lambdaHandlerConstructor(
+            dependencies,
+            validRequest,
+            context,
+          );
+        });
+
+        it("Logs the error", async () => {
+          expect(consoleErrorSpy).toHaveBeenCalledWithLogFields({
+            messageCode: "MOBILE_ASYNC_ERROR_WRITING_AUDIT_EVENT",
+            data: {
+              auditEventName: "DCMAW_ASYNC_CRI_5XXERROR",
+            },
+          });
+        });
+
+        it("Returns 500 Internal server error", async () => {
+          expect(result).toStrictEqual({
+            statusCode: 500,
+            body: JSON.stringify({
+              error: "server_error",
+              error_description: "Internal Server Error",
+            }),
+            headers: expectedSecurityHeaders,
+          });
+        });
+      });
+
+      describe("Given DCMAW_ASYNC_CRI_5XXERROR event successfully to write to TxMA", () => {
+        it("Writes DCMAW_ASYNC_CRI_5XXERROR event to TxMA", () => {
+          expect(mockWriteGenericEventSuccessResult).toBeCalledWith({
+            eventName: "DCMAW_ASYNC_CRI_5XXERROR",
+            componentId: "mockIssuer",
+            getNowInMilliseconds: Date.now,
+            govukSigninJourneyId: undefined,
+            sessionId: mockSessionId,
+            sub: undefined,
+            ipAddress: "1.1.1.1",
+            txmaAuditEncoded: "mockTxmaAuditEncodedHeader",
+            redirect_uri: undefined,
+            suspected_fraud_signal: undefined,
+          });
+        });
+
+        it("Returns 500 Internal server error", async () => {
+          expect(result).toStrictEqual({
+            statusCode: 500,
+            body: JSON.stringify({
+              error: "server_error",
+              error_description: "Internal Server Error",
+            }),
+            headers: expectedSecurityHeaders,
+          });
+        });
+      });
+    });
+
+    describe("Given failure is due to client error", () => {
+      describe("Given session was not found", () => {
+        beforeEach(async () => {
+          dependencies.getSessionRegistry = () => ({
+            ...mockInertSessionRegistry,
+            getSession: jest.fn().mockResolvedValue(
+              errorResult({
+                errorType: GetSessionError.SESSION_NOT_FOUND,
+              }),
+            ),
+          });
+          result = await lambdaHandlerConstructor(
+            dependencies,
+            validRequest,
+            context,
+          );
+        });
+
+        describe("Given DCMAW_ASYNC_CRI_4XXERROR event fails to write to TxMA", () => {
+          beforeEach(async () => {
+            dependencies.getEventService = () => ({
+              ...mockInertEventService,
+              writeGenericEvent: jest.fn().mockResolvedValue(
+                errorResult({
+                  errorMessage: "mockError",
+                }),
+              ),
+            });
+
+            result = await lambdaHandlerConstructor(
+              dependencies,
+              validRequest,
+              context,
+            );
+          });
+
+          it("Logs the error", async () => {
+            expect(consoleErrorSpy).toHaveBeenCalledWithLogFields({
+              messageCode: "MOBILE_ASYNC_ERROR_WRITING_AUDIT_EVENT",
+              data: {
+                auditEventName: "DCMAW_ASYNC_CRI_4XXERROR",
+              },
+            });
+          });
+
+          it("Returns 500 Internal Server Error ", async () => {
+            expect(result).toStrictEqual({
+              statusCode: 500,
+              body: JSON.stringify({
+                error: "server_error",
+                error_description: "Internal Server Error",
+              }),
+              headers: expectedSecurityHeaders,
+            });
+          });
+        });
+
+        it("Writes DCMAW_ASYNC_CRI_4XXERROR event to TxMA", () => {
+          expect(mockWriteGenericEventSuccessResult).toBeCalledWith({
+            eventName: "DCMAW_ASYNC_CRI_4XXERROR",
+            componentId: "mockIssuer",
+            getNowInMilliseconds: Date.now,
+            govukSigninJourneyId: undefined,
+            sessionId: mockSessionId,
+            sub: undefined,
+            ipAddress: "1.1.1.1",
+            txmaAuditEncoded: "mockTxmaAuditEncodedHeader",
+            redirect_uri: undefined,
+            suspected_fraud_signal: undefined,
+          });
+        });
+
+        it("Returns 401 Unauthorized", () => {
+          expect(result).toStrictEqual({
+            statusCode: 401,
+            body: JSON.stringify({
+              error: "invalid_session",
+              error_description: "Session does not exist or in incorrect state",
+            }),
+            headers: expectedSecurityHeaders,
+          });
+        });
+      });
+    });
+  });
+
+  describe("Session validation", () => {
+    describe("Given the session is more than 60 minutes old", () => {
+      beforeEach(async () => {
+        jest.useFakeTimers();
+        jest.setSystemTime(NOW_IN_MILLISECONDS);
+        dependencies.getSessionRegistry = () => ({
+          ...mockSuccessfulSessionRegistry,
+          getSession: jest.fn().mockResolvedValue(
+            successResult({
+              ...validBaseSessionAttributes,
+              sessionState: SessionState.BIOMETRIC_TOKEN_ISSUED,
+              createdAt: 1704106740000,
+            }),
+          ),
+        });
+        result = await lambdaHandlerConstructor(
+          dependencies,
+          validRequest,
+          context,
+        );
+      });
+
+      describe("Given DCMAW_ASYNC_CRI_4XXERROR event fails to write to TxMA", () => {
+        beforeEach(async () => {
+          dependencies.getEventService = () => ({
+            ...mockInertEventService,
+            writeGenericEvent: jest.fn().mockResolvedValue(
+              errorResult({
+                errorMessage: "mockError",
+              }),
+            ),
+          });
+
+          result = await lambdaHandlerConstructor(
+            dependencies,
+            validRequest,
+            context,
+          );
+        });
+
+        it("Logs the error", async () => {
+          expect(consoleErrorSpy).toHaveBeenCalledWithLogFields({
+            messageCode: "MOBILE_ASYNC_ERROR_WRITING_AUDIT_EVENT",
+            data: {
+              auditEventName: "DCMAW_ASYNC_CRI_4XXERROR",
+            },
+          });
+        });
+
+        it("Returns 500 Internal Server Error ", async () => {
+          expect(result).toStrictEqual({
+            statusCode: 500,
+            body: JSON.stringify({
+              error: "server_error",
+              error_description: "Internal Server Error",
+            }),
+            headers: expectedSecurityHeaders,
+          });
+        });
+      });
+
+      it("Writes DCMAW_ASYNC_CRI_4XXERROR event to TxMA", () => {
+        expect(mockWriteGenericEventSuccessResult).toBeCalledWith({
+          eventName: "DCMAW_ASYNC_CRI_4XXERROR",
+          componentId: "mockIssuer",
+          getNowInMilliseconds: expect.anything(),
path: backend-api/src/functions/asyncTxmaEvent/asyncTxmaEventHandler.test.ts
user: login: jmooney-dd
body: Discussed and we have a fix
created_at: 2025-03-12T17:06:52Z
author_association: CONTRIBUTOR
diff_hunk: @@ -90,8 +145,281 @@ describe("Async TxMA Event", () => {
     });
   });
 
+  describe("Retrieving a session", () => {
+    describe("Given there is an error retrieving the session", () => {
+      beforeEach(async () => {
+        dependencies.getSessionRegistry = () => ({
+          ...mockInertSessionRegistry,
+          getSession: jest.fn().mockResolvedValue(
+            errorResult({
+              errorType: GetSessionError.INTERNAL_SERVER_ERROR,
+            }),
+          ),
+        });
+        result = await lambdaHandlerConstructor(
+          dependencies,
+          validRequest,
+          context,
+        );
+      });
+
+      describe("Given DCMAW_ASYNC_CRI_5XXERROR event fails to write to TxMA", () => {
+        beforeEach(async () => {
+          dependencies.getEventService = () => ({
+            ...mockInertEventService,
+            writeGenericEvent: jest.fn().mockResolvedValue(
+              errorResult({
+                errorMessage: "mockError",
+              }),
+            ),
+          });
+
+          result = await lambdaHandlerConstructor(
+            dependencies,
+            validRequest,
+            context,
+          );
+        });
+
+        it("Logs the error", async () => {
+          expect(consoleErrorSpy).toHaveBeenCalledWithLogFields({
+            messageCode: "MOBILE_ASYNC_ERROR_WRITING_AUDIT_EVENT",
+            data: {
+              auditEventName: "DCMAW_ASYNC_CRI_5XXERROR",
+            },
+          });
+        });
+
+        it("Returns 500 Internal server error", async () => {
+          expect(result).toStrictEqual({
+            statusCode: 500,
+            body: JSON.stringify({
+              error: "server_error",
+              error_description: "Internal Server Error",
+            }),
+            headers: expectedSecurityHeaders,
+          });
+        });
+      });
+
+      describe("Given DCMAW_ASYNC_CRI_5XXERROR event successfully to write to TxMA", () => {
+        it("Writes DCMAW_ASYNC_CRI_5XXERROR event to TxMA", () => {
+          expect(mockWriteGenericEventSuccessResult).toBeCalledWith({
+            eventName: "DCMAW_ASYNC_CRI_5XXERROR",
+            componentId: "mockIssuer",
+            getNowInMilliseconds: Date.now,
+            govukSigninJourneyId: undefined,
+            sessionId: mockSessionId,
+            sub: undefined,
+            ipAddress: "1.1.1.1",
+            txmaAuditEncoded: "mockTxmaAuditEncodedHeader",
+            redirect_uri: undefined,
+            suspected_fraud_signal: undefined,
+          });
+        });
+
+        it("Returns 500 Internal server error", async () => {
+          expect(result).toStrictEqual({
+            statusCode: 500,
+            body: JSON.stringify({
+              error: "server_error",
+              error_description: "Internal Server Error",
+            }),
+            headers: expectedSecurityHeaders,
+          });
+        });
+      });
+    });
+
+    describe("Given failure is due to client error", () => {
+      describe("Given session was not found", () => {
+        beforeEach(async () => {
+          dependencies.getSessionRegistry = () => ({
+            ...mockInertSessionRegistry,
+            getSession: jest.fn().mockResolvedValue(
+              errorResult({
+                errorType: GetSessionError.SESSION_NOT_FOUND,
+              }),
+            ),
+          });
+          result = await lambdaHandlerConstructor(
+            dependencies,
+            validRequest,
+            context,
+          );
+        });
+
+        describe("Given DCMAW_ASYNC_CRI_4XXERROR event fails to write to TxMA", () => {
+          beforeEach(async () => {
+            dependencies.getEventService = () => ({
+              ...mockInertEventService,
+              writeGenericEvent: jest.fn().mockResolvedValue(
+                errorResult({
+                  errorMessage: "mockError",
+                }),
+              ),
+            });
+
+            result = await lambdaHandlerConstructor(
+              dependencies,
+              validRequest,
+              context,
+            );
+          });
+
+          it("Logs the error", async () => {
+            expect(consoleErrorSpy).toHaveBeenCalledWithLogFields({
+              messageCode: "MOBILE_ASYNC_ERROR_WRITING_AUDIT_EVENT",
+              data: {
+                auditEventName: "DCMAW_ASYNC_CRI_4XXERROR",
+              },
+            });
+          });
+
+          it("Returns 500 Internal Server Error ", async () => {
+            expect(result).toStrictEqual({
+              statusCode: 500,
+              body: JSON.stringify({
+                error: "server_error",
+                error_description: "Internal Server Error",
+              }),
+              headers: expectedSecurityHeaders,
+            });
+          });
+        });
+
+        it("Writes DCMAW_ASYNC_CRI_4XXERROR event to TxMA", () => {
+          expect(mockWriteGenericEventSuccessResult).toBeCalledWith({
+            eventName: "DCMAW_ASYNC_CRI_4XXERROR",
+            componentId: "mockIssuer",
+            getNowInMilliseconds: Date.now,
+            govukSigninJourneyId: undefined,
+            sessionId: mockSessionId,
+            sub: undefined,
+            ipAddress: "1.1.1.1",
+            txmaAuditEncoded: "mockTxmaAuditEncodedHeader",
+            redirect_uri: undefined,
+            suspected_fraud_signal: undefined,
+          });
+        });
+
+        it("Returns 401 Unauthorized", () => {
+          expect(result).toStrictEqual({
+            statusCode: 401,
+            body: JSON.stringify({
+              error: "invalid_session",
+              error_description: "Session does not exist or in incorrect state",
+            }),
+            headers: expectedSecurityHeaders,
+          });
+        });
+      });
+    });
+  });
+
+  describe("Session validation", () => {
+    describe("Given the session is more than 60 minutes old", () => {
+      beforeEach(async () => {
+        jest.useFakeTimers();
+        jest.setSystemTime(NOW_IN_MILLISECONDS);
+        dependencies.getSessionRegistry = () => ({
+          ...mockSuccessfulSessionRegistry,
+          getSession: jest.fn().mockResolvedValue(
+            successResult({
+              ...validBaseSessionAttributes,
+              sessionState: SessionState.BIOMETRIC_TOKEN_ISSUED,
+              createdAt: 1704106740000,
+            }),
+          ),
+        });
+        result = await lambdaHandlerConstructor(
+          dependencies,
+          validRequest,
+          context,
+        );
+      });
+
+      describe("Given DCMAW_ASYNC_CRI_4XXERROR event fails to write to TxMA", () => {
+        beforeEach(async () => {
+          dependencies.getEventService = () => ({
+            ...mockInertEventService,
+            writeGenericEvent: jest.fn().mockResolvedValue(
+              errorResult({
+                errorMessage: "mockError",
+              }),
+            ),
+          });
+
+          result = await lambdaHandlerConstructor(
+            dependencies,
+            validRequest,
+            context,
+          );
+        });
+
+        it("Logs the error", async () => {
+          expect(consoleErrorSpy).toHaveBeenCalledWithLogFields({
+            messageCode: "MOBILE_ASYNC_ERROR_WRITING_AUDIT_EVENT",
+            data: {
+              auditEventName: "DCMAW_ASYNC_CRI_4XXERROR",
+            },
+          });
+        });
+
+        it("Returns 500 Internal Server Error ", async () => {
+          expect(result).toStrictEqual({
+            statusCode: 500,
+            body: JSON.stringify({
+              error: "server_error",
+              error_description: "Internal Server Error",
+            }),
+            headers: expectedSecurityHeaders,
+          });
+        });
+      });
+
+      it("Writes DCMAW_ASYNC_CRI_4XXERROR event to TxMA", () => {
+        expect(mockWriteGenericEventSuccessResult).toBeCalledWith({
+          eventName: "DCMAW_ASYNC_CRI_4XXERROR",
+          componentId: "mockIssuer",
+          getNowInMilliseconds: expect.anything(),
path: backend-api/src/functions/asyncTxmaEvent/asyncTxmaEventHandler.test.ts
user: login: jhumbert-dd
body: Resolved. Thanks @jmooney-dd 
created_at: 2025-03-12T17:09:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -44,9 +49,46 @@ describe("POST /async/txmaEvent", () => {
         "/async/txmaEvent",
         requestBody,
       );
+    });
 
-      console.log("RESPONSE >>>>>", response);
+    it("Returns an error and 401 status code", async () => {
+      expect(response.status).toBe(401);
+      expect(response.data).toStrictEqual({
+        error: "invalid_session",
+        error_description: "Session does not exist or in incorrect state",
+      });
+      expect(response.headers).toEqual(
+        expect.objectContaining(expectedSecurityHeaders),
+      );
     });
+  });
+
+  describe("Given the request is valid", () => {
+    let sessionId: string | null;
+    let response: AxiosResponse;
+
+    beforeAll(async () => {
+      const sub = randomUUID();
+      await createSessionForSub(sub);
+      sessionId = await getActiveSessionIdFromSub(sub);
+      if (!sessionId) throw new Error("Failed to get valid session ID");
path: backend-api/tests/api-tests/txmaEvent.test.ts
user: login: jhumbert-dd
body: updated
created_at: 2025-03-12T17:09:13Z
author_association: CONTRIBUTOR
path: backend-api/src/functions/asyncTxmaEvent/asyncTxmaEventHandler.ts
user: login: jhumbert-dd
body: Removed
created_at: 2025-03-12T17:12:23Z
author_association: CONTRIBUTOR
diff_hunk: @@ -56,6 +77,40 @@ describe("Async TxMA Event", () => {
     });
   });
 
+  describe("Config validation", () => {
+    describe.each(["SESSION_TABLE_NAME"])(
+      "Given %s environment variable is missing",
+      (envVar: string) => {
+        beforeEach(async () => {
+          delete dependencies.env[envVar];
+          result = await lambdaHandlerConstructor(
+            dependencies,
+            validRequest,
+            context,
+          );
+        });
+        it("returns 500 Internal server error", async () => {
path: backend-api/src/functions/asyncTxmaEvent/asyncTxmaEventHandler.test.ts
user: login: jhumbert-dd
body: updated
created_at: 2025-03-12T17:12:53Z
author_association: CONTRIBUTOR
diff_hunk: @@ -56,6 +77,40 @@ describe("Async TxMA Event", () => {
     });
   });
 
+  describe("Config validation", () => {
+    describe.each(["SESSION_TABLE_NAME"])(
+      "Given %s environment variable is missing",
+      (envVar: string) => {
+        beforeEach(async () => {
+          delete dependencies.env[envVar];
+          result = await lambdaHandlerConstructor(
+            dependencies,
+            validRequest,
+            context,
+          );
+        });
+        it("returns 500 Internal server error", async () => {
+          expect(result).toStrictEqual({
+            statusCode: 500,
+            body: JSON.stringify({
+              error: "server_error",
+              error_description: "Internal Server Error",
+            }),
+            headers: expectedSecurityHeaders,
+          });
+        });
+        it("logs INVALID_CONFIG", async () => {
path: backend-api/src/functions/asyncTxmaEvent/asyncTxmaEventHandler.test.ts
user: login: jhumbert-dd
body: updated
created_at: 2025-03-12T17:12:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,24 @@
+import { AttributeValue } from "@aws-sdk/client-dynamodb";
+import { marshall } from "@aws-sdk/util-dynamodb";
+import { Result } from "../../../../utils/result";
+import { SessionAttributes } from "../../session";
+import { getBiometricTokenIssuedSessionAttributes } from "../../updateOperations/sessionAttributes/sessionAttributes";
+import { GetSessionOperation } from "../GetSessionOperation";
+
+export class TxMAEvent implements GetSessionOperation {
+  private readonly sessionId: string;
+
+  constructor({ sessionId }: { sessionId: string }) {
+    this.sessionId = sessionId;
+  }
+
+  getDynamoDbKeyExpression() {
+    return { sessionId: marshall(this.sessionId) };
+  }
path: backend-api/src/functions/common/session/getOperations/TxmaEvent/TxMAEvent.ts
user: login: jhumbert-dd
body: Updated following discussion
created_at: 2025-03-12T17:24:52Z
author_association: CONTRIBUTOR
diff_hunk: @@ -32,6 +50,34 @@ export async function lambdaHandlerConstructor(
     });
     return badRequestResponse("invalid_request", errorMessage);
   }
+  const { sessionId } = validateRequestBodyResult.value;
+  const eventService = dependencies.getEventService(config.TXMA_SQS);
+
+  const sessionRegistry = dependencies.getSessionRegistry(
+    config.SESSION_TABLE_NAME,
+  );
+  const getSessionResult = await sessionRegistry.getSession(
+    sessionId,
+    new TxMAEvent(),
+  );
+  const { ipAddress, txmaAuditEncoded } = getAuditData(event);
+  const eventData = {
+    sessionId,
+    issuer: config.ISSUER,
+    ipAddress,
+    txmaAuditEncoded,
+  };
+  if (getSessionResult.isError) {
+    return handleGetSessionError(
+      eventService,
+      getSessionResult.value,
+      eventData,
+    );
+  }
+  const sessionAttributes = getSessionResult.value;
+
+  const isSessionValid = validateSession(sessionAttributes);
+  if (!isSessionValid) return handleSessionNotFound(eventService, eventData);
path: backend-api/src/functions/asyncTxmaEvent/asyncTxmaEventHandler.ts
user: login: jmooney-dd
body: ```suggestion
  if (!validateSession(sessionAttributes)) return handleSessionNotFound(eventService, eventData);
```

Entirely non-blocking. My thought is we don't use the const anywhere else and the function name provides enough meaning imo, happy for you to resolve tho
created_at: 2025-03-13T10:41:13Z
author_association: CONTRIBUTOR
diff_hunk: @@ -32,6 +50,34 @@ export async function lambdaHandlerConstructor(
     });
     return badRequestResponse("invalid_request", errorMessage);
   }
+  const { sessionId } = validateRequestBodyResult.value;
+  const eventService = dependencies.getEventService(config.TXMA_SQS);
+
+  const sessionRegistry = dependencies.getSessionRegistry(
+    config.SESSION_TABLE_NAME,
+  );
+  const getSessionResult = await sessionRegistry.getSession(
+    sessionId,
+    new TxMAEvent(),
+  );
+  const { ipAddress, txmaAuditEncoded } = getAuditData(event);
+  const eventData = {
+    sessionId,
+    issuer: config.ISSUER,
+    ipAddress,
+    txmaAuditEncoded,
+  };
+  if (getSessionResult.isError) {
+    return handleGetSessionError(
+      eventService,
+      getSessionResult.value,
+      eventData,
+    );
+  }
+  const sessionAttributes = getSessionResult.value;
+
+  const isSessionValid = validateSession(sessionAttributes);
+  if (!isSessionValid) return handleSessionNotFound(eventService, eventData);
path: backend-api/src/functions/asyncTxmaEvent/asyncTxmaEventHandler.ts
user: login: jmooney-dd
body: This or the validateSession could also return a Result and then we follow the typical isError pattern
created_at: 2025-03-13T10:41:57Z
author_association: CONTRIBUTOR
diff_hunk: @@ -42,3 +88,117 @@ export const lambdaHandler = lambdaHandlerConstructor.bind(
   null,
   runtimeDependencies,
 );
+
+interface HandleGetSessionErrorData {
+  sessionId: string;
+  issuer: string;
+  ipAddress: string;
+  txmaAuditEncoded: string | undefined;
+}
+
+async function handleGetSessionError(
+  eventService: IEventService,
+  getSessionResult: SessionRetrievalFailed,
+  data: HandleGetSessionErrorData,
+): Promise<APIGatewayProxyResult> {
+  switch (getSessionResult.errorType) {
+    case GetSessionError.INTERNAL_SERVER_ERROR:
+      return handleInternalServerError(eventService, data);
+    case GetSessionError.SESSION_NOT_FOUND:
+      return handleSessionNotFound(eventService, data);
+  }
+}
+
+interface BaseErrorData {
+  sessionId: string;
+  issuer: string;
+  ipAddress: string;
+  txmaAuditEncoded: string | undefined;
+}
+
+async function handleInternalServerError(
+  eventService: IEventService,
+  data: BaseErrorData,
+): Promise<APIGatewayProxyResult> {
+  const writeEventResult = await writeEvent({
+    eventService,
+    eventName: "DCMAW_ASYNC_CRI_5XXERROR",
+    data,
+  });
+
+  if (writeEventResult.isError) {
+    logger.error(LogMessage.ERROR_WRITING_AUDIT_EVENT, {
+      data: {
+        auditEventName: "DCMAW_ASYNC_CRI_5XXERROR",
+      },
+    });
+  }
+  return serverErrorResponse;
+}
+
+async function handleSessionNotFound(
+  eventService: IEventService,
+  data: BaseErrorData,
+): Promise<APIGatewayProxyResult> {
+  const writeEventResult = await writeEvent({
+    eventService,
+    eventName: "DCMAW_ASYNC_CRI_4XXERROR",
+    data,
+  });
+
+  if (writeEventResult.isError) {
+    logger.error(LogMessage.ERROR_WRITING_AUDIT_EVENT, {
+      data: {
+        auditEventName: "DCMAW_ASYNC_CRI_4XXERROR",
+      },
+    });
+    return serverErrorResponse;
+  }
+  return unauthorizedResponse(
+    "invalid_session",
+    "Session does not exist or in incorrect state",
+  );
+}
+
+async function writeEvent({
+  eventService,
+  eventName,
+  data,
+}: {
+  eventService: IEventService;
+  eventName: GenericEventNames;
+  data: BaseErrorData;
+}): Promise<Result<void, void>> {
+  const { sessionId, issuer, ipAddress, txmaAuditEncoded } = data;
+  return await eventService.writeGenericEvent({
+    eventName,
+    sessionId,
+    componentId: issuer,
+    ipAddress,
+    txmaAuditEncoded,
+    sub: undefined,
+    govukSigninJourneyId: undefined,
+    getNowInMilliseconds: Date.now,
+    redirect_uri: undefined,
+    suspected_fraud_signal: undefined,
+  });
+}
+
+function validateSession(sessionAttributes: SessionAttributes): boolean {
+  const { sessionState, createdAt } = sessionAttributes;
+  if (sessionState !== SessionState.BIOMETRIC_TOKEN_ISSUED) return false;
+  if (isOlderThan60minutes(createdAt)) return false;
+
+  return true;
+}
+
+function isOlderThan60minutes(createdAtInMilliseconds: number) {
+  const SIXTY_MINUTES_IN_MILLISECONDS = 3600000;
+  const validFrom = getTimeNowInMilliseconds() - SIXTY_MINUTES_IN_MILLISECONDS;
+
+  return createdAtInMilliseconds < validFrom;
+}
+
+function getTimeNowInMilliseconds() {
+  return Math.floor(Date.now());
+}
path: backend-api/src/functions/asyncTxmaEvent/asyncTxmaEventHandler.ts
user: login: jmooney-dd
body: question: Why do we need a function that does Math.floor here, can't we just use Date.now() directly?
created_at: 2025-03-13T10:45:48Z
author_association: CONTRIBUTOR
diff_hunk: @@ -37,6 +37,31 @@ export class LogMessage implements LogAttributes {
     "Error occurred while attempting to retrieve active session by subject ID.",
   );
 
+  static readonly GET_SESSION_ATTEMPT = new LogMessage(
+    "MOBILE_ASYNC_GET_SESSION_ATTEMPT",
+    "Attempting to retrieve session by session ID from DynamoDB.",
+  );
+
+  static readonly GET_SESSION_UNEXPECTED_FAILURE = new LogMessage(
+    "MOBILE_ASYNC_GET_SESSION_UNEXPECTED_FAILURE",
+    "An unexpected failure occurred while trying to retrieve the user session from DynamoDB.",
+  );
+
+  static readonly GET_SESSION_SESSION_NOT_FOUND = new LogMessage(
+    "MOBILE_ASYNC_GET_SESSION_SESSION_NOT_FOUND",
+    "No session found when attempting to get the user session from DynamoDB.",
+  );
+
+  static readonly GET_SESSION_SESSION_TOO_OLD = new LogMessage(
+    "MOBILE_ASYNC_GET_SESSION_SESSION_TOO_OLD",
+    "Session was not created within the valid timeframe",
+  );
path: backend-api/src/functions/common/logging/LogMessage.ts
user: login: jmooney-dd
body: issue: we are not currently using this

question: If we do want to log this, should the name be different as the check is in the lambda not in the getSession call
created_at: 2025-03-13T10:49:05Z
author_association: CONTRIBUTOR
diff_hunk: @@ -32,6 +50,34 @@ export async function lambdaHandlerConstructor(
     });
     return badRequestResponse("invalid_request", errorMessage);
   }
+  const { sessionId } = validateRequestBodyResult.value;
+  const eventService = dependencies.getEventService(config.TXMA_SQS);
+
+  const sessionRegistry = dependencies.getSessionRegistry(
+    config.SESSION_TABLE_NAME,
+  );
+  const getSessionResult = await sessionRegistry.getSession(
+    sessionId,
+    new TxMAEvent(),
+  );
+  const { ipAddress, txmaAuditEncoded } = getAuditData(event);
+  const eventData = {
+    sessionId,
+    issuer: config.ISSUER,
+    ipAddress,
+    txmaAuditEncoded,
+  };
+  if (getSessionResult.isError) {
+    return handleGetSessionError(
+      eventService,
+      getSessionResult.value,
+      eventData,
+    );
+  }
+  const sessionAttributes = getSessionResult.value;
+
+  const isSessionValid = validateSession(sessionAttributes);
+  if (!isSessionValid) return handleSessionNotFound(eventService, eventData);
path: backend-api/src/functions/asyncTxmaEvent/asyncTxmaEventHandler.ts
user: login: jhumbert-dd
body: I think it's more declarative and reads a little better as is. I also, personally, find it nice knowing the datatype without actually having it in the name when using boolean naming conventions
created_at: 2025-03-13T10:50:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -131,6 +140,47 @@ export class DynamoDbAdapter implements SessionRegistry {
     }
   }
 
+  async getSession(
+    sessionId: string,
+    getOperation: GetSessionOperation,
+  ): Promise<Result<SessionAttributes, SessionRetrievalFailed>> {
+    let response;
+    try {
+      logger.debug(LogMessage.GET_SESSION_ATTEMPT);
path: backend-api/src/functions/adapters/aws/dynamo/dynamoDbAdapter.ts
user: login: jmooney-dd
body: suggestion: worth logging some extra data here? Both the sessionId and getOperation do not contain PII and therefore could be logged

If you do decide to log this, please ensure you assert on the whole log here as we always want to ensure no PII is getting logged in these debug logs
created_at: 2025-03-13T10:52:20Z
author_association: CONTRIBUTOR
diff_hunk: @@ -42,3 +88,117 @@ export const lambdaHandler = lambdaHandlerConstructor.bind(
   null,
   runtimeDependencies,
 );
+
+interface HandleGetSessionErrorData {
+  sessionId: string;
+  issuer: string;
+  ipAddress: string;
+  txmaAuditEncoded: string | undefined;
+}
+
+async function handleGetSessionError(
+  eventService: IEventService,
+  getSessionResult: SessionRetrievalFailed,
+  data: HandleGetSessionErrorData,
+): Promise<APIGatewayProxyResult> {
+  switch (getSessionResult.errorType) {
+    case GetSessionError.INTERNAL_SERVER_ERROR:
+      return handleInternalServerError(eventService, data);
+    case GetSessionError.SESSION_NOT_FOUND:
+      return handleSessionNotFound(eventService, data);
+  }
+}
+
+interface BaseErrorData {
+  sessionId: string;
+  issuer: string;
+  ipAddress: string;
+  txmaAuditEncoded: string | undefined;
+}
+
+async function handleInternalServerError(
+  eventService: IEventService,
+  data: BaseErrorData,
+): Promise<APIGatewayProxyResult> {
+  const writeEventResult = await writeEvent({
+    eventService,
+    eventName: "DCMAW_ASYNC_CRI_5XXERROR",
+    data,
+  });
+
+  if (writeEventResult.isError) {
+    logger.error(LogMessage.ERROR_WRITING_AUDIT_EVENT, {
+      data: {
+        auditEventName: "DCMAW_ASYNC_CRI_5XXERROR",
+      },
+    });
+  }
+  return serverErrorResponse;
+}
+
+async function handleSessionNotFound(
+  eventService: IEventService,
+  data: BaseErrorData,
+): Promise<APIGatewayProxyResult> {
+  const writeEventResult = await writeEvent({
+    eventService,
+    eventName: "DCMAW_ASYNC_CRI_4XXERROR",
+    data,
+  });
+
+  if (writeEventResult.isError) {
+    logger.error(LogMessage.ERROR_WRITING_AUDIT_EVENT, {
+      data: {
+        auditEventName: "DCMAW_ASYNC_CRI_4XXERROR",
+      },
+    });
+    return serverErrorResponse;
+  }
+  return unauthorizedResponse(
+    "invalid_session",
+    "Session does not exist or in incorrect state",
+  );
+}
+
+async function writeEvent({
+  eventService,
+  eventName,
+  data,
+}: {
+  eventService: IEventService;
+  eventName: GenericEventNames;
+  data: BaseErrorData;
+}): Promise<Result<void, void>> {
+  const { sessionId, issuer, ipAddress, txmaAuditEncoded } = data;
+  return await eventService.writeGenericEvent({
+    eventName,
+    sessionId,
+    componentId: issuer,
+    ipAddress,
+    txmaAuditEncoded,
+    sub: undefined,
+    govukSigninJourneyId: undefined,
+    getNowInMilliseconds: Date.now,
+    redirect_uri: undefined,
+    suspected_fraud_signal: undefined,
+  });
+}
+
+function validateSession(sessionAttributes: SessionAttributes): boolean {
+  const { sessionState, createdAt } = sessionAttributes;
+  if (sessionState !== SessionState.BIOMETRIC_TOKEN_ISSUED) return false;
+  if (isOlderThan60minutes(createdAt)) return false;
+
+  return true;
+}
+
+function isOlderThan60minutes(createdAtInMilliseconds: number) {
+  const SIXTY_MINUTES_IN_MILLISECONDS = 3600000;
+  const validFrom = getTimeNowInMilliseconds() - SIXTY_MINUTES_IN_MILLISECONDS;
+
+  return createdAtInMilliseconds < validFrom;
+}
+
+function getTimeNowInMilliseconds() {
+  return Math.floor(Date.now());
+}
path: backend-api/src/functions/asyncTxmaEvent/asyncTxmaEventHandler.ts
user: login: jhumbert-dd
body: updated
created_at: 2025-03-13T10:54:52Z
author_association: CONTRIBUTOR
diff_hunk: @@ -90,8 +148,277 @@ describe("Async TxMA Event", () => {
     });
   });
 
+  describe("Retrieving a session", () => {
+    describe("Given there is an error retrieving the session", () => {
+      beforeEach(async () => {
+        dependencies.getSessionRegistry = () => ({
+          ...mockInertSessionRegistry,
+          getSession: jest.fn().mockResolvedValue(
+            errorResult({
+              errorType: GetSessionError.INTERNAL_SERVER_ERROR,
+            }),
+          ),
+        });
+        result = await lambdaHandlerConstructor(
+          dependencies,
+          validRequest,
+          context,
+        );
+      });
+
+      describe("Given DCMAW_ASYNC_CRI_5XXERROR event fails to write to TxMA", () => {
+        beforeEach(async () => {
+          dependencies.getEventService = () => ({
+            ...mockInertEventService,
+            writeGenericEvent: jest.fn().mockResolvedValue(
+              errorResult({
+                errorMessage: "mockError",
+              }),
+            ),
+          });
+
+          result = await lambdaHandlerConstructor(
+            dependencies,
+            validRequest,
+            context,
+          );
+        });
+
+        it("Logs the error", async () => {
+          expect(consoleErrorSpy).toHaveBeenCalledWithLogFields({
+            messageCode: "MOBILE_ASYNC_ERROR_WRITING_AUDIT_EVENT",
+            data: {
+              auditEventName: "DCMAW_ASYNC_CRI_5XXERROR",
+            },
+          });
+        });
+
+        it("Returns 500 Internal server error", async () => {
+          expect(result).toStrictEqual({
+            statusCode: 500,
+            body: JSON.stringify({
+              error: "server_error",
+              error_description: "Internal Server Error",
+            }),
+            headers: expectedSecurityHeaders,
+          });
+        });
+      });
+
+      describe("Given DCMAW_ASYNC_CRI_5XXERROR event successfully to write to TxMA", () => {
+        it("Writes DCMAW_ASYNC_CRI_5XXERROR event to TxMA", () => {
+          expect(mockWriteGenericEventSuccessResult).toBeCalledWith({
+            eventName: "DCMAW_ASYNC_CRI_5XXERROR",
+            componentId: "mockIssuer",
+            getNowInMilliseconds: Date.now,
+            govukSigninJourneyId: undefined,
+            sessionId: mockSessionId,
+            sub: undefined,
+            ipAddress: "1.1.1.1",
+            txmaAuditEncoded: "mockTxmaAuditEncodedHeader",
+            redirect_uri: undefined,
+            suspected_fraud_signal: undefined,
+          });
+        });
+
+        it("Returns 500 Internal server error", async () => {
+          expect(result).toStrictEqual({
+            statusCode: 500,
+            body: JSON.stringify({
+              error: "server_error",
+              error_description: "Internal Server Error",
+            }),
+            headers: expectedSecurityHeaders,
+          });
+        });
+      });
+    });
+
+    describe("Given failure is due to client error", () => {
+      describe("Given session was not found", () => {
+        beforeEach(async () => {
+          dependencies.getSessionRegistry = () => ({
+            ...mockInertSessionRegistry,
+            getSession: jest.fn().mockResolvedValue(
+              errorResult({
+                errorType: GetSessionError.SESSION_NOT_FOUND,
+              }),
+            ),
+          });
+          result = await lambdaHandlerConstructor(
+            dependencies,
+            validRequest,
+            context,
+          );
+        });
+
+        describe("Given DCMAW_ASYNC_CRI_4XXERROR event fails to write to TxMA", () => {
+          beforeEach(async () => {
+            dependencies.getEventService = () => ({
+              ...mockInertEventService,
+              writeGenericEvent: jest.fn().mockResolvedValue(
+                errorResult({
+                  errorMessage: "mockError",
+                }),
+              ),
+            });
+
+            result = await lambdaHandlerConstructor(
+              dependencies,
+              validRequest,
+              context,
+            );
+          });
+
+          it("Logs the error", async () => {
+            expect(consoleErrorSpy).toHaveBeenCalledWithLogFields({
+              messageCode: "MOBILE_ASYNC_ERROR_WRITING_AUDIT_EVENT",
+              data: {
+                auditEventName: "DCMAW_ASYNC_CRI_4XXERROR",
+              },
+            });
+          });
+
+          it("Returns 500 Internal Server Error ", async () => {
+            expect(result).toStrictEqual({
+              statusCode: 500,
+              body: JSON.stringify({
+                error: "server_error",
+                error_description: "Internal Server Error",
+              }),
+              headers: expectedSecurityHeaders,
+            });
+          });
+        });
+
+        it("Writes DCMAW_ASYNC_CRI_4XXERROR event to TxMA", () => {
+          expect(mockWriteGenericEventSuccessResult).toBeCalledWith({
+            eventName: "DCMAW_ASYNC_CRI_4XXERROR",
+            componentId: "mockIssuer",
+            getNowInMilliseconds: Date.now,
+            govukSigninJourneyId: undefined,
+            sessionId: mockSessionId,
+            sub: undefined,
+            ipAddress: "1.1.1.1",
+            txmaAuditEncoded: "mockTxmaAuditEncodedHeader",
+            redirect_uri: undefined,
+            suspected_fraud_signal: undefined,
+          });
+        });
+
+        it("Returns 401 Unauthorized", () => {
+          expect(result).toStrictEqual({
+            statusCode: 401,
+            body: JSON.stringify({
+              error: "invalid_session",
+              error_description: "Session does not exist or in incorrect state",
+            }),
+            headers: expectedSecurityHeaders,
+          });
+        });
+      });
+    });
+  });
+
+  describe("Session validation", () => {
+    describe("Given the session is more than 60 minutes old", () => {
+      beforeEach(async () => {
+        dependencies.getSessionRegistry = () => ({
+          ...mockSuccessfulSessionRegistry,
+          getSession: jest.fn().mockResolvedValue(
+            successResult({
+              ...validBaseSessionAttributes,
+              sessionState: SessionState.BIOMETRIC_TOKEN_ISSUED,
+              createdAt: 1704106740000,
+            }),
+          ),
+        });
+        result = await lambdaHandlerConstructor(
+          dependencies,
+          validRequest,
+          context,
+        );
+      });
+
+      describe("Given DCMAW_ASYNC_CRI_4XXERROR event fails to write to TxMA", () => {
+        beforeEach(async () => {
+          dependencies.getEventService = () => ({
+            ...mockInertEventService,
+            writeGenericEvent: jest.fn().mockResolvedValue(
+              errorResult({
+                errorMessage: "mockError",
+              }),
+            ),
+          });
+
+          result = await lambdaHandlerConstructor(
+            dependencies,
+            validRequest,
+            context,
+          );
+        });
+
+        it("Logs the error", async () => {
+          expect(consoleErrorSpy).toHaveBeenCalledWithLogFields({
+            messageCode: "MOBILE_ASYNC_ERROR_WRITING_AUDIT_EVENT",
+            data: {
+              auditEventName: "DCMAW_ASYNC_CRI_4XXERROR",
+            },
+          });
+        });
+
+        it("Returns 500 Internal Server Error ", async () => {
+          expect(result).toStrictEqual({
+            statusCode: 500,
+            body: JSON.stringify({
+              error: "server_error",
+              error_description: "Internal Server Error",
+            }),
+            headers: expectedSecurityHeaders,
+          });
+        });
+      });
+
+      it("Writes DCMAW_ASYNC_CRI_4XXERROR event to TxMA", () => {
+        expect(mockWriteGenericEventSuccessResult).toBeCalledWith({
+          eventName: "DCMAW_ASYNC_CRI_4XXERROR",
+          componentId: "mockIssuer",
+          getNowInMilliseconds: Date.now,
+          govukSigninJourneyId: undefined,
+          sessionId: mockSessionId,
+          sub: undefined,
+          ipAddress: "1.1.1.1",
+          txmaAuditEncoded: "mockTxmaAuditEncodedHeader",
+          redirect_uri: undefined,
+          suspected_fraud_signal: undefined,
+        });
+      });
+
+      it("Returns 401 Unauthorized", () => {
+        expect(result).toStrictEqual({
+          statusCode: 401,
+          body: JSON.stringify({
+            error: "invalid_session",
+            error_description: "Session does not exist or in incorrect state",
+          }),
+          headers: expectedSecurityHeaders,
+        });
+      });
+    });
+  });
+
   describe("Given a valid request is made", () => {
     beforeEach(async () => {
+      dependencies.getSessionRegistry = () => ({
+        ...mockSuccessfulSessionRegistry,
+        getSession: jest.fn().mockResolvedValue(
+          successResult({
+            ...validBaseSessionAttributes,
+            sessionState: SessionState.BIOMETRIC_TOKEN_ISSUED,
path: backend-api/src/functions/asyncTxmaEvent/asyncTxmaEventHandler.test.ts
user: login: jmooney-dd
body: suggestion (non-blocking): You could spread validBiometricTokenIssuedSessionAttributes instead. You then wouldn't need update sessionState here
created_at: 2025-03-13T10:57:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -58,3 +41,68 @@ export const validBiometricSessionFinishedAttributes = {
   sessionState: SessionState.BIOMETRIC_SESSION_FINISHED,
   biometricSessionId: mockBiometricSessionId,
 };
+
+export const validBiometricTokenIssuedSessionAttributesMobileApp = {
+  ...validBiometricSessionFinishedAttributes,
+  redirectUri: "https://www.mockRedirectUri.com",
+};
+
+export const mockInertSessionRegistry: SessionRegistry = {
+  updateSession: jest.fn(() => {
+    throw new Error("Not implemented");
+  }),
+
+  getSession: jest.fn(() => {
+    throw new Error("Not implemented");
+  }),
+};
+
+export const mockSuccessfulSessionRegistry: SessionRegistry = {
+  updateSession: jest.fn().mockResolvedValue(
+    successResult({
+      attributes: validBiometricTokenIssuedSessionAttributesMobileApp,
+    }),
+  ),
+
+  getSession: jest.fn().mockResolvedValue(
+    successResult({
+      attributes: validBaseSessionAttributes,
path: backend-api/src/functions/testUtils/unitTestData.ts
user: login: jmooney-dd
body: suggestion (non-blocking): We could use validBiometricTokenIssuedSessionAttributes here as that's the only shape of attributes that is currently used in the getSession func. Obviously doesn't change anything functionally with the test
created_at: 2025-03-13T10:58:12Z
author_association: CONTRIBUTOR
path: backend-api/tests/api-tests/txmaEvent.test.ts
user: login: jmooney-dd
body: Question: should we be testing and asserting on a TxMA event here?
created_at: 2025-03-13T10:59:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -37,6 +37,31 @@ export class LogMessage implements LogAttributes {
     "Error occurred while attempting to retrieve active session by subject ID.",
   );
 
+  static readonly GET_SESSION_ATTEMPT = new LogMessage(
+    "MOBILE_ASYNC_GET_SESSION_ATTEMPT",
+    "Attempting to retrieve session by session ID from DynamoDB.",
+  );
+
+  static readonly GET_SESSION_UNEXPECTED_FAILURE = new LogMessage(
+    "MOBILE_ASYNC_GET_SESSION_UNEXPECTED_FAILURE",
+    "An unexpected failure occurred while trying to retrieve the user session from DynamoDB.",
+  );
+
+  static readonly GET_SESSION_SESSION_NOT_FOUND = new LogMessage(
+    "MOBILE_ASYNC_GET_SESSION_SESSION_NOT_FOUND",
+    "No session found when attempting to get the user session from DynamoDB.",
+  );
+
+  static readonly GET_SESSION_SESSION_TOO_OLD = new LogMessage(
+    "MOBILE_ASYNC_GET_SESSION_SESSION_TOO_OLD",
+    "Session was not created within the valid timeframe",
+  );
path: backend-api/src/functions/common/logging/LogMessage.ts
user: login: jhumbert-dd
body: Updated
created_at: 2025-03-13T14:48:26Z
author_association: CONTRIBUTOR
diff_hunk: @@ -131,6 +140,47 @@ export class DynamoDbAdapter implements SessionRegistry {
     }
   }
 
+  async getSession(
+    sessionId: string,
+    getOperation: GetSessionOperation,
+  ): Promise<Result<SessionAttributes, SessionRetrievalFailed>> {
+    let response;
+    try {
+      logger.debug(LogMessage.GET_SESSION_ATTEMPT);
path: backend-api/src/functions/adapters/aws/dynamo/dynamoDbAdapter.ts
user: login: jhumbert-dd
body: updated
created_at: 2025-03-13T15:02:23Z
author_association: CONTRIBUTOR
diff_hunk: @@ -90,8 +148,277 @@ describe("Async TxMA Event", () => {
     });
   });
 
+  describe("Retrieving a session", () => {
+    describe("Given there is an error retrieving the session", () => {
+      beforeEach(async () => {
+        dependencies.getSessionRegistry = () => ({
+          ...mockInertSessionRegistry,
+          getSession: jest.fn().mockResolvedValue(
+            errorResult({
+              errorType: GetSessionError.INTERNAL_SERVER_ERROR,
+            }),
+          ),
+        });
+        result = await lambdaHandlerConstructor(
+          dependencies,
+          validRequest,
+          context,
+        );
+      });
+
+      describe("Given DCMAW_ASYNC_CRI_5XXERROR event fails to write to TxMA", () => {
+        beforeEach(async () => {
+          dependencies.getEventService = () => ({
+            ...mockInertEventService,
+            writeGenericEvent: jest.fn().mockResolvedValue(
+              errorResult({
+                errorMessage: "mockError",
+              }),
+            ),
+          });
+
+          result = await lambdaHandlerConstructor(
+            dependencies,
+            validRequest,
+            context,
+          );
+        });
+
+        it("Logs the error", async () => {
+          expect(consoleErrorSpy).toHaveBeenCalledWithLogFields({
+            messageCode: "MOBILE_ASYNC_ERROR_WRITING_AUDIT_EVENT",
+            data: {
+              auditEventName: "DCMAW_ASYNC_CRI_5XXERROR",
+            },
+          });
+        });
+
+        it("Returns 500 Internal server error", async () => {
+          expect(result).toStrictEqual({
+            statusCode: 500,
+            body: JSON.stringify({
+              error: "server_error",
+              error_description: "Internal Server Error",
+            }),
+            headers: expectedSecurityHeaders,
+          });
+        });
+      });
+
+      describe("Given DCMAW_ASYNC_CRI_5XXERROR event successfully to write to TxMA", () => {
+        it("Writes DCMAW_ASYNC_CRI_5XXERROR event to TxMA", () => {
+          expect(mockWriteGenericEventSuccessResult).toBeCalledWith({
+            eventName: "DCMAW_ASYNC_CRI_5XXERROR",
+            componentId: "mockIssuer",
+            getNowInMilliseconds: Date.now,
+            govukSigninJourneyId: undefined,
+            sessionId: mockSessionId,
+            sub: undefined,
+            ipAddress: "1.1.1.1",
+            txmaAuditEncoded: "mockTxmaAuditEncodedHeader",
+            redirect_uri: undefined,
+            suspected_fraud_signal: undefined,
+          });
+        });
+
+        it("Returns 500 Internal server error", async () => {
+          expect(result).toStrictEqual({
+            statusCode: 500,
+            body: JSON.stringify({
+              error: "server_error",
+              error_description: "Internal Server Error",
+            }),
+            headers: expectedSecurityHeaders,
+          });
+        });
+      });
+    });
+
+    describe("Given failure is due to client error", () => {
+      describe("Given session was not found", () => {
+        beforeEach(async () => {
+          dependencies.getSessionRegistry = () => ({
+            ...mockInertSessionRegistry,
+            getSession: jest.fn().mockResolvedValue(
+              errorResult({
+                errorType: GetSessionError.SESSION_NOT_FOUND,
+              }),
+            ),
+          });
+          result = await lambdaHandlerConstructor(
+            dependencies,
+            validRequest,
+            context,
+          );
+        });
+
+        describe("Given DCMAW_ASYNC_CRI_4XXERROR event fails to write to TxMA", () => {
+          beforeEach(async () => {
+            dependencies.getEventService = () => ({
+              ...mockInertEventService,
+              writeGenericEvent: jest.fn().mockResolvedValue(
+                errorResult({
+                  errorMessage: "mockError",
+                }),
+              ),
+            });
+
+            result = await lambdaHandlerConstructor(
+              dependencies,
+              validRequest,
+              context,
+            );
+          });
+
+          it("Logs the error", async () => {
+            expect(consoleErrorSpy).toHaveBeenCalledWithLogFields({
+              messageCode: "MOBILE_ASYNC_ERROR_WRITING_AUDIT_EVENT",
+              data: {
+                auditEventName: "DCMAW_ASYNC_CRI_4XXERROR",
+              },
+            });
+          });
+
+          it("Returns 500 Internal Server Error ", async () => {
+            expect(result).toStrictEqual({
+              statusCode: 500,
+              body: JSON.stringify({
+                error: "server_error",
+                error_description: "Internal Server Error",
+              }),
+              headers: expectedSecurityHeaders,
+            });
+          });
+        });
+
+        it("Writes DCMAW_ASYNC_CRI_4XXERROR event to TxMA", () => {
+          expect(mockWriteGenericEventSuccessResult).toBeCalledWith({
+            eventName: "DCMAW_ASYNC_CRI_4XXERROR",
+            componentId: "mockIssuer",
+            getNowInMilliseconds: Date.now,
+            govukSigninJourneyId: undefined,
+            sessionId: mockSessionId,
+            sub: undefined,
+            ipAddress: "1.1.1.1",
+            txmaAuditEncoded: "mockTxmaAuditEncodedHeader",
+            redirect_uri: undefined,
+            suspected_fraud_signal: undefined,
+          });
+        });
+
+        it("Returns 401 Unauthorized", () => {
+          expect(result).toStrictEqual({
+            statusCode: 401,
+            body: JSON.stringify({
+              error: "invalid_session",
+              error_description: "Session does not exist or in incorrect state",
+            }),
+            headers: expectedSecurityHeaders,
+          });
+        });
+      });
+    });
+  });
+
+  describe("Session validation", () => {
+    describe("Given the session is more than 60 minutes old", () => {
+      beforeEach(async () => {
+        dependencies.getSessionRegistry = () => ({
+          ...mockSuccessfulSessionRegistry,
+          getSession: jest.fn().mockResolvedValue(
+            successResult({
+              ...validBaseSessionAttributes,
+              sessionState: SessionState.BIOMETRIC_TOKEN_ISSUED,
+              createdAt: 1704106740000,
+            }),
+          ),
+        });
+        result = await lambdaHandlerConstructor(
+          dependencies,
+          validRequest,
+          context,
+        );
+      });
+
+      describe("Given DCMAW_ASYNC_CRI_4XXERROR event fails to write to TxMA", () => {
+        beforeEach(async () => {
+          dependencies.getEventService = () => ({
+            ...mockInertEventService,
+            writeGenericEvent: jest.fn().mockResolvedValue(
+              errorResult({
+                errorMessage: "mockError",
+              }),
+            ),
+          });
+
+          result = await lambdaHandlerConstructor(
+            dependencies,
+            validRequest,
+            context,
+          );
+        });
+
+        it("Logs the error", async () => {
+          expect(consoleErrorSpy).toHaveBeenCalledWithLogFields({
+            messageCode: "MOBILE_ASYNC_ERROR_WRITING_AUDIT_EVENT",
+            data: {
+              auditEventName: "DCMAW_ASYNC_CRI_4XXERROR",
+            },
+          });
+        });
+
+        it("Returns 500 Internal Server Error ", async () => {
+          expect(result).toStrictEqual({
+            statusCode: 500,
+            body: JSON.stringify({
+              error: "server_error",
+              error_description: "Internal Server Error",
+            }),
+            headers: expectedSecurityHeaders,
+          });
+        });
+      });
+
+      it("Writes DCMAW_ASYNC_CRI_4XXERROR event to TxMA", () => {
+        expect(mockWriteGenericEventSuccessResult).toBeCalledWith({
+          eventName: "DCMAW_ASYNC_CRI_4XXERROR",
+          componentId: "mockIssuer",
+          getNowInMilliseconds: Date.now,
+          govukSigninJourneyId: undefined,
+          sessionId: mockSessionId,
+          sub: undefined,
+          ipAddress: "1.1.1.1",
+          txmaAuditEncoded: "mockTxmaAuditEncodedHeader",
+          redirect_uri: undefined,
+          suspected_fraud_signal: undefined,
+        });
+      });
+
+      it("Returns 401 Unauthorized", () => {
+        expect(result).toStrictEqual({
+          statusCode: 401,
+          body: JSON.stringify({
+            error: "invalid_session",
+            error_description: "Session does not exist or in incorrect state",
+          }),
+          headers: expectedSecurityHeaders,
+        });
+      });
+    });
+  });
+
   describe("Given a valid request is made", () => {
     beforeEach(async () => {
+      dependencies.getSessionRegistry = () => ({
+        ...mockSuccessfulSessionRegistry,
+        getSession: jest.fn().mockResolvedValue(
+          successResult({
+            ...validBaseSessionAttributes,
+            sessionState: SessionState.BIOMETRIC_TOKEN_ISSUED,
path: backend-api/src/functions/asyncTxmaEvent/asyncTxmaEventHandler.test.ts
user: login: jhumbert-dd
body: updated
created_at: 2025-03-13T15:05:53Z
author_association: CONTRIBUTOR
diff_hunk: @@ -58,3 +41,68 @@ export const validBiometricSessionFinishedAttributes = {
   sessionState: SessionState.BIOMETRIC_SESSION_FINISHED,
   biometricSessionId: mockBiometricSessionId,
 };
+
+export const validBiometricTokenIssuedSessionAttributesMobileApp = {
+  ...validBiometricSessionFinishedAttributes,
+  redirectUri: "https://www.mockRedirectUri.com",
+};
+
+export const mockInertSessionRegistry: SessionRegistry = {
+  updateSession: jest.fn(() => {
+    throw new Error("Not implemented");
+  }),
+
+  getSession: jest.fn(() => {
+    throw new Error("Not implemented");
+  }),
+};
+
+export const mockSuccessfulSessionRegistry: SessionRegistry = {
+  updateSession: jest.fn().mockResolvedValue(
+    successResult({
+      attributes: validBiometricTokenIssuedSessionAttributesMobileApp,
+    }),
+  ),
+
+  getSession: jest.fn().mockResolvedValue(
+    successResult({
+      attributes: validBaseSessionAttributes,
path: backend-api/src/functions/testUtils/unitTestData.ts
user: login: jhumbert-dd
body: updated
created_at: 2025-03-13T15:14:25Z
author_association: CONTRIBUTOR
diff_hunk: @@ -131,6 +139,57 @@ export class DynamoDbAdapter implements SessionRegistry {
     }
   }
 
+  async querySession(
+    sessionId: string,
+    queryOperation: QuerySessionOperation,
+  ): Promise<Result<SessionAttributes, SessionRetrievalFailed>> {
+    let queryCommandOutput: QueryCommandOutput;
+    try {
+      logger.debug(LogMessage.GET_SESSION_ATTEMPT, { data: { sessionId } });
+
+      queryCommandOutput = await this.dynamoDbClient.send(
+        new QueryCommand({
+          TableName: this.tableName,
+          ExpressionAttributeValues:
+            queryOperation.getDynamoDbExpressionAttributeValues(sessionId),
+          KeyConditionExpression:
+            queryOperation.getDynamoDbKeyConditionExpression(),
+        }),
+      );
+    } catch (error: unknown) {
+      const errorData = {
+        ExpressionAttributeValues:
+          queryOperation.getDynamoDbExpressionAttributeValues(sessionId),
+        KeyConditionExpression:
+          queryOperation.getDynamoDbKeyConditionExpression(),
+        error,
path: backend-api/src/functions/adapters/aws/dynamo/dynamoDbAdapter.ts
user: login: jmooney-dd
body: suggestion: log the query operation stuff in the attempt log and only error data on an error log
created_at: 2025-03-14T09:48:17Z
author_association: CONTRIBUTOR
path: backend-api/tests/api-tests/txmaEvent.test.ts
user: login: jhumbert-dd
body: TxMA assertion has been added.
created_at: 2025-03-17T14:51:18Z
author_association: CONTRIBUTOR
diff_hunk: @@ -293,4 +309,234 @@ describe("DynamoDbAdapter", () => {
       });
     });
   });
+
+  describe("getSession", () => {
+    const getOperation: GetSessionOperation = new TxMAEvent();
+    let result: Result<void, SessionRetrievalFailed>;
+
+    describe("On every attempt", () => {
+      beforeEach(async () => {
+        mockDynamoDbClient.on(GetItemCommand).resolves({});
+        await sessionRegistry.getSession(mockSessionId, getOperation);
+      });
+
+      it("Logs the attempt", () => {
+        expect(consoleDebugSpy).toHaveBeenCalledWithLogFields({
+          messageCode: "MOBILE_ASYNC_GET_SESSION_ATTEMPT",
+          data: {
+            sessionId: mockSessionId,
+            getItemCommandInput: {
+              Key: getOperation.getDynamoDbKeyExpression(mockSessionId),
+            },
+          },
+        });
+      });
+    });
+
+    describe("Given there is an unexpected error retrieving the session", () => {
+      beforeEach(async () => {
+        mockDynamoDbClient.on(GetItemCommand).rejects("mock_error");
+        result = await sessionRegistry.getSession(mockSessionId, getOperation);
+      });
+
+      it("Logs the failure", () => {
+        expect(consoleErrorSpy).toHaveBeenCalledWithLogFields({
+          messageCode: "MOBILE_ASYNC_GET_SESSION_UNEXPECTED_FAILURE",
+        });
+      });
+
+      it("Returns failure with server error", () => {
+        expect(result).toEqual(
+          errorResult({
+            errorType: GetSessionError.INTERNAL_SERVER_ERROR,
+          }),
+        );
+      });
+    });
+
+    describe("Given the session was not found", () => {
+      beforeEach(async () => {
+        mockDynamoDbClient.on(GetItemCommand).resolves({});
+        result = await sessionRegistry.getSession(mockSessionId, getOperation);
+      });
+
+      it("Logs the failure", () => {
+        expect(consoleErrorSpy).toHaveBeenCalledWithLogFields({
+          messageCode: "MOBILE_ASYNC_GET_SESSION_SESSION_NOT_FOUND",
+          session: "Session not found",
+        });
+      });
+
+      it("Returns failure with an invalid session error", () => {
+        expect(result).toEqual(
+          errorResult({
+            errorType: GetSessionError.SESSION_NOT_FOUND,
+            session: "Session not found",
+          }),
+        );
+      });
+    });
+
+    describe("Session validation", () => {
+      describe("Given the session is in the wrong state", () => {
+        const invalidBiometricTokenIssuedSessionAttributesWrongSessionState = {
+          ...validBiometricTokenIssuedSessionAttributes,
+          createdAt: 1704106860000, // 2024-01-01 11:01:00.000
+          sessionState: SessionState.AUTH_SESSION_CREATED,
+        };
+        const invalidBiometricTokenIssuedSessionAttributesWrongSessionStateItem =
+          {
+            Item: marshall(
+              invalidBiometricTokenIssuedSessionAttributesWrongSessionState,
+            ),
+          };
+
+        beforeEach(async () => {
+          mockDynamoDbClient
+            .on(GetItemCommand)
+            .resolves(
+              invalidBiometricTokenIssuedSessionAttributesWrongSessionStateItem,
+            );
+          result = await sessionRegistry.getSession(
+            mockSessionId,
+            getOperation,
+          );
+        });
+
+        it("Logs the failure", () => {
+          expect(consoleErrorSpy).toHaveBeenCalledWithLogFields({
+            messageCode: "MOBILE_ASYNC_GET_SESSION_SESSION_NOT_FOUND",
+            errorMessage:
+              "Session could not be parsed or session validation failed",
+            session:
+              invalidBiometricTokenIssuedSessionAttributesWrongSessionState,
+          });
+        });
+
+        it("Returns failure with an invalid session error", () => {
+          expect(result).toStrictEqual(
+            errorResult({
+              errorType: GetSessionError.SESSION_NOT_FOUND,
+              session:
+                invalidBiometricTokenIssuedSessionAttributesWrongSessionState,
+            }),
+          );
+        });
+      });
+
+      describe("Given the session is more than 60 minutes old", () => {
+        const invalidBiometricTokenIssuedSessionAttributesSessionTooOld = {
+          ...validBiometricTokenIssuedSessionAttributes,
+          createdAt: 1704106740000, // 2024-01-01 10:59:00.000
+        };
+        const biometricTokenIssuedSessionAttributesSessionTooOldItem = {
+          Item: marshall(
+            invalidBiometricTokenIssuedSessionAttributesSessionTooOld,
+          ),
+        };
+
+        beforeEach(async () => {
+          mockDynamoDbClient
+            .on(GetItemCommand)
+            .resolves(biometricTokenIssuedSessionAttributesSessionTooOldItem);
+          result = await sessionRegistry.getSession(
+            mockSessionId,
+            getOperation,
+          );
+        });
+
+        it("Logs the failure", () => {
+          expect(consoleErrorSpy).toHaveBeenCalledWithLogFields({
+            messageCode: "MOBILE_ASYNC_GET_SESSION_SESSION_NOT_FOUND",
path: backend-api/src/functions/adapters/aws/dynamo/dynamoDbAdapter.test.ts
user: login: jhumbert-dd
body: Missing `session` field?
created_at: 2025-03-17T15:06:19Z
author_association: CONTRIBUTOR
diff_hunk: @@ -293,4 +309,234 @@ describe("DynamoDbAdapter", () => {
       });
     });
   });
+
+  describe("getSession", () => {
+    const getOperation: GetSessionOperation = new TxMAEvent();
+    let result: Result<void, SessionRetrievalFailed>;
+
+    describe("On every attempt", () => {
+      beforeEach(async () => {
+        mockDynamoDbClient.on(GetItemCommand).resolves({});
+        await sessionRegistry.getSession(mockSessionId, getOperation);
+      });
+
+      it("Logs the attempt", () => {
+        expect(consoleDebugSpy).toHaveBeenCalledWithLogFields({
+          messageCode: "MOBILE_ASYNC_GET_SESSION_ATTEMPT",
+          data: {
+            sessionId: mockSessionId,
+            getItemCommandInput: {
+              Key: getOperation.getDynamoDbKeyExpression(mockSessionId),
+            },
+          },
+        });
+      });
+    });
+
+    describe("Given there is an unexpected error retrieving the session", () => {
+      beforeEach(async () => {
+        mockDynamoDbClient.on(GetItemCommand).rejects("mock_error");
+        result = await sessionRegistry.getSession(mockSessionId, getOperation);
+      });
+
+      it("Logs the failure", () => {
+        expect(consoleErrorSpy).toHaveBeenCalledWithLogFields({
+          messageCode: "MOBILE_ASYNC_GET_SESSION_UNEXPECTED_FAILURE",
+        });
+      });
+
+      it("Returns failure with server error", () => {
+        expect(result).toEqual(
+          errorResult({
+            errorType: GetSessionError.INTERNAL_SERVER_ERROR,
+          }),
+        );
+      });
+    });
+
+    describe("Given the session was not found", () => {
+      beforeEach(async () => {
+        mockDynamoDbClient.on(GetItemCommand).resolves({});
+        result = await sessionRegistry.getSession(mockSessionId, getOperation);
+      });
+
+      it("Logs the failure", () => {
+        expect(consoleErrorSpy).toHaveBeenCalledWithLogFields({
+          messageCode: "MOBILE_ASYNC_GET_SESSION_SESSION_NOT_FOUND",
+          session: "Session not found",
+        });
+      });
+
+      it("Returns failure with an invalid session error", () => {
+        expect(result).toEqual(
+          errorResult({
+            errorType: GetSessionError.SESSION_NOT_FOUND,
+            session: "Session not found",
+          }),
+        );
+      });
+    });
+
+    describe("Session validation", () => {
+      describe("Given the session is in the wrong state", () => {
+        const invalidBiometricTokenIssuedSessionAttributesWrongSessionState = {
+          ...validBiometricTokenIssuedSessionAttributes,
+          createdAt: 1704106860000, // 2024-01-01 11:01:00.000
+          sessionState: SessionState.AUTH_SESSION_CREATED,
+        };
+        const invalidBiometricTokenIssuedSessionAttributesWrongSessionStateItem =
+          {
+            Item: marshall(
+              invalidBiometricTokenIssuedSessionAttributesWrongSessionState,
+            ),
+          };
+
+        beforeEach(async () => {
+          mockDynamoDbClient
+            .on(GetItemCommand)
+            .resolves(
+              invalidBiometricTokenIssuedSessionAttributesWrongSessionStateItem,
+            );
+          result = await sessionRegistry.getSession(
+            mockSessionId,
+            getOperation,
+          );
+        });
+
+        it("Logs the failure", () => {
+          expect(consoleErrorSpy).toHaveBeenCalledWithLogFields({
+            messageCode: "MOBILE_ASYNC_GET_SESSION_SESSION_NOT_FOUND",
+            errorMessage:
+              "Session could not be parsed or session validation failed",
+            session:
+              invalidBiometricTokenIssuedSessionAttributesWrongSessionState,
+          });
+        });
+
+        it("Returns failure with an invalid session error", () => {
+          expect(result).toStrictEqual(
+            errorResult({
+              errorType: GetSessionError.SESSION_NOT_FOUND,
+              session:
+                invalidBiometricTokenIssuedSessionAttributesWrongSessionState,
+            }),
+          );
+        });
+      });
+
+      describe("Given the session is more than 60 minutes old", () => {
+        const invalidBiometricTokenIssuedSessionAttributesSessionTooOld = {
+          ...validBiometricTokenIssuedSessionAttributes,
+          createdAt: 1704106740000, // 2024-01-01 10:59:00.000
+        };
+        const biometricTokenIssuedSessionAttributesSessionTooOldItem = {
+          Item: marshall(
+            invalidBiometricTokenIssuedSessionAttributesSessionTooOld,
+          ),
+        };
+
+        beforeEach(async () => {
+          mockDynamoDbClient
+            .on(GetItemCommand)
+            .resolves(biometricTokenIssuedSessionAttributesSessionTooOldItem);
+          result = await sessionRegistry.getSession(
+            mockSessionId,
+            getOperation,
+          );
+        });
+
+        it("Logs the failure", () => {
+          expect(consoleErrorSpy).toHaveBeenCalledWithLogFields({
+            messageCode: "MOBILE_ASYNC_GET_SESSION_SESSION_NOT_FOUND",
path: backend-api/src/functions/adapters/aws/dynamo/dynamoDbAdapter.test.ts
user: login: jhumbert-dd
body: Added
created_at: 2025-03-17T15:17:12Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,29 +1,37 @@
-import { expect } from "@jest/globals";
-import "dotenv/config";
-import "../../../../../tests/testUtils/matchers";
-import { DynamoDbAdapter } from "./dynamoDbAdapter";
-import { BiometricTokenIssued } from "../../../common/session/updateOperations/BiometricTokenIssued/BiometricTokenIssued";
-import {
-  SessionRegistry,
-  UpdateSessionError,
-  SessionUpdateFailed,
-  SessionUpdated,
-} from "../../../common/session/SessionRegistry";
 import {
   ConditionalCheckFailedException,
   DynamoDBClient,
+  GetItemCommand,
   ReturnValue,
   ReturnValuesOnConditionCheckFailure,
   UpdateItemCommand,
 } from "@aws-sdk/client-dynamodb";
+import { marshall, unmarshall } from "@aws-sdk/util-dynamodb";
+import { expect } from "@jest/globals";
 import { mockClient } from "aws-sdk-client-mock";
-import { errorResult, Result, successResult } from "../../../utils/result";
+import "dotenv/config";
path: backend-api/src/functions/adapters/aws/dynamo/dynamoDbAdapter.test.ts
user: login: sandymay-dd
body: suggestion: Please may we check if we need this? I wouldn't have thought the adapter tests need a .env file. I may be wrong
created_at: 2025-03-17T15:17:47Z
author_association: CONTRIBUTOR
diff_hunk: @@ -293,4 +309,234 @@ describe("DynamoDbAdapter", () => {
       });
     });
   });
+
+  describe("getSession", () => {
+    const getOperation: GetSessionOperation = new TxMAEvent();
path: backend-api/src/functions/adapters/aws/dynamo/dynamoDbAdapter.test.ts
user: login: sandymay-dd
body: suggestion: `new TxMAEventGetSessionOperation()` perhaps? We can roll this out in a follow-up ticket if we're happy
created_at: 2025-03-17T15:21:45Z
author_association: CONTRIBUTOR
diff_hunk: @@ -293,4 +309,234 @@ describe("DynamoDbAdapter", () => {
       });
     });
   });
+
+  describe("getSession", () => {
+    const getOperation: GetSessionOperation = new TxMAEvent();
+    let result: Result<void, SessionRetrievalFailed>;
path: backend-api/src/functions/adapters/aws/dynamo/dynamoDbAdapter.test.ts
user: login: sandymay-dd
body: issue: this can also return a success response. Is `void` correct here?
created_at: 2025-03-17T15:24:56Z
author_association: CONTRIBUTOR
diff_hunk: @@ -293,4 +309,234 @@ describe("DynamoDbAdapter", () => {
       });
     });
   });
+
+  describe("getSession", () => {
+    const getOperation: GetSessionOperation = new TxMAEvent();
+    let result: Result<void, SessionRetrievalFailed>;
+
+    describe("On every attempt", () => {
+      beforeEach(async () => {
+        mockDynamoDbClient.on(GetItemCommand).resolves({});
+        await sessionRegistry.getSession(mockSessionId, getOperation);
+      });
+
+      it("Logs the attempt", () => {
+        expect(consoleDebugSpy).toHaveBeenCalledWithLogFields({
+          messageCode: "MOBILE_ASYNC_GET_SESSION_ATTEMPT",
+          data: {
+            sessionId: mockSessionId,
+            getItemCommandInput: {
+              Key: getOperation.getDynamoDbKeyExpression(mockSessionId),
+            },
+          },
+        });
+      });
+    });
+
+    describe("Given there is an unexpected error retrieving the session", () => {
+      beforeEach(async () => {
+        mockDynamoDbClient.on(GetItemCommand).rejects("mock_error");
+        result = await sessionRegistry.getSession(mockSessionId, getOperation);
+      });
+
+      it("Logs the failure", () => {
+        expect(consoleErrorSpy).toHaveBeenCalledWithLogFields({
+          messageCode: "MOBILE_ASYNC_GET_SESSION_UNEXPECTED_FAILURE",
path: backend-api/src/functions/adapters/aws/dynamo/dynamoDbAdapter.test.ts
user: login: sandymay-dd
body: suggestion: assert that the we log the error
created_at: 2025-03-17T15:29:17Z
author_association: CONTRIBUTOR
path: backend-api/src/functions/common/session/sessionAttributes/sessionAttributes.test.ts
user: login: jhumbert-dd
body: I think `sessionState` and `createdAt` validation assertions should be added to this file
created_at: 2025-03-17T16:28:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -98,7 +98,7 @@ describe("POST /async/biometricToken", () => {
         "/async/biometricToken",
         requestBody,
       );
-    }, 15000);
+    }, 25000);
path: backend-api/tests/api-tests/biometricToken.test.ts
user: login: jhumbert-dd
body: Increased following the rule of thumb of `5000` ms for each `await` (including nested ones) after having an intermittent failure. 
created_at: 2025-03-17T16:33:09Z
author_association: CONTRIBUTOR
diff_hunk: @@ -90,8 +148,403 @@ describe("Async TxMA Event", () => {
     });
   });
 
+  describe("Retrieving a session", () => {
+    describe("Given there is an error retrieving the session", () => {
+      beforeEach(async () => {
+        dependencies.getSessionRegistry = () => ({
+          ...mockInertSessionRegistry,
+          getSession: jest.fn().mockResolvedValue(
+            errorResult({
+              errorType: GetSessionError.INTERNAL_SERVER_ERROR,
+            }),
+          ),
+        });
+        result = await lambdaHandlerConstructor(
+          dependencies,
+          validRequest,
+          context,
+        );
+      });
+
+      describe("Given DCMAW_ASYNC_CRI_5XXERROR event fails to write to TxMA", () => {
+        beforeEach(async () => {
+          dependencies.getEventService = () => ({
+            ...mockInertEventService,
+            writeGenericEvent: jest.fn().mockResolvedValue(
+              errorResult({
+                errorMessage: "mockError",
+              }),
+            ),
+          });
+
+          result = await lambdaHandlerConstructor(
+            dependencies,
+            validRequest,
+            context,
+          );
+        });
+
+        it("Logs the error", async () => {
+          expect(consoleErrorSpy).toHaveBeenCalledWithLogFields({
+            messageCode: "MOBILE_ASYNC_ERROR_WRITING_AUDIT_EVENT",
+            data: {
+              auditEventName: "DCMAW_ASYNC_CRI_5XXERROR",
+            },
+          });
+        });
+
+        it("Returns 500 Internal server error", async () => {
+          expect(result).toStrictEqual({
+            statusCode: 500,
+            body: JSON.stringify({
+              error: "server_error",
+              error_description: "Internal Server Error",
+            }),
+            headers: expectedSecurityHeaders,
+          });
+        });
+      });
+
+      describe("Given DCMAW_ASYNC_CRI_5XXERROR event successfully to write to TxMA", () => {
+        it("Writes DCMAW_ASYNC_CRI_5XXERROR event to TxMA", () => {
+          expect(mockWriteGenericEventSuccessResult).toBeCalledWith({
+            eventName: "DCMAW_ASYNC_CRI_5XXERROR",
+            componentId: "mockIssuer",
+            getNowInMilliseconds: Date.now,
+            govukSigninJourneyId: undefined,
+            sessionId: mockSessionId,
+            sub: undefined,
+            ipAddress: "1.1.1.1",
+            txmaAuditEncoded: "mockTxmaAuditEncodedHeader",
+            redirect_uri: undefined,
+            suspected_fraud_signal: undefined,
+          });
+        });
+
+        it("Returns 500 Internal server error", async () => {
+          expect(result).toStrictEqual({
+            statusCode: 500,
+            body: JSON.stringify({
+              error: "server_error",
+              error_description: "Internal Server Error",
+            }),
+            headers: expectedSecurityHeaders,
+          });
+        });
+      });
+    });
+
+    describe("Given failure is due to client error", () => {
+      describe("Given session was not found", () => {
+        beforeEach(async () => {
+          dependencies.getSessionRegistry = () => ({
+            ...mockInertSessionRegistry,
+            getSession: jest.fn().mockResolvedValue(
+              errorResult({
+                errorType: GetSessionError.SESSION_NOT_FOUND,
+                session: "Session not found",
+              }),
+            ),
+          });
+          result = await lambdaHandlerConstructor(
+            dependencies,
+            validRequest,
+            context,
+          );
+        });
+
+        describe("Given DCMAW_ASYNC_CRI_4XXERROR event fails to write to TxMA", () => {
+          beforeEach(async () => {
+            dependencies.getEventService = () => ({
+              ...mockInertEventService,
+              writeGenericEvent: jest.fn().mockResolvedValue(
+                errorResult({
+                  errorMessage: "mockError",
+                }),
+              ),
+            });
+
+            result = await lambdaHandlerConstructor(
+              dependencies,
+              validRequest,
+              context,
+            );
+          });
+
+          it("Logs the error", async () => {
+            expect(consoleErrorSpy).toHaveBeenCalledWithLogFields({
+              messageCode: "MOBILE_ASYNC_ERROR_WRITING_AUDIT_EVENT",
+              data: {
+                auditEventName: "DCMAW_ASYNC_CRI_4XXERROR",
+              },
+            });
+          });
+
+          it("Returns 500 Internal Server Error ", async () => {
+            expect(result).toStrictEqual({
+              statusCode: 500,
+              body: JSON.stringify({
+                error: "server_error",
+                error_description: "Internal Server Error",
+              }),
+              headers: expectedSecurityHeaders,
+            });
+          });
+        });
+
+        it("Writes DCMAW_ASYNC_CRI_4XXERROR event to TxMA", () => {
+          expect(mockWriteGenericEventSuccessResult).toBeCalledWith({
+            eventName: "DCMAW_ASYNC_CRI_4XXERROR",
+            componentId: "mockIssuer",
+            getNowInMilliseconds: Date.now,
+            govukSigninJourneyId: undefined,
+            sessionId: mockSessionId,
+            sub: undefined,
+            ipAddress: "1.1.1.1",
+            txmaAuditEncoded: "mockTxmaAuditEncodedHeader",
+            redirect_uri: undefined,
+            suspected_fraud_signal: undefined,
+          });
+        });
+
+        it("Logs the error", async () => {
+          expect(consoleErrorSpy).toHaveBeenCalledWithLogFields({
+            messageCode: "MOBILE_ASYNC_TXMA_EVENT_INVALID_SESSION",
+            data: {
+              auditEventName: "DCMAW_ASYNC_CRI_4XXERROR",
+              session: "Session not found",
+            },
+          });
+        });
+
+        it("Returns 401 Unauthorized", () => {
+          expect(result).toStrictEqual({
+            statusCode: 401,
+            body: JSON.stringify({
+              error: "invalid_session",
+              error_description: "Session does not exist or in incorrect state",
+            }),
+            headers: expectedSecurityHeaders,
+          });
+        });
+      });
+    });
+  });
+
+  describe("Session validation", () => {
path: backend-api/src/functions/asyncTxmaEvent/asyncTxmaEventHandler.test.ts
user: login: jhumbert-dd
body: I think this can be condensed into one test as the `dynamoAdapter` tests cover the conditional branching for session validation.
created_at: 2025-03-17T16:46:37Z
author_association: CONTRIBUTOR
diff_hunk: @@ -293,4 +309,234 @@ describe("DynamoDbAdapter", () => {
       });
     });
   });
+
+  describe("getSession", () => {
+    const getOperation: GetSessionOperation = new TxMAEvent();
+    let result: Result<void, SessionRetrievalFailed>;
+
+    describe("On every attempt", () => {
+      beforeEach(async () => {
+        mockDynamoDbClient.on(GetItemCommand).resolves({});
+        await sessionRegistry.getSession(mockSessionId, getOperation);
+      });
+
+      it("Logs the attempt", () => {
+        expect(consoleDebugSpy).toHaveBeenCalledWithLogFields({
+          messageCode: "MOBILE_ASYNC_GET_SESSION_ATTEMPT",
+          data: {
+            sessionId: mockSessionId,
+            getItemCommandInput:
+              getOperation.getDynamoDbKeyExpression(mockSessionId),
+          },
+        });
+      });
+    });
+
+    describe("Given there is an unexpected error retrieving the session", () => {
+      beforeEach(async () => {
+        mockDynamoDbClient.on(GetItemCommand).rejects("mock_error");
+        result = await sessionRegistry.getSession(mockSessionId, getOperation);
+      });
+
+      it("Logs the failure", () => {
+        expect(consoleErrorSpy).toHaveBeenCalledWithLogFields({
+          messageCode: "MOBILE_ASYNC_GET_SESSION_UNEXPECTED_FAILURE",
+        });
+      });
+
+      it("Returns failure with server error", () => {
+        expect(result).toEqual(
+          errorResult({
+            errorType: GetSessionError.INTERNAL_SERVER_ERROR,
+          }),
+        );
+      });
+    });
+
+    describe("Given the session was not found", () => {
+      beforeEach(async () => {
+        mockDynamoDbClient.on(GetItemCommand).resolves({});
+        result = await sessionRegistry.getSession(mockSessionId, getOperation);
+      });
+
+      it("Logs the failure", () => {
+        expect(consoleErrorSpy).toHaveBeenCalledWithLogFields({
+          messageCode: "MOBILE_ASYNC_GET_SESSION_SESSION_NOT_FOUND",
+          session: "Session not found",
+        });
+      });
+
+      it("Returns failure with an invalid session error", () => {
+        expect(result).toEqual(
+          errorResult({
+            errorType: GetSessionError.SESSION_NOT_FOUND,
+            session: "Session not found",
+          }),
+        );
+      });
+    });
+
+    describe("Session validation", () => {
+      describe("Given the session is in the wrong state", () => {
+        const invalidBiometricTokenIssuedSessionAttributesWrongSessionState = {
+          ...validBiometricTokenIssuedSessionAttributes,
+          createdAt: 1704106860000, // 2024-01-01 11:01:00.000
path: backend-api/src/functions/adapters/aws/dynamo/dynamoDbAdapter.test.ts
user: login: sandymay-dd
body: question: the unitTestData.ts file already has this set as a valid value - is there a reason why we're overriding it? I may be missing something
created_at: 2025-03-17T20:50:14Z
author_association: CONTRIBUTOR
diff_hunk: @@ -293,4 +309,234 @@ describe("DynamoDbAdapter", () => {
       });
     });
   });
+
+  describe("getSession", () => {
+    const getOperation: GetSessionOperation = new TxMAEvent();
+    let result: Result<void, SessionRetrievalFailed>;
+
+    describe("On every attempt", () => {
+      beforeEach(async () => {
+        mockDynamoDbClient.on(GetItemCommand).resolves({});
+        await sessionRegistry.getSession(mockSessionId, getOperation);
+      });
+
+      it("Logs the attempt", () => {
+        expect(consoleDebugSpy).toHaveBeenCalledWithLogFields({
+          messageCode: "MOBILE_ASYNC_GET_SESSION_ATTEMPT",
+          data: {
+            sessionId: mockSessionId,
+            getItemCommandInput:
+              getOperation.getDynamoDbKeyExpression(mockSessionId),
+          },
+        });
+      });
+    });
+
+    describe("Given there is an unexpected error retrieving the session", () => {
+      beforeEach(async () => {
+        mockDynamoDbClient.on(GetItemCommand).rejects("mock_error");
+        result = await sessionRegistry.getSession(mockSessionId, getOperation);
+      });
+
+      it("Logs the failure", () => {
+        expect(consoleErrorSpy).toHaveBeenCalledWithLogFields({
+          messageCode: "MOBILE_ASYNC_GET_SESSION_UNEXPECTED_FAILURE",
+        });
+      });
+
+      it("Returns failure with server error", () => {
+        expect(result).toEqual(
+          errorResult({
+            errorType: GetSessionError.INTERNAL_SERVER_ERROR,
+          }),
+        );
+      });
+    });
+
+    describe("Given the session was not found", () => {
+      beforeEach(async () => {
+        mockDynamoDbClient.on(GetItemCommand).resolves({});
+        result = await sessionRegistry.getSession(mockSessionId, getOperation);
+      });
+
+      it("Logs the failure", () => {
+        expect(consoleErrorSpy).toHaveBeenCalledWithLogFields({
+          messageCode: "MOBILE_ASYNC_GET_SESSION_SESSION_NOT_FOUND",
+          session: "Session not found",
+        });
+      });
+
+      it("Returns failure with an invalid session error", () => {
+        expect(result).toEqual(
+          errorResult({
+            errorType: GetSessionError.SESSION_NOT_FOUND,
+            session: "Session not found",
+          }),
+        );
+      });
+    });
+
+    describe("Session validation", () => {
+      describe("Given the session is in the wrong state", () => {
+        const invalidBiometricTokenIssuedSessionAttributesWrongSessionState = {
path: backend-api/src/functions/adapters/aws/dynamo/dynamoDbAdapter.test.ts
user: login: sandymay-dd
body: question: I wonder whether we should move this into the `unitTestData.ts` file - is this something you've considered? If we do, I'd keep the marshalled item in this file.
created_at: 2025-03-17T20:58:45Z
author_association: CONTRIBUTOR
diff_hunk: @@ -293,4 +309,234 @@ describe("DynamoDbAdapter", () => {
       });
     });
   });
+
+  describe("getSession", () => {
+    const getOperation: GetSessionOperation = new TxMAEvent();
+    let result: Result<void, SessionRetrievalFailed>;
+
+    describe("On every attempt", () => {
+      beforeEach(async () => {
+        mockDynamoDbClient.on(GetItemCommand).resolves({});
+        await sessionRegistry.getSession(mockSessionId, getOperation);
+      });
+
+      it("Logs the attempt", () => {
+        expect(consoleDebugSpy).toHaveBeenCalledWithLogFields({
+          messageCode: "MOBILE_ASYNC_GET_SESSION_ATTEMPT",
+          data: {
+            sessionId: mockSessionId,
+            getItemCommandInput:
+              getOperation.getDynamoDbKeyExpression(mockSessionId),
+          },
+        });
+      });
+    });
+
+    describe("Given there is an unexpected error retrieving the session", () => {
+      beforeEach(async () => {
+        mockDynamoDbClient.on(GetItemCommand).rejects("mock_error");
+        result = await sessionRegistry.getSession(mockSessionId, getOperation);
+      });
+
+      it("Logs the failure", () => {
+        expect(consoleErrorSpy).toHaveBeenCalledWithLogFields({
+          messageCode: "MOBILE_ASYNC_GET_SESSION_UNEXPECTED_FAILURE",
+        });
+      });
+
+      it("Returns failure with server error", () => {
+        expect(result).toEqual(
+          errorResult({
+            errorType: GetSessionError.INTERNAL_SERVER_ERROR,
+          }),
+        );
+      });
+    });
+
+    describe("Given the session was not found", () => {
+      beforeEach(async () => {
+        mockDynamoDbClient.on(GetItemCommand).resolves({});
+        result = await sessionRegistry.getSession(mockSessionId, getOperation);
+      });
+
+      it("Logs the failure", () => {
+        expect(consoleErrorSpy).toHaveBeenCalledWithLogFields({
+          messageCode: "MOBILE_ASYNC_GET_SESSION_SESSION_NOT_FOUND",
+          session: "Session not found",
+        });
+      });
+
+      it("Returns failure with an invalid session error", () => {
+        expect(result).toEqual(
+          errorResult({
+            errorType: GetSessionError.SESSION_NOT_FOUND,
+            session: "Session not found",
+          }),
+        );
+      });
+    });
+
+    describe("Session validation", () => {
+      describe("Given the session is in the wrong state", () => {
+        const invalidBiometricTokenIssuedSessionAttributesWrongSessionState = {
+          ...validBiometricTokenIssuedSessionAttributes,
+          createdAt: 1704106860000, // 2024-01-01 11:01:00.000
+          sessionState: SessionState.AUTH_SESSION_CREATED,
+        };
+        const invalidBiometricTokenIssuedSessionAttributesWrongSessionStateItem =
+          {
+            Item: marshall(
+              invalidBiometricTokenIssuedSessionAttributesWrongSessionState,
+            ),
+          };
+
+        beforeEach(async () => {
+          mockDynamoDbClient
+            .on(GetItemCommand)
+            .resolves(
+              invalidBiometricTokenIssuedSessionAttributesWrongSessionStateItem,
+            );
+          result = await sessionRegistry.getSession(
+            mockSessionId,
+            getOperation,
+          );
+        });
+
+        it("Logs the failure", () => {
+          expect(consoleErrorSpy).toHaveBeenCalledWithLogFields({
+            messageCode: "MOBILE_ASYNC_GET_SESSION_SESSION_NOT_FOUND",
path: backend-api/src/functions/adapters/aws/dynamo/dynamoDbAdapter.test.ts
user: login: sandymay-dd
body: suggestion: MOBILE_ASYNC_GET_SESSION_SESSION_INVALID perhaps.


I don't think this is a case of session not found. It's a case of the session failing validation. The log message is misleading here. 
created_at: 2025-03-17T21:11:32Z
author_association: CONTRIBUTOR
diff_hunk: @@ -293,4 +309,234 @@ describe("DynamoDbAdapter", () => {
       });
     });
   });
+
+  describe("getSession", () => {
+    const getOperation: GetSessionOperation = new TxMAEvent();
+    let result: Result<void, SessionRetrievalFailed>;
+
+    describe("On every attempt", () => {
+      beforeEach(async () => {
+        mockDynamoDbClient.on(GetItemCommand).resolves({});
+        await sessionRegistry.getSession(mockSessionId, getOperation);
+      });
+
+      it("Logs the attempt", () => {
+        expect(consoleDebugSpy).toHaveBeenCalledWithLogFields({
+          messageCode: "MOBILE_ASYNC_GET_SESSION_ATTEMPT",
+          data: {
+            sessionId: mockSessionId,
+            getItemCommandInput:
+              getOperation.getDynamoDbKeyExpression(mockSessionId),
+          },
+        });
+      });
+    });
+
+    describe("Given there is an unexpected error retrieving the session", () => {
+      beforeEach(async () => {
+        mockDynamoDbClient.on(GetItemCommand).rejects("mock_error");
+        result = await sessionRegistry.getSession(mockSessionId, getOperation);
+      });
+
+      it("Logs the failure", () => {
+        expect(consoleErrorSpy).toHaveBeenCalledWithLogFields({
+          messageCode: "MOBILE_ASYNC_GET_SESSION_UNEXPECTED_FAILURE",
+        });
+      });
+
+      it("Returns failure with server error", () => {
+        expect(result).toEqual(
+          errorResult({
+            errorType: GetSessionError.INTERNAL_SERVER_ERROR,
+          }),
+        );
+      });
+    });
+
+    describe("Given the session was not found", () => {
+      beforeEach(async () => {
+        mockDynamoDbClient.on(GetItemCommand).resolves({});
+        result = await sessionRegistry.getSession(mockSessionId, getOperation);
+      });
+
+      it("Logs the failure", () => {
+        expect(consoleErrorSpy).toHaveBeenCalledWithLogFields({
+          messageCode: "MOBILE_ASYNC_GET_SESSION_SESSION_NOT_FOUND",
+          session: "Session not found",
+        });
+      });
+
+      it("Returns failure with an invalid session error", () => {
+        expect(result).toEqual(
+          errorResult({
+            errorType: GetSessionError.SESSION_NOT_FOUND,
+            session: "Session not found",
+          }),
+        );
+      });
+    });
+
+    describe("Session validation", () => {
+      describe("Given the session is in the wrong state", () => {
+        const invalidBiometricTokenIssuedSessionAttributesWrongSessionState = {
+          ...validBiometricTokenIssuedSessionAttributes,
+          createdAt: 1704106860000, // 2024-01-01 11:01:00.000
+          sessionState: SessionState.AUTH_SESSION_CREATED,
+        };
+        const invalidBiometricTokenIssuedSessionAttributesWrongSessionStateItem =
+          {
+            Item: marshall(
+              invalidBiometricTokenIssuedSessionAttributesWrongSessionState,
+            ),
+          };
+
+        beforeEach(async () => {
+          mockDynamoDbClient
+            .on(GetItemCommand)
+            .resolves(
+              invalidBiometricTokenIssuedSessionAttributesWrongSessionStateItem,
+            );
+          result = await sessionRegistry.getSession(
+            mockSessionId,
+            getOperation,
+          );
+        });
+
+        it("Logs the failure", () => {
+          expect(consoleErrorSpy).toHaveBeenCalledWithLogFields({
+            messageCode: "MOBILE_ASYNC_GET_SESSION_SESSION_NOT_FOUND",
+            errorMessage:
+              "Session could not be parsed or session validation failed",
path: backend-api/src/functions/adapters/aws/dynamo/dynamoDbAdapter.test.ts
user: login: sandymay-dd
body: though: In an ideal world, we would like to know _why_ the session validation fails. This isn't something we currently do in the UpdateSession operations, but has historically been important intelligence for debugging issues in the live service. 

created_at: 2025-03-17T21:16:49Z
author_association: CONTRIBUTOR
diff_hunk: @@ -293,4 +309,234 @@ describe("DynamoDbAdapter", () => {
       });
     });
   });
+
+  describe("getSession", () => {
+    const getOperation: GetSessionOperation = new TxMAEvent();
+    let result: Result<void, SessionRetrievalFailed>;
+
+    describe("On every attempt", () => {
+      beforeEach(async () => {
+        mockDynamoDbClient.on(GetItemCommand).resolves({});
+        await sessionRegistry.getSession(mockSessionId, getOperation);
+      });
+
+      it("Logs the attempt", () => {
+        expect(consoleDebugSpy).toHaveBeenCalledWithLogFields({
+          messageCode: "MOBILE_ASYNC_GET_SESSION_ATTEMPT",
+          data: {
+            sessionId: mockSessionId,
+            getItemCommandInput:
+              getOperation.getDynamoDbKeyExpression(mockSessionId),
+          },
+        });
+      });
+    });
+
+    describe("Given there is an unexpected error retrieving the session", () => {
+      beforeEach(async () => {
+        mockDynamoDbClient.on(GetItemCommand).rejects("mock_error");
+        result = await sessionRegistry.getSession(mockSessionId, getOperation);
+      });
+
+      it("Logs the failure", () => {
+        expect(consoleErrorSpy).toHaveBeenCalledWithLogFields({
+          messageCode: "MOBILE_ASYNC_GET_SESSION_UNEXPECTED_FAILURE",
+        });
+      });
+
+      it("Returns failure with server error", () => {
+        expect(result).toEqual(
+          errorResult({
+            errorType: GetSessionError.INTERNAL_SERVER_ERROR,
+          }),
+        );
+      });
+    });
+
+    describe("Given the session was not found", () => {
+      beforeEach(async () => {
+        mockDynamoDbClient.on(GetItemCommand).resolves({});
+        result = await sessionRegistry.getSession(mockSessionId, getOperation);
+      });
+
+      it("Logs the failure", () => {
+        expect(consoleErrorSpy).toHaveBeenCalledWithLogFields({
+          messageCode: "MOBILE_ASYNC_GET_SESSION_SESSION_NOT_FOUND",
+          session: "Session not found",
+        });
+      });
+
+      it("Returns failure with an invalid session error", () => {
+        expect(result).toEqual(
+          errorResult({
+            errorType: GetSessionError.SESSION_NOT_FOUND,
+            session: "Session not found",
+          }),
+        );
+      });
+    });
+
+    describe("Session validation", () => {
+      describe("Given the session is in the wrong state", () => {
+        const invalidBiometricTokenIssuedSessionAttributesWrongSessionState = {
+          ...validBiometricTokenIssuedSessionAttributes,
+          createdAt: 1704106860000, // 2024-01-01 11:01:00.000
+          sessionState: SessionState.AUTH_SESSION_CREATED,
+        };
+        const invalidBiometricTokenIssuedSessionAttributesWrongSessionStateItem =
+          {
+            Item: marshall(
+              invalidBiometricTokenIssuedSessionAttributesWrongSessionState,
+            ),
+          };
+
+        beforeEach(async () => {
+          mockDynamoDbClient
+            .on(GetItemCommand)
+            .resolves(
+              invalidBiometricTokenIssuedSessionAttributesWrongSessionStateItem,
+            );
+          result = await sessionRegistry.getSession(
+            mockSessionId,
+            getOperation,
+          );
+        });
+
+        it("Logs the failure", () => {
+          expect(consoleErrorSpy).toHaveBeenCalledWithLogFields({
+            messageCode: "MOBILE_ASYNC_GET_SESSION_SESSION_NOT_FOUND",
+            errorMessage:
+              "Session could not be parsed or session validation failed",
+            session:
+              invalidBiometricTokenIssuedSessionAttributesWrongSessionState,
+          });
+        });
+
+        it("Returns failure with an invalid session error", () => {
+          expect(result).toStrictEqual(
+            errorResult({
+              errorType: GetSessionError.SESSION_NOT_FOUND,
path: backend-api/src/functions/adapters/aws/dynamo/dynamoDbAdapter.test.ts
user: login: sandymay-dd
body: suggestion: I think we should have a SESSION_INVALID or something similar here.
created_at: 2025-03-17T21:17:24Z
author_association: CONTRIBUTOR
diff_hunk: @@ -293,4 +309,234 @@ describe("DynamoDbAdapter", () => {
       });
     });
   });
+
+  describe("getSession", () => {
+    const getOperation: GetSessionOperation = new TxMAEvent();
+    let result: Result<void, SessionRetrievalFailed>;
+
+    describe("On every attempt", () => {
+      beforeEach(async () => {
+        mockDynamoDbClient.on(GetItemCommand).resolves({});
+        await sessionRegistry.getSession(mockSessionId, getOperation);
+      });
+
+      it("Logs the attempt", () => {
+        expect(consoleDebugSpy).toHaveBeenCalledWithLogFields({
+          messageCode: "MOBILE_ASYNC_GET_SESSION_ATTEMPT",
+          data: {
+            sessionId: mockSessionId,
+            getItemCommandInput:
+              getOperation.getDynamoDbKeyExpression(mockSessionId),
+          },
+        });
+      });
+    });
+
+    describe("Given there is an unexpected error retrieving the session", () => {
+      beforeEach(async () => {
+        mockDynamoDbClient.on(GetItemCommand).rejects("mock_error");
+        result = await sessionRegistry.getSession(mockSessionId, getOperation);
+      });
+
+      it("Logs the failure", () => {
+        expect(consoleErrorSpy).toHaveBeenCalledWithLogFields({
+          messageCode: "MOBILE_ASYNC_GET_SESSION_UNEXPECTED_FAILURE",
+        });
+      });
+
+      it("Returns failure with server error", () => {
+        expect(result).toEqual(
+          errorResult({
+            errorType: GetSessionError.INTERNAL_SERVER_ERROR,
+          }),
+        );
+      });
+    });
+
+    describe("Given the session was not found", () => {
+      beforeEach(async () => {
+        mockDynamoDbClient.on(GetItemCommand).resolves({});
+        result = await sessionRegistry.getSession(mockSessionId, getOperation);
+      });
+
+      it("Logs the failure", () => {
+        expect(consoleErrorSpy).toHaveBeenCalledWithLogFields({
+          messageCode: "MOBILE_ASYNC_GET_SESSION_SESSION_NOT_FOUND",
+          session: "Session not found",
+        });
+      });
+
+      it("Returns failure with an invalid session error", () => {
+        expect(result).toEqual(
+          errorResult({
+            errorType: GetSessionError.SESSION_NOT_FOUND,
+            session: "Session not found",
+          }),
+        );
+      });
+    });
+
+    describe("Session validation", () => {
+      describe("Given the session is in the wrong state", () => {
+        const invalidBiometricTokenIssuedSessionAttributesWrongSessionState = {
+          ...validBiometricTokenIssuedSessionAttributes,
+          createdAt: 1704106860000, // 2024-01-01 11:01:00.000
+          sessionState: SessionState.AUTH_SESSION_CREATED,
+        };
+        const invalidBiometricTokenIssuedSessionAttributesWrongSessionStateItem =
+          {
+            Item: marshall(
+              invalidBiometricTokenIssuedSessionAttributesWrongSessionState,
+            ),
+          };
+
+        beforeEach(async () => {
+          mockDynamoDbClient
+            .on(GetItemCommand)
+            .resolves(
+              invalidBiometricTokenIssuedSessionAttributesWrongSessionStateItem,
+            );
+          result = await sessionRegistry.getSession(
+            mockSessionId,
+            getOperation,
+          );
+        });
+
+        it("Logs the failure", () => {
+          expect(consoleErrorSpy).toHaveBeenCalledWithLogFields({
+            messageCode: "MOBILE_ASYNC_GET_SESSION_SESSION_NOT_FOUND",
+            errorMessage:
+              "Session could not be parsed or session validation failed",
+            session:
+              invalidBiometricTokenIssuedSessionAttributesWrongSessionState,
+          });
+        });
+
+        it("Returns failure with an invalid session error", () => {
+          expect(result).toStrictEqual(
+            errorResult({
+              errorType: GetSessionError.SESSION_NOT_FOUND,
+              session:
+                invalidBiometricTokenIssuedSessionAttributesWrongSessionState,
+            }),
+          );
+        });
+      });
+
+      describe("Given the session is more than 60 minutes old", () => {
+        const invalidBiometricTokenIssuedSessionAttributesSessionTooOld = {
+          ...validBiometricTokenIssuedSessionAttributes,
+          createdAt: 1704106740000, // 2024-01-01 10:59:00.000
+        };
+        const biometricTokenIssuedSessionAttributesSessionTooOldItem = {
+          Item: marshall(
+            invalidBiometricTokenIssuedSessionAttributesSessionTooOld,
+          ),
+        };
+
+        beforeEach(async () => {
+          mockDynamoDbClient
+            .on(GetItemCommand)
+            .resolves(biometricTokenIssuedSessionAttributesSessionTooOldItem);
+          result = await sessionRegistry.getSession(
+            mockSessionId,
+            getOperation,
+          );
+        });
+
+        it("Logs the failure", () => {
+          expect(consoleErrorSpy).toHaveBeenCalledWithLogFields({
+            messageCode: "MOBILE_ASYNC_GET_SESSION_SESSION_NOT_FOUND",
path: backend-api/src/functions/adapters/aws/dynamo/dynamoDbAdapter.test.ts
user: login: sandymay-dd
body: suggestion: as above.
created_at: 2025-03-17T21:18:35Z
author_association: CONTRIBUTOR
diff_hunk: @@ -293,4 +309,234 @@ describe("DynamoDbAdapter", () => {
       });
     });
   });
+
+  describe("getSession", () => {
+    const getOperation: GetSessionOperation = new TxMAEvent();
+    let result: Result<void, SessionRetrievalFailed>;
+
+    describe("On every attempt", () => {
+      beforeEach(async () => {
+        mockDynamoDbClient.on(GetItemCommand).resolves({});
+        await sessionRegistry.getSession(mockSessionId, getOperation);
+      });
+
+      it("Logs the attempt", () => {
+        expect(consoleDebugSpy).toHaveBeenCalledWithLogFields({
+          messageCode: "MOBILE_ASYNC_GET_SESSION_ATTEMPT",
+          data: {
+            sessionId: mockSessionId,
+            getItemCommandInput:
+              getOperation.getDynamoDbKeyExpression(mockSessionId),
+          },
+        });
+      });
+    });
+
+    describe("Given there is an unexpected error retrieving the session", () => {
+      beforeEach(async () => {
+        mockDynamoDbClient.on(GetItemCommand).rejects("mock_error");
+        result = await sessionRegistry.getSession(mockSessionId, getOperation);
+      });
+
+      it("Logs the failure", () => {
+        expect(consoleErrorSpy).toHaveBeenCalledWithLogFields({
+          messageCode: "MOBILE_ASYNC_GET_SESSION_UNEXPECTED_FAILURE",
+        });
+      });
+
+      it("Returns failure with server error", () => {
+        expect(result).toEqual(
+          errorResult({
+            errorType: GetSessionError.INTERNAL_SERVER_ERROR,
+          }),
+        );
+      });
+    });
+
+    describe("Given the session was not found", () => {
+      beforeEach(async () => {
+        mockDynamoDbClient.on(GetItemCommand).resolves({});
+        result = await sessionRegistry.getSession(mockSessionId, getOperation);
+      });
+
+      it("Logs the failure", () => {
+        expect(consoleErrorSpy).toHaveBeenCalledWithLogFields({
+          messageCode: "MOBILE_ASYNC_GET_SESSION_SESSION_NOT_FOUND",
+          session: "Session not found",
+        });
+      });
+
+      it("Returns failure with an invalid session error", () => {
+        expect(result).toEqual(
+          errorResult({
+            errorType: GetSessionError.SESSION_NOT_FOUND,
+            session: "Session not found",
+          }),
+        );
+      });
+    });
+
+    describe("Session validation", () => {
+      describe("Given the session is in the wrong state", () => {
+        const invalidBiometricTokenIssuedSessionAttributesWrongSessionState = {
+          ...validBiometricTokenIssuedSessionAttributes,
+          createdAt: 1704106860000, // 2024-01-01 11:01:00.000
+          sessionState: SessionState.AUTH_SESSION_CREATED,
+        };
+        const invalidBiometricTokenIssuedSessionAttributesWrongSessionStateItem =
+          {
+            Item: marshall(
+              invalidBiometricTokenIssuedSessionAttributesWrongSessionState,
+            ),
+          };
+
+        beforeEach(async () => {
+          mockDynamoDbClient
+            .on(GetItemCommand)
+            .resolves(
+              invalidBiometricTokenIssuedSessionAttributesWrongSessionStateItem,
+            );
+          result = await sessionRegistry.getSession(
+            mockSessionId,
+            getOperation,
+          );
+        });
+
+        it("Logs the failure", () => {
+          expect(consoleErrorSpy).toHaveBeenCalledWithLogFields({
+            messageCode: "MOBILE_ASYNC_GET_SESSION_SESSION_NOT_FOUND",
+            errorMessage:
+              "Session could not be parsed or session validation failed",
+            session:
+              invalidBiometricTokenIssuedSessionAttributesWrongSessionState,
+          });
+        });
+
+        it("Returns failure with an invalid session error", () => {
+          expect(result).toStrictEqual(
+            errorResult({
+              errorType: GetSessionError.SESSION_NOT_FOUND,
+              session:
+                invalidBiometricTokenIssuedSessionAttributesWrongSessionState,
+            }),
+          );
+        });
+      });
+
+      describe("Given the session is more than 60 minutes old", () => {
+        const invalidBiometricTokenIssuedSessionAttributesSessionTooOld = {
+          ...validBiometricTokenIssuedSessionAttributes,
+          createdAt: 1704106740000, // 2024-01-01 10:59:00.000
+        };
+        const biometricTokenIssuedSessionAttributesSessionTooOldItem = {
+          Item: marshall(
+            invalidBiometricTokenIssuedSessionAttributesSessionTooOld,
+          ),
+        };
+
+        beforeEach(async () => {
+          mockDynamoDbClient
+            .on(GetItemCommand)
+            .resolves(biometricTokenIssuedSessionAttributesSessionTooOldItem);
+          result = await sessionRegistry.getSession(
+            mockSessionId,
+            getOperation,
+          );
+        });
+
+        it("Logs the failure", () => {
+          expect(consoleErrorSpy).toHaveBeenCalledWithLogFields({
+            messageCode: "MOBILE_ASYNC_GET_SESSION_SESSION_NOT_FOUND",
+            session: invalidBiometricTokenIssuedSessionAttributesSessionTooOld,
+          });
+        });
+
+        it("Returns failure with an invalid session error", () => {
+          expect(result).toStrictEqual(
+            errorResult({
+              errorType: GetSessionError.SESSION_NOT_FOUND,
path: backend-api/src/functions/adapters/aws/dynamo/dynamoDbAdapter.test.ts
user: login: sandymay-dd
body: suggestion: as above
created_at: 2025-03-17T21:18:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -293,4 +309,234 @@ describe("DynamoDbAdapter", () => {
       });
     });
   });
+
+  describe("getSession", () => {
+    const getOperation: GetSessionOperation = new TxMAEvent();
+    let result: Result<void, SessionRetrievalFailed>;
+
+    describe("On every attempt", () => {
+      beforeEach(async () => {
+        mockDynamoDbClient.on(GetItemCommand).resolves({});
+        await sessionRegistry.getSession(mockSessionId, getOperation);
+      });
+
+      it("Logs the attempt", () => {
+        expect(consoleDebugSpy).toHaveBeenCalledWithLogFields({
+          messageCode: "MOBILE_ASYNC_GET_SESSION_ATTEMPT",
+          data: {
+            sessionId: mockSessionId,
+            getItemCommandInput:
+              getOperation.getDynamoDbKeyExpression(mockSessionId),
+          },
+        });
+      });
+    });
+
+    describe("Given there is an unexpected error retrieving the session", () => {
+      beforeEach(async () => {
+        mockDynamoDbClient.on(GetItemCommand).rejects("mock_error");
+        result = await sessionRegistry.getSession(mockSessionId, getOperation);
+      });
+
+      it("Logs the failure", () => {
+        expect(consoleErrorSpy).toHaveBeenCalledWithLogFields({
+          messageCode: "MOBILE_ASYNC_GET_SESSION_UNEXPECTED_FAILURE",
+        });
+      });
+
+      it("Returns failure with server error", () => {
+        expect(result).toEqual(
+          errorResult({
+            errorType: GetSessionError.INTERNAL_SERVER_ERROR,
+          }),
+        );
+      });
+    });
+
+    describe("Given the session was not found", () => {
+      beforeEach(async () => {
+        mockDynamoDbClient.on(GetItemCommand).resolves({});
+        result = await sessionRegistry.getSession(mockSessionId, getOperation);
+      });
+
+      it("Logs the failure", () => {
+        expect(consoleErrorSpy).toHaveBeenCalledWithLogFields({
+          messageCode: "MOBILE_ASYNC_GET_SESSION_SESSION_NOT_FOUND",
+          session: "Session not found",
+        });
+      });
+
+      it("Returns failure with an invalid session error", () => {
+        expect(result).toEqual(
+          errorResult({
+            errorType: GetSessionError.SESSION_NOT_FOUND,
+            session: "Session not found",
+          }),
+        );
+      });
+    });
+
+    describe("Session validation", () => {
+      describe("Given the session is in the wrong state", () => {
+        const invalidBiometricTokenIssuedSessionAttributesWrongSessionState = {
+          ...validBiometricTokenIssuedSessionAttributes,
+          createdAt: 1704106860000, // 2024-01-01 11:01:00.000
+          sessionState: SessionState.AUTH_SESSION_CREATED,
+        };
+        const invalidBiometricTokenIssuedSessionAttributesWrongSessionStateItem =
+          {
+            Item: marshall(
+              invalidBiometricTokenIssuedSessionAttributesWrongSessionState,
+            ),
+          };
+
+        beforeEach(async () => {
+          mockDynamoDbClient
+            .on(GetItemCommand)
+            .resolves(
+              invalidBiometricTokenIssuedSessionAttributesWrongSessionStateItem,
+            );
+          result = await sessionRegistry.getSession(
+            mockSessionId,
+            getOperation,
+          );
+        });
+
+        it("Logs the failure", () => {
+          expect(consoleErrorSpy).toHaveBeenCalledWithLogFields({
+            messageCode: "MOBILE_ASYNC_GET_SESSION_SESSION_NOT_FOUND",
+            errorMessage:
+              "Session could not be parsed or session validation failed",
+            session:
+              invalidBiometricTokenIssuedSessionAttributesWrongSessionState,
+          });
+        });
+
+        it("Returns failure with an invalid session error", () => {
+          expect(result).toStrictEqual(
+            errorResult({
+              errorType: GetSessionError.SESSION_NOT_FOUND,
+              session:
+                invalidBiometricTokenIssuedSessionAttributesWrongSessionState,
+            }),
+          );
+        });
+      });
+
+      describe("Given the session is more than 60 minutes old", () => {
+        const invalidBiometricTokenIssuedSessionAttributesSessionTooOld = {
+          ...validBiometricTokenIssuedSessionAttributes,
+          createdAt: 1704106740000, // 2024-01-01 10:59:00.000
+        };
+        const biometricTokenIssuedSessionAttributesSessionTooOldItem = {
+          Item: marshall(
+            invalidBiometricTokenIssuedSessionAttributesSessionTooOld,
+          ),
+        };
+
+        beforeEach(async () => {
+          mockDynamoDbClient
+            .on(GetItemCommand)
+            .resolves(biometricTokenIssuedSessionAttributesSessionTooOldItem);
+          result = await sessionRegistry.getSession(
+            mockSessionId,
+            getOperation,
+          );
+        });
+
+        it("Logs the failure", () => {
+          expect(consoleErrorSpy).toHaveBeenCalledWithLogFields({
+            messageCode: "MOBILE_ASYNC_GET_SESSION_SESSION_NOT_FOUND",
+            session: invalidBiometricTokenIssuedSessionAttributesSessionTooOld,
+          });
+        });
+
+        it("Returns failure with an invalid session error", () => {
+          expect(result).toStrictEqual(
+            errorResult({
+              errorType: GetSessionError.SESSION_NOT_FOUND,
+              session:
+                invalidBiometricTokenIssuedSessionAttributesSessionTooOld,
+            }),
+          );
+        });
+      });
+    });
+
+    describe("Given the session was found", () => {
path: backend-api/src/functions/adapters/aws/dynamo/dynamoDbAdapter.test.ts
user: login: sandymay-dd
body: suggestion: please may you give the describe block structure/naming a review? The tests that validate session age/state also thematically fit within 'Given the session was found'

<img width="774" alt="image" src="https://github.com/user-attachments/assets/3fbdc808-f206-4f9c-8e09-5050640d5404" />

created_at: 2025-03-17T21:22:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -293,4 +309,234 @@ describe("DynamoDbAdapter", () => {
       });
     });
   });
+
+  describe("getSession", () => {
+    const getOperation: GetSessionOperation = new TxMAEvent();
+    let result: Result<void, SessionRetrievalFailed>;
+
+    describe("On every attempt", () => {
+      beforeEach(async () => {
+        mockDynamoDbClient.on(GetItemCommand).resolves({});
+        await sessionRegistry.getSession(mockSessionId, getOperation);
+      });
+
+      it("Logs the attempt", () => {
+        expect(consoleDebugSpy).toHaveBeenCalledWithLogFields({
+          messageCode: "MOBILE_ASYNC_GET_SESSION_ATTEMPT",
+          data: {
+            sessionId: mockSessionId,
+            getItemCommandInput:
+              getOperation.getDynamoDbKeyExpression(mockSessionId),
+          },
+        });
+      });
+    });
+
+    describe("Given there is an unexpected error retrieving the session", () => {
+      beforeEach(async () => {
+        mockDynamoDbClient.on(GetItemCommand).rejects("mock_error");
+        result = await sessionRegistry.getSession(mockSessionId, getOperation);
+      });
+
+      it("Logs the failure", () => {
+        expect(consoleErrorSpy).toHaveBeenCalledWithLogFields({
+          messageCode: "MOBILE_ASYNC_GET_SESSION_UNEXPECTED_FAILURE",
+        });
+      });
+
+      it("Returns failure with server error", () => {
+        expect(result).toEqual(
+          errorResult({
+            errorType: GetSessionError.INTERNAL_SERVER_ERROR,
+          }),
+        );
+      });
+    });
+
+    describe("Given the session was not found", () => {
+      beforeEach(async () => {
+        mockDynamoDbClient.on(GetItemCommand).resolves({});
+        result = await sessionRegistry.getSession(mockSessionId, getOperation);
+      });
+
+      it("Logs the failure", () => {
+        expect(consoleErrorSpy).toHaveBeenCalledWithLogFields({
+          messageCode: "MOBILE_ASYNC_GET_SESSION_SESSION_NOT_FOUND",
+          session: "Session not found",
+        });
+      });
+
+      it("Returns failure with an invalid session error", () => {
+        expect(result).toEqual(
+          errorResult({
+            errorType: GetSessionError.SESSION_NOT_FOUND,
+            session: "Session not found",
+          }),
+        );
+      });
+    });
+
+    describe("Session validation", () => {
+      describe("Given the session is in the wrong state", () => {
+        const invalidBiometricTokenIssuedSessionAttributesWrongSessionState = {
+          ...validBiometricTokenIssuedSessionAttributes,
+          createdAt: 1704106860000, // 2024-01-01 11:01:00.000
+          sessionState: SessionState.AUTH_SESSION_CREATED,
+        };
+        const invalidBiometricTokenIssuedSessionAttributesWrongSessionStateItem =
+          {
+            Item: marshall(
+              invalidBiometricTokenIssuedSessionAttributesWrongSessionState,
+            ),
+          };
+
+        beforeEach(async () => {
+          mockDynamoDbClient
+            .on(GetItemCommand)
+            .resolves(
+              invalidBiometricTokenIssuedSessionAttributesWrongSessionStateItem,
+            );
+          result = await sessionRegistry.getSession(
+            mockSessionId,
+            getOperation,
+          );
+        });
+
+        it("Logs the failure", () => {
+          expect(consoleErrorSpy).toHaveBeenCalledWithLogFields({
+            messageCode: "MOBILE_ASYNC_GET_SESSION_SESSION_NOT_FOUND",
+            errorMessage:
+              "Session could not be parsed or session validation failed",
+            session:
+              invalidBiometricTokenIssuedSessionAttributesWrongSessionState,
+          });
+        });
+
+        it("Returns failure with an invalid session error", () => {
+          expect(result).toStrictEqual(
+            errorResult({
+              errorType: GetSessionError.SESSION_NOT_FOUND,
+              session:
+                invalidBiometricTokenIssuedSessionAttributesWrongSessionState,
+            }),
+          );
+        });
+      });
+
+      describe("Given the session is more than 60 minutes old", () => {
+        const invalidBiometricTokenIssuedSessionAttributesSessionTooOld = {
+          ...validBiometricTokenIssuedSessionAttributes,
+          createdAt: 1704106740000, // 2024-01-01 10:59:00.000
+        };
+        const biometricTokenIssuedSessionAttributesSessionTooOldItem = {
+          Item: marshall(
+            invalidBiometricTokenIssuedSessionAttributesSessionTooOld,
+          ),
+        };
+
+        beforeEach(async () => {
+          mockDynamoDbClient
+            .on(GetItemCommand)
+            .resolves(biometricTokenIssuedSessionAttributesSessionTooOldItem);
+          result = await sessionRegistry.getSession(
+            mockSessionId,
+            getOperation,
+          );
+        });
+
+        it("Logs the failure", () => {
+          expect(consoleErrorSpy).toHaveBeenCalledWithLogFields({
+            messageCode: "MOBILE_ASYNC_GET_SESSION_SESSION_NOT_FOUND",
+            session: invalidBiometricTokenIssuedSessionAttributesSessionTooOld,
+          });
+        });
+
+        it("Returns failure with an invalid session error", () => {
+          expect(result).toStrictEqual(
+            errorResult({
+              errorType: GetSessionError.SESSION_NOT_FOUND,
+              session:
+                invalidBiometricTokenIssuedSessionAttributesSessionTooOld,
+            }),
+          );
+        });
+      });
+    });
+
+    describe("Given the session was found", () => {
+      describe("Given invalid session attributes were returned in response", () => {
+        const invalidBiometricTokenIssuedSessionAttributesMissingAttributes = {
+          clientId: "mockClientId",
+          govukSigninJourneyId: "mockGovukSigninJourneyId",
+          createdAt: 1704106860000, // 2024-01-01 11:01:00.000
+          issuer: "mockIssuer",
+          clientState: "mockClientState",
+          subjectIdentifier: "mockSubjectIdentifier",
+          timeToLive: 12345,
+        };
+        beforeEach(async () => {
+          mockDynamoDbClient.on(GetItemCommand).resolves({
+            Item: marshall(
+              invalidBiometricTokenIssuedSessionAttributesMissingAttributes,
+            ),
+          });
+          result = await sessionRegistry.getSession(
+            mockSessionId,
+            getOperation,
+          );
+        });
+
+        it("Logs the failure", () => {
+          expect(consoleErrorSpy).toHaveBeenCalledWithLogFields({
+            messageCode: "MOBILE_ASYNC_GET_SESSION_SESSION_NOT_FOUND",
+            errorMessage:
+              "Session could not be parsed or session validation failed",
+            session:
+              invalidBiometricTokenIssuedSessionAttributesMissingAttributes,
+          });
+        });
+
+        it("Returns failure with server error", () => {
+          expect(result).toEqual(
+            errorResult({
+              errorType: GetSessionError.SESSION_NOT_FOUND,
path: backend-api/src/functions/adapters/aws/dynamo/dynamoDbAdapter.test.ts
user: login: sandymay-dd
body: suggestion: This should be GetSessionError.INTERNAL_SERVER_ERROR. 

We control what gets written to Dynamo. If fields are missing, it's because of an error we've caused.
created_at: 2025-03-17T21:34:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -293,4 +309,234 @@ describe("DynamoDbAdapter", () => {
       });
     });
   });
+
+  describe("getSession", () => {
+    const getOperation: GetSessionOperation = new TxMAEvent();
+    let result: Result<void, SessionRetrievalFailed>;
+
+    describe("On every attempt", () => {
+      beforeEach(async () => {
+        mockDynamoDbClient.on(GetItemCommand).resolves({});
+        await sessionRegistry.getSession(mockSessionId, getOperation);
+      });
+
+      it("Logs the attempt", () => {
+        expect(consoleDebugSpy).toHaveBeenCalledWithLogFields({
+          messageCode: "MOBILE_ASYNC_GET_SESSION_ATTEMPT",
+          data: {
+            sessionId: mockSessionId,
+            getItemCommandInput:
+              getOperation.getDynamoDbKeyExpression(mockSessionId),
+          },
+        });
+      });
+    });
+
+    describe("Given there is an unexpected error retrieving the session", () => {
+      beforeEach(async () => {
+        mockDynamoDbClient.on(GetItemCommand).rejects("mock_error");
+        result = await sessionRegistry.getSession(mockSessionId, getOperation);
+      });
+
+      it("Logs the failure", () => {
+        expect(consoleErrorSpy).toHaveBeenCalledWithLogFields({
+          messageCode: "MOBILE_ASYNC_GET_SESSION_UNEXPECTED_FAILURE",
+        });
+      });
+
+      it("Returns failure with server error", () => {
+        expect(result).toEqual(
+          errorResult({
+            errorType: GetSessionError.INTERNAL_SERVER_ERROR,
+          }),
+        );
+      });
+    });
+
+    describe("Given the session was not found", () => {
+      beforeEach(async () => {
+        mockDynamoDbClient.on(GetItemCommand).resolves({});
+        result = await sessionRegistry.getSession(mockSessionId, getOperation);
+      });
+
+      it("Logs the failure", () => {
+        expect(consoleErrorSpy).toHaveBeenCalledWithLogFields({
+          messageCode: "MOBILE_ASYNC_GET_SESSION_SESSION_NOT_FOUND",
+          session: "Session not found",
+        });
+      });
+
+      it("Returns failure with an invalid session error", () => {
+        expect(result).toEqual(
+          errorResult({
+            errorType: GetSessionError.SESSION_NOT_FOUND,
+            session: "Session not found",
+          }),
+        );
+      });
+    });
+
+    describe("Session validation", () => {
+      describe("Given the session is in the wrong state", () => {
+        const invalidBiometricTokenIssuedSessionAttributesWrongSessionState = {
+          ...validBiometricTokenIssuedSessionAttributes,
+          createdAt: 1704106860000, // 2024-01-01 11:01:00.000
+          sessionState: SessionState.AUTH_SESSION_CREATED,
+        };
+        const invalidBiometricTokenIssuedSessionAttributesWrongSessionStateItem =
+          {
+            Item: marshall(
+              invalidBiometricTokenIssuedSessionAttributesWrongSessionState,
+            ),
+          };
+
+        beforeEach(async () => {
+          mockDynamoDbClient
+            .on(GetItemCommand)
+            .resolves(
+              invalidBiometricTokenIssuedSessionAttributesWrongSessionStateItem,
+            );
+          result = await sessionRegistry.getSession(
+            mockSessionId,
+            getOperation,
+          );
+        });
+
+        it("Logs the failure", () => {
+          expect(consoleErrorSpy).toHaveBeenCalledWithLogFields({
+            messageCode: "MOBILE_ASYNC_GET_SESSION_SESSION_NOT_FOUND",
+            errorMessage:
+              "Session could not be parsed or session validation failed",
+            session:
+              invalidBiometricTokenIssuedSessionAttributesWrongSessionState,
+          });
+        });
+
+        it("Returns failure with an invalid session error", () => {
+          expect(result).toStrictEqual(
+            errorResult({
+              errorType: GetSessionError.SESSION_NOT_FOUND,
+              session:
+                invalidBiometricTokenIssuedSessionAttributesWrongSessionState,
+            }),
+          );
+        });
+      });
+
+      describe("Given the session is more than 60 minutes old", () => {
+        const invalidBiometricTokenIssuedSessionAttributesSessionTooOld = {
+          ...validBiometricTokenIssuedSessionAttributes,
+          createdAt: 1704106740000, // 2024-01-01 10:59:00.000
+        };
+        const biometricTokenIssuedSessionAttributesSessionTooOldItem = {
+          Item: marshall(
+            invalidBiometricTokenIssuedSessionAttributesSessionTooOld,
+          ),
+        };
+
+        beforeEach(async () => {
+          mockDynamoDbClient
+            .on(GetItemCommand)
+            .resolves(biometricTokenIssuedSessionAttributesSessionTooOldItem);
+          result = await sessionRegistry.getSession(
+            mockSessionId,
+            getOperation,
+          );
+        });
+
+        it("Logs the failure", () => {
+          expect(consoleErrorSpy).toHaveBeenCalledWithLogFields({
+            messageCode: "MOBILE_ASYNC_GET_SESSION_SESSION_NOT_FOUND",
+            session: invalidBiometricTokenIssuedSessionAttributesSessionTooOld,
+          });
+        });
+
+        it("Returns failure with an invalid session error", () => {
+          expect(result).toStrictEqual(
+            errorResult({
+              errorType: GetSessionError.SESSION_NOT_FOUND,
+              session:
+                invalidBiometricTokenIssuedSessionAttributesSessionTooOld,
+            }),
+          );
+        });
+      });
+    });
+
+    describe("Given the session was found", () => {
+      describe("Given invalid session attributes were returned in response", () => {
+        const invalidBiometricTokenIssuedSessionAttributesMissingAttributes = {
+          clientId: "mockClientId",
+          govukSigninJourneyId: "mockGovukSigninJourneyId",
+          createdAt: 1704106860000, // 2024-01-01 11:01:00.000
+          issuer: "mockIssuer",
+          clientState: "mockClientState",
+          subjectIdentifier: "mockSubjectIdentifier",
+          timeToLive: 12345,
+        };
+        beforeEach(async () => {
+          mockDynamoDbClient.on(GetItemCommand).resolves({
+            Item: marshall(
+              invalidBiometricTokenIssuedSessionAttributesMissingAttributes,
+            ),
+          });
+          result = await sessionRegistry.getSession(
+            mockSessionId,
+            getOperation,
+          );
+        });
+
+        it("Logs the failure", () => {
+          expect(consoleErrorSpy).toHaveBeenCalledWithLogFields({
+            messageCode: "MOBILE_ASYNC_GET_SESSION_SESSION_NOT_FOUND",
+            errorMessage:
+              "Session could not be parsed or session validation failed",
+            session:
+              invalidBiometricTokenIssuedSessionAttributesMissingAttributes,
+          });
+        });
+
+        it("Returns failure with server error", () => {
+          expect(result).toEqual(
+            errorResult({
+              errorType: GetSessionError.SESSION_NOT_FOUND,
+              session:
+                invalidBiometricTokenIssuedSessionAttributesMissingAttributes,
+            }),
+          );
+        });
+      });
+
+      describe("Given valid session attributes were returned in response", () => {
+        const validBiometricTokenIssuedSessionAttributesItem = marshall({
+          ...validBiometricTokenIssuedSessionAttributes,
+          createdAt: 1704106860000, // 2024-01-01 11:01:00.000
path: backend-api/src/functions/adapters/aws/dynamo/dynamoDbAdapter.test.ts
user: login: sandymay-dd
body: suggestion: as above, we shouldn't need to override this. 
created_at: 2025-03-17T21:34:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -131,6 +141,55 @@ export class DynamoDbAdapter implements SessionRegistry {
     }
   }
 
+  async getSession(
+    sessionId: string,
+    getOperation: GetSessionOperation,
+  ): Promise<Result<SessionAttributes, SessionRetrievalFailed>> {
+    let response;
+    try {
+      const getItemCommandInput =
+        getOperation.getDynamoDbKeyExpression(sessionId);
+      logger.debug(LogMessage.GET_SESSION_ATTEMPT, {
+        data: { sessionId, getItemCommandInput },
+      });
+
+      response = await this.dynamoDbClient.send(
+        new GetItemCommand({
+          TableName: this.tableName,
+          ...getItemCommandInput,
+        }),
+      );
+    } catch (error: unknown) {
+      return this.handleGetSessionInternalServerError(error);
+    }
+
+    const responseItem = response.Item;
+    if (responseItem == null) {
+      logger.error(LogMessage.GET_SESSION_SESSION_NOT_FOUND, {
+        session: "Session not found",
+      });
+
+      return errorResult({
+        errorType: GetSessionError.SESSION_NOT_FOUND,
+        session: "Session not found",
+      });
+    }
path: backend-api/src/functions/adapters/aws/dynamo/dynamoDbAdapter.ts
user: login: sandymay-dd
body: suggestion: this could be moved into its own function `handle...()`
created_at: 2025-03-17T21:38:19Z
author_association: CONTRIBUTOR
diff_hunk: @@ -235,4 +294,34 @@ export class DynamoDbAdapter implements SessionRegistry {
       errorType: UpdateSessionError.INTERNAL_SERVER_ERROR,
     });
   }
+
+  private handleGetSessionInternalServerError(
+    error: unknown,
+  ): FailureWithValue<SessionRetrievalFailedInternalServerError> {
+    logger.error(LogMessage.GET_SESSION_UNEXPECTED_FAILURE, {
+      error,
+    });
+    return errorResult({
+      errorType: GetSessionError.INTERNAL_SERVER_ERROR,
+    });
+  }
+
+  private handleGetSessionNotFoundError({
path: backend-api/src/functions/adapters/aws/dynamo/dynamoDbAdapter.ts
user: login: sandymay-dd
body: issue: this is currently called when the session _could_ be found, but when the session is invalid. I'd suggest we look to have two functions split out from this:

1) handleGetSessionNotFoundError: This is only used when the `session` record doesn't exist. We would then need to remove the `session` parameter from the log message.

2) handleGetSessionSessionInvalidError (or something similarly named to align with existing conventions): This is used for when the session is not valid. 






created_at: 2025-03-17T21:46:28Z
author_association: CONTRIBUTOR
diff_hunk: @@ -37,6 +36,14 @@ export const isBaseSessionAttributes = (
   return true;
 };
 
+export const isBaseSessionAttributes = (
+  item: Record<string, NativeAttributeValue>,
+): item is BaseSessionAttributes => {
+  if (!isCommonSessionAttributes(item)) return false;
+  if (typeof item.sessionState !== "string") return false;
+  return true;
path: backend-api/src/functions/common/session/sessionAttributes/sessionAttributes.ts
user: login: sandymay-dd
body: question: what was the intent behind this change? I'm struggling to see why sessionState isn't a base attribute given it's present in every session record. 
created_at: 2025-03-17T21:53:07Z
author_association: CONTRIBUTOR
path: backend-api/src/functions/common/session/sessionAttributes/sessionAttributes.ts
user: login: sandymay-dd
body: suggestion: please may we sanity check whether each function needs to be exported - looking at main we export `isBaseSessionAttributes` when we shouldn't.
created_at: 2025-03-17T21:57:43Z
author_association: CONTRIBUTOR
path: backend-api/src/functions/common/session/sessionAttributes/sessionAttributes.ts
user: login: sandymay-dd
body: suggestion: please may you look at updating the unit tests in sessionAttributes.test.ts for these changes?
created_at: 2025-03-17T22:02:27Z
author_association: CONTRIBUTOR
diff_hunk: @@ -293,4 +309,234 @@ describe("DynamoDbAdapter", () => {
       });
     });
   });
+
+  describe("getSession", () => {
+    const getOperation: GetSessionOperation = new TxMAEvent();
+    let result: Result<void, SessionRetrievalFailed>;
+
+    describe("On every attempt", () => {
+      beforeEach(async () => {
+        mockDynamoDbClient.on(GetItemCommand).resolves({});
+        await sessionRegistry.getSession(mockSessionId, getOperation);
+      });
+
+      it("Logs the attempt", () => {
+        expect(consoleDebugSpy).toHaveBeenCalledWithLogFields({
+          messageCode: "MOBILE_ASYNC_GET_SESSION_ATTEMPT",
+          data: {
+            sessionId: mockSessionId,
+            getItemCommandInput:
+              getOperation.getDynamoDbKeyExpression(mockSessionId),
+          },
+        });
+      });
+    });
+
+    describe("Given there is an unexpected error retrieving the session", () => {
+      beforeEach(async () => {
+        mockDynamoDbClient.on(GetItemCommand).rejects("mock_error");
+        result = await sessionRegistry.getSession(mockSessionId, getOperation);
+      });
+
+      it("Logs the failure", () => {
+        expect(consoleErrorSpy).toHaveBeenCalledWithLogFields({
+          messageCode: "MOBILE_ASYNC_GET_SESSION_UNEXPECTED_FAILURE",
+        });
+      });
+
+      it("Returns failure with server error", () => {
+        expect(result).toEqual(
+          errorResult({
+            errorType: GetSessionError.INTERNAL_SERVER_ERROR,
+          }),
+        );
+      });
+    });
+
+    describe("Given the session was not found", () => {
+      beforeEach(async () => {
+        mockDynamoDbClient.on(GetItemCommand).resolves({});
+        result = await sessionRegistry.getSession(mockSessionId, getOperation);
+      });
+
+      it("Logs the failure", () => {
+        expect(consoleErrorSpy).toHaveBeenCalledWithLogFields({
+          messageCode: "MOBILE_ASYNC_GET_SESSION_SESSION_NOT_FOUND",
+          session: "Session not found",
+        });
+      });
+
+      it("Returns failure with an invalid session error", () => {
+        expect(result).toEqual(
+          errorResult({
+            errorType: GetSessionError.SESSION_NOT_FOUND,
+            session: "Session not found",
+          }),
+        );
+      });
+    });
+
+    describe("Session validation", () => {
+      describe("Given the session is in the wrong state", () => {
path: backend-api/src/functions/adapters/aws/dynamo/dynamoDbAdapter.test.ts
user: login: sandymay-dd
body: prompt for tomorrow: how should we write unit tests for the UpdateSession function validation. Should we be testing every implementation of the UpdateSession operation within this adapter? 
created_at: 2025-03-17T22:16:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -293,4 +309,234 @@ describe("DynamoDbAdapter", () => {
       });
     });
   });
+
+  describe("getSession", () => {
+    const getOperation: GetSessionOperation = new TxMAEvent();
+    let result: Result<void, SessionRetrievalFailed>;
path: backend-api/src/functions/adapters/aws/dynamo/dynamoDbAdapter.test.ts
user: login: jhumbert-dd
body: Yeah, I believe `void` translates to a success response without a value (`emptySuccess()`)
created_at: 2025-03-18T09:18:12Z
author_association: CONTRIBUTOR
diff_hunk: @@ -293,4 +309,234 @@ describe("DynamoDbAdapter", () => {
       });
     });
   });
+
+  describe("getSession", () => {
+    const getOperation: GetSessionOperation = new TxMAEvent();
+    let result: Result<void, SessionRetrievalFailed>;
+
+    describe("On every attempt", () => {
+      beforeEach(async () => {
+        mockDynamoDbClient.on(GetItemCommand).resolves({});
+        await sessionRegistry.getSession(mockSessionId, getOperation);
+      });
+
+      it("Logs the attempt", () => {
+        expect(consoleDebugSpy).toHaveBeenCalledWithLogFields({
+          messageCode: "MOBILE_ASYNC_GET_SESSION_ATTEMPT",
+          data: {
+            sessionId: mockSessionId,
+            getItemCommandInput:
+              getOperation.getDynamoDbKeyExpression(mockSessionId),
+          },
+        });
+      });
+    });
+
+    describe("Given there is an unexpected error retrieving the session", () => {
+      beforeEach(async () => {
+        mockDynamoDbClient.on(GetItemCommand).rejects("mock_error");
+        result = await sessionRegistry.getSession(mockSessionId, getOperation);
+      });
+
+      it("Logs the failure", () => {
+        expect(consoleErrorSpy).toHaveBeenCalledWithLogFields({
+          messageCode: "MOBILE_ASYNC_GET_SESSION_UNEXPECTED_FAILURE",
+        });
+      });
+
+      it("Returns failure with server error", () => {
+        expect(result).toEqual(
+          errorResult({
+            errorType: GetSessionError.INTERNAL_SERVER_ERROR,
+          }),
+        );
+      });
+    });
+
+    describe("Given the session was not found", () => {
+      beforeEach(async () => {
+        mockDynamoDbClient.on(GetItemCommand).resolves({});
+        result = await sessionRegistry.getSession(mockSessionId, getOperation);
+      });
+
+      it("Logs the failure", () => {
+        expect(consoleErrorSpy).toHaveBeenCalledWithLogFields({
+          messageCode: "MOBILE_ASYNC_GET_SESSION_SESSION_NOT_FOUND",
+          session: "Session not found",
+        });
+      });
+
+      it("Returns failure with an invalid session error", () => {
+        expect(result).toEqual(
+          errorResult({
+            errorType: GetSessionError.SESSION_NOT_FOUND,
+            session: "Session not found",
+          }),
+        );
+      });
+    });
+
+    describe("Session validation", () => {
+      describe("Given the session is in the wrong state", () => {
+        const invalidBiometricTokenIssuedSessionAttributesWrongSessionState = {
+          ...validBiometricTokenIssuedSessionAttributes,
+          createdAt: 1704106860000, // 2024-01-01 11:01:00.000
path: backend-api/src/functions/adapters/aws/dynamo/dynamoDbAdapter.test.ts
user: login: jhumbert-dd
body: no reason - I'll remove this
created_at: 2025-03-18T09:21:35Z
author_association: CONTRIBUTOR
diff_hunk: @@ -37,6 +36,14 @@ export const isBaseSessionAttributes = (
   return true;
 };
 
+export const isBaseSessionAttributes = (
+  item: Record<string, NativeAttributeValue>,
+): item is BaseSessionAttributes => {
+  if (!isCommonSessionAttributes(item)) return false;
+  if (typeof item.sessionState !== "string") return false;
+  return true;
path: backend-api/src/functions/common/session/sessionAttributes/sessionAttributes.ts
user: login: jhumbert-dd
body: This was part of my working out of how type guard functions work and is leftover mistake. I've updated to include sessionState in the base attributes
created_at: 2025-03-18T11:14:48Z
author_association: CONTRIBUTOR
path: backend-api/src/functions/common/session/sessionAttributes/sessionAttributes.test.ts
user: login: jhumbert-dd
body: My reason was because some validation was already being done here before this work. Validation has now been moved to another layer.
created_at: 2025-03-19T15:52:21Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,29 +1,37 @@
-import { expect } from "@jest/globals";
-import "dotenv/config";
-import "../../../../../tests/testUtils/matchers";
-import { DynamoDbAdapter } from "./dynamoDbAdapter";
-import { BiometricTokenIssued } from "../../../common/session/updateOperations/BiometricTokenIssued/BiometricTokenIssued";
-import {
-  SessionRegistry,
-  UpdateSessionError,
-  SessionUpdateFailed,
-  SessionUpdated,
-} from "../../../common/session/SessionRegistry";
 import {
   ConditionalCheckFailedException,
   DynamoDBClient,
+  GetItemCommand,
   ReturnValue,
   ReturnValuesOnConditionCheckFailure,
   UpdateItemCommand,
 } from "@aws-sdk/client-dynamodb";
+import { marshall, unmarshall } from "@aws-sdk/util-dynamodb";
+import { expect } from "@jest/globals";
 import { mockClient } from "aws-sdk-client-mock";
-import { errorResult, Result, successResult } from "../../../utils/result";
+import "dotenv/config";
path: backend-api/src/functions/adapters/aws/dynamo/dynamoDbAdapter.test.ts
user: login: jhumbert-dd
body: removed
created_at: 2025-03-19T16:01:47Z
author_association: CONTRIBUTOR
diff_hunk: @@ -293,4 +309,234 @@ describe("DynamoDbAdapter", () => {
       });
     });
   });
+
+  describe("getSession", () => {
+    const getOperation: GetSessionOperation = new TxMAEvent();
+    let result: Result<void, SessionRetrievalFailed>;
+
+    describe("On every attempt", () => {
+      beforeEach(async () => {
+        mockDynamoDbClient.on(GetItemCommand).resolves({});
+        await sessionRegistry.getSession(mockSessionId, getOperation);
+      });
+
+      it("Logs the attempt", () => {
+        expect(consoleDebugSpy).toHaveBeenCalledWithLogFields({
+          messageCode: "MOBILE_ASYNC_GET_SESSION_ATTEMPT",
+          data: {
+            sessionId: mockSessionId,
+            getItemCommandInput:
+              getOperation.getDynamoDbKeyExpression(mockSessionId),
+          },
+        });
+      });
+    });
+
+    describe("Given there is an unexpected error retrieving the session", () => {
+      beforeEach(async () => {
+        mockDynamoDbClient.on(GetItemCommand).rejects("mock_error");
+        result = await sessionRegistry.getSession(mockSessionId, getOperation);
+      });
+
+      it("Logs the failure", () => {
+        expect(consoleErrorSpy).toHaveBeenCalledWithLogFields({
+          messageCode: "MOBILE_ASYNC_GET_SESSION_UNEXPECTED_FAILURE",
+        });
+      });
+
+      it("Returns failure with server error", () => {
+        expect(result).toEqual(
+          errorResult({
+            errorType: GetSessionError.INTERNAL_SERVER_ERROR,
+          }),
+        );
+      });
+    });
+
+    describe("Given the session was not found", () => {
+      beforeEach(async () => {
+        mockDynamoDbClient.on(GetItemCommand).resolves({});
+        result = await sessionRegistry.getSession(mockSessionId, getOperation);
+      });
+
+      it("Logs the failure", () => {
+        expect(consoleErrorSpy).toHaveBeenCalledWithLogFields({
+          messageCode: "MOBILE_ASYNC_GET_SESSION_SESSION_NOT_FOUND",
+          session: "Session not found",
+        });
+      });
+
+      it("Returns failure with an invalid session error", () => {
+        expect(result).toEqual(
+          errorResult({
+            errorType: GetSessionError.SESSION_NOT_FOUND,
+            session: "Session not found",
+          }),
+        );
+      });
+    });
+
+    describe("Session validation", () => {
+      describe("Given the session is in the wrong state", () => {
+        const invalidBiometricTokenIssuedSessionAttributesWrongSessionState = {
+          ...validBiometricTokenIssuedSessionAttributes,
+          createdAt: 1704106860000, // 2024-01-01 11:01:00.000
path: backend-api/src/functions/adapters/aws/dynamo/dynamoDbAdapter.test.ts
user: login: jhumbert-dd
body: removed
created_at: 2025-03-19T16:44:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -131,6 +141,55 @@ export class DynamoDbAdapter implements SessionRegistry {
     }
   }
 
+  async getSession(
+    sessionId: string,
+    getOperation: GetSessionOperation,
+  ): Promise<Result<SessionAttributes, SessionRetrievalFailed>> {
+    let response;
+    try {
+      const getItemCommandInput =
+        getOperation.getDynamoDbKeyExpression(sessionId);
+      logger.debug(LogMessage.GET_SESSION_ATTEMPT, {
+        data: { sessionId, getItemCommandInput },
+      });
+
+      response = await this.dynamoDbClient.send(
+        new GetItemCommand({
+          TableName: this.tableName,
+          ...getItemCommandInput,
+        }),
+      );
+    } catch (error: unknown) {
+      return this.handleGetSessionInternalServerError(error);
+    }
+
+    const responseItem = response.Item;
+    if (responseItem == null) {
+      logger.error(LogMessage.GET_SESSION_SESSION_NOT_FOUND, {
+        session: "Session not found",
+      });
+
+      return errorResult({
+        errorType: GetSessionError.SESSION_NOT_FOUND,
+        session: "Session not found",
+      });
+    }
path: backend-api/src/functions/adapters/aws/dynamo/dynamoDbAdapter.ts
user: login: jhumbert-dd
body: updated
created_at: 2025-03-19T16:45:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -235,4 +294,34 @@ export class DynamoDbAdapter implements SessionRegistry {
       errorType: UpdateSessionError.INTERNAL_SERVER_ERROR,
     });
   }
+
+  private handleGetSessionInternalServerError(
+    error: unknown,
+  ): FailureWithValue<SessionRetrievalFailedInternalServerError> {
+    logger.error(LogMessage.GET_SESSION_UNEXPECTED_FAILURE, {
+      error,
+    });
+    return errorResult({
+      errorType: GetSessionError.INTERNAL_SERVER_ERROR,
+    });
+  }
+
+  private handleGetSessionNotFoundError({
path: backend-api/src/functions/adapters/aws/dynamo/dynamoDbAdapter.ts
user: login: jhumbert-dd
body: updated
created_at: 2025-03-19T16:45:41Z
author_association: CONTRIBUTOR
path: backend-api/src/functions/common/session/sessionAttributes/sessionAttributes.ts
user: login: jhumbert-dd
body: updated
created_at: 2025-03-19T16:51:27Z
author_association: CONTRIBUTOR
path: backend-api/src/functions/common/session/sessionAttributes/sessionAttributes.ts
user: login: jhumbert-dd
body: removed unused export declarations
created_at: 2025-03-19T17:02:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -293,4 +309,234 @@ describe("DynamoDbAdapter", () => {
       });
     });
   });
+
+  describe("getSession", () => {
+    const getOperation: GetSessionOperation = new TxMAEvent();
path: backend-api/src/functions/adapters/aws/dynamo/dynamoDbAdapter.test.ts
user: login: jhumbert-dd
body: Updated
created_at: 2025-03-19T20:40:55Z
author_association: CONTRIBUTOR
diff_hunk: @@ -293,4 +309,234 @@ describe("DynamoDbAdapter", () => {
       });
     });
   });
+
+  describe("getSession", () => {
+    const getOperation: GetSessionOperation = new TxMAEvent();
+    let result: Result<void, SessionRetrievalFailed>;
+
+    describe("On every attempt", () => {
+      beforeEach(async () => {
+        mockDynamoDbClient.on(GetItemCommand).resolves({});
+        await sessionRegistry.getSession(mockSessionId, getOperation);
+      });
+
+      it("Logs the attempt", () => {
+        expect(consoleDebugSpy).toHaveBeenCalledWithLogFields({
+          messageCode: "MOBILE_ASYNC_GET_SESSION_ATTEMPT",
+          data: {
+            sessionId: mockSessionId,
+            getItemCommandInput: {
+              Key: getOperation.getDynamoDbKeyExpression(mockSessionId),
+            },
+          },
+        });
+      });
+    });
+
+    describe("Given there is an unexpected error retrieving the session", () => {
+      beforeEach(async () => {
+        mockDynamoDbClient.on(GetItemCommand).rejects("mock_error");
+        result = await sessionRegistry.getSession(mockSessionId, getOperation);
+      });
+
+      it("Logs the failure", () => {
+        expect(consoleErrorSpy).toHaveBeenCalledWithLogFields({
+          messageCode: "MOBILE_ASYNC_GET_SESSION_UNEXPECTED_FAILURE",
path: backend-api/src/functions/adapters/aws/dynamo/dynamoDbAdapter.test.ts
user: login: jhumbert-dd
body: I wasn't able to test within the `toHaveBeenCalledWithLogFields` assertion so added separate ones below. It would be great to know if there's a better way to do this
created_at: 2025-03-19T20:49:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -293,4 +309,234 @@ describe("DynamoDbAdapter", () => {
       });
     });
   });
+
+  describe("getSession", () => {
+    const getOperation: GetSessionOperation = new TxMAEvent();
+    let result: Result<void, SessionRetrievalFailed>;
+
+    describe("On every attempt", () => {
+      beforeEach(async () => {
+        mockDynamoDbClient.on(GetItemCommand).resolves({});
+        await sessionRegistry.getSession(mockSessionId, getOperation);
+      });
+
+      it("Logs the attempt", () => {
+        expect(consoleDebugSpy).toHaveBeenCalledWithLogFields({
+          messageCode: "MOBILE_ASYNC_GET_SESSION_ATTEMPT",
+          data: {
+            sessionId: mockSessionId,
+            getItemCommandInput:
+              getOperation.getDynamoDbKeyExpression(mockSessionId),
+          },
+        });
+      });
+    });
+
+    describe("Given there is an unexpected error retrieving the session", () => {
+      beforeEach(async () => {
+        mockDynamoDbClient.on(GetItemCommand).rejects("mock_error");
+        result = await sessionRegistry.getSession(mockSessionId, getOperation);
+      });
+
+      it("Logs the failure", () => {
+        expect(consoleErrorSpy).toHaveBeenCalledWithLogFields({
+          messageCode: "MOBILE_ASYNC_GET_SESSION_UNEXPECTED_FAILURE",
+        });
+      });
+
+      it("Returns failure with server error", () => {
+        expect(result).toEqual(
+          errorResult({
+            errorType: GetSessionError.INTERNAL_SERVER_ERROR,
+          }),
+        );
+      });
+    });
+
+    describe("Given the session was not found", () => {
+      beforeEach(async () => {
+        mockDynamoDbClient.on(GetItemCommand).resolves({});
+        result = await sessionRegistry.getSession(mockSessionId, getOperation);
+      });
+
+      it("Logs the failure", () => {
+        expect(consoleErrorSpy).toHaveBeenCalledWithLogFields({
+          messageCode: "MOBILE_ASYNC_GET_SESSION_SESSION_NOT_FOUND",
+          session: "Session not found",
+        });
+      });
+
+      it("Returns failure with an invalid session error", () => {
+        expect(result).toEqual(
+          errorResult({
+            errorType: GetSessionError.SESSION_NOT_FOUND,
+            session: "Session not found",
+          }),
+        );
+      });
+    });
+
+    describe("Session validation", () => {
+      describe("Given the session is in the wrong state", () => {
path: backend-api/src/functions/adapters/aws/dynamo/dynamoDbAdapter.test.ts
user: login: jhumbert-dd
body: I think one test for attribute type validation and one test for session should be enough. I'll remove one of the session validation tests from each method
created_at: 2025-03-20T09:21:47Z
author_association: CONTRIBUTOR
diff_hunk: @@ -293,4 +309,234 @@ describe("DynamoDbAdapter", () => {
       });
     });
   });
+
+  describe("getSession", () => {
+    const getOperation: GetSessionOperation = new TxMAEvent();
+    let result: Result<void, SessionRetrievalFailed>;
+
+    describe("On every attempt", () => {
+      beforeEach(async () => {
+        mockDynamoDbClient.on(GetItemCommand).resolves({});
+        await sessionRegistry.getSession(mockSessionId, getOperation);
+      });
+
+      it("Logs the attempt", () => {
+        expect(consoleDebugSpy).toHaveBeenCalledWithLogFields({
+          messageCode: "MOBILE_ASYNC_GET_SESSION_ATTEMPT",
+          data: {
+            sessionId: mockSessionId,
+            getItemCommandInput:
+              getOperation.getDynamoDbKeyExpression(mockSessionId),
+          },
+        });
+      });
+    });
+
+    describe("Given there is an unexpected error retrieving the session", () => {
+      beforeEach(async () => {
+        mockDynamoDbClient.on(GetItemCommand).rejects("mock_error");
+        result = await sessionRegistry.getSession(mockSessionId, getOperation);
+      });
+
+      it("Logs the failure", () => {
+        expect(consoleErrorSpy).toHaveBeenCalledWithLogFields({
+          messageCode: "MOBILE_ASYNC_GET_SESSION_UNEXPECTED_FAILURE",
+        });
+      });
+
+      it("Returns failure with server error", () => {
+        expect(result).toEqual(
+          errorResult({
+            errorType: GetSessionError.INTERNAL_SERVER_ERROR,
+          }),
+        );
+      });
+    });
+
+    describe("Given the session was not found", () => {
+      beforeEach(async () => {
+        mockDynamoDbClient.on(GetItemCommand).resolves({});
+        result = await sessionRegistry.getSession(mockSessionId, getOperation);
+      });
+
+      it("Logs the failure", () => {
+        expect(consoleErrorSpy).toHaveBeenCalledWithLogFields({
+          messageCode: "MOBILE_ASYNC_GET_SESSION_SESSION_NOT_FOUND",
+          session: "Session not found",
+        });
+      });
+
+      it("Returns failure with an invalid session error", () => {
+        expect(result).toEqual(
+          errorResult({
+            errorType: GetSessionError.SESSION_NOT_FOUND,
+            session: "Session not found",
+          }),
+        );
+      });
+    });
+
+    describe("Session validation", () => {
+      describe("Given the session is in the wrong state", () => {
path: backend-api/src/functions/adapters/aws/dynamo/dynamoDbAdapter.test.ts
user: login: jhumbert-dd
body: updated
created_at: 2025-03-20T09:51:12Z
author_association: CONTRIBUTOR
diff_hunk: @@ -93,3 +93,22 @@ Resources:
                   - ec2:CreateNetworkInterface
                   - ec2:DeleteNetworkInterface
                 Resource: '*'
+        - PolicyName: AsyncTxmaEventFunctionDynamodbPolicy
+          PolicyDocument:
+            Statement:
+              - Effect: Allow
+                Action:
+                  - "dynamodb:GetItem"
+                Resource: !GetAtt SessionsTable.Arn
+        - PolicyName: AsyncTxmaEventFunctionTxmaSQSPolicy
+          PolicyDocument:
+            Version: "2012-10-17"
+            Statement:
+              - Effect: Allow
+                Action:
+                  - sqs:SendMessage
+                Resource: !GetAtt TxMASQSQueue.Arn
+              - Effect: Allow
+                Action:
+                  - kms:GenerateDataKey
path: backend-api/infra/functions/txmaEvent.yaml
user: login: chris-cooksley-gds
body: I think our other lambdas have also required `kms:Decrypt` on the key in order to write to SQS. Can we double-check whether the TxMA write definitely works without that (and, if not, add it in)?

I feel like the API tests wouldn't be passing if this didn't work, but I'm confused as to why it wouldn't be needed here 🤷 
created_at: 2025-03-20T10:41:33Z
author_association: CONTRIBUTOR
diff_hunk: @@ -58,3 +43,68 @@ export const validBiometricSessionFinishedAttributes = {
   sessionState: SessionState.BIOMETRIC_SESSION_FINISHED,
   biometricSessionId: mockBiometricSessionId,
 };
+
+export const validBiometricTokenIssuedSessionAttributesMobileApp = {
+  ...validBiometricSessionFinishedAttributes,
+  redirectUri: "https://www.mockRedirectUri.com",
+};
+
+export const mockInertSessionRegistry: SessionRegistry = {
+  updateSession: jest.fn(() => {
+    throw new Error("Not implemented");
+  }),
+
+  getSession: jest.fn(() => {
+    throw new Error("Not implemented");
+  }),
+};
+
+export const mockSuccessfulSessionRegistry: SessionRegistry = {
path: backend-api/src/functions/testUtils/unitTestData.ts
user: login: chris-cooksley-gds
body: Thoughts: it feels a bit odd that our generic successful SessionRegistry should be returning biometric token issued session attributes specifically. And I wonder if this exposes a bit of an underlying issue with having a generic successful SessionRegistry in the first place - since different Lambdas will be expecting different session attributes back from the session registry, depending on where we are in the state machine. I wonder if a bit of repetition will be cleaner than the abstraction here - i.e. reverting to each unit test suite having its own successful session registry (based on the inert one here), returning the appropriate session attributes for that Lambda. Happy to discuss.
created_at: 2025-03-20T11:06:22Z
author_association: CONTRIBUTOR
diff_hunk: @@ -32,13 +50,184 @@ export async function lambdaHandlerConstructor(
     });
     return badRequestResponse("invalid_request", errorMessage);
   }
+  const { sessionId } = validateRequestBodyResult.value;
+  const eventService = dependencies.getEventService(config.TXMA_SQS);
 
-  logger.info(LogMessage.TXMA_EVENT_COMPLETED);
+  const sessionRegistry = dependencies.getSessionRegistry(
+    config.SESSION_TABLE_NAME,
+  );
+  const getSessionResult = await sessionRegistry.getSession(
+    sessionId,
+    new TxMAEventGetSessionOperation(),
+  );
 
+  const { ipAddress, txmaAuditEncoded } = getAuditData(event);
+  const eventData = {
+    sessionId,
+    issuer: config.ISSUER,
+    ipAddress,
+    txmaAuditEncoded,
+  };
+  if (getSessionResult.isError) {
+    return handleGetSessionError({
+      eventService,
+      errorData: getSessionResult.value,
+      eventData,
+    });
+  }
+
+  logger.info(LogMessage.TXMA_EVENT_COMPLETED);
   return notImplementedResponse;
 }
 
 export const lambdaHandler = lambdaHandlerConstructor.bind(
   null,
   runtimeDependencies,
 );
+
+interface BaseEventData {
+  sessionId: string;
+  issuer: string;
+  ipAddress: string;
+  txmaAuditEncoded: string | undefined;
+}
+
+async function handleGetSessionError({
+  eventService,
+  errorData,
+  eventData,
+}: {
+  eventService: IEventService;
+  errorData: GetSessionFailed;
+  eventData: BaseEventData;
+}): Promise<APIGatewayProxyResult> {
+  switch (errorData.errorType) {
+    case GetSessionError.INTERNAL_SERVER_ERROR:
+      return handleInternalServerError({ eventService, eventData });
+    case GetSessionError.SESSION_NOT_FOUND:
+      return handleSessionNotFound({
+        eventService,
+        eventData,
+      });
+    case GetSessionError.SESSION_INVALID:
+      return handleSessionInvalid({
+        eventService,
+        eventData,
+        sessionData: errorData.data,
+      });
+  }
+}
+
+async function handleInternalServerError({
+  eventService,
+  eventData,
+}: {
+  eventService: IEventService;
+  eventData: BaseEventData;
+}): Promise<APIGatewayProxyResult> {
+  const writeEventResult = await writeEvent({
+    eventService,
+    eventName: "DCMAW_ASYNC_CRI_5XXERROR",
+    eventData,
+  });
+
+  if (writeEventResult.isError) {
+    logger.error(LogMessage.ERROR_WRITING_AUDIT_EVENT, {
+      data: {
+        auditEventName: "DCMAW_ASYNC_CRI_5XXERROR",
+      },
+    });
+  }
+  return serverErrorResponse;
+}
+
+async function handleSessionNotFound({
+  eventService,
+  eventData,
+}: {
+  eventService: IEventService;
+  eventData: BaseEventData;
+}): Promise<APIGatewayProxyResult> {
+  const writeEventResult = await writeEvent({
+    eventService,
+    eventName: "DCMAW_ASYNC_CRI_4XXERROR",
+    eventData,
+  });
+
+  const auditEventName = "DCMAW_ASYNC_CRI_4XXERROR";
path: backend-api/src/functions/asyncTxmaEvent/asyncTxmaEventHandler.ts
user: login: chris-cooksley-gds
body: Nitpick: if we're const-ing this anyway, might as well do it sooner and use it for `eventName` above
created_at: 2025-03-20T11:16:45Z
author_association: CONTRIBUTOR
diff_hunk: @@ -32,13 +50,184 @@ export async function lambdaHandlerConstructor(
     });
     return badRequestResponse("invalid_request", errorMessage);
   }
+  const { sessionId } = validateRequestBodyResult.value;
+  const eventService = dependencies.getEventService(config.TXMA_SQS);
 
-  logger.info(LogMessage.TXMA_EVENT_COMPLETED);
+  const sessionRegistry = dependencies.getSessionRegistry(
+    config.SESSION_TABLE_NAME,
+  );
+  const getSessionResult = await sessionRegistry.getSession(
+    sessionId,
+    new TxMAEventGetSessionOperation(),
+  );
 
+  const { ipAddress, txmaAuditEncoded } = getAuditData(event);
+  const eventData = {
+    sessionId,
+    issuer: config.ISSUER,
+    ipAddress,
+    txmaAuditEncoded,
+  };
+  if (getSessionResult.isError) {
+    return handleGetSessionError({
+      eventService,
+      errorData: getSessionResult.value,
+      eventData,
+    });
+  }
+
+  logger.info(LogMessage.TXMA_EVENT_COMPLETED);
   return notImplementedResponse;
 }
 
 export const lambdaHandler = lambdaHandlerConstructor.bind(
   null,
   runtimeDependencies,
 );
+
+interface BaseEventData {
+  sessionId: string;
+  issuer: string;
+  ipAddress: string;
+  txmaAuditEncoded: string | undefined;
+}
+
+async function handleGetSessionError({
+  eventService,
+  errorData,
+  eventData,
+}: {
+  eventService: IEventService;
+  errorData: GetSessionFailed;
+  eventData: BaseEventData;
+}): Promise<APIGatewayProxyResult> {
+  switch (errorData.errorType) {
+    case GetSessionError.INTERNAL_SERVER_ERROR:
+      return handleInternalServerError({ eventService, eventData });
+    case GetSessionError.SESSION_NOT_FOUND:
+      return handleSessionNotFound({
+        eventService,
+        eventData,
+      });
+    case GetSessionError.SESSION_INVALID:
+      return handleSessionInvalid({
+        eventService,
+        eventData,
+        sessionData: errorData.data,
+      });
+  }
+}
+
+async function handleInternalServerError({
+  eventService,
+  eventData,
+}: {
+  eventService: IEventService;
+  eventData: BaseEventData;
+}): Promise<APIGatewayProxyResult> {
+  const writeEventResult = await writeEvent({
+    eventService,
+    eventName: "DCMAW_ASYNC_CRI_5XXERROR",
+    eventData,
+  });
+
+  if (writeEventResult.isError) {
+    logger.error(LogMessage.ERROR_WRITING_AUDIT_EVENT, {
+      data: {
+        auditEventName: "DCMAW_ASYNC_CRI_5XXERROR",
+      },
+    });
+  }
+  return serverErrorResponse;
+}
+
+async function handleSessionNotFound({
+  eventService,
+  eventData,
+}: {
+  eventService: IEventService;
+  eventData: BaseEventData;
+}): Promise<APIGatewayProxyResult> {
+  const writeEventResult = await writeEvent({
+    eventService,
+    eventName: "DCMAW_ASYNC_CRI_4XXERROR",
+    eventData,
+  });
+
+  const auditEventName = "DCMAW_ASYNC_CRI_4XXERROR";
+  if (writeEventResult.isError) {
+    logger.error(LogMessage.ERROR_WRITING_AUDIT_EVENT, {
+      data: { auditEventName },
+    });
+    return serverErrorResponse;
+  }
+
+  logger.error(LogMessage.TXMA_EVENT_SESSION_NOT_FOUND, {
path: backend-api/src/functions/asyncTxmaEvent/asyncTxmaEventHandler.ts
user: login: chris-cooksley-gds
body: We'll already be logging `GET_SESSION_SESSION_NOT_FOUND` in the dynamo adapter when this happens, so I think we can omit this log message to avoid double-logging the same error.
created_at: 2025-03-20T11:22:18Z
author_association: CONTRIBUTOR
diff_hunk: @@ -32,13 +50,184 @@ export async function lambdaHandlerConstructor(
     });
     return badRequestResponse("invalid_request", errorMessage);
   }
+  const { sessionId } = validateRequestBodyResult.value;
+  const eventService = dependencies.getEventService(config.TXMA_SQS);
 
-  logger.info(LogMessage.TXMA_EVENT_COMPLETED);
+  const sessionRegistry = dependencies.getSessionRegistry(
+    config.SESSION_TABLE_NAME,
+  );
+  const getSessionResult = await sessionRegistry.getSession(
+    sessionId,
+    new TxMAEventGetSessionOperation(),
+  );
 
+  const { ipAddress, txmaAuditEncoded } = getAuditData(event);
+  const eventData = {
+    sessionId,
+    issuer: config.ISSUER,
+    ipAddress,
+    txmaAuditEncoded,
+  };
+  if (getSessionResult.isError) {
+    return handleGetSessionError({
+      eventService,
+      errorData: getSessionResult.value,
+      eventData,
+    });
+  }
+
+  logger.info(LogMessage.TXMA_EVENT_COMPLETED);
   return notImplementedResponse;
 }
 
 export const lambdaHandler = lambdaHandlerConstructor.bind(
   null,
   runtimeDependencies,
 );
+
+interface BaseEventData {
+  sessionId: string;
+  issuer: string;
+  ipAddress: string;
+  txmaAuditEncoded: string | undefined;
+}
+
+async function handleGetSessionError({
+  eventService,
+  errorData,
+  eventData,
+}: {
+  eventService: IEventService;
+  errorData: GetSessionFailed;
+  eventData: BaseEventData;
+}): Promise<APIGatewayProxyResult> {
+  switch (errorData.errorType) {
+    case GetSessionError.INTERNAL_SERVER_ERROR:
+      return handleInternalServerError({ eventService, eventData });
+    case GetSessionError.SESSION_NOT_FOUND:
+      return handleSessionNotFound({
+        eventService,
+        eventData,
+      });
+    case GetSessionError.SESSION_INVALID:
+      return handleSessionInvalid({
+        eventService,
+        eventData,
+        sessionData: errorData.data,
+      });
+  }
+}
+
+async function handleInternalServerError({
+  eventService,
+  eventData,
+}: {
+  eventService: IEventService;
+  eventData: BaseEventData;
+}): Promise<APIGatewayProxyResult> {
+  const writeEventResult = await writeEvent({
+    eventService,
+    eventName: "DCMAW_ASYNC_CRI_5XXERROR",
+    eventData,
+  });
+
+  if (writeEventResult.isError) {
+    logger.error(LogMessage.ERROR_WRITING_AUDIT_EVENT, {
+      data: {
+        auditEventName: "DCMAW_ASYNC_CRI_5XXERROR",
+      },
+    });
+  }
+  return serverErrorResponse;
+}
+
+async function handleSessionNotFound({
+  eventService,
+  eventData,
+}: {
+  eventService: IEventService;
+  eventData: BaseEventData;
+}): Promise<APIGatewayProxyResult> {
+  const writeEventResult = await writeEvent({
+    eventService,
+    eventName: "DCMAW_ASYNC_CRI_4XXERROR",
+    eventData,
+  });
+
+  const auditEventName = "DCMAW_ASYNC_CRI_4XXERROR";
+  if (writeEventResult.isError) {
+    logger.error(LogMessage.ERROR_WRITING_AUDIT_EVENT, {
+      data: { auditEventName },
+    });
+    return serverErrorResponse;
+  }
+
+  logger.error(LogMessage.TXMA_EVENT_SESSION_NOT_FOUND, {
+    data: { auditEventName },
+  });
+  return unauthorizedResponse(
+    "invalid_session",
+    "Session does not exist or in incorrect state",
+  );
+}
+
+async function handleSessionInvalid({
+  eventService,
+  eventData,
+  sessionData,
+}: {
+  eventService: IEventService;
+  eventData: BaseEventData;
+  sessionData: GetSessionValidateSessionErrorData;
+}): Promise<APIGatewayProxyResult> {
+  const writeEventResult = await writeEvent({
+    eventService,
+    eventName: "DCMAW_ASYNC_CRI_4XXERROR",
+    eventData,
+  });
+
+  const auditEventName = "DCMAW_ASYNC_CRI_4XXERROR";
+  if (writeEventResult.isError) {
+    logger.error(LogMessage.ERROR_WRITING_AUDIT_EVENT, {
+      data: {
+        auditEventName,
+      },
+    });
+    return serverErrorResponse;
+  }
+
+  logger.error(LogMessage.TXMA_EVENT_SESSION_INVALID, {
path: backend-api/src/functions/asyncTxmaEvent/asyncTxmaEventHandler.ts
user: login: chris-cooksley-gds
body: Same point here - this will be more or less redundant with `GET_SESSION_SESSION_INVALID` already logged in the adapter.
created_at: 2025-03-20T11:23:24Z
author_association: CONTRIBUTOR
diff_hunk: @@ -32,13 +50,184 @@ export async function lambdaHandlerConstructor(
     });
     return badRequestResponse("invalid_request", errorMessage);
   }
+  const { sessionId } = validateRequestBodyResult.value;
+  const eventService = dependencies.getEventService(config.TXMA_SQS);
 
-  logger.info(LogMessage.TXMA_EVENT_COMPLETED);
+  const sessionRegistry = dependencies.getSessionRegistry(
+    config.SESSION_TABLE_NAME,
+  );
+  const getSessionResult = await sessionRegistry.getSession(
+    sessionId,
+    new TxMAEventGetSessionOperation(),
+  );
 
+  const { ipAddress, txmaAuditEncoded } = getAuditData(event);
+  const eventData = {
+    sessionId,
+    issuer: config.ISSUER,
+    ipAddress,
+    txmaAuditEncoded,
+  };
+  if (getSessionResult.isError) {
+    return handleGetSessionError({
+      eventService,
+      errorData: getSessionResult.value,
+      eventData,
+    });
+  }
+
+  logger.info(LogMessage.TXMA_EVENT_COMPLETED);
   return notImplementedResponse;
 }
 
 export const lambdaHandler = lambdaHandlerConstructor.bind(
   null,
   runtimeDependencies,
 );
+
+interface BaseEventData {
+  sessionId: string;
+  issuer: string;
+  ipAddress: string;
+  txmaAuditEncoded: string | undefined;
+}
+
+async function handleGetSessionError({
+  eventService,
+  errorData,
+  eventData,
+}: {
+  eventService: IEventService;
+  errorData: GetSessionFailed;
+  eventData: BaseEventData;
+}): Promise<APIGatewayProxyResult> {
+  switch (errorData.errorType) {
+    case GetSessionError.INTERNAL_SERVER_ERROR:
+      return handleInternalServerError({ eventService, eventData });
+    case GetSessionError.SESSION_NOT_FOUND:
+      return handleSessionNotFound({
+        eventService,
+        eventData,
+      });
+    case GetSessionError.SESSION_INVALID:
+      return handleSessionInvalid({
+        eventService,
+        eventData,
+        sessionData: errorData.data,
+      });
+  }
+}
+
+async function handleInternalServerError({
+  eventService,
+  eventData,
+}: {
+  eventService: IEventService;
+  eventData: BaseEventData;
+}): Promise<APIGatewayProxyResult> {
+  const writeEventResult = await writeEvent({
+    eventService,
+    eventName: "DCMAW_ASYNC_CRI_5XXERROR",
+    eventData,
+  });
+
+  if (writeEventResult.isError) {
+    logger.error(LogMessage.ERROR_WRITING_AUDIT_EVENT, {
+      data: {
+        auditEventName: "DCMAW_ASYNC_CRI_5XXERROR",
+      },
+    });
+  }
+  return serverErrorResponse;
+}
+
+async function handleSessionNotFound({
+  eventService,
+  eventData,
+}: {
+  eventService: IEventService;
+  eventData: BaseEventData;
+}): Promise<APIGatewayProxyResult> {
+  const writeEventResult = await writeEvent({
+    eventService,
+    eventName: "DCMAW_ASYNC_CRI_4XXERROR",
+    eventData,
+  });
+
+  const auditEventName = "DCMAW_ASYNC_CRI_4XXERROR";
+  if (writeEventResult.isError) {
+    logger.error(LogMessage.ERROR_WRITING_AUDIT_EVENT, {
+      data: { auditEventName },
+    });
+    return serverErrorResponse;
+  }
+
+  logger.error(LogMessage.TXMA_EVENT_SESSION_NOT_FOUND, {
+    data: { auditEventName },
+  });
+  return unauthorizedResponse(
+    "invalid_session",
+    "Session does not exist or in incorrect state",
+  );
+}
+
+async function handleSessionInvalid({
+  eventService,
+  eventData,
+  sessionData,
+}: {
+  eventService: IEventService;
+  eventData: BaseEventData;
+  sessionData: GetSessionValidateSessionErrorData;
+}): Promise<APIGatewayProxyResult> {
+  const writeEventResult = await writeEvent({
+    eventService,
+    eventName: "DCMAW_ASYNC_CRI_4XXERROR",
+    eventData,
+  });
+
+  const auditEventName = "DCMAW_ASYNC_CRI_4XXERROR";
+  if (writeEventResult.isError) {
+    logger.error(LogMessage.ERROR_WRITING_AUDIT_EVENT, {
+      data: {
+        auditEventName,
+      },
+    });
+    return serverErrorResponse;
+  }
+
+  logger.error(LogMessage.TXMA_EVENT_SESSION_INVALID, {
+    data: {
+      auditEventName,
+      ...sessionData,
+    },
+  });
+  return unauthorizedResponse(
+    "invalid_session",
+    "Session does not exist or in incorrect state",
+  );
+}
+
+async function writeEvent({
+  eventService,
+  eventName,
+  eventData,
+}: {
+  eventService: IEventService;
+  eventName: GenericEventNames;
+  eventData: BaseEventData;
+}): Promise<Result<void, void>> {
+  const { sessionId, issuer, ipAddress, txmaAuditEncoded } = eventData;
+  return await eventService.writeGenericEvent({
+    eventName,
+    sessionId,
+    componentId: issuer,
+    ipAddress,
+    txmaAuditEncoded,
+    sub: undefined,
path: backend-api/src/functions/asyncTxmaEvent/asyncTxmaEventHandler.ts
user: login: chris-cooksley-gds
body: Nitpick: can we just omit the sub, journeyId, redirect_uri and fraud signal, instead of adding them in as undefined?
created_at: 2025-03-20T11:25:14Z
author_association: CONTRIBUTOR
diff_hunk: @@ -32,13 +50,184 @@ export async function lambdaHandlerConstructor(
     });
     return badRequestResponse("invalid_request", errorMessage);
   }
+  const { sessionId } = validateRequestBodyResult.value;
+  const eventService = dependencies.getEventService(config.TXMA_SQS);
 
-  logger.info(LogMessage.TXMA_EVENT_COMPLETED);
+  const sessionRegistry = dependencies.getSessionRegistry(
+    config.SESSION_TABLE_NAME,
+  );
+  const getSessionResult = await sessionRegistry.getSession(
+    sessionId,
+    new TxMAEventGetSessionOperation(),
+  );
 
+  const { ipAddress, txmaAuditEncoded } = getAuditData(event);
+  const eventData = {
+    sessionId,
+    issuer: config.ISSUER,
+    ipAddress,
+    txmaAuditEncoded,
+  };
+  if (getSessionResult.isError) {
+    return handleGetSessionError({
+      eventService,
+      errorData: getSessionResult.value,
+      eventData,
+    });
+  }
+
+  logger.info(LogMessage.TXMA_EVENT_COMPLETED);
   return notImplementedResponse;
 }
 
 export const lambdaHandler = lambdaHandlerConstructor.bind(
   null,
   runtimeDependencies,
 );
+
+interface BaseEventData {
+  sessionId: string;
+  issuer: string;
+  ipAddress: string;
+  txmaAuditEncoded: string | undefined;
+}
+
+async function handleGetSessionError({
+  eventService,
+  errorData,
+  eventData,
+}: {
+  eventService: IEventService;
+  errorData: GetSessionFailed;
+  eventData: BaseEventData;
+}): Promise<APIGatewayProxyResult> {
+  switch (errorData.errorType) {
+    case GetSessionError.INTERNAL_SERVER_ERROR:
+      return handleInternalServerError({ eventService, eventData });
+    case GetSessionError.SESSION_NOT_FOUND:
+      return handleSessionNotFound({
+        eventService,
+        eventData,
+      });
+    case GetSessionError.SESSION_INVALID:
+      return handleSessionInvalid({
+        eventService,
+        eventData,
+        sessionData: errorData.data,
+      });
+  }
+}
+
+async function handleInternalServerError({
+  eventService,
+  eventData,
+}: {
+  eventService: IEventService;
+  eventData: BaseEventData;
+}): Promise<APIGatewayProxyResult> {
+  const writeEventResult = await writeEvent({
+    eventService,
+    eventName: "DCMAW_ASYNC_CRI_5XXERROR",
+    eventData,
+  });
+
+  if (writeEventResult.isError) {
+    logger.error(LogMessage.ERROR_WRITING_AUDIT_EVENT, {
+      data: {
+        auditEventName: "DCMAW_ASYNC_CRI_5XXERROR",
+      },
+    });
+  }
+  return serverErrorResponse;
+}
+
+async function handleSessionNotFound({
+  eventService,
+  eventData,
+}: {
+  eventService: IEventService;
+  eventData: BaseEventData;
+}): Promise<APIGatewayProxyResult> {
+  const writeEventResult = await writeEvent({
+    eventService,
+    eventName: "DCMAW_ASYNC_CRI_4XXERROR",
+    eventData,
+  });
+
+  const auditEventName = "DCMAW_ASYNC_CRI_4XXERROR";
path: backend-api/src/functions/asyncTxmaEvent/asyncTxmaEventHandler.ts
user: login: jhumbert-dd
body: Agree. This is no longer applicable following the removal of writing to TxMA
created_at: 2025-03-20T12:55:30Z
author_association: CONTRIBUTOR
diff_hunk: @@ -32,13 +50,184 @@ export async function lambdaHandlerConstructor(
     });
     return badRequestResponse("invalid_request", errorMessage);
   }
+  const { sessionId } = validateRequestBodyResult.value;
+  const eventService = dependencies.getEventService(config.TXMA_SQS);
 
-  logger.info(LogMessage.TXMA_EVENT_COMPLETED);
+  const sessionRegistry = dependencies.getSessionRegistry(
+    config.SESSION_TABLE_NAME,
+  );
+  const getSessionResult = await sessionRegistry.getSession(
+    sessionId,
+    new TxMAEventGetSessionOperation(),
+  );
 
+  const { ipAddress, txmaAuditEncoded } = getAuditData(event);
+  const eventData = {
+    sessionId,
+    issuer: config.ISSUER,
+    ipAddress,
+    txmaAuditEncoded,
+  };
+  if (getSessionResult.isError) {
+    return handleGetSessionError({
+      eventService,
+      errorData: getSessionResult.value,
+      eventData,
+    });
+  }
+
+  logger.info(LogMessage.TXMA_EVENT_COMPLETED);
   return notImplementedResponse;
 }
 
 export const lambdaHandler = lambdaHandlerConstructor.bind(
   null,
   runtimeDependencies,
 );
+
+interface BaseEventData {
+  sessionId: string;
+  issuer: string;
+  ipAddress: string;
+  txmaAuditEncoded: string | undefined;
+}
+
+async function handleGetSessionError({
+  eventService,
+  errorData,
+  eventData,
+}: {
+  eventService: IEventService;
+  errorData: GetSessionFailed;
+  eventData: BaseEventData;
+}): Promise<APIGatewayProxyResult> {
+  switch (errorData.errorType) {
+    case GetSessionError.INTERNAL_SERVER_ERROR:
+      return handleInternalServerError({ eventService, eventData });
+    case GetSessionError.SESSION_NOT_FOUND:
+      return handleSessionNotFound({
+        eventService,
+        eventData,
+      });
+    case GetSessionError.SESSION_INVALID:
+      return handleSessionInvalid({
+        eventService,
+        eventData,
+        sessionData: errorData.data,
+      });
+  }
+}
+
+async function handleInternalServerError({
+  eventService,
+  eventData,
+}: {
+  eventService: IEventService;
+  eventData: BaseEventData;
+}): Promise<APIGatewayProxyResult> {
+  const writeEventResult = await writeEvent({
+    eventService,
+    eventName: "DCMAW_ASYNC_CRI_5XXERROR",
+    eventData,
+  });
+
+  if (writeEventResult.isError) {
+    logger.error(LogMessage.ERROR_WRITING_AUDIT_EVENT, {
+      data: {
+        auditEventName: "DCMAW_ASYNC_CRI_5XXERROR",
+      },
+    });
+  }
+  return serverErrorResponse;
+}
+
+async function handleSessionNotFound({
+  eventService,
+  eventData,
+}: {
+  eventService: IEventService;
+  eventData: BaseEventData;
+}): Promise<APIGatewayProxyResult> {
+  const writeEventResult = await writeEvent({
+    eventService,
+    eventName: "DCMAW_ASYNC_CRI_4XXERROR",
+    eventData,
+  });
+
+  const auditEventName = "DCMAW_ASYNC_CRI_4XXERROR";
+  if (writeEventResult.isError) {
+    logger.error(LogMessage.ERROR_WRITING_AUDIT_EVENT, {
+      data: { auditEventName },
+    });
+    return serverErrorResponse;
+  }
+
+  logger.error(LogMessage.TXMA_EVENT_SESSION_NOT_FOUND, {
+    data: { auditEventName },
+  });
+  return unauthorizedResponse(
+    "invalid_session",
+    "Session does not exist or in incorrect state",
+  );
+}
+
+async function handleSessionInvalid({
+  eventService,
+  eventData,
+  sessionData,
+}: {
+  eventService: IEventService;
+  eventData: BaseEventData;
+  sessionData: GetSessionValidateSessionErrorData;
+}): Promise<APIGatewayProxyResult> {
+  const writeEventResult = await writeEvent({
+    eventService,
+    eventName: "DCMAW_ASYNC_CRI_4XXERROR",
+    eventData,
+  });
+
+  const auditEventName = "DCMAW_ASYNC_CRI_4XXERROR";
+  if (writeEventResult.isError) {
+    logger.error(LogMessage.ERROR_WRITING_AUDIT_EVENT, {
+      data: {
+        auditEventName,
+      },
+    });
+    return serverErrorResponse;
+  }
+
+  logger.error(LogMessage.TXMA_EVENT_SESSION_INVALID, {
+    data: {
+      auditEventName,
+      ...sessionData,
+    },
+  });
+  return unauthorizedResponse(
+    "invalid_session",
+    "Session does not exist or in incorrect state",
+  );
+}
+
+async function writeEvent({
+  eventService,
+  eventName,
+  eventData,
+}: {
+  eventService: IEventService;
+  eventName: GenericEventNames;
+  eventData: BaseEventData;
+}): Promise<Result<void, void>> {
+  const { sessionId, issuer, ipAddress, txmaAuditEncoded } = eventData;
+  return await eventService.writeGenericEvent({
+    eventName,
+    sessionId,
+    componentId: issuer,
+    ipAddress,
+    txmaAuditEncoded,
+    sub: undefined,
path: backend-api/src/functions/asyncTxmaEvent/asyncTxmaEventHandler.ts
user: login: jhumbert-dd
body: Thanks @chris-cooksley-gds. This is no longer necessary following [decision to remove writing to TxMA](https://gds.slack.com/archives/C065WKXD7B6/p1742469639159639?thread_ts=1742462958.369549&cid=C065WKXD7B6)
created_at: 2025-03-20T12:58:46Z
author_association: CONTRIBUTOR
diff_hunk: @@ -32,13 +50,184 @@ export async function lambdaHandlerConstructor(
     });
     return badRequestResponse("invalid_request", errorMessage);
   }
+  const { sessionId } = validateRequestBodyResult.value;
+  const eventService = dependencies.getEventService(config.TXMA_SQS);
 
-  logger.info(LogMessage.TXMA_EVENT_COMPLETED);
+  const sessionRegistry = dependencies.getSessionRegistry(
+    config.SESSION_TABLE_NAME,
+  );
+  const getSessionResult = await sessionRegistry.getSession(
+    sessionId,
+    new TxMAEventGetSessionOperation(),
+  );
 
+  const { ipAddress, txmaAuditEncoded } = getAuditData(event);
+  const eventData = {
+    sessionId,
+    issuer: config.ISSUER,
+    ipAddress,
+    txmaAuditEncoded,
+  };
+  if (getSessionResult.isError) {
+    return handleGetSessionError({
+      eventService,
+      errorData: getSessionResult.value,
+      eventData,
+    });
+  }
+
+  logger.info(LogMessage.TXMA_EVENT_COMPLETED);
   return notImplementedResponse;
 }
 
 export const lambdaHandler = lambdaHandlerConstructor.bind(
   null,
   runtimeDependencies,
 );
+
+interface BaseEventData {
+  sessionId: string;
+  issuer: string;
+  ipAddress: string;
+  txmaAuditEncoded: string | undefined;
+}
+
+async function handleGetSessionError({
+  eventService,
+  errorData,
+  eventData,
+}: {
+  eventService: IEventService;
+  errorData: GetSessionFailed;
+  eventData: BaseEventData;
+}): Promise<APIGatewayProxyResult> {
+  switch (errorData.errorType) {
+    case GetSessionError.INTERNAL_SERVER_ERROR:
+      return handleInternalServerError({ eventService, eventData });
+    case GetSessionError.SESSION_NOT_FOUND:
+      return handleSessionNotFound({
+        eventService,
+        eventData,
+      });
+    case GetSessionError.SESSION_INVALID:
+      return handleSessionInvalid({
+        eventService,
+        eventData,
+        sessionData: errorData.data,
+      });
+  }
+}
+
+async function handleInternalServerError({
+  eventService,
+  eventData,
+}: {
+  eventService: IEventService;
+  eventData: BaseEventData;
+}): Promise<APIGatewayProxyResult> {
+  const writeEventResult = await writeEvent({
+    eventService,
+    eventName: "DCMAW_ASYNC_CRI_5XXERROR",
+    eventData,
+  });
+
+  if (writeEventResult.isError) {
+    logger.error(LogMessage.ERROR_WRITING_AUDIT_EVENT, {
+      data: {
+        auditEventName: "DCMAW_ASYNC_CRI_5XXERROR",
+      },
+    });
+  }
+  return serverErrorResponse;
+}
+
+async function handleSessionNotFound({
+  eventService,
+  eventData,
+}: {
+  eventService: IEventService;
+  eventData: BaseEventData;
+}): Promise<APIGatewayProxyResult> {
+  const writeEventResult = await writeEvent({
+    eventService,
+    eventName: "DCMAW_ASYNC_CRI_4XXERROR",
+    eventData,
+  });
+
+  const auditEventName = "DCMAW_ASYNC_CRI_4XXERROR";
+  if (writeEventResult.isError) {
+    logger.error(LogMessage.ERROR_WRITING_AUDIT_EVENT, {
+      data: { auditEventName },
+    });
+    return serverErrorResponse;
+  }
+
+  logger.error(LogMessage.TXMA_EVENT_SESSION_NOT_FOUND, {
path: backend-api/src/functions/asyncTxmaEvent/asyncTxmaEventHandler.ts
user: login: jhumbert-dd
body: Updated
created_at: 2025-03-20T13:16:09Z
author_association: CONTRIBUTOR
diff_hunk: @@ -32,13 +50,184 @@ export async function lambdaHandlerConstructor(
     });
     return badRequestResponse("invalid_request", errorMessage);
   }
+  const { sessionId } = validateRequestBodyResult.value;
+  const eventService = dependencies.getEventService(config.TXMA_SQS);
 
-  logger.info(LogMessage.TXMA_EVENT_COMPLETED);
+  const sessionRegistry = dependencies.getSessionRegistry(
+    config.SESSION_TABLE_NAME,
+  );
+  const getSessionResult = await sessionRegistry.getSession(
+    sessionId,
+    new TxMAEventGetSessionOperation(),
+  );
 
+  const { ipAddress, txmaAuditEncoded } = getAuditData(event);
+  const eventData = {
+    sessionId,
+    issuer: config.ISSUER,
+    ipAddress,
+    txmaAuditEncoded,
+  };
+  if (getSessionResult.isError) {
+    return handleGetSessionError({
+      eventService,
+      errorData: getSessionResult.value,
+      eventData,
+    });
+  }
+
+  logger.info(LogMessage.TXMA_EVENT_COMPLETED);
   return notImplementedResponse;
 }
 
 export const lambdaHandler = lambdaHandlerConstructor.bind(
   null,
   runtimeDependencies,
 );
+
+interface BaseEventData {
+  sessionId: string;
+  issuer: string;
+  ipAddress: string;
+  txmaAuditEncoded: string | undefined;
+}
+
+async function handleGetSessionError({
+  eventService,
+  errorData,
+  eventData,
+}: {
+  eventService: IEventService;
+  errorData: GetSessionFailed;
+  eventData: BaseEventData;
+}): Promise<APIGatewayProxyResult> {
+  switch (errorData.errorType) {
+    case GetSessionError.INTERNAL_SERVER_ERROR:
+      return handleInternalServerError({ eventService, eventData });
+    case GetSessionError.SESSION_NOT_FOUND:
+      return handleSessionNotFound({
+        eventService,
+        eventData,
+      });
+    case GetSessionError.SESSION_INVALID:
+      return handleSessionInvalid({
+        eventService,
+        eventData,
+        sessionData: errorData.data,
+      });
+  }
+}
+
+async function handleInternalServerError({
+  eventService,
+  eventData,
+}: {
+  eventService: IEventService;
+  eventData: BaseEventData;
+}): Promise<APIGatewayProxyResult> {
+  const writeEventResult = await writeEvent({
+    eventService,
+    eventName: "DCMAW_ASYNC_CRI_5XXERROR",
+    eventData,
+  });
+
+  if (writeEventResult.isError) {
+    logger.error(LogMessage.ERROR_WRITING_AUDIT_EVENT, {
+      data: {
+        auditEventName: "DCMAW_ASYNC_CRI_5XXERROR",
+      },
+    });
+  }
+  return serverErrorResponse;
+}
+
+async function handleSessionNotFound({
+  eventService,
+  eventData,
+}: {
+  eventService: IEventService;
+  eventData: BaseEventData;
+}): Promise<APIGatewayProxyResult> {
+  const writeEventResult = await writeEvent({
+    eventService,
+    eventName: "DCMAW_ASYNC_CRI_4XXERROR",
+    eventData,
+  });
+
+  const auditEventName = "DCMAW_ASYNC_CRI_4XXERROR";
+  if (writeEventResult.isError) {
+    logger.error(LogMessage.ERROR_WRITING_AUDIT_EVENT, {
+      data: { auditEventName },
+    });
+    return serverErrorResponse;
+  }
+
+  logger.error(LogMessage.TXMA_EVENT_SESSION_NOT_FOUND, {
+    data: { auditEventName },
+  });
+  return unauthorizedResponse(
+    "invalid_session",
+    "Session does not exist or in incorrect state",
+  );
+}
+
+async function handleSessionInvalid({
+  eventService,
+  eventData,
+  sessionData,
+}: {
+  eventService: IEventService;
+  eventData: BaseEventData;
+  sessionData: GetSessionValidateSessionErrorData;
+}): Promise<APIGatewayProxyResult> {
+  const writeEventResult = await writeEvent({
+    eventService,
+    eventName: "DCMAW_ASYNC_CRI_4XXERROR",
+    eventData,
+  });
+
+  const auditEventName = "DCMAW_ASYNC_CRI_4XXERROR";
+  if (writeEventResult.isError) {
+    logger.error(LogMessage.ERROR_WRITING_AUDIT_EVENT, {
+      data: {
+        auditEventName,
+      },
+    });
+    return serverErrorResponse;
+  }
+
+  logger.error(LogMessage.TXMA_EVENT_SESSION_INVALID, {
path: backend-api/src/functions/asyncTxmaEvent/asyncTxmaEventHandler.ts
user: login: jhumbert-dd
body: Updated
created_at: 2025-03-20T13:16:16Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,25 @@
+import { AttributeValue, GetItemCommandInput } from "@aws-sdk/client-dynamodb";
+import { Result } from "../../../utils/result";
+import { SessionAttributes } from "../session";
+import {
+  ValidateSessionErrorInvalidAttributeData,
+  ValidateSessionAttributes,
+} from "../SessionRegistry";
+
+export interface GetSessionOperation {
+  getDynamoDbGetCommandInput({
path: backend-api/src/functions/common/session/getOperations/GetSessionOperation.ts
user: login: chris-cooksley-gds
body: Praise: I've come to like the pattern of using an operation to polymorphize(is that a word) our gets. I'd originally thought it'd be simpler to just pass in the session state we wanted to get, but putting it in an operation like this means we don't need any conditional logic in the adapter to figure out what kind of validation we need to do, and can instead keep that logic with the specific operation, which is nice and clean.

Thinking out loud: what I'm not as sure about is having `getDynamoDbGetCommandInput` here. - the only situation I can think of where this wouldn't be the same constant value, is when we're getting an active session. But that scenario is different from the others in quite a few ways (e.g. we'll need to specify the index and the sub, but not the session ID), to the point where I suspect it would be cleaner to just handle it with a different method rather than try to stretch this pattern to handle the different approach. That way we can move the command input into the adapter and the GetSessionOperations can be a bit simpler - i.e. we'll always use the session ID and main table to retrieve a session in some state, and what will vary is the attributes that session has and how they might be validated. Happy to discuss and/or defer to the group on this one.
created_at: 2025-03-20T13:55:48Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,55 @@
+import { AttributeValue, GetItemCommandInput } from "@aws-sdk/client-dynamodb";
+import { marshall } from "@aws-sdk/util-dynamodb";
+import { emptySuccess, errorResult, Result } from "../../../../utils/result";
+import {
+  isOlderThan60minutes,
+  SessionAttributes,
+  SessionState,
+} from "../../session";
+import { getBiometricTokenIssuedSessionAttributes } from "../../sessionAttributes/sessionAttributes";
+import { GetSessionOperation } from "../GetSessionOperation";
+import {
+  ValidateSessionErrorInvalidAttributeData,
+  ValidateSessionAttributes,
+} from "../../SessionRegistry";
+
+export class TxMAEventGetSessionOperation implements GetSessionOperation {
path: backend-api/src/functions/common/session/getOperations/TxmaEvent/TxmaEventGetSessionOperation.ts
user: login: chris-cooksley-gds
body: Suggestion: currently this operation is named after the (only) caller that currently uses it. It might be cleaner to instead name it after what kind of session it gets - e.g. `GetBiometricTokenIssued` or `GetSessionBiometricTokenIssued`. That would make its behaviour obvious just from the name.
created_at: 2025-03-20T15:25:36Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,55 @@
+import { AttributeValue, GetItemCommandInput } from "@aws-sdk/client-dynamodb";
+import { marshall } from "@aws-sdk/util-dynamodb";
+import { emptySuccess, errorResult, Result } from "../../../../utils/result";
+import {
+  isOlderThan60minutes,
+  SessionAttributes,
+  SessionState,
+} from "../../session";
+import { getBiometricTokenIssuedSessionAttributes } from "../../sessionAttributes/sessionAttributes";
+import { GetSessionOperation } from "../GetSessionOperation";
+import {
+  ValidateSessionErrorInvalidAttributeData,
+  ValidateSessionAttributes,
+} from "../../SessionRegistry";
+
+export class TxMAEventGetSessionOperation implements GetSessionOperation {
+  getDynamoDbGetCommandInput({
+    tableName,
+    keyValue,
+  }: {
+    tableName: string;
+    keyValue: string;
+  }): GetItemCommandInput {
+    return {
+      TableName: tableName,
+      Key: { sessionId: marshall(keyValue) },
+    };
+  }
+
+  getSessionAttributesFromDynamoDbItem(
+    item: Record<string, AttributeValue> | undefined,
+  ): Result<SessionAttributes, void> {
+    return getBiometricTokenIssuedSessionAttributes(item);
+  }
+
+  validateSession(
+    attributes: ValidateSessionAttributes,
+  ): Result<void, ValidateSessionErrorInvalidAttributeData> {
+    const { sessionState, createdAt } = attributes;
+
+    if (!sessionState || sessionState !== SessionState.BIOMETRIC_TOKEN_ISSUED) {
+      return errorResult({
+        invalidAttribute: { sessionState },
path: backend-api/src/functions/common/session/getOperations/TxmaEvent/TxmaEventGetSessionOperation.ts
user: login: chris-cooksley-gds
body: Nitpick: very much a nice-to-have, but we could append the invalid attribute to an array in case of either of these failures. Then if the array has anything in it, we could return an errorResult with `invalidAttributes` (plural), thereby communicating everything that was wrong instead of just the first thing.
created_at: 2025-03-20T15:28:25Z
author_association: CONTRIBUTOR
diff_hunk: @@ -49,6 +55,15 @@ export const getBiometricTokenIssuedSessionAttributes = (
   return successResult(sessionAttributes);
 };
 
+const isBiometricTokenIssuedSessionAttributes = (
path: backend-api/src/functions/common/session/sessionAttributes/sessionAttributes.ts
user: login: chris-cooksley-gds
body: Praise: great way to reduce repetition
created_at: 2025-03-20T15:33:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -131,6 +143,67 @@ export class DynamoDbAdapter implements SessionRegistry {
     }
   }
 
+  async getSession(
+    sessionId: string,
+    getOperation: GetSessionOperation,
+  ): Promise<Result<SessionAttributes, GetSessionFailed>> {
+    const getItemCommandInput = getOperation.getDynamoDbGetCommandInput({
+      tableName: this.tableName,
+      keyValue: sessionId,
+    });
+
+    let response;
+    try {
+      logger.debug(LogMessage.GET_SESSION_ATTEMPT, {
+        data: { sessionId, getItemCommandInput },
+      });
+
+      response = await this.dynamoDbClient.send(
+        new GetItemCommand(getItemCommandInput),
+      );
+    } catch (error: unknown) {
+      return this.handleGetSessionInternalServerError({
+        error,
+        getItemCommandInput,
+      });
+    }
+
+    const responseItem = response.Item;
+    if (responseItem == null) {
+      return this.handleGetSessionNotFoundError({
+        errorMessage: sessionNotFound,
+      });
+    }
+
+    // Attribute type validation
+    const getSessionAttributesResult =
+      getOperation.getSessionAttributesFromDynamoDbItem(responseItem);
+    if (getSessionAttributesResult.isError) {
path: backend-api/src/functions/adapters/aws/dynamo/dynamoDbAdapter.ts
user: login: chris-cooksley-gds
body: So, if getting the attributes fails, we'll treat it as session not found (and down the line, the caller will get a 401, indicating that it's their fault). But if a session is corrupted/malformed in this way - e.g. one of its attributes is of the wrong type, or the session object is null, or something like that - doesn't that imply that something in _our_ system is wrong, rather than a client error? I wonder if we ought to be treating these failures as server errors (so long as we can still identify when it's a well-formed session in an incorrect state and return the right error). CC @sandymay-dd 
created_at: 2025-03-20T15:38:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -42,4 +50,27 @@ export class BiometricSessionFinished implements UpdateSessionOperation {
       return getBiometricSessionFinishedSessionAttributes(item);
     }
   }
+
+  validateSession(
path: backend-api/src/functions/common/session/updateOperations/BiometricSessionFinished/BiometricSessionFinished.ts
user: login: chris-cooksley-gds
body: How come we're adding new validation functionality to update operations as part of this ticket? I think our update operations already include session state/createdAt checks as part of their conditional expressions - and in any case I'd expect a separate ticket if we needed to change those.
created_at: 2025-03-20T15:42:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -13,28 +13,34 @@ export type SessionAttributes =
 
 export interface BaseSessionAttributes {
   clientId: string;
-  govukSigninJourneyId: string;
+  clientState: string;
   createdAt: number;
+  govukSigninJourneyId: string;
   issuer: string;
   sessionId: string;
   sessionState: SessionState;
-  clientState: string;
   subjectIdentifier: string;
   timeToLive: number;
   redirectUri?: string;
 }
 
 export interface BiometricTokenIssuedSessionAttributes
   extends BaseSessionAttributes {
-  sessionState: SessionState.BIOMETRIC_TOKEN_ISSUED;
   documentType: DocumentType;
   opaqueId: string;
+  sessionState: SessionState.BIOMETRIC_TOKEN_ISSUED;
 }
 
 export interface BiometricSessionFinishedAttributes
   extends BaseSessionAttributes {
-  sessionState: SessionState.BIOMETRIC_SESSION_FINISHED;
   documentType: DocumentType;
   opaqueId: string;
+  sessionState: SessionState.BIOMETRIC_SESSION_FINISHED;
   biometricSessionId: string;
 }
+
+export function isOlderThan60minutes(createdAtInMilliseconds: number) {
path: backend-api/src/functions/common/session/session.ts
user: login: chris-cooksley-gds
body: Currently this file is pretty much* entirely types/interfaces. Given that this function is functional, it might be better placed elsewhere.

Also, the `m` in minutes should be capitalised, but that's a nitpick.

*technically enums aren't just types
created_at: 2025-03-20T15:48:09Z
author_association: CONTRIBUTOR
path: backend-api/src/functions/common/session/SessionRegistry.ts
user: login: chris-cooksley-gds
body: Nitpick: as we chatted about earlier - there's an argument that we could improve readability by cutting down on some of these types, without losing much of value in terms of type safety. But totally non-blocking, as it's very subjective.
created_at: 2025-03-20T15:50:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -225,14 +312,65 @@ export class DynamoDbAdapter implements SessionRegistry {
 
   private handleUpdateSessionInternalServerError(
     error: unknown,
-    updateExpressionDataToLog: UpdateExpressionDataToLog,
+    data: UpdateOperationDataToLog,
   ): FailureWithValue<SessionUpdateFailedInternalServerError> {
     logger.error(LogMessage.UPDATE_SESSION_UNEXPECTED_FAILURE, {
-      error: error,
-      data: updateExpressionDataToLog,
+      error,
+      data,
     });
     return errorResult({
       errorType: UpdateSessionError.INTERNAL_SERVER_ERROR,
     });
   }
+
+  private handleGetSessionInternalServerError({
+    error,
+    getItemCommandInput,
+  }: {
+    error: unknown;
+    getItemCommandInput: GetItemCommandInput;
+  }): FailureWithValue<GetSessionInternalServerError> {
+    logger.error(LogMessage.GET_SESSION_UNEXPECTED_FAILURE, {
+      error,
+      getItemCommandInput,
+    });
+    return errorResult({
+      errorType: GetSessionError.INTERNAL_SERVER_ERROR,
+    });
+  }
+
+  private handleGetSessionNotFoundError({
+    errorMessage,
path: backend-api/src/functions/adapters/aws/dynamo/dynamoDbAdapter.ts
user: login: chris-cooksley-gds
body: Currently I think this error message is always the constant 'Session not found', which is information already fully communicated by the log message/message code. And I don't think the errorMessage in the result sent back to the handler is used, either. I think we can probably just remove this parameter, and the constant, and omit the errorMessage from the log/result.
created_at: 2025-03-20T15:57:32Z
author_association: CONTRIBUTOR
diff_hunk: @@ -26,17 +44,83 @@ export interface SessionUpdateFailedInternalServerError {
   errorType: UpdateSessionError.INTERNAL_SERVER_ERROR;
 }
 
-export interface SessionUpdateFailedSessionNotFound {
+interface SessionUpdateFailedSessionNotFound {
   errorType: UpdateSessionError.SESSION_NOT_FOUND;
 }
 
-export enum UpdateSessionError {
+export type UpdateOperationDataToLog =
+  | UpdateExpressionDataToLog
+  | UpdateSessionValidateSessionErrorData;
+
+interface UpdateExpressionDataToLog {
+  updateExpression: string;
+  conditionExpression: string | undefined;
+}
+
+export interface UpdateSessionValidateSessionErrorData {
+  invalidAttribute:
+    | UpdateSessionInvalidSessionAttribute
+    | BiometricSessionFinishedInvalidSessionAttribute;
+}
+
+interface UpdateSessionInvalidSessionAttribute {
+  sessionState?: Exclude<SessionState, SessionState.BIOMETRIC_TOKEN_ISSUED>;
+  createdAt?: number;
+}
+
+export interface BiometricSessionFinishedValidateSessionErrorData {
+  invalidAttribute: BiometricSessionFinishedInvalidSessionAttribute;
+}
+
+interface BiometricSessionFinishedInvalidSessionAttribute {
+  sessionState?: Exclude<SessionState, SessionState.BIOMETRIC_SESSION_FINISHED>;
+  createdAt?: number;
+}
+
+// Get session
+
+export enum GetSessionError {
   INTERNAL_SERVER_ERROR = "INTERNAL_SERVER_ERROR",
-  CONDITIONAL_CHECK_FAILURE = "CONDITIONAL_CHECK_FAILURE",
   SESSION_NOT_FOUND = "SESSION_NOT_FOUND",
+  SESSION_INVALID = "SESSION_INVALID",
 }
 
-export interface UpdateExpressionDataToLog {
-  updateExpression: string;
-  conditionExpression: string | undefined;
+export type GetSessionFailed =
path: backend-api/src/functions/common/session/SessionRegistry.ts
user: login: chris-cooksley-gds
body: Come to think of it, I don't think we actually use any information in the caller other than whether or not it was an internal server error. I wonder if instead of a GetSessionFailed, we could literally just return a GetSessionError (and perhaps even roll SESSION_NOT_FOUND and SESSION_INVALID into one while we're at it, since they get treated in the same way).
created_at: 2025-03-20T15:59:58Z
author_association: CONTRIBUTOR
diff_hunk: @@ -42,4 +50,27 @@ export class BiometricSessionFinished implements UpdateSessionOperation {
       return getBiometricSessionFinishedSessionAttributes(item);
     }
   }
+
+  validateSession(
path: backend-api/src/functions/common/session/updateOperations/BiometricSessionFinished/BiometricSessionFinished.ts
user: login: jhumbert-dd
body: Ah yes, I think the validation method should only be on the get operation as it's not possible to get a session back if the conditional / filter expression fails. I'll remove this from the update interface
created_at: 2025-03-20T16:03:16Z
author_association: CONTRIBUTOR
diff_hunk: @@ -293,4 +431,204 @@ describe("DynamoDbAdapter", () => {
       });
     });
   });
+
+  describe("getSession", () => {
+    const getOperation: GetSessionOperation =
+      new TxMAEventGetSessionOperation();
+    let result: Result<void, GetSessionFailed>;
+
+    describe("On every attempt", () => {
+      beforeEach(async () => {
+        mockDynamoDbClient.on(GetItemCommand).resolves({});
+        await sessionRegistry.getSession(mockSessionId, getOperation);
+      });
+
+      it("Logs the attempt", () => {
+        expect(consoleDebugSpy).toHaveBeenCalledWithLogFields({
+          messageCode: "MOBILE_ASYNC_GET_SESSION_ATTEMPT",
+          data: {
+            sessionId: mockSessionId,
+            getItemCommandInput: getOperation.getDynamoDbGetCommandInput({
+              tableName: "mock_table_name",
+              keyValue: mockSessionId,
+            }),
+          },
+        });
+      });
+    });
+
+    describe("Given there is an unexpected error retrieving the session", () => {
+      beforeEach(async () => {
+        mockDynamoDbClient.on(GetItemCommand).rejects("mock_error");
+        result = await sessionRegistry.getSession(mockSessionId, getOperation);
+      });
+
+      it("Logs the failure", () => {
+        expect(consoleErrorSpy).toHaveBeenCalledWithLogFields({
+          messageCode: "MOBILE_ASYNC_GET_SESSION_UNEXPECTED_FAILURE",
+          getItemCommandInput: {
+            TableName: "mock_table_name",
+            Key: { sessionId: marshall(mockSessionId) },
+          },
+        });
+
+        const { name, message } = JSON.parse(
+          consoleErrorSpy.mock.calls[0][0],
+        ).error;
+        expect(name).toEqual("Error");
+        expect(message).toEqual("mock_error");
+      });
+
+      it("Returns failure with server error", () => {
+        expect(result).toEqual(
+          errorResult({
+            errorType: GetSessionError.INTERNAL_SERVER_ERROR,
+          }),
+        );
+      });
+    });
+
+    describe("Given the session was not found", () => {
+      beforeEach(async () => {
+        mockDynamoDbClient.on(GetItemCommand).resolves({});
+        result = await sessionRegistry.getSession(mockSessionId, getOperation);
+      });
+
+      it("Logs the failure", () => {
+        expect(consoleErrorSpy).toHaveBeenCalledWithLogFields({
+          messageCode: "MOBILE_ASYNC_GET_SESSION_SESSION_NOT_FOUND",
+          errorMessage: "Session not found",
+        });
+      });
+
+      it("Returns failure with an invalid session error", () => {
+        expect(result).toEqual(
+          errorResult({
+            errorType: GetSessionError.SESSION_NOT_FOUND,
+            errorMessage: "Session not found",
+          }),
+        );
+      });
+    });
+
+    describe("Session validation", () => {
+      describe("Given session attributes type validation failed", () => {
+        const invalidBiometricTokenIssuedSessionAttributesWrongType = {
+          ...validBiometricTokenIssuedSessionAttributes,
+          sessionState: 12345,
+        };
+
+        beforeEach(async () => {
+          mockDynamoDbClient.on(GetItemCommand).resolves({
+            Item: marshall(
+              invalidBiometricTokenIssuedSessionAttributesWrongType,
+            ),
+          });
+          result = await sessionRegistry.getSession(
+            mockSessionId,
+            getOperation,
+          );
+        });
+
+        it("Logs the failure", () => {
+          expect(consoleErrorSpy).toHaveBeenCalledWithLogFields({
+            messageCode: "MOBILE_ASYNC_GET_SESSION_SESSION_NOT_FOUND",
+            errorMessage: "Session not found",
+          });
+        });
+
+        it("Returns failure with server error", () => {
path: backend-api/src/functions/adapters/aws/dynamo/dynamoDbAdapter.test.ts
user: login: chris-cooksley-gds
body: Nitpick: not a server error
created_at: 2025-03-20T16:09:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -56,6 +80,40 @@ describe("Async TxMA Event", () => {
     });
   });
 
+  describe("Config validation", () => {
+    describe.each(["SESSION_TABLE_NAME"])(
path: backend-api/src/functions/asyncTxmaEvent/asyncTxmaEventHandler.test.ts
user: login: chris-cooksley-gds
body: Can we add the other two environment variables to this list?
created_at: 2025-03-20T16:11:22Z
author_association: CONTRIBUTOR
diff_hunk: @@ -121,3 +272,17 @@ const validRequest = buildRequest({
     eventName: "DCMAW_ASYNC_HYBRID_BILLING_STARTED",
   }),
 });
+
+export const mockTxmaEventSessionRegistrySuccess: SessionRegistry = {
+  updateSession: jest.fn().mockResolvedValue(
path: backend-api/src/functions/asyncTxmaEvent/asyncTxmaEventHandler.test.ts
user: login: chris-cooksley-gds
body: Nitpick: I think we could also just start with an inert session registry for this - this lambda won't need a working `updateSession` implementation, and this way we won't need to maintain one. I think the same goes for some of the other handler unit tests.
created_at: 2025-03-20T16:12:41Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,128 @@
+import { marshall, unmarshall } from "@aws-sdk/util-dynamodb";
+import {
+  invalidCreatedAt,
+  mockSessionId,
+  NOW_IN_MILLISECONDS,
+  validBiometricTokenIssuedSessionAttributes,
+  validCreatedAt,
+} from "../../../../testUtils/unitTestData";
+import {
+  emptyFailure,
+  emptySuccess,
+  errorResult,
+  successResult,
+} from "../../../../utils/result";
+import { TxMAEventGetSessionOperation } from "./TxmaEventGetSessionOperation";
+import { SessionState } from "../../session";
+
+describe("TxMA event", () => {
+  let getSessionOperation: TxMAEventGetSessionOperation;
+
+  beforeEach(() => {
+    jest.useFakeTimers();
+    jest.setSystemTime(NOW_IN_MILLISECONDS);
+    getSessionOperation = new TxMAEventGetSessionOperation();
+  });
+
+  describe("When I request the DynamoDB command input", () => {
+    it("Returns the appropriate GetItemCommand input", () => {
+      const result = getSessionOperation.getDynamoDbGetCommandInput({
+        tableName: "mock_table_name",
+        keyValue: mockSessionId,
+      });
+      expect(result).toEqual({
+        TableName: "mock_table_name",
+        Key: { sessionId: marshall(mockSessionId) },
path: backend-api/src/functions/common/session/getOperations/TxmaEvent/TxmaEventGetSessionOperation.test.ts
user: login: chris-cooksley-gds
body: Nitpick: might be better to assert on the literal object returned rather than use the `marshall` helper function (which is part of the source code implementation) here. Otherwise we never test that the marshall function is actually doing what we expect it to be doing.
created_at: 2025-03-20T16:13:54Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,128 @@
+import { marshall, unmarshall } from "@aws-sdk/util-dynamodb";
+import {
+  invalidCreatedAt,
+  mockSessionId,
+  NOW_IN_MILLISECONDS,
+  validBiometricTokenIssuedSessionAttributes,
+  validCreatedAt,
+} from "../../../../testUtils/unitTestData";
+import {
+  emptyFailure,
+  emptySuccess,
+  errorResult,
+  successResult,
+} from "../../../../utils/result";
+import { TxMAEventGetSessionOperation } from "./TxmaEventGetSessionOperation";
+import { SessionState } from "../../session";
+
+describe("TxMA event", () => {
+  let getSessionOperation: TxMAEventGetSessionOperation;
+
+  beforeEach(() => {
+    jest.useFakeTimers();
+    jest.setSystemTime(NOW_IN_MILLISECONDS);
+    getSessionOperation = new TxMAEventGetSessionOperation();
+  });
+
+  describe("When I request the DynamoDB command input", () => {
+    it("Returns the appropriate GetItemCommand input", () => {
+      const result = getSessionOperation.getDynamoDbGetCommandInput({
+        tableName: "mock_table_name",
+        keyValue: mockSessionId,
+      });
+      expect(result).toEqual({
+        TableName: "mock_table_name",
+        Key: { sessionId: marshall(mockSessionId) },
+      });
+    });
+  });
+
+  describe("When I request the getSessionAttributesFromDynamoDbItem", () => {
+    describe("Given a session attributes item was provided that does not include all BiometricTokenIssuedSessionAttributes properties", () => {
+      it("Returns an emptyFailure", () => {
+        const result = getSessionOperation.getSessionAttributesFromDynamoDbItem(
+          marshall({
+            sessionId: mockSessionId,
+          }),
+        );
+
+        expect(result).toEqual(emptyFailure());
+      });
+    });
+
+    describe("Given a valid BiometricTokenIssuedSessionAttributes item was provided", () => {
+      const validBiometricTokenIssuedSessionAttributesItem = marshall(
+        validBiometricTokenIssuedSessionAttributes,
+      );
+
+      it("Returns successResult with BiometricTokenIssuedSessionAttributes session attributes", () => {
+        const result = getSessionOperation.getSessionAttributesFromDynamoDbItem(
+          marshall(validBiometricTokenIssuedSessionAttributes),
+        );
+
+        expect(result).toEqual(
+          successResult(
+            unmarshall(validBiometricTokenIssuedSessionAttributesItem),
+          ),
+        );
+      });
+    });
+  });
+
+  describe("Session attribute validation", () => {
+    describe("Given the session is in the wrong state - sessionState = AUTH_SESSION_CREATED", () => {
+      const invalidSessionAttributesWrongSessionState = {
+        createdAt: validCreatedAt,
+        sessionState: SessionState.AUTH_SESSION_CREATED,
+      };
+
+      it("Return and error result with the invalid attribute", () => {
+        const result = getSessionOperation.validateSession(
+          invalidSessionAttributesWrongSessionState,
+        );
+
+        expect(result).toEqual(
+          errorResult({
+            invalidAttribute: {
+              sessionState: SessionState.AUTH_SESSION_CREATED,
+            },
+          }),
+        );
+      });
+    });
+
+    describe("Given the session is too old", () => {
+      const invalidSessionAttributesSessionTooOld = {
+        sessionState: SessionState.BIOMETRIC_TOKEN_ISSUED,
+        createdAt: invalidCreatedAt,
+      };
+
+      it("Return and error result with the invalid attribute", () => {
+        const result = getSessionOperation.validateSession(
+          invalidSessionAttributesSessionTooOld,
+        );
+
+        expect(result).toEqual(
+          errorResult({
+            invalidAttribute: { createdAt: invalidCreatedAt },
+          }),
+        );
+      });
+    });
+
+    describe("Given the session is valid", () => {
+      const validSessionAttributes = {
+        sessionState: SessionState.BIOMETRIC_TOKEN_ISSUED,
+        createdAt: validBiometricTokenIssuedSessionAttributes.createdAt,
+      };
+
+      it("Return and error result with the invalid attribute", () => {
path: backend-api/src/functions/common/session/getOperations/TxmaEvent/TxmaEventGetSessionOperation.test.ts
user: login: chris-cooksley-gds
body: Nitpick: test name needs updating, this is a happy path.
created_at: 2025-03-20T16:14:51Z
author_association: CONTRIBUTOR
diff_hunk: @@ -131,6 +143,67 @@ export class DynamoDbAdapter implements SessionRegistry {
     }
   }
 
+  async getSession(
+    sessionId: string,
+    getOperation: GetSessionOperation,
+  ): Promise<Result<SessionAttributes, GetSessionFailed>> {
+    const getItemCommandInput = getOperation.getDynamoDbGetCommandInput({
+      tableName: this.tableName,
+      keyValue: sessionId,
+    });
+
+    let response;
+    try {
+      logger.debug(LogMessage.GET_SESSION_ATTEMPT, {
+        data: { sessionId, getItemCommandInput },
+      });
+
+      response = await this.dynamoDbClient.send(
+        new GetItemCommand(getItemCommandInput),
+      );
+    } catch (error: unknown) {
+      return this.handleGetSessionInternalServerError({
+        error,
+        getItemCommandInput,
+      });
+    }
+
+    const responseItem = response.Item;
+    if (responseItem == null) {
+      return this.handleGetSessionNotFoundError({
+        errorMessage: sessionNotFound,
+      });
+    }
+
+    // Attribute type validation
+    const getSessionAttributesResult =
+      getOperation.getSessionAttributesFromDynamoDbItem(responseItem);
+    if (getSessionAttributesResult.isError) {
path: backend-api/src/functions/adapters/aws/dynamo/dynamoDbAdapter.ts
user: login: sandymay-dd
body: Hopefully I haven't got the wrong end of the stick:

If we have type/structural issues with our session record, we should return a server error.

If we have a value validation failure (e.g. session age), we return a 401 Unauthorized. 

I thought this was the pattern we'd implemented for the update operations 👀 
created_at: 2025-03-20T16:23:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -131,6 +143,67 @@ export class DynamoDbAdapter implements SessionRegistry {
     }
   }
 
+  async getSession(
+    sessionId: string,
+    getOperation: GetSessionOperation,
+  ): Promise<Result<SessionAttributes, GetSessionFailed>> {
+    const getItemCommandInput = getOperation.getDynamoDbGetCommandInput({
+      tableName: this.tableName,
+      keyValue: sessionId,
+    });
+
+    let response;
+    try {
+      logger.debug(LogMessage.GET_SESSION_ATTEMPT, {
+        data: { sessionId, getItemCommandInput },
+      });
+
+      response = await this.dynamoDbClient.send(
+        new GetItemCommand(getItemCommandInput),
+      );
+    } catch (error: unknown) {
+      return this.handleGetSessionInternalServerError({
+        error,
+        getItemCommandInput,
+      });
+    }
+
+    const responseItem = response.Item;
+    if (responseItem == null) {
+      return this.handleGetSessionNotFoundError({
+        errorMessage: sessionNotFound,
+      });
+    }
+
+    // Attribute type validation
+    const getSessionAttributesResult =
+      getOperation.getSessionAttributesFromDynamoDbItem(responseItem);
+    if (getSessionAttributesResult.isError) {
path: backend-api/src/functions/adapters/aws/dynamo/dynamoDbAdapter.ts
user: login: chris-cooksley-gds
body: That's the right end of the stick, unless I've also got the wrong end of the stick. So, currently, it looks like we're returning a 401 unauthorized for type/structural issues (which are surfaced here by `getSessionAttributesFromDynamoDbItem`). I propose that we instead return a server error in this scenario.
created_at: 2025-03-20T16:33:08Z
author_association: CONTRIBUTOR
diff_hunk: @@ -293,4 +309,234 @@ describe("DynamoDbAdapter", () => {
       });
     });
   });
+
+  describe("getSession", () => {
+    const getOperation: GetSessionOperation = new TxMAEvent();
+    let result: Result<void, SessionRetrievalFailed>;
+
+    describe("On every attempt", () => {
+      beforeEach(async () => {
+        mockDynamoDbClient.on(GetItemCommand).resolves({});
+        await sessionRegistry.getSession(mockSessionId, getOperation);
+      });
+
+      it("Logs the attempt", () => {
+        expect(consoleDebugSpy).toHaveBeenCalledWithLogFields({
+          messageCode: "MOBILE_ASYNC_GET_SESSION_ATTEMPT",
+          data: {
+            sessionId: mockSessionId,
+            getItemCommandInput:
+              getOperation.getDynamoDbKeyExpression(mockSessionId),
+          },
+        });
+      });
+    });
+
+    describe("Given there is an unexpected error retrieving the session", () => {
+      beforeEach(async () => {
+        mockDynamoDbClient.on(GetItemCommand).rejects("mock_error");
+        result = await sessionRegistry.getSession(mockSessionId, getOperation);
+      });
+
+      it("Logs the failure", () => {
+        expect(consoleErrorSpy).toHaveBeenCalledWithLogFields({
+          messageCode: "MOBILE_ASYNC_GET_SESSION_UNEXPECTED_FAILURE",
+        });
+      });
+
+      it("Returns failure with server error", () => {
+        expect(result).toEqual(
+          errorResult({
+            errorType: GetSessionError.INTERNAL_SERVER_ERROR,
+          }),
+        );
+      });
+    });
+
+    describe("Given the session was not found", () => {
+      beforeEach(async () => {
+        mockDynamoDbClient.on(GetItemCommand).resolves({});
+        result = await sessionRegistry.getSession(mockSessionId, getOperation);
+      });
+
+      it("Logs the failure", () => {
+        expect(consoleErrorSpy).toHaveBeenCalledWithLogFields({
+          messageCode: "MOBILE_ASYNC_GET_SESSION_SESSION_NOT_FOUND",
+          session: "Session not found",
+        });
+      });
+
+      it("Returns failure with an invalid session error", () => {
+        expect(result).toEqual(
+          errorResult({
+            errorType: GetSessionError.SESSION_NOT_FOUND,
+            session: "Session not found",
+          }),
+        );
+      });
+    });
+
+    describe("Session validation", () => {
+      describe("Given the session is in the wrong state", () => {
+        const invalidBiometricTokenIssuedSessionAttributesWrongSessionState = {
path: backend-api/src/functions/adapters/aws/dynamo/dynamoDbAdapter.test.ts
user: login: jhumbert-dd
body: Yeah, I planned to refactor test data. This file has been updated
created_at: 2025-03-20T16:44:48Z
author_association: CONTRIBUTOR
diff_hunk: @@ -293,4 +309,234 @@ describe("DynamoDbAdapter", () => {
       });
     });
   });
+
+  describe("getSession", () => {
+    const getOperation: GetSessionOperation = new TxMAEvent();
+    let result: Result<void, SessionRetrievalFailed>;
+
+    describe("On every attempt", () => {
+      beforeEach(async () => {
+        mockDynamoDbClient.on(GetItemCommand).resolves({});
+        await sessionRegistry.getSession(mockSessionId, getOperation);
+      });
+
+      it("Logs the attempt", () => {
+        expect(consoleDebugSpy).toHaveBeenCalledWithLogFields({
+          messageCode: "MOBILE_ASYNC_GET_SESSION_ATTEMPT",
+          data: {
+            sessionId: mockSessionId,
+            getItemCommandInput:
+              getOperation.getDynamoDbKeyExpression(mockSessionId),
+          },
+        });
+      });
+    });
+
+    describe("Given there is an unexpected error retrieving the session", () => {
+      beforeEach(async () => {
+        mockDynamoDbClient.on(GetItemCommand).rejects("mock_error");
+        result = await sessionRegistry.getSession(mockSessionId, getOperation);
+      });
+
+      it("Logs the failure", () => {
+        expect(consoleErrorSpy).toHaveBeenCalledWithLogFields({
+          messageCode: "MOBILE_ASYNC_GET_SESSION_UNEXPECTED_FAILURE",
+        });
+      });
+
+      it("Returns failure with server error", () => {
+        expect(result).toEqual(
+          errorResult({
+            errorType: GetSessionError.INTERNAL_SERVER_ERROR,
+          }),
+        );
+      });
+    });
+
+    describe("Given the session was not found", () => {
+      beforeEach(async () => {
+        mockDynamoDbClient.on(GetItemCommand).resolves({});
+        result = await sessionRegistry.getSession(mockSessionId, getOperation);
+      });
+
+      it("Logs the failure", () => {
+        expect(consoleErrorSpy).toHaveBeenCalledWithLogFields({
+          messageCode: "MOBILE_ASYNC_GET_SESSION_SESSION_NOT_FOUND",
+          session: "Session not found",
+        });
+      });
+
+      it("Returns failure with an invalid session error", () => {
+        expect(result).toEqual(
+          errorResult({
+            errorType: GetSessionError.SESSION_NOT_FOUND,
+            session: "Session not found",
+          }),
+        );
+      });
+    });
+
+    describe("Session validation", () => {
+      describe("Given the session is in the wrong state", () => {
+        const invalidBiometricTokenIssuedSessionAttributesWrongSessionState = {
+          ...validBiometricTokenIssuedSessionAttributes,
+          createdAt: 1704106860000, // 2024-01-01 11:01:00.000
+          sessionState: SessionState.AUTH_SESSION_CREATED,
+        };
+        const invalidBiometricTokenIssuedSessionAttributesWrongSessionStateItem =
+          {
+            Item: marshall(
+              invalidBiometricTokenIssuedSessionAttributesWrongSessionState,
+            ),
+          };
+
+        beforeEach(async () => {
+          mockDynamoDbClient
+            .on(GetItemCommand)
+            .resolves(
+              invalidBiometricTokenIssuedSessionAttributesWrongSessionStateItem,
+            );
+          result = await sessionRegistry.getSession(
+            mockSessionId,
+            getOperation,
+          );
+        });
+
+        it("Logs the failure", () => {
+          expect(consoleErrorSpy).toHaveBeenCalledWithLogFields({
+            messageCode: "MOBILE_ASYNC_GET_SESSION_SESSION_NOT_FOUND",
path: backend-api/src/functions/adapters/aws/dynamo/dynamoDbAdapter.test.ts
user: login: jhumbert-dd
body: updated
created_at: 2025-03-20T17:02:33Z
author_association: CONTRIBUTOR
diff_hunk: @@ -293,4 +309,234 @@ describe("DynamoDbAdapter", () => {
       });
     });
   });
+
+  describe("getSession", () => {
+    const getOperation: GetSessionOperation = new TxMAEvent();
+    let result: Result<void, SessionRetrievalFailed>;
+
+    describe("On every attempt", () => {
+      beforeEach(async () => {
+        mockDynamoDbClient.on(GetItemCommand).resolves({});
+        await sessionRegistry.getSession(mockSessionId, getOperation);
+      });
+
+      it("Logs the attempt", () => {
+        expect(consoleDebugSpy).toHaveBeenCalledWithLogFields({
+          messageCode: "MOBILE_ASYNC_GET_SESSION_ATTEMPT",
+          data: {
+            sessionId: mockSessionId,
+            getItemCommandInput:
+              getOperation.getDynamoDbKeyExpression(mockSessionId),
+          },
+        });
+      });
+    });
+
+    describe("Given there is an unexpected error retrieving the session", () => {
+      beforeEach(async () => {
+        mockDynamoDbClient.on(GetItemCommand).rejects("mock_error");
+        result = await sessionRegistry.getSession(mockSessionId, getOperation);
+      });
+
+      it("Logs the failure", () => {
+        expect(consoleErrorSpy).toHaveBeenCalledWithLogFields({
+          messageCode: "MOBILE_ASYNC_GET_SESSION_UNEXPECTED_FAILURE",
+        });
+      });
+
+      it("Returns failure with server error", () => {
+        expect(result).toEqual(
+          errorResult({
+            errorType: GetSessionError.INTERNAL_SERVER_ERROR,
+          }),
+        );
+      });
+    });
+
+    describe("Given the session was not found", () => {
+      beforeEach(async () => {
+        mockDynamoDbClient.on(GetItemCommand).resolves({});
+        result = await sessionRegistry.getSession(mockSessionId, getOperation);
+      });
+
+      it("Logs the failure", () => {
+        expect(consoleErrorSpy).toHaveBeenCalledWithLogFields({
+          messageCode: "MOBILE_ASYNC_GET_SESSION_SESSION_NOT_FOUND",
+          session: "Session not found",
+        });
+      });
+
+      it("Returns failure with an invalid session error", () => {
+        expect(result).toEqual(
+          errorResult({
+            errorType: GetSessionError.SESSION_NOT_FOUND,
+            session: "Session not found",
+          }),
+        );
+      });
+    });
+
+    describe("Session validation", () => {
+      describe("Given the session is in the wrong state", () => {
+        const invalidBiometricTokenIssuedSessionAttributesWrongSessionState = {
+          ...validBiometricTokenIssuedSessionAttributes,
+          createdAt: 1704106860000, // 2024-01-01 11:01:00.000
+          sessionState: SessionState.AUTH_SESSION_CREATED,
+        };
+        const invalidBiometricTokenIssuedSessionAttributesWrongSessionStateItem =
+          {
+            Item: marshall(
+              invalidBiometricTokenIssuedSessionAttributesWrongSessionState,
+            ),
+          };
+
+        beforeEach(async () => {
+          mockDynamoDbClient
+            .on(GetItemCommand)
+            .resolves(
+              invalidBiometricTokenIssuedSessionAttributesWrongSessionStateItem,
+            );
+          result = await sessionRegistry.getSession(
+            mockSessionId,
+            getOperation,
+          );
+        });
+
+        it("Logs the failure", () => {
+          expect(consoleErrorSpy).toHaveBeenCalledWithLogFields({
+            messageCode: "MOBILE_ASYNC_GET_SESSION_SESSION_NOT_FOUND",
+            errorMessage:
+              "Session could not be parsed or session validation failed",
path: backend-api/src/functions/adapters/aws/dynamo/dynamoDbAdapter.test.ts
user: login: jhumbert-dd
body: This now logs the invalid attribute and which will be adapted to handle multiple invalid attributes
created_at: 2025-03-20T17:06:40Z
author_association: CONTRIBUTOR
diff_hunk: @@ -293,4 +309,234 @@ describe("DynamoDbAdapter", () => {
       });
     });
   });
+
+  describe("getSession", () => {
+    const getOperation: GetSessionOperation = new TxMAEvent();
+    let result: Result<void, SessionRetrievalFailed>;
+
+    describe("On every attempt", () => {
+      beforeEach(async () => {
+        mockDynamoDbClient.on(GetItemCommand).resolves({});
+        await sessionRegistry.getSession(mockSessionId, getOperation);
+      });
+
+      it("Logs the attempt", () => {
+        expect(consoleDebugSpy).toHaveBeenCalledWithLogFields({
+          messageCode: "MOBILE_ASYNC_GET_SESSION_ATTEMPT",
+          data: {
+            sessionId: mockSessionId,
+            getItemCommandInput:
+              getOperation.getDynamoDbKeyExpression(mockSessionId),
+          },
+        });
+      });
+    });
+
+    describe("Given there is an unexpected error retrieving the session", () => {
+      beforeEach(async () => {
+        mockDynamoDbClient.on(GetItemCommand).rejects("mock_error");
+        result = await sessionRegistry.getSession(mockSessionId, getOperation);
+      });
+
+      it("Logs the failure", () => {
+        expect(consoleErrorSpy).toHaveBeenCalledWithLogFields({
+          messageCode: "MOBILE_ASYNC_GET_SESSION_UNEXPECTED_FAILURE",
+        });
+      });
+
+      it("Returns failure with server error", () => {
+        expect(result).toEqual(
+          errorResult({
+            errorType: GetSessionError.INTERNAL_SERVER_ERROR,
+          }),
+        );
+      });
+    });
+
+    describe("Given the session was not found", () => {
+      beforeEach(async () => {
+        mockDynamoDbClient.on(GetItemCommand).resolves({});
+        result = await sessionRegistry.getSession(mockSessionId, getOperation);
+      });
+
+      it("Logs the failure", () => {
+        expect(consoleErrorSpy).toHaveBeenCalledWithLogFields({
+          messageCode: "MOBILE_ASYNC_GET_SESSION_SESSION_NOT_FOUND",
+          session: "Session not found",
+        });
+      });
+
+      it("Returns failure with an invalid session error", () => {
+        expect(result).toEqual(
+          errorResult({
+            errorType: GetSessionError.SESSION_NOT_FOUND,
+            session: "Session not found",
+          }),
+        );
+      });
+    });
+
+    describe("Session validation", () => {
+      describe("Given the session is in the wrong state", () => {
+        const invalidBiometricTokenIssuedSessionAttributesWrongSessionState = {
+          ...validBiometricTokenIssuedSessionAttributes,
+          createdAt: 1704106860000, // 2024-01-01 11:01:00.000
+          sessionState: SessionState.AUTH_SESSION_CREATED,
+        };
+        const invalidBiometricTokenIssuedSessionAttributesWrongSessionStateItem =
+          {
+            Item: marshall(
+              invalidBiometricTokenIssuedSessionAttributesWrongSessionState,
+            ),
+          };
+
+        beforeEach(async () => {
+          mockDynamoDbClient
+            .on(GetItemCommand)
+            .resolves(
+              invalidBiometricTokenIssuedSessionAttributesWrongSessionStateItem,
+            );
+          result = await sessionRegistry.getSession(
+            mockSessionId,
+            getOperation,
+          );
+        });
+
+        it("Logs the failure", () => {
+          expect(consoleErrorSpy).toHaveBeenCalledWithLogFields({
+            messageCode: "MOBILE_ASYNC_GET_SESSION_SESSION_NOT_FOUND",
+            errorMessage:
+              "Session could not be parsed or session validation failed",
+            session:
+              invalidBiometricTokenIssuedSessionAttributesWrongSessionState,
+          });
+        });
+
+        it("Returns failure with an invalid session error", () => {
+          expect(result).toStrictEqual(
+            errorResult({
+              errorType: GetSessionError.SESSION_NOT_FOUND,
path: backend-api/src/functions/adapters/aws/dynamo/dynamoDbAdapter.test.ts
user: login: jhumbert-dd
body: updated
created_at: 2025-03-20T17:07:10Z
author_association: CONTRIBUTOR
diff_hunk: @@ -293,4 +309,234 @@ describe("DynamoDbAdapter", () => {
       });
     });
   });
+
+  describe("getSession", () => {
+    const getOperation: GetSessionOperation = new TxMAEvent();
+    let result: Result<void, SessionRetrievalFailed>;
+
+    describe("On every attempt", () => {
+      beforeEach(async () => {
+        mockDynamoDbClient.on(GetItemCommand).resolves({});
+        await sessionRegistry.getSession(mockSessionId, getOperation);
+      });
+
+      it("Logs the attempt", () => {
+        expect(consoleDebugSpy).toHaveBeenCalledWithLogFields({
+          messageCode: "MOBILE_ASYNC_GET_SESSION_ATTEMPT",
+          data: {
+            sessionId: mockSessionId,
+            getItemCommandInput:
+              getOperation.getDynamoDbKeyExpression(mockSessionId),
+          },
+        });
+      });
+    });
+
+    describe("Given there is an unexpected error retrieving the session", () => {
+      beforeEach(async () => {
+        mockDynamoDbClient.on(GetItemCommand).rejects("mock_error");
+        result = await sessionRegistry.getSession(mockSessionId, getOperation);
+      });
+
+      it("Logs the failure", () => {
+        expect(consoleErrorSpy).toHaveBeenCalledWithLogFields({
+          messageCode: "MOBILE_ASYNC_GET_SESSION_UNEXPECTED_FAILURE",
+        });
+      });
+
+      it("Returns failure with server error", () => {
+        expect(result).toEqual(
+          errorResult({
+            errorType: GetSessionError.INTERNAL_SERVER_ERROR,
+          }),
+        );
+      });
+    });
+
+    describe("Given the session was not found", () => {
+      beforeEach(async () => {
+        mockDynamoDbClient.on(GetItemCommand).resolves({});
+        result = await sessionRegistry.getSession(mockSessionId, getOperation);
+      });
+
+      it("Logs the failure", () => {
+        expect(consoleErrorSpy).toHaveBeenCalledWithLogFields({
+          messageCode: "MOBILE_ASYNC_GET_SESSION_SESSION_NOT_FOUND",
+          session: "Session not found",
+        });
+      });
+
+      it("Returns failure with an invalid session error", () => {
+        expect(result).toEqual(
+          errorResult({
+            errorType: GetSessionError.SESSION_NOT_FOUND,
+            session: "Session not found",
+          }),
+        );
+      });
+    });
+
+    describe("Session validation", () => {
+      describe("Given the session is in the wrong state", () => {
+        const invalidBiometricTokenIssuedSessionAttributesWrongSessionState = {
+          ...validBiometricTokenIssuedSessionAttributes,
+          createdAt: 1704106860000, // 2024-01-01 11:01:00.000
+          sessionState: SessionState.AUTH_SESSION_CREATED,
+        };
+        const invalidBiometricTokenIssuedSessionAttributesWrongSessionStateItem =
+          {
+            Item: marshall(
+              invalidBiometricTokenIssuedSessionAttributesWrongSessionState,
+            ),
+          };
+
+        beforeEach(async () => {
+          mockDynamoDbClient
+            .on(GetItemCommand)
+            .resolves(
+              invalidBiometricTokenIssuedSessionAttributesWrongSessionStateItem,
+            );
+          result = await sessionRegistry.getSession(
+            mockSessionId,
+            getOperation,
+          );
+        });
+
+        it("Logs the failure", () => {
+          expect(consoleErrorSpy).toHaveBeenCalledWithLogFields({
+            messageCode: "MOBILE_ASYNC_GET_SESSION_SESSION_NOT_FOUND",
+            errorMessage:
+              "Session could not be parsed or session validation failed",
+            session:
+              invalidBiometricTokenIssuedSessionAttributesWrongSessionState,
+          });
+        });
+
+        it("Returns failure with an invalid session error", () => {
+          expect(result).toStrictEqual(
+            errorResult({
+              errorType: GetSessionError.SESSION_NOT_FOUND,
+              session:
+                invalidBiometricTokenIssuedSessionAttributesWrongSessionState,
+            }),
+          );
+        });
+      });
+
+      describe("Given the session is more than 60 minutes old", () => {
+        const invalidBiometricTokenIssuedSessionAttributesSessionTooOld = {
+          ...validBiometricTokenIssuedSessionAttributes,
+          createdAt: 1704106740000, // 2024-01-01 10:59:00.000
+        };
+        const biometricTokenIssuedSessionAttributesSessionTooOldItem = {
+          Item: marshall(
+            invalidBiometricTokenIssuedSessionAttributesSessionTooOld,
+          ),
+        };
+
+        beforeEach(async () => {
+          mockDynamoDbClient
+            .on(GetItemCommand)
+            .resolves(biometricTokenIssuedSessionAttributesSessionTooOldItem);
+          result = await sessionRegistry.getSession(
+            mockSessionId,
+            getOperation,
+          );
+        });
+
+        it("Logs the failure", () => {
+          expect(consoleErrorSpy).toHaveBeenCalledWithLogFields({
+            messageCode: "MOBILE_ASYNC_GET_SESSION_SESSION_NOT_FOUND",
path: backend-api/src/functions/adapters/aws/dynamo/dynamoDbAdapter.test.ts
user: login: jhumbert-dd
body: This test case has been removed as validation branching is tested in the getOperation class
created_at: 2025-03-20T17:08:58Z
author_association: CONTRIBUTOR
diff_hunk: @@ -293,4 +309,234 @@ describe("DynamoDbAdapter", () => {
       });
     });
   });
+
+  describe("getSession", () => {
+    const getOperation: GetSessionOperation = new TxMAEvent();
+    let result: Result<void, SessionRetrievalFailed>;
+
+    describe("On every attempt", () => {
+      beforeEach(async () => {
+        mockDynamoDbClient.on(GetItemCommand).resolves({});
+        await sessionRegistry.getSession(mockSessionId, getOperation);
+      });
+
+      it("Logs the attempt", () => {
+        expect(consoleDebugSpy).toHaveBeenCalledWithLogFields({
+          messageCode: "MOBILE_ASYNC_GET_SESSION_ATTEMPT",
+          data: {
+            sessionId: mockSessionId,
+            getItemCommandInput:
+              getOperation.getDynamoDbKeyExpression(mockSessionId),
+          },
+        });
+      });
+    });
+
+    describe("Given there is an unexpected error retrieving the session", () => {
+      beforeEach(async () => {
+        mockDynamoDbClient.on(GetItemCommand).rejects("mock_error");
+        result = await sessionRegistry.getSession(mockSessionId, getOperation);
+      });
+
+      it("Logs the failure", () => {
+        expect(consoleErrorSpy).toHaveBeenCalledWithLogFields({
+          messageCode: "MOBILE_ASYNC_GET_SESSION_UNEXPECTED_FAILURE",
+        });
+      });
+
+      it("Returns failure with server error", () => {
+        expect(result).toEqual(
+          errorResult({
+            errorType: GetSessionError.INTERNAL_SERVER_ERROR,
+          }),
+        );
+      });
+    });
+
+    describe("Given the session was not found", () => {
+      beforeEach(async () => {
+        mockDynamoDbClient.on(GetItemCommand).resolves({});
+        result = await sessionRegistry.getSession(mockSessionId, getOperation);
+      });
+
+      it("Logs the failure", () => {
+        expect(consoleErrorSpy).toHaveBeenCalledWithLogFields({
+          messageCode: "MOBILE_ASYNC_GET_SESSION_SESSION_NOT_FOUND",
+          session: "Session not found",
+        });
+      });
+
+      it("Returns failure with an invalid session error", () => {
+        expect(result).toEqual(
+          errorResult({
+            errorType: GetSessionError.SESSION_NOT_FOUND,
+            session: "Session not found",
+          }),
+        );
+      });
+    });
+
+    describe("Session validation", () => {
+      describe("Given the session is in the wrong state", () => {
+        const invalidBiometricTokenIssuedSessionAttributesWrongSessionState = {
+          ...validBiometricTokenIssuedSessionAttributes,
+          createdAt: 1704106860000, // 2024-01-01 11:01:00.000
+          sessionState: SessionState.AUTH_SESSION_CREATED,
+        };
+        const invalidBiometricTokenIssuedSessionAttributesWrongSessionStateItem =
+          {
+            Item: marshall(
+              invalidBiometricTokenIssuedSessionAttributesWrongSessionState,
+            ),
+          };
+
+        beforeEach(async () => {
+          mockDynamoDbClient
+            .on(GetItemCommand)
+            .resolves(
+              invalidBiometricTokenIssuedSessionAttributesWrongSessionStateItem,
+            );
+          result = await sessionRegistry.getSession(
+            mockSessionId,
+            getOperation,
+          );
+        });
+
+        it("Logs the failure", () => {
+          expect(consoleErrorSpy).toHaveBeenCalledWithLogFields({
+            messageCode: "MOBILE_ASYNC_GET_SESSION_SESSION_NOT_FOUND",
+            errorMessage:
+              "Session could not be parsed or session validation failed",
+            session:
+              invalidBiometricTokenIssuedSessionAttributesWrongSessionState,
+          });
+        });
+
+        it("Returns failure with an invalid session error", () => {
+          expect(result).toStrictEqual(
+            errorResult({
+              errorType: GetSessionError.SESSION_NOT_FOUND,
+              session:
+                invalidBiometricTokenIssuedSessionAttributesWrongSessionState,
+            }),
+          );
+        });
+      });
+
+      describe("Given the session is more than 60 minutes old", () => {
+        const invalidBiometricTokenIssuedSessionAttributesSessionTooOld = {
+          ...validBiometricTokenIssuedSessionAttributes,
+          createdAt: 1704106740000, // 2024-01-01 10:59:00.000
+        };
+        const biometricTokenIssuedSessionAttributesSessionTooOldItem = {
+          Item: marshall(
+            invalidBiometricTokenIssuedSessionAttributesSessionTooOld,
+          ),
+        };
+
+        beforeEach(async () => {
+          mockDynamoDbClient
+            .on(GetItemCommand)
+            .resolves(biometricTokenIssuedSessionAttributesSessionTooOldItem);
+          result = await sessionRegistry.getSession(
+            mockSessionId,
+            getOperation,
+          );
+        });
+
+        it("Logs the failure", () => {
+          expect(consoleErrorSpy).toHaveBeenCalledWithLogFields({
+            messageCode: "MOBILE_ASYNC_GET_SESSION_SESSION_NOT_FOUND",
+            session: invalidBiometricTokenIssuedSessionAttributesSessionTooOld,
+          });
+        });
+
+        it("Returns failure with an invalid session error", () => {
+          expect(result).toStrictEqual(
+            errorResult({
+              errorType: GetSessionError.SESSION_NOT_FOUND,
path: backend-api/src/functions/adapters/aws/dynamo/dynamoDbAdapter.test.ts
user: login: jhumbert-dd
body: [related response](https://github.com/govuk-one-login/mobile-id-check-async/pull/469/files/aa3c8a73b3513d50fe4bd5d8a6abc6392f461a9a#r2006100725)
created_at: 2025-03-20T17:10:23Z
author_association: CONTRIBUTOR
diff_hunk: @@ -293,4 +309,234 @@ describe("DynamoDbAdapter", () => {
       });
     });
   });
+
+  describe("getSession", () => {
+    const getOperation: GetSessionOperation = new TxMAEvent();
+    let result: Result<void, SessionRetrievalFailed>;
+
+    describe("On every attempt", () => {
+      beforeEach(async () => {
+        mockDynamoDbClient.on(GetItemCommand).resolves({});
+        await sessionRegistry.getSession(mockSessionId, getOperation);
+      });
+
+      it("Logs the attempt", () => {
+        expect(consoleDebugSpy).toHaveBeenCalledWithLogFields({
+          messageCode: "MOBILE_ASYNC_GET_SESSION_ATTEMPT",
+          data: {
+            sessionId: mockSessionId,
+            getItemCommandInput:
+              getOperation.getDynamoDbKeyExpression(mockSessionId),
+          },
+        });
+      });
+    });
+
+    describe("Given there is an unexpected error retrieving the session", () => {
+      beforeEach(async () => {
+        mockDynamoDbClient.on(GetItemCommand).rejects("mock_error");
+        result = await sessionRegistry.getSession(mockSessionId, getOperation);
+      });
+
+      it("Logs the failure", () => {
+        expect(consoleErrorSpy).toHaveBeenCalledWithLogFields({
+          messageCode: "MOBILE_ASYNC_GET_SESSION_UNEXPECTED_FAILURE",
+        });
+      });
+
+      it("Returns failure with server error", () => {
+        expect(result).toEqual(
+          errorResult({
+            errorType: GetSessionError.INTERNAL_SERVER_ERROR,
+          }),
+        );
+      });
+    });
+
+    describe("Given the session was not found", () => {
+      beforeEach(async () => {
+        mockDynamoDbClient.on(GetItemCommand).resolves({});
+        result = await sessionRegistry.getSession(mockSessionId, getOperation);
+      });
+
+      it("Logs the failure", () => {
+        expect(consoleErrorSpy).toHaveBeenCalledWithLogFields({
+          messageCode: "MOBILE_ASYNC_GET_SESSION_SESSION_NOT_FOUND",
+          session: "Session not found",
+        });
+      });
+
+      it("Returns failure with an invalid session error", () => {
+        expect(result).toEqual(
+          errorResult({
+            errorType: GetSessionError.SESSION_NOT_FOUND,
+            session: "Session not found",
+          }),
+        );
+      });
+    });
+
+    describe("Session validation", () => {
+      describe("Given the session is in the wrong state", () => {
+        const invalidBiometricTokenIssuedSessionAttributesWrongSessionState = {
path: backend-api/src/functions/adapters/aws/dynamo/dynamoDbAdapter.test.ts
user: login: jhumbert-dd
body: updated
created_at: 2025-03-21T09:25:28Z
author_association: CONTRIBUTOR
diff_hunk: @@ -293,4 +309,234 @@ describe("DynamoDbAdapter", () => {
       });
     });
   });
+
+  describe("getSession", () => {
+    const getOperation: GetSessionOperation = new TxMAEvent();
+    let result: Result<void, SessionRetrievalFailed>;
+
+    describe("On every attempt", () => {
+      beforeEach(async () => {
+        mockDynamoDbClient.on(GetItemCommand).resolves({});
+        await sessionRegistry.getSession(mockSessionId, getOperation);
+      });
+
+      it("Logs the attempt", () => {
+        expect(consoleDebugSpy).toHaveBeenCalledWithLogFields({
+          messageCode: "MOBILE_ASYNC_GET_SESSION_ATTEMPT",
+          data: {
+            sessionId: mockSessionId,
+            getItemCommandInput:
+              getOperation.getDynamoDbKeyExpression(mockSessionId),
+          },
+        });
+      });
+    });
+
+    describe("Given there is an unexpected error retrieving the session", () => {
+      beforeEach(async () => {
+        mockDynamoDbClient.on(GetItemCommand).rejects("mock_error");
+        result = await sessionRegistry.getSession(mockSessionId, getOperation);
+      });
+
+      it("Logs the failure", () => {
+        expect(consoleErrorSpy).toHaveBeenCalledWithLogFields({
+          messageCode: "MOBILE_ASYNC_GET_SESSION_UNEXPECTED_FAILURE",
+        });
+      });
+
+      it("Returns failure with server error", () => {
+        expect(result).toEqual(
+          errorResult({
+            errorType: GetSessionError.INTERNAL_SERVER_ERROR,
+          }),
+        );
+      });
+    });
+
+    describe("Given the session was not found", () => {
+      beforeEach(async () => {
+        mockDynamoDbClient.on(GetItemCommand).resolves({});
+        result = await sessionRegistry.getSession(mockSessionId, getOperation);
+      });
+
+      it("Logs the failure", () => {
+        expect(consoleErrorSpy).toHaveBeenCalledWithLogFields({
+          messageCode: "MOBILE_ASYNC_GET_SESSION_SESSION_NOT_FOUND",
+          session: "Session not found",
+        });
+      });
+
+      it("Returns failure with an invalid session error", () => {
+        expect(result).toEqual(
+          errorResult({
+            errorType: GetSessionError.SESSION_NOT_FOUND,
+            session: "Session not found",
+          }),
+        );
+      });
+    });
+
+    describe("Session validation", () => {
+      describe("Given the session is in the wrong state", () => {
+        const invalidBiometricTokenIssuedSessionAttributesWrongSessionState = {
+          ...validBiometricTokenIssuedSessionAttributes,
+          createdAt: 1704106860000, // 2024-01-01 11:01:00.000
+          sessionState: SessionState.AUTH_SESSION_CREATED,
+        };
+        const invalidBiometricTokenIssuedSessionAttributesWrongSessionStateItem =
+          {
+            Item: marshall(
+              invalidBiometricTokenIssuedSessionAttributesWrongSessionState,
+            ),
+          };
+
+        beforeEach(async () => {
+          mockDynamoDbClient
+            .on(GetItemCommand)
+            .resolves(
+              invalidBiometricTokenIssuedSessionAttributesWrongSessionStateItem,
+            );
+          result = await sessionRegistry.getSession(
+            mockSessionId,
+            getOperation,
+          );
+        });
+
+        it("Logs the failure", () => {
+          expect(consoleErrorSpy).toHaveBeenCalledWithLogFields({
+            messageCode: "MOBILE_ASYNC_GET_SESSION_SESSION_NOT_FOUND",
+            errorMessage:
+              "Session could not be parsed or session validation failed",
+            session:
+              invalidBiometricTokenIssuedSessionAttributesWrongSessionState,
+          });
+        });
+
+        it("Returns failure with an invalid session error", () => {
+          expect(result).toStrictEqual(
+            errorResult({
+              errorType: GetSessionError.SESSION_NOT_FOUND,
+              session:
+                invalidBiometricTokenIssuedSessionAttributesWrongSessionState,
+            }),
+          );
+        });
+      });
+
+      describe("Given the session is more than 60 minutes old", () => {
+        const invalidBiometricTokenIssuedSessionAttributesSessionTooOld = {
+          ...validBiometricTokenIssuedSessionAttributes,
+          createdAt: 1704106740000, // 2024-01-01 10:59:00.000
+        };
+        const biometricTokenIssuedSessionAttributesSessionTooOldItem = {
+          Item: marshall(
+            invalidBiometricTokenIssuedSessionAttributesSessionTooOld,
+          ),
+        };
+
+        beforeEach(async () => {
+          mockDynamoDbClient
+            .on(GetItemCommand)
+            .resolves(biometricTokenIssuedSessionAttributesSessionTooOldItem);
+          result = await sessionRegistry.getSession(
+            mockSessionId,
+            getOperation,
+          );
+        });
+
+        it("Logs the failure", () => {
+          expect(consoleErrorSpy).toHaveBeenCalledWithLogFields({
+            messageCode: "MOBILE_ASYNC_GET_SESSION_SESSION_NOT_FOUND",
+            session: invalidBiometricTokenIssuedSessionAttributesSessionTooOld,
+          });
+        });
+
+        it("Returns failure with an invalid session error", () => {
+          expect(result).toStrictEqual(
+            errorResult({
+              errorType: GetSessionError.SESSION_NOT_FOUND,
+              session:
+                invalidBiometricTokenIssuedSessionAttributesSessionTooOld,
+            }),
+          );
+        });
+      });
+    });
+
+    describe("Given the session was found", () => {
path: backend-api/src/functions/adapters/aws/dynamo/dynamoDbAdapter.test.ts
user: login: jhumbert-dd
body: updated
created_at: 2025-03-21T09:26:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -293,4 +309,234 @@ describe("DynamoDbAdapter", () => {
       });
     });
   });
+
+  describe("getSession", () => {
+    const getOperation: GetSessionOperation = new TxMAEvent();
+    let result: Result<void, SessionRetrievalFailed>;
+
+    describe("On every attempt", () => {
+      beforeEach(async () => {
+        mockDynamoDbClient.on(GetItemCommand).resolves({});
+        await sessionRegistry.getSession(mockSessionId, getOperation);
+      });
+
+      it("Logs the attempt", () => {
+        expect(consoleDebugSpy).toHaveBeenCalledWithLogFields({
+          messageCode: "MOBILE_ASYNC_GET_SESSION_ATTEMPT",
+          data: {
+            sessionId: mockSessionId,
+            getItemCommandInput:
+              getOperation.getDynamoDbKeyExpression(mockSessionId),
+          },
+        });
+      });
+    });
+
+    describe("Given there is an unexpected error retrieving the session", () => {
+      beforeEach(async () => {
+        mockDynamoDbClient.on(GetItemCommand).rejects("mock_error");
+        result = await sessionRegistry.getSession(mockSessionId, getOperation);
+      });
+
+      it("Logs the failure", () => {
+        expect(consoleErrorSpy).toHaveBeenCalledWithLogFields({
+          messageCode: "MOBILE_ASYNC_GET_SESSION_UNEXPECTED_FAILURE",
+        });
+      });
+
+      it("Returns failure with server error", () => {
+        expect(result).toEqual(
+          errorResult({
+            errorType: GetSessionError.INTERNAL_SERVER_ERROR,
+          }),
+        );
+      });
+    });
+
+    describe("Given the session was not found", () => {
+      beforeEach(async () => {
+        mockDynamoDbClient.on(GetItemCommand).resolves({});
+        result = await sessionRegistry.getSession(mockSessionId, getOperation);
+      });
+
+      it("Logs the failure", () => {
+        expect(consoleErrorSpy).toHaveBeenCalledWithLogFields({
+          messageCode: "MOBILE_ASYNC_GET_SESSION_SESSION_NOT_FOUND",
+          session: "Session not found",
+        });
+      });
+
+      it("Returns failure with an invalid session error", () => {
+        expect(result).toEqual(
+          errorResult({
+            errorType: GetSessionError.SESSION_NOT_FOUND,
+            session: "Session not found",
+          }),
+        );
+      });
+    });
+
+    describe("Session validation", () => {
+      describe("Given the session is in the wrong state", () => {
+        const invalidBiometricTokenIssuedSessionAttributesWrongSessionState = {
+          ...validBiometricTokenIssuedSessionAttributes,
+          createdAt: 1704106860000, // 2024-01-01 11:01:00.000
+          sessionState: SessionState.AUTH_SESSION_CREATED,
+        };
+        const invalidBiometricTokenIssuedSessionAttributesWrongSessionStateItem =
+          {
+            Item: marshall(
+              invalidBiometricTokenIssuedSessionAttributesWrongSessionState,
+            ),
+          };
+
+        beforeEach(async () => {
+          mockDynamoDbClient
+            .on(GetItemCommand)
+            .resolves(
+              invalidBiometricTokenIssuedSessionAttributesWrongSessionStateItem,
+            );
+          result = await sessionRegistry.getSession(
+            mockSessionId,
+            getOperation,
+          );
+        });
+
+        it("Logs the failure", () => {
+          expect(consoleErrorSpy).toHaveBeenCalledWithLogFields({
+            messageCode: "MOBILE_ASYNC_GET_SESSION_SESSION_NOT_FOUND",
+            errorMessage:
+              "Session could not be parsed or session validation failed",
+            session:
+              invalidBiometricTokenIssuedSessionAttributesWrongSessionState,
+          });
+        });
+
+        it("Returns failure with an invalid session error", () => {
+          expect(result).toStrictEqual(
+            errorResult({
+              errorType: GetSessionError.SESSION_NOT_FOUND,
+              session:
+                invalidBiometricTokenIssuedSessionAttributesWrongSessionState,
+            }),
+          );
+        });
+      });
+
+      describe("Given the session is more than 60 minutes old", () => {
+        const invalidBiometricTokenIssuedSessionAttributesSessionTooOld = {
+          ...validBiometricTokenIssuedSessionAttributes,
+          createdAt: 1704106740000, // 2024-01-01 10:59:00.000
+        };
+        const biometricTokenIssuedSessionAttributesSessionTooOldItem = {
+          Item: marshall(
+            invalidBiometricTokenIssuedSessionAttributesSessionTooOld,
+          ),
+        };
+
+        beforeEach(async () => {
+          mockDynamoDbClient
+            .on(GetItemCommand)
+            .resolves(biometricTokenIssuedSessionAttributesSessionTooOldItem);
+          result = await sessionRegistry.getSession(
+            mockSessionId,
+            getOperation,
+          );
+        });
+
+        it("Logs the failure", () => {
+          expect(consoleErrorSpy).toHaveBeenCalledWithLogFields({
+            messageCode: "MOBILE_ASYNC_GET_SESSION_SESSION_NOT_FOUND",
+            session: invalidBiometricTokenIssuedSessionAttributesSessionTooOld,
+          });
+        });
+
+        it("Returns failure with an invalid session error", () => {
+          expect(result).toStrictEqual(
+            errorResult({
+              errorType: GetSessionError.SESSION_NOT_FOUND,
+              session:
+                invalidBiometricTokenIssuedSessionAttributesSessionTooOld,
+            }),
+          );
+        });
+      });
+    });
+
+    describe("Given the session was found", () => {
+      describe("Given invalid session attributes were returned in response", () => {
+        const invalidBiometricTokenIssuedSessionAttributesMissingAttributes = {
+          clientId: "mockClientId",
+          govukSigninJourneyId: "mockGovukSigninJourneyId",
+          createdAt: 1704106860000, // 2024-01-01 11:01:00.000
+          issuer: "mockIssuer",
+          clientState: "mockClientState",
+          subjectIdentifier: "mockSubjectIdentifier",
+          timeToLive: 12345,
+        };
+        beforeEach(async () => {
+          mockDynamoDbClient.on(GetItemCommand).resolves({
+            Item: marshall(
+              invalidBiometricTokenIssuedSessionAttributesMissingAttributes,
+            ),
+          });
+          result = await sessionRegistry.getSession(
+            mockSessionId,
+            getOperation,
+          );
+        });
+
+        it("Logs the failure", () => {
+          expect(consoleErrorSpy).toHaveBeenCalledWithLogFields({
+            messageCode: "MOBILE_ASYNC_GET_SESSION_SESSION_NOT_FOUND",
+            errorMessage:
+              "Session could not be parsed or session validation failed",
+            session:
+              invalidBiometricTokenIssuedSessionAttributesMissingAttributes,
+          });
+        });
+
+        it("Returns failure with server error", () => {
+          expect(result).toEqual(
+            errorResult({
+              errorType: GetSessionError.SESSION_NOT_FOUND,
+              session:
+                invalidBiometricTokenIssuedSessionAttributesMissingAttributes,
+            }),
+          );
+        });
+      });
+
+      describe("Given valid session attributes were returned in response", () => {
+        const validBiometricTokenIssuedSessionAttributesItem = marshall({
+          ...validBiometricTokenIssuedSessionAttributes,
+          createdAt: 1704106860000, // 2024-01-01 11:01:00.000
path: backend-api/src/functions/adapters/aws/dynamo/dynamoDbAdapter.test.ts
user: login: jhumbert-dd
body: updated
created_at: 2025-03-21T09:26:25Z
author_association: CONTRIBUTOR
diff_hunk: @@ -58,3 +43,68 @@ export const validBiometricSessionFinishedAttributes = {
   sessionState: SessionState.BIOMETRIC_SESSION_FINISHED,
   biometricSessionId: mockBiometricSessionId,
 };
+
+export const validBiometricTokenIssuedSessionAttributesMobileApp = {
+  ...validBiometricSessionFinishedAttributes,
+  redirectUri: "https://www.mockRedirectUri.com",
+};
+
+export const mockInertSessionRegistry: SessionRegistry = {
+  updateSession: jest.fn(() => {
+    throw new Error("Not implemented");
+  }),
+
+  getSession: jest.fn(() => {
+    throw new Error("Not implemented");
+  }),
+};
+
+export const mockSuccessfulSessionRegistry: SessionRegistry = {
path: backend-api/src/functions/testUtils/unitTestData.ts
user: login: jhumbert-dd
body: Update to each test suite using its own successful session registry
created_at: 2025-03-21T09:52:40Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,55 @@
+import { AttributeValue, GetItemCommandInput } from "@aws-sdk/client-dynamodb";
+import { marshall } from "@aws-sdk/util-dynamodb";
+import { emptySuccess, errorResult, Result } from "../../../../utils/result";
+import {
+  isOlderThan60minutes,
+  SessionAttributes,
+  SessionState,
+} from "../../session";
+import { getBiometricTokenIssuedSessionAttributes } from "../../sessionAttributes/sessionAttributes";
+import { GetSessionOperation } from "../GetSessionOperation";
+import {
+  ValidateSessionErrorInvalidAttributeData,
+  ValidateSessionAttributes,
+} from "../../SessionRegistry";
+
+export class TxMAEventGetSessionOperation implements GetSessionOperation {
path: backend-api/src/functions/common/session/getOperations/TxmaEvent/TxmaEventGetSessionOperation.ts
user: login: jhumbert-dd
body: Renamed to `GetSessionBiometricTokenIssued`
created_at: 2025-03-21T10:09:11Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,55 @@
+import { AttributeValue, GetItemCommandInput } from "@aws-sdk/client-dynamodb";
+import { marshall } from "@aws-sdk/util-dynamodb";
+import { emptySuccess, errorResult, Result } from "../../../../utils/result";
+import {
+  isOlderThan60minutes,
+  SessionAttributes,
+  SessionState,
+} from "../../session";
+import { getBiometricTokenIssuedSessionAttributes } from "../../sessionAttributes/sessionAttributes";
+import { GetSessionOperation } from "../GetSessionOperation";
+import {
+  ValidateSessionErrorInvalidAttributeData,
+  ValidateSessionAttributes,
+} from "../../SessionRegistry";
+
+export class TxMAEventGetSessionOperation implements GetSessionOperation {
+  getDynamoDbGetCommandInput({
+    tableName,
+    keyValue,
+  }: {
+    tableName: string;
+    keyValue: string;
+  }): GetItemCommandInput {
+    return {
+      TableName: tableName,
+      Key: { sessionId: marshall(keyValue) },
+    };
+  }
+
+  getSessionAttributesFromDynamoDbItem(
+    item: Record<string, AttributeValue> | undefined,
+  ): Result<SessionAttributes, void> {
+    return getBiometricTokenIssuedSessionAttributes(item);
+  }
+
+  validateSession(
+    attributes: ValidateSessionAttributes,
+  ): Result<void, ValidateSessionErrorInvalidAttributeData> {
+    const { sessionState, createdAt } = attributes;
+
+    if (!sessionState || sessionState !== SessionState.BIOMETRIC_TOKEN_ISSUED) {
+      return errorResult({
+        invalidAttribute: { sessionState },
path: backend-api/src/functions/common/session/getOperations/TxmaEvent/TxmaEventGetSessionOperation.ts
user: login: jhumbert-dd
body: I've updated to return a single object containing all invalid attributes as I think it would read better in the logs. Is there a benefit to returning an array of objects which each have one invalid attribute inside? If so, it wouldn't be much effort to switch methods. cc @sandymay-dd
created_at: 2025-03-21T10:14:01Z
author_association: CONTRIBUTOR
diff_hunk: @@ -131,6 +143,67 @@ export class DynamoDbAdapter implements SessionRegistry {
     }
   }
 
+  async getSession(
+    sessionId: string,
+    getOperation: GetSessionOperation,
+  ): Promise<Result<SessionAttributes, GetSessionFailed>> {
+    const getItemCommandInput = getOperation.getDynamoDbGetCommandInput({
+      tableName: this.tableName,
+      keyValue: sessionId,
+    });
+
+    let response;
+    try {
+      logger.debug(LogMessage.GET_SESSION_ATTEMPT, {
+        data: { sessionId, getItemCommandInput },
+      });
+
+      response = await this.dynamoDbClient.send(
+        new GetItemCommand(getItemCommandInput),
+      );
+    } catch (error: unknown) {
+      return this.handleGetSessionInternalServerError({
+        error,
+        getItemCommandInput,
+      });
+    }
+
+    const responseItem = response.Item;
+    if (responseItem == null) {
+      return this.handleGetSessionNotFoundError({
+        errorMessage: sessionNotFound,
+      });
+    }
+
+    // Attribute type validation
+    const getSessionAttributesResult =
+      getOperation.getSessionAttributesFromDynamoDbItem(responseItem);
+    if (getSessionAttributesResult.isError) {
path: backend-api/src/functions/adapters/aws/dynamo/dynamoDbAdapter.ts
user: login: jhumbert-dd
body: Updated
created_at: 2025-03-21T10:57:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -13,28 +13,34 @@ export type SessionAttributes =
 
 export interface BaseSessionAttributes {
   clientId: string;
-  govukSigninJourneyId: string;
+  clientState: string;
   createdAt: number;
+  govukSigninJourneyId: string;
   issuer: string;
   sessionId: string;
   sessionState: SessionState;
-  clientState: string;
   subjectIdentifier: string;
   timeToLive: number;
   redirectUri?: string;
 }
 
 export interface BiometricTokenIssuedSessionAttributes
   extends BaseSessionAttributes {
-  sessionState: SessionState.BIOMETRIC_TOKEN_ISSUED;
   documentType: DocumentType;
   opaqueId: string;
+  sessionState: SessionState.BIOMETRIC_TOKEN_ISSUED;
 }
 
 export interface BiometricSessionFinishedAttributes
   extends BaseSessionAttributes {
-  sessionState: SessionState.BIOMETRIC_SESSION_FINISHED;
   documentType: DocumentType;
   opaqueId: string;
+  sessionState: SessionState.BIOMETRIC_SESSION_FINISHED;
   biometricSessionId: string;
 }
+
+export function isOlderThan60minutes(createdAtInMilliseconds: number) {
path: backend-api/src/functions/common/session/session.ts
user: login: jhumbert-dd
body: Update. Moved function to the main utils file as I didn't think it needed it's own file anywhere else. But, I'm happy to move it again if there is a better place for it
created_at: 2025-03-21T11:06:00Z
author_association: CONTRIBUTOR
diff_hunk: @@ -225,14 +312,65 @@ export class DynamoDbAdapter implements SessionRegistry {
 
   private handleUpdateSessionInternalServerError(
     error: unknown,
-    updateExpressionDataToLog: UpdateExpressionDataToLog,
+    data: UpdateOperationDataToLog,
   ): FailureWithValue<SessionUpdateFailedInternalServerError> {
     logger.error(LogMessage.UPDATE_SESSION_UNEXPECTED_FAILURE, {
-      error: error,
-      data: updateExpressionDataToLog,
+      error,
+      data,
     });
     return errorResult({
       errorType: UpdateSessionError.INTERNAL_SERVER_ERROR,
     });
   }
+
+  private handleGetSessionInternalServerError({
+    error,
+    getItemCommandInput,
+  }: {
+    error: unknown;
+    getItemCommandInput: GetItemCommandInput;
+  }): FailureWithValue<GetSessionInternalServerError> {
+    logger.error(LogMessage.GET_SESSION_UNEXPECTED_FAILURE, {
+      error,
+      getItemCommandInput,
+    });
+    return errorResult({
+      errorType: GetSessionError.INTERNAL_SERVER_ERROR,
+    });
+  }
+
+  private handleGetSessionNotFoundError({
+    errorMessage,
path: backend-api/src/functions/adapters/aws/dynamo/dynamoDbAdapter.ts
user: login: jhumbert-dd
body: Removed
created_at: 2025-03-21T11:09:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -293,4 +431,204 @@ describe("DynamoDbAdapter", () => {
       });
     });
   });
+
+  describe("getSession", () => {
+    const getOperation: GetSessionOperation =
+      new TxMAEventGetSessionOperation();
+    let result: Result<void, GetSessionFailed>;
+
+    describe("On every attempt", () => {
+      beforeEach(async () => {
+        mockDynamoDbClient.on(GetItemCommand).resolves({});
+        await sessionRegistry.getSession(mockSessionId, getOperation);
+      });
+
+      it("Logs the attempt", () => {
+        expect(consoleDebugSpy).toHaveBeenCalledWithLogFields({
+          messageCode: "MOBILE_ASYNC_GET_SESSION_ATTEMPT",
+          data: {
+            sessionId: mockSessionId,
+            getItemCommandInput: getOperation.getDynamoDbGetCommandInput({
+              tableName: "mock_table_name",
+              keyValue: mockSessionId,
+            }),
+          },
+        });
+      });
+    });
+
+    describe("Given there is an unexpected error retrieving the session", () => {
+      beforeEach(async () => {
+        mockDynamoDbClient.on(GetItemCommand).rejects("mock_error");
+        result = await sessionRegistry.getSession(mockSessionId, getOperation);
+      });
+
+      it("Logs the failure", () => {
+        expect(consoleErrorSpy).toHaveBeenCalledWithLogFields({
+          messageCode: "MOBILE_ASYNC_GET_SESSION_UNEXPECTED_FAILURE",
+          getItemCommandInput: {
+            TableName: "mock_table_name",
+            Key: { sessionId: marshall(mockSessionId) },
+          },
+        });
+
+        const { name, message } = JSON.parse(
+          consoleErrorSpy.mock.calls[0][0],
+        ).error;
+        expect(name).toEqual("Error");
+        expect(message).toEqual("mock_error");
+      });
+
+      it("Returns failure with server error", () => {
+        expect(result).toEqual(
+          errorResult({
+            errorType: GetSessionError.INTERNAL_SERVER_ERROR,
+          }),
+        );
+      });
+    });
+
+    describe("Given the session was not found", () => {
+      beforeEach(async () => {
+        mockDynamoDbClient.on(GetItemCommand).resolves({});
+        result = await sessionRegistry.getSession(mockSessionId, getOperation);
+      });
+
+      it("Logs the failure", () => {
+        expect(consoleErrorSpy).toHaveBeenCalledWithLogFields({
+          messageCode: "MOBILE_ASYNC_GET_SESSION_SESSION_NOT_FOUND",
+          errorMessage: "Session not found",
+        });
+      });
+
+      it("Returns failure with an invalid session error", () => {
+        expect(result).toEqual(
+          errorResult({
+            errorType: GetSessionError.SESSION_NOT_FOUND,
+            errorMessage: "Session not found",
+          }),
+        );
+      });
+    });
+
+    describe("Session validation", () => {
+      describe("Given session attributes type validation failed", () => {
+        const invalidBiometricTokenIssuedSessionAttributesWrongType = {
+          ...validBiometricTokenIssuedSessionAttributes,
+          sessionState: 12345,
+        };
+
+        beforeEach(async () => {
+          mockDynamoDbClient.on(GetItemCommand).resolves({
+            Item: marshall(
+              invalidBiometricTokenIssuedSessionAttributesWrongType,
+            ),
+          });
+          result = await sessionRegistry.getSession(
+            mockSessionId,
+            getOperation,
+          );
+        });
+
+        it("Logs the failure", () => {
+          expect(consoleErrorSpy).toHaveBeenCalledWithLogFields({
+            messageCode: "MOBILE_ASYNC_GET_SESSION_SESSION_NOT_FOUND",
+            errorMessage: "Session not found",
+          });
+        });
+
+        it("Returns failure with server error", () => {
path: backend-api/src/functions/adapters/aws/dynamo/dynamoDbAdapter.test.ts
user: login: jhumbert-dd
body: This test has been updated to return an in server error
created_at: 2025-03-21T11:25:32Z
author_association: CONTRIBUTOR
diff_hunk: @@ -42,4 +50,27 @@ export class BiometricSessionFinished implements UpdateSessionOperation {
       return getBiometricSessionFinishedSessionAttributes(item);
     }
   }
+
+  validateSession(
path: backend-api/src/functions/common/session/updateOperations/BiometricSessionFinished/BiometricSessionFinished.ts
user: login: jhumbert-dd
body: Removed
created_at: 2025-03-21T11:53:45Z
author_association: CONTRIBUTOR
diff_hunk: @@ -26,17 +44,83 @@ export interface SessionUpdateFailedInternalServerError {
   errorType: UpdateSessionError.INTERNAL_SERVER_ERROR;
 }
 
-export interface SessionUpdateFailedSessionNotFound {
+interface SessionUpdateFailedSessionNotFound {
   errorType: UpdateSessionError.SESSION_NOT_FOUND;
 }
 
-export enum UpdateSessionError {
+export type UpdateOperationDataToLog =
+  | UpdateExpressionDataToLog
+  | UpdateSessionValidateSessionErrorData;
+
+interface UpdateExpressionDataToLog {
+  updateExpression: string;
+  conditionExpression: string | undefined;
+}
+
+export interface UpdateSessionValidateSessionErrorData {
+  invalidAttribute:
+    | UpdateSessionInvalidSessionAttribute
+    | BiometricSessionFinishedInvalidSessionAttribute;
+}
+
+interface UpdateSessionInvalidSessionAttribute {
+  sessionState?: Exclude<SessionState, SessionState.BIOMETRIC_TOKEN_ISSUED>;
+  createdAt?: number;
+}
+
+export interface BiometricSessionFinishedValidateSessionErrorData {
+  invalidAttribute: BiometricSessionFinishedInvalidSessionAttribute;
+}
+
+interface BiometricSessionFinishedInvalidSessionAttribute {
+  sessionState?: Exclude<SessionState, SessionState.BIOMETRIC_SESSION_FINISHED>;
+  createdAt?: number;
+}
+
+// Get session
+
+export enum GetSessionError {
   INTERNAL_SERVER_ERROR = "INTERNAL_SERVER_ERROR",
-  CONDITIONAL_CHECK_FAILURE = "CONDITIONAL_CHECK_FAILURE",
   SESSION_NOT_FOUND = "SESSION_NOT_FOUND",
+  SESSION_INVALID = "SESSION_INVALID",
 }
 
-export interface UpdateExpressionDataToLog {
-  updateExpression: string;
-  conditionExpression: string | undefined;
+export type GetSessionFailed =
path: backend-api/src/functions/common/session/SessionRegistry.ts
user: login: jhumbert-dd
body: Updated
created_at: 2025-03-21T12:11:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -56,6 +80,40 @@ describe("Async TxMA Event", () => {
     });
   });
 
+  describe("Config validation", () => {
+    describe.each(["SESSION_TABLE_NAME"])(
path: backend-api/src/functions/asyncTxmaEvent/asyncTxmaEventHandler.test.ts
user: login: jhumbert-dd
body: Thanks. I've removed the other two as they're not being used in this PR
created_at: 2025-03-21T12:35:33Z
author_association: CONTRIBUTOR
diff_hunk: @@ -121,3 +272,17 @@ const validRequest = buildRequest({
     eventName: "DCMAW_ASYNC_HYBRID_BILLING_STARTED",
   }),
 });
+
+export const mockTxmaEventSessionRegistrySuccess: SessionRegistry = {
+  updateSession: jest.fn().mockResolvedValue(
path: backend-api/src/functions/asyncTxmaEvent/asyncTxmaEventHandler.test.ts
user: login: jhumbert-dd
body: Updated
created_at: 2025-03-21T12:40:04Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,128 @@
+import { marshall, unmarshall } from "@aws-sdk/util-dynamodb";
+import {
+  invalidCreatedAt,
+  mockSessionId,
+  NOW_IN_MILLISECONDS,
+  validBiometricTokenIssuedSessionAttributes,
+  validCreatedAt,
+} from "../../../../testUtils/unitTestData";
+import {
+  emptyFailure,
+  emptySuccess,
+  errorResult,
+  successResult,
+} from "../../../../utils/result";
+import { TxMAEventGetSessionOperation } from "./TxmaEventGetSessionOperation";
+import { SessionState } from "../../session";
+
+describe("TxMA event", () => {
+  let getSessionOperation: TxMAEventGetSessionOperation;
+
+  beforeEach(() => {
+    jest.useFakeTimers();
+    jest.setSystemTime(NOW_IN_MILLISECONDS);
+    getSessionOperation = new TxMAEventGetSessionOperation();
+  });
+
+  describe("When I request the DynamoDB command input", () => {
+    it("Returns the appropriate GetItemCommand input", () => {
+      const result = getSessionOperation.getDynamoDbGetCommandInput({
+        tableName: "mock_table_name",
+        keyValue: mockSessionId,
+      });
+      expect(result).toEqual({
+        TableName: "mock_table_name",
+        Key: { sessionId: marshall(mockSessionId) },
path: backend-api/src/functions/common/session/getOperations/TxmaEvent/TxmaEventGetSessionOperation.test.ts
user: login: jhumbert-dd
body: Updated
created_at: 2025-03-21T12:44:56Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,128 @@
+import { marshall, unmarshall } from "@aws-sdk/util-dynamodb";
+import {
+  invalidCreatedAt,
+  mockSessionId,
+  NOW_IN_MILLISECONDS,
+  validBiometricTokenIssuedSessionAttributes,
+  validCreatedAt,
+} from "../../../../testUtils/unitTestData";
+import {
+  emptyFailure,
+  emptySuccess,
+  errorResult,
+  successResult,
+} from "../../../../utils/result";
+import { TxMAEventGetSessionOperation } from "./TxmaEventGetSessionOperation";
+import { SessionState } from "../../session";
+
+describe("TxMA event", () => {
+  let getSessionOperation: TxMAEventGetSessionOperation;
+
+  beforeEach(() => {
+    jest.useFakeTimers();
+    jest.setSystemTime(NOW_IN_MILLISECONDS);
+    getSessionOperation = new TxMAEventGetSessionOperation();
+  });
+
+  describe("When I request the DynamoDB command input", () => {
+    it("Returns the appropriate GetItemCommand input", () => {
+      const result = getSessionOperation.getDynamoDbGetCommandInput({
+        tableName: "mock_table_name",
+        keyValue: mockSessionId,
+      });
+      expect(result).toEqual({
+        TableName: "mock_table_name",
+        Key: { sessionId: marshall(mockSessionId) },
+      });
+    });
+  });
+
+  describe("When I request the getSessionAttributesFromDynamoDbItem", () => {
+    describe("Given a session attributes item was provided that does not include all BiometricTokenIssuedSessionAttributes properties", () => {
+      it("Returns an emptyFailure", () => {
+        const result = getSessionOperation.getSessionAttributesFromDynamoDbItem(
+          marshall({
+            sessionId: mockSessionId,
+          }),
+        );
+
+        expect(result).toEqual(emptyFailure());
+      });
+    });
+
+    describe("Given a valid BiometricTokenIssuedSessionAttributes item was provided", () => {
+      const validBiometricTokenIssuedSessionAttributesItem = marshall(
+        validBiometricTokenIssuedSessionAttributes,
+      );
+
+      it("Returns successResult with BiometricTokenIssuedSessionAttributes session attributes", () => {
+        const result = getSessionOperation.getSessionAttributesFromDynamoDbItem(
+          marshall(validBiometricTokenIssuedSessionAttributes),
+        );
+
+        expect(result).toEqual(
+          successResult(
+            unmarshall(validBiometricTokenIssuedSessionAttributesItem),
+          ),
+        );
+      });
+    });
+  });
+
+  describe("Session attribute validation", () => {
+    describe("Given the session is in the wrong state - sessionState = AUTH_SESSION_CREATED", () => {
+      const invalidSessionAttributesWrongSessionState = {
+        createdAt: validCreatedAt,
+        sessionState: SessionState.AUTH_SESSION_CREATED,
+      };
+
+      it("Return and error result with the invalid attribute", () => {
+        const result = getSessionOperation.validateSession(
+          invalidSessionAttributesWrongSessionState,
+        );
+
+        expect(result).toEqual(
+          errorResult({
+            invalidAttribute: {
+              sessionState: SessionState.AUTH_SESSION_CREATED,
+            },
+          }),
+        );
+      });
+    });
+
+    describe("Given the session is too old", () => {
+      const invalidSessionAttributesSessionTooOld = {
+        sessionState: SessionState.BIOMETRIC_TOKEN_ISSUED,
+        createdAt: invalidCreatedAt,
+      };
+
+      it("Return and error result with the invalid attribute", () => {
+        const result = getSessionOperation.validateSession(
+          invalidSessionAttributesSessionTooOld,
+        );
+
+        expect(result).toEqual(
+          errorResult({
+            invalidAttribute: { createdAt: invalidCreatedAt },
+          }),
+        );
+      });
+    });
+
+    describe("Given the session is valid", () => {
+      const validSessionAttributes = {
+        sessionState: SessionState.BIOMETRIC_TOKEN_ISSUED,
+        createdAt: validBiometricTokenIssuedSessionAttributes.createdAt,
+      };
+
+      it("Return and error result with the invalid attribute", () => {
path: backend-api/src/functions/common/session/getOperations/TxmaEvent/TxmaEventGetSessionOperation.test.ts
user: login: jhumbert-dd
body: Updated
created_at: 2025-03-21T12:51:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,54 @@
+import { AttributeValue, GetItemCommandInput } from "@aws-sdk/client-dynamodb";
+import { marshall } from "@aws-sdk/util-dynamodb";
+import { emptySuccess, errorResult, Result } from "../../../../utils/result";
+import { SessionAttributes, SessionState } from "../../session";
+import { getTxmaEventBiometricTokenIssuedSessionAttributes } from "../../sessionAttributes/sessionAttributes";
+import {
+  ValidateSessionAttributes,
+  ValidateSessionErrorInvalidAttributesData,
+  ValidateSessionErrorInvalidAttributeTypeData,
+  ValidateSessionInvalidAttributes,
+} from "../../SessionRegistry";
+import { GetSessionOperation } from "../GetSessionOperation";
+import { isOlderThan60Minutes } from "../../../../utils/utils";
+
+export class GetSessionBiometricTokenIssued implements GetSessionOperation {
+  getDynamoDbGetCommandInput({
+    tableName,
+    keyValue,
+  }: {
+    tableName: string;
+    keyValue: string;
+  }): GetItemCommandInput {
+    return {
+      TableName: tableName,
+      Key: { sessionId: marshall(keyValue) },
+    };
+  }
+
+  getSessionAttributesFromDynamoDbItem(
+    item: Record<string, AttributeValue>,
+  ): Result<SessionAttributes, ValidateSessionErrorInvalidAttributeTypeData> {
+    return getTxmaEventBiometricTokenIssuedSessionAttributes(item);
+  }
+
+  validateSession(
+    attributes: ValidateSessionAttributes,
+  ): Result<void, ValidateSessionErrorInvalidAttributesData> {
+    const { sessionState, createdAt } = attributes;
+    const invalidAttributes: ValidateSessionInvalidAttributes = {};
+
+    if (!sessionState || sessionState !== SessionState.BIOMETRIC_TOKEN_ISSUED) {
+      invalidAttributes.sessionState = sessionState;
path: backend-api/src/functions/common/session/getOperations/TxmaEvent/GetSessionBiometricTokenIssued.ts
user: login: sandymay-dd
body: non-blocking: I personally find an array cleaner to see, but it's very much a preference. 

It avoids needing to do two things to invalidAttributes (Object.entries and length) to check whether to return an error. Such a minor thing, I'm not fussed at all.
created_at: 2025-03-21T12:58:53Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,55 @@
+import { AttributeValue, GetItemCommandInput } from "@aws-sdk/client-dynamodb";
+import { marshall } from "@aws-sdk/util-dynamodb";
+import { emptySuccess, errorResult, Result } from "../../../../utils/result";
+import {
+  isOlderThan60minutes,
+  SessionAttributes,
+  SessionState,
+} from "../../session";
+import { getBiometricTokenIssuedSessionAttributes } from "../../sessionAttributes/sessionAttributes";
+import { GetSessionOperation } from "../GetSessionOperation";
+import {
+  ValidateSessionErrorInvalidAttributeData,
+  ValidateSessionAttributes,
+} from "../../SessionRegistry";
+
+export class TxMAEventGetSessionOperation implements GetSessionOperation {
+  getDynamoDbGetCommandInput({
+    tableName,
+    keyValue,
+  }: {
+    tableName: string;
+    keyValue: string;
+  }): GetItemCommandInput {
+    return {
+      TableName: tableName,
+      Key: { sessionId: marshall(keyValue) },
+    };
+  }
+
+  getSessionAttributesFromDynamoDbItem(
+    item: Record<string, AttributeValue> | undefined,
+  ): Result<SessionAttributes, void> {
+    return getBiometricTokenIssuedSessionAttributes(item);
+  }
+
+  validateSession(
+    attributes: ValidateSessionAttributes,
+  ): Result<void, ValidateSessionErrorInvalidAttributeData> {
+    const { sessionState, createdAt } = attributes;
+
+    if (!sessionState || sessionState !== SessionState.BIOMETRIC_TOKEN_ISSUED) {
+      return errorResult({
+        invalidAttribute: { sessionState },
path: backend-api/src/functions/common/session/getOperations/TxmaEvent/TxmaEventGetSessionOperation.ts
user: login: sandymay-dd
body: left a comment on this one of the function itself, apologies missed this thread
created_at: 2025-03-21T12:59:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -225,14 +298,59 @@ export class DynamoDbAdapter implements SessionRegistry {
 
   private handleUpdateSessionInternalServerError(
     error: unknown,
-    updateExpressionDataToLog: UpdateExpressionDataToLog,
+    data: UpdateOperationDataToLog,
   ): FailureWithValue<SessionUpdateFailedInternalServerError> {
     logger.error(LogMessage.UPDATE_SESSION_UNEXPECTED_FAILURE, {
-      error: error,
-      data: updateExpressionDataToLog,
+      error,
+      data,
     });
     return errorResult({
       errorType: UpdateSessionError.INTERNAL_SERVER_ERROR,
     });
   }
+
+  private handleGetSessionInternalServerError({
+    error,
+    getItemCommandInput,
+    sessionAttributes,
+  }: {
+    error: unknown;
+    getItemCommandInput: GetItemCommandInput;
+    sessionAttributes?: InvalidSessionAttributeTypes;
+  }): FailureWithValue<GetSessionInternalServerError> {
+    logger.error(LogMessage.GET_SESSION_UNEXPECTED_FAILURE, {
+      error,
+      getItemCommandInput,
path: backend-api/src/functions/adapters/aws/dynamo/dynamoDbAdapter.ts
user: login: jmooney-dd
body: issue: sometimes when we log errors, we log the operation details and other times we don't, could we make this consistent?

Was there a reason for this? IMO the operation should just be logged in the initial attempt debug log, I don't think we need it in subsequent logs but I would rather we always log it or we don't for errors. See [here](https://github.com/govuk-one-login/mobile-id-check-async/pull/469/files#diff-31ab14319bd3be272553261c41d8f2c3dfe1e6a82fbcc17ccc6cd077e0290cd1R331-R332) (just below) for the sessionNotFound scenario where we do not log this

I've not checked other logger.errors yet in this PR but could we check that we're being consistent thought the changes
created_at: 2025-03-21T13:52:43Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,54 @@
+import { AttributeValue, GetItemCommandInput } from "@aws-sdk/client-dynamodb";
+import { marshall } from "@aws-sdk/util-dynamodb";
+import { emptySuccess, errorResult, Result } from "../../../../utils/result";
+import { SessionAttributes, SessionState } from "../../session";
+import { getTxmaEventBiometricTokenIssuedSessionAttributes } from "../../sessionAttributes/sessionAttributes";
+import {
+  ValidateSessionAttributes,
+  ValidateSessionErrorInvalidAttributesData,
+  ValidateSessionErrorInvalidAttributeTypeData,
+  ValidateSessionInvalidAttributes,
+} from "../../SessionRegistry";
+import { GetSessionOperation } from "../GetSessionOperation";
+import { isOlderThan60Minutes } from "../../../../utils/utils";
+
+export class GetSessionBiometricTokenIssued implements GetSessionOperation {
+  getDynamoDbGetCommandInput({
+    tableName,
+    keyValue,
+  }: {
+    tableName: string;
+    keyValue: string;
+  }): GetItemCommandInput {
+    return {
+      TableName: tableName,
+      Key: { sessionId: marshall(keyValue) },
+    };
+  }
+
+  getSessionAttributesFromDynamoDbItem(
+    item: Record<string, AttributeValue>,
+  ): Result<SessionAttributes, ValidateSessionErrorInvalidAttributeTypeData> {
+    return getTxmaEventBiometricTokenIssuedSessionAttributes(item);
+  }
+
+  validateSession(
+    attributes: ValidateSessionAttributes,
+  ): Result<void, ValidateSessionErrorInvalidAttributesData> {
+    const { sessionState, createdAt } = attributes;
+    const invalidAttributes: ValidateSessionInvalidAttributes = {};
+
+    if (!sessionState || sessionState !== SessionState.BIOMETRIC_TOKEN_ISSUED) {
+      invalidAttributes.sessionState = sessionState;
path: backend-api/src/functions/common/session/getOperations/TxmaEvent/GetSessionBiometricTokenIssued.ts
user: login: jhumbert-dd
body: Yeah, I see your point. I think the two steps in this case might be worth session cleaner logs instead of an array of objects with only one property in each. Will keep as is for now
created_at: 2025-03-21T14:03:10Z
author_association: CONTRIBUTOR
diff_hunk: @@ -46,15 +31,93 @@ export const validBaseSessionAttributes = {
   timeToLive: 12345,
 };
 
+export const invalidBaseSessionAttributeTypes = {
+  clientId: "mockClientId",
+  govukSigninJourneyId: "mockGovukSigninJourneyId",
+  createdAt: validCreatedAt,
+  issuer: 12345, // Invalid type
+  sessionId: mockSessionId,
+  sessionState: SessionState.AUTH_SESSION_CREATED,
+  clientState: "mockClientState",
+  subjectIdentifier: "mockSubjectIdentifier",
+  timeToLive: "12345", // Invalid type
+};
+
+// Biometric token issued
+
 export const validBiometricTokenIssuedSessionAttributes = {
   ...validBaseSessionAttributes,
   sessionState: SessionState.BIOMETRIC_TOKEN_ISSUED,
   documentType: "NFC_PASSPORT",
   opaqueId: "mockOpaqueId",
 };
 
+export const validBiometricTokenIssuedSessionAttributesMobileApp = {
path: backend-api/src/functions/testUtils/unitTestData.ts
user: login: jmooney-dd
body: suggestion (non-blocking): 

```suggestion
export const validBiometricTokenIssuedSessionAttributes = {
```

Other than 'base' attributes, the other attribute sets have typically just followed a naming pattern based purely on the state, so I don't think it's that helpful to state `MobileApp` here

Entirely non-blocking, if you like it all good to resolve
created_at: 2025-03-21T14:03:36Z
author_association: CONTRIBUTOR
diff_hunk: @@ -46,15 +31,93 @@ export const validBaseSessionAttributes = {
   timeToLive: 12345,
 };
 
+export const invalidBaseSessionAttributeTypes = {
+  clientId: "mockClientId",
+  govukSigninJourneyId: "mockGovukSigninJourneyId",
+  createdAt: validCreatedAt,
+  issuer: 12345, // Invalid type
+  sessionId: mockSessionId,
+  sessionState: SessionState.AUTH_SESSION_CREATED,
+  clientState: "mockClientState",
+  subjectIdentifier: "mockSubjectIdentifier",
+  timeToLive: "12345", // Invalid type
+};
+
+// Biometric token issued
+
 export const validBiometricTokenIssuedSessionAttributes = {
   ...validBaseSessionAttributes,
   sessionState: SessionState.BIOMETRIC_TOKEN_ISSUED,
   documentType: "NFC_PASSPORT",
   opaqueId: "mockOpaqueId",
 };
 
+export const validBiometricTokenIssuedSessionAttributesMobileApp = {
path: backend-api/src/functions/testUtils/unitTestData.ts
user: login: jmooney-dd
body: Curious if you did think of anything I'm missing about adding MobileApp
created_at: 2025-03-21T14:04:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -46,15 +31,93 @@ export const validBaseSessionAttributes = {
   timeToLive: 12345,
 };
 
+export const invalidBaseSessionAttributeTypes = {
+  clientId: "mockClientId",
+  govukSigninJourneyId: "mockGovukSigninJourneyId",
+  createdAt: validCreatedAt,
+  issuer: 12345, // Invalid type
+  sessionId: mockSessionId,
+  sessionState: SessionState.AUTH_SESSION_CREATED,
+  clientState: "mockClientState",
+  subjectIdentifier: "mockSubjectIdentifier",
+  timeToLive: "12345", // Invalid type
+};
+
+// Biometric token issued
+
 export const validBiometricTokenIssuedSessionAttributes = {
   ...validBaseSessionAttributes,
   sessionState: SessionState.BIOMETRIC_TOKEN_ISSUED,
   documentType: "NFC_PASSPORT",
   opaqueId: "mockOpaqueId",
 };
 
+export const validBiometricTokenIssuedSessionAttributesMobileApp = {
+  ...validBiometricTokenIssuedSessionAttributes,
+  redirectUri: "https://www.mockRedirectUri.com",
+};
+
+export const invalidBiometricTokenIssuedSessionAttributesWrongSessionState = {
+  ...validBiometricTokenIssuedSessionAttributes,
+  sessionState: SessionState.AUTH_SESSION_CREATED,
+};
+
+export const invalidBiometricTokenIssuedSessionAttributeTypes = {
+  ...validBiometricTokenIssuedSessionAttributes,
+  issuer: invalidBaseSessionAttributeTypes.issuer,
+  timeToLive: invalidBaseSessionAttributeTypes.timeToLive,
+};
+
+// Biometric session finished
+
 export const validBiometricSessionFinishedAttributes = {
   ...validBiometricTokenIssuedSessionAttributes,
   sessionState: SessionState.BIOMETRIC_SESSION_FINISHED,
   biometricSessionId: mockBiometricSessionId,
 };
+
+export const validBiometricSessionFinishedAttributesMobileApp = {
+  ...validBiometricSessionFinishedAttributes,
+  redirectUri: "https://www.mockRedirectUri.com",
+};
+
+// Session registry
path: backend-api/src/functions/testUtils/unitTestData.ts
user: login: jmooney-dd
body: suggestion (non-blocking): remove comment, it's clear by the naming and types what this is
created_at: 2025-03-21T14:05:10Z
author_association: CONTRIBUTOR
diff_hunk: @@ -46,15 +31,93 @@ export const validBaseSessionAttributes = {
   timeToLive: 12345,
 };
 
+export const invalidBaseSessionAttributeTypes = {
+  clientId: "mockClientId",
+  govukSigninJourneyId: "mockGovukSigninJourneyId",
+  createdAt: validCreatedAt,
+  issuer: 12345, // Invalid type
+  sessionId: mockSessionId,
+  sessionState: SessionState.AUTH_SESSION_CREATED,
+  clientState: "mockClientState",
+  subjectIdentifier: "mockSubjectIdentifier",
+  timeToLive: "12345", // Invalid type
+};
+
+// Biometric token issued
+
 export const validBiometricTokenIssuedSessionAttributes = {
   ...validBaseSessionAttributes,
   sessionState: SessionState.BIOMETRIC_TOKEN_ISSUED,
   documentType: "NFC_PASSPORT",
   opaqueId: "mockOpaqueId",
 };
 
+export const validBiometricTokenIssuedSessionAttributesMobileApp = {
+  ...validBiometricTokenIssuedSessionAttributes,
+  redirectUri: "https://www.mockRedirectUri.com",
+};
+
+export const invalidBiometricTokenIssuedSessionAttributesWrongSessionState = {
+  ...validBiometricTokenIssuedSessionAttributes,
+  sessionState: SessionState.AUTH_SESSION_CREATED,
+};
+
+export const invalidBiometricTokenIssuedSessionAttributeTypes = {
+  ...validBiometricTokenIssuedSessionAttributes,
+  issuer: invalidBaseSessionAttributeTypes.issuer,
+  timeToLive: invalidBaseSessionAttributeTypes.timeToLive,
+};
+
+// Biometric session finished
+
 export const validBiometricSessionFinishedAttributes = {
   ...validBiometricTokenIssuedSessionAttributes,
   sessionState: SessionState.BIOMETRIC_SESSION_FINISHED,
   biometricSessionId: mockBiometricSessionId,
 };
+
+export const validBiometricSessionFinishedAttributesMobileApp = {
+  ...validBiometricSessionFinishedAttributes,
+  redirectUri: "https://www.mockRedirectUri.com",
+};
+
+// Session registry
+
+export const mockInertSessionRegistry: SessionRegistry = {
+  updateSession: jest.fn(() => {
+    throw new Error("Not implemented");
+  }),
+
+  getSession: jest.fn(() => {
+    throw new Error("Not implemented");
+  }),
+};
+
+// Event service
path: backend-api/src/functions/testUtils/unitTestData.ts
user: login: jmooney-dd
body: suggestion (non-blocking): remove comment, it's clear by the naming and types what this is
created_at: 2025-03-21T14:05:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -31,23 +37,75 @@ describe("POST /async/txmaEvent", () => {
     });
   });
 
-  describe("Given there is a valid request", () => {
+  describe("Given the session is not valid", () => {
     let response: AxiosResponse;
+    let sessionId: string;
 
     beforeAll(async () => {
+      sessionId = mockSessionId;
       const requestBody = {
-        sessionId: mockSessionId,
+        sessionId,
         eventName: "DCMAW_ASYNC_HYBRID_BILLING_STARTED",
       };
 
       response = await SESSIONS_API_INSTANCE.post(
         "/async/txmaEvent",
         requestBody,
       );
+    });
 
-      console.log("RESPONSE >>>>>", response);
+    it("Returns an error and 401 status code", async () => {
+      expect(response.status).toBe(401);
path: backend-api/tests/api-tests/txmaEvent.test.ts
user: login: jmooney-dd
body: ```suggestion
    it("Returns 401 Unauthorized response with invalid_session error", async () => {
      expect(response.status).toBe(401);
      expect(response.statusText).toBe("Unauthorized");
```
created_at: 2025-03-21T14:07:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -31,23 +37,75 @@ describe("POST /async/txmaEvent", () => {
     });
   });
 
-  describe("Given there is a valid request", () => {
+  describe("Given the session is not valid", () => {
     let response: AxiosResponse;
+    let sessionId: string;
 
     beforeAll(async () => {
+      sessionId = mockSessionId;
       const requestBody = {
-        sessionId: mockSessionId,
+        sessionId,
         eventName: "DCMAW_ASYNC_HYBRID_BILLING_STARTED",
       };
 
       response = await SESSIONS_API_INSTANCE.post(
         "/async/txmaEvent",
         requestBody,
       );
+    });
 
-      console.log("RESPONSE >>>>>", response);
+    it("Returns an error and 401 status code", async () => {
+      expect(response.status).toBe(401);
+      expect(response.data).toStrictEqual({
+        error: "invalid_session",
+        error_description: "Session does not exist or in incorrect state",
+      });
+      expect(response.headers).toEqual(
+        expect.objectContaining(expectedSecurityHeaders),
+      );
     });
 
+    it("Writes an event with the correct event_name", async () => {
+      const eventsResponse = await pollForEvents({
+        partitionKey: `SESSION#${sessionId}`,
+        sortKeyPrefix: `TXMA#EVENT_NAME#DCMAW_ASYNC_CRI_4XXERROR`,
+        numberOfEvents: 1,
+      });
+
+      expect(eventsResponse[0].event).toEqual(
+        expect.objectContaining({
+          event_name: "DCMAW_ASYNC_CRI_4XXERROR",
+        }),
+      );
+    }, 40000);
+  });
+
+  describe("Given the request is valid", () => {
+    let sessionId: string | null;
+    let response: AxiosResponse;
+
+    beforeAll(async () => {
+      const sub = randomUUID();
+      await createSessionForSub(sub);
+      sessionId = await getActiveSessionIdFromSub(sub);
+      const biometricTokenRequestBody = {
+        sessionId,
+        documentType: "NFC_PASSPORT",
+      };
+      await SESSIONS_API_INSTANCE.post(
+        "/async/biometricToken",
+        biometricTokenRequestBody,
+      );
path: backend-api/tests/api-tests/txmaEvent.test.ts
user: login: jmooney-dd
body: suggestion/question (non-blocking): should this step be in an api-test helper?
created_at: 2025-03-21T14:08:46Z
author_association: CONTRIBUTOR
diff_hunk: @@ -225,14 +298,59 @@ export class DynamoDbAdapter implements SessionRegistry {
 
   private handleUpdateSessionInternalServerError(
     error: unknown,
-    updateExpressionDataToLog: UpdateExpressionDataToLog,
+    data: UpdateOperationDataToLog,
   ): FailureWithValue<SessionUpdateFailedInternalServerError> {
     logger.error(LogMessage.UPDATE_SESSION_UNEXPECTED_FAILURE, {
-      error: error,
-      data: updateExpressionDataToLog,
+      error,
+      data,
     });
     return errorResult({
       errorType: UpdateSessionError.INTERNAL_SERVER_ERROR,
     });
   }
+
+  private handleGetSessionInternalServerError({
+    error,
+    getItemCommandInput,
+    sessionAttributes,
+  }: {
+    error: unknown;
+    getItemCommandInput: GetItemCommandInput;
+    sessionAttributes?: InvalidSessionAttributeTypes;
+  }): FailureWithValue<GetSessionInternalServerError> {
+    logger.error(LogMessage.GET_SESSION_UNEXPECTED_FAILURE, {
+      error,
+      getItemCommandInput,
path: backend-api/src/functions/adapters/aws/dynamo/dynamoDbAdapter.ts
user: login: jhumbert-dd
body: Updated by removing data from error log
created_at: 2025-03-21T14:09:06Z
author_association: CONTRIBUTOR
diff_hunk: @@ -46,15 +31,93 @@ export const validBaseSessionAttributes = {
   timeToLive: 12345,
 };
 
+export const invalidBaseSessionAttributeTypes = {
+  clientId: "mockClientId",
+  govukSigninJourneyId: "mockGovukSigninJourneyId",
+  createdAt: validCreatedAt,
+  issuer: 12345, // Invalid type
+  sessionId: mockSessionId,
+  sessionState: SessionState.AUTH_SESSION_CREATED,
+  clientState: "mockClientState",
+  subjectIdentifier: "mockSubjectIdentifier",
+  timeToLive: "12345", // Invalid type
+};
+
+// Biometric token issued
+
 export const validBiometricTokenIssuedSessionAttributes = {
   ...validBaseSessionAttributes,
   sessionState: SessionState.BIOMETRIC_TOKEN_ISSUED,
   documentType: "NFC_PASSPORT",
   opaqueId: "mockOpaqueId",
 };
 
+export const validBiometricTokenIssuedSessionAttributesMobileApp = {
+  ...validBiometricTokenIssuedSessionAttributes,
+  redirectUri: "https://www.mockRedirectUri.com",
+};
+
+export const invalidBiometricTokenIssuedSessionAttributesWrongSessionState = {
+  ...validBiometricTokenIssuedSessionAttributes,
+  sessionState: SessionState.AUTH_SESSION_CREATED,
+};
+
+export const invalidBiometricTokenIssuedSessionAttributeTypes = {
+  ...validBiometricTokenIssuedSessionAttributes,
+  issuer: invalidBaseSessionAttributeTypes.issuer,
+  timeToLive: invalidBaseSessionAttributeTypes.timeToLive,
+};
+
+// Biometric session finished
+
 export const validBiometricSessionFinishedAttributes = {
   ...validBiometricTokenIssuedSessionAttributes,
   sessionState: SessionState.BIOMETRIC_SESSION_FINISHED,
   biometricSessionId: mockBiometricSessionId,
 };
+
+export const validBiometricSessionFinishedAttributesMobileApp = {
+  ...validBiometricSessionFinishedAttributes,
+  redirectUri: "https://www.mockRedirectUri.com",
+};
+
+// Session registry
+
+export const mockInertSessionRegistry: SessionRegistry = {
+  updateSession: jest.fn(() => {
+    throw new Error("Not implemented");
+  }),
+
+  getSession: jest.fn(() => {
+    throw new Error("Not implemented");
+  }),
+};
+
+// Event service
path: backend-api/src/functions/testUtils/unitTestData.ts
user: login: jmooney-dd
body: Might be a few others like this in the file if you do want to remove, won't comment on anymore
created_at: 2025-03-21T14:16:19Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,26 +1,29 @@
 import { expect } from "@jest/globals";
-import "../../../tests/testUtils/matchers";
-import "dotenv/config";
 import { APIGatewayProxyResult, Context } from "aws-lambda";
+import "dotenv/config";
path: backend-api/src/functions/asyncBiometricToken/asyncBiometricTokenHandler.test.ts
user: login: jmooney-dd
body: issue: should this be removed, I don't think it's needed
created_at: 2025-03-21T14:17:43Z
author_association: CONTRIBUTOR
diff_hunk: @@ -46,15 +31,93 @@ export const validBaseSessionAttributes = {
   timeToLive: 12345,
 };
 
+export const invalidBaseSessionAttributeTypes = {
+  clientId: "mockClientId",
+  govukSigninJourneyId: "mockGovukSigninJourneyId",
+  createdAt: validCreatedAt,
+  issuer: 12345, // Invalid type
+  sessionId: mockSessionId,
+  sessionState: SessionState.AUTH_SESSION_CREATED,
+  clientState: "mockClientState",
+  subjectIdentifier: "mockSubjectIdentifier",
+  timeToLive: "12345", // Invalid type
+};
+
+// Biometric token issued
+
 export const validBiometricTokenIssuedSessionAttributes = {
   ...validBaseSessionAttributes,
   sessionState: SessionState.BIOMETRIC_TOKEN_ISSUED,
   documentType: "NFC_PASSPORT",
   opaqueId: "mockOpaqueId",
 };
 
+export const validBiometricTokenIssuedSessionAttributesMobileApp = {
path: backend-api/src/functions/testUtils/unitTestData.ts
user: login: jhumbert-dd
body: As the values are different for MAM and DAD sessions, do you have a suggestion of how to distinguish them? No worries if not but as maybe appending `Mam` instead of `MobileApp` is better?
created_at: 2025-03-21T14:19:43Z
author_association: CONTRIBUTOR
diff_hunk: @@ -46,15 +31,93 @@ export const validBaseSessionAttributes = {
   timeToLive: 12345,
 };
 
+export const invalidBaseSessionAttributeTypes = {
+  clientId: "mockClientId",
+  govukSigninJourneyId: "mockGovukSigninJourneyId",
+  createdAt: validCreatedAt,
+  issuer: 12345, // Invalid type
+  sessionId: mockSessionId,
+  sessionState: SessionState.AUTH_SESSION_CREATED,
+  clientState: "mockClientState",
+  subjectIdentifier: "mockSubjectIdentifier",
+  timeToLive: "12345", // Invalid type
+};
+
+// Biometric token issued
+
 export const validBiometricTokenIssuedSessionAttributes = {
   ...validBaseSessionAttributes,
   sessionState: SessionState.BIOMETRIC_TOKEN_ISSUED,
   documentType: "NFC_PASSPORT",
   opaqueId: "mockOpaqueId",
 };
 
+export const validBiometricTokenIssuedSessionAttributesMobileApp = {
path: backend-api/src/functions/testUtils/unitTestData.ts
user: login: jmooney-dd
body: Nope that makes sense, I didn't think of that, happy to leave it as is
created_at: 2025-03-21T14:23:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -31,23 +37,75 @@ describe("POST /async/txmaEvent", () => {
     });
   });
 
-  describe("Given there is a valid request", () => {
+  describe("Given the session is not valid", () => {
     let response: AxiosResponse;
+    let sessionId: string;
 
     beforeAll(async () => {
+      sessionId = mockSessionId;
       const requestBody = {
-        sessionId: mockSessionId,
+        sessionId,
         eventName: "DCMAW_ASYNC_HYBRID_BILLING_STARTED",
       };
 
       response = await SESSIONS_API_INSTANCE.post(
         "/async/txmaEvent",
         requestBody,
       );
+    });
 
-      console.log("RESPONSE >>>>>", response);
+    it("Returns an error and 401 status code", async () => {
+      expect(response.status).toBe(401);
+      expect(response.data).toStrictEqual({
+        error: "invalid_session",
+        error_description: "Session does not exist or in incorrect state",
+      });
+      expect(response.headers).toEqual(
+        expect.objectContaining(expectedSecurityHeaders),
+      );
     });
 
+    it("Writes an event with the correct event_name", async () => {
+      const eventsResponse = await pollForEvents({
+        partitionKey: `SESSION#${sessionId}`,
+        sortKeyPrefix: `TXMA#EVENT_NAME#DCMAW_ASYNC_CRI_4XXERROR`,
+        numberOfEvents: 1,
+      });
+
+      expect(eventsResponse[0].event).toEqual(
+        expect.objectContaining({
+          event_name: "DCMAW_ASYNC_CRI_4XXERROR",
+        }),
+      );
+    }, 40000);
+  });
+
+  describe("Given the request is valid", () => {
+    let sessionId: string | null;
+    let response: AxiosResponse;
+
+    beforeAll(async () => {
+      const sub = randomUUID();
+      await createSessionForSub(sub);
+      sessionId = await getActiveSessionIdFromSub(sub);
+      const biometricTokenRequestBody = {
+        sessionId,
+        documentType: "NFC_PASSPORT",
+      };
+      await SESSIONS_API_INSTANCE.post(
+        "/async/biometricToken",
+        biometricTokenRequestBody,
+      );
path: backend-api/tests/api-tests/txmaEvent.test.ts
user: login: jmooney-dd
body: We could leave api test helpers to when we write the real 200 api test
created_at: 2025-03-21T14:28:57Z
author_association: CONTRIBUTOR
diff_hunk: @@ -31,23 +37,75 @@ describe("POST /async/txmaEvent", () => {
     });
   });
 
-  describe("Given there is a valid request", () => {
+  describe("Given the session is not valid", () => {
     let response: AxiosResponse;
+    let sessionId: string;
 
     beforeAll(async () => {
+      sessionId = mockSessionId;
       const requestBody = {
-        sessionId: mockSessionId,
+        sessionId,
         eventName: "DCMAW_ASYNC_HYBRID_BILLING_STARTED",
       };
 
       response = await SESSIONS_API_INSTANCE.post(
         "/async/txmaEvent",
         requestBody,
       );
+    });
 
-      console.log("RESPONSE >>>>>", response);
+    it("Returns an error and 401 status code", async () => {
+      expect(response.status).toBe(401);
path: backend-api/tests/api-tests/txmaEvent.test.ts
user: login: jhumbert-dd
body: Updated this and the one above
created_at: 2025-03-21T14:30:07Z
author_association: CONTRIBUTOR
diff_hunk: @@ -46,15 +31,93 @@ export const validBaseSessionAttributes = {
   timeToLive: 12345,
 };
 
+export const invalidBaseSessionAttributeTypes = {
+  clientId: "mockClientId",
+  govukSigninJourneyId: "mockGovukSigninJourneyId",
+  createdAt: validCreatedAt,
+  issuer: 12345, // Invalid type
+  sessionId: mockSessionId,
+  sessionState: SessionState.AUTH_SESSION_CREATED,
+  clientState: "mockClientState",
+  subjectIdentifier: "mockSubjectIdentifier",
+  timeToLive: "12345", // Invalid type
+};
+
+// Biometric token issued
+
 export const validBiometricTokenIssuedSessionAttributes = {
   ...validBaseSessionAttributes,
   sessionState: SessionState.BIOMETRIC_TOKEN_ISSUED,
   documentType: "NFC_PASSPORT",
   opaqueId: "mockOpaqueId",
 };
 
+export const validBiometricTokenIssuedSessionAttributesMobileApp = {
+  ...validBiometricTokenIssuedSessionAttributes,
+  redirectUri: "https://www.mockRedirectUri.com",
+};
+
+export const invalidBiometricTokenIssuedSessionAttributesWrongSessionState = {
+  ...validBiometricTokenIssuedSessionAttributes,
+  sessionState: SessionState.AUTH_SESSION_CREATED,
+};
+
+export const invalidBiometricTokenIssuedSessionAttributeTypes = {
+  ...validBiometricTokenIssuedSessionAttributes,
+  issuer: invalidBaseSessionAttributeTypes.issuer,
+  timeToLive: invalidBaseSessionAttributeTypes.timeToLive,
+};
+
+// Biometric session finished
+
 export const validBiometricSessionFinishedAttributes = {
   ...validBiometricTokenIssuedSessionAttributes,
   sessionState: SessionState.BIOMETRIC_SESSION_FINISHED,
   biometricSessionId: mockBiometricSessionId,
 };
+
+export const validBiometricSessionFinishedAttributesMobileApp = {
+  ...validBiometricSessionFinishedAttributes,
+  redirectUri: "https://www.mockRedirectUri.com",
+};
+
+// Session registry
path: backend-api/src/functions/testUtils/unitTestData.ts
user: login: jhumbert-dd
body: Added these in to see at a glance where new test data should live but happy to remove as the value is minimal 
created_at: 2025-03-21T14:33:11Z
author_association: CONTRIBUTOR
diff_hunk: @@ -90,8 +145,101 @@ describe("Async TxMA Event", () => {
     });
   });
 
+  describe("Retrieving a session", () => {
path: backend-api/src/functions/asyncTxmaEvent/asyncTxmaEventHandler.test.ts
user: login: jmooney-dd
body: Question: is this structure correct?

```
  describe("Retrieving a session", () => {
    describe("Given there is an error retrieving the session", () => {
    describe("Given failure is due to client error", () => {
    });
  });
```

I don't think they should be at the same level as `Given failure is due to client error` is still still part of `Given there is an error retrieving the session`.

I think these blocks actually signify different types of errors so I think at very least the wording of these blocks should be changed
created_at: 2025-03-21T16:14:23Z
author_association: CONTRIBUTOR
diff_hunk: @@ -90,8 +145,101 @@ describe("Async TxMA Event", () => {
     });
   });
 
+  describe("Retrieving a session", () => {
path: backend-api/src/functions/asyncTxmaEvent/asyncTxmaEventHandler.test.ts
user: login: jmooney-dd
body: Follow-up question re this, the error naming confuses me a bit, because one is for a client error and one is for unexpected internal server error but we often consider the latter to be the former.

I can see [here](https://github.com/govuk-one-login/mobile-id-check-async/pull/469/files#diff-ab20b4e7135539be28f182dce1aca34edd0765c7c9a0fc66b5abccff35f77175R79) where you have enum

```
export enum GetSessionError {
  INTERNAL_SERVER_ERROR = "INTERNAL_SERVER_ERROR",
  CLIENT_ERROR = "CLIENT_ERROR",
}
```

If this makes sense to you then happy to not question this anymore for this commit
created_at: 2025-03-21T16:17:08Z
author_association: CONTRIBUTOR
diff_hunk: @@ -46,15 +31,93 @@ export const validBaseSessionAttributes = {
   timeToLive: 12345,
 };
 
+export const invalidBaseSessionAttributeTypes = {
+  clientId: "mockClientId",
+  govukSigninJourneyId: "mockGovukSigninJourneyId",
+  createdAt: validCreatedAt,
+  issuer: 12345, // Invalid type
+  sessionId: mockSessionId,
+  sessionState: SessionState.AUTH_SESSION_CREATED,
+  clientState: "mockClientState",
+  subjectIdentifier: "mockSubjectIdentifier",
+  timeToLive: "12345", // Invalid type
+};
+
+// Biometric token issued
+
 export const validBiometricTokenIssuedSessionAttributes = {
   ...validBaseSessionAttributes,
   sessionState: SessionState.BIOMETRIC_TOKEN_ISSUED,
   documentType: "NFC_PASSPORT",
   opaqueId: "mockOpaqueId",
 };
 
+export const validBiometricTokenIssuedSessionAttributesMobileApp = {
+  ...validBiometricTokenIssuedSessionAttributes,
+  redirectUri: "https://www.mockRedirectUri.com",
+};
+
+export const invalidBiometricTokenIssuedSessionAttributesWrongSessionState = {
+  ...validBiometricTokenIssuedSessionAttributes,
+  sessionState: SessionState.AUTH_SESSION_CREATED,
+};
+
+export const invalidBiometricTokenIssuedSessionAttributeTypes = {
+  ...validBiometricTokenIssuedSessionAttributes,
+  issuer: invalidBaseSessionAttributeTypes.issuer,
+  timeToLive: invalidBaseSessionAttributeTypes.timeToLive,
+};
+
+// Biometric session finished
+
 export const validBiometricSessionFinishedAttributes = {
   ...validBiometricTokenIssuedSessionAttributes,
   sessionState: SessionState.BIOMETRIC_SESSION_FINISHED,
   biometricSessionId: mockBiometricSessionId,
 };
+
+export const validBiometricSessionFinishedAttributesMobileApp = {
+  ...validBiometricSessionFinishedAttributes,
+  redirectUri: "https://www.mockRedirectUri.com",
+};
+
+// Session registry
path: backend-api/src/functions/testUtils/unitTestData.ts
user: login: jhumbert-dd
body: Removed
created_at: 2025-03-21T16:27:51Z
author_association: CONTRIBUTOR
diff_hunk: @@ -46,15 +31,93 @@ export const validBaseSessionAttributes = {
   timeToLive: 12345,
 };
 
+export const invalidBaseSessionAttributeTypes = {
+  clientId: "mockClientId",
+  govukSigninJourneyId: "mockGovukSigninJourneyId",
+  createdAt: validCreatedAt,
+  issuer: 12345, // Invalid type
+  sessionId: mockSessionId,
+  sessionState: SessionState.AUTH_SESSION_CREATED,
+  clientState: "mockClientState",
+  subjectIdentifier: "mockSubjectIdentifier",
+  timeToLive: "12345", // Invalid type
+};
+
+// Biometric token issued
+
 export const validBiometricTokenIssuedSessionAttributes = {
   ...validBaseSessionAttributes,
   sessionState: SessionState.BIOMETRIC_TOKEN_ISSUED,
   documentType: "NFC_PASSPORT",
   opaqueId: "mockOpaqueId",
 };
 
+export const validBiometricTokenIssuedSessionAttributesMobileApp = {
+  ...validBiometricTokenIssuedSessionAttributes,
+  redirectUri: "https://www.mockRedirectUri.com",
+};
+
+export const invalidBiometricTokenIssuedSessionAttributesWrongSessionState = {
+  ...validBiometricTokenIssuedSessionAttributes,
+  sessionState: SessionState.AUTH_SESSION_CREATED,
+};
+
+export const invalidBiometricTokenIssuedSessionAttributeTypes = {
+  ...validBiometricTokenIssuedSessionAttributes,
+  issuer: invalidBaseSessionAttributeTypes.issuer,
+  timeToLive: invalidBaseSessionAttributeTypes.timeToLive,
+};
+
+// Biometric session finished
+
 export const validBiometricSessionFinishedAttributes = {
   ...validBiometricTokenIssuedSessionAttributes,
   sessionState: SessionState.BIOMETRIC_SESSION_FINISHED,
   biometricSessionId: mockBiometricSessionId,
 };
+
+export const validBiometricSessionFinishedAttributesMobileApp = {
+  ...validBiometricSessionFinishedAttributes,
+  redirectUri: "https://www.mockRedirectUri.com",
+};
+
+// Session registry
+
+export const mockInertSessionRegistry: SessionRegistry = {
+  updateSession: jest.fn(() => {
+    throw new Error("Not implemented");
+  }),
+
+  getSession: jest.fn(() => {
+    throw new Error("Not implemented");
+  }),
+};
+
+// Event service
path: backend-api/src/functions/testUtils/unitTestData.ts
user: login: jhumbert-dd
body: Removed
created_at: 2025-03-21T16:28:01Z
author_association: CONTRIBUTOR
diff_hunk: @@ -90,8 +145,101 @@ describe("Async TxMA Event", () => {
     });
   });
 
+  describe("Retrieving a session", () => {
path: backend-api/src/functions/asyncTxmaEvent/asyncTxmaEventHandler.test.ts
user: login: jhumbert-dd
body: Updated
created_at: 2025-03-21T16:58:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,26 +1,29 @@
 import { expect } from "@jest/globals";
-import "../../../tests/testUtils/matchers";
-import "dotenv/config";
 import { APIGatewayProxyResult, Context } from "aws-lambda";
+import "dotenv/config";
path: backend-api/src/functions/asyncBiometricToken/asyncBiometricTokenHandler.test.ts
user: login: jhumbert-dd
body: Updated
created_at: 2025-03-21T16:58:28Z
author_association: CONTRIBUTOR
diff_hunk: @@ -31,23 +37,75 @@ describe("POST /async/txmaEvent", () => {
     });
   });
 
-  describe("Given there is a valid request", () => {
+  describe("Given the session is not valid", () => {
     let response: AxiosResponse;
+    let sessionId: string;
 
     beforeAll(async () => {
+      sessionId = mockSessionId;
       const requestBody = {
-        sessionId: mockSessionId,
+        sessionId,
         eventName: "DCMAW_ASYNC_HYBRID_BILLING_STARTED",
       };
 
       response = await SESSIONS_API_INSTANCE.post(
         "/async/txmaEvent",
         requestBody,
       );
+    });
 
-      console.log("RESPONSE >>>>>", response);
+    it("Returns an error and 401 status code", async () => {
+      expect(response.status).toBe(401);
+      expect(response.data).toStrictEqual({
+        error: "invalid_session",
+        error_description: "Session does not exist or in incorrect state",
+      });
+      expect(response.headers).toEqual(
+        expect.objectContaining(expectedSecurityHeaders),
+      );
     });
 
+    it("Writes an event with the correct event_name", async () => {
+      const eventsResponse = await pollForEvents({
+        partitionKey: `SESSION#${sessionId}`,
+        sortKeyPrefix: `TXMA#EVENT_NAME#DCMAW_ASYNC_CRI_4XXERROR`,
+        numberOfEvents: 1,
+      });
+
+      expect(eventsResponse[0].event).toEqual(
+        expect.objectContaining({
+          event_name: "DCMAW_ASYNC_CRI_4XXERROR",
+        }),
+      );
+    }, 40000);
+  });
+
+  describe("Given the request is valid", () => {
+    let sessionId: string | null;
+    let response: AxiosResponse;
+
+    beforeAll(async () => {
+      const sub = randomUUID();
+      await createSessionForSub(sub);
+      sessionId = await getActiveSessionIdFromSub(sub);
+      const biometricTokenRequestBody = {
+        sessionId,
+        documentType: "NFC_PASSPORT",
+      };
+      await SESSIONS_API_INSTANCE.post(
+        "/async/biometricToken",
+        biometricTokenRequestBody,
+      );
path: backend-api/tests/api-tests/txmaEvent.test.ts
user: login: jhumbert-dd
body: updated
created_at: 2025-03-21T17:02:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,25 @@
+import { AttributeValue, GetItemCommandInput } from "@aws-sdk/client-dynamodb";
+import { Result } from "../../../utils/result";
+import { SessionAttributes } from "../session";
+import {
+  ValidateSessionErrorInvalidAttributeData,
+  ValidateSessionAttributes,
+} from "../SessionRegistry";
+
+export interface GetSessionOperation {
+  getDynamoDbGetCommandInput({
path: backend-api/src/functions/common/session/getOperations/GetSessionOperation.ts
user: login: sandymay-dd
body: I've suggested leaving it as-is given where we are in the sprint, We agree with the sentiment of the comment and ordinarily would action it.

This is one to revisit fresh when we implement this interface for the issueBiometricCredential lambda.


created_at: 2025-03-21T17:14:00Z
author_association: CONTRIBUTOR
path: backend-api/src/functions/common/session/SessionRegistry.ts
user: login: sandymay-dd
body: We agree that there's type proliferation. A not for now one, given you said it's non-blocking. 
created_at: 2025-03-21T17:22:01Z
author_association: CONTRIBUTOR
diff_hunk: @@ -293,4 +309,234 @@ describe("DynamoDbAdapter", () => {
       });
     });
   });
+
+  describe("getSession", () => {
+    const getOperation: GetSessionOperation = new TxMAEvent();
+    let result: Result<void, SessionRetrievalFailed>;
+
+    describe("On every attempt", () => {
+      beforeEach(async () => {
+        mockDynamoDbClient.on(GetItemCommand).resolves({});
+        await sessionRegistry.getSession(mockSessionId, getOperation);
+      });
+
+      it("Logs the attempt", () => {
+        expect(consoleDebugSpy).toHaveBeenCalledWithLogFields({
+          messageCode: "MOBILE_ASYNC_GET_SESSION_ATTEMPT",
+          data: {
+            sessionId: mockSessionId,
+            getItemCommandInput:
+              getOperation.getDynamoDbKeyExpression(mockSessionId),
+          },
+        });
+      });
+    });
+
+    describe("Given there is an unexpected error retrieving the session", () => {
+      beforeEach(async () => {
+        mockDynamoDbClient.on(GetItemCommand).rejects("mock_error");
+        result = await sessionRegistry.getSession(mockSessionId, getOperation);
+      });
+
+      it("Logs the failure", () => {
+        expect(consoleErrorSpy).toHaveBeenCalledWithLogFields({
+          messageCode: "MOBILE_ASYNC_GET_SESSION_UNEXPECTED_FAILURE",
+        });
+      });
+
+      it("Returns failure with server error", () => {
+        expect(result).toEqual(
+          errorResult({
+            errorType: GetSessionError.INTERNAL_SERVER_ERROR,
+          }),
+        );
+      });
+    });
+
+    describe("Given the session was not found", () => {
+      beforeEach(async () => {
+        mockDynamoDbClient.on(GetItemCommand).resolves({});
+        result = await sessionRegistry.getSession(mockSessionId, getOperation);
+      });
+
+      it("Logs the failure", () => {
+        expect(consoleErrorSpy).toHaveBeenCalledWithLogFields({
+          messageCode: "MOBILE_ASYNC_GET_SESSION_SESSION_NOT_FOUND",
+          session: "Session not found",
+        });
+      });
+
+      it("Returns failure with an invalid session error", () => {
+        expect(result).toEqual(
+          errorResult({
+            errorType: GetSessionError.SESSION_NOT_FOUND,
+            session: "Session not found",
+          }),
+        );
+      });
+    });
+
+    describe("Session validation", () => {
+      describe("Given the session is in the wrong state", () => {
+        const invalidBiometricTokenIssuedSessionAttributesWrongSessionState = {
+          ...validBiometricTokenIssuedSessionAttributes,
+          createdAt: 1704106860000, // 2024-01-01 11:01:00.000
+          sessionState: SessionState.AUTH_SESSION_CREATED,
+        };
+        const invalidBiometricTokenIssuedSessionAttributesWrongSessionStateItem =
+          {
+            Item: marshall(
+              invalidBiometricTokenIssuedSessionAttributesWrongSessionState,
+            ),
+          };
+
+        beforeEach(async () => {
+          mockDynamoDbClient
+            .on(GetItemCommand)
+            .resolves(
+              invalidBiometricTokenIssuedSessionAttributesWrongSessionStateItem,
+            );
+          result = await sessionRegistry.getSession(
+            mockSessionId,
+            getOperation,
+          );
+        });
+
+        it("Logs the failure", () => {
+          expect(consoleErrorSpy).toHaveBeenCalledWithLogFields({
+            messageCode: "MOBILE_ASYNC_GET_SESSION_SESSION_NOT_FOUND",
+            errorMessage:
+              "Session could not be parsed or session validation failed",
+            session:
+              invalidBiometricTokenIssuedSessionAttributesWrongSessionState,
+          });
+        });
+
+        it("Returns failure with an invalid session error", () => {
+          expect(result).toStrictEqual(
+            errorResult({
+              errorType: GetSessionError.SESSION_NOT_FOUND,
+              session:
+                invalidBiometricTokenIssuedSessionAttributesWrongSessionState,
+            }),
+          );
+        });
+      });
+
+      describe("Given the session is more than 60 minutes old", () => {
+        const invalidBiometricTokenIssuedSessionAttributesSessionTooOld = {
+          ...validBiometricTokenIssuedSessionAttributes,
+          createdAt: 1704106740000, // 2024-01-01 10:59:00.000
+        };
+        const biometricTokenIssuedSessionAttributesSessionTooOldItem = {
+          Item: marshall(
+            invalidBiometricTokenIssuedSessionAttributesSessionTooOld,
+          ),
+        };
+
+        beforeEach(async () => {
+          mockDynamoDbClient
+            .on(GetItemCommand)
+            .resolves(biometricTokenIssuedSessionAttributesSessionTooOldItem);
+          result = await sessionRegistry.getSession(
+            mockSessionId,
+            getOperation,
+          );
+        });
+
+        it("Logs the failure", () => {
+          expect(consoleErrorSpy).toHaveBeenCalledWithLogFields({
+            messageCode: "MOBILE_ASYNC_GET_SESSION_SESSION_NOT_FOUND",
+            session: invalidBiometricTokenIssuedSessionAttributesSessionTooOld,
+          });
+        });
+
+        it("Returns failure with an invalid session error", () => {
+          expect(result).toStrictEqual(
+            errorResult({
+              errorType: GetSessionError.SESSION_NOT_FOUND,
+              session:
+                invalidBiometricTokenIssuedSessionAttributesSessionTooOld,
+            }),
+          );
+        });
+      });
+    });
+
+    describe("Given the session was found", () => {
+      describe("Given invalid session attributes were returned in response", () => {
+        const invalidBiometricTokenIssuedSessionAttributesMissingAttributes = {
+          clientId: "mockClientId",
+          govukSigninJourneyId: "mockGovukSigninJourneyId",
+          createdAt: 1704106860000, // 2024-01-01 11:01:00.000
+          issuer: "mockIssuer",
+          clientState: "mockClientState",
+          subjectIdentifier: "mockSubjectIdentifier",
+          timeToLive: 12345,
+        };
+        beforeEach(async () => {
+          mockDynamoDbClient.on(GetItemCommand).resolves({
+            Item: marshall(
+              invalidBiometricTokenIssuedSessionAttributesMissingAttributes,
+            ),
+          });
+          result = await sessionRegistry.getSession(
+            mockSessionId,
+            getOperation,
+          );
+        });
+
+        it("Logs the failure", () => {
+          expect(consoleErrorSpy).toHaveBeenCalledWithLogFields({
+            messageCode: "MOBILE_ASYNC_GET_SESSION_SESSION_NOT_FOUND",
+            errorMessage:
+              "Session could not be parsed or session validation failed",
+            session:
+              invalidBiometricTokenIssuedSessionAttributesMissingAttributes,
+          });
+        });
+
+        it("Returns failure with server error", () => {
+          expect(result).toEqual(
+            errorResult({
+              errorType: GetSessionError.SESSION_NOT_FOUND,
path: backend-api/src/functions/adapters/aws/dynamo/dynamoDbAdapter.test.ts
user: login: jhumbert-dd
body: Updated
created_at: 2025-03-21T17:42:02Z
author_association: CONTRIBUTOR

PR comments:
user: login: sonarqubecloud[bot]
created_at: 2025-03-21T17:46:53Z
body: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=mobile-id-check-async&pullRequest=469) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 New issues](https://sonarcloud.io/project/issues?id=mobile-id-check-async&pullRequest=469&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=mobile-id-check-async&pullRequest=469&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=mobile-id-check-async&pullRequest=469&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [100.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=mobile-id-check-async&pullRequest=469&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=mobile-id-check-async&pullRequest=469&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=mobile-id-check-async&pullRequest=469)


author_association: NONE