auth-vir 3.0.0 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -46,6 +46,7 @@ export type BackendAuthClientConfig<DatabaseUser extends AnyObject, UserId exten
46
46
  * their user identity. Otherwise, this is `undefined`.
47
47
  */
48
48
  assumingUser: AssumedUserParams | undefined;
49
+ requestHeaders: Readonly<IncomingHttpHeaders>;
49
50
  }) => MaybePromise<DatabaseUser | undefined | null>;
50
51
  /**
51
52
  * Get JWT keys produced by {@link generateNewJwtKeys}. Make sure that each time this is
@@ -59,6 +60,12 @@ export type BackendAuthClientConfig<DatabaseUser extends AnyObject, UserId exten
59
60
  */
60
61
  isDev: boolean;
61
62
  } & PartialWithUndefined<{
63
+ /** If this returns true, logging will be enabled while handling the relevant session. */
64
+ enableLogging(params: {
65
+ user: DatabaseUser | undefined;
66
+ userId: UserId | undefined;
67
+ assumedUserParams: AssumedUserParams | undefined;
68
+ }): boolean;
62
69
  /**
63
70
  * Overwrite the header name used for tracking is an admin is assuming the identity of
64
71
  * another user.
@@ -71,6 +78,8 @@ export type BackendAuthClientConfig<DatabaseUser extends AnyObject, UserId exten
71
78
  generateServiceOrigin(params: {
72
79
  requestHeaders: Readonly<IncomingHttpHeaders>;
73
80
  }): MaybePromise<undefined | string>;
81
+ /** If provided, logs will be sent to this method. */
82
+ log?: (message: string, extraData: AnyObject) => void;
74
83
  /**
75
84
  * Set this to allow specific users (determined by `canAssumeUser`) to assume the identity
76
85
  * of other users. This should only be used for admins so that they can troubleshoot user
@@ -140,6 +149,12 @@ export declare class BackendAuthClient<DatabaseUser extends AnyObject, UserId ex
140
149
  protected readonly config: BackendAuthClientConfig<DatabaseUser, UserId, AssumedUserParams>;
141
150
  protected cachedParsedJwtKeys: Record<string, Readonly<JwtKeys>>;
142
151
  constructor(config: BackendAuthClientConfig<DatabaseUser, UserId, AssumedUserParams>);
152
+ /** Conditionally logs a message if logging is enabled for the given user context. */
153
+ protected logForUser(params: {
154
+ user: DatabaseUser | undefined;
155
+ userId: UserId | undefined;
156
+ assumedUserParams: AssumedUserParams | undefined;
157
+ }, message: string, extra?: Record<string, unknown>): void;
143
158
  /** Get all the parameters used for cookie generation. */
144
159
  protected getCookieParams({ isSignUpCookie, requestHeaders, }: {
145
160
  /**
@@ -152,10 +167,11 @@ export declare class BackendAuthClient<DatabaseUser extends AnyObject, UserId ex
152
167
  requestHeaders: Readonly<IncomingHttpHeaders> | undefined;
153
168
  }): Promise<Readonly<CookieParams>>;
154
169
  /** Calls the provided `getUserFromDatabase` config. */
155
- protected getDatabaseUser({ isSignUpCookie, userId, assumingUser, }: {
170
+ protected getDatabaseUser({ isSignUpCookie, userId, assumingUser, requestHeaders, }: {
156
171
  userId: UserId | undefined;
157
172
  assumingUser: AssumedUserParams | undefined;
158
173
  isSignUpCookie: boolean;
174
+ requestHeaders: IncomingHttpHeaders;
159
175
  }): Promise<undefined | DatabaseUser>;
160
176
  /** Creates a `'cookie-set'` header to refresh the user's session cookie. */
161
177
  protected createCookieRefreshHeaders({ userIdResult, requestHeaders, }: {
@@ -163,9 +179,9 @@ export declare class BackendAuthClient<DatabaseUser extends AnyObject, UserId ex
163
179
  requestHeaders: IncomingHttpHeaders;
164
180
  }): Promise<OutgoingHttpHeaders | undefined>;
165
181
  /** Reads the user's assumed user headers and, if configured, gets the assumed user. */
166
- protected getAssumedUser({ headers, user, }: {
182
+ protected getAssumedUser({ requestHeaders, user, }: {
167
183
  user: DatabaseUser;
168
- headers: IncomingHttpHeaders;
184
+ requestHeaders: IncomingHttpHeaders;
169
185
  }): Promise<DatabaseUser | undefined>;
170
186
  /** Securely extract a user from their request headers. */
171
187
  getSecureUser({ requestHeaders, isSignUpCookie, allowUserAuthRefresh, }: {
@@ -28,10 +28,27 @@ export class BackendAuthClient {
28
28
  constructor(config) {
29
29
  this.config = config;
30
30
  }
31
+ /** Conditionally logs a message if logging is enabled for the given user context. */
32
+ logForUser(params, message, extra) {
33
+ if (this.config.enableLogging?.(params)) {
34
+ const extraData = {
35
+ userId: params.userId,
36
+ ...extra,
37
+ };
38
+ if (this.config.log) {
39
+ this.config.log(message, extraData);
40
+ }
41
+ else {
42
+ console.info(`[auth-vir] ${message}`, extraData);
43
+ }
44
+ }
45
+ }
31
46
  /** Get all the parameters used for cookie generation. */
32
47
  async getCookieParams({ isSignUpCookie, requestHeaders, }) {
33
48
  const serviceOrigin = requestHeaders
34
- ? await this.config.generateServiceOrigin?.({ requestHeaders })
49
+ ? await this.config.generateServiceOrigin?.({
50
+ requestHeaders,
51
+ })
35
52
  : undefined;
36
53
  return {
37
54
  cookieDuration: this.config.userSessionIdleTimeout || defaultSessionIdleTimeout,
@@ -42,7 +59,7 @@ export class BackendAuthClient {
42
59
  };
43
60
  }
44
61
  /** Calls the provided `getUserFromDatabase` config. */
45
- async getDatabaseUser({ isSignUpCookie, userId, assumingUser, }) {
62
+ async getDatabaseUser({ isSignUpCookie, userId, assumingUser, requestHeaders, }) {
46
63
  if (!userId) {
47
64
  return undefined;
48
65
  }
@@ -50,8 +67,16 @@ export class BackendAuthClient {
50
67
  assumingUser,
51
68
  userId,
52
69
  isSignUpCookie,
70
+ requestHeaders,
53
71
  });
54
72
  if (!authenticatedUser) {
73
+ this.logForUser({
74
+ user: undefined,
75
+ userId,
76
+ assumedUserParams: assumingUser,
77
+ }, 'getUserFromDatabase returned no user', {
78
+ isSignUpCookie,
79
+ });
55
80
  return undefined;
56
81
  }
57
82
  return authenticatedUser;
@@ -66,6 +91,14 @@ export class BackendAuthClient {
66
91
  relativeTo: calculateRelativeDate(userIdResult.jwtExpiration, clockSkew),
67
92
  });
68
93
  if (isExpiredAlready) {
94
+ this.logForUser({
95
+ user: undefined,
96
+ userId: userIdResult.userId,
97
+ assumedUserParams: undefined,
98
+ }, 'Session refresh denied: JWT already expired (even with clock skew tolerance)', {
99
+ jwtExpiration: userIdResult.jwtExpiration,
100
+ now: JSON.stringify(now),
101
+ });
69
102
  return undefined;
70
103
  }
71
104
  /**
@@ -81,6 +114,15 @@ export class BackendAuthClient {
81
114
  relativeTo: maxSessionEndDate,
82
115
  });
83
116
  if (isSessionExpired) {
117
+ this.logForUser({
118
+ user: undefined,
119
+ userId: userIdResult.userId,
120
+ assumedUserParams: undefined,
121
+ }, 'Session refresh denied: max session duration exceeded', {
122
+ sessionStartedAt: userIdResult.sessionStartedAt,
123
+ maxSessionEndDate: JSON.stringify(maxSessionEndDate),
124
+ now: JSON.stringify(now),
125
+ });
84
126
  return undefined;
85
127
  }
86
128
  }
@@ -111,15 +153,23 @@ export class BackendAuthClient {
111
153
  };
112
154
  }
113
155
  else {
156
+ this.logForUser({
157
+ user: undefined,
158
+ userId: userIdResult.userId,
159
+ assumedUserParams: undefined,
160
+ }, 'Session refresh skipped: not yet ready for refresh', {
161
+ jwtIssuedAt: userIdResult.jwtIssuedAt,
162
+ sessionRefreshStartTime,
163
+ });
114
164
  return undefined;
115
165
  }
116
166
  }
117
167
  /** Reads the user's assumed user headers and, if configured, gets the assumed user. */
118
- async getAssumedUser({ headers, user, }) {
168
+ async getAssumedUser({ requestHeaders, user, }) {
119
169
  if (!this.config.assumeUser || !(await this.config.assumeUser.canAssumeUser(user))) {
120
170
  return undefined;
121
171
  }
122
- const assumedUserHeader = ensureArray(headers[this.config.assumedUserHeaderName || AuthHeaderName.AssumedUser])[0];
172
+ const assumedUserHeader = ensureArray(requestHeaders[this.config.assumedUserHeaderName || AuthHeaderName.AssumedUser])[0];
123
173
  if (!assumedUserHeader) {
124
174
  return undefined;
125
175
  }
@@ -131,6 +181,7 @@ export class BackendAuthClient {
131
181
  isSignUpCookie: false,
132
182
  userId: parsedAssumedUserData.userId,
133
183
  assumingUser: parsedAssumedUserData.assumedUserParams,
184
+ requestHeaders,
134
185
  });
135
186
  return assumedUser;
136
187
  }
@@ -138,18 +189,33 @@ export class BackendAuthClient {
138
189
  async getSecureUser({ requestHeaders, isSignUpCookie, allowUserAuthRefresh, }) {
139
190
  const userIdResult = await extractUserIdFromRequestHeaders(requestHeaders, await this.getJwtParams(), this.config.csrf, isSignUpCookie ? AuthCookieName.SignUp : AuthCookieName.Auth);
140
191
  if (!userIdResult) {
192
+ this.logForUser({
193
+ user: undefined,
194
+ userId: undefined,
195
+ assumedUserParams: undefined,
196
+ }, 'getSecureUser: failed to extract user ID from request headers (invalid JWT, missing cookie, or CSRF mismatch)', {
197
+ isSignUpCookie,
198
+ });
141
199
  return undefined;
142
200
  }
143
201
  const user = await this.getDatabaseUser({
144
202
  userId: userIdResult.userId,
145
203
  assumingUser: undefined,
146
204
  isSignUpCookie,
205
+ requestHeaders,
147
206
  });
148
207
  if (!user) {
208
+ this.logForUser({
209
+ user: undefined,
210
+ userId: userIdResult.userId,
211
+ assumedUserParams: undefined,
212
+ }, 'getSecureUser: user not found in database', {
213
+ isSignUpCookie,
214
+ });
149
215
  return undefined;
150
216
  }
151
217
  const assumedUser = await this.getAssumedUser({
152
- headers: requestHeaders,
218
+ requestHeaders,
153
219
  user,
154
220
  });
155
221
  const cookieRefreshHeaders = (await this.createCookieRefreshHeaders({
@@ -172,7 +238,9 @@ export class BackendAuthClient {
172
238
  const cachedParsedKeys = this.cachedParsedJwtKeys[cacheKey];
173
239
  const parsedKeys = cachedParsedKeys || (await parseJwtKeys(rawJwtKeys));
174
240
  if (!cachedParsedKeys) {
175
- this.cachedParsedJwtKeys = { [cacheKey]: parsedKeys };
241
+ this.cachedParsedJwtKeys = {
242
+ [cacheKey]: parsedKeys,
243
+ };
176
244
  }
177
245
  return {
178
246
  jwtKeys: parsedKeys,
@@ -238,11 +306,17 @@ export class BackendAuthClient {
238
306
  async getInsecureOrSecureUser(params) {
239
307
  const secureUser = await this.getSecureUser(params);
240
308
  if (secureUser) {
241
- return { secureUser };
309
+ return {
310
+ secureUser,
311
+ };
242
312
  }
243
313
  // eslint-disable-next-line @typescript-eslint/no-deprecated
244
314
  const insecureUser = await this.getInsecureUser(params);
245
- return insecureUser ? { insecureUser } : {};
315
+ return insecureUser
316
+ ? {
317
+ insecureUser,
318
+ }
319
+ : {};
246
320
  }
247
321
  /**
248
322
  * @deprecated This only half authenticates the user. It should only be used in circumstances
@@ -253,14 +327,25 @@ export class BackendAuthClient {
253
327
  // eslint-disable-next-line @typescript-eslint/no-deprecated
254
328
  const userIdResult = await insecureExtractUserIdFromCookieAlone(requestHeaders, await this.getJwtParams(), AuthCookieName.Auth);
255
329
  if (!userIdResult) {
330
+ this.logForUser({
331
+ user: undefined,
332
+ userId: undefined,
333
+ assumedUserParams: undefined,
334
+ }, 'getInsecureUser: failed to extract user ID from cookie (invalid JWT or missing cookie)');
256
335
  return undefined;
257
336
  }
258
337
  const user = await this.getDatabaseUser({
259
338
  isSignUpCookie: false,
260
339
  userId: userIdResult.userId,
261
340
  assumingUser: undefined,
341
+ requestHeaders,
262
342
  });
263
343
  if (!user) {
344
+ this.logForUser({
345
+ user: undefined,
346
+ userId: userIdResult.userId,
347
+ assumedUserParams: undefined,
348
+ }, 'getInsecureUser: user not found in database');
264
349
  return undefined;
265
350
  }
266
351
  const refreshHeaders = allowUserAuthRefresh &&
@@ -26,7 +26,9 @@ export class FrontendAuthClient {
26
26
  });
27
27
  }
28
28
  },
29
- debounce: config.checkUser.debounce || { minutes: 1 },
29
+ debounce: config.checkUser.debounce || {
30
+ minutes: 1,
31
+ },
30
32
  fireImmediately: false,
31
33
  });
32
34
  }
package/dist/cookie.js CHANGED
@@ -29,7 +29,9 @@ export async function generateAuthCookie(userJwtData, cookieConfig) {
29
29
  HttpOnly: true,
30
30
  Path: '/',
31
31
  SameSite: 'Strict',
32
- 'MAX-AGE': convertDuration(cookieConfig.cookieDuration, { seconds: true }).seconds,
32
+ 'MAX-AGE': convertDuration(cookieConfig.cookieDuration, {
33
+ seconds: true,
34
+ }).seconds,
33
35
  Secure: !cookieConfig.isDev,
34
36
  }),
35
37
  expiration,
@@ -17,7 +17,9 @@ export const csrfTokenShape = defineShape({
17
17
  * @category Internal
18
18
  * @default {minutes: 5}
19
19
  */
20
- export const defaultAllowedClockSkew = { minutes: 5 };
20
+ export const defaultAllowedClockSkew = {
21
+ minutes: 5,
22
+ };
21
23
  /**
22
24
  * Generates a random, cryptographically secure CSRF token.
23
25
  *
package/dist/jwt/jwt.js CHANGED
@@ -2,8 +2,13 @@ import { assertWrap, check } from '@augment-vir/assert';
2
2
  import { calculateRelativeDate, convertDuration, createFullDateInUserTimezone, createUtcFullDate, getNowInUtcTimezone, toTimestamp, } from 'date-vir';
3
3
  import { EncryptJWT, jwtDecrypt, jwtVerify, SignJWT } from 'jose';
4
4
  import { defaultAllowedClockSkew } from '../csrf-token.js';
5
- const encryptionProtectedHeader = { alg: 'dir', enc: 'A256GCM' };
6
- const signingProtectedHeader = { alg: 'HS512' };
5
+ const encryptionProtectedHeader = {
6
+ alg: 'dir',
7
+ enc: 'A256GCM',
8
+ };
9
+ const signingProtectedHeader = {
10
+ alg: 'HS512',
11
+ };
7
12
  /**
8
13
  * JWT uses seconds since the epoch per RFC 7519, whereas `toTimestamp` uses milliseconds.
9
14
  *
@@ -28,7 +33,9 @@ export function parseJwtTimestamp(seconds) {
28
33
  export async function createJwt(
29
34
  /** The data to be included in the JWT. */
30
35
  data, params) {
31
- const rawJwt = new SignJWT({ data })
36
+ const rawJwt = new SignJWT({
37
+ data,
38
+ })
32
39
  .setProtectedHeader(signingProtectedHeader)
33
40
  .setIssuedAt(params.issuedAt
34
41
  ? toJwtTimestamp(createFullDateInUserTimezone(params.issuedAt))
@@ -40,7 +47,9 @@ data, params) {
40
47
  rawJwt.setNotBefore(toJwtTimestamp(createFullDateInUserTimezone(params.notValidUntil)));
41
48
  }
42
49
  const signedJwt = await rawJwt.sign(params.jwtKeys.signingKey);
43
- return await new EncryptJWT({ jwt: signedJwt })
50
+ return await new EncryptJWT({
51
+ jwt: signedJwt,
52
+ })
44
53
  .setProtectedHeader(encryptionProtectedHeader)
45
54
  .encrypt(params.jwtKeys.encryptionKey);
46
55
  }
@@ -58,7 +67,9 @@ export async function parseJwt(encryptedJwt, params) {
58
67
  else if (!check.isString(decryptedJwt.payload.jwt)) {
59
68
  throw new TypeError('Decrypted jwt is not a string.');
60
69
  }
61
- const clockToleranceSeconds = convertDuration(params.allowedClockSkew || defaultAllowedClockSkew, { seconds: true }).seconds;
70
+ const clockToleranceSeconds = convertDuration(params.allowedClockSkew || defaultAllowedClockSkew, {
71
+ seconds: true,
72
+ }).seconds;
62
73
  const verifiedJwt = await jwtVerify(decryptedJwt.payload.jwt, params.jwtKeys.signingKey, {
63
74
  issuer: params.issuer,
64
75
  audience: params.audience,
@@ -19,7 +19,9 @@ export const userJwtDataShape = defineShape({
19
19
  * enforce the max session duration. If not present, the session is considered to have started
20
20
  * when the JWT was issued.
21
21
  */
22
- sessionStartedAt: optionalShape(0, { alsoUndefined: true }),
22
+ sessionStartedAt: optionalShape(0, {
23
+ alsoUndefined: true,
24
+ }),
23
25
  });
24
26
  /**
25
27
  * Creates a new signed and encrypted {@link JwtUserData} when a client (frontend) successfully
@@ -44,7 +44,10 @@ init = {}) {
44
44
  delete store[key];
45
45
  },
46
46
  setItem(key, value) {
47
- accessRecord.setItem.push({ key, value });
47
+ accessRecord.setItem.push({
48
+ key,
49
+ value,
50
+ });
48
51
  store[key] = value;
49
52
  },
50
53
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "auth-vir",
3
- "version": "3.0.0",
3
+ "version": "3.1.0",
4
4
  "description": "Auth made easy and secure via JWT cookies, CSRF tokens, and password hashing helpers.",
5
5
  "keywords": [
6
6
  "auth",
@@ -41,9 +41,9 @@
41
41
  "test:web": "virmator test web"
42
42
  },
43
43
  "dependencies": {
44
- "@augment-vir/assert": "^31.65.0",
45
- "@augment-vir/common": "^31.65.0",
46
- "date-vir": "^8.1.1",
44
+ "@augment-vir/assert": "^31.67.1",
45
+ "@augment-vir/common": "^31.67.1",
46
+ "date-vir": "^8.2.0",
47
47
  "detect-activity": "^1.0.0",
48
48
  "hash-wasm": "^4.12.0",
49
49
  "jose": "^6.1.3",
@@ -52,9 +52,9 @@
52
52
  "url-vir": "^2.1.7"
53
53
  },
54
54
  "devDependencies": {
55
- "@augment-vir/test": "^31.65.0",
55
+ "@augment-vir/test": "^31.67.1",
56
56
  "@prisma/client": "^6.19.2",
57
- "@types/node": "^25.3.0",
57
+ "@types/node": "^25.3.3",
58
58
  "@web/dev-server-esbuild": "^1.0.5",
59
59
  "@web/test-runner": "^0.20.2",
60
60
  "@web/test-runner-commands": "^0.9.0",
@@ -77,6 +77,7 @@ export type BackendAuthClientConfig<
77
77
  * their user identity. Otherwise, this is `undefined`.
78
78
  */
79
79
  assumingUser: AssumedUserParams | undefined;
80
+ requestHeaders: Readonly<IncomingHttpHeaders>;
80
81
  }) => MaybePromise<DatabaseUser | undefined | null>;
81
82
  /**
82
83
  * Get JWT keys produced by {@link generateNewJwtKeys}. Make sure that each time this is
@@ -90,6 +91,12 @@ export type BackendAuthClientConfig<
90
91
  */
91
92
  isDev: boolean;
92
93
  } & PartialWithUndefined<{
94
+ /** If this returns true, logging will be enabled while handling the relevant session. */
95
+ enableLogging(params: {
96
+ user: DatabaseUser | undefined;
97
+ userId: UserId | undefined;
98
+ assumedUserParams: AssumedUserParams | undefined;
99
+ }): boolean;
93
100
  /**
94
101
  * Overwrite the header name used for tracking is an admin is assuming the identity of
95
102
  * another user.
@@ -102,6 +109,8 @@ export type BackendAuthClientConfig<
102
109
  generateServiceOrigin(params: {
103
110
  requestHeaders: Readonly<IncomingHttpHeaders>;
104
111
  }): MaybePromise<undefined | string>;
112
+ /** If provided, logs will be sent to this method. */
113
+ log?: (message: string, extraData: AnyObject) => void;
105
114
  /**
106
115
  * Set this to allow specific users (determined by `canAssumeUser`) to assume the identity
107
116
  * of other users. This should only be used for admins so that they can troubleshoot user
@@ -196,6 +205,30 @@ export class BackendAuthClient<
196
205
  protected readonly config: BackendAuthClientConfig<DatabaseUser, UserId, AssumedUserParams>,
197
206
  ) {}
198
207
 
208
+ /** Conditionally logs a message if logging is enabled for the given user context. */
209
+ protected logForUser(
210
+ params: {
211
+ user: DatabaseUser | undefined;
212
+ userId: UserId | undefined;
213
+ assumedUserParams: AssumedUserParams | undefined;
214
+ },
215
+ message: string,
216
+ extra?: Record<string, unknown>,
217
+ ): void {
218
+ if (this.config.enableLogging?.(params)) {
219
+ const extraData = {
220
+ userId: params.userId,
221
+ ...extra,
222
+ };
223
+
224
+ if (this.config.log) {
225
+ this.config.log(message, extraData);
226
+ } else {
227
+ console.info(`[auth-vir] ${message}`, extraData);
228
+ }
229
+ }
230
+ }
231
+
199
232
  /** Get all the parameters used for cookie generation. */
200
233
  protected async getCookieParams({
201
234
  isSignUpCookie,
@@ -211,7 +244,9 @@ export class BackendAuthClient<
211
244
  requestHeaders: Readonly<IncomingHttpHeaders> | undefined;
212
245
  }): Promise<Readonly<CookieParams>> {
213
246
  const serviceOrigin = requestHeaders
214
- ? await this.config.generateServiceOrigin?.({requestHeaders})
247
+ ? await this.config.generateServiceOrigin?.({
248
+ requestHeaders,
249
+ })
215
250
  : undefined;
216
251
 
217
252
  return {
@@ -228,10 +263,12 @@ export class BackendAuthClient<
228
263
  isSignUpCookie,
229
264
  userId,
230
265
  assumingUser,
266
+ requestHeaders,
231
267
  }: {
232
268
  userId: UserId | undefined;
233
269
  assumingUser: AssumedUserParams | undefined;
234
270
  isSignUpCookie: boolean;
271
+ requestHeaders: IncomingHttpHeaders;
235
272
  }): Promise<undefined | DatabaseUser> {
236
273
  if (!userId) {
237
274
  return undefined;
@@ -241,9 +278,21 @@ export class BackendAuthClient<
241
278
  assumingUser,
242
279
  userId,
243
280
  isSignUpCookie,
281
+ requestHeaders,
244
282
  });
245
283
 
246
284
  if (!authenticatedUser) {
285
+ this.logForUser(
286
+ {
287
+ user: undefined,
288
+ userId,
289
+ assumedUserParams: assumingUser,
290
+ },
291
+ 'getUserFromDatabase returned no user',
292
+ {
293
+ isSignUpCookie,
294
+ },
295
+ );
247
296
  return undefined;
248
297
  }
249
298
 
@@ -269,6 +318,18 @@ export class BackendAuthClient<
269
318
  });
270
319
 
271
320
  if (isExpiredAlready) {
321
+ this.logForUser(
322
+ {
323
+ user: undefined,
324
+ userId: userIdResult.userId,
325
+ assumedUserParams: undefined,
326
+ },
327
+ 'Session refresh denied: JWT already expired (even with clock skew tolerance)',
328
+ {
329
+ jwtExpiration: userIdResult.jwtExpiration,
330
+ now: JSON.stringify(now),
331
+ },
332
+ );
272
333
  return undefined;
273
334
  }
274
335
 
@@ -286,6 +347,19 @@ export class BackendAuthClient<
286
347
  });
287
348
 
288
349
  if (isSessionExpired) {
350
+ this.logForUser(
351
+ {
352
+ user: undefined,
353
+ userId: userIdResult.userId,
354
+ assumedUserParams: undefined,
355
+ },
356
+ 'Session refresh denied: max session duration exceeded',
357
+ {
358
+ sessionStartedAt: userIdResult.sessionStartedAt,
359
+ maxSessionEndDate: JSON.stringify(maxSessionEndDate),
360
+ now: JSON.stringify(now),
361
+ },
362
+ );
289
363
  return undefined;
290
364
  }
291
365
  }
@@ -323,24 +397,36 @@ export class BackendAuthClient<
323
397
  }),
324
398
  };
325
399
  } else {
400
+ this.logForUser(
401
+ {
402
+ user: undefined,
403
+ userId: userIdResult.userId,
404
+ assumedUserParams: undefined,
405
+ },
406
+ 'Session refresh skipped: not yet ready for refresh',
407
+ {
408
+ jwtIssuedAt: userIdResult.jwtIssuedAt,
409
+ sessionRefreshStartTime,
410
+ },
411
+ );
326
412
  return undefined;
327
413
  }
328
414
  }
329
415
 
330
416
  /** Reads the user's assumed user headers and, if configured, gets the assumed user. */
331
417
  protected async getAssumedUser({
332
- headers,
418
+ requestHeaders,
333
419
  user,
334
420
  }: {
335
421
  user: DatabaseUser;
336
- headers: IncomingHttpHeaders;
422
+ requestHeaders: IncomingHttpHeaders;
337
423
  }): Promise<DatabaseUser | undefined> {
338
424
  if (!this.config.assumeUser || !(await this.config.assumeUser.canAssumeUser(user))) {
339
425
  return undefined;
340
426
  }
341
427
 
342
428
  const assumedUserHeader: string | undefined = ensureArray(
343
- headers[this.config.assumedUserHeaderName || AuthHeaderName.AssumedUser],
429
+ requestHeaders[this.config.assumedUserHeaderName || AuthHeaderName.AssumedUser],
344
430
  )[0];
345
431
 
346
432
  if (!assumedUserHeader) {
@@ -358,6 +444,7 @@ export class BackendAuthClient<
358
444
  isSignUpCookie: false,
359
445
  userId: parsedAssumedUserData.userId,
360
446
  assumingUser: parsedAssumedUserData.assumedUserParams,
447
+ requestHeaders,
361
448
  });
362
449
 
363
450
  return assumedUser;
@@ -385,6 +472,17 @@ export class BackendAuthClient<
385
472
  isSignUpCookie ? AuthCookieName.SignUp : AuthCookieName.Auth,
386
473
  );
387
474
  if (!userIdResult) {
475
+ this.logForUser(
476
+ {
477
+ user: undefined,
478
+ userId: undefined,
479
+ assumedUserParams: undefined,
480
+ },
481
+ 'getSecureUser: failed to extract user ID from request headers (invalid JWT, missing cookie, or CSRF mismatch)',
482
+ {
483
+ isSignUpCookie,
484
+ },
485
+ );
388
486
  return undefined;
389
487
  }
390
488
 
@@ -392,14 +490,26 @@ export class BackendAuthClient<
392
490
  userId: userIdResult.userId,
393
491
  assumingUser: undefined,
394
492
  isSignUpCookie,
493
+ requestHeaders,
395
494
  });
396
495
 
397
496
  if (!user) {
497
+ this.logForUser(
498
+ {
499
+ user: undefined,
500
+ userId: userIdResult.userId,
501
+ assumedUserParams: undefined,
502
+ },
503
+ 'getSecureUser: user not found in database',
504
+ {
505
+ isSignUpCookie,
506
+ },
507
+ );
398
508
  return undefined;
399
509
  }
400
510
 
401
511
  const assumedUser = await this.getAssumedUser({
402
- headers: requestHeaders,
512
+ requestHeaders,
403
513
  user,
404
514
  });
405
515
 
@@ -429,7 +539,9 @@ export class BackendAuthClient<
429
539
  const parsedKeys = cachedParsedKeys || (await parseJwtKeys(rawJwtKeys));
430
540
 
431
541
  if (!cachedParsedKeys) {
432
- this.cachedParsedJwtKeys = {[cacheKey]: parsedKeys};
542
+ this.cachedParsedJwtKeys = {
543
+ [cacheKey]: parsedKeys,
544
+ };
433
545
  }
434
546
  return {
435
547
  jwtKeys: parsedKeys,
@@ -578,13 +690,19 @@ export class BackendAuthClient<
578
690
  const secureUser = await this.getSecureUser(params);
579
691
 
580
692
  if (secureUser) {
581
- return {secureUser};
693
+ return {
694
+ secureUser,
695
+ };
582
696
  }
583
697
 
584
698
  // eslint-disable-next-line @typescript-eslint/no-deprecated
585
699
  const insecureUser = await this.getInsecureUser(params);
586
700
 
587
- return insecureUser ? {insecureUser} : {};
701
+ return insecureUser
702
+ ? {
703
+ insecureUser,
704
+ }
705
+ : {};
588
706
  }
589
707
 
590
708
  /**
@@ -612,6 +730,14 @@ export class BackendAuthClient<
612
730
  );
613
731
 
614
732
  if (!userIdResult) {
733
+ this.logForUser(
734
+ {
735
+ user: undefined,
736
+ userId: undefined,
737
+ assumedUserParams: undefined,
738
+ },
739
+ 'getInsecureUser: failed to extract user ID from cookie (invalid JWT or missing cookie)',
740
+ );
615
741
  return undefined;
616
742
  }
617
743
 
@@ -619,9 +745,18 @@ export class BackendAuthClient<
619
745
  isSignUpCookie: false,
620
746
  userId: userIdResult.userId,
621
747
  assumingUser: undefined,
748
+ requestHeaders,
622
749
  });
623
750
 
624
751
  if (!user) {
752
+ this.logForUser(
753
+ {
754
+ user: undefined,
755
+ userId: userIdResult.userId,
756
+ assumedUserParams: undefined,
757
+ },
758
+ 'getInsecureUser: user not found in database',
759
+ );
625
760
  return undefined;
626
761
  }
627
762
 
@@ -112,7 +112,9 @@ export class FrontendAuthClient<AssumedUserParams extends JsonCompatibleObject =
112
112
  });
113
113
  }
114
114
  },
115
- debounce: config.checkUser.debounce || {minutes: 1},
115
+ debounce: config.checkUser.debounce || {
116
+ minutes: 1,
117
+ },
116
118
  fireImmediately: false,
117
119
  });
118
120
  }
package/src/cookie.ts CHANGED
@@ -91,7 +91,9 @@ export async function generateAuthCookie(
91
91
  HttpOnly: true,
92
92
  Path: '/',
93
93
  SameSite: 'Strict',
94
- 'MAX-AGE': convertDuration(cookieConfig.cookieDuration, {seconds: true}).seconds,
94
+ 'MAX-AGE': convertDuration(cookieConfig.cookieDuration, {
95
+ seconds: true,
96
+ }).seconds,
95
97
  Secure: !cookieConfig.isDev,
96
98
  }),
97
99
  expiration,
package/src/csrf-token.ts CHANGED
@@ -38,7 +38,9 @@ export type CsrfToken = typeof csrfTokenShape.runtimeType;
38
38
  * @category Internal
39
39
  * @default {minutes: 5}
40
40
  */
41
- export const defaultAllowedClockSkew: Readonly<AnyDuration> = {minutes: 5};
41
+ export const defaultAllowedClockSkew: Readonly<AnyDuration> = {
42
+ minutes: 5,
43
+ };
42
44
 
43
45
  /**
44
46
  * Generates a random, cryptographically secure CSRF token.
package/src/jwt/jwt.ts CHANGED
@@ -16,8 +16,13 @@ import {EncryptJWT, jwtDecrypt, jwtVerify, SignJWT} from 'jose';
16
16
  import {defaultAllowedClockSkew} from '../csrf-token.js';
17
17
  import {type JwtKeys} from './jwt-keys.js';
18
18
 
19
- const encryptionProtectedHeader = {alg: 'dir', enc: 'A256GCM'};
20
- const signingProtectedHeader = {alg: 'HS512'};
19
+ const encryptionProtectedHeader = {
20
+ alg: 'dir',
21
+ enc: 'A256GCM',
22
+ };
23
+ const signingProtectedHeader = {
24
+ alg: 'HS512',
25
+ };
21
26
 
22
27
  /**
23
28
  * Params for {@link createJwt}.
@@ -110,7 +115,9 @@ export async function createJwt<JwtData extends AnyObject = AnyObject>(
110
115
  data: JwtData,
111
116
  params: Readonly<CreateJwtParams>,
112
117
  ): Promise<string> {
113
- const rawJwt = new SignJWT({data})
118
+ const rawJwt = new SignJWT({
119
+ data,
120
+ })
114
121
  .setProtectedHeader(signingProtectedHeader)
115
122
  .setIssuedAt(
116
123
  params.issuedAt
@@ -129,7 +136,9 @@ export async function createJwt<JwtData extends AnyObject = AnyObject>(
129
136
 
130
137
  const signedJwt = await rawJwt.sign(params.jwtKeys.signingKey);
131
138
 
132
- return await new EncryptJWT({jwt: signedJwt})
139
+ return await new EncryptJWT({
140
+ jwt: signedJwt,
141
+ })
133
142
  .setProtectedHeader(encryptionProtectedHeader)
134
143
  .encrypt(params.jwtKeys.encryptionKey);
135
144
  }
@@ -182,7 +191,9 @@ export async function parseJwt<JwtData extends AnyObject = AnyObject>(
182
191
 
183
192
  const clockToleranceSeconds = convertDuration(
184
193
  params.allowedClockSkew || defaultAllowedClockSkew,
185
- {seconds: true},
194
+ {
195
+ seconds: true,
196
+ },
186
197
  ).seconds;
187
198
 
188
199
  const verifiedJwt = await jwtVerify(decryptedJwt.payload.jwt, params.jwtKeys.signingKey, {
@@ -27,7 +27,9 @@ export const userJwtDataShape = defineShape({
27
27
  * enforce the max session duration. If not present, the session is considered to have started
28
28
  * when the JWT was issued.
29
29
  */
30
- sessionStartedAt: optionalShape(0, {alsoUndefined: true}),
30
+ sessionStartedAt: optionalShape(0, {
31
+ alsoUndefined: true,
32
+ }),
31
33
  });
32
34
 
33
35
  /**
@@ -59,7 +59,10 @@ export function createMockLocalStorage(
59
59
  delete store[key];
60
60
  },
61
61
  setItem(key, value) {
62
- accessRecord.setItem.push({key, value});
62
+ accessRecord.setItem.push({
63
+ key,
64
+ value,
65
+ });
63
66
  store[key] = value;
64
67
  },
65
68
  };