auth-vir 4.0.0 → 5.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/dist/cookie.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { check } from '@augment-vir/assert';
2
- import { escapeStringForRegExp, safeMatch } from '@augment-vir/common';
3
- import { calculateRelativeDate, convertDuration, getNowInUtcTimezone, } from 'date-vir';
2
+ import { escapeStringForRegExp, safeMatch, } from '@augment-vir/common';
3
+ import { convertDuration } from 'date-vir';
4
4
  import { parseUrl } from 'url-vir';
5
5
  import { createUserJwt, parseUserJwt } from './jwt/user-jwt.js';
6
6
  /**
@@ -8,34 +8,66 @@ import { createUserJwt, parseUserJwt } from './jwt/user-jwt.js';
8
8
  *
9
9
  * @category Internal
10
10
  */
11
- export var AuthCookieName;
12
- (function (AuthCookieName) {
11
+ export var AuthCookie;
12
+ (function (AuthCookie) {
13
13
  /** Used for a full user login auth. */
14
- AuthCookieName["Auth"] = "auth";
14
+ AuthCookie["Auth"] = "auth";
15
15
  /** Use for a temporary "just signed up" auth. */
16
- AuthCookieName["SignUp"] = "sign-up";
17
- })(AuthCookieName || (AuthCookieName = {}));
16
+ AuthCookie["SignUp"] = "sign-up";
17
+ /** Used for storing the CSRF token. Not `HttpOnly` so that frontend JS can read it. */
18
+ AuthCookie["Csrf"] = "auth-vir-csrf";
19
+ })(AuthCookie || (AuthCookie = {}));
20
+ function generateSetCookie({ name, value, httpOnly, cookieConfig, }) {
21
+ return generateCookie({
22
+ [name]: value,
23
+ Domain: parseUrl(cookieConfig.hostOrigin).hostname,
24
+ HttpOnly: httpOnly,
25
+ Path: '/',
26
+ SameSite: 'Strict',
27
+ 'MAX-AGE': cookieConfig.cookieDuration
28
+ ? convertDuration(cookieConfig.cookieDuration, {
29
+ seconds: true,
30
+ }).seconds
31
+ : 0,
32
+ Secure: !cookieConfig.isDev,
33
+ });
34
+ }
18
35
  /**
19
36
  * Generate a secure cookie that stores the user JWT data. Used in host (backend) code.
20
37
  *
21
38
  * @category Internal
22
39
  */
23
40
  export async function generateAuthCookie(userJwtData, cookieConfig) {
24
- const expiration = calculateRelativeDate(getNowInUtcTimezone(), cookieConfig.cookieDuration);
25
- return {
26
- cookie: generateCookie({
27
- [cookieConfig.cookieName || 'auth']: await createUserJwt(userJwtData, cookieConfig.jwtParams),
28
- Domain: parseUrl(cookieConfig.hostOrigin).hostname,
29
- HttpOnly: true,
30
- Path: '/',
31
- SameSite: 'Strict',
32
- 'MAX-AGE': convertDuration(cookieConfig.cookieDuration, {
33
- seconds: true,
34
- }).seconds,
35
- Secure: !cookieConfig.isDev,
36
- }),
37
- expiration,
38
- };
41
+ return generateSetCookie({
42
+ name: cookieConfig.authCookie || AuthCookie.Auth,
43
+ value: await createUserJwt(userJwtData, cookieConfig.jwtParams),
44
+ httpOnly: true,
45
+ cookieConfig,
46
+ });
47
+ }
48
+ /**
49
+ * Generate a CSRF token cookie. This cookie is intentionally not `HttpOnly` so that frontend
50
+ * JavaScript can read it and inject the value as a request header for double-submit verification.
51
+ *
52
+ * The CSRF cookie uses a fixed 400-day MAX-AGE rather than matching the auth cookie duration. 400
53
+ * days is the cross-browser safe maximum (Chrome caps cookie lifetimes at 400 days; other browsers
54
+ * accept it as-is). The CSRF token is only meaningful when paired with a valid JWT, so it doesn't
55
+ * need its own expiration management. It gets regenerated on every fresh login.
56
+ *
57
+ * @category Internal
58
+ */
59
+ export function generateCsrfCookie(csrfToken, cookieConfig) {
60
+ return generateSetCookie({
61
+ name: AuthCookie.Csrf,
62
+ value: csrfToken,
63
+ httpOnly: false,
64
+ cookieConfig: {
65
+ ...cookieConfig,
66
+ cookieDuration: {
67
+ days: 400,
68
+ },
69
+ },
70
+ });
39
71
  }
40
72
  /**
41
73
  * Generate a cookie value that will clear the previous auth cookie. Use this when signing out.
@@ -43,14 +75,24 @@ export async function generateAuthCookie(userJwtData, cookieConfig) {
43
75
  * @category Internal
44
76
  */
45
77
  export function clearAuthCookie(cookieConfig) {
46
- return generateCookie({
47
- [cookieConfig.cookieName || 'auth']: 'redacted',
48
- Domain: parseUrl(cookieConfig.hostOrigin).hostname,
49
- HttpOnly: true,
50
- Path: '/',
51
- SameSite: 'Strict',
52
- 'MAX-AGE': 0,
53
- Secure: !cookieConfig.isDev,
78
+ return generateSetCookie({
79
+ name: cookieConfig.authCookie || AuthCookie.Auth,
80
+ value: 'redacted',
81
+ httpOnly: true,
82
+ cookieConfig,
83
+ });
84
+ }
85
+ /**
86
+ * Generate a cookie value that will clear the CSRF token cookie. Use this when signing out.
87
+ *
88
+ * @category Internal
89
+ */
90
+ export function clearCsrfCookie(cookieConfig) {
91
+ return generateSetCookie({
92
+ name: AuthCookie.Csrf,
93
+ value: 'redacted',
94
+ httpOnly: false,
95
+ cookieConfig,
54
96
  });
55
97
  }
56
98
  /**
@@ -83,7 +125,7 @@ export function generateCookie(params) {
83
125
  * @category Internal
84
126
  * @returns The extracted auth Cookie JWT data or `undefined` if no valid auth JWT data was found.
85
127
  */
86
- export async function extractCookieJwt(rawCookie, jwtParams, cookieName = AuthCookieName.Auth) {
128
+ export async function extractCookieJwt(rawCookie, jwtParams, cookieName) {
87
129
  const cookieRegExp = new RegExp(`${escapeStringForRegExp(cookieName)}=[^;]+(?:;|$)`);
88
130
  const [cookieValue] = safeMatch(rawCookie, cookieRegExp);
89
131
  if (!cookieValue) {
@@ -1,15 +1,4 @@
1
- import { type PartialWithUndefined, type SelectFrom } from '@augment-vir/common';
2
- import { type AnyDuration } from 'date-vir';
3
1
  import { type RequireExactlyOne } from 'type-fest';
4
- import { type CsrfTokenStore } from './csrf-token-store.js';
5
- /**
6
- * Default allowed clock skew for JWT expiration checks. Accounts for differences between server and
7
- * client clocks.
8
- *
9
- * @category Internal
10
- * @default {minutes: 5}
11
- */
12
- export declare const defaultAllowedClockSkew: Readonly<AnyDuration>;
13
2
  /**
14
3
  * Generates a random, cryptographically secure CSRF token string.
15
4
  *
@@ -34,53 +23,11 @@ export type CsrfHeaderNameOption = RequireExactlyOne<{
34
23
  * @category Auth : Client
35
24
  * @category Auth : Host
36
25
  */
37
- export declare function resolveCsrfHeaderName(option: Readonly<CsrfHeaderNameOption>): string;
38
- /**
39
- * Extract the CSRF token header from a response.
40
- *
41
- * @category Auth : Client
42
- */
43
- export declare function extractCsrfTokenHeader(response: Readonly<PartialWithUndefined<SelectFrom<Response, {
44
- headers: true;
45
- }>>>, csrfHeaderNameOption: Readonly<CsrfHeaderNameOption>): string | undefined;
46
- /**
47
- * Stores the given CSRF token into IndexedDB.
48
- *
49
- * @category Auth : Client
50
- */
51
- export declare function storeCsrfToken(csrfToken: string, options: Readonly<CsrfHeaderNameOption> & PartialWithUndefined<{
52
- /**
53
- * Allows mocking or overriding the default CSRF token store.
54
- *
55
- * @default getDefaultCsrfTokenStore()
56
- */
57
- csrfTokenStore: CsrfTokenStore;
58
- }>): Promise<void>;
59
- /**
60
- * Used in client (frontend) code to retrieve the current CSRF token in order to send it with
61
- * requests to the host (backend).
62
- *
63
- * @category Auth : Client
64
- */
65
- export declare function getCurrentCsrfToken(options: Readonly<CsrfHeaderNameOption> & PartialWithUndefined<{
66
- /**
67
- * Allows mocking or overriding the default CSRF token store.
68
- *
69
- * @default getDefaultCsrfTokenStore()
70
- */
71
- csrfTokenStore: CsrfTokenStore;
72
- }>): Promise<string | undefined>;
26
+ export declare function resolveCsrfHeaderName(options: Readonly<CsrfHeaderNameOption>): string;
73
27
  /**
74
- * Wipes the current stored CSRF token. This should be used by client (frontend) code to react to a
75
- * session timeout.
28
+ * Used in client (frontend) code to retrieve the current CSRF token from the browser cookie in
29
+ * order to send it with requests to the host (backend).
76
30
  *
77
31
  * @category Auth : Client
78
32
  */
79
- export declare function wipeCurrentCsrfToken(options: Readonly<CsrfHeaderNameOption> & PartialWithUndefined<{
80
- /**
81
- * Allows mocking or overriding the default CSRF token store.
82
- *
83
- * @default getDefaultCsrfTokenStore()
84
- */
85
- csrfTokenStore: CsrfTokenStore;
86
- }>): Promise<void>;
33
+ export declare function getCurrentCsrfToken(): string | undefined;
@@ -1,15 +1,6 @@
1
- import { randomString } from '@augment-vir/common';
2
- import { getDefaultCsrfTokenStore } from './csrf-token-store.js';
3
- /**
4
- * Default allowed clock skew for JWT expiration checks. Accounts for differences between server and
5
- * client clocks.
6
- *
7
- * @category Internal
8
- * @default {minutes: 5}
9
- */
10
- export const defaultAllowedClockSkew = {
11
- minutes: 5,
12
- };
1
+ import { check } from '@augment-vir/assert';
2
+ import { escapeStringForRegExp, randomString, safeMatch } from '@augment-vir/common';
3
+ import { AuthCookie } from './cookie.js';
13
4
  /**
14
5
  * Generates a random, cryptographically secure CSRF token string.
15
6
  *
@@ -24,51 +15,28 @@ export function generateCsrfToken() {
24
15
  * @category Auth : Client
25
16
  * @category Auth : Host
26
17
  */
27
- export function resolveCsrfHeaderName(option) {
28
- if ('csrfHeaderName' in option && option.csrfHeaderName) {
29
- return option.csrfHeaderName;
18
+ export function resolveCsrfHeaderName(options) {
19
+ if ('csrfHeaderName' in options && options.csrfHeaderName) {
20
+ return options.csrfHeaderName;
30
21
  }
31
22
  else {
32
23
  return [
33
- option.csrfHeaderPrefix,
24
+ options.csrfHeaderPrefix,
34
25
  'auth-vir',
35
26
  'csrf-token',
36
- ].join('-');
27
+ ]
28
+ .filter(check.isTruthy)
29
+ .join('-');
37
30
  }
38
31
  }
39
32
  /**
40
- * Extract the CSRF token header from a response.
41
- *
42
- * @category Auth : Client
43
- */
44
- export function extractCsrfTokenHeader(response, csrfHeaderNameOption) {
45
- const csrfTokenHeaderName = resolveCsrfHeaderName(csrfHeaderNameOption);
46
- return response.headers?.get(csrfTokenHeaderName) || undefined;
47
- }
48
- /**
49
- * Stores the given CSRF token into IndexedDB.
50
- *
51
- * @category Auth : Client
52
- */
53
- export async function storeCsrfToken(csrfToken, options) {
54
- await (options.csrfTokenStore || (await getDefaultCsrfTokenStore())).setCsrfToken(csrfToken);
55
- }
56
- /**
57
- * Used in client (frontend) code to retrieve the current CSRF token in order to send it with
58
- * requests to the host (backend).
59
- *
60
- * @category Auth : Client
61
- */
62
- export async function getCurrentCsrfToken(options) {
63
- return ((await (options.csrfTokenStore || (await getDefaultCsrfTokenStore())).getCsrfToken()) ||
64
- undefined);
65
- }
66
- /**
67
- * Wipes the current stored CSRF token. This should be used by client (frontend) code to react to a
68
- * session timeout.
33
+ * Used in client (frontend) code to retrieve the current CSRF token from the browser cookie in
34
+ * order to send it with requests to the host (backend).
69
35
  *
70
36
  * @category Auth : Client
71
37
  */
72
- export async function wipeCurrentCsrfToken(options) {
73
- await (options.csrfTokenStore || (await getDefaultCsrfTokenStore())).deleteCsrfToken();
38
+ export function getCurrentCsrfToken() {
39
+ const cookieRegExp = new RegExp(`${escapeStringForRegExp(AuthCookie.Csrf)}=([^;]+)`);
40
+ const [, value,] = safeMatch(globalThis.document.cookie, cookieRegExp);
41
+ return value || undefined;
74
42
  }
package/dist/index.d.ts CHANGED
@@ -3,11 +3,9 @@ export * from './auth-client/frontend-auth.client.js';
3
3
  export * from './auth-client/is-session-refresh-ready.js';
4
4
  export * from './auth.js';
5
5
  export * from './cookie.js';
6
- export * from './csrf-token-store.js';
7
6
  export * from './csrf-token.js';
8
7
  export * from './hash.js';
9
8
  export * from './headers.js';
10
9
  export * from './jwt/jwt-keys.js';
11
10
  export * from './jwt/jwt.js';
12
11
  export * from './jwt/user-jwt.js';
13
- export * from './mock-csrf-token-store.js';
package/dist/index.js CHANGED
@@ -3,11 +3,9 @@ export * from './auth-client/frontend-auth.client.js';
3
3
  export * from './auth-client/is-session-refresh-ready.js';
4
4
  export * from './auth.js';
5
5
  export * from './cookie.js';
6
- export * from './csrf-token-store.js';
7
6
  export * from './csrf-token.js';
8
7
  export * from './hash.js';
9
8
  export * from './headers.js';
10
9
  export * from './jwt/jwt-keys.js';
11
10
  export * from './jwt/jwt.js';
12
11
  export * from './jwt/user-jwt.js';
13
- export * from './mock-csrf-token-store.js';
package/dist/jwt/jwt.d.ts CHANGED
@@ -1,6 +1,14 @@
1
- import { type AnyObject, type PartialWithUndefined } from '@augment-vir/common';
1
+ import { type AnyObject, type PartialWithUndefined, type SelectFrom } from '@augment-vir/common';
2
2
  import { type AnyDuration, type DateLike, type FullDate, type UtcTimezone } from 'date-vir';
3
3
  import { type JwtKeys } from './jwt-keys.js';
4
+ /**
5
+ * Default allowed clock skew for JWT expiration checks. Accounts for differences between server and
6
+ * client clocks.
7
+ *
8
+ * @category Internal
9
+ * @default {minutes: 5}
10
+ */
11
+ export declare const defaultAllowedClockSkew: Readonly<AnyDuration>;
4
12
  /**
5
13
  * Params for {@link createJwt}.
6
14
  *
@@ -85,7 +93,11 @@ data: JwtData, params: Readonly<CreateJwtParams>): Promise<string>;
85
93
  *
86
94
  * @category Internal
87
95
  */
88
- export type ParseJwtParams = Readonly<Pick<CreateJwtParams, 'issuer' | 'audience' | 'jwtKeys'>> & PartialWithUndefined<{
96
+ export type ParseJwtParams = Readonly<SelectFrom<CreateJwtParams, {
97
+ issuer: true;
98
+ audience: true;
99
+ jwtKeys: true;
100
+ }>> & PartialWithUndefined<{
89
101
  /**
90
102
  * Allowed clock skew tolerance for JWT expiration and timestamp checks. Accounts for
91
103
  * differences between server and client clocks.
package/dist/jwt/jwt.js CHANGED
@@ -1,7 +1,6 @@
1
1
  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
- import { defaultAllowedClockSkew } from '../csrf-token.js';
5
4
  const encryptionProtectedHeader = {
6
5
  alg: 'dir',
7
6
  enc: 'A256GCM',
@@ -9,6 +8,16 @@ const encryptionProtectedHeader = {
9
8
  const signingProtectedHeader = {
10
9
  alg: 'HS512',
11
10
  };
11
+ /**
12
+ * Default allowed clock skew for JWT expiration checks. Accounts for differences between server and
13
+ * client clocks.
14
+ *
15
+ * @category Internal
16
+ * @default {minutes: 5}
17
+ */
18
+ export const defaultAllowedClockSkew = {
19
+ minutes: 5,
20
+ };
12
21
  /**
13
22
  * JWT uses seconds since the epoch per RFC 7519, whereas `toTimestamp` uses milliseconds.
14
23
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "auth-vir",
3
- "version": "4.0.0",
3
+ "version": "5.0.1",
4
4
  "description": "Auth made easy and secure via JWT cookies, CSRF tokens, and password hashing helpers.",
5
5
  "keywords": [
6
6
  "auth",
@@ -41,21 +41,20 @@
41
41
  "test:web": "virmator test web"
42
42
  },
43
43
  "dependencies": {
44
- "@augment-vir/assert": "^31.68.1",
45
- "@augment-vir/common": "^31.68.1",
44
+ "@augment-vir/assert": "^31.68.2",
45
+ "@augment-vir/common": "^31.68.2",
46
46
  "date-vir": "^8.2.1",
47
47
  "detect-activity": "^1.0.0",
48
48
  "hash-wasm": "^4.12.0",
49
49
  "jose": "^6.2.1",
50
- "local-db-client": "^1.0.0",
51
50
  "object-shape-tester": "^6.11.0",
52
51
  "type-fest": "^5.4.4",
53
52
  "url-vir": "^2.1.7"
54
53
  },
55
54
  "devDependencies": {
56
- "@augment-vir/test": "^31.68.1",
55
+ "@augment-vir/test": "^31.68.2",
57
56
  "@prisma/client": "^6.19.2",
58
- "@types/node": "^25.3.3",
57
+ "@types/node": "^25.5.0",
59
58
  "@web/dev-server-esbuild": "^1.0.5",
60
59
  "@web/test-runner": "^0.20.2",
61
60
  "@web/test-runner-commands": "^0.9.0",
@@ -21,15 +21,11 @@ import {
21
21
  insecureExtractUserIdFromCookieAlone,
22
22
  type UserIdResult,
23
23
  } from '../auth.js';
24
- import {AuthCookieName, generateAuthCookie, type CookieParams} from '../cookie.js';
25
- import {
26
- defaultAllowedClockSkew,
27
- resolveCsrfHeaderName,
28
- type CsrfHeaderNameOption,
29
- } from '../csrf-token.js';
24
+ import {AuthCookie, generateAuthCookie, generateCsrfCookie, type CookieParams} from '../cookie.js';
25
+ import {type CsrfHeaderNameOption} from '../csrf-token.js';
30
26
  import {AuthHeaderName, mergeHeaderValues} from '../headers.js';
31
27
  import {generateNewJwtKeys, parseJwtKeys, type JwtKeys, type RawJwtKeys} from '../jwt/jwt-keys.js';
32
- import {type CreateJwtParams, type ParseJwtParams} from '../jwt/jwt.js';
28
+ import {defaultAllowedClockSkew, type CreateJwtParams, type ParseJwtParams} from '../jwt/jwt.js';
33
29
  import {isSessionRefreshReady} from './is-session-refresh-ready.js';
34
30
 
35
31
  /**
@@ -254,7 +250,7 @@ export class BackendAuthClient<
254
250
  hostOrigin: serviceOrigin || this.config.serviceOrigin,
255
251
  jwtParams: await this.getJwtParams(),
256
252
  isDev: this.config.isDev,
257
- cookieName: isSignUpCookie ? AuthCookieName.SignUp : AuthCookieName.Auth,
253
+ authCookie: isSignUpCookie ? AuthCookie.SignUp : AuthCookie.Auth,
258
254
  };
259
255
  }
260
256
 
@@ -373,14 +369,13 @@ export class BackendAuthClient<
373
369
  });
374
370
 
375
371
  if (isRefreshReady) {
376
- const isSignUpCookie = userIdResult.cookieName === AuthCookieName.SignUp;
372
+ const isSignUpCookie = userIdResult.cookieName === AuthCookie.SignUp;
377
373
  const cookieParams = await this.getCookieParams({
378
374
  isSignUpCookie,
379
375
  requestHeaders,
380
376
  });
381
377
 
382
- const csrfHeaderName = resolveCsrfHeaderName(this.config.csrf);
383
- const {cookie} = await generateAuthCookie(
378
+ const authCookie = await generateAuthCookie(
384
379
  {
385
380
  csrfToken: userIdResult.csrfToken,
386
381
  userId: userIdResult.userId,
@@ -388,10 +383,13 @@ export class BackendAuthClient<
388
383
  },
389
384
  cookieParams,
390
385
  );
386
+ const csrfCookie = generateCsrfCookie(userIdResult.csrfToken, cookieParams);
391
387
 
392
388
  return {
393
- 'set-cookie': cookie,
394
- [csrfHeaderName]: userIdResult.csrfToken,
389
+ 'set-cookie': [
390
+ authCookie,
391
+ csrfCookie,
392
+ ],
395
393
  };
396
394
  } else {
397
395
  this.logForUser(
@@ -466,7 +464,7 @@ export class BackendAuthClient<
466
464
  requestHeaders,
467
465
  await this.getJwtParams(),
468
466
  this.config.csrf,
469
- isSignUpCookie ? AuthCookieName.SignUp : AuthCookieName.Auth,
467
+ isSignUpCookie ? AuthCookie.SignUp : AuthCookie.Auth,
470
468
  );
471
469
  if (!userIdResult) {
472
470
  this.logForUser(
@@ -510,16 +508,31 @@ export class BackendAuthClient<
510
508
  user,
511
509
  });
512
510
 
513
- const cookieRefreshHeaders =
514
- (await this.createCookieRefreshHeaders({
515
- userIdResult,
516
- requestHeaders,
517
- })) || {};
511
+ const cookieRefreshHeaders = allowUserAuthRefresh
512
+ ? await this.createCookieRefreshHeaders({
513
+ userIdResult,
514
+ requestHeaders,
515
+ })
516
+ : undefined;
517
+
518
+ /**
519
+ * Always include the CSRF cookie so it gets re-established if the browser clears it. When
520
+ * session refresh fires, its headers already include a CSRF cookie.
521
+ */
522
+ const csrfCookie = generateCsrfCookie(userIdResult.csrfToken, {
523
+ hostOrigin:
524
+ (await this.config.generateServiceOrigin?.({
525
+ requestHeaders,
526
+ })) || this.config.serviceOrigin,
527
+ isDev: this.config.isDev,
528
+ });
518
529
 
519
530
  return {
520
531
  user: assumedUser || user,
521
532
  isAssumed: !!assumedUser,
522
- responseHeaders: allowUserAuthRefresh ? cookieRefreshHeaders : {},
533
+ responseHeaders: {
534
+ 'set-cookie': mergeHeaderValues(cookieRefreshHeaders?.['set-cookie'], csrfCookie),
535
+ },
523
536
  };
524
537
  }
525
538
 
@@ -604,8 +617,8 @@ export class BackendAuthClient<
604
617
  userId: UserId;
605
618
  cookieParams: Readonly<CookieParams>;
606
619
  existingUserIdResult: Readonly<UserIdResult<UserId>>;
607
- }): Promise<Record<string, string>> {
608
- const {cookie} = await generateAuthCookie(
620
+ }): Promise<Record<string, string | string[]>> {
621
+ const authCookie = await generateAuthCookie(
609
622
  {
610
623
  csrfToken: existingUserIdResult.csrfToken,
611
624
  userId,
@@ -614,8 +627,13 @@ export class BackendAuthClient<
614
627
  cookieParams,
615
628
  );
616
629
 
630
+ const csrfCookie = generateCsrfCookie(existingUserIdResult.csrfToken, cookieParams);
631
+
617
632
  return {
618
- 'set-cookie': cookie,
633
+ 'set-cookie': [
634
+ authCookie,
635
+ csrfCookie,
636
+ ],
619
637
  };
620
638
  }
621
639
 
@@ -629,7 +647,7 @@ export class BackendAuthClient<
629
647
  requestHeaders: IncomingHttpHeaders;
630
648
  isSignUpCookie: boolean;
631
649
  }): Promise<OutgoingHttpHeaders> {
632
- const oppositeCookieName = isSignUpCookie ? AuthCookieName.Auth : AuthCookieName.SignUp;
650
+ const oppositeCookieName = isSignUpCookie ? AuthCookie.Auth : AuthCookie.SignUp;
633
651
  const hasExistingOppositeCookie = requestHeaders.cookie?.includes(`${oppositeCookieName}=`);
634
652
 
635
653
  const discardOppositeCookieHeaders = hasExistingOppositeCookie
@@ -645,7 +663,7 @@ export class BackendAuthClient<
645
663
  requestHeaders,
646
664
  await this.getJwtParams(),
647
665
  this.config.csrf,
648
- isSignUpCookie ? AuthCookieName.SignUp : AuthCookieName.Auth,
666
+ isSignUpCookie ? AuthCookie.SignUp : AuthCookie.Auth,
649
667
  );
650
668
 
651
669
  const cookieParams = await this.getCookieParams({
@@ -659,7 +677,7 @@ export class BackendAuthClient<
659
677
  cookieParams,
660
678
  existingUserIdResult,
661
679
  })
662
- : await generateSuccessfulLoginHeaders(userId, cookieParams, this.config.csrf);
680
+ : await generateSuccessfulLoginHeaders(userId, cookieParams);
663
681
 
664
682
  return {
665
683
  ...newCookieHeaders,
@@ -738,7 +756,7 @@ export class BackendAuthClient<
738
756
  const userIdResult = await insecureExtractUserIdFromCookieAlone<UserId>(
739
757
  requestHeaders,
740
758
  await this.getJwtParams(),
741
- AuthCookieName.Auth,
759
+ AuthCookie.Auth,
742
760
  );
743
761
 
744
762
  if (!userIdResult) {
@@ -9,13 +9,10 @@ import {
9
9
  import {type AnyDuration} from 'date-vir';
10
10
  import {listenToActivity} from 'detect-activity';
11
11
  import {type EmptyObject} from 'type-fest';
12
- import {type CsrfTokenStore} from '../csrf-token-store.js';
13
12
  import {
14
13
  type CsrfHeaderNameOption,
15
- extractCsrfTokenHeader,
16
14
  getCurrentCsrfToken,
17
15
  resolveCsrfHeaderName,
18
- storeCsrfToken,
19
16
  } from '../csrf-token.js';
20
17
  import {AuthHeaderName} from '../headers.js';
21
18
 
@@ -75,8 +72,7 @@ export type FrontendAuthClientConfig = Readonly<{
75
72
  assumedUserHeaderName: string;
76
73
 
77
74
  overrides: PartialWithUndefined<{
78
- localStorage: Pick<Storage, 'setItem' | 'removeItem' | 'getItem'>;
79
- csrfTokenStore: CsrfTokenStore;
75
+ localStorage: SelectFrom<Storage, {setItem: true; removeItem: true; getItem: true}>;
80
76
  }>;
81
77
  }>;
82
78
 
@@ -121,14 +117,6 @@ export class FrontendAuthClient<AssumedUserParams extends JsonCompatibleObject =
121
117
  this.removeActivityListener?.();
122
118
  }
123
119
 
124
- /** Wraps {@link getCurrentCsrfToken} to retrieve the stored CSRF token string. */
125
- public async getCurrentCsrfToken(): Promise<string | undefined> {
126
- return await getCurrentCsrfToken({
127
- ...this.config.csrf,
128
- csrfTokenStore: this.config.overrides?.csrfTokenStore,
129
- });
130
- }
131
-
132
120
  /**
133
121
  * Assume the given user. Pass `undefined` to wipe the currently assumed user.
134
122
  *
@@ -174,8 +162,8 @@ export class FrontendAuthClient<AssumedUserParams extends JsonCompatibleObject =
174
162
  * `@augment-vir/common`](https://electrovir.github.io/augment-vir/functions/mergeDeep.html) to
175
163
  * combine them with these.
176
164
  */
177
- public async createAuthenticatedRequestInit(): Promise<RequestInit> {
178
- const csrfToken = await this.getCurrentCsrfToken();
165
+ public createAuthenticatedRequestInit(): RequestInit {
166
+ const csrfToken = getCurrentCsrfToken();
179
167
 
180
168
  const assumedUser = this.getAssumedUser();
181
169
  const headers: HeadersInit = {
@@ -204,38 +192,18 @@ export class FrontendAuthClient<AssumedUserParams extends JsonCompatibleObject =
204
192
  }
205
193
 
206
194
  /**
207
- * Use to handle a login response. Automatically stores the CSRF token.
195
+ * Use to handle a login response. The CSRF token cookie is automatically stored by the browser
196
+ * from the `Set-Cookie` response header.
208
197
  *
209
198
  * @throws Error if the login response failed.
210
- * @throws Error if the login response has an invalid CSRF token.
211
199
  */
212
200
  public async handleLoginResponse(
213
- response: Readonly<
214
- SelectFrom<
215
- Response,
216
- {
217
- headers: true;
218
- ok: true;
219
- }
220
- >
221
- >,
201
+ response: Readonly<SelectFrom<Response, {ok: true}>>,
222
202
  ): Promise<void> {
223
203
  if (!response.ok) {
224
204
  await this.logout();
225
205
  throw new Error('Login response failed.');
226
206
  }
227
-
228
- const csrfToken = extractCsrfTokenHeader(response, this.config.csrf);
229
-
230
- if (!csrfToken) {
231
- await this.logout();
232
- throw new Error('Did not receive any CSRF token.');
233
- }
234
-
235
- await storeCsrfToken(csrfToken, {
236
- ...this.config.csrf,
237
- csrfTokenStore: this.config.overrides?.csrfTokenStore,
238
- });
239
207
  }
240
208
 
241
209
  /**