coyotte508 HF Staff commited on
Commit
3199efb
·
unverified ·
1 Parent(s): 3b53c7a

Add COUPLE_SESSION_WITH_COOKIE_NAME env var (#1881)

Browse files

* Add COUPLE_SESSION_WITH_COOKIE_NAME env var

This means the user's session will be reset if the value of the coupled cookie name changes.

Useful if the app is deployed on a subpath of another site, and you want to reset the session when the auth
for the user changes

* fix test

.env CHANGED
@@ -80,6 +80,9 @@ ALTERNATIVE_REDIRECT_URLS=`[]`
80
  ### Cookies
81
  # name of the cookie used to store the session
82
  COOKIE_NAME=hf-chat
 
 
 
83
  # specify secure behaviour for cookies
84
  COOKIE_SAMESITE=# can be "lax", "strict", "none" or left empty
85
  COOKIE_SECURE=# set to true to only allow cookies over https
 
80
  ### Cookies
81
  # name of the cookie used to store the session
82
  COOKIE_NAME=hf-chat
83
+ # If the value of this cookie changes, the session is destroyed. Useful if chat-ui is deployed on a subpath
84
+ # of your domain, and you want chat ui sessions to reset if the user's auth changes
85
+ COUPLE_SESSION_WITH_COOKIE_NAME=
86
  # specify secure behaviour for cookies
87
  COOKIE_SAMESITE=# can be "lax", "strict", "none" or left empty
88
  COOKIE_SECURE=# set to true to only allow cookies over https
src/lib/server/auth.ts CHANGED
@@ -17,6 +17,7 @@ import { logger } from "$lib/server/logger";
17
  import { ObjectId } from "mongodb";
18
  import type { Cookie } from "elysia";
19
  import { adminTokenManager } from "./adminToken";
 
20
 
21
  export interface OIDCSettings {
22
  redirectURI: string;
@@ -72,14 +73,27 @@ export function refreshSessionCookie(cookies: Cookies, sessionId: string) {
72
  });
73
  }
74
 
75
- export async function findUser(sessionId: string) {
 
 
 
 
 
 
76
  const session = await collections.sessions.findOne({ sessionId });
77
 
78
  if (!session) {
79
- return null;
 
 
 
 
80
  }
81
 
82
- return await collections.users.findOne({ _id: session.userId });
 
 
 
83
  }
