auth-vir 2.7.2 → 3.0.1

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.
package/README.md CHANGED
@@ -81,10 +81,19 @@ import {
81
81
  parseJwtKeys,
82
82
  type CookieParams,
83
83
  type CreateJwtParams,
84
+ type CsrfHeaderNameOption,
84
85
  } from 'auth-vir';
85
86
 
86
87
  type MyUserId = string;
87
88
 
89
+ /**
90
+ * The CSRF header prefix for this app. Either `csrfHeaderPrefix` or `csrfHeaderName` must be
91
+ * provided to all CSRF-related functions.
92
+ */
93
+ const csrfOption: CsrfHeaderNameOption = {
94
+ csrfHeaderPrefix: 'my-app',
95
+ };
96
+
88
97
  /**
89
98
  * Use this for a /login endpoint.
90
99
  *
@@ -105,7 +114,7 @@ export async function handleLogin(
105
114
  throw new Error('Credentials mismatch.');
106
115
  }
107
116
 
108
- const authHeaders = await generateSuccessfulLoginHeaders(user.id, cookieParams);
117
+ const authHeaders = await generateSuccessfulLoginHeaders(user.id, cookieParams, csrfOption);
109
118
  response.setHeaders(new Headers(authHeaders));
110
119
  }
111
120
 
@@ -121,7 +130,7 @@ export async function createUser(
121
130
  ) {
122
131
  const newUser = await createUserInDatabase(userRequestData);
123
132
 
124
- const authHeaders = await generateSuccessfulLoginHeaders(newUser.id, cookieParams);
133
+ const authHeaders = await generateSuccessfulLoginHeaders(newUser.id, cookieParams, csrfOption);
125
134
  response.setHeaders(new Headers(authHeaders));
126
135
  }
127
136
 
@@ -132,7 +141,7 @@ export async function createUser(
132
141
  */
