Repository: govuk-one-login/authentication-api
Pull request #6273: ATO-1481: improve back channel logout
Author: login: Louisasa
Created: 2025-04-03T08:37:32Z | Merged: 2025-04-24T09:30:07Z

### Wider context of change

We want to improve the back channel logout logic to send an alarm if it fails.

### What’s changed

New cloudwatch alarm and HttpRequestService now throws if status isn't 200

### Manual testing

Tested on dev with a stub with backchannel logout url. Also removed authorize lambda to test it passes too.

### Checklist

- [x] Lambdas have correct permissions for the resources they're accessing. **N/A**
- [x] Impact on orch and auth mutual dependencies has been checked. **N/A**
- [x] Changes have been made to contract tests or not required. **N/A**
- [x] Changes have been made to the simulator or not required. **N/A**
- [x] Changes have been made to stubs or not required. **N/A**
- [x] Successfully deployed to authdev or not required.
- [x] Successfully run Authentication acceptance tests against sandpit or not required. **N/A**


Diff:
diff --git a/oidc-api/src/main/java/uk/gov/di/authentication/oidc/exceptions/PostRequestFailureException.java b/oidc-api/src/main/java/uk/gov/di/authentication/oidc/exceptions/PostRequestFailureException.java
new file mode 100644
index 00000000000..b0292dcf726
--- /dev/null
+++ b/oidc-api/src/main/java/uk/gov/di/authentication/oidc/exceptions/PostRequestFailureException.java
@@ -0,0 +1,7 @@
+package uk.gov.di.authentication.oidc.exceptions;
+
+public class PostRequestFailureException extends RuntimeException {
+    public PostRequestFailureException(String message) {
+        super(message);
+    }
+}
diff --git a/oidc-api/src/main/java/uk/gov/di/authentication/oidc/lambda/BackChannelLogoutRequestHandler.java b/oidc-api/src/main/java/uk/gov/di/authentication/oidc/lambda/BackChannelLogoutRequestHandler.java
index 6dbe3945041..f5584a6eb36 100644
--- a/oidc-api/src/main/java/uk/gov/di/authentication/oidc/lambda/BackChannelLogoutRequestHandler.java
+++ b/oidc-api/src/main/java/uk/gov/di/authentication/oidc/lambda/BackChannelLogoutRequestHandler.java
@@ -2,6 +2,7 @@
 
 import com.amazonaws.services.lambda.runtime.Context;
 import com.amazonaws.services.lambda.runtime.RequestHandler;
+import com.amazonaws.services.lambda.runtime.events.SQSBatchResponse;
 import com.amazonaws.services.lambda.runtime.events.SQSEvent;
 import com.amazonaws.services.lambda.runtime.events.SQSEvent.SQSMessage;
 import com.nimbusds.jose.JWSAlgorithm;
@@ -9,6 +10,7 @@
 import org.apache.logging.log4j.LogManager;
 import org.apache.logging.log4j.Logger;
 import org.apache.logging.log4j.ThreadContext;
+import uk.gov.di.authentication.oidc.exceptions.PostRequestFailureException;
 import uk.gov.di.authentication.oidc.services.HttpRequestService;
 import uk.gov.di.orchestration.shared.api.OidcAPI;
 import uk.gov.di.orchestration.shared.entity.BackChannelLogoutMessage;
@@ -23,6 +25,8 @@
 import java.net.URI;
 import java.time.Clock;
 import java.time.temporal.ChronoUnit;
+import java.util.ArrayList;
+import java.util.List;
 import java.util.Map;
 import java.util.Optional;
 import java.util.UUID;
