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/README.md CHANGED
@@ -73,6 +73,7 @@ Here's a full example of how to use all host / server / backend side auth functi
73
73
  ```TypeScript
74
74
  import {type ClientRequest, type ServerResponse} from 'node:http';
75
75
  import {
76
+ AuthCookie,
76
77
  doesPasswordMatchHash,
77
78
  extractUserIdFromRequestHeaders,
78
79
  generateNewJwtKeys,
@@ -114,8 +115,15 @@ export async function handleLogin(
114
115
  throw new Error('Credentials mismatch.');
115
116
  }
116
117
 
117
- const authHeaders = await generateSuccessfulLoginHeaders(user.id, cookieParams, csrfOption);
118
- response.setHeaders(new Headers(authHeaders));
118
+ const authHeaders = await generateSuccessfulLoginHeaders(user.id, cookieParams);
119
+ Object.entries(authHeaders).forEach(
120
+ ([
121
+ key,
122
+ value,
123
+ ]) => {
124
+ response.setHeader(key, value);
125
+ },
126
+ );
119
127
  }
120
128
 
121
129
  /**
@@ -130,8 +138,15 @@ export async function createUser(
130
138
  ) {
131
139
  const newUser = await createUserInDatabase(userRequestData);
132
140
 
133
- const authHeaders = await generateSuccessfulLoginHeaders(newUser.id, cookieParams, csrfOption);
134
- response.setHeaders(new Headers(authHeaders));
141
+ const authHeaders = await generateSuccessfulLoginHeaders(newUser.id, cookieParams);
142
+ Object.entries(authHeaders).forEach(
143
+ ([
144
+ key,
145
+ value,
146
+ ]) => {
147
+ response.setHeader(key, value);
148
+ },
149
+ );
135
150
  }
136
151
 
137
152
  /**
@@ -141,7 +156,12 @@ export async function createUser(
141
156
  */
142
157
  export async function getAuthenticatedUser(request: ClientRequest) {
143
158
  const userId = (
144
- await extractUserIdFromRequestHeaders<MyUserId>(request.getHeaders(), jwtParams, csrfOption)
159
+ await extractUserIdFromRequestHeaders<MyUserId>(
160
+ request.getHeaders(),
161
+ jwtParams,
162
+ csrfOption,
163
+ AuthCookie.Auth,
164
+ )
145
165
  )?.userId;
146
166
  const user = userId ? findUserInDatabaseById(userId) : undefined;
147
167
 
@@ -260,13 +280,7 @@ Here's a full example of how to use all the client / frontend side auth function
260
280
 
261
281
  ```TypeScript
262
282
  import {HttpStatus} from '@augment-vir/common';
263
- import {
264
- type CsrfHeaderNameOption,
265
- getCurrentCsrfToken,
266
- handleAuthResponse,
267
- resolveCsrfHeaderName,
268
- wipeCurrentCsrfToken,
269
- } from 'auth-vir';
283
+ import {type CsrfHeaderNameOption, getCurrentCsrfToken, resolveCsrfHeaderName} from 'auth-vir';
270
284
 
271
285
  /**
272
286
  * The CSRF header prefix for this app. Either `csrfHeaderPrefix` or `csrfHeaderName` must be
@@ -281,7 +295,7 @@ export async function sendLoginRequest(
281
295
  userLoginData: {username: string; password: string},
282
296
  loginUrl: string,
283
297
  ) {
284
- if (await getCurrentCsrfToken(csrfOption)) {
298
+ if (getCurrentCsrfToken()) {
285
299
  throw new Error('Already logged in.');
286
300
  }
287
301
 
@@ -291,7 +305,7 @@ export async function sendLoginRequest(
291
305
  credentials: 'include',
292
306
  });
293
307
 
294
- await handleAuthResponse(response, csrfOption);
308
+ /** The CSRF token cookie is automatically stored by the browser from the Set-Cookie header. */
295
309
 
296
310
  return response;
297
311
  }
@@ -302,7 +316,7 @@ export async function sendAuthenticatedRequest(
302
316
  requestInit: Omit<RequestInit, 'headers'> = {},
303
317
  headers: Record<string, string> = {},
304
318
  ) {
305
- const csrfToken = await getCurrentCsrfToken(csrfOption);
319
+ const csrfToken = getCurrentCsrfToken();
306
320
 
307
321
  if (!csrfToken) {
308
322
  throw new Error('Not authenticated.');
@@ -317,22 +331,21 @@ export async function sendAuthenticatedRequest(
317
331
  },
318
332
  });
319
333
 
320
- /**
321
- * This indicates the user is no longer authorized and thus needs to login again. (This likely
322
- * means that their session timed out or they clicked a "log out" button onr your website in
323
- * another tab.)
324
- */
325
334
  if (response.status === HttpStatus.Unauthorized) {
326
- await wipeCurrentCsrfToken(csrfOption);
327
335
  throw new Error(`User no longer logged in.`);
328
336
  } else {
329
337
  return response;
330
338
  }
331
339
  }
332
340
 
333
- /** Call this when the user explicitly clicks a "log out" button. */
334
- export async function logout() {
335
- await wipeCurrentCsrfToken(csrfOption);
341
+ /**
342
+ * Call this when the user explicitly clicks a "log out" button. The backend clears the auth and
343
+ * CSRF cookies via Set-Cookie headers.
344
+ */
345
+ export async function logout(logoutUrl: string) {
346
+ await sendAuthenticatedRequest(logoutUrl, {
347
+ method: 'post',
348
+ });
336
349
  }
337
350
  ```
338
351
 
@@ -217,7 +217,7 @@ export declare class BackendAuthClient<DatabaseUser extends AnyObject, UserId ex
217
217
  userId: UserId;
218
218
  cookieParams: Readonly<CookieParams>;
219
219
  existingUserIdResult: Readonly<UserIdResult<UserId>>;
220
- }): Promise<Record<string, string>>;
220
+ }): Promise<Record<string, string | string[]>>;
221
221
  /** Use these headers to log a user in. */
222
222
  createLoginHeaders({ userId, requestHeaders, isSignUpCookie, }: {
223
223
  userId: UserId;
@@ -1,10 +1,10 @@
1
1
  import { ensureArray, } from '@augment-vir/common';
2
2
  import { calculateRelativeDate, createUtcFullDate, getNowInUtcTimezone, isDateAfter, } from 'date-vir';
3
3
  import { extractUserIdFromRequestHeaders, generateLogoutHeaders, generateSuccessfulLoginHeaders, insecureExtractUserIdFromCookieAlone, } from '../auth.js';
4
- import { AuthCookieName, generateAuthCookie } from '../cookie.js';
5
- import { defaultAllowedClockSkew, resolveCsrfHeaderName, } from '../csrf-token.js';
4
+ import { AuthCookie, generateAuthCookie, generateCsrfCookie } from '../cookie.js';
6
5
  import { AuthHeaderName, mergeHeaderValues } from '../headers.js';
7
6
  import { parseJwtKeys } from '../jwt/jwt-keys.js';
7
+ import { defaultAllowedClockSkew } from '../jwt/jwt.js';
8
8
  import { isSessionRefreshReady } from './is-session-refresh-ready.js';
9
9
  const defaultSessionIdleTimeout = {
10
10
  minutes: 20,
@@ -55,7 +55,7 @@ export class BackendAuthClient {
55
55
  hostOrigin: serviceOrigin || this.config.serviceOrigin,
56
56
  jwtParams: await this.getJwtParams(),
57
57
  isDev: this.config.isDev,
58
- cookieName: isSignUpCookie ? AuthCookieName.SignUp : AuthCookieName.Auth,
58
+ authCookie: isSignUpCookie ? AuthCookie.SignUp : AuthCookie.Auth,
59
59
  };
60
60
  }
61
61
  /** Calls the provided `getUserFromDatabase` config. */
@@ -133,20 +133,22 @@ export class BackendAuthClient {
133
133
  sessionRefreshStartTime,
134
134
  });
135
135
  if (isRefreshReady) {
136
- const isSignUpCookie = userIdResult.cookieName === AuthCookieName.SignUp;
136
+ const isSignUpCookie = userIdResult.cookieName === AuthCookie.SignUp;
137
137
  const cookieParams = await this.getCookieParams({
138
138
  isSignUpCookie,
139
139
  requestHeaders,
140
140
  });
141
- const csrfHeaderName = resolveCsrfHeaderName(this.config.csrf);
142
- const { cookie } = await generateAuthCookie({
141
+ const authCookie = await generateAuthCookie({
143
142
  csrfToken: userIdResult.csrfToken,
144
143
  userId: userIdResult.userId,
145
144
  sessionStartedAt: userIdResult.sessionStartedAt || Date.now(),
146
145
  }, cookieParams);
146
+ const csrfCookie = generateCsrfCookie(userIdResult.csrfToken, cookieParams);
147
147
  return {
148
- 'set-cookie': cookie,
149
- [csrfHeaderName]: userIdResult.csrfToken,
148
+ 'set-cookie': [
149
+ authCookie,
150
+ csrfCookie,
151
+ ],
150
152
  };
151
153
  }
152
154
  else {
@@ -184,7 +186,7 @@ export class BackendAuthClient {
184
186
  }
185
187
  /** Securely extract a user from their request headers. */
186
188
  async getSecureUser({ requestHeaders, isSignUpCookie, allowUserAuthRefresh, }) {
187
- const userIdResult = await extractUserIdFromRequestHeaders(requestHeaders, await this.getJwtParams(), this.config.csrf, isSignUpCookie ? AuthCookieName.SignUp : AuthCookieName.Auth);
189
+ const userIdResult = await extractUserIdFromRequestHeaders(requestHeaders, await this.getJwtParams(), this.config.csrf, isSignUpCookie ? AuthCookie.SignUp : AuthCookie.Auth);
188
190
  if (!userIdResult) {
189
191
  this.logForUser({
190
192
  user: undefined,
@@ -215,14 +217,28 @@ export class BackendAuthClient {
215
217
  requestHeaders,
216
218
  user,
217
219
  });
218
- const cookieRefreshHeaders = (await this.createCookieRefreshHeaders({
219
- userIdResult,
220
- requestHeaders,
221
- })) || {};
220
+ const cookieRefreshHeaders = allowUserAuthRefresh
221
+ ? await this.createCookieRefreshHeaders({
222
+ userIdResult,
223
+ requestHeaders,
224
+ })
225
+ : undefined;
226
+ /**
227
+ * Always include the CSRF cookie so it gets re-established if the browser clears it. When
228
+ * session refresh fires, its headers already include a CSRF cookie.
229
+ */
230
+ const csrfCookie = generateCsrfCookie(userIdResult.csrfToken, {
231
+ hostOrigin: (await this.config.generateServiceOrigin?.({
232
+ requestHeaders,
233
+ })) || this.config.serviceOrigin,
234
+ isDev: this.config.isDev,
235
+ });
222
236
  return {
223
237
  user: assumedUser || user,
224
238
  isAssumed: !!assumedUser,
225
- responseHeaders: allowUserAuthRefresh ? cookieRefreshHeaders : {},
239
+ responseHeaders: {
240
+ 'set-cookie': mergeHeaderValues(cookieRefreshHeaders?.['set-cookie'], csrfCookie),
241
+ },
226
242
  };
227
243
  }
228
244
  /**
@@ -270,18 +286,22 @@ export class BackendAuthClient {
270
286
  * generating a new one.
271
287
  */
272
288
  async refreshLoginHeaders({ userId, cookieParams, existingUserIdResult, }) {
273
- const { cookie } = await generateAuthCookie({
289
+ const authCookie = await generateAuthCookie({
274
290
  csrfToken: existingUserIdResult.csrfToken,
275
291
  userId,
276
292
  sessionStartedAt: existingUserIdResult.sessionStartedAt,
277
293
  }, cookieParams);
294
+ const csrfCookie = generateCsrfCookie(existingUserIdResult.csrfToken, cookieParams);
278
295
  return {
279
- 'set-cookie': cookie,
296
+ 'set-cookie': [
297
+ authCookie,
298
+ csrfCookie,
299
+ ],
280
300
  };
281
301
  }
282
302
  /** Use these headers to log a user in. */
283
303
  async createLoginHeaders({ userId, requestHeaders, isSignUpCookie, }) {
284
- const oppositeCookieName = isSignUpCookie ? AuthCookieName.Auth : AuthCookieName.SignUp;
304
+ const oppositeCookieName = isSignUpCookie ? AuthCookie.Auth : AuthCookie.SignUp;
285
305
  const hasExistingOppositeCookie = requestHeaders.cookie?.includes(`${oppositeCookieName}=`);
286
306
  const discardOppositeCookieHeaders = hasExistingOppositeCookie
287
307
  ? generateLogoutHeaders(await this.getCookieParams({
@@ -289,7 +309,7 @@ export class BackendAuthClient {
289
309
  requestHeaders,
290
310
  }))
291
311
  : undefined;
292
- const existingUserIdResult = await extractUserIdFromRequestHeaders(requestHeaders, await this.getJwtParams(), this.config.csrf, isSignUpCookie ? AuthCookieName.SignUp : AuthCookieName.Auth);
312
+ const existingUserIdResult = await extractUserIdFromRequestHeaders(requestHeaders, await this.getJwtParams(), this.config.csrf, isSignUpCookie ? AuthCookie.SignUp : AuthCookie.Auth);
293
313
  const cookieParams = await this.getCookieParams({
294
314
  isSignUpCookie,
295
315
  requestHeaders,
@@ -300,7 +320,7 @@ export class BackendAuthClient {
300
320
  cookieParams,
301
321
  existingUserIdResult,
302
322
  })
303
- : await generateSuccessfulLoginHeaders(userId, cookieParams, this.config.csrf);
323
+ : await generateSuccessfulLoginHeaders(userId, cookieParams);
304
324
  return {
305
325
  ...newCookieHeaders,
306
326
  'set-cookie': mergeHeaderValues(newCookieHeaders['set-cookie'], discardOppositeCookieHeaders?.['set-cookie']),
@@ -334,7 +354,7 @@ export class BackendAuthClient {
334
354
  */
335
355
  async getInsecureUser({ requestHeaders, allowUserAuthRefresh, }) {
336
356
  // eslint-disable-next-line @typescript-eslint/no-deprecated
337
- const userIdResult = await insecureExtractUserIdFromCookieAlone(requestHeaders, await this.getJwtParams(), AuthCookieName.Auth);
357
+ const userIdResult = await insecureExtractUserIdFromCookieAlone(requestHeaders, await this.getJwtParams(), AuthCookie.Auth);
338
358
  if (!userIdResult) {
339
359
  this.logForUser({
340
360
  user: undefined,
@@ -1,7 +1,6 @@
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 CsrfTokenStore } from '../csrf-token-store.js';
5
4
  import { type CsrfHeaderNameOption } from '../csrf-token.js';
6
5
  /**
7
6
  * Config for {@link FrontendAuthClient}.
@@ -50,8 +49,11 @@ export type FrontendAuthClientConfig = Readonly<{
50
49
  */
51
50
  assumedUserHeaderName: string;
52
51
  overrides: PartialWithUndefined<{
53
- localStorage: Pick<Storage, 'setItem' | 'removeItem' | 'getItem'>;
54
- csrfTokenStore: CsrfTokenStore;
52
+ localStorage: SelectFrom<Storage, {
53
+ setItem: true;
54
+ removeItem: true;
55
+ getItem: true;
56
+ }>;
55
57
  }>;
56
58
  }>;
57
59
  /**
@@ -72,8 +74,6 @@ export declare class FrontendAuthClient<AssumedUserParams extends JsonCompatible
72
74
  * interval).
73
75
  */
74
76
  destroy(): void;
75
- /** Wraps {@link getCurrentCsrfToken} to retrieve the stored CSRF token string. */
76
- getCurrentCsrfToken(): Promise<string | undefined>;
77
77
  /**
78
78
  * Assume the given user. Pass `undefined` to wipe the currently assumed user.
79
79
  *
@@ -88,17 +88,16 @@ export declare class FrontendAuthClient<AssumedUserParams extends JsonCompatible
88
88
  * `@augment-vir/common`](https://electrovir.github.io/augment-vir/functions/mergeDeep.html) to
89
89
  * combine them with these.
90
90
  */
91
- createAuthenticatedRequestInit(): Promise<RequestInit>;
91
+ createAuthenticatedRequestInit(): RequestInit;
92
92
  /** Wipes the current user auth. */
93
93
  logout(): Promise<void>;
94
94
  /**
95
- * Use to handle a login response. Automatically stores the CSRF token.
95
+ * Use to handle a login response. The CSRF token cookie is automatically stored by the browser
96
+ * from the `Set-Cookie` response header.
96
97
  *
97
98
  * @throws Error if the login response failed.
98
- * @throws Error if the login response has an invalid CSRF token.
99
99
  */
100
100
  handleLoginResponse(response: Readonly<SelectFrom<Response, {
101
- headers: true;
102
101
  ok: true;
103
102
  }>>): Promise<void>;
104
103
  /**
@@ -1,6 +1,6 @@
1
1
  import { HttpStatus, } from '@augment-vir/common';
2
2
  import { listenToActivity } from 'detect-activity';
3
- import { extractCsrfTokenHeader, getCurrentCsrfToken, resolveCsrfHeaderName, storeCsrfToken, } from '../csrf-token.js';
3
+ import { getCurrentCsrfToken, resolveCsrfHeaderName, } from '../csrf-token.js';
4
4
  import { AuthHeaderName } from '../headers.js';
5
5
  /**
6
6
  * An auth client for sending and validating client requests to a backend. This should only be used
@@ -41,13 +41,6 @@ export class FrontendAuthClient {
41
41
  this.userCheckInterval?.clearInterval();
42
42
  this.removeActivityListener?.();
43
43
  }
44
- /** Wraps {@link getCurrentCsrfToken} to retrieve the stored CSRF token string. */
45
- async getCurrentCsrfToken() {
46
- return await getCurrentCsrfToken({
47
- ...this.config.csrf,
48
- csrfTokenStore: this.config.overrides?.csrfTokenStore,
49
- });
50
- }
51
44
  /**
52
45
  * Assume the given user. Pass `undefined` to wipe the currently assumed user.
53
46
  *
@@ -85,8 +78,8 @@ export class FrontendAuthClient {
85
78
  * `@augment-vir/common`](https://electrovir.github.io/augment-vir/functions/mergeDeep.html) to
86
79
  * combine them with these.
87
80
  */
88
- async createAuthenticatedRequestInit() {
89
- const csrfToken = await this.getCurrentCsrfToken();
81
+ createAuthenticatedRequestInit() {
82
+ const csrfToken = getCurrentCsrfToken();
90
83
  const assumedUser = this.getAssumedUser();
91
84
  const headers = {
92
85
  ...(csrfToken
@@ -110,25 +103,16 @@ export class FrontendAuthClient {
110
103
  await this.config.authClearedCallback?.();
111
104
  }
112
105
  /**
113
- * Use to handle a login response. Automatically stores the CSRF token.
106
+ * Use to handle a login response. The CSRF token cookie is automatically stored by the browser
107
+ * from the `Set-Cookie` response header.
114
108
  *
115
109
  * @throws Error if the login response failed.
116
- * @throws Error if the login response has an invalid CSRF token.
117
110
  */
118
111
  async handleLoginResponse(response) {
119
112
  if (!response.ok) {
120
113
  await this.logout();
121
114
  throw new Error('Login response failed.');
122
115
  }
123
- const csrfToken = extractCsrfTokenHeader(response, this.config.csrf);
124
- if (!csrfToken) {
125
- await this.logout();
126
- throw new Error('Did not receive any CSRF token.');
127
- }
128
- await storeCsrfToken(csrfToken, {
129
- ...this.config.csrf,
130
- csrfTokenStore: this.config.overrides?.csrfTokenStore,
131
- });
132
116
  }
133
117
  /**
134
118
  * Use to verify _all_ responses received from the backend. Immediately logs the user out once
package/dist/auth.d.ts CHANGED
@@ -1,7 +1,6 @@
1
- import { type PartialWithUndefined } from '@augment-vir/common';
1
+ import { type SelectFrom } from '@augment-vir/common';
2
2
  import { type FullDate, type UtcTimezone } from 'date-vir';
3
- import { type CookieParams } from './cookie.js';
4
- import { type CsrfTokenStore } from './csrf-token-store.js';
3
+ import { AuthCookie, type CookieParams } from './cookie.js';
5
4
  import { type CsrfHeaderNameOption } from './csrf-token.js';
6
5
  import { type ParseJwtParams } from './jwt/jwt.js';
7
6
  import { type JwtUserData } from './jwt/user-jwt.js';
@@ -21,7 +20,7 @@ export type UserIdResult<UserId extends string | number> = {
21
20
  jwtExpiration: FullDate<UtcTimezone>;
22
21
  /** When the JWT was issued (`iat` claim). */
23
22
  jwtIssuedAt: FullDate<UtcTimezone>;
24
- cookieName: string;
23
+ cookieName: AuthCookie;
25
24
  /** The CSRF token embedded in the JWT. */
26
25
  csrfToken: string;
27
26
  /**
@@ -38,7 +37,7 @@ export type UserIdResult<UserId extends string | number> = {
38
37
  * @category Auth : Host
39
38
  * @returns The extracted user id or `undefined` if no valid auth headers exist.
40
39
  */
41
- export declare function extractUserIdFromRequestHeaders<UserId extends string | number>(headers: HeaderContainer, jwtParams: Readonly<ParseJwtParams>, csrfHeaderNameOption: Readonly<CsrfHeaderNameOption>, cookieName?: string): Promise<Readonly<UserIdResult<UserId>> | undefined>;
40
+ export declare function extractUserIdFromRequestHeaders<UserId extends string | number>(headers: HeaderContainer, jwtParams: Readonly<ParseJwtParams>, csrfHeaderNameOption: Readonly<CsrfHeaderNameOption>, cookieName?: AuthCookie): Promise<Readonly<UserIdResult<UserId>> | undefined>;
42
41
  /**
43
42
  * Extract a user id from just the cookie, without CSRF token validation. This is _less secure_ than
44
43
  * {@link extractUserIdFromRequestHeaders} as a result. This should only be used in rare
@@ -47,41 +46,29 @@ export declare function extractUserIdFromRequestHeaders<UserId extends string |
47
46
  * @deprecated Prefer {@link extractUserIdFromRequestHeaders} instead: it is more secure.
48
47
  * @category Auth : Host
49
48
  */
50
- export declare function insecureExtractUserIdFromCookieAlone<UserId extends string | number>(headers: HeaderContainer, jwtParams: Readonly<ParseJwtParams>, cookieName?: string): Promise<Readonly<UserIdResult<UserId>> | undefined>;
49
+ export declare function insecureExtractUserIdFromCookieAlone<UserId extends string | number>(headers: HeaderContainer, jwtParams: Readonly<ParseJwtParams>, cookieName: AuthCookie): Promise<Readonly<UserIdResult<UserId>> | undefined>;
51
50
  /**
52
- * Used by host (backend) code to set headers on a response object.
51
+ * Used by host (backend) code to set headers on a response object. Sets both the auth JWT cookie
52
+ * and the CSRF token cookie. The CSRF cookie is not `HttpOnly` so that frontend JavaScript can read
53
+ * it and inject the value as a request header.
53
54
  *
54
55
  * @category Auth : Host
55
56
  */
56
57
  export declare function generateSuccessfulLoginHeaders(
57
58
  /** The id from your database of the user you're authenticating. */
58
- userId: string | number, cookieConfig: Readonly<CookieParams>, csrfHeaderNameOption: Readonly<CsrfHeaderNameOption>,
59
+ userId: string | number, cookieConfig: Readonly<CookieParams>,
59
60
  /**
60
61
  * The timestamp (in seconds) when the session originally started. If not provided, the current
61
62
  * time will be used (for new sessions).
62
63
  */
63
- sessionStartedAt?: number | undefined): Promise<Record<string, string>>;
64
+ sessionStartedAt?: number | undefined): Promise<Record<string, string[]>>;
64
65
  /**
65
66
  * Used by host (backend) code to set headers on a response object when the user has logged out or
66
67
  * failed to authorize.
67
68
  *
68
69
  * @category Auth : Host
69
70
  */
70
- export declare function generateLogoutHeaders(cookieConfig: Readonly<Pick<CookieParams, 'cookieName' | 'hostOrigin' | 'isDev'>>): Record<string, string>;
71
- /**
72
- * Store auth data on a client (frontend) after receiving an auth response from the host (backend).
73
- * Specifically, this stores the CSRF token into IndexedDB (which doesn't need to be a secret).
74
- * Alternatively, if the given response failed, this will wipe the existing (if any) stored CSRF
75
- * token.
76
- *
77
- * @category Auth : Client
78
- * @throws Error if no CSRF token header is found.
79
- */
80
- export declare function handleAuthResponse(response: Readonly<Pick<Response, 'ok' | 'headers'>>, options: Readonly<CsrfHeaderNameOption> & PartialWithUndefined<{
81
- /**
82
- * Allows mocking or overriding the default CSRF token store.
83
- *
84
- * @default getDefaultCsrfTokenStore()
85
- */
86
- csrfTokenStore: CsrfTokenStore;
87
- }>): Promise<void>;
71
+ export declare function generateLogoutHeaders(cookieConfig: Readonly<SelectFrom<CookieParams, {
72
+ hostOrigin: true;
73
+ isDev: true;
74
+ }>>): Record<string, string[]>;
package/dist/auth.js CHANGED
@@ -1,5 +1,5 @@
1
- import { AuthCookieName, clearAuthCookie, extractCookieJwt, generateAuthCookie, } from './cookie.js';
2
- import { extractCsrfTokenHeader, generateCsrfToken, resolveCsrfHeaderName, storeCsrfToken, } from './csrf-token.js';
1
+ import { AuthCookie, clearAuthCookie, clearCsrfCookie, extractCookieJwt, generateAuthCookie, generateCsrfCookie, } from './cookie.js';
2
+ import { generateCsrfToken, resolveCsrfHeaderName } from './csrf-token.js';
3
3
  function readHeader(headers, headerName) {
4
4
  if (headers instanceof Headers) {
5
5
  return headers.get(headerName) || undefined;
@@ -28,7 +28,7 @@ function readCsrfTokenHeader(headers, csrfHeaderNameOption) {
28
28
  * @category Auth : Host
29
29
  * @returns The extracted user id or `undefined` if no valid auth headers exist.
30
30
  */
31
- export async function extractUserIdFromRequestHeaders(headers, jwtParams, csrfHeaderNameOption, cookieName = AuthCookieName.Auth) {
31
+ export async function extractUserIdFromRequestHeaders(headers, jwtParams, csrfHeaderNameOption, cookieName = AuthCookie.Auth) {
32
32
  try {
33
33
  const csrfToken = readCsrfTokenHeader(headers, csrfHeaderNameOption);
34
34
  const cookie = readHeader(headers, 'cookie');
@@ -60,7 +60,7 @@ export async function extractUserIdFromRequestHeaders(headers, jwtParams, csrfHe
60
60
  * @deprecated Prefer {@link extractUserIdFromRequestHeaders} instead: it is more secure.
61
61
  * @category Auth : Host
62
62
  */
63
- export async function insecureExtractUserIdFromCookieAlone(headers, jwtParams, cookieName = AuthCookieName.Auth) {
63
+ export async function insecureExtractUserIdFromCookieAlone(headers, jwtParams, cookieName) {
64
64
  try {
65
65
  const cookie = readHeader(headers, 'cookie');
66
66
  if (!cookie) {
@@ -84,28 +84,32 @@ export async function insecureExtractUserIdFromCookieAlone(headers, jwtParams, c
84
84
  }
85
85
  }
86
86
  /**
87
- * Used by host (backend) code to set headers on a response object.
87
+ * Used by host (backend) code to set headers on a response object. Sets both the auth JWT cookie
88
+ * and the CSRF token cookie. The CSRF cookie is not `HttpOnly` so that frontend JavaScript can read
89
+ * it and inject the value as a request header.
88
90
  *
89
91
  * @category Auth : Host
90
92
  */
91
93
  export async function generateSuccessfulLoginHeaders(
92
94
  /** The id from your database of the user you're authenticating. */
93
- userId, cookieConfig, csrfHeaderNameOption,
95
+ userId, cookieConfig,
94
96
  /**
95
97
  * The timestamp (in seconds) when the session originally started. If not provided, the current
96
98
  * time will be used (for new sessions).
97
99
  */
98
100
  sessionStartedAt) {
99
101
  const csrfToken = generateCsrfToken();
100
- const csrfHeaderName = resolveCsrfHeaderName(csrfHeaderNameOption);
101
- const { cookie } = await generateAuthCookie({
102
+ const authCookie = await generateAuthCookie({
102
103
  csrfToken,
103
104
  userId,
104
105
  sessionStartedAt: sessionStartedAt ?? Date.now(),
105
106
  }, cookieConfig);
107
+ const csrfCookie = generateCsrfCookie(csrfToken, cookieConfig);
106
108
  return {
107
- 'set-cookie': cookie,
108
- [csrfHeaderName]: csrfToken,
109
+ 'set-cookie': [
110
+ authCookie,
111
+ csrfCookie,
112
+ ],
109
113
  };
110
114
  }
111
115
  /**
@@ -116,25 +120,9 @@ sessionStartedAt) {
116
120
  */
117
121
  export function generateLogoutHeaders(cookieConfig) {
118
122
  return {
119
- 'set-cookie': clearAuthCookie(cookieConfig),
123
+ 'set-cookie': [
124
+ clearAuthCookie(cookieConfig),
125
+ clearCsrfCookie(cookieConfig),
126
+ ],
120
127
  };
121
128
  }
122
- /**
123
- * Store auth data on a client (frontend) after receiving an auth response from the host (backend).
124
- * Specifically, this stores the CSRF token into IndexedDB (which doesn't need to be a secret).
125
- * Alternatively, if the given response failed, this will wipe the existing (if any) stored CSRF
126
- * token.
127
- *
128
- * @category Auth : Client
129
- * @throws Error if no CSRF token header is found.
130
- */
131
- export async function handleAuthResponse(response, options) {
132
- if (!response.ok) {
133
- return;
134
- }
135
- const csrfToken = extractCsrfTokenHeader(response, options);
136
- if (!csrfToken) {
137
- throw new Error('Did not receive any CSRF token.');
138
- }
139
- await storeCsrfToken(csrfToken, options);
140
- }
package/dist/cookie.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { type PartialWithUndefined } from '@augment-vir/common';
2
- import { type AnyDuration, type FullDate, type UtcTimezone } from 'date-vir';
1
+ import { type PartialWithUndefined, type SelectFrom } from '@augment-vir/common';
2
+ import { type AnyDuration } from 'date-vir';
3
3
  import { type Primitive } from 'type-fest';
4
4
  import { type CreateJwtParams, type ParseJwtParams, type ParsedJwt } from './jwt/jwt.js';
5
5
  import { type JwtUserData } from './jwt/user-jwt.js';
@@ -8,11 +8,13 @@ import { type JwtUserData } from './jwt/user-jwt.js';
8
8
  *
9
9
  * @category Internal
10
10
  */
11
- export declare enum AuthCookieName {
11
+ export declare enum AuthCookie {
12
12
  /** Used for a full user login auth. */
13
13
  Auth = "auth",
14
14
  /** Use for a temporary "just signed up" auth. */
15
- SignUp = "sign-up"
15
+ SignUp = "sign-up",
16
+ /** Used for storing the CSRF token. Not `HttpOnly` so that frontend JS can read it. */
17
+ Csrf = "auth-vir-csrf"
16
18
  }
17
19
  /**
18
20
  * Parameters for {@link generateAuthCookie}.
@@ -38,8 +40,13 @@ export type CookieParams = {
38
40
  * client, etc.
39
41
  */
40
42
  jwtParams: Readonly<CreateJwtParams>;
41
- cookieName?: string;
42
43
  } & PartialWithUndefined<{
44
+ /**
45
+ * Which auth cookie name to use.
46
+ *
47
+ * @default AuthCookie.Auth
48
+ */
49
+ authCookie: AuthCookie;
43
50
  /**
44
51
  * Is set to `true` (which should only be done in development environments), the cookie will be
45
52
  * allowed in insecure requests (non HTTPS requests).
@@ -49,26 +56,46 @@ export type CookieParams = {
49
56
  isDev: boolean;
50
57
  }>;
51
58
  /**
52
- * Output from {@link generateAuthCookie}.
59
+ * Generate a secure cookie that stores the user JWT data. Used in host (backend) code.
53
60
  *
54
61
  * @category Internal
55
62
  */
56
- export type GenerateAuthCookieResult = {
57
- cookie: string;
58
- expiration: FullDate<UtcTimezone>;
59
- };
63
+ export declare function generateAuthCookie(userJwtData: Readonly<JwtUserData>, cookieConfig: Readonly<CookieParams>): Promise<string>;
60
64
  /**
61
- * Generate a secure cookie that stores the user JWT data. Used in host (backend) code.
65
+ * Generate a CSRF token cookie. This cookie is intentionally not `HttpOnly` so that frontend
66
+ * JavaScript can read it and inject the value as a request header for double-submit verification.
67
+ *
68
+ * The CSRF cookie uses a fixed 400-day MAX-AGE rather than matching the auth cookie duration. 400
69
+ * days is the cross-browser safe maximum (Chrome caps cookie lifetimes at 400 days; other browsers
70
+ * accept it as-is). The CSRF token is only meaningful when paired with a valid JWT, so it doesn't
71
+ * need its own expiration management. It gets regenerated on every fresh login.
62
72
  *
63
73
  * @category Internal
64
74
  */
65
- export declare function generateAuthCookie(userJwtData: Readonly<JwtUserData>, cookieConfig: Readonly<CookieParams>): Promise<GenerateAuthCookieResult>;
75
+ export declare function generateCsrfCookie(csrfToken: string, cookieConfig: Readonly<SelectFrom<CookieParams, {
76
+ hostOrigin: true;
77
+ isDev: true;
78
+ }>>): string;
66
79
  /**
67
80
  * Generate a cookie value that will clear the previous auth cookie. Use this when signing out.
68
81
  *
69
82
  * @category Internal
70
83
  */
71
- export declare function clearAuthCookie(cookieConfig: Readonly<Pick<CookieParams, 'cookieName' | 'hostOrigin' | 'isDev'>>): string;
84
+ export declare function clearAuthCookie(cookieConfig: Readonly<SelectFrom<CookieParams, {
85
+ hostOrigin: true;
86
+ isDev: true;
87
+ }>> & PartialWithUndefined<{
88
+ authCookie: AuthCookie;
89
+ }>): string;
90
+ /**
91
+ * Generate a cookie value that will clear the CSRF token cookie. Use this when signing out.
92
+ *
93
+ * @category Internal
94
+ */
95
+ export declare function clearCsrfCookie(cookieConfig: Readonly<SelectFrom<CookieParams, {
96
+ hostOrigin: true;
97
+ isDev: true;
98
+ }>>): string;
72
99
  /**
73
100
  * Generate a cookie string from a raw set of parameters.
74
101
  *
@@ -81,4 +108,4 @@ export declare function generateCookie(params: Readonly<Record<string, Exclude<P
81
108
  * @category Internal
82
109
  * @returns The extracted auth Cookie JWT data or `undefined` if no valid auth JWT data was found.
83
110
  */
84
- export declare function extractCookieJwt(rawCookie: string, jwtParams: Readonly<ParseJwtParams>, cookieName?: string): Promise<undefined | ParsedJwt<JwtUserData>>;
111
+ export declare function extractCookieJwt(rawCookie: string, jwtParams: Readonly<ParseJwtParams>, cookieName: AuthCookie): Promise<undefined | ParsedJwt<JwtUserData>>;