Repository: SciCatProject/scicat-backend-next
Pull request #1884: feat: add mongo session store
Author: login: minottic
Created: 2025-05-05T16:50:47Z | Merged: 2025-05-07T14:15:41Z

<!--
Follow semantic-release guidelines for the PR title, which is used in the changelog.

Title should follow the format `<type>(<scope>): <subject>`, where
- Type is one of: build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test|BREAKING CHANGE
- Scope (optional) describes the place of the change (eg a particular milestone) and is usually omitted
- subject should be a non-capitalized one-line description in present imperative tense and not ending with a period

See https://github.com/angular/angular.js/blob/master/DEVELOPERS.md#-git-commit-guidelines for more details.
-->

## Description
<!-- Short description of the pull request -->
Built based on #1804, it refactors express session to use nestjs [middlewars](https://docs.nestjs.com/middleware) and DI

## Motivation
<!-- Background on use case, changes needed -->
Allows stateful sessions in case of multiple replicas deployment. Mongo store is configurable, and if left empty, it falls back to Memorystore. 

## Changes:
<!-- Please provide a list of the changes implemented by this PR -->

* refactor app.use to middleware
* DI configService
* DI mongoConnection
* configurable mongo Store
* reuse `session` DB from default
* uses JWT expiration as session expiration
* it restricts middleware to /oidc endpoints

## Tests included

- [x] Included for each change/fix?
- [x] Passing? <!-- Merge will not be approved unless tests pass -->

## Documentation
- [ ] swagger documentation updated (required for API changes)
- [ ] official documentation updated

### official documentation info
<!-- If you have updated the official documentation, please provide PR # and URL of the updated pages -->



Diff:
diff --git a/README.md b/README.md
index 22bc6500f..30938ce1d 100644
--- a/README.md
+++ b/README.md
@@ -143,6 +143,7 @@ Valid environment variables for the .env file. See [.env.example](/.env.example)
 | `ACCESS_GROUPS_OIDCPAYLOAD_ENABLED` | string | Yes | Flag to enable/disable fetching access groups directly from OIDC response. Requires specifying a field via `OIDC_ACCESS_GROUPS_PROPERTY` to extract access groups. | false |
 | `DOI_PREFIX` | string | | The facility DOI prefix, with trailing slash. | |
 | `EXPRESS_SESSION_SECRET` | string | No | Secret used to set up express session. Required if using OIDC authentication | |
+| `EXPRESS_SESSION_STORE` | string | Yes | Where to store the express session. When "mongo" on mongo else in memory  | |
 | `HTTP_MAX_REDIRECTS` | number | Yes | Max redirects for HTTP requests. | 5 |
 | `HTTP_TIMEOUT` | number | Yes | Timeout for HTTP requests in ms. | 5000 |
 | `JWT_SECRET` | string | | The secret for your JWT token, used for authorization. | |
diff --git a/package-lock.json b/package-lock.json
index 5dfed2bfb..b0faa9fe6 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -31,6 +31,7 @@
         "bcrypt": "^5.1.0",
         "class-transformer": "^0.5.1",
         "class-validator": "^0.14.0",
+        "connect-mongo": "^5.1.0",
         "dotenv": "^16.0.3",
         "express-session": "^1.17.3",
         "handlebars": "^4.7.7",
@@ -64,7 +65,7 @@
         "@types/bcrypt": "^5.0.0",
         "@types/chai": "^5.0.0",
         "@types/express": "^5.0.0",
-        "@types/express-session": "^1.17.4",
+        "@types/express-session": "^1.18.1",
         "@types/jest": "^27.0.2",
         "@types/js-yaml": "^4.0.9",
         "@types/jsonpath-plus": "^5.0.5",
@@ -4477,6 +4478,18 @@
         "safer-buffer": "~2.1.0"
       }
     },
+    "node_modules/asn1.js": {
+      "version": "5.4.1",
+      "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz",
+      "integrity": "sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==",
+      "license": "MIT",
+      "dependencies": {
+        "bn.js": "^4.0.0",
+        "inherits": "^2.0.1",
+        "minimalistic-assert": "^1.0.0",
+        "safer-buffer": "^2.1.0"
+      }
+    },
     "node_modules/assert-never": {
       "version": "1.4.0",
       "resolved": "https://registry.npmjs.org/assert-never/-/assert-never-1.4.0.tgz",
@@ -4743,6 +4756,12 @@
         "node": ">= 6"
       }
     },
