Repository: auth0/nextjs-auth0
Pull request #1158 (2023-04-17 08:29:21)

feat: add optional session param to genId function

- [x] All new/changed/fixed functionality is covered by tests (or N/A)
- [x] I have added documentation for all new/changed functionality (or N/A)

Typedoc needs to be run, I can't run it, links are generated wrong (to PSoltes repo)

### 📋 Changes

Adds session as param to `genId` function, to better link session store keys to exact user sessions.

### 📎 References

https://github.com/auth0/nextjs-auth0/issues/1024

### 🎯 Testing

Use second param in genId function. Create a key for session, that is contained in eg. user object (nickname, email). I recommend encrypting this info, if sensitive to user.



Patch:
diff --git a/src/auth0-session/config.ts b/src/auth0-session/config.ts
index 8bcbb160..f2e99dbb 100644
--- a/src/auth0-session/config.ts
+++ b/src/auth0-session/config.ts
@@ -208,7 +208,10 @@ export interface SessionConfig {
    *
    * **IMPORTANT** You must use a suitably unique value to prevent collisions.
    */
-  genId?: <Req = any>(req: Req) => string | Promise<string>;
+  genId?: <Req = any, SessionType extends { [key: string]: any } = { [key: string]: any }>(
+    req: Req,
+    session: SessionType
+  ) => string | Promise<string>;
 
   /**
    * If you want your session duration to be rolling, resetting everytime the
diff --git a/src/auth0-session/get-config.ts b/src/auth0-session/get-config.ts
index 584fc33e..16e9f19b 100644
--- a/src/auth0-session/get-config.ts
+++ b/src/auth0-session/get-config.ts
@@ -31,7 +31,7 @@ const paramsSchema = Joi.object({
     autoSave: Joi.boolean().optional().default(true),
     name: Joi.string().token().optional().default('appSession'),
     store: Joi.object().optional(),
-    genId: Joi.function().maxArity(1).when(Joi.ref('store'), { then: Joi.required() }),
+    genId: Joi.function().maxArity(2).when(Joi.ref('store'), { then: Joi.required() }),
     storeIDToken: Joi.boolean().optional().default(true),
     cookie: Joi.object({
       domain: Joi.string().optional(),
diff --git a/src/auth0-session/session/stateful-session.ts b/src/auth0-session/session/stateful-session.ts
index 6c5adba9..eb56d430 100644
--- a/src/auth0-session/session/stateful-session.ts
+++ b/src/auth0-session/session/stateful-session.ts
@@ -85,7 +85,7 @@ export class StatefulSession<
     }
 
     if (!sessionId) {
-      sessionId = await genId!(req);
+      sessionId = await genId!(req, session);
       debug('generated new session id %o', sessionId);
     }
     debug('set session %o', sessionId);
diff --git a/src/config.ts b/src/config.ts
index 3f2817e4..fbc21eed 100644
--- a/src/config.ts
+++ b/src/config.ts
@@ -218,7 +218,10 @@ export interface SessionConfig {
    * **IMPORTANT** If you override this, you must use a suitable value from your platform to
    * prevent collisions. For example, for Node: `require('crypto').randomBytes(16).toString('hex')`.
    */
-  genId?: <Req = any>(req: Req) => string | Promise<string>;
+  genId?: <Req = any, SessionType extends { [key: string]: any } = { [key: string]: any }>(
+    req: Req,
+    session: SessionType
+  ) => string | Promise<string>;
 
   /**
    * If you want your session duration to be rolling, resetting everytime the


Test patch:
diff --git a/tests/auth0-session/session/stateful-session.test.ts b/tests/auth0-session/session/stateful-session.test.ts
index 16bb6e10..f556d1b4 100644
--- a/tests/auth0-session/session/stateful-session.test.ts
+++ b/tests/auth0-session/session/stateful-session.test.ts
@@ -148,6 +148,17 @@ describe('StatefulSession', () => {
     expect(cookieJar.getCookieStringSync(baseURL)).toMatch(/^appSession=foobar\..+/);
   });
 
+  it('should provide current user session to custom session id generator', async () => {
+    const genId = jest.fn().mockImplementation((_req, session) => session.state);
+    const baseURL = await setup({ ...config, session: { ...config.session, genId } });
+    const cookieJar = await login(baseURL);
+    const genIdParams = genId.mock.calls.at(0);
+    expect(genIdParams.length).toEqual(2);
+    expect('id_token' in genIdParams.at(1)).toBeTruthy();
+    const regex = `^appSession=${genIdParams.at(1).state}\..+`;
+    expect(cookieJar.getCookieStringSync(baseURL)).toMatch(new RegExp(regex));
+  });
+
   it('should regenerate the session when a new user is logging in over an existing user', async () => {
     await store.set('foo', await getPayload());
     const baseURL = await setup(config);
diff --git a/tests/stateful-session.test.ts b/tests/stateful-session.test.ts
index dd5eafc7..fa804101 100644
--- a/tests/stateful-session.test.ts
+++ b/tests/stateful-session.test.ts
@@ -98,4 +98,15 @@ describe('next stateful session', () => {
     expect(Object.keys(store)).toHaveLength(1);
     expect(cookieJar.getCookieStringSync(baseUrl)).toMatch(/^appSession=foo\..+/);
   });
+
+  it('should provide current user session to custom session id generator', async () => {
+    const genId = jest.fn().mockImplementation((_req, session) => session.user.nickname);
+    const baseURL = await setup({ ...config, session: { ...config.session, genId } });
+    const cookieJar = await login(baseURL);
+    const genIdParams = genId.mock.calls.at(0);
+    expect(genIdParams.length).toEqual(2);
+    expect('idToken' in genIdParams.at(1)).toBeTruthy();
+    expect('user' in genIdParams.at(1)).toBeTruthy();
+    expect(cookieJar.getCookieStringSync(baseURL)).toMatch(/^appSession=__test_nickname__\..+/);
+  });
 });