Repository: govuk-one-login/di-account-management-frontend
Pull request #2200: OLH-2664: create a logout return route to handle redirects after calling OIDC end session
Author: login: latifcabinetoffice
Created: 2025-05-20T15:19:55Z | Merged: 2025-05-23T11:43:48Z

## Proposed changes

<!-- Provide a general summary of your changes in the title above -->
<!-- Include the Jira ticket number in square brackets as prefix, eg `[OLH-XXXX] PR Title` -->
[OLH-2664] Create a logout return route to handle redirects after calling OIDC end session
### What changed
<!-- Describe the changes in detail - the "what"-->
Created a new logout-redirect route which the oidc will redirect to. We will then use the state to redirect the user somewhere in home. Currently supported redirects are: temp suspended, perm blocked, account deleted, and default

### Why did it change
<!-- Describe the reason these changes were made - the "why" -->
Adding post logout redirects requires support from another team which is adding overhead. With this implementation we will only require one url and can use the state value to redirect within home. The state is validated our side and defaults to sign out.

### Testing
Tested locally with temp suspended, perm suspended, account deletion and normal logout.

[OLH-2664]: https://govukverify.atlassian.net/browse/OLH-2664?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ

Awaiting OIDC changes before merge.


Diff:
diff --git a/src/app.constants.ts b/src/app.constants.ts
index 6b83de7aa..c29210f3a 100644
--- a/src/app.constants.ts
+++ b/src/app.constants.ts
@@ -1,3 +1,4 @@
+import { getBaseUrl } from "./config";
 import { EventType, UserJourney } from "./utils/state-machine";
 
 export const PATH_DATA: Record<
@@ -185,6 +186,9 @@ export const PATH_DATA: Record<
   UNAVAILABLE_TEMPORARY: {
     url: "/unavailable-temporary",
   },
