Skip to content

Commit 004900a

Browse files
:bug fix(auth-refresh): update supertokens-node and add refresh failure logging (#1170)
* :bug fix(auth-refresh): update supertokens-node and add refresh failure logging * :bug fix(auth-refresh): prevent ws attempt to refresh the session * :bug fix(auth-refresh): update supertokens verifySession mocks
1 parent 1a71480 commit 004900a

File tree

5 files changed

+142
-27
lines changed

5 files changed

+142
-27
lines changed

packages/backend/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
"rrule": "^2.7.2",
2424
"saslprep": "^1.0.3",
2525
"socket.io": "^4.7.5",
26-
"supertokens-node": "^20.0.5",
26+
"supertokens-node": "^23.0.1",
2727
"tslib": "^2.4.0"
2828
},
2929
"devDependencies": {

packages/backend/src/__tests__/helpers/mock.setup.ts

Lines changed: 60 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,18 @@
1-
import { Handler, Response } from "express";
1+
import { Handler, NextFunction, Response } from "express";
22
import { GoogleApis } from "googleapis";
33
import mergeWith, { default as mockMergeWith } from "lodash.mergewith";
44
import { randomUUID } from "node:crypto";
55
import { SessionRequest } from "supertokens-node/framework/express";
6-
import { BaseResponse } from "supertokens-node/lib/build/framework";
7-
import { SessionContainerInterface } from "supertokens-node/lib/build/recipe/session/types";
6+
import {
7+
ExpressRequest,
8+
ExpressResponse,
9+
} from "supertokens-node/lib/build/framework/express/framework";
10+
import {
11+
APIOptions,
12+
SessionContainerInterface,
13+
VerifySessionOptions,
14+
} from "supertokens-node/lib/build/recipe/session/types";
15+
import { UserContext } from "supertokens-node/lib/build/types";
816
import { createMockCalendarListEntry as mockCalendarListCreate } from "@core/__tests__/helpers/gcal.factory";
917
import { gSchema$CalendarListEntry } from "@core/types/gcal";
1018
import { UserMetadata } from "@core/types/user.types";
@@ -49,12 +57,12 @@ function mockGoogleapis() {
4957
function mockSuperToken() {
5058
const userMetadata = new Map<string, UserMetadata>();
5159

52-
function verifySession() {
53-
return (
54-
req: SessionRequest,
55-
_res: Response & BaseResponse,
56-
next?: (err?: unknown) => void,
57-
) => {
60+
function verifySession(input: {
61+
verifySessionOptions?: VerifySessionOptions;
62+
options: APIOptions;
63+
userContext: UserContext;
64+
}) {
65+
return (req: SessionRequest, _res: Response, next?: NextFunction) => {
5866
try {
5967
const cookies = (req.headers.cookie?.split(";") ?? [])?.reduce(
6068
(items, item) => {
@@ -98,10 +106,12 @@ function mockSuperToken() {
98106
},
99107
} as SessionContainerInterface;
100108

101-
return next ? next() : undefined;
109+
return next?.();
102110
}
103111

104-
throw new Error("invalid superToken session");
112+
if (input?.verifySessionOptions?.sessionRequired) {
113+
throw new Error("invalid superToken session");
114+
}
105115
} catch (error) {
106116
if (next) {
107117
next(error);
@@ -163,6 +173,45 @@ function mockSuperToken() {
163173
return mergeWith(userMetadataModule, { default: userMetadataModule });
164174
},
165175
);
176+
177+
mockModule(
178+
"supertokens-node/lib/build/recipe/session/recipe",
179+
(
180+
session: typeof import("supertokens-node/lib/build/recipe/session/recipe"),
181+
) => {
182+
const getInstanceOrThrowError =
183+
session.default.getInstanceOrThrowError.bind(session.default);
184+
185+
const sessionModule = mergeWith(session, {
186+
default: mergeWith(session.default, {
187+
getInstanceOrThrowError: jest.fn(() => {
188+
const instance = getInstanceOrThrowError();
189+
190+
return mergeWith(instance, {
191+
apiImpl: mergeWith(instance.apiImpl, {
192+
verifySession: jest.fn(
193+
async (input: {
194+
verifySessionOptions: VerifySessionOptions | undefined;
195+
options: APIOptions;
196+
userContext: UserContext;
197+
}) => {
198+
const req = input.options.req as ExpressRequest;
199+
const res = input.options.res as ExpressResponse;
200+
201+
verifySession(input)(req.original, res.original);
202+
203+
return Promise.resolve(req.original.session);
204+
},
205+
),
206+
}),
207+
});
208+
}),
209+
}),
210+
});
211+
212+
return sessionModule;
213+
},
214+
);
166215
}
167216

168217
function mockWinstonLogger() {

packages/backend/src/common/middleware/supertokens.middleware.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,14 @@ import {
1111
PORT_DEFAULT_BACKEND,
1212
PORT_DEFAULT_WEB,
1313
} from "@core/constants/core.constants";
14+
import { Status } from "@core/errors/status.codes";
15+
import { Logger } from "@core/logger/winston.logger";
1416
import { ENV } from "@backend/common/constants/env.constants";
1517
import { SupertokensAccessTokenPayload } from "@backend/common/types/supertokens.types";
1618
import { webSocketServer } from "@backend/servers/websocket/websocket.server";
1719

20+
const logger = Logger("app:supertokens.middleware");
21+
1822
export const initSupertokens = () => {
1923
SuperTokens.init({
2024
appInfo: {
@@ -32,6 +36,18 @@ export const initSupertokens = () => {
3236
recipeList: [
3337
Dashboard.init(),
3438
Session.init({
39+
errorHandlers: {
40+
onTryRefreshToken: async (message, _request, response) => {
41+
logger.warn(
42+
`Session expired: ${message}. User tried to refresh the session.`,
43+
);
44+
45+
response.setStatusCode(Status.UNAUTHORIZED);
46+
response.sendJSONResponse({
47+
error: "Session expired. Please log in again.",
48+
});
49+
},
50+
},
3551
override: {
3652
apis(originalImplementation) {
3753
return {
@@ -60,6 +76,10 @@ export const initSupertokens = () => {
6076

6177
webSocketServer.handleUserRefreshToken(socketId!);
6278

79+
logger.debug(
80+
`Session refreshed for user ${data.sub} client.`,
81+
);
82+
6383
return session;
6484
},
6585
);

packages/backend/src/servers/websocket/websocket.server.ts

Lines changed: 53 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,14 @@
1-
import { Request } from "express";
1+
import { NextFunction, Request, Response } from "express";
22
import { Server as HttpServer } from "node:http";
33
import { ExtendedError, Server as SocketIOServer } from "socket.io";
4-
import { verifySession } from "supertokens-node/recipe/session/framework/express";
4+
import { SessionRequest } from "supertokens-node/framework/express";
5+
import {
6+
ExpressRequest,
7+
ExpressResponse,
8+
} from "supertokens-node/lib/build/framework/express/framework";
9+
import SessionError from "supertokens-node/lib/build/recipe/session/error";
10+
import SessionRecipe from "supertokens-node/lib/build/recipe/session/recipe";
11+
import { makeDefaultUserContextFromAPI } from "supertokens-node/lib/build/utils";
512
import {
613
EVENT_CHANGED,
714
EVENT_CHANGE_PROCESSED,
@@ -16,6 +23,7 @@ import {
1623
USER_REFRESH_TOKEN,
1724
USER_SIGN_OUT,
1825
} from "@core/constants/websocket.constants";
26+
import { Status } from "@core/errors/status.codes";
1927
import { Logger } from "@core/logger/winston.logger";
2028
import { UserMetadata } from "@core/types/user.types";
2129
import {
@@ -197,6 +205,48 @@ class WebSocketServer {
197205
return this.notifyClient(socketId!, event, ...payload);
198206
}
199207

208+
/**
209+
* verifySession
210+
*
211+
* We are manually verifying the session here
212+
* to prevent the default supertokens behavior
213+
* of attempting to refresh the session if it is expired internally
214+
* since the socket's session might be stale.
215+
* We offload the refresh mechanism to the client.
216+
*/
217+
private async verifySession(
218+
req: SessionRequest,
219+
res: Response,
220+
next: NextFunction,
221+
) {
222+
try {
223+
const request = new ExpressRequest(req);
224+
const response = new ExpressResponse(res);
225+
const userContext = makeDefaultUserContextFromAPI(request);
226+
const sessionRecipe = SessionRecipe.getInstanceOrThrowError();
227+
const session = await sessionRecipe.verifySession(
228+
{ sessionRequired: true },
229+
request,
230+
response,
231+
userContext,
232+
);
233+
234+
Object.assign(req, { session });
235+
236+
next();
237+
} catch (err) {
238+
const error = err as SessionError;
239+
240+
logger.error(error.message, error);
241+
242+
res.writeHead(Status.UNAUTHORIZED, {
243+
"Content-Type": "application/json",
244+
});
245+
246+
res.end(JSON.stringify({ type: error.type, message: "Invalid Session" }));
247+
}
248+
}
249+
200250
init(server: HttpServer) {
201251
this.wsServer = new SocketIOServer<
202252
ClientToServerEvents,
@@ -205,7 +255,7 @@ class WebSocketServer {
205255
SocketData
206256
>(server, { cors: { origin: ENV.ORIGINS_ALLOWED, credentials: true } });
207257

208-
this.wsServer.engine.use(verifySession());
258+
this.wsServer.engine.use(this.verifySession.bind(this));
209259

210260
this.wsServer.engine.generateId = this.generateId.bind(this);
211261

yarn.lock

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4892,11 +4892,6 @@ cookie-signature@1.0.6:
48924892
resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c"
48934893
integrity sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==
48944894

4895-
cookie@0.4.0:
4896-
version "0.4.0"
4897-
resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.0.tgz#beb437e7022b3b6d49019d088665303ebe9c14ba"
4898-
integrity sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==
4899-
49004895
cookie@0.7.1:
49014896
version "0.7.1"
49024897
resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.7.1.tgz#2f73c42142d5d5cf71310a74fc4ae61670e5dbc9"
@@ -4907,7 +4902,7 @@ cookie@^0.4.2:
49074902
resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.2.tgz#0e41f24de5ecf317947c82fc789e06a884824432"
49084903
integrity sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==
49094904

4910-
cookie@~0.7.2:
4905+
cookie@^0.7.2, cookie@~0.7.2:
49114906
version "0.7.2"
49124907
resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.7.2.tgz#556369c472a2ba910f2979891b526b3436237ed7"
49134908
integrity sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==
@@ -10735,7 +10730,7 @@ serve-static@1.16.2:
1073510730
parseurl "~1.3.3"
1073610731
send "0.19.0"
1073710732

10738-
set-cookie-parser@^2.4.6:
10733+
set-cookie-parser@^2.4.6, set-cookie-parser@^2.6.0:
1073910734
version "2.7.1"
1074010735
resolved "https://registry.yarnpkg.com/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz#3016f150072202dfbe90fadee053573cc89d2943"
1074110736
integrity sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==
@@ -11383,14 +11378,14 @@ supertokens-js-override@0.0.4, supertokens-js-override@^0.0.4:
1138311378
resolved "https://registry.yarnpkg.com/supertokens-js-override/-/supertokens-js-override-0.0.4.tgz#9af583fbc5e1f0195dbb358c4fcf75f44c76dc09"
1138411379
integrity sha512-r0JFBjkMIdep3Lbk3JA+MpnpuOtw4RSyrlRAbrzMcxwiYco3GFWl/daimQZ5b1forOiUODpOlXbSOljP/oyurg==
1138511380

11386-
supertokens-node@^20.0.5:
11387-
version "20.1.7"
11388-
resolved "https://registry.yarnpkg.com/supertokens-node/-/supertokens-node-20.1.7.tgz#d06511de0891b22d499fb219519864aef29706d9"
11389-
integrity sha512-Ol3LhYksxBBpmmzx8MX9sPSaaVI4x58gMpHbdy7wlzwpC3TM44HIEmAMD++ig3oOUNSvppqQVDkAcCFNafpl1w==
11381+
supertokens-node@^23.0.1:
11382+
version "23.0.1"
11383+
resolved "https://registry.yarnpkg.com/supertokens-node/-/supertokens-node-23.0.1.tgz#adc12cef47a0c1af1eddfe84db49ae8ff73af855"
11384+
integrity sha512-cCuY9Y5Mj93Pg1ktbqilouWgAoQWniQauftB4Ef6rfOchogx13XTo1pNP14zezn2rSf7WIPb9iaZb5zif6TKtQ==
1139011385
dependencies:
1139111386
buffer "^6.0.3"
1139211387
content-type "^1.0.5"
11393-
cookie "0.4.0"
11388+
cookie "^0.7.2"
1139411389
cross-fetch "^3.1.6"
1139511390
debug "^4.3.3"
1139611391
jose "^4.13.1"
@@ -11399,6 +11394,7 @@ supertokens-node@^20.0.5:
1139911394
pako "^2.1.0"
1140011395
pkce-challenge "^3.0.0"
1140111396
process "^0.11.10"
11397+
set-cookie-parser "^2.6.0"
1140211398
supertokens-js-override "^0.0.4"
1140311399
tldts "^6.1.48"
1140411400
twilio "^4.19.3"

0 commit comments

Comments
 (0)