auth-vir 3.1.2 → 5.0.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.
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)).csrfToken) {
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.');
@@ -313,26 +327,25 @@ export async function sendAuthenticatedRequest(
313
327
  credentials: 'include',
314
328
  headers: {
315
329
  ...headers,
316
- [resolveCsrfHeaderName(csrfOption)]: csrfToken.token,
330
+ [resolveCsrfHeaderName(csrfOption)]: csrfToken,
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,23 +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, expiration } = 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]: JSON.stringify({
150
- token: userIdResult.csrfToken,
151
- expiration,
152
- }),
148
+ 'set-cookie': [
149
+ authCookie,
150
+ csrfCookie,
151
+ ],
153
152
  };
154
153
  }
155
154
  else {
@@ -187,7 +186,7 @@ export class BackendAuthClient {
187
186
  }
188
187
  /** Securely extract a user from their request headers. */
189
188
  async getSecureUser({ requestHeaders, isSignUpCookie, allowUserAuthRefresh, }) {
190
- 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);
191
190
  if (!userIdResult) {
192
191
  this.logForUser({
193
192
  user: undefined,
@@ -218,14 +217,28 @@ export class BackendAuthClient {
218
217
  requestHeaders,
219
218
  user,
220
219
  });
221
- const cookieRefreshHeaders = (await this.createCookieRefreshHeaders({
222
- userIdResult,
223
- requestHeaders,
224
- })) || {};
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
+ });
225
236
  return {
226
237
  user: assumedUser || user,
227
238
  isAssumed: !!assumedUser,
228
- responseHeaders: allowUserAuthRefresh ? cookieRefreshHeaders : {},
239
+ responseHeaders: {
240
+ 'set-cookie': mergeHeaderValues(cookieRefreshHeaders?.['set-cookie'], csrfCookie),
241
+ },
229
242
  };
230
243
  }
231
244
  /**
@@ -273,18 +286,22 @@ export class BackendAuthClient {
273
286
  * generating a new one.
274
287
  */
275
288
  async refreshLoginHeaders({ userId, cookieParams, existingUserIdResult, }) {
276
- const { cookie } = await generateAuthCookie({
289
+ const authCookie = await generateAuthCookie({
277
290
  csrfToken: existingUserIdResult.csrfToken,
278
291
  userId,
279
292
  sessionStartedAt: existingUserIdResult.sessionStartedAt,
280
293
  }, cookieParams);
294
+ const csrfCookie = generateCsrfCookie(existingUserIdResult.csrfToken, cookieParams);
281
295
  return {
282
- 'set-cookie': cookie,
296
+ 'set-cookie': [
297
+ authCookie,
298
+ csrfCookie,
299
+ ],
283
300
  };
284
301
  }
285
302
  /** Use these headers to log a user in. */
286
303
  async createLoginHeaders({ userId, requestHeaders, isSignUpCookie, }) {
287
- const oppositeCookieName = isSignUpCookie ? AuthCookieName.Auth : AuthCookieName.SignUp;
304
+ const oppositeCookieName = isSignUpCookie ? AuthCookie.Auth : AuthCookie.SignUp;
288
305
  const hasExistingOppositeCookie = requestHeaders.cookie?.includes(`${oppositeCookieName}=`);
289
306
  const discardOppositeCookieHeaders = hasExistingOppositeCookie
290
307
  ? generateLogoutHeaders(await this.getCookieParams({
@@ -292,7 +309,7 @@ export class BackendAuthClient {
292
309
  requestHeaders,
293
310
  }))
294
311
  : undefined;
295
- 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);
296
313
  const cookieParams = await this.getCookieParams({
297
314
  isSignUpCookie,
298
315
  requestHeaders,
@@ -303,7 +320,7 @@ export class BackendAuthClient {
303
320
  cookieParams,
304
321
  existingUserIdResult,
305
322
  })
306
- : await generateSuccessfulLoginHeaders(userId, cookieParams, this.config.csrf);
323
+ : await generateSuccessfulLoginHeaders(userId, cookieParams);
307
324
  return {
308
325
  ...newCookieHeaders,
309
326
  'set-cookie': mergeHeaderValues(newCookieHeaders['set-cookie'], discardOppositeCookieHeaders?.['set-cookie']),
@@ -337,7 +354,7 @@ export class BackendAuthClient {
337
354
  */
338
355
  async getInsecureUser({ requestHeaders, allowUserAuthRefresh, }) {
339
356
  // eslint-disable-next-line @typescript-eslint/no-deprecated
340
- const userIdResult = await insecureExtractUserIdFromCookieAlone(requestHeaders, await this.getJwtParams(), AuthCookieName.Auth);
357
+ const userIdResult = await insecureExtractUserIdFromCookieAlone(requestHeaders, await this.getJwtParams(), AuthCookie.Auth);
341
358
  if (!userIdResult) {
342
359
  this.logForUser({
343
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}.
@@ -49,16 +48,12 @@ export type FrontendAuthClientConfig = Readonly<{
49
48
  * another user.
50
49
  */
51
50
  assumedUserHeaderName: string;
52
- /**
53
- * Allowed clock skew tolerance for CSRF token expiration checks. Accounts for differences
54
- * between server and client clocks.
55
- *
56
- * @default {minutes: 5}
57
- */
58
- allowedClockSkew: Readonly<AnyDuration>;
59
51
  overrides: PartialWithUndefined<{
60
- localStorage: Pick<Storage, 'setItem' | 'removeItem' | 'getItem'>;
61
- csrfTokenStore: CsrfTokenStore;
52
+ localStorage: SelectFrom<Storage, {
53
+ setItem: true;
54
+ removeItem: true;
55
+ getItem: true;
56
+ }>;
62
57
  }>;
63
58
  }>;
64
59
  /**
@@ -79,8 +74,6 @@ export declare class FrontendAuthClient<AssumedUserParams extends JsonCompatible
79
74
  * interval).
80
75
  */
81
76
  destroy(): void;
82
- /** Wraps {@link getCurrentCsrfToken} to automatically handle wiping an invalid CSRF token. */
83
- getCurrentCsrfToken(): Promise<string | undefined>;
84
77
  /**
85
78
  * Assume the given user. Pass `undefined` to wipe the currently assumed user.
86
79
  *
@@ -95,17 +88,16 @@ export declare class FrontendAuthClient<AssumedUserParams extends JsonCompatible
95
88
  * `@augment-vir/common`](https://electrovir.github.io/augment-vir/functions/mergeDeep.html) to
96
89
  * combine them with these.
97
90
  */
98
- createAuthenticatedRequestInit(): Promise<RequestInit>;
91
+ createAuthenticatedRequestInit(): RequestInit;
99
92
  /** Wipes the current user auth. */
100
93
  logout(): Promise<void>;
101
94
  /**
102
- * 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.
103
97
  *
104
98
  * @throws Error if the login response failed.
105
- * @throws Error if the login response has an invalid CSRF token.
106
99
  */
107
100
  handleLoginResponse(response: Readonly<SelectFrom<Response, {
108
- headers: true;
109
101
  ok: true;
110
102
  }>>): Promise<void>;
111
103
  /**
@@ -1,6 +1,6 @@
1
1
  import { HttpStatus, } from '@augment-vir/common';
2
2
  import { listenToActivity } from 'detect-activity';
3
- import { defaultAllowedClockSkew, 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,18 +41,6 @@ export class FrontendAuthClient {
41
41
  this.userCheckInterval?.clearInterval();
42
42
  this.removeActivityListener?.();
43
43
  }
44
- /** Wraps {@link getCurrentCsrfToken} to automatically handle wiping an invalid CSRF token. */
45
- async getCurrentCsrfToken() {
46
- const csrfTokenResult = await getCurrentCsrfToken({
47
- ...this.config.csrf,
48
- csrfTokenStore: this.config.overrides?.csrfTokenStore,
49
- allowedClockSkew: this.config.allowedClockSkew || defaultAllowedClockSkew,
50
- });
51
- if (csrfTokenResult.failure) {
52
- return undefined;
53
- }
54
- return csrfTokenResult.csrfToken.token;
55
- }
56
44
  /**
57
45
  * Assume the given user. Pass `undefined` to wipe the currently assumed user.
58
46
  *
@@ -90,8 +78,8 @@ export class FrontendAuthClient {
90
78
  * `@augment-vir/common`](https://electrovir.github.io/augment-vir/functions/mergeDeep.html) to
91
79
  * combine them with these.
92
80
  */
93
- async createAuthenticatedRequestInit() {
94
- const csrfToken = await this.getCurrentCsrfToken();
81
+ createAuthenticatedRequestInit() {
82
+ const csrfToken = getCurrentCsrfToken();
95
83
  const assumedUser = this.getAssumedUser();
96
84
  const headers = {
97
85
  ...(csrfToken
@@ -115,27 +103,16 @@ export class FrontendAuthClient {
115
103
  await this.config.authClearedCallback?.();
116
104
  }
117
105
  /**
118
- * 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.
119
108
  *
120
109
  * @throws Error if the login response failed.
121
- * @throws Error if the login response has an invalid CSRF token.
122
110
  */
123
111
  async handleLoginResponse(response) {
124
112
  if (!response.ok) {
125
113
  await this.logout();
126
114
  throw new Error('Login response failed.');
127
115
  }
128
- const { csrfToken } = extractCsrfTokenHeader(response, this.config.csrf, {
129
- allowedClockSkew: this.config.allowedClockSkew || defaultAllowedClockSkew,
130
- });
131
- if (!csrfToken) {
132
- await this.logout();
133
- throw new Error('Did not receive any CSRF token.');
134
- }
135
- await storeCsrfToken(csrfToken, {
136
- ...this.config.csrf,
137
- csrfTokenStore: this.config.overrides?.csrfTokenStore,
138
- });
139
116
  }
140
117
  /**
141
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, parseCsrfToken, 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;
@@ -18,12 +18,7 @@ function readHeader(headers, headerName) {
18
18
  }
19
19
  }
20
20
  function readCsrfTokenHeader(headers, csrfHeaderNameOption) {
21
- const rawCsrfToken = readHeader(headers, resolveCsrfHeaderName(csrfHeaderNameOption));
22
- if (!rawCsrfToken) {
23
- return undefined;
24
- }
25
- const token = parseCsrfToken(rawCsrfToken).csrfToken?.token || rawCsrfToken;
26
- return token;
21
+ return readHeader(headers, resolveCsrfHeaderName(csrfHeaderNameOption));
27
22
  }
28
23
  /**
29
24
  * Extract the user id from a request by checking both the request cookie and CSRF token. This is
@@ -33,7 +28,7 @@ function readCsrfTokenHeader(headers, csrfHeaderNameOption) {
33
28
  * @category Auth : Host
34
29
  * @returns The extracted user id or `undefined` if no valid auth headers exist.
35
30
  */
36
- export async function extractUserIdFromRequestHeaders(headers, jwtParams, csrfHeaderNameOption, cookieName = AuthCookieName.Auth) {
31
+ export async function extractUserIdFromRequestHeaders(headers, jwtParams, csrfHeaderNameOption, cookieName = AuthCookie.Auth) {
37
32
  try {
38
33
  const csrfToken = readCsrfTokenHeader(headers, csrfHeaderNameOption);
39
34
  const cookie = readHeader(headers, 'cookie');
@@ -65,7 +60,7 @@ export async function extractUserIdFromRequestHeaders(headers, jwtParams, csrfHe
65
60
  * @deprecated Prefer {@link extractUserIdFromRequestHeaders} instead: it is more secure.
66
61
  * @category Auth : Host
67
62
  */
68
- export async function insecureExtractUserIdFromCookieAlone(headers, jwtParams, cookieName = AuthCookieName.Auth) {
63
+ export async function insecureExtractUserIdFromCookieAlone(headers, jwtParams, cookieName) {
69
64
  try {
70
65
  const cookie = readHeader(headers, 'cookie');
71
66
  if (!cookie) {
@@ -89,31 +84,32 @@ export async function insecureExtractUserIdFromCookieAlone(headers, jwtParams, c
89
84
  }
90
85
  }
91
86
  /**
92
- * 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.
93
90
  *
94
91
  * @category Auth : Host
95
92
  */
96
93
  export async function generateSuccessfulLoginHeaders(
97
94
  /** The id from your database of the user you're authenticating. */
98
- userId, cookieConfig, csrfHeaderNameOption,
95
+ userId, cookieConfig,
99
96
  /**
100
97
  * The timestamp (in seconds) when the session originally started. If not provided, the current
101
98
  * time will be used (for new sessions).
102
99
  */
103
100
  sessionStartedAt) {
104
- const csrfToken = generateCsrfToken(cookieConfig.cookieDuration);
105
- const csrfHeaderName = resolveCsrfHeaderName(csrfHeaderNameOption);
106
- const { cookie, expiration } = await generateAuthCookie({
107
- csrfToken: csrfToken.token,
101
+ const csrfToken = generateCsrfToken();
102
+ const authCookie = await generateAuthCookie({
103
+ csrfToken,
108
104
  userId,
109
105
  sessionStartedAt: sessionStartedAt ?? Date.now(),
110
106
  }, cookieConfig);
107
+ const csrfCookie = generateCsrfCookie(csrfToken, cookieConfig);
111
108
  return {
112
- 'set-cookie': cookie,
113
- [csrfHeaderName]: JSON.stringify({
114
- token: csrfToken.token,
115
- expiration,
116
- }),
109
+ 'set-cookie': [
110
+ authCookie,
111
+ csrfCookie,
112
+ ],
117
113
  };
118
114
  }
119
115
  /**
@@ -124,25 +120,9 @@ sessionStartedAt) {
124
120
  */
125
121
  export function generateLogoutHeaders(cookieConfig) {
126
122
  return {
127
- 'set-cookie': clearAuthCookie(cookieConfig),
123
+ 'set-cookie': [
124
+ clearAuthCookie(cookieConfig),
125
+ clearCsrfCookie(cookieConfig),
126
+ ],
128
127
  };
129
128
  }
130
- /**
131
- * Store auth data on a client (frontend) after receiving an auth response from the host (backend).
132
- * Specifically, this stores the CSRF token into IndexedDB (which doesn't need to be a secret).
133
- * Alternatively, if the given response failed, this will wipe the existing (if any) stored CSRF
134
- * token.
135
- *
136
- * @category Auth : Client
137
- * @throws Error if no CSRF token header is found.
138
- */
139
- export async function handleAuthResponse(response, options) {
140
- if (!response.ok) {
141
- return;
142
- }
143
- const { csrfToken } = extractCsrfTokenHeader(response, options);
144
- if (!csrfToken) {
145
- throw new Error('Did not receive any CSRF token.');
146
- }
147
- await storeCsrfToken(csrfToken, options);
148
- }