133
142
  export async function getAuthenticatedUser(request: ClientRequest) {
134
143
  const userId = (
135
- await extractUserIdFromRequestHeaders<MyUserId>(request.getHeaders(), jwtParams)
144
+ await extractUserIdFromRequestHeaders<MyUserId>(request.getHeaders(), jwtParams, csrfOption)
136
145
  )?.userId;
137
146
  const user = userId ? findUserInDatabaseById(userId) : undefined;
138
147
 
@@ -252,15 +261,28 @@ Here's a full example of how to use all the client / frontend side auth function
252
261
 
253
262
  ```TypeScript
254
263
  import {HttpStatus} from '@augment-vir/common';
255
- import {AuthHeaderName} from '../headers.js';
256
- import {getCurrentCsrfToken, handleAuthResponse, wipeCurrentCsrfToken} from 'auth-vir';
264
+ import {
265
+ type CsrfHeaderNameOption,
266
+ getCurrentCsrfToken,
267
+ handleAuthResponse,
268
+ resolveCsrfHeaderName,
269
+ wipeCurrentCsrfToken,
270
+ } from 'auth-vir';
271
+
272
+ /**
273
+ * The CSRF header prefix for this app. Either `csrfHeaderPrefix` or `csrfHeaderName` must be
274
+ * provided to all CSRF-related functions.
275
+ */
276
+ const csrfOption: CsrfHeaderNameOption = {
277
+ csrfHeaderPrefix: 'my-app',
278
+ };
257
279
 
258
280
  /** Call this when the user logs in for the first time this session. */
259
281
  export async function sendLoginRequest(
260
282
  userLoginData: {username: string; password: string},
261
283
  loginUrl: string,
262
284
  ) {
263
- if (getCurrentCsrfToken().csrfToken) {
285
+ if (getCurrentCsrfToken(csrfOption).csrfToken) {
264
286
  throw new Error('Already logged in.');
265
287
  }
266
288
 
@@ -270,7 +292,7 @@ export async function sendLoginRequest(
270
292
  credentials: 'include',
271
293
  });
272
294
 
273
- handleAuthResponse(response);
295
+ handleAuthResponse(response, csrfOption);
274
296
 
275
297
  return response;
276
298
  }
@@ -281,7 +303,7 @@ export async function sendAuthenticatedRequest(
281
303
  requestInit: Omit<RequestInit, 'headers'> = {},
282
304
  headers: Record<string, string> = {},
283
305
  ) {
284
- const {csrfToken} = getCurrentCsrfToken();
306
+ const {csrfToken} = getCurrentCsrfToken(csrfOption);
285
307
 
286
308
  if (!csrfToken) {
287
309
  throw new Error('Not authenticated.');
@@ -292,7 +314,7 @@ export async function sendAuthenticatedRequest(
292
314
  credentials: 'include',
293
315
  headers: {
294
316
  ...headers,
295
- [AuthHeaderName.CsrfToken]: csrfToken.token,
317
+ [resolveCsrfHeaderName(csrfOption)]: csrfToken.token,
296
318
  },
297
319
  });
298
320
 
@@ -302,7 +324,7 @@ export async function sendAuthenticatedRequest(
302
324
  * another tab.)
303
325
  */
304
326
  if (response.status === HttpStatus.Unauthorized) {
305
- wipeCurrentCsrfToken();
327
+ wipeCurrentCsrfToken(csrfOption);
306
328
  throw new Error(`User no longer logged in.`);
307
329
  } else {
308
330
  return response;
@@ -311,7 +333,7 @@ export async function sendAuthenticatedRequest(
311
333
 
312
334
  /** Call this when the user explicitly clicks a "log out" button. */
313
335
  export function logout() {
314
- wipeCurrentCsrfToken();
336
+ wipeCurrentCsrfToken(csrfOption);
315
337
  }
316
338
  ```
317
339
 
@@ -4,9 +4,9 @@ import { type IncomingHttpHeaders, type OutgoingHttpHeaders } from 'node:http';
4
4
  import { type EmptyObject, type RequireExactlyOne, type RequireOneOrNone } from 'type-fest';
5
5
  import { type UserIdResult } from '../auth.js';
6
6
  import { type CookieParams } from '../cookie.js';
7
- import { AuthHeaderName } from '../headers.js';
7
+ import { type CsrfHeaderNameOption } from '../csrf-token.js';
8
8
  import { type JwtKeys, type RawJwtKeys } from '../jwt/jwt-keys.js';
9
- import { type CreateJwtParams } from '../jwt/jwt.js';
9
+ import { type CreateJwtParams, type ParseJwtParams } from '../jwt/jwt.js';
10
10
  /**
11
11
  * Output from `BackendAuthClient.getSecureUser()`.
12
12
  *
@@ -31,7 +31,8 @@ export type GetUserResult<DatabaseUser extends AnyObject> = {
31
31
  *
32
32
  * @category Internal
33
33
  */
34
- export type BackendAuthClientConfig<DatabaseUser extends AnyObject, UserId extends string | number, AssumedUserParams extends JsonCompatibleObject = EmptyObject, CsrfHeaderName extends string = AuthHeaderName.CsrfToken> = Readonly<{
34
+ export type BackendAuthClientConfig<DatabaseUser extends AnyObject, UserId extends string | number, AssumedUserParams extends JsonCompatibleObject = EmptyObject> = Readonly<{
35
+ csrf: Readonly<CsrfHeaderNameOption>;
35
36
  /** The origin of your backend that is offering auth cookies. */
36
37
  serviceOrigin: string;
37
38
  /** Finds the relevant user from your own database. */
@@ -45,6 +46,7 @@ export type BackendAuthClientConfig<DatabaseUser extends AnyObject, UserId exten
45
46
  * their user identity. Otherwise, this is `undefined`.
46
47
  */
47
48
  assumingUser: AssumedUserParams | undefined;
49
+ requestHeaders: Readonly<IncomingHttpHeaders>;
48
50
  }) => MaybePromise<DatabaseUser | undefined | null>;
49
51
  /**
50
52
  * Get JWT keys produced by {@link generateNewJwtKeys}. Make sure that each time this is
@@ -58,6 +60,11 @@ export type BackendAuthClientConfig<DatabaseUser extends AnyObject, UserId exten
58
60
  */
59
61
  isDev: boolean;
60
62
  } & PartialWithUndefined<{
63
+ /**
64
+ * Overwrite the header name used for tracking is an admin is assuming the identity of
65
+ * another user.
66
+ */
67
+ assumedUserHeaderName: string;
61
68
  /**
62
69
  * Optionally generate a service origin from request headers. The generated origin is used
63
70
  * for set-cookie headers.
@@ -107,30 +114,33 @@ export type BackendAuthClientConfig<DatabaseUser extends AnyObject, UserId exten
107
114
  *
108
115
  * @default {minutes: 2}
109
116
  */
110
- sessionRefreshTimeout: Readonly<AnyDuration>;
117
+ sessionRefreshStartTime: Readonly<AnyDuration>;
111
118
  /**
112
119
  * The maximum duration a session can last, regardless of activity. After this time, the
113
120
  * user will be logged out even if they are actively using the application.
114
121
  *
115
- * @default {weeks: 2}
122
+ * @default {days: 1.5}
116
123
  */
117
124
  maxSessionDuration: Readonly<AnyDuration>;
118
- overrides: PartialWithUndefined<{
119
- csrfHeaderName: CsrfHeaderName;
120
- assumedUserHeaderName: string;
121
- }>;
125
+ /**
126
+ * Allowed clock skew tolerance for JWT and CSRF token expiration checks. Accounts for
127
+ * differences between server and client clocks.
128
+ *
129
+ * @default {minutes: 5}
130
+ */
131
+ allowedClockSkew: Readonly<AnyDuration>;
122
132
  }>>;
123
133
  /**
124
134
  * An auth client for creating and validating JWTs embedded in cookies. This should only be used in
125
135
  * a backend environment as it accesses native Node packages.
126
136
  *
127
137
  * @category Auth : Host
128
- * @category Client
138
+ * @category Clients
129
139
  */
130
- export declare class BackendAuthClient<DatabaseUser extends AnyObject, UserId extends string | number, AssumedUserParams extends AnyObject = EmptyObject, CsrfHeaderName extends string = AuthHeaderName.CsrfToken> {
131
- protected readonly config: BackendAuthClientConfig<DatabaseUser, UserId, AssumedUserParams, CsrfHeaderName>;
140
+ export declare class BackendAuthClient<DatabaseUser extends AnyObject, UserId extends string | number, AssumedUserParams extends AnyObject = EmptyObject> {
141
+ protected readonly config: BackendAuthClientConfig<DatabaseUser, UserId, AssumedUserParams>;
132
142
  protected cachedParsedJwtKeys: Record<string, Readonly<JwtKeys>>;
133
- constructor(config: BackendAuthClientConfig<DatabaseUser, UserId, AssumedUserParams, CsrfHeaderName>);
143
+ constructor(config: BackendAuthClientConfig<DatabaseUser, UserId, AssumedUserParams>);
134
144
  /** Get all the parameters used for cookie generation. */
135
145
  protected getCookieParams({ isSignUpCookie, requestHeaders, }: {
136
146
  /**
@@ -143,10 +153,11 @@ export declare class BackendAuthClient<DatabaseUser extends AnyObject, UserId ex
143
153
  requestHeaders: Readonly<IncomingHttpHeaders> | undefined;
144
154
  }): Promise<Readonly<CookieParams>>;
145
155
  /** Calls the provided `getUserFromDatabase` config. */
146
- protected getDatabaseUser({ isSignUpCookie, userId, assumingUser, }: {
156
+ protected getDatabaseUser({ isSignUpCookie, userId, assumingUser, requestHeaders, }: {
147
157
  userId: UserId | undefined;
148
158
  assumingUser: AssumedUserParams | undefined;
149
159
  isSignUpCookie: boolean;
160
+ requestHeaders: IncomingHttpHeaders;
150
161
  }): Promise<undefined | DatabaseUser>;
151
162
  /** Creates a `'cookie-set'` header to refresh the user's session cookie. */
152
163
  protected createCookieRefreshHeaders({ userIdResult, requestHeaders, }: {
@@ -154,9 +165,9 @@ export declare class BackendAuthClient<DatabaseUser extends AnyObject, UserId ex
154
165
  requestHeaders: IncomingHttpHeaders;
155
166
  }): Promise<OutgoingHttpHeaders | undefined>;
156
167
  /** Reads the user's assumed user headers and, if configured, gets the assumed user. */
157
- protected getAssumedUser({ headers, user, }: {
168
+ protected getAssumedUser({ requestHeaders, user, }: {
158
169
  user: DatabaseUser;
159
- headers: IncomingHttpHeaders;
170
+ requestHeaders: IncomingHttpHeaders;
160
171
  }): Promise<DatabaseUser | undefined>;
161
172
  /** Securely extract a user from their request headers. */
162
173
  getSecureUser({ requestHeaders, isSignUpCookie, allowUserAuthRefresh, }: {
@@ -173,7 +184,7 @@ export declare class BackendAuthClient<DatabaseUser extends AnyObject, UserId ex
173
184
  * Get all the JWT params used when creating the auth cookie, in case you need them for
174
185
  * something else too.
175
186
  */
176
- getJwtParams(): Promise<Readonly<CreateJwtParams>>;
187
+ getJwtParams(): Promise<Readonly<CreateJwtParams> & ParseJwtParams>;
177
188
  /** Use these headers to log out the user. */
178
189
  createLogoutHeaders(params: Readonly<RequireExactlyOne<{
179
190
  allCookies: true;
@@ -181,7 +192,7 @@ export declare class BackendAuthClient<DatabaseUser extends AnyObject, UserId ex
181
192
  }> & {
182
193
  /** Overrides the client's already established `serviceOrigin`. */
183
194
  serviceOrigin?: string | undefined;
184
- }>): Promise<Partial<Record<CsrfHeaderName, string>> & {
195
+ }>): Promise<Record<string, string | string[]> & {
185
196
  'set-cookie': string[];
186
197
  }>;
187
198
  /** Use these headers to log a user in. */
@@ -1,25 +1,26 @@
1
1
  import { ensureArray, } from '@augment-vir/common';
2
- import { calculateRelativeDate, createUtcFullDate, getNowInUtcTimezone, isDateAfter, negateDuration, } from 'date-vir';
2
+ import { calculateRelativeDate, createUtcFullDate, getNowInUtcTimezone, isDateAfter, } from 'date-vir';
3
3
  import { extractUserIdFromRequestHeaders, generateLogoutHeaders, generateSuccessfulLoginHeaders, insecureExtractUserIdFromCookieAlone, } from '../auth.js';
4
- import { AuthCookieName } from '../cookie.js';
4
+ import { AuthCookieName, generateAuthCookie } from '../cookie.js';
5
+ import { defaultAllowedClockSkew, resolveCsrfHeaderName, } from '../csrf-token.js';
5
6
  import { AuthHeaderName, mergeHeaderValues } from '../headers.js';
6
7
  import { parseJwtKeys } from '../jwt/jwt-keys.js';
7
- import { authLog } from '../log.js';
8
+ import { isSessionRefreshReady } from './is-session-refresh-ready.js';
8
9
  const defaultSessionIdleTimeout = {
9
10
  minutes: 20,
10
11
  };
11
- const defaultSessionRefreshTimeout = {
12
+ const defaultSessionRefreshStartTime = {
12
13
  minutes: 2,
13
14
  };
14
15
  const defaultMaxSessionDuration = {
15
- weeks: 2,
16
+ days: 1.5,
16
17
  };
17
18
  /**
18
19
  * An auth client for creating and validating JWTs embedded in cookies. This should only be used in
19
20
  * a backend environment as it accesses native Node packages.
20
21
  *
21
22
  * @category Auth : Host
22
- * @category Client
23
+ * @category Clients
23
24
  */
24
25
  export class BackendAuthClient {
25
26
  config;
@@ -41,7 +42,7 @@ export class BackendAuthClient {
41
42
  };
42
43
  }
43
44
  /** Calls the provided `getUserFromDatabase` config. */
44
- async getDatabaseUser({ isSignUpCookie, userId, assumingUser, }) {
45
+ async getDatabaseUser({ isSignUpCookie, userId, assumingUser, requestHeaders, }) {
45
46
  if (!userId) {
46
47
  return undefined;
47
48
  }
@@ -49,6 +50,7 @@ export class BackendAuthClient {
49
50
  assumingUser,
50
51
  userId,
51
52
  isSignUpCookie,
53
+ requestHeaders,
52
54
  });
53
55
  if (!authenticatedUser) {
54
56
  return undefined;
@@ -58,16 +60,13 @@ export class BackendAuthClient {
58
60
  /** Creates a `'cookie-set'` header to refresh the user's session cookie. */
59
61
  async createCookieRefreshHeaders({ userIdResult, requestHeaders, }) {
60
62
  const now = getNowInUtcTimezone();
61
- /** Double check that the JWT hasn't already expired. */
63
+ const clockSkew = this.config.allowedClockSkew || defaultAllowedClockSkew;
64
+ /** Double check that the JWT hasn't already expired (with clock skew tolerance). */
62
65
  const isExpiredAlready = isDateAfter({
63
66
  fullDate: now,
64
- relativeTo: userIdResult.jwtExpiration,
67
+ relativeTo: calculateRelativeDate(userIdResult.jwtExpiration, clockSkew),
65
68
  });
66
69
  if (isExpiredAlready) {
67
- authLog('auth-vir: SESSION EXPIRED - JWT already expired, user will be logged out', {
68
- userId: userIdResult.userId,
69
- jwtExpiration: userIdResult.jwtExpiration,
70
- });
71
70
  return undefined;
72
71
  }
73
72
  /**
@@ -83,51 +82,45 @@ export class BackendAuthClient {
83
82
  relativeTo: maxSessionEndDate,
84
83
  });
85
84
  if (isSessionExpired) {
86
- authLog('auth-vir: SESSION EXPIRED - max session duration exceeded, user will be logged out', {
87
- userId: userIdResult.userId,
88
- sessionStartedAt: userIdResult.sessionStartedAt,
89
- maxSessionDuration,
90
- });
91
85
  return undefined;
92
86
  }
93
87
  }
94
- /**
95
- * This check performs the following: the current time + the refresh threshold > JWT
96
- * expiration.
97
- *
98
- * Visually, this check looks like this:
99
- *
100
- * X C=======Y=======R Z
101
- *
102
- * - C = current time
103
- * - R = C + refresh threshold
104
- * - `=` = the time frame in which {@link isRefreshReady} = true.
105
- * - X = JWT expiration that has already expired (rejected by {@link isExpiredAlready}.
106
- * - Y = JWT expiration within the refresh threshold: {@link isRefreshReady} = true.
107
- * - Z = JWT expiration outside the refresh threshold: {@link isRefreshReady} = false.
108
- */
109
- const sessionRefreshTimeout = this.config.sessionRefreshTimeout || defaultSessionRefreshTimeout;
110
- const isRefreshReady = isDateAfter({
111
- fullDate: now,
112
- relativeTo: calculateRelativeDate(userIdResult.jwtExpiration, negateDuration(sessionRefreshTimeout)),
88
+ const sessionRefreshStartTime = this.config.sessionRefreshStartTime || defaultSessionRefreshStartTime;
89
+ const isRefreshReady = isSessionRefreshReady({
90
+ now,
91
+ jwtIssuedAt: userIdResult.jwtIssuedAt,
92
+ sessionRefreshStartTime,
113
93
  });
114
94
  if (isRefreshReady) {
115
- return this.createLoginHeaders({
95
+ const isSignUpCookie = userIdResult.cookieName === AuthCookieName.SignUp;
96
+ const cookieParams = await this.getCookieParams({
97
+ isSignUpCookie,
116
98
  requestHeaders,
117
- userId: userIdResult.userId,
118
- isSignUpCookie: userIdResult.cookieName === AuthCookieName.SignUp,
119
99
  });
100
+ const csrfHeaderName = resolveCsrfHeaderName(this.config.csrf);
101
+ const { cookie, expiration } = await generateAuthCookie({
102
+ csrfToken: userIdResult.csrfToken,
103
+ userId: userIdResult.userId,
104
+ sessionStartedAt: userIdResult.sessionStartedAt || Date.now(),
105
+ }, cookieParams);
106
+ return {
107
+ 'set-cookie': cookie,
108
+ [csrfHeaderName]: JSON.stringify({
109
+ token: userIdResult.csrfToken,
110
+ expiration,
111
+ }),
112
+ };
120
113
  }
121
114
  else {
122
115
  return undefined;
123
116
  }
124
117
  }
125
118
  /** Reads the user's assumed user headers and, if configured, gets the assumed user. */
126
- async getAssumedUser({ headers, user, }) {
119
+ async getAssumedUser({ requestHeaders, user, }) {
127
120
  if (!this.config.assumeUser || !(await this.config.assumeUser.canAssumeUser(user))) {
128
121
  return undefined;
129
122
  }
130
- const assumedUserHeader = ensureArray(headers[this.config.overrides?.assumedUserHeaderName || AuthHeaderName.AssumedUser])[0];
123
+ const assumedUserHeader = ensureArray(requestHeaders[this.config.assumedUserHeaderName || AuthHeaderName.AssumedUser])[0];
131
124
  if (!assumedUserHeader) {
132
125
  return undefined;
133
126
  }
@@ -139,31 +132,27 @@ export class BackendAuthClient {
139
132
  isSignUpCookie: false,
140
133
  userId: parsedAssumedUserData.userId,
141
134
  assumingUser: parsedAssumedUserData.assumedUserParams,
135
+ requestHeaders,
142
136
  });
143
137
  return assumedUser;
144
138
  }
145
139
  /** Securely extract a user from their request headers. */
146
140
  async getSecureUser({ requestHeaders, isSignUpCookie, allowUserAuthRefresh, }) {
147
- const userIdResult = await extractUserIdFromRequestHeaders(requestHeaders, await this.getJwtParams(), isSignUpCookie ? AuthCookieName.SignUp : AuthCookieName.Auth, this.config.overrides);
141
+ const userIdResult = await extractUserIdFromRequestHeaders(requestHeaders, await this.getJwtParams(), this.config.csrf, isSignUpCookie ? AuthCookieName.SignUp : AuthCookieName.Auth);
148
142
  if (!userIdResult) {
149
- if (!isSignUpCookie) {
150
- authLog('auth-vir: getSecureUser failed - could not extract user from request');
151
- }
152
143
  return undefined;
153
144
  }
154
145
  const user = await this.getDatabaseUser({
155
146
  userId: userIdResult.userId,
156
147
  assumingUser: undefined,
157
148
  isSignUpCookie,
149
+ requestHeaders,
158
150
  });
159
151
  if (!user) {
160
- authLog('auth-vir: getSecureUser failed - user not found in database', {
161
- userId: userIdResult.userId,
162
- });
163
152
  return undefined;
164
153
  }
165
154
  const assumedUser = await this.getAssumedUser({
166
- headers: requestHeaders,
155
+ requestHeaders,
167
156
  user,
168
157
  });
169
158
  const cookieRefreshHeaders = (await this.createCookieRefreshHeaders({
@@ -184,7 +173,7 @@ export class BackendAuthClient {
184
173
  const rawJwtKeys = await this.config.getJwtKeys();
185
174
  const cacheKey = JSON.stringify(rawJwtKeys);
186
175
  const cachedParsedKeys = this.cachedParsedJwtKeys[cacheKey];
187
- const parsedKeys = cachedParsedKeys ?? (await parseJwtKeys(rawJwtKeys));
176
+ const parsedKeys = cachedParsedKeys || (await parseJwtKeys(rawJwtKeys));
188
177
  if (!cachedParsedKeys) {
189
178
  this.cachedParsedJwtKeys = { [cacheKey]: parsedKeys };
190
179
  }
@@ -193,25 +182,22 @@ export class BackendAuthClient {
193
182
  audience: 'server-context',
194
183
  issuer: 'server-auth',
195
184
  jwtDuration: this.config.userSessionIdleTimeout || defaultSessionIdleTimeout,
185
+ allowedClockSkew: this.config.allowedClockSkew || defaultAllowedClockSkew,
196
186
  };
197
187
  }
198
188
  /** Use these headers to log out the user. */
199
189
  async createLogoutHeaders(params) {
200
- authLog('auth-vir: LOGOUT - BackendAuthClient.createLogoutHeaders called', {
201
- allCookies: 'allCookies' in params ? params.allCookies : undefined,
202
- isSignUpCookie: 'isSignUpCookie' in params ? params.isSignUpCookie : undefined,
203
- }, new Error().stack);
204
190
  const signUpCookieHeaders = params.allCookies || params.isSignUpCookie
205
191
  ? generateLogoutHeaders(await this.getCookieParams({
206
192
  isSignUpCookie: true,
207
193
  requestHeaders: undefined,
208
- }), this.config.overrides)
194
+ }), this.config.csrf)
209
195
  : undefined;
210
196
  const authCookieHeaders = params.allCookies || !params.isSignUpCookie
211
197
  ? generateLogoutHeaders(await this.getCookieParams({
212
198
  isSignUpCookie: false,
213
199
  requestHeaders: undefined,
214
- }), this.config.overrides)
200
+ }), this.config.csrf)
215
201
  : undefined;
216
202
  const setCookieHeader = {
217
203
  'set-cookie': mergeHeaderValues(signUpCookieHeaders?.['set-cookie'], authCookieHeaders?.['set-cookie']),
@@ -233,14 +219,14 @@ export class BackendAuthClient {
233
219
  ? generateLogoutHeaders(await this.getCookieParams({
234
220
  isSignUpCookie: !isSignUpCookie,
235
221
  requestHeaders,
236
- }), this.config.overrides)
222
+ }), this.config.csrf)
237
223
  : undefined;
238
- const existingUserIdResult = await extractUserIdFromRequestHeaders(requestHeaders, await this.getJwtParams(), isSignUpCookie ? AuthCookieName.SignUp : AuthCookieName.Auth, this.config.overrides);
224
+ const existingUserIdResult = await extractUserIdFromRequestHeaders(requestHeaders, await this.getJwtParams(), this.config.csrf, isSignUpCookie ? AuthCookieName.SignUp : AuthCookieName.Auth);
239
225
  const sessionStartedAt = existingUserIdResult?.sessionStartedAt;
240
226
  const newCookieHeaders = await generateSuccessfulLoginHeaders(userId, await this.getCookieParams({
241
227
  isSignUpCookie,
242
228
  requestHeaders,
243
- }), this.config.overrides, sessionStartedAt);
229
+ }), this.config.csrf, sessionStartedAt);
244
230
  return {
245
231
  ...newCookieHeaders,
246
232
  'set-cookie': mergeHeaderValues(newCookieHeaders['set-cookie'], discardOppositeCookieHeaders?.['set-cookie']),
@@ -270,18 +256,15 @@ export class BackendAuthClient {
270
256
  // eslint-disable-next-line @typescript-eslint/no-deprecated
271
257
  const userIdResult = await insecureExtractUserIdFromCookieAlone(requestHeaders, await this.getJwtParams(), AuthCookieName.Auth);
272
258
  if (!userIdResult) {
273
- authLog('auth-vir: getInsecureUser failed - could not extract user from request');
274
259
  return undefined;
275
260
  }
276
261
  const user = await this.getDatabaseUser({
277
262
  isSignUpCookie: false,
278
263
  userId: userIdResult.userId,
279
264
  assumingUser: undefined,
265
+ requestHeaders,
280
266
  });
281
267
  if (!user) {
282
- authLog('auth-vir: getInsecureUser failed - user not found in database', {
283
- userId: userIdResult.userId,
284
- });
285
268
  return undefined;
286
269
  }
287
270
  const refreshHeaders = allowUserAuthRefresh &&
@@ -1,12 +1,15 @@
1
1
  import { type createBlockingInterval, type JsonCompatibleObject, type MaybePromise, type PartialWithUndefined, type SelectFrom } from '@augment-vir/common';
2
2
  import { type AnyDuration } from 'date-vir';
3
3
  import { type EmptyObject } from 'type-fest';
4
+ import { type CsrfHeaderNameOption } from '../csrf-token.js';
4
5
  /**
5
6
  * Config for {@link FrontendAuthClient}.
6
7
  *
7
8
  * @category Internal
8
9
  */
9
- export type FrontendAuthClientConfig = PartialWithUndefined<{
10
+ export type FrontendAuthClientConfig = Readonly<{
11
+ csrf: Readonly<CsrfHeaderNameOption>;
12
+ }> & PartialWithUndefined<{
10
13
  /**
11
14
  * Determine if the current user can assume the identity of another user. If this is not
12
15
  * defined, all users will be blocked from assuming other user identities.
@@ -15,8 +18,8 @@ export type FrontendAuthClientConfig = PartialWithUndefined<{
15
18
  /** Called whenever the current user becomes unauthorized and their CSRF token is wiped. */
16
19
  authClearedCallback: () => MaybePromise<void>;
17
20
  /**
18
- * Performs automatic checks on an interval to see if the user is still authenticated. Omit this
19
- * to turn off automatic checks.
21
+ * Performs automatic checks on an interval to see if the user is still authenticated. Omit
22
+ * this to turn off automatic checks.
20
23
  */
21
24
  checkUser: {
22
25
  /**
@@ -27,8 +30,8 @@ export type FrontendAuthClientConfig = PartialWithUndefined<{
27
30
  * If the user is not currently authorized, this should return `undefined` to prevent
28
31
  * unnecessary network traffic.
29
32
  *
30
- * This will be called any time the user interacts with the page, debounced by the adjacent
31
- * `debounce` property.
33
+ * This will be called any time the user interacts with the page, debounced by the
34
+ * adjacent `debounce` property.
32
35
  */
33
36
  performCheck: () => MaybePromise<SelectFrom<Response, {
34
37
  status: true;
@@ -40,10 +43,20 @@ export type FrontendAuthClientConfig = PartialWithUndefined<{
40
43
  */
41
44
  debounce?: AnyDuration | undefined;
42
45
  };
46
+ /**
47
+ * Overwrite the header name used for tracking is an admin is assuming the identity of
48
+ * another user.
49
+ */
50
+ assumedUserHeaderName: string;
51
+ /**
52
+ * Allowed clock skew tolerance for CSRF token expiration checks. Accounts for differences
53
+ * between server and client clocks.
54
+ *
55
+ * @default {minutes: 5}
56
+ */
57
+ allowedClockSkew: Readonly<AnyDuration>;
43
58
  overrides: PartialWithUndefined<{
44
59
  localStorage: Pick<Storage, 'setItem' | 'removeItem' | 'getItem'>;
45
- csrfHeaderName: string;
46
- assumedUserHeaderName: string;
47
60
  }>;
48
61
  }>;
49
62
  /**
@@ -51,21 +64,21 @@ export type FrontendAuthClientConfig = PartialWithUndefined<{
51
64
  * in a frontend environment as it accesses native browser APIs.
52
65
  *
53
66
  * @category Auth : Client
54
- * @category Client
67
+ * @category Clients
55
68
  */
56
69
  export declare class FrontendAuthClient<AssumedUserParams extends JsonCompatibleObject = EmptyObject> {
57
70
  protected readonly config: FrontendAuthClientConfig;
58
71
  protected userCheckInterval: undefined | ReturnType<typeof createBlockingInterval>;
59
72
  /** Used to clean up the activity listener on `.destroy()`. */
60
73
  protected removeActivityListener: VoidFunction | undefined;
61
- constructor(config?: FrontendAuthClientConfig);
74
+ constructor(config: FrontendAuthClientConfig);
62
75
  /**
63
76
  * Destroys the client and performs all necessary cleanup (like clearing the user check
64
77
  * interval).
65
78
  */
66
79
  destroy(): void;
67
80
  /** Wraps {@link getCurrentCsrfToken} to automatically handle wiping an invalid CSRF token. */
68
- getCurrentCsrfToken(): Promise<string | undefined>;
81
+ getCurrentCsrfToken(): string | undefined;
69
82
  /**
70
83
  * Assume the given user. Pass `undefined` to wipe the currently assumed user.
71
84
  *
@@ -80,7 +93,7 @@ export declare class FrontendAuthClient<AssumedUserParams extends JsonCompatible
80
93
  * `@augment-vir/common`](https://electrovir.github.io/augment-vir/functions/mergeDeep.html) to
81
94
  * combine them with these.
82
95
  */
83
- createAuthenticatedRequestInit(): Promise<RequestInit>;
96
+ createAuthenticatedRequestInit(): RequestInit;
84
97
  /** Wipes the current user auth. */
85
98
  logout(): Promise<void>;
86
99
  /**