+  LOGOUT_REDIRECT: {
+    url: "/logout-redirect",
+  },
 };
 
 export const MFA_METHODS = {
@@ -324,3 +328,27 @@ export const OIDC_ERRORS = {
 export const SESSION_ID_UNKNOWN = "session-id-unknown";
 export const CLIENT_SESSION_ID_UNKNOWN = "client-session-id-unknown";
 export const PERSISTENT_SESSION_ID_UNKNOWN = "persistent-session-id-unknown";
+
+export const LogoutState = {
+  Suspended: "suspended",
+  Blocked: "blocked",
+  AccountDeletion: "accountDeletion",
+  Default: "default",
+} as const;
+
+export type LogoutStateType = (typeof LogoutState)[keyof typeof LogoutState];
+
+export const LogoutRedirect = {
+  [LogoutState.Suspended]: {
+    url: getBaseUrl() + PATH_DATA.UNAVAILABLE_TEMPORARY.url,
+  },
+  [LogoutState.Blocked]: {
+    url: getBaseUrl() + PATH_DATA.UNAVAILABLE_PERMANENT.url,
+  },
+  [LogoutState.AccountDeletion]: {
+    url: getBaseUrl() + PATH_DATA.ACCOUNT_DELETED_CONFIRMATION.url,
+  },
+  [LogoutState.Default]: {
+    url: getBaseUrl() + PATH_DATA.USER_SIGNED_OUT.url,
+  },
+};
diff --git a/src/app.ts b/src/app.ts
index f5ebc0585..61815afd8 100644
--- a/src/app.ts
+++ b/src/app.ts
@@ -76,6 +76,7 @@ import { addBackupSmsRouter } from "./components/add-mfa-method-sms/add-mfa-meth
 import { deleteMfaMethodRouter } from "./components/delete-mfa-method/delete-mfa-method-routes";
 import { switchBackupMethodRouter } from "./components/switch-backup-method/switch-backup-method-routes";
 import { changeDefaultMethodRouter } from "./components/change-default-method/change-default-method-routes";
+import { logoutRedirectRouter } from "./components/logout-redirect/logout-redirect-routes";
 import { isUserLoggedInMiddleware } from "./middleware/is-user-logged-in-middleware";
 import { applyOverloadProtection } from "./middleware/overload-protection-middleware";
 import { getOIDCClient } from "./utils/oidc";
@@ -229,6 +230,7 @@ async function createApp(): Promise<express.Application> {
   app.use(yourServicesRouter);
   app.use(oidcAuthCallbackRouter);
   app.use(startRouter);
+  app.use(logoutRedirectRouter);
   app.use(logoutRouter);
   app.use(isUserLoggedInMiddleware);
   app.use(enterPasswordRouter);
diff --git a/src/components/delete-account/delete-account-controller.ts b/src/components/delete-account/delete-account-controller.ts
index 1db530dbf..d9e14ba26 100644
--- a/src/components/delete-account/delete-account-controller.ts
+++ b/src/components/delete-account/delete-account-controller.ts
@@ -2,15 +2,15 @@ import { Request, Response } from "express";
 import { ExpressRouteFunc } from "../../types";
 import { DeleteAccountServiceInterface } from "./types";
 import { deleteAccountService } from "./delete-account-service";
-import { PATH_DATA } from "../../app.constants";
 import { EventType, getNextState } from "../../utils/state-machine";
-import { getAppEnv, getBaseUrl, getSNSDeleteTopic } from "../../config";
-import { clearCookies, destroyUserSessions } from "../../utils/session-store";
+import { getAppEnv, getSNSDeleteTopic } from "../../config";
 import {
   containsGovUkPublishingService,
   getYourServicesForAccountDeletion,
 } from "../../utils/yourServices";
 import { getTxmaHeader } from "../../utils/txma-header";
+import { handleLogout } from "../../utils/logout";
+import { LogoutState } from "../../app.constants";
 
 export async function deleteAccountGet(
   req: Request,
@@ -85,16 +85,6 @@ export function deleteAccountPost(
       EventType.ValueUpdated
     );
 
-    const logoutUrl = req.oidc.endSessionUrl({
-      id_token_hint: req.session.user.tokens.idToken,
-      post_logout_redirect_uri:
-        getBaseUrl() + PATH_DATA.ACCOUNT_DELETED_CONFIRMATION.url,
-    });
-
-    await destroyUserSessions(req, subjectId, req.app.locals.sessionStore);
-
-    clearCookies(req, res, ["am"]);
-
-    return res.redirect(logoutUrl);
+    await handleLogout(req, res, LogoutState.AccountDeletion);
   };
 }
diff --git a/src/components/delete-account/tests/delete-account-integration.test.ts b/src/components/delete-account/tests/delete-account-integration.test.ts
index 80b2e8ab7..b9378bc80 100644
--- a/src/components/delete-account/tests/delete-account-integration.test.ts
+++ b/src/components/delete-account/tests/delete-account-integration.test.ts
@@ -193,7 +193,7 @@ describe("Integration:: delete account", () => {
     );
   });
 
-  it("post should redirect to end session endpoint", (done) => {
+  it("post should redirect to end session endpoint", async () => {
     nock(baseApi).post(API_ENDPOINTS.DELETE_ACCOUNT).once().reply(204);
     nock(govUkPublishingBaseApi)
       .delete(`${API_ENDPOINTS.ALPHA_GOV_ACCOUNT}${TEST_SUBJECT_ID}`)
@@ -213,9 +213,9 @@ describe("Integration:: delete account", () => {
       .expect(
         "Location",
         `${opApi}/logout?id_token_hint=${idToken}&post_logout_redirect_uri=${encodeURIComponent(
-          `${baseUrl}${PATH_DATA.ACCOUNT_DELETED_CONFIRMATION.url}`
-        )}`
+          `${baseUrl}${PATH_DATA.LOGOUT_REDIRECT.url}`
+        )}&state=accountDeletion`
       )
-      .expect(302, done);
+      .expect(302);
   });
 });
diff --git a/src/components/enter-password/enter-password-controller.ts b/src/components/enter-password/enter-password-controller.ts
index 9dd8e3629..bf34f46a4 100644
--- a/src/components/enter-password/enter-password-controller.ts
+++ b/src/components/enter-password/enter-password-controller.ts
@@ -2,7 +2,7 @@ import { Request, Response } from "express";
 import { EnterPasswordServiceInterface } from "./types";
 import { enterPasswordService } from "./enter-password-service";
 import { ExpressRouteFunc } from "../../types";
-import { PATH_DATA } from "../../app.constants";
+import { PATH_DATA, LogoutState } from "../../app.constants";
 import {
   formatValidationError,
   renderBadRequest,
@@ -176,10 +176,10 @@ async function handleIntervention(
 ): Promise<void> {
   switch (intervention) {
     case "BLOCKED":
-      await handleLogout(req, res, PATH_DATA.UNAVAILABLE_PERMANENT.url);
+      await handleLogout(req, res, LogoutState.Blocked);
       break;
     case "SUSPENDED":
-      await handleLogout(req, res, PATH_DATA.UNAVAILABLE_TEMPORARY.url);
+      await handleLogout(req, res, LogoutState.Suspended);
       break;
     default:
       renderPasswordError(
diff --git a/src/components/enter-password/tests/enter-password-controller.test.ts b/src/components/enter-password/tests/enter-password-controller.test.ts
index e09233f01..a27b762f9 100644
--- a/src/components/enter-password/tests/enter-password-controller.test.ts
+++ b/src/components/enter-password/tests/enter-password-controller.test.ts
@@ -1,9 +1,7 @@
 import { expect } from "chai";
 import { describe } from "mocha";
-
 import { sinon } from "../../../../test/utils/test-utils";
 import { Request, Response } from "express";
-
 import {
   enterPasswordGet,
   enterPasswordPost,
@@ -194,11 +192,7 @@ describe("enter password controller", () => {
 
       await enterPasswordPost(fakeService)(req as Request, res as Response);
 
-      expect(handleLogoutStub).to.have.been.calledWith(
-        req,
-        res,
-        PATH_DATA.UNAVAILABLE_PERMANENT.url
-      );
+      expect(handleLogoutStub).to.have.been.calledWith(req, res, "blocked");
     });
 
     it("should logout and redirect to unavailable temporary when intervention is SUSPENDED", async () => {
@@ -223,11 +217,7 @@ describe("enter password controller", () => {
 
       await enterPasswordPost(fakeService)(req as Request, res as Response);
 
-      expect(handleLogoutStub).to.have.been.calledWith(
-        req,
-        res,
-        PATH_DATA.UNAVAILABLE_TEMPORARY.url
-      );
+      expect(handleLogoutStub).to.have.been.calledWith(req, res, "suspended");
     });
   });
 });
diff --git a/src/components/enter-password/tests/enter-password-integration.test.ts b/src/components/enter-password/tests/enter-password-integration.test.ts
index 21d42e3ca..9bd503701 100644
--- a/src/components/enter-password/tests/enter-password-integration.test.ts
+++ b/src/components/enter-password/tests/enter-password-integration.test.ts
@@ -384,9 +384,10 @@ describe("Integration::enter password", () => {
       .undefined;
     expect(res.headers.location).to.contain("/oidc/logout");
     expect(res.headers.location).to.contain(
-      `post_logout_redirect_uri=${encodeURIComponent(getBaseUrl() + PATH_DATA.UNAVAILABLE_PERMANENT.url)}`
+      `post_logout_redirect_uri=${encodeURIComponent(getBaseUrl() + PATH_DATA.LOGOUT_REDIRECT.url)}`
     );
     await setTokenAndCookies();
+    expect(res.headers.location).to.contain(`state=blocked`);
   });
 
   it("should redirect to unavailable temporary when intervention SUSPENDED", async () => {
@@ -412,9 +413,10 @@ describe("Integration::enter password", () => {
       .undefined;
     expect(res.headers.location).to.contain("/oidc/logout");
     expect(res.headers.location).to.contain(
-      `post_logout_redirect_uri=${encodeURIComponent(getBaseUrl() + PATH_DATA.UNAVAILABLE_TEMPORARY.url)}`
+      `post_logout_redirect_uri=${encodeURIComponent(getBaseUrl() + PATH_DATA.LOGOUT_REDIRECT.url)}`
     );
     await setTokenAndCookies();
+    expect(res.headers.location).to.contain(`state=suspended`);
   });
 
   it("should show incorrect password error for unknown intervention", async () => {
diff --git a/src/components/logout-redirect/logout-redirect-controller.ts b/src/components/logout-redirect/logout-redirect-controller.ts
new file mode 100644
index 000000000..fa4849038
--- /dev/null
+++ b/src/components/logout-redirect/logout-redirect-controller.ts
@@ -0,0 +1,19 @@
+import { Request, Response } from "express";
+import { LogoutRedirect, LogoutState } from "../../app.constants";
+
+export async function logoutRedirectGet(
+  req: Request,
+  res: Response
+): Promise<void> {
+  const state = req.query?.state;
+
+  if (state === LogoutState.Suspended) {
+    res.redirect(LogoutRedirect[LogoutState.Suspended].url);
+  } else if (state === LogoutState.Blocked) {
+    res.redirect(LogoutRedirect[LogoutState.Blocked].url);
+  } else if (state === LogoutState.AccountDeletion) {
+    res.redirect(LogoutRedirect[LogoutState.AccountDeletion].url);
+  } else {
+    res.redirect(LogoutRedirect[LogoutState.Default].url);
+  }
+}
diff --git a/src/components/logout-redirect/logout-redirect-routes.ts b/src/components/logout-redirect/logout-redirect-routes.ts
new file mode 100644
index 000000000..457b0e8e0
--- /dev/null
+++ b/src/components/logout-redirect/logout-redirect-routes.ts
@@ -0,0 +1,13 @@
+import * as express from "express";
+import { logoutRedirectGet } from "./logout-redirect-controller";
+import { PATH_DATA } from "../../app.constants";
+import { globalTryCatchAsync } from "../../utils/global-try-catch";
+
+const router = express.Router();
+
+router.get(
+  PATH_DATA.LOGOUT_REDIRECT.url,
+  globalTryCatchAsync(logoutRedirectGet)
+);
+
+export { router as logoutRedirectRouter };
diff --git a/src/components/logout-redirect/tests/logout-redirect-controller.test.ts b/src/components/logout-redirect/tests/logout-redirect-controller.test.ts
new file mode 100644
index 000000000..217b580ba
--- /dev/null
+++ b/src/components/logout-redirect/tests/logout-redirect-controller.test.ts
@@ -0,0 +1,66 @@
+import { expect } from "chai";
+import { describe } from "mocha";
+import { sinon } from "../../../../test/utils/test-utils";
+import { logoutRedirectGet } from "../logout-redirect-controller";
+
+import {
+  RequestBuilder,
+  ResponseBuilder,
+} from "../../../../test/utils/builders";
+import { LogoutState, PATH_DATA } from "../../../app.constants";
+import { getBaseUrl } from "../../../config";
+
+describe("logout redirect controller", () => {
+  let req: any;
+  let res: any;
+
+  beforeEach(() => {
+    req = new RequestBuilder().build();
+
+    res = new ResponseBuilder().withRedirect(sinon.fake(() => {})).build();
+  });
+
+  afterEach(() => {
+    sinon.restore();
+  });
+
+  it("should redirect to suspended", async () => {
+    req.query = { state: LogoutState.Suspended };
+
+    await logoutRedirectGet(req, res);
+
+    expect(res.redirect).to.have.calledWith(
+      `${getBaseUrl() + PATH_DATA.UNAVAILABLE_TEMPORARY.url}`
+    );
+  });
+
+  it("should redirect to blocked", async () => {
+    req.query = { state: LogoutState.Blocked };
+
+    await logoutRedirectGet(req, res);
+
+    expect(res.redirect).to.have.calledWith(
+      `${getBaseUrl() + PATH_DATA.UNAVAILABLE_PERMANENT.url}`
+    );
+  });
+
+  it("should redirect to account deletion", async () => {
+    req.query = { state: LogoutState.AccountDeletion };
+
+    await logoutRedirectGet(req, res);
+
+    expect(res.redirect).to.have.calledWith(
+      `${getBaseUrl() + PATH_DATA.ACCOUNT_DELETED_CONFIRMATION.url}`
+    );
+  });
+
+  it("should redirect to default if state is not set", async () => {
+    req.query = {};
+
+    await logoutRedirectGet(req, res);
+
+    expect(res.redirect).to.have.calledWith(
+      `${getBaseUrl() + PATH_DATA.USER_SIGNED_OUT.url}`
+    );
+  });
+});
diff --git a/src/components/logout/logout-controller.ts b/src/components/logout/logout-controller.ts
index 4bfcc88c4..0e3656c13 100644
--- a/src/components/logout/logout-controller.ts
+++ b/src/components/logout/logout-controller.ts
@@ -1,6 +1,7 @@
 import { Request, Response } from "express";
 import { handleLogout } from "../../utils/logout";
+import { LogoutState } from "../../app.constants";
 
 export async function logoutPost(req: Request, res: Response): Promise<void> {
-  await handleLogout(req, res);
+  await handleLogout(req, res, LogoutState.Default);
 }
diff --git a/src/utils/logout.ts b/src/utils/logout.ts
index bcb642743..0f56e19d2 100644
--- a/src/utils/logout.ts
+++ b/src/utils/logout.ts
@@ -1,25 +1,30 @@
 import { Request, Response } from "express";
-import { destroyUserSessions } from "./session-store";
+import { clearCookies, destroyUserSessions } from "./session-store";
 import { getBaseUrl } from "../config";
+import { LogoutState, LogoutStateType, PATH_DATA } from "../app.constants";
+import { EndSessionParameters } from "openid-client";
 
 export async function handleLogout(
   req: Request,
   res: Response,
-  post_logout_redirect_uri?: string
+  state: LogoutStateType
 ): Promise<void> {
-  const idToken = req.session.user.tokens.idToken;
-  await destroyUserSessions(
-    req,
-    req.session.user.subjectId,
-    req.app.locals.sessionStore
-  );
-  res.cookie("lo", "true");
+  const { idToken } = req.session.user.tokens;
+  const { subjectId } = req.session.user;
+  await destroyUserSessions(req, subjectId, req.app.locals.sessionStore);
 
-  const endSessionParams: any = { id_token_hint: idToken };
-  if (post_logout_redirect_uri) {
-    endSessionParams.post_logout_redirect_uri = `${getBaseUrl()}${post_logout_redirect_uri}}`;
+  if (state === LogoutState.AccountDeletion) {
+    clearCookies(req, res, ["am"]);
+  } else {
+    res.cookie("lo", "true");
   }
 
+  const endSessionParams: EndSessionParameters = {
+    id_token_hint: idToken,
+    post_logout_redirect_uri: `${getBaseUrl()}${PATH_DATA.LOGOUT_REDIRECT.url}`,
+    state: state,
+  };
+
   const redirectUrl = req.oidc.endSessionUrl(endSessionParams);
   res.redirect(redirectUrl);
 }


Reviews:
user: login: liegeandlief
body: A few small tweaks and changes but otherwise this looking good. Sorry about all the 'Not needed' in the test file but it was just the easiest way to highlight things which aren't being used in the tests so can be removed to make them easier to read
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-22T11:21:43Z
user: login: latifcabinetoffice
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-22T11:48:55Z
user: login: liegeandlief
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-22T12:18:38Z
user: login: latifcabinetoffice
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-22T12:53:31Z
user: login: liegeandlief
body: Lookin' good ✨ 
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-22T13:32:16Z
user: login: latifcabinetoffice
body: Block until OIDC changes
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-22T14:40:38Z

Review comments:
diff_hunk: @@ -0,0 +1,13 @@
+import * as express from "express";
+import { logoutRedirectGet } from "./logout-redirect-controller";
+import { PATH_DATA } from "../../app.constants";
+import { globalTryCatchAsync } from "../../utils/global-try-catch";
+
+const router = express.Router();
+
+router.get(
+  PATH_DATA.LOGOUT_REDIRECT.url,
path: src/components/logout-redirect/logout-redirect-routes.ts
user: login: liegeandlief
body: Probably worth putting the `requiresAuthMiddleware` middleware on this route
created_at: 2025-05-22T11:07:18Z
author_association: CONTRIBUTOR
diff_hunk: @@ -324,3 +328,31 @@ export const OIDC_ERRORS = {
 export const SESSION_ID_UNKNOWN = "session-id-unknown";
 export const CLIENT_SESSION_ID_UNKNOWN = "client-session-id-unknown";
 export const PERSISTENT_SESSION_ID_UNKNOWN = "persistent-session-id-unknown";
+
+export const LogoutState = {
+  Suspended: "suspended",
+  Blocked: "blocked",
+  ManualLogout: "manualLogout",
+  AccountDeletion: "accountDeletion",
+  Default: "default",
+} as const;
+
+export type LogoutStateType = (typeof LogoutState)[keyof typeof LogoutState];
+
+export const LogoutRedirect = {
+  [LogoutState.Suspended]: {
+    url: getBaseUrl() + PATH_DATA.UNAVAILABLE_TEMPORARY.url,
+  },
+  [LogoutState.Blocked]: {
+    url: getBaseUrl() + PATH_DATA.UNAVAILABLE_PERMANENT.url,
+  },
+  [LogoutState.ManualLogout]: {
+    url: getBaseUrl() + PATH_DATA.SIGN_OUT.url,
path: src/app.constants.ts
user: login: liegeandlief
body: I think this should be `PATH_DATA.USER_SIGNED_OUT.url`
created_at: 2025-05-22T11:09:19Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,19 @@
+import { Request, Response } from "express";
+import { LogoutRedirect, LogoutState } from "../../app.constants";
+
+export async function logoutRedirectGet(
+  req: Request,
+  res: Response
+): Promise<void> {
+  const state = req.query?.state;
+
+  if (state === LogoutState.Suspended) {
+    res.redirect(LogoutRedirect[LogoutState.Suspended].url);
+  } else if (state === LogoutState.Blocked) {
+    res.redirect(LogoutRedirect[LogoutState.Blocked].url);
+  } else if (state === LogoutState.AccountDeletion) {
+    res.redirect(LogoutRedirect[LogoutState.AccountDeletion].url);
+  } else {
+    res.redirect(LogoutRedirect[LogoutState.Default].url);
+  }
+}
path: src/components/logout-redirect/logout-redirect-controller.ts
user: login: liegeandlief
body: Need to handle manual logout case here too in case the URL changes to be different from the default
created_at: 2025-05-22T11:14:44Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,25 +1,40 @@
 import { Request, Response } from "express";
-import { destroyUserSessions } from "./session-store";
+import { clearCookies, destroyUserSessions } from "./session-store";
 import { getBaseUrl } from "../config";
+import {
+  HTTP_STATUS_CODES,
+  LogoutState,
+  LogoutStateType,
+  PATH_DATA,
+} from "../app.constants";
+import { EndSessionParameters } from "openid-client";
 
 export async function handleLogout(
   req: Request,
   res: Response,
-  post_logout_redirect_uri?: string
+  state: LogoutStateType
 ): Promise<void> {
-  const idToken = req.session.user.tokens.idToken;
-  await destroyUserSessions(
-    req,
-    req.session.user.subjectId,
-    req.app.locals.sessionStore
-  );
-  res.cookie("lo", "true");
+  try {
+    const { idToken } = req.session.user.tokens;
+    const { subjectId } = req.session.user;
+    await destroyUserSessions(req, subjectId, req.app.locals.sessionStore);
 
-  const endSessionParams: any = { id_token_hint: idToken };
-  if (post_logout_redirect_uri) {
-    endSessionParams.post_logout_redirect_uri = `${getBaseUrl()}${post_logout_redirect_uri}}`;
-  }
+    if (state === LogoutState.AccountDeletion) {
+      clearCookies(req, res, ["am"]);
+    } else {
+      res.cookie("lo", "true");
+    }
+
+    const endSessionParams: EndSessionParameters = {
+      id_token_hint: idToken,
+      post_logout_redirect_uri: `${getBaseUrl()}${PATH_DATA.LOGOUT_REDIRECT.url}`,
+      state: state,
+    };
 
-  const redirectUrl = req.oidc.endSessionUrl(endSessionParams);
-  res.redirect(redirectUrl);
+    const redirectUrl = req.oidc.endSessionUrl(endSessionParams);
+    res.redirect(redirectUrl);
+  } catch {
+    res.status(HTTP_STATUS_CODES.INTERNAL_SERVER_ERROR);
+    res.render("common/errors/500.njk");
+  }
path: src/utils/logout.ts
user: login: liegeandlief
body: Is this error handling code necessary, would it not automatically bubble up and show this error page anyway?
created_at: 2025-05-22T11:16:53Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,80 @@
+import { expect } from "chai";
+import { describe } from "mocha";
+import { sinon } from "../../../../test/utils/test-utils";
+import { logoutRedirectGet } from "../logout-redirect-controller";
+
+import {
+  RequestBuilder,
+  ResponseBuilder,
+  TXMA_AUDIT_ENCODED,
+} from "../../../../test/utils/builders";
+import { LogoutState, PATH_DATA } from "../../../app.constants";
+import { getBaseUrl } from "../../../config";
+
+describe("logout redirect controller", () => {
+  let req: any;
+  let res: any;
+
+  beforeEach(() => {
+    req = new RequestBuilder()
+      .withBody({})
path: src/components/logout-redirect/tests/logout-redirect-controller.test.ts
user: login: liegeandlief
body: Not needed
created_at: 2025-05-22T11:18:38Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,80 @@
+import { expect } from "chai";
+import { describe } from "mocha";
+import { sinon } from "../../../../test/utils/test-utils";
+import { logoutRedirectGet } from "../logout-redirect-controller";
+
+import {
+  RequestBuilder,
+  ResponseBuilder,
+  TXMA_AUDIT_ENCODED,
+} from "../../../../test/utils/builders";
+import { LogoutState, PATH_DATA } from "../../../app.constants";
+import { getBaseUrl } from "../../../config";
+
+describe("logout redirect controller", () => {
+  let req: any;
+  let res: any;
+
+  beforeEach(() => {
+    req = new RequestBuilder()
+      .withBody({})
+      .withSessionUserState({ changeAuthApp: {} })
path: src/components/logout-redirect/tests/logout-redirect-controller.test.ts
user: login: liegeandlief
body: Not needed
created_at: 2025-05-22T11:18:44Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,80 @@
+import { expect } from "chai";
+import { describe } from "mocha";
+import { sinon } from "../../../../test/utils/test-utils";
+import { logoutRedirectGet } from "../logout-redirect-controller";
+
+import {
+  RequestBuilder,
+  ResponseBuilder,
+  TXMA_AUDIT_ENCODED,
+} from "../../../../test/utils/builders";
+import { LogoutState, PATH_DATA } from "../../../app.constants";
+import { getBaseUrl } from "../../../config";
+
+describe("logout redirect controller", () => {
+  let req: any;
+  let res: any;
+
+  beforeEach(() => {
+    req = new RequestBuilder()
+      .withBody({})
+      .withSessionUserState({ changeAuthApp: {} })
+      .withTranslate(sinon.fake())
path: src/components/logout-redirect/tests/logout-redirect-controller.test.ts
user: login: liegeandlief
body: Not needed
created_at: 2025-05-22T11:18:48Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,80 @@
+import { expect } from "chai";
+import { describe } from "mocha";
+import { sinon } from "../../../../test/utils/test-utils";
+import { logoutRedirectGet } from "../logout-redirect-controller";
+
+import {
+  RequestBuilder,
+  ResponseBuilder,
+  TXMA_AUDIT_ENCODED,
+} from "../../../../test/utils/builders";
+import { LogoutState, PATH_DATA } from "../../../app.constants";
+import { getBaseUrl } from "../../../config";
+
+describe("logout redirect controller", () => {
+  let req: any;
+  let res: any;
+
+  beforeEach(() => {
+    req = new RequestBuilder()
+      .withBody({})
+      .withSessionUserState({ changeAuthApp: {} })
+      .withTranslate(sinon.fake())
+      .withHeaders({ "txma-audit-encoded": TXMA_AUDIT_ENCODED })
path: src/components/logout-redirect/tests/logout-redirect-controller.test.ts
user: login: liegeandlief
body: Not needed
created_at: 2025-05-22T11:18:52Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,80 @@
+import { expect } from "chai";
+import { describe } from "mocha";
+import { sinon } from "../../../../test/utils/test-utils";
+import { logoutRedirectGet } from "../logout-redirect-controller";
+
+import {
+  RequestBuilder,
+  ResponseBuilder,
+  TXMA_AUDIT_ENCODED,
+} from "../../../../test/utils/builders";
+import { LogoutState, PATH_DATA } from "../../../app.constants";
+import { getBaseUrl } from "../../../config";
+
+describe("logout redirect controller", () => {
+  let req: any;
+  let res: any;
+
+  beforeEach(() => {
+    req = new RequestBuilder()
+      .withBody({})
+      .withSessionUserState({ changeAuthApp: {} })
+      .withTranslate(sinon.fake())
+      .withHeaders({ "txma-audit-encoded": TXMA_AUDIT_ENCODED })
+      .build();
+
+    res = new ResponseBuilder()
+      .withRender(sinon.fake())
+      .withRedirect(sinon.fake(() => {}))
+      .withStatus(sinon.fake())
path: src/components/logout-redirect/tests/logout-redirect-controller.test.ts
user: login: liegeandlief
body: Not needed
created_at: 2025-05-22T11:19:13Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,80 @@
+import { expect } from "chai";
+import { describe } from "mocha";
+import { sinon } from "../../../../test/utils/test-utils";
+import { logoutRedirectGet } from "../logout-redirect-controller";
+
+import {
+  RequestBuilder,
+  ResponseBuilder,
+  TXMA_AUDIT_ENCODED,
+} from "../../../../test/utils/builders";
+import { LogoutState, PATH_DATA } from "../../../app.constants";
+import { getBaseUrl } from "../../../config";
+
+describe("logout redirect controller", () => {
+  let req: any;
+  let res: any;
+
+  beforeEach(() => {
+    req = new RequestBuilder()
+      .withBody({})
+      .withSessionUserState({ changeAuthApp: {} })
+      .withTranslate(sinon.fake())
+      .withHeaders({ "txma-audit-encoded": TXMA_AUDIT_ENCODED })
+      .build();
+
+    res = new ResponseBuilder()
+      .withRender(sinon.fake())
path: src/components/logout-redirect/tests/logout-redirect-controller.test.ts
user: login: liegeandlief
body: Not needed
created_at: 2025-05-22T11:19:22Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,80 @@
+import { expect } from "chai";
+import { describe } from "mocha";
+import { sinon } from "../../../../test/utils/test-utils";
+import { logoutRedirectGet } from "../logout-redirect-controller";
+
+import {
+  RequestBuilder,
+  ResponseBuilder,
+  TXMA_AUDIT_ENCODED,
+} from "../../../../test/utils/builders";
+import { LogoutState, PATH_DATA } from "../../../app.constants";
+import { getBaseUrl } from "../../../config";
+
+describe("logout redirect controller", () => {
+  let req: any;
+  let res: any;
+
+  beforeEach(() => {
+    req = new RequestBuilder()
+      .withBody({})
+      .withSessionUserState({ changeAuthApp: {} })
+      .withTranslate(sinon.fake())
+      .withHeaders({ "txma-audit-encoded": TXMA_AUDIT_ENCODED })
+      .build();
+
+    res = new ResponseBuilder()
+      .withRender(sinon.fake())
+      .withRedirect(sinon.fake(() => {}))
+      .withStatus(sinon.fake())
+      .build();
+  });
+
+  afterEach(() => {
+    sinon.restore();
+  });
+
+  it("should redirect to suspended", async () => {
+    req.query = { state: LogoutState.Suspended };
+    req.session.destroy = sinon.fake();
path: src/components/logout-redirect/tests/logout-redirect-controller.test.ts
user: login: liegeandlief
body: Not needed
created_at: 2025-05-22T11:19:36Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,80 @@
+import { expect } from "chai";
+import { describe } from "mocha";
+import { sinon } from "../../../../test/utils/test-utils";
+import { logoutRedirectGet } from "../logout-redirect-controller";
+
+import {
+  RequestBuilder,
+  ResponseBuilder,
+  TXMA_AUDIT_ENCODED,
+} from "../../../../test/utils/builders";
+import { LogoutState, PATH_DATA } from "../../../app.constants";
+import { getBaseUrl } from "../../../config";
+
+describe("logout redirect controller", () => {
+  let req: any;
+  let res: any;
+
+  beforeEach(() => {
+    req = new RequestBuilder()
+      .withBody({})
+      .withSessionUserState({ changeAuthApp: {} })
+      .withTranslate(sinon.fake())
+      .withHeaders({ "txma-audit-encoded": TXMA_AUDIT_ENCODED })
+      .build();
+
+    res = new ResponseBuilder()
+      .withRender(sinon.fake())
+      .withRedirect(sinon.fake(() => {}))
+      .withStatus(sinon.fake())
+      .build();
+  });
+
+  afterEach(() => {
+    sinon.restore();
+  });
+
+  it("should redirect to suspended", async () => {
+    req.query = { state: LogoutState.Suspended };
+    req.session.destroy = sinon.fake();
+
+    await logoutRedirectGet(req, res);
+
+    expect(res.redirect).to.have.calledWith(
+      `${getBaseUrl() + PATH_DATA.UNAVAILABLE_TEMPORARY.url}`
+    );
+  });
+
+  it("should redirect to blocked", async () => {
+    req.query = { state: LogoutState.Blocked };
+    req.session.destroy = sinon.fake();
path: src/components/logout-redirect/tests/logout-redirect-controller.test.ts
user: login: liegeandlief
body: Not needed
created_at: 2025-05-22T11:19:46Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,80 @@
+import { expect } from "chai";
+import { describe } from "mocha";
+import { sinon } from "../../../../test/utils/test-utils";
+import { logoutRedirectGet } from "../logout-redirect-controller";
+
+import {
+  RequestBuilder,
+  ResponseBuilder,
+  TXMA_AUDIT_ENCODED,
+} from "../../../../test/utils/builders";
+import { LogoutState, PATH_DATA } from "../../../app.constants";
+import { getBaseUrl } from "../../../config";
+
+describe("logout redirect controller", () => {
+  let req: any;
+  let res: any;
+
+  beforeEach(() => {
+    req = new RequestBuilder()
+      .withBody({})
+      .withSessionUserState({ changeAuthApp: {} })
+      .withTranslate(sinon.fake())
+      .withHeaders({ "txma-audit-encoded": TXMA_AUDIT_ENCODED })
+      .build();
+
+    res = new ResponseBuilder()
+      .withRender(sinon.fake())
+      .withRedirect(sinon.fake(() => {}))
+      .withStatus(sinon.fake())
+      .build();
+  });
+
+  afterEach(() => {
+    sinon.restore();
+  });
+
+  it("should redirect to suspended", async () => {
+    req.query = { state: LogoutState.Suspended };
+    req.session.destroy = sinon.fake();
+
+    await logoutRedirectGet(req, res);
+
+    expect(res.redirect).to.have.calledWith(
+      `${getBaseUrl() + PATH_DATA.UNAVAILABLE_TEMPORARY.url}`
+    );
+  });
+
+  it("should redirect to blocked", async () => {
+    req.query = { state: LogoutState.Blocked };
+    req.session.destroy = sinon.fake();
+
+    await logoutRedirectGet(req, res);
+
+    expect(res.redirect).to.have.calledWith(
+      `${getBaseUrl() + PATH_DATA.UNAVAILABLE_PERMANENT.url}`
+    );
+  });
+
+  it("should redirect to account deletion", async () => {
+    req.query = { state: LogoutState.AccountDeletion };
+    req.session.destroy = sinon.fake();
path: src/components/logout-redirect/tests/logout-redirect-controller.test.ts
user: login: liegeandlief
body: Not needed
created_at: 2025-05-22T11:20:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,80 @@
+import { expect } from "chai";
+import { describe } from "mocha";
+import { sinon } from "../../../../test/utils/test-utils";
+import { logoutRedirectGet } from "../logout-redirect-controller";
+
+import {
+  RequestBuilder,
+  ResponseBuilder,
+  TXMA_AUDIT_ENCODED,
+} from "../../../../test/utils/builders";
+import { LogoutState, PATH_DATA } from "../../../app.constants";
+import { getBaseUrl } from "../../../config";
+
+describe("logout redirect controller", () => {
+  let req: any;
+  let res: any;
+
+  beforeEach(() => {
+    req = new RequestBuilder()
+      .withBody({})
+      .withSessionUserState({ changeAuthApp: {} })
+      .withTranslate(sinon.fake())
+      .withHeaders({ "txma-audit-encoded": TXMA_AUDIT_ENCODED })
+      .build();
+
+    res = new ResponseBuilder()
+      .withRender(sinon.fake())
+      .withRedirect(sinon.fake(() => {}))
+      .withStatus(sinon.fake())
+      .build();
+  });
+
+  afterEach(() => {
+    sinon.restore();
+  });
+
+  it("should redirect to suspended", async () => {
+    req.query = { state: LogoutState.Suspended };
+    req.session.destroy = sinon.fake();
+
+    await logoutRedirectGet(req, res);
+
+    expect(res.redirect).to.have.calledWith(
+      `${getBaseUrl() + PATH_DATA.UNAVAILABLE_TEMPORARY.url}`
+    );
+  });
+
+  it("should redirect to blocked", async () => {
+    req.query = { state: LogoutState.Blocked };
+    req.session.destroy = sinon.fake();
+
+    await logoutRedirectGet(req, res);
+
+    expect(res.redirect).to.have.calledWith(
+      `${getBaseUrl() + PATH_DATA.UNAVAILABLE_PERMANENT.url}`
+    );
+  });
+
+  it("should redirect to account deletion", async () => {
+    req.query = { state: LogoutState.AccountDeletion };
+    req.session.destroy = sinon.fake();
+
+    await logoutRedirectGet(req, res);
+
+    expect(res.redirect).to.have.calledWith(
+      `${getBaseUrl() + PATH_DATA.ACCOUNT_DELETED_CONFIRMATION.url}`
+    );
+  });
+
+  it("should redirect to default if state is not set", async () => {
+    req.query = {};
+    req.session.destroy = sinon.fake();
path: src/components/logout-redirect/tests/logout-redirect-controller.test.ts
user: login: liegeandlief
body: Not needed
created_at: 2025-05-22T11:20:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,80 @@
+import { expect } from "chai";
+import { describe } from "mocha";
+import { sinon } from "../../../../test/utils/test-utils";
+import { logoutRedirectGet } from "../logout-redirect-controller";
+
+import {
+  RequestBuilder,
+  ResponseBuilder,
+  TXMA_AUDIT_ENCODED,
+} from "../../../../test/utils/builders";
+import { LogoutState, PATH_DATA } from "../../../app.constants";
+import { getBaseUrl } from "../../../config";
+
+describe("logout redirect controller", () => {
+  let req: any;
+  let res: any;
+
+  beforeEach(() => {
+    req = new RequestBuilder()
+      .withBody({})
+      .withSessionUserState({ changeAuthApp: {} })
+      .withTranslate(sinon.fake())
+      .withHeaders({ "txma-audit-encoded": TXMA_AUDIT_ENCODED })
+      .build();
+
+    res = new ResponseBuilder()
+      .withRender(sinon.fake())
+      .withRedirect(sinon.fake(() => {}))
+      .withStatus(sinon.fake())
+      .build();
+  });
+
+  afterEach(() => {
+    sinon.restore();
+  });
+
+  it("should redirect to suspended", async () => {
+    req.query = { state: LogoutState.Suspended };
+    req.session.destroy = sinon.fake();
+
+    await logoutRedirectGet(req, res);
+
+    expect(res.redirect).to.have.calledWith(
+      `${getBaseUrl() + PATH_DATA.UNAVAILABLE_TEMPORARY.url}`
+    );
+  });
+
+  it("should redirect to blocked", async () => {
+    req.query = { state: LogoutState.Blocked };
+    req.session.destroy = sinon.fake();
+
+    await logoutRedirectGet(req, res);
+
+    expect(res.redirect).to.have.calledWith(
+      `${getBaseUrl() + PATH_DATA.UNAVAILABLE_PERMANENT.url}`
+    );
+  });
+
+  it("should redirect to account deletion", async () => {
+    req.query = { state: LogoutState.AccountDeletion };
+    req.session.destroy = sinon.fake();
+
+    await logoutRedirectGet(req, res);
+
+    expect(res.redirect).to.have.calledWith(
+      `${getBaseUrl() + PATH_DATA.ACCOUNT_DELETED_CONFIRMATION.url}`
+    );
+  });
+
+  it("should redirect to default if state is not set", async () => {
+    req.query = {};
+    req.session.destroy = sinon.fake();
+
+    await logoutRedirectGet(req, res);
+
+    expect(res.redirect).to.have.calledWith(
+      `${getBaseUrl() + PATH_DATA.USER_SIGNED_OUT.url}`
+    );
+  });
+});
path: src/components/logout-redirect/tests/logout-redirect-controller.test.ts
user: login: liegeandlief
body: Also add a test case for manual logout state
created_at: 2025-05-22T11:20:33Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,13 @@
+import * as express from "express";
+import { logoutRedirectGet } from "./logout-redirect-controller";
+import { PATH_DATA } from "../../app.constants";
+import { globalTryCatchAsync } from "../../utils/global-try-catch";
+
+const router = express.Router();
+
+router.get(
+  PATH_DATA.LOGOUT_REDIRECT.url,
path: src/components/logout-redirect/logout-redirect-routes.ts
user: login: latifcabinetoffice
body: hmmm I had it there before but as the user's already logged out they get redirected to sign in and don't get to view the redirected page i.e. temp suspended, I don't think there's a risk the unauthorised used can get redirected to something malicious 
created_at: 2025-05-22T11:48:55Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,13 @@
+import * as express from "express";
+import { logoutRedirectGet } from "./logout-redirect-controller";
+import { PATH_DATA } from "../../app.constants";
+import { globalTryCatchAsync } from "../../utils/global-try-catch";
+
+const router = express.Router();
+
+router.get(
+  PATH_DATA.LOGOUT_REDIRECT.url,
path: src/components/logout-redirect/logout-redirect-routes.ts
user: login: liegeandlief
body: Oh yeah good point, ignore me
created_at: 2025-05-22T12:18:38Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,80 @@
+import { expect } from "chai";
+import { describe } from "mocha";
+import { sinon } from "../../../../test/utils/test-utils";
+import { logoutRedirectGet } from "../logout-redirect-controller";
+
+import {
+  RequestBuilder,
+  ResponseBuilder,
+  TXMA_AUDIT_ENCODED,
+} from "../../../../test/utils/builders";
+import { LogoutState, PATH_DATA } from "../../../app.constants";
+import { getBaseUrl } from "../../../config";
+
+describe("logout redirect controller", () => {
+  let req: any;
+  let res: any;
+
+  beforeEach(() => {
+    req = new RequestBuilder()
+      .withBody({})
+      .withSessionUserState({ changeAuthApp: {} })
+      .withTranslate(sinon.fake())
+      .withHeaders({ "txma-audit-encoded": TXMA_AUDIT_ENCODED })
+      .build();
+
+    res = new ResponseBuilder()
+      .withRender(sinon.fake())
+      .withRedirect(sinon.fake(() => {}))
+      .withStatus(sinon.fake())
+      .build();
+  });
+
+  afterEach(() => {
+    sinon.restore();
+  });
+
+  it("should redirect to suspended", async () => {
+    req.query = { state: LogoutState.Suspended };
+    req.session.destroy = sinon.fake();
+
+    await logoutRedirectGet(req, res);
+
+    expect(res.redirect).to.have.calledWith(
+      `${getBaseUrl() + PATH_DATA.UNAVAILABLE_TEMPORARY.url}`
+    );
+  });
+
+  it("should redirect to blocked", async () => {
+    req.query = { state: LogoutState.Blocked };
+    req.session.destroy = sinon.fake();
+
+    await logoutRedirectGet(req, res);
+
+    expect(res.redirect).to.have.calledWith(
+      `${getBaseUrl() + PATH_DATA.UNAVAILABLE_PERMANENT.url}`
+    );
+  });
+
+  it("should redirect to account deletion", async () => {
+    req.query = { state: LogoutState.AccountDeletion };
+    req.session.destroy = sinon.fake();
+
+    await logoutRedirectGet(req, res);
+
+    expect(res.redirect).to.have.calledWith(
+      `${getBaseUrl() + PATH_DATA.ACCOUNT_DELETED_CONFIRMATION.url}`
+    );
+  });
+
+  it("should redirect to default if state is not set", async () => {
+    req.query = {};
+    req.session.destroy = sinon.fake();
+
+    await logoutRedirectGet(req, res);
+
+    expect(res.redirect).to.have.calledWith(
+      `${getBaseUrl() + PATH_DATA.USER_SIGNED_OUT.url}`
+    );
+  });
+});
path: src/components/logout-redirect/tests/logout-redirect-controller.test.ts
user: login: latifcabinetoffice
body: I realised manual logout should be default so i've removed it and replaced it's calls with default
created_at: 2025-05-22T12:53:31Z
author_association: CONTRIBUTOR

PR comments:
user: login: sonarqubecloud[bot]
created_at: 2025-05-23T08:47:50Z
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=di-account-management-frontend&pullRequest=2200) **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=di-account-management-frontend&pullRequest=2200&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=di-account-management-frontend&pullRequest=2200&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=di-account-management-frontend&pullRequest=2200&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=di-account-management-frontend&pullRequest=2200&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=di-account-management-frontend&pullRequest=2200&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=di-account-management-frontend&pullRequest=2200)


author_association: NONE