auth-vir 5.1.0 → 5.2.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 +5 -5
- package/dist/auth-client/backend-auth.client.d.ts +6 -0
- package/dist/auth-client/backend-auth.client.js +29 -5
- package/dist/auth-client/frontend-auth.client.d.ts +5 -0
- package/dist/auth-client/frontend-auth.client.js +1 -1
- package/dist/auth.d.ts +22 -9
- package/dist/auth.js +15 -5
- package/dist/cookie.d.ts +27 -3
- package/dist/cookie.js +22 -7
- package/dist/csrf-token.d.ts +1 -1
- package/dist/csrf-token.js +4 -3
- package/package.json +1 -1
- package/src/auth-client/backend-auth.client.ts +40 -18
- package/src/auth-client/frontend-auth.client.ts +6 -1
- package/src/auth.ts +50 -24
- package/src/cookie.ts +54 -14
- package/src/csrf-token.ts +4 -3
package/README.md
CHANGED
|
@@ -156,12 +156,12 @@ export async function createUser(
|
|
|
156
156
|
*/
|
|
157
157
|
export async function getAuthenticatedUser(request: ClientRequest) {
|
|
158
158
|
const userId = (
|
|
159
|
-
await extractUserIdFromRequestHeaders<MyUserId>(
|
|
160
|
-
request.getHeaders(),
|
|
159
|
+
await extractUserIdFromRequestHeaders<MyUserId>({
|
|
160
|
+
headers: request.getHeaders(),
|
|
161
161
|
jwtParams,
|
|
162
|
-
csrfOption,
|
|
163
|
-
AuthCookie.Auth,
|
|
164
|
-
)
|
|
162
|
+
csrfHeaderNameOption: csrfOption,
|
|
163
|
+
cookieName: AuthCookie.Auth,
|
|
164
|
+
})
|
|
165
165
|
)?.userId;
|
|
166
166
|
const user = userId ? findUserInDatabaseById(userId) : undefined;
|
|
167
167
|
|
|
@@ -150,6 +150,12 @@ export type BackendAuthClientConfig<DatabaseUser extends AnyObject, UserId exten
|
|
|
150
150
|
* JWT embedded in the `HttpOnly` auth cookie.
|
|
151
151
|
*/
|
|
152
152
|
csrfCookieOrigin: string;
|
|
153
|
+
/**
|
|
154
|
+
* Optional suffix appended to cookie names (e.g., `'staging'` produces `auth-staging`,
|
|
155
|
+
* `auth-vir-csrf-staging`). When `undefined`, cookie names are unchanged. Useful for
|
|
156
|
+
* running multiple environments on the same domain without cookie collisions.
|
|
157
|
+
*/
|
|
158
|
+
cookieNameSuffix: string;
|
|
153
159
|
}>>;
|
|
154
160
|
/**
|
|
155
161
|
* An auth client for creating and validating JWTs embedded in cookies. This should only be used in
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { ensureArray, } from '@augment-vir/common';
|
|
2
2
|
import { calculateRelativeDate, createUtcFullDate, getNowInUtcTimezone, isDateAfter, } from 'date-vir';
|
|
3
3
|
import { extractUserIdFromRequestHeaders, generateLogoutHeaders, insecureExtractUserIdFromCookieAlone, } from '../auth.js';
|
|
4
|
-
import { AuthCookie, clearCsrfCookie, generateAuthCookie, generateCsrfCookie, } from '../cookie.js';
|
|
4
|
+
import { AuthCookie, clearCsrfCookie, generateAuthCookie, generateCsrfCookie, resolveCookieName, } from '../cookie.js';
|
|
5
5
|
import { generateCsrfToken } from '../csrf-token.js';
|
|
6
6
|
import { AuthHeaderName, mergeHeaderValues } from '../headers.js';
|
|
7
7
|
import { parseJwtKeys } from '../jwt/jwt-keys.js';
|
|
@@ -64,6 +64,7 @@ export class BackendAuthClient {
|
|
|
64
64
|
jwtParams: await this.getJwtParams(),
|
|
65
65
|
isDev: this.config.isDev,
|
|
66
66
|
authCookie: isSignUpCookie ? AuthCookie.SignUp : AuthCookie.Auth,
|
|
67
|
+
cookieNameSuffix: this.config.cookieNameSuffix,
|
|
67
68
|
};
|
|
68
69
|
}
|
|
69
70
|
/** Calls the provided `getUserFromDatabase` config. */
|
|
@@ -154,6 +155,7 @@ export class BackendAuthClient {
|
|
|
154
155
|
const csrfCookie = generateCsrfCookie(userIdResult.csrfToken, {
|
|
155
156
|
...cookieParams,
|
|
156
157
|
hostOrigin: this.resolveCsrfCookieOrigin(cookieParams.hostOrigin),
|
|
158
|
+
cookieNameSuffix: this.config.cookieNameSuffix,
|
|
157
159
|
});
|
|
158
160
|
return {
|
|
159
161
|
'set-cookie': [
|
|
@@ -197,7 +199,13 @@ export class BackendAuthClient {
|
|
|
197
199
|
}
|
|
198
200
|
/** Securely extract a user from their request headers. */
|
|
199
201
|
async getSecureUser({ requestHeaders, isSignUpCookie, allowUserAuthRefresh, }) {
|
|
200
|
-
const userIdResult = await extractUserIdFromRequestHeaders(
|
|
202
|
+
const userIdResult = await extractUserIdFromRequestHeaders({
|
|
203
|
+
headers: requestHeaders,
|
|
204
|
+
jwtParams: await this.getJwtParams(),
|
|
205
|
+
csrfHeaderNameOption: this.config.csrf,
|
|
206
|
+
cookieName: isSignUpCookie ? AuthCookie.SignUp : AuthCookie.Auth,
|
|
207
|
+
cookieNameSuffix: this.config.cookieNameSuffix,
|
|
208
|
+
});
|
|
201
209
|
if (!userIdResult) {
|
|
202
210
|
this.logForUser({
|
|
203
211
|
user: undefined,
|
|
@@ -244,6 +252,7 @@ export class BackendAuthClient {
|
|
|
244
252
|
const csrfCookie = generateCsrfCookie(userIdResult.csrfToken, {
|
|
245
253
|
hostOrigin: this.resolveCsrfCookieOrigin(authCookieOrigin),
|
|
246
254
|
isDev: this.config.isDev,
|
|
255
|
+
cookieNameSuffix: this.config.cookieNameSuffix,
|
|
247
256
|
});
|
|
248
257
|
return {
|
|
249
258
|
user: assumedUser || user,
|
|
@@ -302,6 +311,7 @@ export class BackendAuthClient {
|
|
|
302
311
|
? clearCsrfCookie({
|
|
303
312
|
hostOrigin: this.config.csrfCookieOrigin,
|
|
304
313
|
isDev: this.config.isDev,
|
|
314
|
+
cookieNameSuffix: this.config.cookieNameSuffix,
|
|
305
315
|
})
|
|
306
316
|
: undefined;
|
|
307
317
|
return {
|
|
@@ -321,6 +331,7 @@ export class BackendAuthClient {
|
|
|
321
331
|
const csrfCookie = generateCsrfCookie(existingUserIdResult.csrfToken, {
|
|
322
332
|
...cookieParams,
|
|
323
333
|
hostOrigin: this.resolveCsrfCookieOrigin(cookieParams.hostOrigin),
|
|
334
|
+
cookieNameSuffix: this.config.cookieNameSuffix,
|
|
324
335
|
});
|
|
325
336
|
return {
|
|
326
337
|
'set-cookie': [
|
|
@@ -340,6 +351,7 @@ export class BackendAuthClient {
|
|
|
340
351
|
const csrfCookie = generateCsrfCookie(csrfToken, {
|
|
341
352
|
...cookieParams,
|
|
342
353
|
hostOrigin: this.resolveCsrfCookieOrigin(cookieParams.hostOrigin),
|
|
354
|
+
cookieNameSuffix: this.config.cookieNameSuffix,
|
|
343
355
|
});
|
|
344
356
|
return {
|
|
345
357
|
'set-cookie': [
|
|
@@ -351,7 +363,8 @@ export class BackendAuthClient {
|
|
|
351
363
|
/** Use these headers to log a user in. */
|
|
352
364
|
async createLoginHeaders({ userId, requestHeaders, isSignUpCookie, }) {
|
|
353
365
|
const oppositeCookieName = isSignUpCookie ? AuthCookie.Auth : AuthCookie.SignUp;
|
|
354
|
-
const
|
|
366
|
+
const resolvedOppositeCookieName = resolveCookieName(oppositeCookieName, this.config.cookieNameSuffix);
|
|
367
|
+
const hasExistingOppositeCookie = requestHeaders.cookie?.includes(`${resolvedOppositeCookieName}=`);
|
|
355
368
|
const discardOppositeCookieHeaders = hasExistingOppositeCookie
|
|
356
369
|
? generateLogoutHeaders(await this.getCookieParams({
|
|
357
370
|
isSignUpCookie: !isSignUpCookie,
|
|
@@ -360,7 +373,13 @@ export class BackendAuthClient {
|
|
|
360
373
|
preserveCsrf: true,
|
|
361
374
|
})
|
|
362
375
|
: undefined;
|
|
363
|
-
const existingUserIdResult = await extractUserIdFromRequestHeaders(
|
|
376
|
+
const existingUserIdResult = await extractUserIdFromRequestHeaders({
|
|
377
|
+
headers: requestHeaders,
|
|
378
|
+
jwtParams: await this.getJwtParams(),
|
|
379
|
+
csrfHeaderNameOption: this.config.csrf,
|
|
380
|
+
cookieName: isSignUpCookie ? AuthCookie.SignUp : AuthCookie.Auth,
|
|
381
|
+
cookieNameSuffix: this.config.cookieNameSuffix,
|
|
382
|
+
});
|
|
364
383
|
const cookieParams = await this.getCookieParams({
|
|
365
384
|
isSignUpCookie,
|
|
366
385
|
requestHeaders,
|
|
@@ -405,7 +424,12 @@ export class BackendAuthClient {
|
|
|
405
424
|
*/
|
|
406
425
|
async getInsecureUser({ requestHeaders, allowUserAuthRefresh, }) {
|
|
407
426
|
// eslint-disable-next-line @typescript-eslint/no-deprecated
|
|
408
|
-
const userIdResult = await insecureExtractUserIdFromCookieAlone(
|
|
427
|
+
const userIdResult = await insecureExtractUserIdFromCookieAlone({
|
|
428
|
+
headers: requestHeaders,
|
|
429
|
+
jwtParams: await this.getJwtParams(),
|
|
430
|
+
cookieName: AuthCookie.Auth,
|
|
431
|
+
cookieNameSuffix: this.config.cookieNameSuffix,
|
|
432
|
+
});
|
|
409
433
|
if (!userIdResult) {
|
|
410
434
|
this.logForUser({
|
|
411
435
|
user: undefined,
|
|
@@ -10,6 +10,11 @@ import { type CsrfHeaderNameOption } from '../csrf-token.js';
|
|
|
10
10
|
export type FrontendAuthClientConfig = Readonly<{
|
|
11
11
|
csrf: Readonly<CsrfHeaderNameOption>;
|
|
12
12
|
}> & PartialWithUndefined<{
|
|
13
|
+
/**
|
|
14
|
+
* Optional suffix appended to cookie names (e.g., `'staging'` produces
|
|
15
|
+
* `auth-vir-csrf-staging`). When `undefined`, cookie names are unchanged.
|
|
16
|
+
*/
|
|
17
|
+
cookieNameSuffix: string;
|
|
13
18
|
/**
|
|
14
19
|
* Determine if the current user can assume the identity of another user. If this is not
|
|
15
20
|
* defined, all users will be blocked from assuming other user identities.
|
|
@@ -79,7 +79,7 @@ export class FrontendAuthClient {
|
|
|
79
79
|
* combine them with these.
|
|
80
80
|
*/
|
|
81
81
|
createAuthenticatedRequestInit() {
|
|
82
|
-
const csrfToken = getCurrentCsrfToken();
|
|
82
|
+
const csrfToken = getCurrentCsrfToken(this.config.cookieNameSuffix);
|
|
83
83
|
const assumedUser = this.getAssumedUser();
|
|
84
84
|
const headers = {
|
|
85
85
|
...(csrfToken
|
package/dist/auth.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { type SelectFrom } from '@augment-vir/common';
|
|
1
|
+
import { type PartialWithUndefined, type SelectFrom } from '@augment-vir/common';
|
|
2
2
|
import { type FullDate, type UtcTimezone } from 'date-vir';
|
|
3
|
-
import { AuthCookie, type CookieParams } from './cookie.js';
|
|
3
|
+
import { type AuthCookie, type CookieParams } from './cookie.js';
|
|
4
4
|
import { type CsrfHeaderNameOption } from './csrf-token.js';
|
|
5
5
|
import { type ParseJwtParams } from './jwt/jwt.js';
|
|
6
6
|
import { type JwtUserData } from './jwt/user-jwt.js';
|
|
@@ -37,7 +37,13 @@ export type UserIdResult<UserId extends string | number> = {
|
|
|
37
37
|
* @category Auth : Host
|
|
38
38
|
* @returns The extracted user id or `undefined` if no valid auth headers exist.
|
|
39
39
|
*/
|
|
40
|
-
export declare function extractUserIdFromRequestHeaders<UserId extends string | number>(headers
|
|
40
|
+
export declare function extractUserIdFromRequestHeaders<UserId extends string | number>({ headers, jwtParams, csrfHeaderNameOption, cookieName, cookieNameSuffix, }: Readonly<{
|
|
41
|
+
headers: HeaderContainer;
|
|
42
|
+
jwtParams: Readonly<ParseJwtParams>;
|
|
43
|
+
csrfHeaderNameOption: Readonly<CsrfHeaderNameOption>;
|
|
44
|
+
cookieName: AuthCookie;
|
|
45
|
+
cookieNameSuffix?: string | undefined;
|
|
46
|
+
}>): Promise<Readonly<UserIdResult<UserId>> | undefined>;
|
|
41
47
|
/**
|
|
42
48
|
* Extract a user id from just the cookie, without CSRF token validation. This is _less secure_ than
|
|
43
49
|
* {@link extractUserIdFromRequestHeaders} as a result. This should only be used in rare
|
|
@@ -46,7 +52,12 @@ export declare function extractUserIdFromRequestHeaders<UserId extends string |
|
|
|
46
52
|
* @deprecated Prefer {@link extractUserIdFromRequestHeaders} instead: it is more secure.
|
|
47
53
|
* @category Auth : Host
|
|
48
54
|
*/
|
|
49
|
-
export declare function insecureExtractUserIdFromCookieAlone<UserId extends string | number>(headers
|
|
55
|
+
export declare function insecureExtractUserIdFromCookieAlone<UserId extends string | number>({ headers, jwtParams, cookieName, cookieNameSuffix, }: Readonly<{
|
|
56
|
+
headers: HeaderContainer;
|
|
57
|
+
jwtParams: Readonly<ParseJwtParams>;
|
|
58
|
+
cookieName: AuthCookie;
|
|
59
|
+
cookieNameSuffix?: string | undefined;
|
|
60
|
+
}>): Promise<Readonly<UserIdResult<UserId>> | undefined>;
|
|
50
61
|
/**
|
|
51
62
|
* Used by host (backend) code to set headers on a response object. Sets both the auth JWT cookie
|
|
52
63
|
* and the CSRF token cookie. The CSRF cookie is not `HttpOnly` so that frontend JavaScript can read
|
|
@@ -71,11 +82,13 @@ sessionStartedAt?: number | undefined): Promise<Record<string, string[]>>;
|
|
|
71
82
|
export declare function generateLogoutHeaders(cookieConfig: Readonly<SelectFrom<CookieParams, {
|
|
72
83
|
hostOrigin: true;
|
|
73
84
|
isDev: true;
|
|
74
|
-
}
|
|
85
|
+
}>> & PartialWithUndefined<{
|
|
86
|
+
cookieNameSuffix: string;
|
|
87
|
+
}>, options?: Readonly<PartialWithUndefined<{
|
|
75
88
|
/**
|
|
76
|
-
* When `true`, the CSRF cookie is preserved (not cleared). Use this when clearing only
|
|
77
|
-
* cookie type (e.g., the auth cookie) while keeping the other active session (e.g.,
|
|
89
|
+
* When `true`, the CSRF cookie is preserved (not cleared). Use this when clearing only
|
|
90
|
+
* one cookie type (e.g., the auth cookie) while keeping the other active session (e.g.,
|
|
78
91
|
* sign-up) that still needs its CSRF token.
|
|
79
92
|
*/
|
|
80
|
-
preserveCsrf
|
|
81
|
-
}
|
|
93
|
+
preserveCsrf: boolean;
|
|
94
|
+
}>>): Record<string, string[]>;
|
package/dist/auth.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { clearAuthCookie, clearCsrfCookie, extractCookieJwt, generateAuthCookie, generateCsrfCookie, } from './cookie.js';
|
|
2
2
|
import { generateCsrfToken, resolveCsrfHeaderName } from './csrf-token.js';
|
|
3
3
|
function readHeader(headers, headerName) {
|
|
4
4
|
if (headers instanceof Headers) {
|
|
@@ -28,14 +28,19 @@ 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
|
|
31
|
+
export async function extractUserIdFromRequestHeaders({ headers, jwtParams, csrfHeaderNameOption, cookieName, cookieNameSuffix, }) {
|
|
32
32
|
try {
|
|
33
33
|
const csrfToken = readCsrfTokenHeader(headers, csrfHeaderNameOption);
|
|
34
34
|
const cookie = readHeader(headers, 'cookie');
|
|
35
35
|
if (!cookie || !csrfToken) {
|
|
36
36
|
return undefined;
|
|
37
37
|
}
|
|
38
|
-
const jwt = await extractCookieJwt(
|
|
38
|
+
const jwt = await extractCookieJwt({
|
|
39
|
+
rawCookie: cookie,
|
|
40
|
+
jwtParams,
|
|
41
|
+
cookieName,
|
|
42
|
+
cookieNameSuffix,
|
|
43
|
+
});
|
|
39
44
|
if (!jwt || jwt.data.csrfToken !== csrfToken) {
|
|
40
45
|
return undefined;
|
|
41
46
|
}
|
|
@@ -60,13 +65,18 @@ export async function extractUserIdFromRequestHeaders(headers, jwtParams, csrfHe
|
|
|
60
65
|
* @deprecated Prefer {@link extractUserIdFromRequestHeaders} instead: it is more secure.
|
|
61
66
|
* @category Auth : Host
|
|
62
67
|
*/
|
|
63
|
-
export async function insecureExtractUserIdFromCookieAlone(headers, jwtParams, cookieName) {
|
|
68
|
+
export async function insecureExtractUserIdFromCookieAlone({ headers, jwtParams, cookieName, cookieNameSuffix, }) {
|
|
64
69
|
try {
|
|
65
70
|
const cookie = readHeader(headers, 'cookie');
|
|
66
71
|
if (!cookie) {
|
|
67
72
|
return undefined;
|
|
68
73
|
}
|
|
69
|
-
const jwt = await extractCookieJwt(
|
|
74
|
+
const jwt = await extractCookieJwt({
|
|
75
|
+
rawCookie: cookie,
|
|
76
|
+
jwtParams,
|
|
77
|
+
cookieName,
|
|
78
|
+
cookieNameSuffix,
|
|
79
|
+
});
|
|
70
80
|
if (!jwt) {
|
|
71
81
|
return undefined;
|
|
72
82
|
}
|
package/dist/cookie.d.ts
CHANGED
|
@@ -16,6 +16,13 @@ export declare enum AuthCookie {
|
|
|
16
16
|
/** Used for storing the CSRF token. Not `HttpOnly` so that frontend JS can read it. */
|
|
17
17
|
Csrf = "auth-vir-csrf"
|
|
18
18
|
}
|
|
19
|
+
/**
|
|
20
|
+
* Resolves a cookie name by appending a suffix when provided. When `cookieNameSuffix` is
|
|
21
|
+
* `undefined`, the base name is returned unchanged.
|
|
22
|
+
*
|
|
23
|
+
* @category Internal
|
|
24
|
+
*/
|
|
25
|
+
export declare function resolveCookieName(baseCookieName: AuthCookie, cookieNameSuffix?: string | undefined): string;
|
|
19
26
|
/**
|
|
20
27
|
* Parameters for {@link generateAuthCookie}.
|
|
21
28
|
*
|
|
@@ -54,6 +61,12 @@ export type CookieParams = {
|
|
|
54
61
|
* @default false
|
|
55
62
|
*/
|
|
56
63
|
isDev: boolean;
|
|
64
|
+
/**
|
|
65
|
+
* Optional suffix appended to cookie names (e.g., `'staging'` produces `auth-staging`). When
|
|
66
|
+
* `undefined`, cookie names are unchanged. Useful for running multiple environments on the same
|
|
67
|
+
* domain without cookie collisions.
|
|
68
|
+
*/
|
|
69
|
+
cookieNameSuffix: string;
|
|
57
70
|
}>;
|
|
58
71
|
/**
|
|
59
72
|
* Generate a secure cookie that stores the user JWT data. Used in host (backend) code.
|
|
@@ -75,7 +88,9 @@ export declare function generateAuthCookie(userJwtData: Readonly<JwtUserData>, c
|
|
|
75
88
|
export declare function generateCsrfCookie(csrfToken: string, cookieConfig: Readonly<SelectFrom<CookieParams, {
|
|
76
89
|
hostOrigin: true;
|
|
77
90
|
isDev: true;
|
|
78
|
-
}>>
|
|
91
|
+
}>> & PartialWithUndefined<{
|
|
92
|
+
cookieNameSuffix: string;
|
|
93
|
+
}>): string;
|
|
79
94
|
/**
|
|
80
95
|
* Generate a cookie value that will clear the previous auth cookie. Use this when signing out.
|
|
81
96
|
*
|
|
@@ -86,6 +101,7 @@ export declare function clearAuthCookie(cookieConfig: Readonly<SelectFrom<Cookie
|
|
|
86
101
|
isDev: true;
|
|
87
102
|
}>> & PartialWithUndefined<{
|
|
88
103
|
authCookie: AuthCookie;
|
|
104
|
+
cookieNameSuffix: string;
|
|
89
105
|
}>): string;
|
|
90
106
|
/**
|
|
91
107
|
* Generate a cookie value that will clear the CSRF token cookie. Use this when signing out.
|
|
@@ -95,7 +111,9 @@ export declare function clearAuthCookie(cookieConfig: Readonly<SelectFrom<Cookie
|
|
|
95
111
|
export declare function clearCsrfCookie(cookieConfig: Readonly<SelectFrom<CookieParams, {
|
|
96
112
|
hostOrigin: true;
|
|
97
113
|
isDev: true;
|
|
98
|
-
}>>
|
|
114
|
+
}>> & PartialWithUndefined<{
|
|
115
|
+
cookieNameSuffix: string;
|
|
116
|
+
}>): string;
|
|
99
117
|
/**
|
|
100
118
|
* Generate a cookie string from a raw set of parameters.
|
|
101
119
|
*
|
|
@@ -108,4 +126,10 @@ export declare function generateCookie(params: Readonly<Record<string, Exclude<P
|
|
|
108
126
|
* @category Internal
|
|
109
127
|
* @returns The extracted auth Cookie JWT data or `undefined` if no valid auth JWT data was found.
|
|
110
128
|
*/
|
|
111
|
-
export declare function extractCookieJwt(rawCookie
|
|
129
|
+
export declare function extractCookieJwt({ rawCookie, jwtParams, cookieName, cookieNameSuffix, }: {
|
|
130
|
+
rawCookie: string;
|
|
131
|
+
jwtParams: Readonly<ParseJwtParams>;
|
|
132
|
+
cookieName: AuthCookie;
|
|
133
|
+
} & PartialWithUndefined<{
|
|
134
|
+
cookieNameSuffix: string;
|
|
135
|
+
}>): Promise<undefined | ParsedJwt<JwtUserData>>;
|
package/dist/cookie.js
CHANGED
|
@@ -17,6 +17,20 @@ export var AuthCookie;
|
|
|
17
17
|
/** Used for storing the CSRF token. Not `HttpOnly` so that frontend JS can read it. */
|
|
18
18
|
AuthCookie["Csrf"] = "auth-vir-csrf";
|
|
19
19
|
})(AuthCookie || (AuthCookie = {}));
|
|
20
|
+
/**
|
|
21
|
+
* Resolves a cookie name by appending a suffix when provided. When `cookieNameSuffix` is
|
|
22
|
+
* `undefined`, the base name is returned unchanged.
|
|
23
|
+
*
|
|
24
|
+
* @category Internal
|
|
25
|
+
*/
|
|
26
|
+
export function resolveCookieName(baseCookieName, cookieNameSuffix) {
|
|
27
|
+
return [
|
|
28
|
+
baseCookieName,
|
|
29
|
+
cookieNameSuffix,
|
|
30
|
+
]
|
|
31
|
+
.filter(check.isTruthy)
|
|
32
|
+
.join('-');
|
|
33
|
+
}
|
|
20
34
|
function generateSetCookie({ name, value, httpOnly, cookieConfig, }) {
|
|
21
35
|
return generateCookie({
|
|
22
36
|
[name]: value,
|
|
@@ -39,7 +53,7 @@ function generateSetCookie({ name, value, httpOnly, cookieConfig, }) {
|
|
|
39
53
|
*/
|
|
40
54
|
export async function generateAuthCookie(userJwtData, cookieConfig) {
|
|
41
55
|
return generateSetCookie({
|
|
42
|
-
name: cookieConfig.authCookie || AuthCookie.Auth,
|
|
56
|
+
name: resolveCookieName(cookieConfig.authCookie || AuthCookie.Auth, cookieConfig.cookieNameSuffix),
|
|
43
57
|
value: await createUserJwt(userJwtData, cookieConfig.jwtParams),
|
|
44
58
|
httpOnly: true,
|
|
45
59
|
cookieConfig,
|
|
@@ -58,7 +72,7 @@ export async function generateAuthCookie(userJwtData, cookieConfig) {
|
|
|
58
72
|
*/
|
|
59
73
|
export function generateCsrfCookie(csrfToken, cookieConfig) {
|
|
60
74
|
return generateSetCookie({
|
|
61
|
-
name: AuthCookie.Csrf,
|
|
75
|
+
name: resolveCookieName(AuthCookie.Csrf, cookieConfig.cookieNameSuffix),
|
|
62
76
|
value: csrfToken,
|
|
63
77
|
httpOnly: false,
|
|
64
78
|
cookieConfig: {
|
|
@@ -76,7 +90,7 @@ export function generateCsrfCookie(csrfToken, cookieConfig) {
|
|
|
76
90
|
*/
|
|
77
91
|
export function clearAuthCookie(cookieConfig) {
|
|
78
92
|
return generateSetCookie({
|
|
79
|
-
name: cookieConfig.authCookie || AuthCookie.Auth,
|
|
93
|
+
name: resolveCookieName(cookieConfig.authCookie || AuthCookie.Auth, cookieConfig.cookieNameSuffix),
|
|
80
94
|
value: 'redacted',
|
|
81
95
|
httpOnly: true,
|
|
82
96
|
cookieConfig,
|
|
@@ -89,7 +103,7 @@ export function clearAuthCookie(cookieConfig) {
|
|
|
89
103
|
*/
|
|
90
104
|
export function clearCsrfCookie(cookieConfig) {
|
|
91
105
|
return generateSetCookie({
|
|
92
|
-
name: AuthCookie.Csrf,
|
|
106
|
+
name: resolveCookieName(AuthCookie.Csrf, cookieConfig.cookieNameSuffix),
|
|
93
107
|
value: 'redacted',
|
|
94
108
|
httpOnly: false,
|
|
95
109
|
cookieConfig,
|
|
@@ -125,13 +139,14 @@ export function generateCookie(params) {
|
|
|
125
139
|
* @category Internal
|
|
126
140
|
* @returns The extracted auth Cookie JWT data or `undefined` if no valid auth JWT data was found.
|
|
127
141
|
*/
|
|
128
|
-
export async function extractCookieJwt(rawCookie, jwtParams, cookieName) {
|
|
129
|
-
const
|
|
142
|
+
export async function extractCookieJwt({ rawCookie, jwtParams, cookieName, cookieNameSuffix, }) {
|
|
143
|
+
const resolvedName = resolveCookieName(cookieName, cookieNameSuffix);
|
|
144
|
+
const cookieRegExp = new RegExp(`${escapeStringForRegExp(resolvedName)}=[^;]+(?:;|$)`);
|
|
130
145
|
const [cookieValue] = safeMatch(rawCookie, cookieRegExp);
|
|
131
146
|
if (!cookieValue) {
|
|
132
147
|
return undefined;
|
|
133
148
|
}
|
|
134
|
-
const rawJwt = cookieValue.replace(`${
|
|
149
|
+
const rawJwt = cookieValue.replace(`${resolvedName}=`, '').replace(';', '');
|
|
135
150
|
const jwt = await parseUserJwt(rawJwt, jwtParams);
|
|
136
151
|
return jwt;
|
|
137
152
|
}
|
package/dist/csrf-token.d.ts
CHANGED
|
@@ -30,4 +30,4 @@ export declare function resolveCsrfHeaderName(options: Readonly<CsrfHeaderNameOp
|
|
|
30
30
|
*
|
|
31
31
|
* @category Auth : Client
|
|
32
32
|
*/
|
|
33
|
-
export declare function getCurrentCsrfToken(): string | undefined;
|
|
33
|
+
export declare function getCurrentCsrfToken(cookieNameSuffix?: string | undefined): string | undefined;
|
package/dist/csrf-token.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { check } from '@augment-vir/assert';
|
|
2
2
|
import { escapeStringForRegExp, randomString, safeMatch } from '@augment-vir/common';
|
|
3
|
-
import { AuthCookie } from './cookie.js';
|
|
3
|
+
import { AuthCookie, resolveCookieName } from './cookie.js';
|
|
4
4
|
/**
|
|
5
5
|
* Generates a random, cryptographically secure CSRF token string.
|
|
6
6
|
*
|
|
@@ -35,8 +35,9 @@ export function resolveCsrfHeaderName(options) {
|
|
|
35
35
|
*
|
|
36
36
|
* @category Auth : Client
|
|
37
37
|
*/
|
|
38
|
-
export function getCurrentCsrfToken() {
|
|
39
|
-
const
|
|
38
|
+
export function getCurrentCsrfToken(cookieNameSuffix) {
|
|
39
|
+
const resolvedName = resolveCookieName(AuthCookie.Csrf, cookieNameSuffix);
|
|
40
|
+
const cookieRegExp = new RegExp(`${escapeStringForRegExp(resolvedName)}=([^;]+)`);
|
|
40
41
|
const [, value,] = safeMatch(globalThis.document.cookie, cookieRegExp);
|
|
41
42
|
return value || undefined;
|
|
42
43
|
}
|
package/package.json
CHANGED
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
clearCsrfCookie,
|
|
26
26
|
generateAuthCookie,
|
|
27
27
|
generateCsrfCookie,
|
|
28
|
+
resolveCookieName,
|
|
28
29
|
type CookieParams,
|
|
29
30
|
} from '../cookie.js';
|
|
30
31
|
import {generateCsrfToken, type CsrfHeaderNameOption} from '../csrf-token.js';
|
|
@@ -186,6 +187,12 @@ export type BackendAuthClientConfig<
|
|
|
186
187
|
* JWT embedded in the `HttpOnly` auth cookie.
|
|
187
188
|
*/
|
|
188
189
|
csrfCookieOrigin: string;
|
|
190
|
+
/**
|
|
191
|
+
* Optional suffix appended to cookie names (e.g., `'staging'` produces `auth-staging`,
|
|
192
|
+
* `auth-vir-csrf-staging`). When `undefined`, cookie names are unchanged. Useful for
|
|
193
|
+
* running multiple environments on the same domain without cookie collisions.
|
|
194
|
+
*/
|
|
195
|
+
cookieNameSuffix: string;
|
|
189
196
|
}>
|
|
190
197
|
>;
|
|
191
198
|
|
|
@@ -277,6 +284,7 @@ export class BackendAuthClient<
|
|
|
277
284
|
jwtParams: await this.getJwtParams(),
|
|
278
285
|
isDev: this.config.isDev,
|
|
279
286
|
authCookie: isSignUpCookie ? AuthCookie.SignUp : AuthCookie.Auth,
|
|
287
|
+
cookieNameSuffix: this.config.cookieNameSuffix,
|
|
280
288
|
};
|
|
281
289
|
}
|
|
282
290
|
|
|
@@ -412,6 +420,7 @@ export class BackendAuthClient<
|
|
|
412
420
|
const csrfCookie = generateCsrfCookie(userIdResult.csrfToken, {
|
|
413
421
|
...cookieParams,
|
|
414
422
|
hostOrigin: this.resolveCsrfCookieOrigin(cookieParams.hostOrigin),
|
|
423
|
+
cookieNameSuffix: this.config.cookieNameSuffix,
|
|
415
424
|
});
|
|
416
425
|
|
|
417
426
|
return {
|
|
@@ -489,12 +498,13 @@ export class BackendAuthClient<
|
|
|
489
498
|
*/
|
|
490
499
|
allowUserAuthRefresh: boolean;
|
|
491
500
|
}): Promise<GetUserResult<DatabaseUser> | undefined> {
|
|
492
|
-
const userIdResult = await extractUserIdFromRequestHeaders<UserId>(
|
|
493
|
-
requestHeaders,
|
|
494
|
-
await this.getJwtParams(),
|
|
495
|
-
this.config.csrf,
|
|
496
|
-
isSignUpCookie ? AuthCookie.SignUp : AuthCookie.Auth,
|
|
497
|
-
|
|
501
|
+
const userIdResult = await extractUserIdFromRequestHeaders<UserId>({
|
|
502
|
+
headers: requestHeaders,
|
|
503
|
+
jwtParams: await this.getJwtParams(),
|
|
504
|
+
csrfHeaderNameOption: this.config.csrf,
|
|
505
|
+
cookieName: isSignUpCookie ? AuthCookie.SignUp : AuthCookie.Auth,
|
|
506
|
+
cookieNameSuffix: this.config.cookieNameSuffix,
|
|
507
|
+
});
|
|
498
508
|
if (!userIdResult) {
|
|
499
509
|
this.logForUser(
|
|
500
510
|
{
|
|
@@ -556,6 +566,7 @@ export class BackendAuthClient<
|
|
|
556
566
|
const csrfCookie = generateCsrfCookie(userIdResult.csrfToken, {
|
|
557
567
|
hostOrigin: this.resolveCsrfCookieOrigin(authCookieOrigin),
|
|
558
568
|
isDev: this.config.isDev,
|
|
569
|
+
cookieNameSuffix: this.config.cookieNameSuffix,
|
|
559
570
|
});
|
|
560
571
|
|
|
561
572
|
return {
|
|
@@ -645,6 +656,7 @@ export class BackendAuthClient<
|
|
|
645
656
|
? clearCsrfCookie({
|
|
646
657
|
hostOrigin: this.config.csrfCookieOrigin,
|
|
647
658
|
isDev: this.config.isDev,
|
|
659
|
+
cookieNameSuffix: this.config.cookieNameSuffix,
|
|
648
660
|
})
|
|
649
661
|
: undefined;
|
|
650
662
|
|
|
@@ -682,6 +694,7 @@ export class BackendAuthClient<
|
|
|
682
694
|
const csrfCookie = generateCsrfCookie(existingUserIdResult.csrfToken, {
|
|
683
695
|
...cookieParams,
|
|
684
696
|
hostOrigin: this.resolveCsrfCookieOrigin(cookieParams.hostOrigin),
|
|
697
|
+
cookieNameSuffix: this.config.cookieNameSuffix,
|
|
685
698
|
});
|
|
686
699
|
|
|
687
700
|
return {
|
|
@@ -711,6 +724,7 @@ export class BackendAuthClient<
|
|
|
711
724
|
const csrfCookie = generateCsrfCookie(csrfToken, {
|
|
712
725
|
...cookieParams,
|
|
713
726
|
hostOrigin: this.resolveCsrfCookieOrigin(cookieParams.hostOrigin),
|
|
727
|
+
cookieNameSuffix: this.config.cookieNameSuffix,
|
|
714
728
|
});
|
|
715
729
|
|
|
716
730
|
return {
|
|
@@ -732,7 +746,13 @@ export class BackendAuthClient<
|
|
|
732
746
|
isSignUpCookie: boolean;
|
|
733
747
|
}): Promise<OutgoingHttpHeaders> {
|
|
734
748
|
const oppositeCookieName = isSignUpCookie ? AuthCookie.Auth : AuthCookie.SignUp;
|
|
735
|
-
const
|
|
749
|
+
const resolvedOppositeCookieName = resolveCookieName(
|
|
750
|
+
oppositeCookieName,
|
|
751
|
+
this.config.cookieNameSuffix,
|
|
752
|
+
);
|
|
753
|
+
const hasExistingOppositeCookie = requestHeaders.cookie?.includes(
|
|
754
|
+
`${resolvedOppositeCookieName}=`,
|
|
755
|
+
);
|
|
736
756
|
|
|
737
757
|
const discardOppositeCookieHeaders = hasExistingOppositeCookie
|
|
738
758
|
? generateLogoutHeaders(
|
|
@@ -746,12 +766,13 @@ export class BackendAuthClient<
|
|
|
746
766
|
)
|
|
747
767
|
: undefined;
|
|
748
768
|
|
|
749
|
-
const existingUserIdResult = await extractUserIdFromRequestHeaders<UserId>(
|
|
750
|
-
requestHeaders,
|
|
751
|
-
await this.getJwtParams(),
|
|
752
|
-
this.config.csrf,
|
|
753
|
-
isSignUpCookie ? AuthCookie.SignUp : AuthCookie.Auth,
|
|
754
|
-
|
|
769
|
+
const existingUserIdResult = await extractUserIdFromRequestHeaders<UserId>({
|
|
770
|
+
headers: requestHeaders,
|
|
771
|
+
jwtParams: await this.getJwtParams(),
|
|
772
|
+
csrfHeaderNameOption: this.config.csrf,
|
|
773
|
+
cookieName: isSignUpCookie ? AuthCookie.SignUp : AuthCookie.Auth,
|
|
774
|
+
cookieNameSuffix: this.config.cookieNameSuffix,
|
|
775
|
+
});
|
|
755
776
|
|
|
756
777
|
const cookieParams = await this.getCookieParams({
|
|
757
778
|
isSignUpCookie,
|
|
@@ -840,11 +861,12 @@ export class BackendAuthClient<
|
|
|
840
861
|
allowUserAuthRefresh: boolean;
|
|
841
862
|
}): Promise<GetUserResult<DatabaseUser> | undefined> {
|
|
842
863
|
// eslint-disable-next-line @typescript-eslint/no-deprecated
|
|
843
|
-
const userIdResult = await insecureExtractUserIdFromCookieAlone<UserId>(
|
|
844
|
-
requestHeaders,
|
|
845
|
-
await this.getJwtParams(),
|
|
846
|
-
AuthCookie.Auth,
|
|
847
|
-
|
|
864
|
+
const userIdResult = await insecureExtractUserIdFromCookieAlone<UserId>({
|
|
865
|
+
headers: requestHeaders,
|
|
866
|
+
jwtParams: await this.getJwtParams(),
|
|
867
|
+
cookieName: AuthCookie.Auth,
|
|
868
|
+
cookieNameSuffix: this.config.cookieNameSuffix,
|
|
869
|
+
});
|
|
848
870
|
|
|
849
871
|
if (!userIdResult) {
|
|
850
872
|
this.logForUser(
|
|
@@ -25,6 +25,11 @@ export type FrontendAuthClientConfig = Readonly<{
|
|
|
25
25
|
csrf: Readonly<CsrfHeaderNameOption>;
|
|
26
26
|
}> &
|
|
27
27
|
PartialWithUndefined<{
|
|
28
|
+
/**
|
|
29
|
+
* Optional suffix appended to cookie names (e.g., `'staging'` produces
|
|
30
|
+
* `auth-vir-csrf-staging`). When `undefined`, cookie names are unchanged.
|
|
31
|
+
*/
|
|
32
|
+
cookieNameSuffix: string;
|
|
28
33
|
/**
|
|
29
34
|
* Determine if the current user can assume the identity of another user. If this is not
|
|
30
35
|
* defined, all users will be blocked from assuming other user identities.
|
|
@@ -163,7 +168,7 @@ export class FrontendAuthClient<AssumedUserParams extends JsonCompatibleObject =
|
|
|
163
168
|
* combine them with these.
|
|
164
169
|
*/
|
|
165
170
|
public createAuthenticatedRequestInit(): RequestInit {
|
|
166
|
-
const csrfToken = getCurrentCsrfToken();
|
|
171
|
+
const csrfToken = getCurrentCsrfToken(this.config.cookieNameSuffix);
|
|
167
172
|
|
|
168
173
|
const assumedUser = this.getAssumedUser();
|
|
169
174
|
const headers: HeadersInit = {
|
package/src/auth.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import {type SelectFrom} from '@augment-vir/common';
|
|
1
|
+
import {type PartialWithUndefined, type SelectFrom} from '@augment-vir/common';
|
|
2
2
|
import {type FullDate, type UtcTimezone} from 'date-vir';
|
|
3
3
|
import {
|
|
4
|
-
AuthCookie,
|
|
4
|
+
type AuthCookie,
|
|
5
5
|
clearAuthCookie,
|
|
6
6
|
clearCsrfCookie,
|
|
7
7
|
type CookieParams,
|
|
@@ -71,12 +71,19 @@ function readCsrfTokenHeader(
|
|
|
71
71
|
* @category Auth : Host
|
|
72
72
|
* @returns The extracted user id or `undefined` if no valid auth headers exist.
|
|
73
73
|
*/
|
|
74
|
-
export async function extractUserIdFromRequestHeaders<UserId extends string | number>(
|
|
75
|
-
headers
|
|
76
|
-
jwtParams
|
|
77
|
-
csrfHeaderNameOption
|
|
78
|
-
cookieName
|
|
79
|
-
|
|
74
|
+
export async function extractUserIdFromRequestHeaders<UserId extends string | number>({
|
|
75
|
+
headers,
|
|
76
|
+
jwtParams,
|
|
77
|
+
csrfHeaderNameOption,
|
|
78
|
+
cookieName,
|
|
79
|
+
cookieNameSuffix,
|
|
80
|
+
}: Readonly<{
|
|
81
|
+
headers: HeaderContainer;
|
|
82
|
+
jwtParams: Readonly<ParseJwtParams>;
|
|
83
|
+
csrfHeaderNameOption: Readonly<CsrfHeaderNameOption>;
|
|
84
|
+
cookieName: AuthCookie;
|
|
85
|
+
cookieNameSuffix?: string | undefined;
|
|
86
|
+
}>): Promise<Readonly<UserIdResult<UserId>> | undefined> {
|
|
80
87
|
try {
|
|
81
88
|
const csrfToken = readCsrfTokenHeader(headers, csrfHeaderNameOption);
|
|
82
89
|
const cookie = readHeader(headers, 'cookie');
|
|
@@ -85,7 +92,12 @@ export async function extractUserIdFromRequestHeaders<UserId extends string | nu
|
|
|
85
92
|
return undefined;
|
|
86
93
|
}
|
|
87
94
|
|
|
88
|
-
const jwt = await extractCookieJwt(
|
|
95
|
+
const jwt = await extractCookieJwt({
|
|
96
|
+
rawCookie: cookie,
|
|
97
|
+
jwtParams,
|
|
98
|
+
cookieName,
|
|
99
|
+
cookieNameSuffix,
|
|
100
|
+
});
|
|
89
101
|
|
|
90
102
|
if (!jwt || jwt.data.csrfToken !== csrfToken) {
|
|
91
103
|
return undefined;
|
|
@@ -112,11 +124,17 @@ export async function extractUserIdFromRequestHeaders<UserId extends string | nu
|
|
|
112
124
|
* @deprecated Prefer {@link extractUserIdFromRequestHeaders} instead: it is more secure.
|
|
113
125
|
* @category Auth : Host
|
|
114
126
|
*/
|
|
115
|
-
export async function insecureExtractUserIdFromCookieAlone<UserId extends string | number>(
|
|
116
|
-
headers
|
|
117
|
-
jwtParams
|
|
118
|
-
cookieName
|
|
119
|
-
|
|
127
|
+
export async function insecureExtractUserIdFromCookieAlone<UserId extends string | number>({
|
|
128
|
+
headers,
|
|
129
|
+
jwtParams,
|
|
130
|
+
cookieName,
|
|
131
|
+
cookieNameSuffix,
|
|
132
|
+
}: Readonly<{
|
|
133
|
+
headers: HeaderContainer;
|
|
134
|
+
jwtParams: Readonly<ParseJwtParams>;
|
|
135
|
+
cookieName: AuthCookie;
|
|
136
|
+
cookieNameSuffix?: string | undefined;
|
|
137
|
+
}>): Promise<Readonly<UserIdResult<UserId>> | undefined> {
|
|
120
138
|
try {
|
|
121
139
|
const cookie = readHeader(headers, 'cookie');
|
|
122
140
|
|
|
@@ -124,7 +142,12 @@ export async function insecureExtractUserIdFromCookieAlone<UserId extends string
|
|
|
124
142
|
return undefined;
|
|
125
143
|
}
|
|
126
144
|
|
|
127
|
-
const jwt = await extractCookieJwt(
|
|
145
|
+
const jwt = await extractCookieJwt({
|
|
146
|
+
rawCookie: cookie,
|
|
147
|
+
jwtParams,
|
|
148
|
+
cookieName,
|
|
149
|
+
cookieNameSuffix,
|
|
150
|
+
});
|
|
128
151
|
|
|
129
152
|
if (!jwt) {
|
|
130
153
|
return undefined;
|
|
@@ -188,15 +211,18 @@ export async function generateSuccessfulLoginHeaders(
|
|
|
188
211
|
* @category Auth : Host
|
|
189
212
|
*/
|
|
190
213
|
export function generateLogoutHeaders(
|
|
191
|
-
cookieConfig: Readonly<SelectFrom<CookieParams, {hostOrigin: true; isDev: true}
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
214
|
+
cookieConfig: Readonly<SelectFrom<CookieParams, {hostOrigin: true; isDev: true}>> &
|
|
215
|
+
PartialWithUndefined<{cookieNameSuffix: string}>,
|
|
216
|
+
options?: Readonly<
|
|
217
|
+
PartialWithUndefined<{
|
|
218
|
+
/**
|
|
219
|
+
* When `true`, the CSRF cookie is preserved (not cleared). Use this when clearing only
|
|
220
|
+
* one cookie type (e.g., the auth cookie) while keeping the other active session (e.g.,
|
|
221
|
+
* sign-up) that still needs its CSRF token.
|
|
222
|
+
*/
|
|
223
|
+
preserveCsrf: boolean;
|
|
224
|
+
}>
|
|
225
|
+
>,
|
|
200
226
|
): Record<string, string[]> {
|
|
201
227
|
return {
|
|
202
228
|
'set-cookie': [
|
package/src/cookie.ts
CHANGED
|
@@ -25,6 +25,24 @@ export enum AuthCookie {
|
|
|
25
25
|
Csrf = 'auth-vir-csrf',
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
/**
|
|
29
|
+
* Resolves a cookie name by appending a suffix when provided. When `cookieNameSuffix` is
|
|
30
|
+
* `undefined`, the base name is returned unchanged.
|
|
31
|
+
*
|
|
32
|
+
* @category Internal
|
|
33
|
+
*/
|
|
34
|
+
export function resolveCookieName(
|
|
35
|
+
baseCookieName: AuthCookie,
|
|
36
|
+
cookieNameSuffix?: string | undefined,
|
|
37
|
+
): string {
|
|
38
|
+
return [
|
|
39
|
+
baseCookieName,
|
|
40
|
+
cookieNameSuffix,
|
|
41
|
+
]
|
|
42
|
+
.filter(check.isTruthy)
|
|
43
|
+
.join('-');
|
|
44
|
+
}
|
|
45
|
+
|
|
28
46
|
/**
|
|
29
47
|
* Parameters for {@link generateAuthCookie}.
|
|
30
48
|
*
|
|
@@ -63,6 +81,12 @@ export type CookieParams = {
|
|
|
63
81
|
* @default false
|
|
64
82
|
*/
|
|
65
83
|
isDev: boolean;
|
|
84
|
+
/**
|
|
85
|
+
* Optional suffix appended to cookie names (e.g., `'staging'` produces `auth-staging`). When
|
|
86
|
+
* `undefined`, cookie names are unchanged. Useful for running multiple environments on the same
|
|
87
|
+
* domain without cookie collisions.
|
|
88
|
+
*/
|
|
89
|
+
cookieNameSuffix: string;
|
|
66
90
|
}>;
|
|
67
91
|
|
|
68
92
|
function generateSetCookie({
|
|
@@ -102,7 +126,10 @@ export async function generateAuthCookie(
|
|
|
102
126
|
cookieConfig: Readonly<CookieParams>,
|
|
103
127
|
): Promise<string> {
|
|
104
128
|
return generateSetCookie({
|
|
105
|
-
name:
|
|
129
|
+
name: resolveCookieName(
|
|
130
|
+
cookieConfig.authCookie || AuthCookie.Auth,
|
|
131
|
+
cookieConfig.cookieNameSuffix,
|
|
132
|
+
),
|
|
106
133
|
value: await createUserJwt(userJwtData, cookieConfig.jwtParams),
|
|
107
134
|
httpOnly: true,
|
|
108
135
|
cookieConfig,
|
|
@@ -122,10 +149,11 @@ export async function generateAuthCookie(
|
|
|
122
149
|
*/
|
|
123
150
|
export function generateCsrfCookie(
|
|
124
151
|
csrfToken: string,
|
|
125
|
-
cookieConfig: Readonly<SelectFrom<CookieParams, {hostOrigin: true; isDev: true}
|
|
152
|
+
cookieConfig: Readonly<SelectFrom<CookieParams, {hostOrigin: true; isDev: true}>> &
|
|
153
|
+
PartialWithUndefined<{cookieNameSuffix: string}>,
|
|
126
154
|
): string {
|
|
127
155
|
return generateSetCookie({
|
|
128
|
-
name: AuthCookie.Csrf,
|
|
156
|
+
name: resolveCookieName(AuthCookie.Csrf, cookieConfig.cookieNameSuffix),
|
|
129
157
|
value: csrfToken,
|
|
130
158
|
httpOnly: false,
|
|
131
159
|
cookieConfig: {
|
|
@@ -144,10 +172,13 @@ export function generateCsrfCookie(
|
|
|
144
172
|
*/
|
|
145
173
|
export function clearAuthCookie(
|
|
146
174
|
cookieConfig: Readonly<SelectFrom<CookieParams, {hostOrigin: true; isDev: true}>> &
|
|
147
|
-
PartialWithUndefined<{authCookie: AuthCookie}>,
|
|
175
|
+
PartialWithUndefined<{authCookie: AuthCookie; cookieNameSuffix: string}>,
|
|
148
176
|
) {
|
|
149
177
|
return generateSetCookie({
|
|
150
|
-
name:
|
|
178
|
+
name: resolveCookieName(
|
|
179
|
+
cookieConfig.authCookie || AuthCookie.Auth,
|
|
180
|
+
cookieConfig.cookieNameSuffix,
|
|
181
|
+
),
|
|
151
182
|
value: 'redacted',
|
|
152
183
|
httpOnly: true,
|
|
153
184
|
cookieConfig,
|
|
@@ -160,10 +191,11 @@ export function clearAuthCookie(
|
|
|
160
191
|
* @category Internal
|
|
161
192
|
*/
|
|
162
193
|
export function clearCsrfCookie(
|
|
163
|
-
cookieConfig: Readonly<SelectFrom<CookieParams, {hostOrigin: true; isDev: true}
|
|
194
|
+
cookieConfig: Readonly<SelectFrom<CookieParams, {hostOrigin: true; isDev: true}>> &
|
|
195
|
+
PartialWithUndefined<{cookieNameSuffix: string}>,
|
|
164
196
|
) {
|
|
165
197
|
return generateSetCookie({
|
|
166
|
-
name: AuthCookie.Csrf,
|
|
198
|
+
name: resolveCookieName(AuthCookie.Csrf, cookieConfig.cookieNameSuffix),
|
|
167
199
|
value: 'redacted',
|
|
168
200
|
httpOnly: false,
|
|
169
201
|
cookieConfig,
|
|
@@ -206,12 +238,20 @@ export function generateCookie(
|
|
|
206
238
|
* @category Internal
|
|
207
239
|
* @returns The extracted auth Cookie JWT data or `undefined` if no valid auth JWT data was found.
|
|
208
240
|
*/
|
|
209
|
-
export async function extractCookieJwt(
|
|
210
|
-
rawCookie
|
|
211
|
-
jwtParams
|
|
212
|
-
cookieName
|
|
213
|
-
|
|
214
|
-
|
|
241
|
+
export async function extractCookieJwt({
|
|
242
|
+
rawCookie,
|
|
243
|
+
jwtParams,
|
|
244
|
+
cookieName,
|
|
245
|
+
cookieNameSuffix,
|
|
246
|
+
}: {
|
|
247
|
+
rawCookie: string;
|
|
248
|
+
jwtParams: Readonly<ParseJwtParams>;
|
|
249
|
+
cookieName: AuthCookie;
|
|
250
|
+
} & PartialWithUndefined<{
|
|
251
|
+
cookieNameSuffix: string;
|
|
252
|
+
}>): Promise<undefined | ParsedJwt<JwtUserData>> {
|
|
253
|
+
const resolvedName = resolveCookieName(cookieName, cookieNameSuffix);
|
|
254
|
+
const cookieRegExp = new RegExp(`${escapeStringForRegExp(resolvedName)}=[^;]+(?:;|$)`);
|
|
215
255
|
|
|
216
256
|
const [cookieValue] = safeMatch(rawCookie, cookieRegExp);
|
|
217
257
|
|
|
@@ -219,7 +259,7 @@ export async function extractCookieJwt(
|
|
|
219
259
|
return undefined;
|
|
220
260
|
}
|
|
221
261
|
|
|
222
|
-
const rawJwt = cookieValue.replace(`${
|
|
262
|
+
const rawJwt = cookieValue.replace(`${resolvedName}=`, '').replace(';', '');
|
|
223
263
|
|
|
224
264
|
const jwt = await parseUserJwt(rawJwt, jwtParams);
|
|
225
265
|
|
package/src/csrf-token.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {check} from '@augment-vir/assert';
|
|
2
2
|
import {escapeStringForRegExp, randomString, safeMatch} from '@augment-vir/common';
|
|
3
3
|
import {type RequireExactlyOne} from 'type-fest';
|
|
4
|
-
import {AuthCookie} from './cookie.js';
|
|
4
|
+
import {AuthCookie, resolveCookieName} from './cookie.js';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Generates a random, cryptographically secure CSRF token string.
|
|
@@ -51,8 +51,9 @@ export function resolveCsrfHeaderName(options: Readonly<CsrfHeaderNameOption>):
|
|
|
51
51
|
*
|
|
52
52
|
* @category Auth : Client
|
|
53
53
|
*/
|
|
54
|
-
export function getCurrentCsrfToken(): string | undefined {
|
|
55
|
-
const
|
|
54
|
+
export function getCurrentCsrfToken(cookieNameSuffix?: string | undefined): string | undefined {
|
|
55
|
+
const resolvedName = resolveCookieName(AuthCookie.Csrf, cookieNameSuffix);
|
|
56
|
+
const cookieRegExp = new RegExp(`${escapeStringForRegExp(resolvedName)}=([^;]+)`);
|
|
56
57
|
const [
|
|
57
58
|
,
|
|
58
59
|
value,
|