Hello everyone!
Looking for some help here. Not really sure that it’s a bug, so didn’t want to go to GitHub first. I’m currently working on setting up a custom Session Store in an ExpressJS application that will store session in a PostgreSQL database. I wanted to integrate the connect-pg-simple
library into my application, but it hasn’t be as seamless as I was expecting.
Below is a snippet that details how I am setting up the middleware.
server.use(auth({
auth0Logout: false,
authorizationParams: {
redirect_uri: openIDConnectConfig.OAUTH2_REDIRECT_URI,
response_type: 'code',
scope: 'openid email phone',
},
authRequired: false,
baseURL: 'http://localhost:8080',
clientID: openIDConnectConfig.OAUTH2_CLIENT_ID,
clientSecret: openIDConnectConfig.OAUTH2_CLIENT_SECRET,
issuerBaseURL: openIDConnectConfig.OAUTH2_ISSUER_BASE_URL,
routes: {
login: false,
logout: false,
callback: '/v1/oauth2/callback'
},
secret: Buffer.from(openIDConnectConfig.OAUTH2_SECRET).toString('base64'),
session: {
store: new pgSession({ pool, tableName: 'session' }) as unknown as SessionStore,
cookie: {
httpOnly: true
}
},
}));
I have a controller setup with various auth methods inside that I’ve been using for login/logout/etc.
import { Http } from "@status/codes";
import { randomInt } from "crypto";
import { Request, Response } from "express";
import { Get, HttpCode, JsonController, OnUndefined, Req, Res } from "routing-controllers";
import { Service } from "typedi";
@Service()
@JsonController('/oauth2')
export class OpenIDConnectAuthenticationController {
@Get('/login')
@OnUndefined(Http.Ok)
async login(@Res() response: Response): Promise<void> {
await response.oidc.login({ returnTo: 'http://localhost:8080' });
}
@Get('/logout')
@OnUndefined(Http.Ok)
async logout(@Res() response: Response): Promise<void> {
await response.oidc.logout();
}
@Get('/is-authenticated')
@HttpCode(Http.Ok)
isAuthenticated(@Req() request: Request): boolean {
return request.oidc.isAuthenticated();
}
@Get('/user-info')
@HttpCode(Http.Ok)
userInfo(@Req() request: Request): Promise<Record<string, unknown>> {
return request.oidc.fetchUserInfo();
}
@Get('/random-number')
@HttpCode(Http.Ok)
randomNumber(): number {
return randomInt(1_000);
}
}
This code works fine, mostly just test code to play around with the library. Not going to be used in production or anything. I’ve been running into a problem when calling the isAuthenticated
or randomNumber
methods. Whenever I call either one of those methods, I end up getting the following error.
Error [ERR_STREAM_WRITE_AFTER_END]: write after end
at new NodeError (internal/errors.js:322:7)
at writeAfterEnd (_http_outgoing.js:694:15)
at ServerResponse.end (_http_outgoing.js:815:7)
at ServerResponse.resEnd [as end] (webpack://express-cognito/./node_modules/express-openid-connect/lib/appSession.js?:362:19)
at processTicksAndRejections (internal/process/task_queues.js:95:5)
Emitted 'error' event on ServerResponse instance at:
at writeAfterEndNT (_http_outgoing.js:753:7)
at processTicksAndRejections (internal/process/task_queues.js:83:21) {
code: 'ERR_STREAM_WRITE_AFTER_END'
}
So, I ended up thinking, maybe I was just setting up the postgres session store incorrectly. So I decided to write my own custom session store that looks like this…
export class InMemorySessionStore implements SessionStore {
private store: Map<string, SessionStorePayload>;
constructor() {
this.store = new Map<string, SessionStorePayload>();
}
get(sid: string, callback: (err: unknown, session?: SessionStorePayload) => void) {
this.store.has(sid) ? callback(null, this.store.get(sid)) : callback(null, null);
}
set(sid: string, session: SessionStorePayload, callback?: (err?: unknown) => void) {
this.store.set(sid, session);
callback();
}
destroy(sid: string, callback?: (err?: unknown) => void) {
if(this.store.has(sid)) {
this.store.delete(sid);
}
callback();
}
}
Again, this isn’t going to be production code or anything. I’ve mostly just been trying to toy around with the library and learn how things work.
Interestingly enough, if I plug this InMemorySessionStore
into the auth
middleware, I still get that ERR_STREAM_WRITE_AFTER_END
error message. I thought the code was pretty simple, so I started digging into the express-openid-connect
code a little bit, and realized that inside of appSession.js
, the following was causing the problem…
// line (349)
if (isCustomStore) {
const id = existingSessionValue || generateId(req);
onHeaders(res, () =>
store.setCookie(req[REGENERATED_SESSION_ID] || id, req, res, { iat })
);
const { end: origEnd } = res;
res.end = async function resEnd(...args) {
try {
/* This await is what seems to be causing the issue */
/* Removing it fixes the issue, and doesn't cause any */
/* Noticeable problems with either SessionStore */
await store.set(id, req, res, {
iat,
});
origEnd.call(res, ...args);
} catch (e) {
// need to restore the original `end` so that it gets
// called after `next(e)` calls the express error handling mw
res.end = origEnd;
process.nextTick(() => next(e));
}
};
} else {
onHeaders(res, () => store.setCookie(req, res, { iat }));
}
So I noticed that without that await everything seems to be ok. The header issue goes away and login/logout still seem to function appropriately. Sessions are now being stored either in memory or in a PostgreSQL database I’m running locally while doing all this.
Does anyone have any experience with setting up these custom session stores? Am I doing something wrong? Looking at the SessionStore
interface, it looks like get/set/destroy
all return void and aren’t returning Promise<void>
. They are also being fed callbacks, so I wouldn’t think that these should be returning promises. So why the await? Why the async function?
I’m rather new to JavaScript/TypeScript, so I could be doing something wrong or just not understanding why the await is there. Just looking for help with the issue.
Thanks for all the help!
- Which SDK this is regarding: NodeJS
- SDK Version: 2.7.2
- Platform Version: Windows 11
- Code Snippets/Error Messages/Supporting Details/Screenshots: