auth-vir 3.1.2 → 5.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +38 -25
- package/dist/auth-client/backend-auth.client.d.ts +1 -1
- package/dist/auth-client/backend-auth.client.js +40 -23
- package/dist/auth-client/frontend-auth.client.d.ts +8 -16
- package/dist/auth-client/frontend-auth.client.js +5 -28
- package/dist/auth.d.ts +14 -27
- package/dist/auth.js +21 -41
- package/dist/cookie.d.ts +41 -14
- package/dist/cookie.js +73 -31
- package/dist/csrf-token.d.ts +6 -136
- package/dist/csrf-token.js +19 -118
- 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 -30
- package/src/auth-client/frontend-auth.client.ts +6 -55
- package/src/auth.ts +28 -72
- package/src/cookie.ts +99 -48
- package/src/csrf-token.ts +22 -232
- 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/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,75 +1,15 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
wrapInTry,
|
|
4
|
-
type PartialWithUndefined,
|
|
5
|
-
type SelectFrom,
|
|
6
|
-
} from '@augment-vir/common';
|
|
7
|
-
import {
|
|
8
|
-
calculateRelativeDate,
|
|
9
|
-
fullDateShape,
|
|
10
|
-
getNowInUtcTimezone,
|
|
11
|
-
isDateAfter,
|
|
12
|
-
type AnyDuration,
|
|
13
|
-
} from 'date-vir';
|
|
14
|
-
import {defineShape, parseJsonWithShape} from 'object-shape-tester';
|
|
1
|
+
import {check} from '@augment-vir/assert';
|
|
2
|
+
import {escapeStringForRegExp, randomString, safeMatch} from '@augment-vir/common';
|
|
15
3
|
import {type RequireExactlyOne} from 'type-fest';
|
|
16
|
-
import {
|
|
4
|
+
import {AuthCookie} from './cookie.js';
|
|
17
5
|
|
|
18
6
|
/**
|
|
19
|
-
*
|
|
7
|
+
* Generates a random, cryptographically secure CSRF token string.
|
|
20
8
|
*
|
|
21
9
|
* @category Internal
|
|
22
10
|
*/
|
|
23
|
-
export
|
|
24
|
-
|
|
25
|
-
expiration: fullDateShape,
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* A cryptographically CSRF token with expiration date.
|
|
30
|
-
*
|
|
31
|
-
* @category Internal
|
|
32
|
-
*/
|
|
33
|
-
export type CsrfToken = typeof csrfTokenShape.runtimeType;
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Default allowed clock skew for CSRF token expiration checks. Accounts for differences between
|
|
37
|
-
* server and client clocks when checking token expiration.
|
|
38
|
-
*
|
|
39
|
-
* @category Internal
|
|
40
|
-
* @default {minutes: 5}
|
|
41
|
-
*/
|
|
42
|
-
export const defaultAllowedClockSkew: Readonly<AnyDuration> = {
|
|
43
|
-
minutes: 5,
|
|
44
|
-
};
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Generates a random, cryptographically secure CSRF token.
|
|
48
|
-
*
|
|
49
|
-
* @category Internal
|
|
50
|
-
*/
|
|
51
|
-
export function generateCsrfToken(
|
|
52
|
-
/** How long the CSRF token is valid for. */
|
|
53
|
-
duration: Readonly<AnyDuration>,
|
|
54
|
-
): CsrfToken {
|
|
55
|
-
return {
|
|
56
|
-
token: randomString(256),
|
|
57
|
-
expiration: calculateRelativeDate(getNowInUtcTimezone(), duration),
|
|
58
|
-
};
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* CSRF token failure reasons for {@link GetCsrfTokenResult}.
|
|
63
|
-
*
|
|
64
|
-
* @category Internal
|
|
65
|
-
*/
|
|
66
|
-
export enum CsrfTokenFailureReason {
|
|
67
|
-
/** No CSRF token was found. */
|
|
68
|
-
DoesNotExist = 'does-not-exist',
|
|
69
|
-
/** A CSRF token was found but parsing it failed. */
|
|
70
|
-
ParseFailed = 'parse-failed',
|
|
71
|
-
/** A CSRF token was found and parsed but is expired. */
|
|
72
|
-
Expired = 'expired',
|
|
11
|
+
export function generateCsrfToken(): string {
|
|
12
|
+
return randomString(256);
|
|
73
13
|
}
|
|
74
14
|
|
|
75
15
|
/**
|
|
@@ -91,181 +31,31 @@ export type CsrfHeaderNameOption = RequireExactlyOne<{
|
|
|
91
31
|
* @category Auth : Client
|
|
92
32
|
* @category Auth : Host
|
|
93
33
|
*/
|
|
94
|
-
export function resolveCsrfHeaderName(
|
|
95
|
-
if ('csrfHeaderName' in
|
|
96
|
-
return
|
|
34
|
+
export function resolveCsrfHeaderName(options: Readonly<CsrfHeaderNameOption>): string {
|
|
35
|
+
if ('csrfHeaderName' in options && options.csrfHeaderName) {
|
|
36
|
+
return options.csrfHeaderName;
|
|
97
37
|
} else {
|
|
98
38
|
return [
|
|
99
|
-
|
|
39
|
+
options.csrfHeaderPrefix,
|
|
100
40
|
'auth-vir',
|
|
101
41
|
'csrf-token',
|
|
102
|
-
]
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* Output from {@link getCurrentCsrfToken}.
|
|
108
|
-
*
|
|
109
|
-
* @category Internal
|
|
110
|
-
*/
|
|
111
|
-
export type GetCsrfTokenResult = RequireExactlyOne<{
|
|
112
|
-
csrfToken: Readonly<CsrfToken>;
|
|
113
|
-
failure: CsrfTokenFailureReason;
|
|
114
|
-
}>;
|
|
115
|
-
|
|
116
|
-
/**
|
|
117
|
-
* Extract the CSRF token header from a response.
|
|
118
|
-
*
|
|
119
|
-
* @category Auth : Client
|
|
120
|
-
*/
|
|
121
|
-
export function extractCsrfTokenHeader(
|
|
122
|
-
response: Readonly<PartialWithUndefined<SelectFrom<Response, {headers: true}>>>,
|
|
123
|
-
csrfHeaderNameOption: Readonly<CsrfHeaderNameOption>,
|
|
124
|
-
options?: PartialWithUndefined<{
|
|
125
|
-
/**
|
|
126
|
-
* Allowed clock skew tolerance for CSRF token expiration checks.
|
|
127
|
-
*
|
|
128
|
-
* @default {minutes: 5}
|
|
129
|
-
*/
|
|
130
|
-
allowedClockSkew: Readonly<AnyDuration>;
|
|
131
|
-
}>,
|
|
132
|
-
): Readonly<GetCsrfTokenResult> {
|
|
133
|
-
const csrfTokenHeaderName = resolveCsrfHeaderName(csrfHeaderNameOption);
|
|
134
|
-
|
|
135
|
-
const rawCsrfToken = response.headers?.get(csrfTokenHeaderName);
|
|
136
|
-
|
|
137
|
-
return parseCsrfToken(rawCsrfToken, options);
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
/**
|
|
141
|
-
* Stores the given CSRF token into IndexedDB.
|
|
142
|
-
*
|
|
143
|
-
* @category Auth : Client
|
|
144
|
-
*/
|
|
145
|
-
export async function storeCsrfToken(
|
|
146
|
-
csrfToken: Readonly<CsrfToken>,
|
|
147
|
-
options: Readonly<CsrfHeaderNameOption> &
|
|
148
|
-
PartialWithUndefined<{
|
|
149
|
-
/**
|
|
150
|
-
* Allows mocking or overriding the default CSRF token store.
|
|
151
|
-
*
|
|
152
|
-
* @default getDefaultCsrfTokenStore()
|
|
153
|
-
*/
|
|
154
|
-
csrfTokenStore: CsrfTokenStore;
|
|
155
|
-
}>,
|
|
156
|
-
): Promise<void> {
|
|
157
|
-
await (options.csrfTokenStore || (await getDefaultCsrfTokenStore())).setCsrfToken(
|
|
158
|
-
JSON.stringify(csrfToken),
|
|
159
|
-
);
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
/**
|
|
163
|
-
* Parse a raw CSRF token JSON string.
|
|
164
|
-
*
|
|
165
|
-
* @category Internal
|
|
166
|
-
*/
|
|
167
|
-
export function parseCsrfToken(
|
|
168
|
-
value: string | undefined | null,
|
|
169
|
-
options?: PartialWithUndefined<{
|
|
170
|
-
/**
|
|
171
|
-
* Allowed clock skew tolerance for CSRF token expiration checks. Accounts for differences
|
|
172
|
-
* between server and client clocks.
|
|
173
|
-
*
|
|
174
|
-
* @default {minutes: 5}
|
|
175
|
-
*/
|
|
176
|
-
allowedClockSkew: Readonly<AnyDuration>;
|
|
177
|
-
}>,
|
|
178
|
-
): Readonly<GetCsrfTokenResult> {
|
|
179
|
-
if (!value) {
|
|
180
|
-
return {
|
|
181
|
-
failure: CsrfTokenFailureReason.DoesNotExist,
|
|
182
|
-
};
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
const csrfToken: CsrfToken | undefined = wrapInTry(
|
|
186
|
-
() =>
|
|
187
|
-
parseJsonWithShape(value, csrfTokenShape, {
|
|
188
|
-
/** For forwards / backwards compatibility. */
|
|
189
|
-
allowExtraKeys: true,
|
|
190
|
-
}),
|
|
191
|
-
{
|
|
192
|
-
fallbackValue: undefined,
|
|
193
|
-
},
|
|
194
|
-
);
|
|
195
|
-
|
|
196
|
-
if (!csrfToken) {
|
|
197
|
-
return {
|
|
198
|
-
failure: CsrfTokenFailureReason.ParseFailed,
|
|
199
|
-
};
|
|
42
|
+
]
|
|
43
|
+
.filter(check.isTruthy)
|
|
44
|
+
.join('-');
|
|
200
45
|
}
|
|
201
|
-
|
|
202
|
-
const effectiveExpiration = calculateRelativeDate(
|
|
203
|
-
csrfToken.expiration,
|
|
204
|
-
options?.allowedClockSkew || defaultAllowedClockSkew,
|
|
205
|
-
);
|
|
206
|
-
|
|
207
|
-
if (
|
|
208
|
-
isDateAfter({
|
|
209
|
-
fullDate: getNowInUtcTimezone(),
|
|
210
|
-
relativeTo: effectiveExpiration,
|
|
211
|
-
})
|
|
212
|
-
) {
|
|
213
|
-
return {
|
|
214
|
-
failure: CsrfTokenFailureReason.Expired,
|
|
215
|
-
};
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
return {
|
|
219
|
-
csrfToken,
|
|
220
|
-
};
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
/**
|
|
224
|
-
* Used in client (frontend) code to retrieve the current CSRF token in order to send it with
|
|
225
|
-
* requests to the host (backend).
|
|
226
|
-
*
|
|
227
|
-
* @category Auth : Client
|
|
228
|
-
*/
|
|
229
|
-
export async function getCurrentCsrfToken(
|
|
230
|
-
options: Readonly<CsrfHeaderNameOption> &
|
|
231
|
-
PartialWithUndefined<{
|
|
232
|
-
/**
|
|
233
|
-
* Allows mocking or overriding the default CSRF token store.
|
|
234
|
-
*
|
|
235
|
-
* @default getDefaultCsrfTokenStore()
|
|
236
|
-
*/
|
|
237
|
-
csrfTokenStore: CsrfTokenStore;
|
|
238
|
-
/**
|
|
239
|
-
* Allowed clock skew tolerance for CSRF token expiration checks.
|
|
240
|
-
*
|
|
241
|
-
* @default {minutes: 5}
|
|
242
|
-
*/
|
|
243
|
-
allowedClockSkew: Readonly<AnyDuration>;
|
|
244
|
-
}>,
|
|
245
|
-
): Promise<Readonly<GetCsrfTokenResult>> {
|
|
246
|
-
const rawCsrfToken: string | undefined =
|
|
247
|
-
(await (options.csrfTokenStore || (await getDefaultCsrfTokenStore())).getCsrfToken()) ||
|
|
248
|
-
undefined;
|
|
249
|
-
|
|
250
|
-
return parseCsrfToken(rawCsrfToken, options);
|
|
251
46
|
}
|
|
252
47
|
|
|
253
48
|
/**
|
|
254
|
-
*
|
|
255
|
-
*
|
|
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).
|
|
256
51
|
*
|
|
257
52
|
* @category Auth : Client
|
|
258
53
|
*/
|
|
259
|
-
export
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
*/
|
|
267
|
-
csrfTokenStore: CsrfTokenStore;
|
|
268
|
-
}>,
|
|
269
|
-
): Promise<void> {
|
|
270
|
-
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;
|
|
271
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
|
-
};
|