Skip to content

Commit 7c6a192

Browse files
minotticJohannes Reppinnitrosx
authored
feat: add mongo session store (#1884)
Co-authored-by: Johannes Reppin <[email protected]> Co-authored-by: Max Novelli <[email protected]>
1 parent 06597b0 commit 7c6a192

File tree

10 files changed

+295
-20
lines changed

10 files changed

+295
-20
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,7 @@ Valid environment variables for the .env file. See [.env.example](/.env.example)
143143
| `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 |
144144
| `DOI_PREFIX` | string | | The facility DOI prefix, with trailing slash. | |
145145
| `EXPRESS_SESSION_SECRET` | string | No | Secret used to set up express session. Required if using OIDC authentication | |
146+
| `EXPRESS_SESSION_STORE` | string | Yes | Where to store the express session. When "mongo" on mongo else in memory | |
146147
| `HTTP_MAX_REDIRECTS` | number | Yes | Max redirects for HTTP requests. | 5 |
147148
| `HTTP_TIMEOUT` | number | Yes | Timeout for HTTP requests in ms. | 5000 |
148149
| `JWT_SECRET` | string | | The secret for your JWT token, used for authorization. | |

package-lock.json

Lines changed: 55 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
"bcrypt": "^5.1.0",
5353
"class-transformer": "^0.5.1",
5454
"class-validator": "^0.14.0",
55+
"connect-mongo": "^5.1.0",
5556
"dotenv": "^16.0.3",
5657
"express-session": "^1.17.3",
5758
"handlebars": "^4.7.7",
@@ -93,7 +94,7 @@
9394
"@types/bcrypt": "^5.0.0",
9495
"@types/chai": "^5.0.0",
9596
"@types/express": "^5.0.0",
96-
"@types/express-session": "^1.17.4",
97+
"@types/express-session": "^1.18.1",
9798
"@types/jest": "^27.0.2",
9899
"@types/js-yaml": "^4.0.9",
99100
"@types/jsonpath-plus": "^5.0.5",

src/auth/auth.module.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Module } from "@nestjs/common";
1+
import { MiddlewareConsumer, Module, RequestMethod } from "@nestjs/common";
22
import { AuthService } from "./auth.service";
33
import { UsersModule } from "../users/users.module";
44
import { PassportModule } from "@nestjs/passport";
@@ -14,6 +14,7 @@ import { BuildOpenIdClient, OidcStrategy } from "./strategies/oidc.strategy";
1414
import { accessGroupServiceFactory } from "./access-group-provider/access-group-service-factory";
1515
import { AccessGroupService } from "./access-group-provider/access-group.service";
1616
import { CaslModule } from "src/casl/casl.module";
17+
import { SessionMiddleware } from "./middlewares/session.middleware";
1718

1819
const OidcStrategyFactory = {
1920
provide: "OidcStrategy",
@@ -68,4 +69,17 @@ const OidcStrategyFactory = {
6869
controllers: [AuthController],
6970
exports: [AuthService, JwtModule, PassportModule],
7071
})
71-
export class AuthModule {}
72+
export class AuthModule {
73+
constructor(private configService: ConfigService) {}
74+
75+
configure(consumer: MiddlewareConsumer) {
76+
if (!this.configService.get<string>("expressSession.secret")) return;
77+
consumer
78+
.apply(SessionMiddleware)
79+
.forRoutes(
80+
{ path: "auth/oidc", method: RequestMethod.GET, version: "3" },
81+
{ path: "auth/oidc/callback", method: RequestMethod.GET, version: "3" },
82+
{ path: "auth/logout", method: RequestMethod.POST, version: "3" },
83+
);
84+
}
85+
}

src/auth/auth.service.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ export class AuthService {
5959
async logout(req: Request) {
6060
const logoutURL = this.configService.get<string>("logoutURL") || "";
6161
const expressSessionSecret = this.configService.get<string>(
62-
"expressSessionSecret",
62+
"expressSession.secret",
6363
);
6464

6565
const logoutResult = await this.additionalLogoutTasks(req, logoutURL);
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
import { Test } from "@nestjs/testing";
2+
import { SessionMiddleware } from "./session.middleware";
3+
import { ConfigService } from "@nestjs/config";
4+
import MongoStore from "connect-mongo";
5+
import session from "express-session";
6+
import { Request, Response } from "express";
7+
import { getConnectionToken } from "@nestjs/mongoose";
8+
9+
jest.mock("express-session", () => {
10+
const actual = jest.requireActual("express-session");
11+
return {
12+
__esModule: true,
13+
...actual,
14+
default: jest.fn(() => jest.fn()),
15+
};
16+
});
17+
18+
[
19+
["mongo", 1],
20+
["other", 0],
21+
].forEach(([store, callCount]) => {
22+
describe(`SessionMiddleware ${store}`, () => {
23+
let middleware: SessionMiddleware;
24+
let mongoStoreMock: jest.SpyInstance;
25+
let sessionMock: jest.Mock;
26+
let sessionHandlerMock: jest.Mock;
27+
28+
beforeEach(async () => {
29+
mongoStoreMock = jest.spyOn(MongoStore, "create").mockReturnValue({
30+
client: "mockClient",
31+
ttl: 3600,
32+
} as unknown as MongoStore);
33+
34+
sessionMock = session as unknown as jest.Mock;
35+
sessionHandlerMock = jest.fn();
36+
sessionMock.mockReturnValue(sessionHandlerMock);
37+
38+
const configServiceMock = {
39+
get: jest.fn((key: string) => {
40+
switch (key) {
41+
case "expressSession.secret":
42+
return "secret";
43+
case "expressSession.store":
44+
return store;
45+
case "jwt.expiresIn":
46+
return 3600;
47+
default:
48+
return null;
49+
}
50+
}),
51+
};
52+
53+
const mongoConnectionMock = {
54+
getClient: () => "mockClient",
55+
};
56+
57+
const moduleRef = await Test.createTestingModule({
58+
providers: [
59+
SessionMiddleware,
60+
{
61+
provide: ConfigService,
62+
useValue: configServiceMock,
63+
},
64+
{
65+
provide: getConnectionToken(),
66+
useValue: mongoConnectionMock,
67+
},
68+
],
69+
}).compile();
70+
71+
middleware = moduleRef.get<SessionMiddleware>(SessionMiddleware);
72+
});
73+
74+
afterEach(() => {
75+
jest.clearAllMocks();
76+
jest.resetAllMocks();
77+
});
78+
79+
it("should be defined", () => {
80+
expect(middleware).toBeDefined();
81+
});
82+
83+
it("should call MongoStore.create if store is mongo", () => {
84+
expect(mongoStoreMock).toHaveBeenCalledTimes(callCount as number);
85+
let store = {};
86+
const commonSessionOptions = {
87+
secret: "secret",
88+
resave: false,
89+
saveUninitialized: true,
90+
};
91+
if (callCount)
92+
store = {
93+
store: {
94+
client: "mockClient",
95+
ttl: 3600,
96+
},
97+
};
98+
expect(sessionMock).toHaveBeenCalledWith(
99+
Object.assign({}, commonSessionOptions, store),
100+
);
101+
if (!callCount) return;
102+
expect(mongoStoreMock).toHaveBeenCalledWith({
103+
client: "mockClient",
104+
ttl: 3600,
105+
});
106+
});
107+
108+
it("should invoke session() with proper arguments", () => {
109+
const req = {} as Request;
110+
const res = {} as Response;
111+
const next = jest.fn();
112+
113+
middleware.use(req, res, next);
114+
expect(sessionHandlerMock).toHaveBeenCalledWith(req, res, next);
115+
});
116+
});
117+
});
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { Injectable, NestMiddleware } from "@nestjs/common";
2+
import { ConfigService } from "@nestjs/config";
3+
import { InjectConnection } from "@nestjs/mongoose";
4+
import MongoStore from "connect-mongo";
5+
import { Request, Response, NextFunction, RequestHandler } from "express";
6+
import session, { Store } from "express-session";
7+
import { Connection } from "mongoose";
8+
9+
@Injectable()
10+
export class SessionMiddleware implements NestMiddleware {
11+
private readonly requestHandler: RequestHandler;
12+
constructor(
13+
private readonly configService: ConfigService,
14+
@InjectConnection() private readonly mongoConnection: Connection,
15+
) {
16+
let store: { store: Store } | object = {};
17+
if (this.configService.get<string>("expressSession.store") === "mongo")
18+
store = {
19+
store: MongoStore.create({
20+
client: this.mongoConnection.getClient(),
21+
ttl: this.configService.get<number>("jwt.expiresIn"),
22+
}),
23+
};
24+
this.requestHandler = session({
25+
secret: this.configService.get<string>("expressSession.secret") as string,
26+
resave: false,
27+
saveUninitialized: true,
28+
...store,
29+
});
30+
}
31+
32+
use(req: Request, res: Response, next: NextFunction) {
33+
return this.requestHandler(req, res, next);
34+
}
35+
}

src/config/configuration.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,10 @@ const configuration = () => {
186186
accessGroupProperty: process.env?.OIDC_ACCESS_GROUPS_PROPERTY, // Example: groups
187187
},
188188
doiPrefix: process.env.DOI_PREFIX,
189-
expressSessionSecret: process.env.EXPRESS_SESSION_SECRET,
189+
expressSession: {
190+
secret: process.env.EXPRESS_SESSION_SECRET,
191+
store: process.env.EXPRESS_SESSION_STORE,
192+
},
190193
functionalAccounts: [],
191194
httpMaxRedirects: process.env.HTTP_MAX_REDIRECTS ?? 5,
192195
httpTimeOut: process.env.HTTP_TIMEOUT ?? 5000,

src/main.ts

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import session from "express-session";
21
import { NestFactory } from "@nestjs/core";
32
import {
43
DocumentBuilder,
@@ -92,19 +91,6 @@ async function bootstrap() {
9291
extended: true,
9392
});
9493

95-
const expressSessionSecret = configService.get<string>(
96-
"expressSessionSecret",
97-
);
98-
if (expressSessionSecret) {
99-
app.use(
100-
session({
101-
secret: expressSessionSecret,
102-
resave: false,
103-
saveUninitialized: true,
104-
}),
105-
);
106-
}
107-
10894
const port = configService.get<number>("port") ?? 3000;
10995
Logger.log("Scicat Backend listening on port: " + port, "Main");
11096

0 commit comments

Comments
 (0)