+    "node_modules/bn.js": {
+      "version": "4.12.2",
+      "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz",
+      "integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==",
+      "license": "MIT"
+    },
     "node_modules/body-parser": {
       "version": "2.2.0",
       "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz",
@@ -5625,6 +5644,23 @@
         "proto-list": "~1.2.1"
       }
     },
+    "node_modules/connect-mongo": {
+      "version": "5.1.0",
+      "resolved": "https://registry.npmjs.org/connect-mongo/-/connect-mongo-5.1.0.tgz",
+      "integrity": "sha512-xT0vxQLqyqoUTxPLzlP9a/u+vir0zNkhiy9uAdHjSCcUUf7TS5b55Icw8lVyYFxfemP3Mf9gdwUOgeF3cxCAhw==",
+      "license": "MIT",
+      "dependencies": {
+        "debug": "^4.3.1",
+        "kruptein": "^3.0.0"
+      },
+      "engines": {
+        "node": ">=12.9.0"
+      },
+      "peerDependencies": {
+        "express-session": "^1.17.1",
+        "mongodb": ">= 5.1.0 < 7"
+      }
+    },
     "node_modules/consola": {
       "version": "3.4.2",
       "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz",
@@ -9639,6 +9675,18 @@
         "node": ">=6"
       }
     },
+    "node_modules/kruptein": {
+      "version": "3.0.7",
+      "resolved": "https://registry.npmjs.org/kruptein/-/kruptein-3.0.7.tgz",
+      "integrity": "sha512-vTftnEjfbqFHLqxDUMQCj6gBo5lKqjV4f0JsM8rk8rM3xmvFZ2eSy4YALdaye7E+cDKnEj7eAjFR3vwh8a4PgQ==",
+      "license": "MIT",
+      "dependencies": {
+        "asn1.js": "^5.4.1"
+      },
+      "engines": {
+        "node": ">8"
+      }
+    },
     "node_modules/ldap-filter": {
       "version": "0.3.3",
       "resolved": "https://registry.npmjs.org/ldap-filter/-/ldap-filter-0.3.3.tgz",
@@ -10234,6 +10282,12 @@
         "node": ">=6"
       }
     },
