auth-vir 4.0.0 → 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 +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 +1 -2
- 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/src/auth.ts
CHANGED
|
@@ -1,20 +1,15 @@
|
|
|
1
|
-
import {type
|
|
1
|
+
import {type SelectFrom} from '@augment-vir/common';
|
|
2
2
|
import {type FullDate, type UtcTimezone} from 'date-vir';
|
|
3
3
|
import {
|
|
4
|
-
|
|
4
|
+
AuthCookie,
|
|
5
5
|
clearAuthCookie,
|
|
6
|
+
clearCsrfCookie,
|
|
6
7
|
type CookieParams,
|
|
7
8
|
extractCookieJwt,
|
|
8
9
|
generateAuthCookie,
|
|
10
|
+
generateCsrfCookie,
|
|
9
11
|
} from './cookie.js';
|
|
10
|
-
import {type
|
|
11
|
-
import {
|
|
12
|
-
type CsrfHeaderNameOption,
|
|
13
|
-
extractCsrfTokenHeader,
|
|
14
|
-
generateCsrfToken,
|
|
15
|
-
resolveCsrfHeaderName,
|
|
16
|
-
storeCsrfToken,
|
|
17
|
-
} from './csrf-token.js';
|
|
12
|
+
import {type CsrfHeaderNameOption, generateCsrfToken, resolveCsrfHeaderName} from './csrf-token.js';
|
|
18
13
|
import {type ParseJwtParams} from './jwt/jwt.js';
|
|
19
14
|
import {type JwtUserData} from './jwt/user-jwt.js';
|
|
20
15
|
|
|
@@ -51,7 +46,7 @@ export type UserIdResult<UserId extends string | number> = {
|
|
|
51
46
|
jwtExpiration: FullDate<UtcTimezone>;
|
|
52
47
|
/** When the JWT was issued (`iat` claim). */
|
|
53
48
|
jwtIssuedAt: FullDate<UtcTimezone>;
|
|
54
|
-
cookieName:
|
|
49
|
+
cookieName: AuthCookie;
|
|
55
50
|
/** The CSRF token embedded in the JWT. */
|
|
56
51
|
csrfToken: string;
|
|
57
52
|
/**
|
|
@@ -80,7 +75,7 @@ export async function extractUserIdFromRequestHeaders<UserId extends string | nu
|
|
|
80
75
|
headers: HeaderContainer,
|
|
81
76
|
jwtParams: Readonly<ParseJwtParams>,
|
|
82
77
|
csrfHeaderNameOption: Readonly<CsrfHeaderNameOption>,
|
|
83
|
-
cookieName:
|
|
78
|
+
cookieName: AuthCookie = AuthCookie.Auth,
|
|
84
79
|
): Promise<Readonly<UserIdResult<UserId>> | undefined> {
|
|
85
80
|
try {
|
|
86
81
|
const csrfToken = readCsrfTokenHeader(headers, csrfHeaderNameOption);
|
|
@@ -120,7 +115,7 @@ export async function extractUserIdFromRequestHeaders<UserId extends string | nu
|
|
|
120
115
|
export async function insecureExtractUserIdFromCookieAlone<UserId extends string | number>(
|
|
121
116
|
headers: HeaderContainer,
|
|
122
117
|
jwtParams: Readonly<ParseJwtParams>,
|
|
123
|
-
cookieName:
|
|
118
|
+
cookieName: AuthCookie,
|
|
124
119
|
): Promise<Readonly<UserIdResult<UserId>> | undefined> {
|
|
125
120
|
try {
|
|
126
121
|
const cookie = readHeader(headers, 'cookie');
|
|
@@ -149,7 +144,9 @@ export async function insecureExtractUserIdFromCookieAlone<UserId extends string
|
|
|
149
144
|
}
|
|
150
145
|
|
|
151
146
|
/**
|
|
152
|
-
* Used by host (backend) code to set headers on a response object.
|
|
147
|
+
* Used by host (backend) code to set headers on a response object. Sets both the auth JWT cookie
|
|
148
|
+
* and the CSRF token cookie. The CSRF cookie is not `HttpOnly` so that frontend JavaScript can read
|
|
149
|
+
* it and inject the value as a request header.
|
|
153
150
|
*
|
|
154
151
|
* @category Auth : Host
|
|
155
152
|
*/
|
|
@@ -157,17 +154,15 @@ export async function generateSuccessfulLoginHeaders(
|
|
|
157
154
|
/** The id from your database of the user you're authenticating. */
|
|
158
155
|
userId: string | number,
|
|
159
156
|
cookieConfig: Readonly<CookieParams>,
|
|
160
|
-
csrfHeaderNameOption: Readonly<CsrfHeaderNameOption>,
|
|
161
157
|
/**
|
|
162
158
|
* The timestamp (in seconds) when the session originally started. If not provided, the current
|
|
163
159
|
* time will be used (for new sessions).
|
|
164
160
|
*/
|
|
165
161
|
sessionStartedAt?: number | undefined,
|
|
166
|
-
): Promise<Record<string, string>> {
|
|
162
|
+
): Promise<Record<string, string[]>> {
|
|
167
163
|
const csrfToken = generateCsrfToken();
|
|
168
|
-
const csrfHeaderName = resolveCsrfHeaderName(csrfHeaderNameOption);
|
|
169
164
|
|
|
170
|
-
const
|
|
165
|
+
const authCookie = await generateAuthCookie(
|
|
171
166
|
{
|
|
172
167
|
csrfToken,
|
|
173
168
|
userId,
|
|
@@ -176,9 +171,13 @@ export async function generateSuccessfulLoginHeaders(
|
|
|
176
171
|
cookieConfig,
|
|
177
172
|
);
|
|
178
173
|
|
|
174
|
+
const csrfCookie = generateCsrfCookie(csrfToken, cookieConfig);
|
|
175
|
+
|
|
179
176
|
return {
|
|
180
|
-
'set-cookie':
|
|
181
|
-
|
|
177
|
+
'set-cookie': [
|
|
178
|
+
authCookie,
|
|
179
|
+
csrfCookie,
|
|
180
|
+
],
|
|
182
181
|
};
|
|
183
182
|
}
|
|
184
183
|
|
|
@@ -189,43 +188,12 @@ export async function generateSuccessfulLoginHeaders(
|
|
|
189
188
|
* @category Auth : Host
|
|
190
189
|
*/
|
|
191
190
|
export function generateLogoutHeaders(
|
|
192
|
-
cookieConfig: Readonly<
|
|
193
|
-
): Record<string, string> {
|
|
191
|
+
cookieConfig: Readonly<SelectFrom<CookieParams, {hostOrigin: true; isDev: true}>>,
|
|
192
|
+
): Record<string, string[]> {
|
|
194
193
|
return {
|
|
195
|
-
'set-cookie':
|
|
194
|
+
'set-cookie': [
|
|
195
|
+
clearAuthCookie(cookieConfig),
|
|
196
|
+
clearCsrfCookie(cookieConfig),
|
|
197
|
+
],
|
|
196
198
|
};
|
|
197
199
|
}
|
|
198
|
-
|
|
199
|
-
/**
|
|
200
|
-
* Store auth data on a client (frontend) after receiving an auth response from the host (backend).
|
|
201
|
-
* Specifically, this stores the CSRF token into IndexedDB (which doesn't need to be a secret).
|
|
202
|
-
* Alternatively, if the given response failed, this will wipe the existing (if any) stored CSRF
|
|
203
|
-
* token.
|
|
204
|
-
*
|
|
205
|
-
* @category Auth : Client
|
|
206
|
-
* @throws Error if no CSRF token header is found.
|
|
207
|
-
*/
|
|
208
|
-
export async function handleAuthResponse(
|
|
209
|
-
response: Readonly<Pick<Response, 'ok' | 'headers'>>,
|
|
210
|
-
options: Readonly<CsrfHeaderNameOption> &
|
|
211
|
-
PartialWithUndefined<{
|
|
212
|
-
/**
|
|
213
|
-
* Allows mocking or overriding the default CSRF token store.
|
|
214
|
-
*
|
|
215
|
-
* @default getDefaultCsrfTokenStore()
|
|
216
|
-
*/
|
|
217
|
-
csrfTokenStore: CsrfTokenStore;
|
|
218
|
-
}>,
|
|
219
|
-
): Promise<void> {
|
|
220
|
-
if (!response.ok) {
|
|
221
|
-
return;
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
const csrfToken = extractCsrfTokenHeader(response, options);
|
|
225
|
-
|
|
226
|
-
if (!csrfToken) {
|
|
227
|
-
throw new Error('Did not receive any CSRF token.');
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
await storeCsrfToken(csrfToken, options);
|
|
231
|
-
}
|
package/src/cookie.ts
CHANGED
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
import {check} from '@augment-vir/assert';
|
|
2
|
-
import {escapeStringForRegExp, safeMatch, type PartialWithUndefined} from '@augment-vir/common';
|
|
3
2
|
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
type
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
} from 'date-vir';
|
|
3
|
+
escapeStringForRegExp,
|
|
4
|
+
safeMatch,
|
|
5
|
+
type PartialWithUndefined,
|
|
6
|
+
type SelectFrom,
|
|
7
|
+
} from '@augment-vir/common';
|
|
8
|
+
import {convertDuration, type AnyDuration} from 'date-vir';
|
|
11
9
|
import {type Primitive} from 'type-fest';
|
|
12
10
|
import {parseUrl} from 'url-vir';
|
|
13
11
|
import {type CreateJwtParams, type ParseJwtParams, type ParsedJwt} from './jwt/jwt.js';
|
|
@@ -18,11 +16,13 @@ import {createUserJwt, parseUserJwt, type JwtUserData} from './jwt/user-jwt.js';
|
|
|
18
16
|
*
|
|
19
17
|
* @category Internal
|
|
20
18
|
*/
|
|
21
|
-
export enum
|
|
19
|
+
export enum AuthCookie {
|
|
22
20
|
/** Used for a full user login auth. */
|
|
23
21
|
Auth = 'auth',
|
|
24
22
|
/** Use for a temporary "just signed up" auth. */
|
|
25
23
|
SignUp = 'sign-up',
|
|
24
|
+
/** Used for storing the CSRF token. Not `HttpOnly` so that frontend JS can read it. */
|
|
25
|
+
Csrf = 'auth-vir-csrf',
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
/**
|
|
@@ -49,8 +49,13 @@ export type CookieParams = {
|
|
|
49
49
|
* client, etc.
|
|
50
50
|
*/
|
|
51
51
|
jwtParams: Readonly<CreateJwtParams>;
|
|
52
|
-
cookieName?: string;
|
|
53
52
|
} & PartialWithUndefined<{
|
|
53
|
+
/**
|
|
54
|
+
* Which auth cookie name to use.
|
|
55
|
+
*
|
|
56
|
+
* @default AuthCookie.Auth
|
|
57
|
+
*/
|
|
58
|
+
authCookie: AuthCookie;
|
|
54
59
|
/**
|
|
55
60
|
* Is set to `true` (which should only be done in development environments), the cookie will be
|
|
56
61
|
* allowed in insecure requests (non HTTPS requests).
|
|
@@ -60,15 +65,32 @@ export type CookieParams = {
|
|
|
60
65
|
isDev: boolean;
|
|
61
66
|
}>;
|
|
62
67
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
68
|
+
function generateSetCookie({
|
|
69
|
+
name,
|
|
70
|
+
value,
|
|
71
|
+
httpOnly,
|
|
72
|
+
cookieConfig,
|
|
73
|
+
}: {
|
|
74
|
+
name: string;
|
|
75
|
+
value: string;
|
|
76
|
+
httpOnly: boolean;
|
|
77
|
+
cookieConfig: Readonly<SelectFrom<CookieParams, {hostOrigin: true; isDev: true}>> &
|
|
78
|
+
PartialWithUndefined<SelectFrom<CookieParams, {cookieDuration: true}>>;
|
|
79
|
+
}): string {
|
|
80
|
+
return generateCookie({
|
|
81
|
+
[name]: value,
|
|
82
|
+
Domain: parseUrl(cookieConfig.hostOrigin).hostname,
|
|
83
|
+
HttpOnly: httpOnly,
|
|
84
|
+
Path: '/',
|
|
85
|
+
SameSite: 'Strict',
|
|
86
|
+
'MAX-AGE': cookieConfig.cookieDuration
|
|
87
|
+
? convertDuration(cookieConfig.cookieDuration, {
|
|
88
|
+
seconds: true,
|
|
89
|
+
}).seconds
|
|
90
|
+
: 0,
|
|
91
|
+
Secure: !cookieConfig.isDev,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
72
94
|
|
|
73
95
|
/**
|
|
74
96
|
* Generate a secure cookie that stores the user JWT data. Used in host (backend) code.
|
|
@@ -78,26 +100,41 @@ export type GenerateAuthCookieResult = {
|
|
|
78
100
|
export async function generateAuthCookie(
|
|
79
101
|
userJwtData: Readonly<JwtUserData>,
|
|
80
102
|
cookieConfig: Readonly<CookieParams>,
|
|
81
|
-
): Promise<
|
|
82
|
-
|
|
103
|
+
): Promise<string> {
|
|
104
|
+
return generateSetCookie({
|
|
105
|
+
name: cookieConfig.authCookie || AuthCookie.Auth,
|
|
106
|
+
value: await createUserJwt(userJwtData, cookieConfig.jwtParams),
|
|
107
|
+
httpOnly: true,
|
|
108
|
+
cookieConfig,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
83
111
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
112
|
+
/**
|
|
113
|
+
* Generate a CSRF token cookie. This cookie is intentionally not `HttpOnly` so that frontend
|
|
114
|
+
* JavaScript can read it and inject the value as a request header for double-submit verification.
|
|
115
|
+
*
|
|
116
|
+
* The CSRF cookie uses a fixed 400-day MAX-AGE rather than matching the auth cookie duration. 400
|
|
117
|
+
* days is the cross-browser safe maximum (Chrome caps cookie lifetimes at 400 days; other browsers
|
|
118
|
+
* accept it as-is). The CSRF token is only meaningful when paired with a valid JWT, so it doesn't
|
|
119
|
+
* need its own expiration management. It gets regenerated on every fresh login.
|
|
120
|
+
*
|
|
121
|
+
* @category Internal
|
|
122
|
+
*/
|
|
123
|
+
export function generateCsrfCookie(
|
|
124
|
+
csrfToken: string,
|
|
125
|
+
cookieConfig: Readonly<SelectFrom<CookieParams, {hostOrigin: true; isDev: true}>>,
|
|
126
|
+
): string {
|
|
127
|
+
return generateSetCookie({
|
|
128
|
+
name: AuthCookie.Csrf,
|
|
129
|
+
value: csrfToken,
|
|
130
|
+
httpOnly: false,
|
|
131
|
+
cookieConfig: {
|
|
132
|
+
...cookieConfig,
|
|
133
|
+
cookieDuration: {
|
|
134
|
+
days: 400,
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
});
|
|
101
138
|
}
|
|
102
139
|
|
|
103
140
|
/**
|
|
@@ -106,16 +143,30 @@ export async function generateAuthCookie(
|
|
|
106
143
|
* @category Internal
|
|
107
144
|
*/
|
|
108
145
|
export function clearAuthCookie(
|
|
109
|
-
cookieConfig: Readonly<
|
|
146
|
+
cookieConfig: Readonly<SelectFrom<CookieParams, {hostOrigin: true; isDev: true}>> &
|
|
147
|
+
PartialWithUndefined<{authCookie: AuthCookie}>,
|
|
110
148
|
) {
|
|
111
|
-
return
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
149
|
+
return generateSetCookie({
|
|
150
|
+
name: cookieConfig.authCookie || AuthCookie.Auth,
|
|
151
|
+
value: 'redacted',
|
|
152
|
+
httpOnly: true,
|
|
153
|
+
cookieConfig,
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Generate a cookie value that will clear the CSRF token cookie. Use this when signing out.
|
|
159
|
+
*
|
|
160
|
+
* @category Internal
|
|
161
|
+
*/
|
|
162
|
+
export function clearCsrfCookie(
|
|
163
|
+
cookieConfig: Readonly<SelectFrom<CookieParams, {hostOrigin: true; isDev: true}>>,
|
|
164
|
+
) {
|
|
165
|
+
return generateSetCookie({
|
|
166
|
+
name: AuthCookie.Csrf,
|
|
167
|
+
value: 'redacted',
|
|
168
|
+
httpOnly: false,
|
|
169
|
+
cookieConfig,
|
|
119
170
|
});
|
|
120
171
|
}
|
|
121
172
|
|
|
@@ -158,7 +209,7 @@ export function generateCookie(
|
|
|
158
209
|
export async function extractCookieJwt(
|
|
159
210
|
rawCookie: string,
|
|
160
211
|
jwtParams: Readonly<ParseJwtParams>,
|
|
161
|
-
cookieName:
|
|
212
|
+
cookieName: AuthCookie,
|
|
162
213
|
): Promise<undefined | ParsedJwt<JwtUserData>> {
|
|
163
214
|
const cookieRegExp = new RegExp(`${escapeStringForRegExp(cookieName)}=[^;]+(?:;|$)`);
|
|
164
215
|
|
package/src/csrf-token.ts
CHANGED
|
@@ -1,18 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import {check} from '@augment-vir/assert';
|
|
2
|
+
import {escapeStringForRegExp, randomString, safeMatch} from '@augment-vir/common';
|
|
3
3
|
import {type RequireExactlyOne} from 'type-fest';
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Default allowed clock skew for JWT expiration checks. Accounts for differences between server and
|
|
8
|
-
* client clocks.
|
|
9
|
-
*
|
|
10
|
-
* @category Internal
|
|
11
|
-
* @default {minutes: 5}
|
|
12
|
-
*/
|
|
13
|
-
export const defaultAllowedClockSkew: Readonly<AnyDuration> = {
|
|
14
|
-
minutes: 5,
|
|
15
|
-
};
|
|
4
|
+
import {AuthCookie} from './cookie.js';
|
|
16
5
|
|
|
17
6
|
/**
|
|
18
7
|
* Generates a random, cryptographically secure CSRF token string.
|
|
@@ -42,91 +31,31 @@ export type CsrfHeaderNameOption = RequireExactlyOne<{
|
|
|
42
31
|
* @category Auth : Client
|
|
43
32
|
* @category Auth : Host
|
|
44
33
|
*/
|
|
45
|
-
export function resolveCsrfHeaderName(
|
|
46
|
-
if ('csrfHeaderName' in
|
|
47
|
-
return
|
|
34
|
+
export function resolveCsrfHeaderName(options: Readonly<CsrfHeaderNameOption>): string {
|
|
35
|
+
if ('csrfHeaderName' in options && options.csrfHeaderName) {
|
|
36
|
+
return options.csrfHeaderName;
|
|
48
37
|
} else {
|
|
49
38
|
return [
|
|
50
|
-
|
|
39
|
+
options.csrfHeaderPrefix,
|
|
51
40
|
'auth-vir',
|
|
52
41
|
'csrf-token',
|
|
53
|
-
]
|
|
42
|
+
]
|
|
43
|
+
.filter(check.isTruthy)
|
|
44
|
+
.join('-');
|
|
54
45
|
}
|
|
55
46
|
}
|
|
56
47
|
|
|
57
48
|
/**
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
* @category Auth : Client
|
|
61
|
-
*/
|
|
62
|
-
export function extractCsrfTokenHeader(
|
|
63
|
-
response: Readonly<PartialWithUndefined<SelectFrom<Response, {headers: true}>>>,
|
|
64
|
-
csrfHeaderNameOption: Readonly<CsrfHeaderNameOption>,
|
|
65
|
-
): string | undefined {
|
|
66
|
-
const csrfTokenHeaderName = resolveCsrfHeaderName(csrfHeaderNameOption);
|
|
67
|
-
|
|
68
|
-
return response.headers?.get(csrfTokenHeaderName) || undefined;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* Stores the given CSRF token into IndexedDB.
|
|
73
|
-
*
|
|
74
|
-
* @category Auth : Client
|
|
75
|
-
*/
|
|
76
|
-
export async function storeCsrfToken(
|
|
77
|
-
csrfToken: string,
|
|
78
|
-
options: Readonly<CsrfHeaderNameOption> &
|
|
79
|
-
PartialWithUndefined<{
|
|
80
|
-
/**
|
|
81
|
-
* Allows mocking or overriding the default CSRF token store.
|
|
82
|
-
*
|
|
83
|
-
* @default getDefaultCsrfTokenStore()
|
|
84
|
-
*/
|
|
85
|
-
csrfTokenStore: CsrfTokenStore;
|
|
86
|
-
}>,
|
|
87
|
-
): Promise<void> {
|
|
88
|
-
await (options.csrfTokenStore || (await getDefaultCsrfTokenStore())).setCsrfToken(csrfToken);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* Used in client (frontend) code to retrieve the current CSRF token in order to send it with
|
|
93
|
-
* requests to the host (backend).
|
|
94
|
-
*
|
|
95
|
-
* @category Auth : Client
|
|
96
|
-
*/
|
|
97
|
-
export async function getCurrentCsrfToken(
|
|
98
|
-
options: Readonly<CsrfHeaderNameOption> &
|
|
99
|
-
PartialWithUndefined<{
|
|
100
|
-
/**
|
|
101
|
-
* Allows mocking or overriding the default CSRF token store.
|
|
102
|
-
*
|
|
103
|
-
* @default getDefaultCsrfTokenStore()
|
|
104
|
-
*/
|
|
105
|
-
csrfTokenStore: CsrfTokenStore;
|
|
106
|
-
}>,
|
|
107
|
-
): Promise<string | undefined> {
|
|
108
|
-
return (
|
|
109
|
-
(await (options.csrfTokenStore || (await getDefaultCsrfTokenStore())).getCsrfToken()) ||
|
|
110
|
-
undefined
|
|
111
|
-
);
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* Wipes the current stored CSRF token. This should be used by client (frontend) code to react to a
|
|
116
|
-
* session timeout.
|
|
49
|
+
* Used in client (frontend) code to retrieve the current CSRF token from the browser cookie in
|
|
50
|
+
* order to send it with requests to the host (backend).
|
|
117
51
|
*
|
|
118
52
|
* @category Auth : Client
|
|
119
53
|
*/
|
|
120
|
-
export
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
*/
|
|
128
|
-
csrfTokenStore: CsrfTokenStore;
|
|
129
|
-
}>,
|
|
130
|
-
): Promise<void> {
|
|
131
|
-
await (options.csrfTokenStore || (await getDefaultCsrfTokenStore())).deleteCsrfToken();
|
|
54
|
+
export function getCurrentCsrfToken(): string | undefined {
|
|
55
|
+
const cookieRegExp = new RegExp(`${escapeStringForRegExp(AuthCookie.Csrf)}=([^;]+)`);
|
|
56
|
+
const [
|
|
57
|
+
,
|
|
58
|
+
value,
|
|
59
|
+
] = safeMatch(globalThis.document.cookie, cookieRegExp);
|
|
60
|
+
return value || undefined;
|
|
132
61
|
}
|
package/src/index.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/src/jwt/jwt.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import {assertWrap, check} from '@augment-vir/assert';
|
|
2
|
-
import {type AnyObject, type PartialWithUndefined} from '@augment-vir/common';
|
|
2
|
+
import {type AnyObject, type PartialWithUndefined, type SelectFrom} from '@augment-vir/common';
|
|
3
3
|
import {
|
|
4
4
|
type AnyDuration,
|
|
5
5
|
calculateRelativeDate,
|
|
@@ -13,7 +13,6 @@ import {
|
|
|
13
13
|
type UtcTimezone,
|
|
14
14
|
} from 'date-vir';
|
|
15
15
|
import {EncryptJWT, jwtDecrypt, jwtVerify, SignJWT} from 'jose';
|
|
16
|
-
import {defaultAllowedClockSkew} from '../csrf-token.js';
|
|
17
16
|
import {type JwtKeys} from './jwt-keys.js';
|
|
18
17
|
|
|
19
18
|
const encryptionProtectedHeader = {
|
|
@@ -24,6 +23,17 @@ const signingProtectedHeader = {
|
|
|
24
23
|
alg: 'HS512',
|
|
25
24
|
};
|
|
26
25
|
|
|
26
|
+
/**
|
|
27
|
+
* Default allowed clock skew for JWT expiration checks. Accounts for differences between server and
|
|
28
|
+
* client clocks.
|
|
29
|
+
*
|
|
30
|
+
* @category Internal
|
|
31
|
+
* @default {minutes: 5}
|
|
32
|
+
*/
|
|
33
|
+
export const defaultAllowedClockSkew: Readonly<AnyDuration> = {
|
|
34
|
+
minutes: 5,
|
|
35
|
+
};
|
|
36
|
+
|
|
27
37
|
/**
|
|
28
38
|
* Params for {@link createJwt}.
|
|
29
39
|
*
|
|
@@ -148,7 +158,9 @@ export async function createJwt<JwtData extends AnyObject = AnyObject>(
|
|
|
148
158
|
*
|
|
149
159
|
* @category Internal
|
|
150
160
|
*/
|
|
151
|
-
export type ParseJwtParams = Readonly<
|
|
161
|
+
export type ParseJwtParams = Readonly<
|
|
162
|
+
SelectFrom<CreateJwtParams, {issuer: true; audience: true; jwtKeys: true}>
|
|
163
|
+
> &
|
|
152
164
|
PartialWithUndefined<{
|
|
153
165
|
/**
|
|
154
166
|
* Allowed clock skew tolerance for JWT expiration and timestamp checks. Accounts for
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The interface used for overriding the default CSRF token store in storage functions.
|
|
3
|
-
*
|
|
4
|
-
* @category Internal
|
|
5
|
-
*/
|
|
6
|
-
export type CsrfTokenStore = {
|
|
7
|
-
/** Retrieves the stored CSRF token, if any. */
|
|
8
|
-
getCsrfToken(): Promise<string | undefined>;
|
|
9
|
-
/** Stores a CSRF token. */
|
|
10
|
-
setCsrfToken(value: string): Promise<void>;
|
|
11
|
-
/** Deletes the stored CSRF token. */
|
|
12
|
-
deleteCsrfToken(): Promise<void>;
|
|
13
|
-
};
|
|
14
|
-
/**
|
|
15
|
-
* The default {@link LocalDbClient} instance used for storing CSRF tokens. This uses a dedicated
|
|
16
|
-
* store name to avoid collisions with other storage. Lazily initialized to avoid crashes in Node.js
|
|
17
|
-
* environments where IndexedDB is not available.
|
|
18
|
-
*
|
|
19
|
-
* @category Internal
|
|
20
|
-
*/
|
|
21
|
-
export declare function getDefaultCsrfTokenStore(): Promise<CsrfTokenStore>;
|
package/dist/csrf-token-store.js
DELETED
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
import { LocalDbClient } from 'local-db-client';
|
|
2
|
-
import { defineShape } from 'object-shape-tester';
|
|
3
|
-
const csrfTokenDbShapes = {
|
|
4
|
-
csrfToken: defineShape(''),
|
|
5
|
-
};
|
|
6
|
-
async function createDefaultCsrfTokenStore() {
|
|
7
|
-
const client = await LocalDbClient.createClient(csrfTokenDbShapes, {
|
|
8
|
-
storeName: 'auth-vir-csrf',
|
|
9
|
-
});
|
|
10
|
-
return {
|
|
11
|
-
async getCsrfToken() {
|
|
12
|
-
return (await client.load.csrfToken()) || undefined;
|
|
13
|
-
},
|
|
14
|
-
async setCsrfToken(value) {
|
|
15
|
-
await client.set.csrfToken(value);
|
|
16
|
-
},
|
|
17
|
-
async deleteCsrfToken() {
|
|
18
|
-
await client.delete.csrfToken();
|
|
19
|
-
},
|
|
20
|
-
};
|
|
21
|
-
}
|
|
22
|
-
/**
|
|
23
|
-
* The default {@link LocalDbClient} instance used for storing CSRF tokens. This uses a dedicated
|
|
24
|
-
* store name to avoid collisions with other storage. Lazily initialized to avoid crashes in Node.js
|
|
25
|
-
* environments where IndexedDB is not available.
|
|
26
|
-
*
|
|
27
|
-
* @category Internal
|
|
28
|
-
*/
|
|
29
|
-
export async function getDefaultCsrfTokenStore() {
|
|
30
|
-
if (!cachedStorePromise) {
|
|
31
|
-
cachedStorePromise = createDefaultCsrfTokenStore();
|
|
32
|
-
}
|
|
33
|
-
return cachedStorePromise;
|
|
34
|
-
}
|
|
35
|
-
let cachedStorePromise;
|
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
import { type CsrfTokenStore } from './csrf-token-store.js';
|
|
2
|
-
/**
|
|
3
|
-
* `accessRecord` type for {@link createMockLocalStorage}'s output.
|
|
4
|
-
*
|
|
5
|
-
* @category Internal
|
|
6
|
-
*/
|
|
7
|
-
export type MockLocalStorageAccessRecord = {
|
|
8
|
-
getItem: string[];
|
|
9
|
-
removeItem: string[];
|
|
10
|
-
setItem: {
|
|
11
|
-
key: string;
|
|
12
|
-
value: string;
|
|
13
|
-
}[];
|
|
14
|
-
key: number[];
|
|
15
|
-
};
|
|
16
|
-
/**
|
|
17
|
-
* Create an empty `accessRecord` object, this is to be used in conjunction with
|
|
18
|
-
* {@link createMockLocalStorage}.
|
|
19
|
-
*
|
|
20
|
-
* @category Mock
|
|
21
|
-
*/
|
|
22
|
-
export declare function createEmptyMockLocalStorageAccessRecord(): MockLocalStorageAccessRecord;
|
|
23
|
-
/**
|
|
24
|
-
* Create a LocalStorage mock.
|
|
25
|
-
*
|
|
26
|
-
* @category Mock
|
|
27
|
-
*/
|
|
28
|
-
export declare function createMockLocalStorage(
|
|
29
|
-
/** Set values in here to initialize the mocked localStorage data store contents. */
|
|
30
|
-
init?: Record<string, string>): {
|
|
31
|
-
localStorage: Storage;
|
|
32
|
-
store: Record<string, string>;
|
|
33
|
-
accessRecord: MockLocalStorageAccessRecord;
|
|
34
|
-
};
|
|
35
|
-
/**
|
|
36
|
-
* `accessRecord` type for {@link createMockCsrfTokenStore}'s output.
|
|
37
|
-
*
|
|
38
|
-
* @category Internal
|
|
39
|
-
*/
|
|
40
|
-
export type MockCsrfTokenStoreAccessRecord = {
|
|
41
|
-
getCsrfToken: number;
|
|
42
|
-
setCsrfToken: string[];
|
|
43
|
-
deleteCsrfToken: number;
|
|
44
|
-
};
|
|
45
|
-
/**
|
|
46
|
-
* Create an empty `accessRecord` object, this is to be used in conjunction with
|
|
47
|
-
* {@link createMockCsrfTokenStore}.
|
|
48
|
-
*
|
|
49
|
-
* @category Mock
|
|
50
|
-
*/
|
|
51
|
-
export declare function createEmptyMockCsrfTokenStoreAccessRecord(): MockCsrfTokenStoreAccessRecord;
|
|
52
|
-
/**
|
|
53
|
-
* Create a mock {@link CsrfTokenStore} backed by a simple in-memory object, for use in tests.
|
|
54
|
-
*
|
|
55
|
-
* @category Mock
|
|
56
|
-
*/
|
|
57
|
-
export declare function createMockCsrfTokenStore(
|
|
58
|
-
/** Set an initial value to initialize the mocked store contents. */
|
|
59
|
-
init?: string | undefined): {
|
|
60
|
-
csrfTokenStore: CsrfTokenStore;
|
|
61
|
-
/** The current value held in the mock store. */
|
|
62
|
-
readonly storedValue: string | undefined;
|
|
63
|
-
accessRecord: MockCsrfTokenStoreAccessRecord;
|
|
64
|
-
};
|