84
  export const authCondition = (locals: App.Locals) => {
85
  if (!locals.user && !locals.sessionId) {
@@ -191,6 +205,23 @@ type HeaderRecord =
191
  | { type: "elysia"; value: Record<string, string | undefined> }
192
  | { type: "svelte"; value: Headers };
193
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
194
  export async function authenticateRequest(
195
  headers: HeaderRecord,
196
  cookie: CookieRecord,
@@ -238,12 +269,23 @@ export async function authenticateRequest(
238
  if (token) {
239
  secretSessionId = token;
240
  sessionId = await sha256(token);
241
- const user = await findUser(sessionId);
 
 
 
 
 
 
 
 
 
 
 
242
  return {
243
- user: user ?? undefined,
244
  sessionId,
245
  secretSessionId,
246
- isAdmin: user?.isAdmin || adminTokenManager.isAdmin(sessionId),
247
  };
248
  }
249
 
 
17
  import { ObjectId } from "mongodb";
18
  import type { Cookie } from "elysia";
19
  import { adminTokenManager } from "./adminToken";
20
+ import type { User } from "$lib/types/User";
21
 
22
  export interface OIDCSettings {
23
  redirectURI: string;
 
73
  });
74
  }
75
 
76
+ export async function findUser(
77
+ sessionId: string,
78
+ coupledCookieHash?: string
79
+ ): Promise<{
80
+ user: User | null;
81
+ invalidateSession: boolean;
82
+ }> {
83
  const session = await collections.sessions.findOne({ sessionId });
84
 
85
  if (!session) {
86
+ return { user: null, invalidateSession: false };
87
+ }
88
+
89
+ if (coupledCookieHash && session.coupledCookieHash !== coupledCookieHash) {
90
+ return { user: null, invalidateSession: true };
91
  }
92
 
93
+ return {
94
+ user: await collections.users.findOne({ _id: session.userId }),
95
+ invalidateSession: false,
96
+ };
97
  }
98
  export const authCondition = (locals: App.Locals) => {
99
  if (!locals.user && !locals.sessionId) {
 
205
  | { type: "elysia"; value: Record<string, string | undefined> }
206
  | { type: "svelte"; value: Headers };
207
 
208
+ export async function getCoupledCookieHash(cookie: CookieRecord): Promise<string | undefined> {
209
+ if (!config.COUPLE_SESSION_WITH_COOKIE_NAME) {
210
+ return undefined;
211
+ }
212
+
213
+ const cookieValue =
214
+ cookie.type === "elysia"
215
+ ? cookie.value[config.COUPLE_SESSION_WITH_COOKIE_NAME]?.value
216
+ : cookie.value.get(config.COUPLE_SESSION_WITH_COOKIE_NAME);
217
+
218
+ if (!cookieValue) {
219
+ return "no-cookie";
220
+ }
221
+
222
+ return await sha256(cookieValue);
223
+ }
224
+
225
  export async function authenticateRequest(
226
  headers: HeaderRecord,
227
  cookie: CookieRecord,
 
269
  if (token) {
270
  secretSessionId = token;
271
  sessionId = await sha256(token);
272
+
273
+ const result = await findUser(sessionId, await getCoupledCookieHash(cookie));
274
+
275
+ if (result.invalidateSession) {
276
+ secretSessionId = crypto.randomUUID();
277
+ sessionId = await sha256(secretSessionId);
278
+
279
+ if (await collections.sessions.findOne({ sessionId })) {
280
+ throw new Error("Session ID collision");
281
+ }
282
+ }
283
+
284
  return {
285
+ user: result.user ?? undefined,
286
  sessionId,
287
  secretSessionId,
288
+ isAdmin: result.user?.isAdmin || adminTokenManager.isAdmin(sessionId),
289
  };
290
  }
291
 
src/lib/types/Session.ts CHANGED
@@ -10,4 +10,5 @@ export interface Session extends Timestamps {
10
  ip?: string;
11
  expiresAt: Date;
12
  admin?: boolean;
 
13
  }
 
10
  ip?: string;
11
  expiresAt: Date;
12
  admin?: boolean;
13
+ coupledCookieHash?: string;
14
  }
src/routes/login/callback/updateUser.spec.ts CHANGED
@@ -90,7 +90,7 @@ describe("login", () => {
90
  it("should create default settings for new user", async () => {
91
  await updateUser({ userData, locals, cookies: cookiesMock });
92
 
93
- const user = await findUser(locals.sessionId);
94
 
95
  assert.exists(user);
96
 
 
90
  it("should create default settings for new user", async () => {
91
  await updateUser({ userData, locals, cookies: cookiesMock });
92
 
93
+ const user = (await findUser(locals.sessionId)).user;
94
 
95
  assert.exists(user);
96
 
src/routes/login/callback/updateUser.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { refreshSessionCookie } from "$lib/server/auth";
2
  import { collections } from "$lib/server/database";
3
  import { ObjectId } from "mongodb";
4
  import { DEFAULT_SETTINGS } from "$lib/types/Settings";
@@ -119,6 +119,9 @@ export async function updateUser(params: {
119
 
120
  locals.sessionId = sessionId;
121
 
 
 
 
122
  if (existingUser) {
123
  // update existing user if any
124
  await collections.users.updateOne(
@@ -137,6 +140,7 @@ export async function updateUser(params: {
137
  userAgent,
138
  ip,
139
  expiresAt: addWeeks(new Date(), 2),
 
140
  });
141
  } else {
142
  // user doesn't exist yet, create a new one
@@ -164,6 +168,7 @@ export async function updateUser(params: {
164
  userAgent,
165
  ip,
166
  expiresAt: addWeeks(new Date(), 2),
 
167
  });
168
 
169
  // move pre-existing settings to new user
 
1
+ import { getCoupledCookieHash, refreshSessionCookie } from "$lib/server/auth";
2
  import { collections } from "$lib/server/database";
3
  import { ObjectId } from "mongodb";
4
  import { DEFAULT_SETTINGS } from "$lib/types/Settings";
 
119
 
120
  locals.sessionId = sessionId;
121
 
122
+ // Get cookie hash if coupling is enabled
123
+ const coupledCookieHash = await getCoupledCookieHash({ type: "svelte", value: cookies });
124
+
125
  if (existingUser) {
126
  // update existing user if any
127
  await collections.users.updateOne(
 
140
  userAgent,
141
  ip,
142
  expiresAt: addWeeks(new Date(), 2),
143
+ ...(coupledCookieHash ? { coupledCookieHash } : {}),
144
  });
145
  } else {
146
  // user doesn't exist yet, create a new one
 
168
  userAgent,
169
  ip,
170
  expiresAt: addWeeks(new Date(), 2),
171
+ ...(coupledCookieHash ? { coupledCookieHash } : {}),
172
  });
173
 
174
  // move pre-existing settings to new user