+    "node_modules/minimalistic-assert": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
+      "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==",
+      "license": "ISC"
+    },
     "node_modules/minimatch": {
       "version": "3.1.2",
       "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
diff --git a/package.json b/package.json
index 68432fbe4..a5fdb6dda 100644
--- a/package.json
+++ b/package.json
@@ -52,6 +52,7 @@
     "bcrypt": "^5.1.0",
     "class-transformer": "^0.5.1",
     "class-validator": "^0.14.0",
+    "connect-mongo": "^5.1.0",
     "dotenv": "^16.0.3",
     "express-session": "^1.17.3",
     "handlebars": "^4.7.7",
@@ -93,7 +94,7 @@
     "@types/bcrypt": "^5.0.0",
     "@types/chai": "^5.0.0",
     "@types/express": "^5.0.0",
-    "@types/express-session": "^1.17.4",
+    "@types/express-session": "^1.18.1",
     "@types/jest": "^27.0.2",
     "@types/js-yaml": "^4.0.9",
     "@types/jsonpath-plus": "^5.0.5",
diff --git a/src/auth/auth.module.ts b/src/auth/auth.module.ts
index c42bb8cbd..b6cb1eccc 100644
--- a/src/auth/auth.module.ts
+++ b/src/auth/auth.module.ts
@@ -1,4 +1,4 @@
-import { Module } from "@nestjs/common";
+import { MiddlewareConsumer, Module, RequestMethod } from "@nestjs/common";
 import { AuthService } from "./auth.service";
 import { UsersModule } from "../users/users.module";
 import { PassportModule } from "@nestjs/passport";
@@ -14,6 +14,7 @@ import { BuildOpenIdClient, OidcStrategy } from "./strategies/oidc.strategy";
 import { accessGroupServiceFactory } from "./access-group-provider/access-group-service-factory";
 import { AccessGroupService } from "./access-group-provider/access-group.service";
 import { CaslModule } from "src/casl/casl.module";
+import { SessionMiddleware } from "./middlewares/session.middleware";
 
 const OidcStrategyFactory = {
   provide: "OidcStrategy",
@@ -68,4 +69,17 @@ const OidcStrategyFactory = {
   controllers: [AuthController],
   exports: [AuthService, JwtModule, PassportModule],
 })
-export class AuthModule {}
+export class AuthModule {
+  constructor(private configService: ConfigService) {}
+
+  configure(consumer: MiddlewareConsumer) {
+    if (!this.configService.get<string>("expressSession.secret")) return;
+    consumer
+      .apply(SessionMiddleware)
+      .forRoutes(
+        { path: "auth/oidc", method: RequestMethod.GET, version: "3" },
+        { path: "auth/oidc/callback", method: RequestMethod.GET, version: "3" },
+        { path: "auth/logout", method: RequestMethod.POST, version: "3" },
+      );
+  }
+}
diff --git a/src/auth/auth.service.ts b/src/auth/auth.service.ts
index 4d77c2461..25f565d9c 100644
--- a/src/auth/auth.service.ts
+++ b/src/auth/auth.service.ts
@@ -59,7 +59,7 @@ export class AuthService {
   async logout(req: Request) {
     const logoutURL = this.configService.get<string>("logoutURL") || "";
     const expressSessionSecret = this.configService.get<string>(
-      "expressSessionSecret",
+      "expressSession.secret",
     );
 
     const logoutResult = await this.additionalLogoutTasks(req, logoutURL);
diff --git a/src/auth/middlewares/session.middleware.spec.ts b/src/auth/middlewares/session.middleware.spec.ts
new file mode 100644
index 000000000..abc57452e
--- /dev/null
+++ b/src/auth/middlewares/session.middleware.spec.ts
@@ -0,0 +1,117 @@
+import { Test } from "@nestjs/testing";
+import { SessionMiddleware } from "./session.middleware";
+import { ConfigService } from "@nestjs/config";
+import MongoStore from "connect-mongo";
+import session from "express-session";
+import { Request, Response } from "express";
+import { getConnectionToken } from "@nestjs/mongoose";
+
+jest.mock("express-session", () => {
+  const actual = jest.requireActual("express-session");
+  return {
+    __esModule: true,
+    ...actual,
+    default: jest.fn(() => jest.fn()),
+  };
+});
+
+[
+  ["mongo", 1],
+  ["other", 0],
+].forEach(([store, callCount]) => {
+  describe(`SessionMiddleware ${store}`, () => {
+    let middleware: SessionMiddleware;
+    let mongoStoreMock: jest.SpyInstance;
+    let sessionMock: jest.Mock;
+    let sessionHandlerMock: jest.Mock;
+
+    beforeEach(async () => {
+      mongoStoreMock = jest.spyOn(MongoStore, "create").mockReturnValue({
+        client: "mockClient",
+        ttl: 3600,
+      } as unknown as MongoStore);
+
+      sessionMock = session as unknown as jest.Mock;
+      sessionHandlerMock = jest.fn();
+      sessionMock.mockReturnValue(sessionHandlerMock);
+
+      const configServiceMock = {
+        get: jest.fn((key: string) => {
+          switch (key) {
+            case "expressSession.secret":
+              return "secret";
+            case "expressSession.store":
+              return store;
+            case "jwt.expiresIn":
+              return 3600;
+            default:
+              return null;
+          }
+        }),
+      };
+
+      const mongoConnectionMock = {
+        getClient: () => "mockClient",
+      };
+
+      const moduleRef = await Test.createTestingModule({
+        providers: [
+          SessionMiddleware,
+          {
+            provide: ConfigService,
+            useValue: configServiceMock,
+          },
+          {
+            provide: getConnectionToken(),
+            useValue: mongoConnectionMock,
+          },
+        ],
+      }).compile();
+
+      middleware = moduleRef.get<SessionMiddleware>(SessionMiddleware);
+    });
+
+    afterEach(() => {
+      jest.clearAllMocks();
+      jest.resetAllMocks();
+    });
+
+    it("should be defined", () => {
+      expect(middleware).toBeDefined();
+    });
+
+    it("should call MongoStore.create if store is mongo", () => {
+      expect(mongoStoreMock).toHaveBeenCalledTimes(callCount as number);
+      let store = {};
+      const commonSessionOptions = {
+        secret: "secret",
+        resave: false,
+        saveUninitialized: true,
+      };
+      if (callCount)
+        store = {
+          store: {
+            client: "mockClient",
+            ttl: 3600,
+          },
+        };
+      expect(sessionMock).toHaveBeenCalledWith(
+        Object.assign({}, commonSessionOptions, store),
+      );
+      if (!callCount) return;
+      expect(mongoStoreMock).toHaveBeenCalledWith({
+        client: "mockClient",
+        ttl: 3600,
+      });
+    });
+
+    it("should invoke session() with proper arguments", () => {
+      const req = {} as Request;
+      const res = {} as Response;
+      const next = jest.fn();
+
+      middleware.use(req, res, next);
+      expect(sessionHandlerMock).toHaveBeenCalledWith(req, res, next);
+    });
+  });
+});
diff --git a/src/auth/middlewares/session.middleware.ts b/src/auth/middlewares/session.middleware.ts
new file mode 100644
index 000000000..4a31a592c
--- /dev/null
+++ b/src/auth/middlewares/session.middleware.ts
@@ -0,0 +1,35 @@
+import { Injectable, NestMiddleware } from "@nestjs/common";
+import { ConfigService } from "@nestjs/config";
+import { InjectConnection } from "@nestjs/mongoose";
+import MongoStore from "connect-mongo";
+import { Request, Response, NextFunction, RequestHandler } from "express";
+import session, { Store } from "express-session";
+import { Connection } from "mongoose";
+
+@Injectable()
+export class SessionMiddleware implements NestMiddleware {
+  private readonly requestHandler: RequestHandler;
+  constructor(
+    private readonly configService: ConfigService,
+    @InjectConnection() private readonly mongoConnection: Connection,
+  ) {
+    let store: { store: Store } | object = {};
+    if (this.configService.get<string>("expressSession.store") === "mongo")
+      store = {
+        store: MongoStore.create({
+          client: this.mongoConnection.getClient(),
+          ttl: this.configService.get<number>("jwt.expiresIn"),
+        }),
+      };
+    this.requestHandler = session({
+      secret: this.configService.get<string>("expressSession.secret") as string,
+      resave: false,
+      saveUninitialized: true,
+      ...store,
+    });
+  }
+
+  use(req: Request, res: Response, next: NextFunction) {
+    return this.requestHandler(req, res, next);
+  }
+}
diff --git a/src/config/configuration.ts b/src/config/configuration.ts
index 513678d03..d9455cc37 100644
--- a/src/config/configuration.ts
+++ b/src/config/configuration.ts
@@ -186,7 +186,10 @@ const configuration = () => {
       accessGroupProperty: process.env?.OIDC_ACCESS_GROUPS_PROPERTY, // Example: groups
     },
     doiPrefix: process.env.DOI_PREFIX,
-    expressSessionSecret: process.env.EXPRESS_SESSION_SECRET,
+    expressSession: {
+      secret: process.env.EXPRESS_SESSION_SECRET,
+      store: process.env.EXPRESS_SESSION_STORE,
+    },
     functionalAccounts: [],
     httpMaxRedirects: process.env.HTTP_MAX_REDIRECTS ?? 5,
     httpTimeOut: process.env.HTTP_TIMEOUT ?? 5000,
diff --git a/src/main.ts b/src/main.ts
index 00d4de311..c846a92d6 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -1,4 +1,3 @@
-import session from "express-session";
 import { NestFactory } from "@nestjs/core";
 import {
   DocumentBuilder,
@@ -92,19 +91,6 @@ async function bootstrap() {
     extended: true,
   });
 
-  const expressSessionSecret = configService.get<string>(
-    "expressSessionSecret",
-  );
-  if (expressSessionSecret) {
-    app.use(
-      session({
-        secret: expressSessionSecret,
-        resave: false,
-        saveUninitialized: true,
-      }),
-    );
-  }
-
   const port = configService.get<number>("port") ?? 3000;
   Logger.log("Scicat Backend listening on port: " + port, "Main");
 
diff --git a/test/jest-e2e-tests/oidc.e2e-spec.ts b/test/jest-e2e-tests/oidc.e2e-spec.ts
new file mode 100644
index 000000000..02a29c060
--- /dev/null
+++ b/test/jest-e2e-tests/oidc.e2e-spec.ts
@@ -0,0 +1,64 @@
+import request from "supertest";
+import { Test } from "@nestjs/testing";
+import { INestApplication } from "@nestjs/common";
+import { AppModule } from "src/app.module";
+import { OidcAuthGuard } from "src/auth/guards/oidc.guard";
+import { getConnectionToken } from "@nestjs/mongoose";
+import { Connection } from "mongoose";
+import { ConfigService } from "@nestjs/config";
+
+["mongo", "memory"].forEach((store) => {
+  describe(`OIDC test ${store}`, () => {
+    let app: INestApplication;
+    let mongoConnection: Connection;
+
+    class ConfigServiceMock extends ConfigService {
+      get(key: string) {
+        if (key === "expressSession.store") return store;
+        if (key === "mongodbUri") {
+          const uriParts = super.get(key).split("/");
+          uriParts[3] = "scicat-e2e-test";
+          return uriParts.join("/");
+        }
+        return super.get(key);
+      }
+    }
+
+    beforeAll(async () => {
+      const moduleFixture = await Test.createTestingModule({
+        imports: [AppModule],
+      })
+        .overrideProvider(ConfigService)
+        .useClass(ConfigServiceMock)
+        .overrideGuard(OidcAuthGuard)
+        .useValue({ canActivate: jest.fn(() => true) })
+        .compile();
+
+      app = moduleFixture.createNestApplication();
+      app.setGlobalPrefix("api/v3");
+      mongoConnection =
+        await app.get<Promise<Connection>>(getConnectionToken());
+      await app.init();
+    });
+
+    afterAll(async () => {
+      await app.close();
+    });
+
+    afterEach(async () => {
+      await mongoConnection.db!.dropDatabase();
+    });
+
+    it("Check session is created in DB", async function () {
+      await request(app.getHttpServer())
+        .get("/api/v3/auth/oidc")
+        .set("Accept", "text/html")
+        .expect(200);
+
+      const sessions = mongoConnection.db!.collection("sessions");
+      const session = await sessions.findOne();
+      if (store === "mongo") expect(session).not.toBe(null);
+      else expect(session).toBe(null);
+    });
+  });
+});


Reviews:
user: login: nitrosx
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-07T09:23:55Z
user: login: Junjiequan
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-05-07T09:25:22Z
user: login: minottic
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-07T09:29:42Z
user: login: Junjiequan
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-07T09:36:03Z
user: login: minottic
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-07T09:38:42Z
user: login: Junjiequan
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-07T09:45:23Z
user: login: minottic
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-07T09:47:24Z
user: login: minottic
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-07T11:39:15Z
user: login: Junjiequan
body: Looks good to me 
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-07T13:52:55Z

Review comments:
diff_hunk: @@ -0,0 +1,33 @@
+import { Injectable, NestMiddleware } from "@nestjs/common";
+import { ConfigService } from "@nestjs/config";
+import { InjectConnection } from "@nestjs/mongoose";
+import MongoStore from "connect-mongo";
+import { Request, Response, NextFunction, RequestHandler } from "express";
+import session from "express-session";
+import { Connection } from "mongoose";
+
+@Injectable()
+export class SessionMiddleware implements NestMiddleware {
+  private readonly requestHandler: RequestHandler;
+  constructor(
+    private readonly configService: ConfigService,
+    @InjectConnection() private readonly mongoConnection: Connection,
+  ) {
+    let store = {};
+    if (this.configService.get<string>("expressSession.store") === "mongo")
+      store = MongoStore.create({
+        client: this.mongoConnection.getClient(),
+        ttl: this.configService.get<number>("jwt.expiresIn"),
+      });
+    this.requestHandler = session({
+      secret: this.configService.get<string>("expressSession.secret") as string,
+      resave: false,
+      saveUninitialized: true,
+      ...store,
+    });
path: src/auth/middlewares/session.middleware.ts
user: login: Junjiequan
body: I tested it locally, and the session method does not register the mongoStore.
It appears to me that session function cannot handle `...store`
The correct type should be `store?: Store | undefined;`, you can check the type from the package interface.

```suggestion
    let store: Store | undefined;
    if (this.configService.get<string>("expressSession.store") === "mongo")
      store = MongoStore.create({
        client: this.mongoConnection.getClient(),
        ttl: this.configService.get<number>("jwt.expiresIn"),
      });
    this.requestHandler = session({
      secret: this.configService.get<string>("expressSession.secret") as string,
      resave: false,
      saveUninitialized: true,
      store,
    });
```
created_at: 2025-05-07T09:25:09Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,33 @@
+import { Injectable, NestMiddleware } from "@nestjs/common";
+import { ConfigService } from "@nestjs/config";
+import { InjectConnection } from "@nestjs/mongoose";
+import MongoStore from "connect-mongo";
+import { Request, Response, NextFunction, RequestHandler } from "express";
+import session from "express-session";
+import { Connection } from "mongoose";
+
+@Injectable()
+export class SessionMiddleware implements NestMiddleware {
+  private readonly requestHandler: RequestHandler;
+  constructor(
+    private readonly configService: ConfigService,
+    @InjectConnection() private readonly mongoConnection: Connection,
+  ) {
+    let store = {};
+    if (this.configService.get<string>("expressSession.store") === "mongo")
+      store = MongoStore.create({
+        client: this.mongoConnection.getClient(),
+        ttl: this.configService.get<number>("jwt.expiresIn"),
+      });
+    this.requestHandler = session({
+      secret: this.configService.get<string>("expressSession.secret") as string,
+      resave: false,
+      saveUninitialized: true,
+      ...store,
+    });
path: src/auth/middlewares/session.middleware.ts
user: login: minottic
body: thanks for noticing this, I made a mistake indeed in the latest commit. I'll fix in a min PR and tests! 
created_at: 2025-05-07T09:29:42Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,33 @@
+import { Injectable, NestMiddleware } from "@nestjs/common";
+import { ConfigService } from "@nestjs/config";
+import { InjectConnection } from "@nestjs/mongoose";
+import MongoStore from "connect-mongo";
+import { Request, Response, NextFunction, RequestHandler } from "express";
+import session from "express-session";
+import { Connection } from "mongoose";
+
+@Injectable()
+export class SessionMiddleware implements NestMiddleware {
+  private readonly requestHandler: RequestHandler;
+  constructor(
+    private readonly configService: ConfigService,
+    @InjectConnection() private readonly mongoConnection: Connection,
+  ) {
+    let store = {};
+    if (this.configService.get<string>("expressSession.store") === "mongo")
+      store = MongoStore.create({
+        client: this.mongoConnection.getClient(),
+        ttl: this.configService.get<number>("jwt.expiresIn"),
+      });
+    this.requestHandler = session({
+      secret: this.configService.get<string>("expressSession.secret") as string,
+      resave: false,
+      saveUninitialized: true,
+      ...store,
+    });
path: src/auth/middlewares/session.middleware.ts
user: login: Junjiequan
body: The test looks fine to me. Maybe one additional API test to mock the process and see if user session is created in the db?
created_at: 2025-05-07T09:36:03Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,33 @@
+import { Injectable, NestMiddleware } from "@nestjs/common";
+import { ConfigService } from "@nestjs/config";
+import { InjectConnection } from "@nestjs/mongoose";
+import MongoStore from "connect-mongo";
+import { Request, Response, NextFunction, RequestHandler } from "express";
+import session from "express-session";
+import { Connection } from "mongoose";
+
+@Injectable()
+export class SessionMiddleware implements NestMiddleware {
+  private readonly requestHandler: RequestHandler;
+  constructor(
+    private readonly configService: ConfigService,
+    @InjectConnection() private readonly mongoConnection: Connection,
+  ) {
+    let store = {};
+    if (this.configService.get<string>("expressSession.store") === "mongo")
+      store = MongoStore.create({
+        client: this.mongoConnection.getClient(),
+        ttl: this.configService.get<number>("jwt.expiresIn"),
+      });
+    this.requestHandler = session({
+      secret: this.configService.get<string>("expressSession.secret") as string,
+      resave: false,
+      saveUninitialized: true,
+      ...store,
+    });
path: src/auth/middlewares/session.middleware.ts
user: login: minottic
body: the test was also wrong, see the new commit. This controller test is a good idea I think, I did not add it before because all API tests are done with the /test folder from where I cannot check the mongo session. But I can add one indeed in the auth.controller.spec.ts
created_at: 2025-05-07T09:38:42Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,33 @@
+import { Injectable, NestMiddleware } from "@nestjs/common";
+import { ConfigService } from "@nestjs/config";
+import { InjectConnection } from "@nestjs/mongoose";
+import MongoStore from "connect-mongo";
+import { Request, Response, NextFunction, RequestHandler } from "express";
+import session from "express-session";
+import { Connection } from "mongoose";
+
+@Injectable()
+export class SessionMiddleware implements NestMiddleware {
+  private readonly requestHandler: RequestHandler;
+  constructor(
+    private readonly configService: ConfigService,
+    @InjectConnection() private readonly mongoConnection: Connection,
+  ) {
+    let store = {};
+    if (this.configService.get<string>("expressSession.store") === "mongo")
+      store = MongoStore.create({
+        client: this.mongoConnection.getClient(),
+        ttl: this.configService.get<number>("jwt.expiresIn"),
+      });
+    this.requestHandler = session({
+      secret: this.configService.get<string>("expressSession.secret") as string,
+      resave: false,
+      saveUninitialized: true,
+      ...store,
+    });
path: src/auth/middlewares/session.middleware.ts
user: login: Junjiequan
body: You can run `db.collection("sessions").find({})` under the /test for checking I think, it's been done in many API tests. I think having a spec.ts is also fine, but maybe take more efforts 
 
created_at: 2025-05-07T09:45:23Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,33 @@
+import { Injectable, NestMiddleware } from "@nestjs/common";
+import { ConfigService } from "@nestjs/config";
+import { InjectConnection } from "@nestjs/mongoose";
+import MongoStore from "connect-mongo";
+import { Request, Response, NextFunction, RequestHandler } from "express";
+import session from "express-session";
+import { Connection } from "mongoose";
+
+@Injectable()
+export class SessionMiddleware implements NestMiddleware {
+  private readonly requestHandler: RequestHandler;
+  constructor(
+    private readonly configService: ConfigService,
+    @InjectConnection() private readonly mongoConnection: Connection,
+  ) {
+    let store = {};
+    if (this.configService.get<string>("expressSession.store") === "mongo")
+      store = MongoStore.create({
+        client: this.mongoConnection.getClient(),
+        ttl: this.configService.get<number>("jwt.expiresIn"),
+      });
+    this.requestHandler = session({
+      secret: this.configService.get<string>("expressSession.secret") as string,
+      resave: false,
+      saveUninitialized: true,
+      ...store,
+    });
path: src/auth/middlewares/session.middleware.ts
user: login: minottic
body: ah nice, thanks for pointing out I did not see that! Will do
created_at: 2025-05-07T09:47:24Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,33 @@
+import { Injectable, NestMiddleware } from "@nestjs/common";
+import { ConfigService } from "@nestjs/config";
+import { InjectConnection } from "@nestjs/mongoose";
+import MongoStore from "connect-mongo";
+import { Request, Response, NextFunction, RequestHandler } from "express";
+import session from "express-session";
+import { Connection } from "mongoose";
+
+@Injectable()
+export class SessionMiddleware implements NestMiddleware {
+  private readonly requestHandler: RequestHandler;
+  constructor(
+    private readonly configService: ConfigService,
+    @InjectConnection() private readonly mongoConnection: Connection,
+  ) {
+    let store = {};
+    if (this.configService.get<string>("expressSession.store") === "mongo")
+      store = MongoStore.create({
+        client: this.mongoConnection.getClient(),
+        ttl: this.configService.get<number>("jwt.expiresIn"),
+      });
+    this.requestHandler = session({
+      secret: this.configService.get<string>("expressSession.secret") as string,
+      resave: false,
+      saveUninitialized: true,
+      ...store,
+    });
path: src/auth/middlewares/session.middleware.ts
user: login: minottic
body: done with jest as it required mocking the OidcGuard
created_at: 2025-05-07T11:39:15Z
author_association: MEMBER

PR comments:
user: login: minottic
created_at: 2025-05-05T16:51:15Z
body: superseeds #1803 
author_association: MEMBER
user: login: belfhi
created_at: 2025-05-06T10:00:53Z
body: ah this looks great! I haven't had much time to look into my MR, so I'm happy my suggestion was taken up :)
author_association: NONE