@@ -77,9 +81,19 @@ public Object backChannelLogoutRequestHandler(SQSEvent event, Context context) {
 
         attachLogFieldToLogs(LogLineHelper.LogFieldName.AWS_REQUEST_ID, context.getAwsRequestId());
 
-        event.getRecords().forEach(this::sendLogoutMessage);
+        List<SQSBatchResponse.BatchItemFailure> batchItemFailures = new ArrayList<>();
 
-        return null;
+        for (SQSEvent.SQSMessage message : event.getRecords()) {
+            try {
+                sendLogoutMessage(message);
+            } catch (PostRequestFailureException e) {
+                LOG.warn(e.getMessage());
+                batchItemFailures.add(
+                        new SQSBatchResponse.BatchItemFailure(message.getMessageId()));
+            }
+        }
+
+        return new SQSBatchResponse(batchItemFailures);
     }
 
     private void sendLogoutMessage(SQSMessage record) {
diff --git a/oidc-api/src/main/java/uk/gov/di/authentication/oidc/services/HttpRequestService.java b/oidc-api/src/main/java/uk/gov/di/authentication/oidc/services/HttpRequestService.java
index b96e60bada2..9ef8e9436ec 100644
--- a/oidc-api/src/main/java/uk/gov/di/authentication/oidc/services/HttpRequestService.java
+++ b/oidc-api/src/main/java/uk/gov/di/authentication/oidc/services/HttpRequestService.java
@@ -3,6 +3,7 @@
 import org.apache.logging.log4j.LogManager;
 import org.apache.logging.log4j.Logger;
 import org.apache.logging.log4j.message.ObjectMessage;
+import uk.gov.di.authentication.oidc.exceptions.PostRequestFailureException;
 import uk.gov.di.orchestration.shared.services.CloudwatchMetricsService;
 
 import java.io.IOException;
@@ -31,19 +32,19 @@ public void post(URI uri, String body) {
         try {
             var response = newHttpClient().send(request, BodyHandlers.discarding());
 
-            var logMessage =
-                    Map.of(
-                            "uri",
-                            uri.toString(),
-                            "response-code",
-                            Integer.toString(response.statusCode()));
+            var statusCode = Integer.toString(response.statusCode());
+
+            var logMessage = Map.of("uri", uri.toString(), "response-code", statusCode);
 
             LOG.info(new ObjectMessage(logMessage));
 
             METRICS.putEmbeddedValue(
-                    "BackChannelLogoutRequest",
-                    1,
-                    Map.of("StatusCode", Integer.toString(response.statusCode())));
+                    "BackChannelLogoutRequest", 1, Map.of("StatusCode", statusCode));
+
+            if (!statusCode.equals("200")) {
+                throw new PostRequestFailureException(
+                        "Unable to execute POST request successfully. Status code: " + statusCode);
+            }
 
         } catch (IOException e) {
             LOG.error("Unable to execute POST request successfully: {}", e.getMessage());
diff --git a/oidc-api/src/test/java/uk/gov/di/authentication/oidc/lambda/BackChannelLogoutRequestHandlerTest.java b/oidc-api/src/test/java/uk/gov/di/authentication/oidc/lambda/BackChannelLogoutRequestHandlerTest.java
index 89c934f4be7..40097f35f92 100644
--- a/oidc-api/src/test/java/uk/gov/di/authentication/oidc/lambda/BackChannelLogoutRequestHandlerTest.java
+++ b/oidc-api/src/test/java/uk/gov/di/authentication/oidc/lambda/BackChannelLogoutRequestHandlerTest.java
@@ -1,6 +1,7 @@
 package uk.gov.di.authentication.oidc.lambda;
 
 import com.amazonaws.services.lambda.runtime.Context;
+import com.amazonaws.services.lambda.runtime.events.SQSBatchResponse;
 import com.amazonaws.services.lambda.runtime.events.SQSEvent;
 import com.nimbusds.jwt.JWTClaimsSet;
 import com.nimbusds.jwt.SignedJWT;
@@ -9,6 +10,7 @@
 import org.hamcrest.TypeSafeMatcher;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
+import uk.gov.di.authentication.oidc.exceptions.PostRequestFailureException;
 import uk.gov.di.authentication.oidc.services.HttpRequestService;
 import uk.gov.di.orchestration.shared.api.OidcAPI;
 import uk.gov.di.orchestration.shared.entity.BackChannelLogoutMessage;
@@ -20,6 +22,7 @@
 import java.sql.Date;
 import java.text.ParseException;
 import java.time.Instant;
+import java.util.ArrayList;
 import java.util.List;
 import java.util.Optional;
 import java.util.UUID;
@@ -34,6 +37,7 @@
 import static org.hamcrest.core.Is.is;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doThrow;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.verify;
@@ -90,6 +94,46 @@ void shouldSendRequestToRelyingPartyEndpoint() {
                 .post(URI.create("https://test.account.gov.uk"), "logout_token=serialized-payload");
     }
 
+    @Test
+    void shouldReturnBatchItemFailuresWhenSendRequestFails() {
+        var firstInput =
+                new BackChannelLogoutMessage(
+                        "client-id", "https://test-1.account.gov.uk", "some-subject-id");
+        var secondInput =
+                new BackChannelLogoutMessage(
+                        "client-id", "https://test-2.account.gov.uk", "some-subject-id");
+
+        var jwt = mock(SignedJWT.class);
+
+        when(jwt.serialize()).thenReturn("serialized-payload");
+
+        when(tokenService.generateSignedJwtUsingExternalKey(
+                        any(JWTClaimsSet.class), eq(Optional.of("logout+jwt")), eq(ES256)))
+                .thenReturn(jwt);
+
+        doThrow(new PostRequestFailureException("Post request failed"))
+                .when(request)
+                .post(
+                        URI.create("https://test-2.account.gov.uk"),
+                        "logout_token=serialized-payload");
+
+        var firstMessage = getMessages(firstInput, "firstMessageId");
+        var secondMessage = getMessages(secondInput, "secondMessageId");
+        List<SQSEvent.SQSMessage> messageList = new ArrayList<>();
+        firstMessage.ifPresent(messageList::add);
+        secondMessage.ifPresent(messageList::add);
+
+        var event = new SQSEvent();
+        event.setRecords(messageList);
+
+        var result = handler.handleRequest(event, context);
+
+        List<SQSBatchResponse.BatchItemFailure> batchItemFailures =
+                List.of(new SQSBatchResponse.BatchItemFailure("secondMessageId"));
+
+        assertThat(result, is(new SQSBatchResponse(batchItemFailures)));
+    }
+
     @Test
     void shouldCreateClaimsForBackChannelLogoutMessage() throws ParseException {
         var jwt =
@@ -129,22 +173,24 @@ public void describeTo(Description description) {
     }
 
     private SQSEvent inputEvent(BackChannelLogoutMessage payload) {
-        var messages =
-                Optional.ofNullable(payload)
-                        .map(unchecked(SerializationService.getInstance()::writeValueAsString))
-                        .map(
-                                body -> {
-                                    var message = new SQSEvent.SQSMessage();
-                                    message.setBody(body);
-
-                                    return message;
-                                })
-                        .map(List::of)
-                        .orElse(emptyList());
-
+        var messages = getMessages(payload, "messageId");
         var event = new SQSEvent();
-        event.setRecords(messages);
+        event.setRecords(messages.map(List::of).orElse(emptyList()));
 
         return event;
     }
+
+    private Optional<SQSEvent.SQSMessage> getMessages(
+            BackChannelLogoutMessage payload, String messageId) {
+        return Optional.ofNullable(payload)
+                .map(unchecked(SerializationService.getInstance()::writeValueAsString))
+                .map(
+                        body -> {
+                            var message = new SQSEvent.SQSMessage();
+                            message.setBody(body);
+                            message.setMessageId(messageId);
+
+                            return message;
+                        });
+    }
 }
diff --git a/template.yaml b/template.yaml
index d37327b0cb8..623f0c41c75 100644
--- a/template.yaml
+++ b/template.yaml
@@ -63,6 +63,7 @@ Conditions:
       !Equals [production, !Ref Environment],
     ]
   IsNotProduction: !Not [!Equals [!Ref Environment, production]]
+  IsNotIntegration: !Not [!Equals [!Ref Environment, integration]]
   IsNotDevOrProd:
     !Not [
       !Or [
@@ -1262,6 +1263,8 @@ Resources:
           Type: SQS
           Properties:
             Queue: !GetAtt BackChannelLogoutQueue.Arn
+            FunctionResponseTypes:
+              - ReportBatchItemFailures
       Environment:
         Variables:
           # checkov:skip=CKV_AWS_173: These environment variables do not require encryption.
@@ -4355,6 +4358,29 @@ Resources:
       KeyUsage: ENCRYPT_DECRYPT
       KeySpec: SYMMETRIC_DEFAULT
       EnableKeyRotation: true
+
+  BackChannelLogoutDLQAlarm:
+    Type: AWS::CloudWatch::Alarm
+    Condition: IsNotIntegration
+    Properties:
+      AlarmName: !Sub ${Environment}-backchannel-logout-dlq-alarm
+      AlarmDescription: !Sub "${Environment} backchannel logout DLQ has a message.ACCOUNT: di-orchestration-${Environment}. Runbook: https://govukverify.atlassian.net/wiki/spaces/Orch/pages/5292228639/Runbook+Backchannel+Logout+DLQ+Alarm"
+      ActionsEnabled: true
+      AlarmActions:
+        - !Ref SlackEvents
+      Namespace: AWS/SQS
+      MetricName: ApproximateNumberOfMessagesVisible
+      Dimensions:
+        - Name: QueueName
+          Value:
+            Fn::GetAtt:
+              - BackChannelLogoutDeadLetterQueue
+              - QueueName
+      Statistic: Sum
+      Period: 300
+      EvaluationPeriods: 1
+      Threshold: 1
+      ComparisonOperator: GreaterThanThreshold
   #endregion
 
   #region Storage Token Jwk Lambda


Reviews:
user: login: Louisasa
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-04T08:32:26Z
user: login: sw-mattw
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-09T15:42:40Z
user: login: CarlyG55
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-09T15:47:45Z
user: login: Ryan-Andrews99
body: Super minor comments, overall looks good!
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-09T16:06:45Z
user: login: Louisasa
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-09T16:16:44Z
user: login: CarlyG55
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-10T09:21:38Z
user: login: sw-mattw
body: Just a minor query thanks
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-16T09:06:22Z
user: login: sw-mattw
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-16T10:21:30Z
user: login: sw-mattw
state: DISMISSED
author_association: CONTRIBUTOR
submitted_at: 2025-04-16T10:37:26Z
user: login: sw-mattw
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-24T08:57:46Z

Review comments:
diff_hunk: @@ -4227,6 +4227,26 @@ Resources:
       KeyUsage: ENCRYPT_DECRYPT
       KeySpec: SYMMETRIC_DEFAULT
       EnableKeyRotation: true
+
+  QueueDepthAlarm:
+    Type: AWS::CloudWatch::Alarm
+    Properties:
+      AlarmName: !Sub ${Environment}-backchannel-logout-dlq-depth-alarm
+      AlarmDescription: !Sub "${Environment} backchannel logout DLQ has a message.ACCOUNT: di-orchestration-${Environment}. Runbook: TBC"
path: template.yaml
user: login: Louisasa
body: need to add runbook (maybe)
created_at: 2025-04-04T08:32:26Z
author_association: CONTRIBUTOR
diff_hunk: @@ -4227,6 +4227,26 @@ Resources:
       KeyUsage: ENCRYPT_DECRYPT
       KeySpec: SYMMETRIC_DEFAULT
       EnableKeyRotation: true
+
+  QueueDepthAlarm:
+    Type: AWS::CloudWatch::Alarm
+    Properties:
+      AlarmName: !Sub ${Environment}-backchannel-logout-dlq-depth-alarm
+      AlarmDescription: !Sub "${Environment} backchannel logout DLQ has a message.ACCOUNT: di-orchestration-${Environment}. Runbook: TBC"
path: template.yaml
user: login: sw-mattw
body: Could we maybe add a placeholder runbook page in confluence and add the link in here
created_at: 2025-04-09T15:42:40Z
author_association: CONTRIBUTOR
diff_hunk: @@ -31,19 +32,19 @@ public void post(URI uri, String body) {
         try {
             var response = newHttpClient().send(request, BodyHandlers.discarding());
 
-            var logMessage =
-                    Map.of(
-                            "uri",
-                            uri.toString(),
-                            "response-code",
-                            Integer.toString(response.statusCode()));
+            var statusCode = Integer.toString(response.statusCode());
+
+            var logMessage = Map.of("uri", uri.toString(), "response-code", statusCode);
 
             LOG.info(new ObjectMessage(logMessage));
path: oidc-api/src/main/java/uk/gov/di/authentication/oidc/services/HttpRequestService.java
user: login: CarlyG55
body: Are RPs definitely all replying with a 200 right now? Can we take a look at these logs?

It wouldn't surprise me if, for example, someone currently returns a 204 (even though I know we tell them to return a 200...)
created_at: 2025-04-09T15:47:45Z
author_association: CONTRIBUTOR
diff_hunk: @@ -31,19 +32,19 @@ public void post(URI uri, String body) {
         try {
             var response = newHttpClient().send(request, BodyHandlers.discarding());
 
-            var logMessage =
-                    Map.of(
-                            "uri",
-                            uri.toString(),
-                            "response-code",
-                            Integer.toString(response.statusCode()));
+            var statusCode = Integer.toString(response.statusCode());
+
+            var logMessage = Map.of("uri", uri.toString(), "response-code", statusCode);
 
             LOG.info(new ObjectMessage(logMessage));
 
             METRICS.putEmbeddedValue(
-                    "BackChannelLogoutRequest",
-                    1,
-                    Map.of("StatusCode", Integer.toString(response.statusCode())));
+                    "BackChannelLogoutRequest", 1, Map.of("StatusCode", statusCode));
+
+            if (!statusCode.equals("200")) {
+                throw new PostRequestFailureException(
+                        "Unable to execute POST request successfully. State code: " + statusCode);
path: oidc-api/src/main/java/uk/gov/di/authentication/oidc/services/HttpRequestService.java
user: login: Ryan-Andrews99
body: Typo, think this should say
` "Unable to execute POST request successfully. Status code: " + statusCode"`
created_at: 2025-04-09T16:01:51Z
author_association: CONTRIBUTOR
diff_hunk: @@ -90,6 +93,32 @@ void shouldSendRequestToRelyingPartyEndpoint() {
                 .post(URI.create("https://test.account.gov.uk"), "logout_token=serialized-payload");
     }
 
+    @Test
+    void shouldReturnBatchItemFailuresWhenSendRequestFails() {
+        var input =
+                new BackChannelLogoutMessage(
+                        "client-id", "https://test.account.gov.uk", "some-subject-id");
+
+        var jwt = mock(SignedJWT.class);
+
+        when(jwt.serialize()).thenReturn("serialized-payload");
+
+        when(tokenService.generateSignedJwtUsingExternalKey(
+                        any(JWTClaimsSet.class), eq(Optional.of("logout+jwt")), eq(ES256)))
+                .thenReturn(jwt);
+
+        doThrow(new PostRequestFailureException("Post request failed"))
+                .when(request)
+                .post(URI.create("https://test.account.gov.uk"), "logout_token=serialized-payload");
+
+        var result = handler.handleRequest(inputEvent(input), context);
+
+        List<SQSBatchResponse.BatchItemFailure> batchItemFailures =
+                List.of(new SQSBatchResponse.BatchItemFailure(null));
+
+        assertThat(result, is(new SQSBatchResponse(batchItemFailures)));
path: oidc-api/src/test/java/uk/gov/di/authentication/oidc/lambda/BackChannelLogoutRequestHandlerTest.java
user: login: Ryan-Andrews99
body: Hmm it would be nice to give the record a message id and assert it ends up in this list instead of a list containing null
created_at: 2025-04-09T16:04:04Z
author_association: CONTRIBUTOR
path: oidc-api/src/test/java/uk/gov/di/authentication/oidc/lambda/BackChannelLogoutRequestHandlerTest.java
user: login: Ryan-Andrews99
body: Could we pop the test additions in the previous commit? Also could be nice to add a test here for an event with two records, on which throws and one which doesn't. Then we can assert that only one message ID is in the list.
created_at: 2025-04-09T16:05:52Z
author_association: CONTRIBUTOR
path: oidc-api/src/main/java/uk/gov/di/authentication/oidc/lambda/BackChannelLogoutRequestHandler.java
user: login: Ryan-Andrews99
body: This can be squashed into the previous commit 
created_at: 2025-04-09T16:06:06Z
author_association: CONTRIBUTOR
diff_hunk: @@ -31,19 +32,19 @@ public void post(URI uri, String body) {
         try {
             var response = newHttpClient().send(request, BodyHandlers.discarding());
 
-            var logMessage =
-                    Map.of(
-                            "uri",
-                            uri.toString(),
-                            "response-code",
-                            Integer.toString(response.statusCode()));
+            var statusCode = Integer.toString(response.statusCode());
+
+            var logMessage = Map.of("uri", uri.toString(), "response-code", statusCode);
 
             LOG.info(new ObjectMessage(logMessage));
path: oidc-api/src/main/java/uk/gov/di/authentication/oidc/services/HttpRequestService.java
user: login: Louisasa
body: In the last 30 days none have returned 204, 1 returned 400 (now fixed) and 1 returned 503 (now fixed), the rest are 200s
created_at: 2025-04-09T16:16:44Z
author_association: CONTRIBUTOR
diff_hunk: @@ -31,19 +32,19 @@ public void post(URI uri, String body) {
         try {
             var response = newHttpClient().send(request, BodyHandlers.discarding());
 
-            var logMessage =
-                    Map.of(
-                            "uri",
-                            uri.toString(),
-                            "response-code",
-                            Integer.toString(response.statusCode()));
+            var statusCode = Integer.toString(response.statusCode());
+
+            var logMessage = Map.of("uri", uri.toString(), "response-code", statusCode);
 
             LOG.info(new ObjectMessage(logMessage));
path: oidc-api/src/main/java/uk/gov/di/authentication/oidc/services/HttpRequestService.java
user: login: CarlyG55
body: Ah okay great - in that case happy with that 🙂 
created_at: 2025-04-10T09:21:38Z
author_association: CONTRIBUTOR
diff_hunk: @@ -90,6 +93,44 @@ void shouldSendRequestToRelyingPartyEndpoint() {
                 .post(URI.create("https://test.account.gov.uk"), "logout_token=serialized-payload");
     }
 
+    @Test
+    void shouldReturnBatchItemFailuresWhenSendRequestFails() {
+        var firstInput =
+                new BackChannelLogoutMessage(
+                        "client-id", "https://test-1.account.gov.uk", "some-subject-id");
+        var secondInput =
+                new BackChannelLogoutMessage(
+                        "client-id", "https://test-2.account.gov.uk", "some-subject-id");
+
+        var jwt = mock(SignedJWT.class);
+
+        when(jwt.serialize()).thenReturn("serialized-payload");
+
+        when(tokenService.generateSignedJwtUsingExternalKey(
+                        any(JWTClaimsSet.class), eq(Optional.of("logout+jwt")), eq(ES256)))
+                .thenReturn(jwt);
+
+        doThrow(new PostRequestFailureException("Post request failed"))
+                .when(request)
+                .post(
+                        URI.create("https://test-2.account.gov.uk"),
+                        "logout_token=serialized-payload");
+
+        var firstMessage = getMessages(firstInput, "firstMessageId");
+        var secondMessage = getMessages(secondInput, "secondMessageId");
+
+        var event = new SQSEvent();
+        event.setRecords(firstMessage);
path: oidc-api/src/test/java/uk/gov/di/authentication/oidc/lambda/BackChannelLogoutRequestHandlerTest.java
user: login: sw-mattw
body: Do we need to set this first event? Just thinking it'll be overwritten by the line below
created_at: 2025-04-16T08:52:49Z
author_association: CONTRIBUTOR
diff_hunk: @@ -90,6 +93,44 @@ void shouldSendRequestToRelyingPartyEndpoint() {
                 .post(URI.create("https://test.account.gov.uk"), "logout_token=serialized-payload");
     }
 
+    @Test
+    void shouldReturnBatchItemFailuresWhenSendRequestFails() {
+        var firstInput =
+                new BackChannelLogoutMessage(
+                        "client-id", "https://test-1.account.gov.uk", "some-subject-id");
+        var secondInput =
+                new BackChannelLogoutMessage(
+                        "client-id", "https://test-2.account.gov.uk", "some-subject-id");
+
+        var jwt = mock(SignedJWT.class);
+
+        when(jwt.serialize()).thenReturn("serialized-payload");
+
+        when(tokenService.generateSignedJwtUsingExternalKey(
+                        any(JWTClaimsSet.class), eq(Optional.of("logout+jwt")), eq(ES256)))
+                .thenReturn(jwt);
+
+        doThrow(new PostRequestFailureException("Post request failed"))
+                .when(request)
+                .post(
+                        URI.create("https://test-2.account.gov.uk"),
+                        "logout_token=serialized-payload");
+
+        var firstMessage = getMessages(firstInput, "firstMessageId");
+        var secondMessage = getMessages(secondInput, "secondMessageId");
+
+        var event = new SQSEvent();
+        event.setRecords(firstMessage);
path: oidc-api/src/test/java/uk/gov/di/authentication/oidc/lambda/BackChannelLogoutRequestHandlerTest.java
user: login: sw-mattw
body: Should we place `firstMessage` and `secondMessage` in the same list and call `setRecords` once?
created_at: 2025-04-16T09:04:56Z
author_association: CONTRIBUTOR
diff_hunk: @@ -90,6 +93,44 @@ void shouldSendRequestToRelyingPartyEndpoint() {
                 .post(URI.create("https://test.account.gov.uk"), "logout_token=serialized-payload");
     }
 
+    @Test
+    void shouldReturnBatchItemFailuresWhenSendRequestFails() {
+        var firstInput =
+                new BackChannelLogoutMessage(
+                        "client-id", "https://test-1.account.gov.uk", "some-subject-id");
+        var secondInput =
+                new BackChannelLogoutMessage(
+                        "client-id", "https://test-2.account.gov.uk", "some-subject-id");
+
+        var jwt = mock(SignedJWT.class);
+
+        when(jwt.serialize()).thenReturn("serialized-payload");
+
+        when(tokenService.generateSignedJwtUsingExternalKey(
+                        any(JWTClaimsSet.class), eq(Optional.of("logout+jwt")), eq(ES256)))
+                .thenReturn(jwt);
+
+        doThrow(new PostRequestFailureException("Post request failed"))
+                .when(request)
+                .post(
+                        URI.create("https://test-2.account.gov.uk"),
+                        "logout_token=serialized-payload");
+
+        var firstMessage = getMessages(firstInput, "firstMessageId");
+        var secondMessage = getMessages(secondInput, "secondMessageId");
+
+        var event = new SQSEvent();
+        event.setRecords(firstMessage);
path: oidc-api/src/test/java/uk/gov/di/authentication/oidc/lambda/BackChannelLogoutRequestHandlerTest.java
user: login: sw-mattw
body: Thanks
created_at: 2025-04-16T10:21:30Z
author_association: CONTRIBUTOR

PR comments: