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