@crossauth/sveltekit 1.0.1 → 1.1.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 +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +16 -6181
- package/dist/sveltekitadminclientendpoints.d.ts +13 -12
- package/dist/sveltekitadminclientendpoints.js +187 -0
- package/dist/sveltekitadminendpoints.d.ts +5 -4
- package/dist/sveltekitadminendpoints.js +766 -0
- package/dist/sveltekitapikey.d.ts +4 -3
- package/dist/sveltekitapikey.js +81 -0
- package/dist/sveltekitoauthclient.d.ts +6 -4
- package/dist/sveltekitoauthclient.js +2309 -0
- package/dist/sveltekitoauthserver.d.ts +4 -4
- package/dist/sveltekitoauthserver.js +1350 -0
- package/dist/sveltekitresserver.d.ts +6 -4
- package/dist/sveltekitresserver.js +286 -0
- package/dist/sveltekitserver.d.ts +11 -9
- package/dist/sveltekitserver.js +393 -0
- package/dist/sveltekitsession.d.ts +6 -5
- package/dist/sveltekitsession.js +1112 -0
- package/dist/sveltekitsessionadapter.d.ts +2 -3
- package/dist/sveltekitsessionadapter.js +2 -0
- package/dist/sveltekitsharedclientendpoints.d.ts +7 -6
- package/dist/sveltekitsharedclientendpoints.js +630 -0
- package/dist/sveltekituserclientendpoints.d.ts +13 -12
- package/dist/sveltekituserclientendpoints.js +270 -0
- package/dist/sveltekituserendpoints.d.ts +6 -5
- package/dist/sveltekituserendpoints.js +1813 -0
- package/dist/tests/sveltekitadminclientendpoints.test.js +330 -0
- package/dist/tests/sveltekitadminendpoints.test.js +242 -0
- package/dist/tests/sveltekitapikeyserver.test.js +44 -0
- package/dist/tests/sveltekitoauthclient.test.d.ts +5 -5
- package/dist/tests/sveltekitoauthclient.test.js +1016 -0
- package/dist/tests/sveltekitoauthresserver.test.d.ts +4 -4
- package/dist/tests/sveltekitoauthresserver.test.js +185 -0
- package/dist/tests/sveltekitoauthserver.test.js +673 -0
- package/dist/tests/sveltekituserclientendpoints.test.js +244 -0
- package/dist/tests/sveltekituserendpoints.test.js +152 -0
- package/dist/tests/sveltemock.test.js +36 -0
- package/dist/tests/sveltemocks.d.ts +22 -8
- package/dist/tests/sveltemocks.js +114 -0
- package/dist/tests/sveltesessionhooks.test.js +224 -0
- package/dist/tests/testshared.d.ts +8 -8
- package/dist/tests/testshared.js +344 -0
- package/dist/utils.d.ts +1 -2
- package/dist/utils.js +123 -0
- package/package.json +23 -15
- package/dist/index.cjs +0 -1
|
@@ -0,0 +1,1112 @@
|
|
|
1
|
+
// Copyright (c) 2026 Matthew Baker. All rights reserved. Licenced under the Apache Licence 2.0. See LICENSE file
|
|
2
|
+
import { minimatch } from 'minimatch';
|
|
3
|
+
import { KeyStorage, UserStorage, OAuthClientStorage, SessionManager, Authenticator, Crypto, setParameter, ParamType, toCookieSerializeOptions } from '@crossauth/backend';
|
|
4
|
+
import { CrossauthError, CrossauthLogger, j, ErrorCode, httpStatus } from '@crossauth/common';
|
|
5
|
+
import { UserState } from '@crossauth/common';
|
|
6
|
+
import { error, redirect } from '@sveltejs/kit';
|
|
7
|
+
import { JsonOrFormData } from './utils';
|
|
8
|
+
import { SvelteKitUserEndpoints } from './sveltekituserendpoints';
|
|
9
|
+
import { SvelteKitAdminEndpoints } from './sveltekitadminendpoints';
|
|
10
|
+
import { SvelteKitUserClientEndpoints } from './sveltekituserclientendpoints';
|
|
11
|
+
import { SvelteKitAdminClientEndpoints } from './sveltekitadminclientendpoints';
|
|
12
|
+
import { SvelteKitSessionAdapter } from './sveltekitsessionadapter';
|
|
13
|
+
import { SvelteKitServer } from './sveltekitserver';
|
|
14
|
+
import {} from './tests/sveltemocks';
|
|
15
|
+
export const CSRFHEADER = "X-CROSSAUTH-CSRF";
|
|
16
|
+
/////////////////////////////////////////////////////////////////////////////
|
|
17
|
+
// DEFAULT FUNCTIONS
|
|
18
|
+
/**
|
|
19
|
+
* Default User validator. Doesn't validate password
|
|
20
|
+
*
|
|
21
|
+
* Username must be at least two characters.
|
|
22
|
+
* @param password The password to validate
|
|
23
|
+
* @returns an array of errors. If there were no errors, returns an empty array
|
|
24
|
+
*/
|
|
25
|
+
function defaultUserValidator(user) {
|
|
26
|
+
let errors = [];
|
|
27
|
+
if (user.username == undefined)
|
|
28
|
+
errors.push("Username must be given");
|
|
29
|
+
else if (user.username.length < 2)
|
|
30
|
+
errors.push("Username must be at least 2 characters");
|
|
31
|
+
else if (user.username.length > 254)
|
|
32
|
+
errors.push("Username must be no longer than 254 characters");
|
|
33
|
+
return errors;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Default function for creating users. Can be overridden.
|
|
37
|
+
*
|
|
38
|
+
* Takes any field beginning with `user_` and that is also in
|
|
39
|
+
* `userEditableFields` (without the `user_` prefix).
|
|
40
|
+
*
|
|
41
|
+
* @param event the SvelteKit request event
|
|
42
|
+
* @param userEditableFields the fields a user may edit
|
|
43
|
+
* @returns the new user
|
|
44
|
+
*/
|
|
45
|
+
function defaultCreateUser(event, data, userEditableFields, allowableFactor1 = ["localpassword"]) {
|
|
46
|
+
let state = "active";
|
|
47
|
+
let user = {
|
|
48
|
+
username: data.username ?? "",
|
|
49
|
+
state: state,
|
|
50
|
+
};
|
|
51
|
+
const callerIsAdmin = event.locals.user && SvelteKitServer.isAdminFn(event.locals.user);
|
|
52
|
+
for (let field in data) {
|
|
53
|
+
let name = field.replace(/^user_/, "");
|
|
54
|
+
if (field.startsWith("user_") &&
|
|
55
|
+
(callerIsAdmin || userEditableFields.includes(name))) {
|
|
56
|
+
if ("type_" + name in data) {
|
|
57
|
+
if (data["type_" + name] == "string") {
|
|
58
|
+
user[name] = data[field];
|
|
59
|
+
}
|
|
60
|
+
else if (data["type_" + name] == "number" || data["type_" + name] == "integer" || data["type_" + name] == "float") {
|
|
61
|
+
user[name] = Number(data[field]);
|
|
62
|
+
}
|
|
63
|
+
else if (data["type_" + name] == "boolean") {
|
|
64
|
+
const c = data[field]?.toLocaleLowerCase();
|
|
65
|
+
user[name] = (c == "1" || c == "y" || c == "t" || c == "yes" || c == "true");
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
user[name] = data[field];
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
user.factor1 = "localpassword";
|
|
74
|
+
if (data.factor1 && allowableFactor1.includes(data.factor1)) {
|
|
75
|
+
user.factor1 = data.factor1;
|
|
76
|
+
}
|
|
77
|
+
user.factor2 = data.factor2;
|
|
78
|
+
return user;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Default function for creating users. Can be overridden.
|
|
82
|
+
*
|
|
83
|
+
* Takes any field beginning with `user_` and that is also in
|
|
84
|
+
* `userEditableFields` (without the `user_` prefix).
|
|
85
|
+
*
|
|
86
|
+
* @param user the user to update
|
|
87
|
+
* @param event the SvelteKit request event
|
|
88
|
+
* @param userEditableFields the fields a user may edit
|
|
89
|
+
* @returns the new user
|
|
90
|
+
*/
|
|
91
|
+
function defaultUpdateUser(user, event, data, userEditableFields) {
|
|
92
|
+
const callerIsAdmin = event.locals.user && SvelteKitServer.isAdminFn(event.locals.user);
|
|
93
|
+
for (let field in data) {
|
|
94
|
+
let name = field.replace(/^user_/, "");
|
|
95
|
+
if (field.startsWith("user_") &&
|
|
96
|
+
(callerIsAdmin || userEditableFields.includes(name))) {
|
|
97
|
+
if ("type_" + name in data) {
|
|
98
|
+
if (data["type_" + name] == "string") {
|
|
99
|
+
user[name] = data[field];
|
|
100
|
+
}
|
|
101
|
+
else if (data["type_" + name] == "number" || data["type_" + name] == "integer" || data["type_" + name] == "float") {
|
|
102
|
+
user[name] = Number(data[field]);
|
|
103
|
+
}
|
|
104
|
+
else if (data["type_" + name] == "boolean") {
|
|
105
|
+
const c = data[field]?.toLocaleLowerCase();
|
|
106
|
+
user[name] = (c == "1" || c == "y" || c == "t" || c == "yes" || c == "true");
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
user[name] = data[field];
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return user;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* The Sveltekit session server.
|
|
118
|
+
*
|
|
119
|
+
* You shouldn't have to instantiate this directly. It is created when
|
|
120
|
+
* you create a {@link SvelteKitServer} object.
|
|
121
|
+
|
|
122
|
+
* **Middleware**
|
|
123
|
+
*
|
|
124
|
+
* This class registers one middleware function to fill in the following
|
|
125
|
+
* fields in the request:
|
|
126
|
+
*
|
|
127
|
+
* - `user` a {@link @crossauth/common!User}` object
|
|
128
|
+
* - `authType`: set to `cookie` or undefined
|
|
129
|
+
* - `csrfToken`: a CSRF token that can be used in POST requests
|
|
130
|
+
* - `sessionId` a session ID if one is created
|
|
131
|
+
*/
|
|
132
|
+
export class SvelteKitSessionServer {
|
|
133
|
+
/**
|
|
134
|
+
* Hook to check if the user is logged in and set data in `locals`
|
|
135
|
+
* accordingly.
|
|
136
|
+
*/
|
|
137
|
+
sessionHook;
|
|
138
|
+
twoFAHook;
|
|
139
|
+
/**
|
|
140
|
+
* Key storage taken from constructor args.
|
|
141
|
+
* See {@link SvelteKitSessionServer.constructor}.
|
|
142
|
+
*/
|
|
143
|
+
keyStorage;
|
|
144
|
+
/**
|
|
145
|
+
* Session Manager taken from constructor args.
|
|
146
|
+
* See {@link SvelteKitSessionServer.constructor}.
|
|
147
|
+
*/
|
|
148
|
+
sessionManager;
|
|
149
|
+
/**
|
|
150
|
+
* User storage taken from constructor args.
|
|
151
|
+
* See {@link SvelteKitSessionServer.constructor}.
|
|
152
|
+
*/
|
|
153
|
+
userStorage;
|
|
154
|
+
/**
|
|
155
|
+
* User storage taken from constructor args.
|
|
156
|
+
* See {@link SvelteKitSessionServer.constructor}.
|
|
157
|
+
*/
|
|
158
|
+
clientStorage;
|
|
159
|
+
/**
|
|
160
|
+
* Funtion to validate users upon creation. Taken from the options during
|
|
161
|
+
* construction or the default value.
|
|
162
|
+
* See {@link SvelteKitSessionServerOptions}.
|
|
163
|
+
*/
|
|
164
|
+
validateUserFn = defaultUserValidator;
|
|
165
|
+
/**
|
|
166
|
+
* Funtion to create a user record from form fields. Taken from the options during
|
|
167
|
+
* construction or the default value.
|
|
168
|
+
* See {@link SvelteKitSessionServerOptions}.
|
|
169
|
+
*/
|
|
170
|
+
createUserFn = defaultCreateUser;
|
|
171
|
+
/**
|
|
172
|
+
* Funtion to update a user record from form fields. Taken from the options during
|
|
173
|
+
* construction or the default value.
|
|
174
|
+
* See {@link SvelteKitSessionServerOptions}.
|
|
175
|
+
*/
|
|
176
|
+
updateUserFn = defaultUpdateUser;
|
|
177
|
+
/**
|
|
178
|
+
* The set of authenticators taken from constructor args.
|
|
179
|
+
* See {@link SvelteKitSessionServer.constructor}.
|
|
180
|
+
*/
|
|
181
|
+
authenticators;
|
|
182
|
+
/**
|
|
183
|
+
* The set of allowed authenticators taken from the options during
|
|
184
|
+
* construction.
|
|
185
|
+
*
|
|
186
|
+
* The default is `[{name: "none", friendlyName: "none"}]`
|
|
187
|
+
*/
|
|
188
|
+
allowedFactor2 = [];
|
|
189
|
+
/**
|
|
190
|
+
* The set of allowed authenticators taken from the options during
|
|
191
|
+
* construction.
|
|
192
|
+
*
|
|
193
|
+
* The default is `["none"]`.
|
|
194
|
+
*/
|
|
195
|
+
allowedFactor2Names = [];
|
|
196
|
+
/** Called when a new session token is going to be saved
|
|
197
|
+
* Add additional fields to your session storage here. Return a map of
|
|
198
|
+
* keys to values */
|
|
199
|
+
addToSession;
|
|
200
|
+
/**
|
|
201
|
+
* The set of allowed authenticators taken from the options during
|
|
202
|
+
* construction.
|
|
203
|
+
*/
|
|
204
|
+
validateSession;
|
|
205
|
+
factor2ProtectedPageEndpoints = [];
|
|
206
|
+
factor2ProtectedApiEndpoints = [];
|
|
207
|
+
loginProtectedPageEndpoints = [];
|
|
208
|
+
loginProtectedApiEndpoints = [];
|
|
209
|
+
loginProtectedExceptionPageEndpoints = [];
|
|
210
|
+
loginProtectedExceptionApiEndpoints = [];
|
|
211
|
+
adminPageEndpoints = [];
|
|
212
|
+
adminApiEndpoints = [];
|
|
213
|
+
adminProtectedExceptionPageEndpoints = [];
|
|
214
|
+
adminProtectedExceptionApiEndpoints = [];
|
|
215
|
+
unauthorizedUrl = undefined;
|
|
216
|
+
enableCsrfProtection = true;
|
|
217
|
+
/** Whether email verification is enabled.
|
|
218
|
+
*
|
|
219
|
+
* Reads from constructor options
|
|
220
|
+
*/
|
|
221
|
+
enableEmailVerification = false;
|
|
222
|
+
/** Whether password reset is enabled.
|
|
223
|
+
*
|
|
224
|
+
* Reads from constructor options
|
|
225
|
+
*/
|
|
226
|
+
enablePasswordReset = false;
|
|
227
|
+
factor2Url = "/factor2";
|
|
228
|
+
loginUrl = "/login";
|
|
229
|
+
logoutUrl = "/logout";
|
|
230
|
+
/**
|
|
231
|
+
* Use these to access the `load` and `action` endpoints for functions
|
|
232
|
+
* provided by Crossauth. These are the ones intended for users to
|
|
233
|
+
* have access to.
|
|
234
|
+
*
|
|
235
|
+
* See {@link SvelteKitUserEndpoints}
|
|
236
|
+
*/
|
|
237
|
+
userEndpoints;
|
|
238
|
+
/**
|
|
239
|
+
* Use these to access the `load` and `action` endpoints for functions
|
|
240
|
+
* provided by Crossauth that relate to manipulating OAuth clients in the
|
|
241
|
+
* database. These are the ones intended for users to
|
|
242
|
+
* have access to.
|
|
243
|
+
*
|
|
244
|
+
* See {@link SvelteKitUserEndpoints}
|
|
245
|
+
*/
|
|
246
|
+
userClientEndpoints;
|
|
247
|
+
/**
|
|
248
|
+
* Use these to access the `load` and `action` endpoints for functions
|
|
249
|
+
* provided by Crossauth that relate to manipulating OAuth clients in the
|
|
250
|
+
* database as admin. These are the ones intended for users to
|
|
251
|
+
* have access to.
|
|
252
|
+
*
|
|
253
|
+
* See {@link SvelteKitAdminEndpoints}
|
|
254
|
+
*/
|
|
255
|
+
adminClientEndpoints;
|
|
256
|
+
/**
|
|
257
|
+
* Use these to access the `load` and `action` endpoints for functions
|
|
258
|
+
* provides by Crossauth. These are the ones intended for admins to
|
|
259
|
+
* have access to.
|
|
260
|
+
*
|
|
261
|
+
* See {@link SvelteKitAdminEndpoints}
|
|
262
|
+
*/
|
|
263
|
+
adminEndpoints;
|
|
264
|
+
redirect;
|
|
265
|
+
error;
|
|
266
|
+
/**
|
|
267
|
+
* This is read from options during construction.
|
|
268
|
+
*
|
|
269
|
+
* See {@link SvelteKitServerOptions}.
|
|
270
|
+
*/
|
|
271
|
+
editUserScope;
|
|
272
|
+
userAllowedFactor1 = ["localpassword"];
|
|
273
|
+
adminAllowedFactor1 = ["localpassword"];
|
|
274
|
+
/**
|
|
275
|
+
* Constructor
|
|
276
|
+
* @param keyStorage where session IDs, email verification and reset tokens are stored
|
|
277
|
+
* @param authenticators valid authenticators that can be in `factor1` or `factor2`
|
|
278
|
+
* of the user. See class documentation for {@link SvelteKitServer} for an example.
|
|
279
|
+
* @param options See {@link SvelteKitSessionServerOptions}.
|
|
280
|
+
*/
|
|
281
|
+
constructor(keyStorage, authenticators, options = {}) {
|
|
282
|
+
this.keyStorage = keyStorage;
|
|
283
|
+
this.userStorage = options.userStorage;
|
|
284
|
+
this.clientStorage = options.clientStorage;
|
|
285
|
+
this.authenticators = authenticators;
|
|
286
|
+
this.sessionManager = new SessionManager(keyStorage, authenticators, options);
|
|
287
|
+
this.redirect = options.redirect ?? redirect;
|
|
288
|
+
this.error = options.error ?? error;
|
|
289
|
+
setParameter("factor2Url", ParamType.String, this, options, "FACTOR2_URL");
|
|
290
|
+
if (!this.factor2Url.endsWith("/"))
|
|
291
|
+
this.factor2Url += "/";
|
|
292
|
+
setParameter("factor2ProtectedPageEndpoints", ParamType.JsonArray, this, options, "FACTOR2_PROTECTED_PAGE_ENDPOINTS");
|
|
293
|
+
setParameter("factor2ProtectedApiEndpoints", ParamType.JsonArray, this, options, "FACTOR2_PROTECTED_API_ENDPOINTS");
|
|
294
|
+
setParameter("loginProtectedPageEndpoints", ParamType.JsonArray, this, options, "LOGIN_PROTECTED_PAGE_ENDPOINTS");
|
|
295
|
+
setParameter("loginProtectedApiEndpoints", ParamType.JsonArray, this, options, "LOGIN_PROTECTED_API_ENDPOINTS");
|
|
296
|
+
setParameter("loginProtectedExceptionPageEndpoints", ParamType.JsonArray, this, options, "LOGIN_PROTECTED_EXCEPTION_PAGE_ENDPOINTS");
|
|
297
|
+
setParameter("loginProtectedExceptionApiEndpoints", ParamType.JsonArray, this, options, "LOGIN_PROTECTED_EXCEPTION_API_ENDPOINTS");
|
|
298
|
+
setParameter("adminPageEndpoints", ParamType.JsonArray, this, options, "ADMIN_PAGE_ENDPOINTS");
|
|
299
|
+
setParameter("adminApiEndpoints", ParamType.JsonArray, this, options, "ADMIN_API_ENDPOINTS");
|
|
300
|
+
setParameter("adminProtectedExceptionPageEndpoints", ParamType.JsonArray, this, options, "ADMIN_PROTECTED_EXCEPTION_PAGE_ENDPOINTS");
|
|
301
|
+
setParameter("adminProtectedExceptionApiEndpoints", ParamType.JsonArray, this, options, "ADMIN_PROTECTED_EXCEPTION_API_ENDPOINTS");
|
|
302
|
+
setParameter("loginUrl", ParamType.JsonArray, this, options, "LOGIN_URL");
|
|
303
|
+
setParameter("logoutUrl", ParamType.JsonArray, this, options, "LOGOUT_URL");
|
|
304
|
+
setParameter("unauthorizedUrl", ParamType.JsonArray, this, options, "UNAUTHORIZED_PAGE");
|
|
305
|
+
setParameter("userAllowedFactor1", ParamType.JsonArray, this, options, "USER_ALLOWED_FACTOR1");
|
|
306
|
+
setParameter("adminAllowedFactor1", ParamType.JsonArray, this, options, "ADMIN_ALLOWED_FACTOR1");
|
|
307
|
+
let options1 = {};
|
|
308
|
+
setParameter("allowedFactor2", ParamType.JsonArray, options1, options, "ALLOWED_FACTOR2");
|
|
309
|
+
this.allowedFactor2Names = options.allowedFactor2 ?? ["none"];
|
|
310
|
+
if (options1.allowedFactor2) {
|
|
311
|
+
for (let factor of options1.allowedFactor2) {
|
|
312
|
+
if (factor in this.authenticators) {
|
|
313
|
+
this.allowedFactor2.push({
|
|
314
|
+
name: factor,
|
|
315
|
+
friendlyName: this.authenticators[factor].friendlyName,
|
|
316
|
+
configurable: this.authenticators[factor].secretNames().length > 0,
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
else if (factor == "none") {
|
|
320
|
+
this.allowedFactor2.push({
|
|
321
|
+
name: "none",
|
|
322
|
+
friendlyName: "None",
|
|
323
|
+
configurable: false
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
setParameter("enableEmailVerification", ParamType.Boolean, this, options, "ENABLE_EMAIL_VERIFICATION");
|
|
329
|
+
setParameter("enablePasswordReset", ParamType.Boolean, this, options, "ENABLE_PASSWORD_RESET");
|
|
330
|
+
setParameter("enableCsrfProtection", ParamType.Boolean, this, options, "ENABLE_CSRF_PROTECTION");
|
|
331
|
+
setParameter("editUserScope", ParamType.String, this, options, "EDIT_USER_SCOPE");
|
|
332
|
+
if (options.validateUserFn)
|
|
333
|
+
this.validateUserFn = options.validateUserFn;
|
|
334
|
+
if (options.createUserFn)
|
|
335
|
+
this.createUserFn = options.createUserFn;
|
|
336
|
+
if (options.updateUserFn)
|
|
337
|
+
this.updateUserFn = options.updateUserFn;
|
|
338
|
+
if (options.addToSession)
|
|
339
|
+
this.addToSession = options.addToSession;
|
|
340
|
+
if (options.validateSession)
|
|
341
|
+
this.validateSession = options.validateSession;
|
|
342
|
+
this.userEndpoints = new SvelteKitUserEndpoints(this, options);
|
|
343
|
+
this.adminEndpoints = new SvelteKitAdminEndpoints(this, options);
|
|
344
|
+
this.userClientEndpoints = new SvelteKitUserClientEndpoints(this, options);
|
|
345
|
+
this.adminClientEndpoints = new SvelteKitAdminClientEndpoints(this, options);
|
|
346
|
+
this.sessionHook = async ({ event } /*, response*/) => {
|
|
347
|
+
CrossauthLogger.logger.debug(j({ msg: "Session hook" }));
|
|
348
|
+
let headers = [];
|
|
349
|
+
let status = undefined;
|
|
350
|
+
const csrfCookieName = this.sessionManager.csrfCookieName;
|
|
351
|
+
const sessionCookieName = this.sessionManager.sessionCookieName;
|
|
352
|
+
//const response = await resolve(event);
|
|
353
|
+
// check if CSRF token is in cookie (and signature is valid)
|
|
354
|
+
// remove it if it is not.
|
|
355
|
+
// we are not checking it matches the CSRF token in the header or
|
|
356
|
+
// body at this stage - just removing invalid cookies
|
|
357
|
+
if (this.enableCsrfProtection) {
|
|
358
|
+
CrossauthLogger.logger.debug(j({ msg: "Getting csrf cookie" }));
|
|
359
|
+
let cookieValue;
|
|
360
|
+
try {
|
|
361
|
+
cookieValue = this.getCsrfCookieValue(event);
|
|
362
|
+
if (cookieValue)
|
|
363
|
+
this.sessionManager.validateCsrfCookie(cookieValue);
|
|
364
|
+
}
|
|
365
|
+
catch (e) {
|
|
366
|
+
CrossauthLogger.logger.warn(j({ msg: "Invalid csrf cookie received", cerr: e, hashedCsrfCookie: this.getHashOfCsrfCookie(event) }));
|
|
367
|
+
try {
|
|
368
|
+
this.clearCookie(csrfCookieName, this.sessionManager.csrfCookiePath, event);
|
|
369
|
+
}
|
|
370
|
+
catch (e2) {
|
|
371
|
+
CrossauthLogger.logger.debug(j({ err: e2 }));
|
|
372
|
+
CrossauthLogger.logger.error(j({ cerr: e2, msg: "Couldn't delete CSRF cookie", ip: event.request.referrer, hashedCsrfCookie: this.getHashOfCsrfCookie(event) }));
|
|
373
|
+
}
|
|
374
|
+
cookieValue = undefined;
|
|
375
|
+
event.locals.csrfToken = undefined;
|
|
376
|
+
}
|
|
377
|
+
if (["GET", "OPTIONS", "HEAD"].includes(event.request.method)) {
|
|
378
|
+
// for get methods, create a CSRF token in the request object and response header
|
|
379
|
+
try {
|
|
380
|
+
if (!cookieValue) {
|
|
381
|
+
CrossauthLogger.logger.debug(j({ msg: "Invalid CSRF cookie - recreating" }));
|
|
382
|
+
const { csrfCookie, csrfFormOrHeaderValue } = await this.sessionManager.createCsrfToken();
|
|
383
|
+
this.setCsrfCookie(csrfCookie, event);
|
|
384
|
+
event.locals.csrfToken = csrfFormOrHeaderValue;
|
|
385
|
+
}
|
|
386
|
+
else {
|
|
387
|
+
CrossauthLogger.logger.debug(j({ msg: "Valid CSRF cookie - creating token" }));
|
|
388
|
+
const csrfFormOrHeaderValue = await this.sessionManager.createCsrfFormOrHeaderValue(cookieValue);
|
|
389
|
+
event.locals.csrfToken = csrfFormOrHeaderValue;
|
|
390
|
+
}
|
|
391
|
+
this.setHeader(CSRFHEADER, event.locals.csrfToken, headers);
|
|
392
|
+
//response.headers.set(CSRFHEADER, event.locals.csrfToken);
|
|
393
|
+
}
|
|
394
|
+
catch (e) {
|
|
395
|
+
CrossauthLogger.logger.error(j({ msg: "Couldn't create CSRF token", cerr: e, user: event.locals.user?.username, hashedSessionCookie: this.getHashOfSessionCookie(event) }));
|
|
396
|
+
CrossauthLogger.logger.debug(j({ err: e }));
|
|
397
|
+
this.clearCookie(csrfCookieName, this.sessionManager.csrfCookiePath, event);
|
|
398
|
+
event.locals.csrfToken = undefined;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
else {
|
|
402
|
+
// for other methods, create a new token only if there is already a valid one
|
|
403
|
+
if (cookieValue) {
|
|
404
|
+
try {
|
|
405
|
+
await this.csrfToken(event, headers);
|
|
406
|
+
}
|
|
407
|
+
catch (e) {
|
|
408
|
+
CrossauthLogger.logger.error(j({ msg: "Couldn't create CSRF token", cerr: e, user: event.locals.user?.username, hashedSessionCookie: this.getHashOfSessionCookie(event) }));
|
|
409
|
+
CrossauthLogger.logger.debug(j({ err: e }));
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
// we now either have a valid CSRF token, or none at all (or CSRF
|
|
415
|
+
// protection has been disabled, in which case the CSRF cookie
|
|
416
|
+
// is ignored)
|
|
417
|
+
// validate any session cookie. Remove if invalid
|
|
418
|
+
event.locals.user = undefined;
|
|
419
|
+
event.locals.authType = undefined;
|
|
420
|
+
const sessionCookieValue = this.getSessionCookieValue(event);
|
|
421
|
+
CrossauthLogger.logger.debug(j({ msg: "Getting session cookie" }));
|
|
422
|
+
if (sessionCookieValue) {
|
|
423
|
+
try {
|
|
424
|
+
const sessionId = this.sessionManager.getSessionId(sessionCookieValue);
|
|
425
|
+
let { key, user } = await this.sessionManager.userForSessionId(sessionId);
|
|
426
|
+
if (this.validateSession)
|
|
427
|
+
this.validateSession(key, user, event);
|
|
428
|
+
const endpoint = event.url.pathname;
|
|
429
|
+
CrossauthLogger.logger.debug(j({ msg: "Session cookie is for user " + user }));
|
|
430
|
+
if (user) { // XXX
|
|
431
|
+
if (this.allowedFactor2.length > 0 &&
|
|
432
|
+
(user.state == UserState.factor2ResetNeeded ||
|
|
433
|
+
!this.allowedFactor2Names.includes(user.factor2 ? user.factor2 : "none"))) {
|
|
434
|
+
if (!this.userEndpoints.configureFactor2Url)
|
|
435
|
+
throw new CrossauthError(ErrorCode.Configuration, "Must set configureFactor2Url in session server");
|
|
436
|
+
if (!this.userEndpoints.changeFactor2Url)
|
|
437
|
+
throw new CrossauthError(ErrorCode.Configuration, "Must set changeFactor2Url in session server");
|
|
438
|
+
if (!this.logoutUrl)
|
|
439
|
+
throw new CrossauthError(ErrorCode.Configuration, "Must set logoutUrl in session server");
|
|
440
|
+
if (!([this.userEndpoints.changeFactor2Url, this.userEndpoints.configureFactor2Url, this.loginUrl, this.logoutUrl].includes(endpoint))) {
|
|
441
|
+
status = 302;
|
|
442
|
+
headers.push({ name: "location", value: this.userEndpoints.changeFactor2Url + "?required=true&next=" + encodeURIComponent("login?next=" + event.url) });
|
|
443
|
+
//this.redirect(302, this.userEndpoints.changeFactor2Url + "?required=true&next="+encodeURIComponent("login?next="+event.url));
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
event.locals.sessionId = sessionId;
|
|
448
|
+
event.locals.user = user;
|
|
449
|
+
event.locals.authType = "cookie";
|
|
450
|
+
CrossauthLogger.logger.debug(j({ msg: "Valid session id", user: user?.username }));
|
|
451
|
+
}
|
|
452
|
+
catch (e) {
|
|
453
|
+
CrossauthLogger.logger.warn(j({ msg: "Invalid session cookie received", hashedSessionCookie: this.getHashOfSessionCookie(event) }));
|
|
454
|
+
this.clearCookie(sessionCookieName, this.sessionManager.sessionCookiePath, event);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
//return response;
|
|
458
|
+
return { headers, status };
|
|
459
|
+
};
|
|
460
|
+
this.twoFAHook = async ({ event }) => {
|
|
461
|
+
CrossauthLogger.logger.debug(j({ msg: "twoFAHook", username: event.locals.user?.username }));
|
|
462
|
+
if (!this.userStorage)
|
|
463
|
+
throw this.error(500, "No user storage defined"); // shouldn't happen as checked in SvelteKitServer
|
|
464
|
+
const sessionCookieValue = this.getSessionCookieValue(event);
|
|
465
|
+
const isFactor2PageProtected = this.isFactor2PageProtected(event);
|
|
466
|
+
const isFactor2ApiProtected = this.isFactor2ApiProtected(event);
|
|
467
|
+
let user;
|
|
468
|
+
if (sessionCookieValue) {
|
|
469
|
+
if (event.locals.user)
|
|
470
|
+
user = event.locals.user;
|
|
471
|
+
else {
|
|
472
|
+
const anonUser = await this.getSessionData(event, "user");
|
|
473
|
+
if (anonUser) {
|
|
474
|
+
const resp = await this.userStorage.getUserByUsername(anonUser.username, { skipActiveCheck: true });
|
|
475
|
+
if (resp.user.status == UserState.active || resp.user.state == UserState.factor2ResetNeeded)
|
|
476
|
+
user = resp.user;
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
if (user && sessionCookieValue && user.factor2 != "" && (isFactor2PageProtected || isFactor2ApiProtected)) {
|
|
481
|
+
CrossauthLogger.logger.debug(j({ msg: "Factor2-protected endpoint visited" }));
|
|
482
|
+
if (!(["GET", "OPTIONS", "HEAD"].includes(event.request.method))) {
|
|
483
|
+
const sessionId = this.sessionManager.getSessionId(sessionCookieValue);
|
|
484
|
+
const sessionData = await this.sessionManager.dataForSessionId(sessionId);
|
|
485
|
+
if (("pre2fa") in sessionData) {
|
|
486
|
+
// 2FA has started - validate it
|
|
487
|
+
CrossauthLogger.logger.debug(j({ msg: "Completing 2FA" }));
|
|
488
|
+
// get secrets from the request body
|
|
489
|
+
const authenticator = this.authenticators[sessionData.pre2fa.factor2];
|
|
490
|
+
const secretNames = [...authenticator.secretNames(), ...authenticator.transientSecretNames()];
|
|
491
|
+
let secrets = {};
|
|
492
|
+
const bodyData = new JsonOrFormData();
|
|
493
|
+
await bodyData.loadData(event);
|
|
494
|
+
for (let field of bodyData.keys()) {
|
|
495
|
+
if (secretNames.includes(field))
|
|
496
|
+
secrets[field] = bodyData.get(field) ?? "";
|
|
497
|
+
}
|
|
498
|
+
const sessionCookieValue = this.getSessionCookieValue(event);
|
|
499
|
+
if (!sessionCookieValue)
|
|
500
|
+
throw new CrossauthError(ErrorCode.Unauthorized, "No session cookie found");
|
|
501
|
+
let error1 = undefined;
|
|
502
|
+
try {
|
|
503
|
+
await this.sessionManager.completeTwoFactorPageVisit(secrets, event.locals.sessionId ?? "");
|
|
504
|
+
}
|
|
505
|
+
catch (e) {
|
|
506
|
+
error1 = CrossauthError.asCrossauthError(e);
|
|
507
|
+
CrossauthLogger.logger.debug(j({ err: e }));
|
|
508
|
+
const ce = CrossauthError.asCrossauthError(e);
|
|
509
|
+
CrossauthLogger.logger.error(j({ msg: error1.message, cerr: e, user: bodyData.get("username"), errorCode: ce.code, errorCodeName: ce.codeName }));
|
|
510
|
+
}
|
|
511
|
+
if (error1) {
|
|
512
|
+
if (error1.code == ErrorCode.Expired) {
|
|
513
|
+
// user will not be able to complete this process - delete
|
|
514
|
+
CrossauthLogger.logger.debug(j({ msg: "Error - cancelling 2FA" }));
|
|
515
|
+
// the 2FA data and start again
|
|
516
|
+
try {
|
|
517
|
+
await this.sessionManager.cancelTwoFactorPageVisit(sessionCookieValue);
|
|
518
|
+
}
|
|
519
|
+
catch (e) {
|
|
520
|
+
CrossauthLogger.logger.error(j({ msg: "Failed cancelling 2FA", cerr: e, user: user.username, hashedSessionCookie: this.getHashOfSessionCookie(event) }));
|
|
521
|
+
CrossauthLogger.logger.debug(j({ err: e }));
|
|
522
|
+
}
|
|
523
|
+
this.error(401, { message: "Sorry, your code has expired" });
|
|
524
|
+
return { ok: false, twofa: true };
|
|
525
|
+
}
|
|
526
|
+
else {
|
|
527
|
+
if (isFactor2PageProtected) {
|
|
528
|
+
return {
|
|
529
|
+
twofa: true,
|
|
530
|
+
ok: false,
|
|
531
|
+
response: new Response('', {
|
|
532
|
+
status: 302,
|
|
533
|
+
statusText: httpStatus(302),
|
|
534
|
+
headers: { Location: this.factor2Url + "?error=" + ErrorCode[error1.code] }
|
|
535
|
+
})
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
else {
|
|
539
|
+
return {
|
|
540
|
+
twofa: true,
|
|
541
|
+
ok: false,
|
|
542
|
+
response: new Response(JSON.stringify({
|
|
543
|
+
ok: false,
|
|
544
|
+
errorMessage: error1.message,
|
|
545
|
+
errorMessages: error1.messages,
|
|
546
|
+
errorCode: error1.code,
|
|
547
|
+
errorCodeName: ErrorCode[error1.code]
|
|
548
|
+
}), {
|
|
549
|
+
status: error1.httpStatus,
|
|
550
|
+
statusText: httpStatus(error1.httpStatus),
|
|
551
|
+
headers: { 'content-tyoe': 'application/json' },
|
|
552
|
+
})
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
// restore original request body
|
|
558
|
+
SvelteKitSessionServer.updateRequest(event, sessionData.pre2fa.body, sessionData.pre2fa["content-type"]);
|
|
559
|
+
return { twofa: true, ok: true };
|
|
560
|
+
}
|
|
561
|
+
else {
|
|
562
|
+
// 2FA has not started - start it
|
|
563
|
+
CrossauthLogger.logger.debug(j({ msg: "Starting 2FA", username: user.username }));
|
|
564
|
+
if (this.enableCsrfProtection && !event.locals.csrfToken) {
|
|
565
|
+
const error = new CrossauthError(ErrorCode.Forbidden, "CSRF token missing");
|
|
566
|
+
return {
|
|
567
|
+
twofa: true,
|
|
568
|
+
ok: false,
|
|
569
|
+
response: new Response(JSON.stringify({
|
|
570
|
+
ok: false,
|
|
571
|
+
errorMessage: error.message,
|
|
572
|
+
errorMessages: error.messages,
|
|
573
|
+
errorCode: error.code,
|
|
574
|
+
errorCodeName: ErrorCode[error.code]
|
|
575
|
+
}), {
|
|
576
|
+
status: error.httpStatus,
|
|
577
|
+
statusText: httpStatus(error.httpStatus),
|
|
578
|
+
headers: {
|
|
579
|
+
...{ 'content-tyoe': 'application/json' },
|
|
580
|
+
}
|
|
581
|
+
})
|
|
582
|
+
};
|
|
583
|
+
}
|
|
584
|
+
const bodyData = new JsonOrFormData();
|
|
585
|
+
await bodyData.loadData(event);
|
|
586
|
+
let contentType = event.request.headers.get("content-type");
|
|
587
|
+
await this.sessionManager.initiateTwoFactorPageVisit(user, event.locals.sessionId ?? "", bodyData.toObject(), event.request.url.replace(/\?.*$/, ""), contentType ? contentType : undefined);
|
|
588
|
+
if (isFactor2PageProtected) {
|
|
589
|
+
return {
|
|
590
|
+
twofa: true,
|
|
591
|
+
ok: true,
|
|
592
|
+
response: new Response('', {
|
|
593
|
+
status: 302,
|
|
594
|
+
statusText: httpStatus(302),
|
|
595
|
+
headers: { Location: this.factor2Url }
|
|
596
|
+
})
|
|
597
|
+
};
|
|
598
|
+
}
|
|
599
|
+
else {
|
|
600
|
+
return {
|
|
601
|
+
twofa: true,
|
|
602
|
+
ok: true,
|
|
603
|
+
response: new Response(JSON.stringify({
|
|
604
|
+
ok: true,
|
|
605
|
+
factor2Required: true
|
|
606
|
+
}), {
|
|
607
|
+
headers: {
|
|
608
|
+
...{ 'content-tyoe': 'application/json' },
|
|
609
|
+
}
|
|
610
|
+
})
|
|
611
|
+
};
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
else {
|
|
616
|
+
CrossauthLogger.logger.debug(j({ msg: "Factor2-protected GET endpoint - cancelling 2FA" }));
|
|
617
|
+
// if we have a get request to one of the protected urls, cancel any pending 2FA
|
|
618
|
+
const sessionCookieValue = this.getSessionCookieValue(event);
|
|
619
|
+
if (sessionCookieValue) {
|
|
620
|
+
const sessionId = this.sessionManager.getSessionId(sessionCookieValue);
|
|
621
|
+
const sessionData = await this.sessionManager.dataForSessionId(sessionId);
|
|
622
|
+
if (("pre2fa") in sessionData) {
|
|
623
|
+
CrossauthLogger.logger.debug(j({ msg: "Cancelling 2FA" }));
|
|
624
|
+
try {
|
|
625
|
+
await this.sessionManager.cancelTwoFactorPageVisit(sessionCookieValue);
|
|
626
|
+
}
|
|
627
|
+
catch (e) {
|
|
628
|
+
CrossauthLogger.logger.debug(j({ err: e }));
|
|
629
|
+
CrossauthLogger.logger.error(j({ msg: "Failed cancelling 2FA", cerr: e, user: user.username, hashedSessionCookie: this.getHashOfSessionCookie(event) }));
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
return { twofa: false, ok: true };
|
|
636
|
+
};
|
|
637
|
+
}
|
|
638
|
+
//////////////
|
|
639
|
+
// Helpers
|
|
640
|
+
/**
|
|
641
|
+
* Returns the session cookie value from the Sveltekit request event
|
|
642
|
+
* @param event the request event
|
|
643
|
+
* @returns the whole cookie value
|
|
644
|
+
*/
|
|
645
|
+
getSessionCookieValue(event) {
|
|
646
|
+
//let allCookies = event.cookies.getAll();
|
|
647
|
+
if (event.cookies && event.cookies.get(this.sessionManager.sessionCookieName)) {
|
|
648
|
+
return event.cookies.get(this.sessionManager.sessionCookieName);
|
|
649
|
+
}
|
|
650
|
+
return undefined;
|
|
651
|
+
}
|
|
652
|
+
/**
|
|
653
|
+
* Returns the session cookie value from the Sveltekit request event
|
|
654
|
+
* @param event the request event
|
|
655
|
+
* @returns the whole cookie value
|
|
656
|
+
*/
|
|
657
|
+
getCsrfCookieValue(event) {
|
|
658
|
+
if (event.cookies) {
|
|
659
|
+
const cookie = event.cookies.get(this.sessionManager.csrfCookieName);
|
|
660
|
+
if (cookie)
|
|
661
|
+
return event.cookies.get(this.sessionManager.csrfCookieName);
|
|
662
|
+
}
|
|
663
|
+
return undefined;
|
|
664
|
+
}
|
|
665
|
+
clearCookie(name, path, event) {
|
|
666
|
+
event.cookies.delete(name, { path });
|
|
667
|
+
}
|
|
668
|
+
/**
|
|
669
|
+
* Sets headers in the request event.
|
|
670
|
+
*
|
|
671
|
+
* Used internally by {@link SvelteKitServer}. Shouldn't be necessary
|
|
672
|
+
* to call this directly.
|
|
673
|
+
* @param headers the headres to set
|
|
674
|
+
* @param resp the response object to set them in
|
|
675
|
+
*/
|
|
676
|
+
setHeaders(headers, resp) {
|
|
677
|
+
for (let header of headers) {
|
|
678
|
+
resp.headers.append(header.name, header.value);
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
/**
|
|
682
|
+
* Sets the CSRF cookie.
|
|
683
|
+
*
|
|
684
|
+
* Used internally. Shouldn't be necessary
|
|
685
|
+
* to call this directly.
|
|
686
|
+
* @param cookie the new cookie and parameters
|
|
687
|
+
* @param event the request event
|
|
688
|
+
*/
|
|
689
|
+
setCsrfCookie(cookie, event) {
|
|
690
|
+
event.cookies.set(cookie.name, cookie.value, toCookieSerializeOptions(cookie.options));
|
|
691
|
+
}
|
|
692
|
+
setHeader(name, value, headers) {
|
|
693
|
+
headers.push({
|
|
694
|
+
name: name,
|
|
695
|
+
value: value,
|
|
696
|
+
});
|
|
697
|
+
}
|
|
698
|
+
/**
|
|
699
|
+
* Returns a hash of the session cookie value.
|
|
700
|
+
*
|
|
701
|
+
* Used only in reporting, so that logs don't contain the actual session ID.
|
|
702
|
+
*
|
|
703
|
+
* @param event the Sveltelkit request event
|
|
704
|
+
* @returns a stering hash of the cookie value
|
|
705
|
+
*/
|
|
706
|
+
getHashOfSessionCookie(event) {
|
|
707
|
+
const cookieValue = this.getSessionCookieValue(event);
|
|
708
|
+
if (!cookieValue)
|
|
709
|
+
return "";
|
|
710
|
+
try {
|
|
711
|
+
return Crypto.hash(cookieValue);
|
|
712
|
+
}
|
|
713
|
+
catch (e) { }
|
|
714
|
+
return "";
|
|
715
|
+
}
|
|
716
|
+
/**
|
|
717
|
+
* Returns a hash of the CSRF cookie value.
|
|
718
|
+
*
|
|
719
|
+
* Used only in reporting, so that logs don't contain the actual CSRF cookie value.
|
|
720
|
+
*
|
|
721
|
+
* @param event the Sveltelkit request event
|
|
722
|
+
* @returns a stering hash of the cookie value
|
|
723
|
+
*/
|
|
724
|
+
getHashOfCsrfCookie(event) {
|
|
725
|
+
const cookieValue = this.getCsrfCookieValue(event);
|
|
726
|
+
if (!cookieValue)
|
|
727
|
+
return "";
|
|
728
|
+
try {
|
|
729
|
+
return Crypto.hash(cookieValue);
|
|
730
|
+
}
|
|
731
|
+
catch (e) { }
|
|
732
|
+
return "";
|
|
733
|
+
}
|
|
734
|
+
/**
|
|
735
|
+
* Returns a CSRF token if the CSRF cookie is valid.
|
|
736
|
+
*
|
|
737
|
+
* Used internally. Shouldn't be necessary
|
|
738
|
+
* to call this directly.
|
|
739
|
+
*
|
|
740
|
+
* @param event the request event
|
|
741
|
+
* @param headers headers the token will be added to, as well as
|
|
742
|
+
* adding it to locals
|
|
743
|
+
* @returns the string CSRF token for inclusion in forms
|
|
744
|
+
*/
|
|
745
|
+
async csrfToken(event, headers) {
|
|
746
|
+
let token = undefined;
|
|
747
|
+
// first try token in header
|
|
748
|
+
if (event.request.headers && event.request.headers.has(CSRFHEADER.toLowerCase())) {
|
|
749
|
+
const header = event.request.headers.get(CSRFHEADER.toLowerCase());
|
|
750
|
+
if (Array.isArray(header))
|
|
751
|
+
token = header[0];
|
|
752
|
+
else if (header)
|
|
753
|
+
token = header;
|
|
754
|
+
}
|
|
755
|
+
// if not in header, try in body
|
|
756
|
+
if (!token) {
|
|
757
|
+
if (!event.request?.body) {
|
|
758
|
+
CrossauthLogger.logger.warn(j({ msg: "Received CSRF header but not token", ip: event.request.referrerPolicy, hashedCsrfCookie: this.getHashOfCsrfCookie(event) }));
|
|
759
|
+
return;
|
|
760
|
+
}
|
|
761
|
+
const contentType = event.request.headers.get("content-type");
|
|
762
|
+
if (contentType == "application/json") {
|
|
763
|
+
const body = await event.request?.clone()?.json();
|
|
764
|
+
token = body.csrfToken;
|
|
765
|
+
}
|
|
766
|
+
else if (contentType == "application/x-www-form-urlencoded" || contentType == "multipart/form-data") {
|
|
767
|
+
const body = await event.request.clone().formData();
|
|
768
|
+
const formValue = body.get("csrfToken");
|
|
769
|
+
if (formValue && typeof formValue == "string")
|
|
770
|
+
token = formValue;
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
if (token) {
|
|
774
|
+
try {
|
|
775
|
+
this.sessionManager.validateDoubleSubmitCsrfToken(this.getCsrfCookieValue(event), token);
|
|
776
|
+
event.locals.csrfToken = token;
|
|
777
|
+
//resp.headers.set(CSRFHEADER, token);
|
|
778
|
+
this.setHeader(CSRFHEADER, token, headers);
|
|
779
|
+
}
|
|
780
|
+
catch (e) {
|
|
781
|
+
CrossauthLogger.logger.warn(j({ msg: "Invalid CSRF token", hashedCsrfCookie: this.getHashOfCsrfCookie(event) }));
|
|
782
|
+
this.clearCookie(this.sessionManager.csrfCookieName, this.sessionManager.csrfCookiePath, event);
|
|
783
|
+
event.locals.csrfToken = undefined;
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
else {
|
|
787
|
+
event.locals.csrfToken = undefined;
|
|
788
|
+
}
|
|
789
|
+
return token;
|
|
790
|
+
}
|
|
791
|
+
/**
|
|
792
|
+
* Used internally to update an existing Sveltekit request object with
|
|
793
|
+
* a new body and headers.
|
|
794
|
+
*
|
|
795
|
+
* Used when restoring a request that was interrupted for 2FA
|
|
796
|
+
*
|
|
797
|
+
* @param event the request event
|
|
798
|
+
* @param params JSON params to add to the new body
|
|
799
|
+
* @param contentType the new content type
|
|
800
|
+
* @returns the updated request event
|
|
801
|
+
*/
|
|
802
|
+
static updateRequest(event, params, contentType) {
|
|
803
|
+
//const contentType = event.headers.get('content-type');
|
|
804
|
+
//const newContentType = contentType == 'application/json' ? 'application/json' : 'application/x-www-form-urlencoded';
|
|
805
|
+
let body;
|
|
806
|
+
if (contentType == 'application/json') {
|
|
807
|
+
body = JSON.stringify(params);
|
|
808
|
+
}
|
|
809
|
+
else {
|
|
810
|
+
body = "";
|
|
811
|
+
for (let name in params) {
|
|
812
|
+
const value = params[name];
|
|
813
|
+
if (body.length > 0)
|
|
814
|
+
body += "&";
|
|
815
|
+
body += encodeURIComponent(name) + "=" + encodeURIComponent(value);
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
event.request = new Request(event.request.url, {
|
|
819
|
+
method: "POST",
|
|
820
|
+
headers: event.request.headers,
|
|
821
|
+
body: body
|
|
822
|
+
});
|
|
823
|
+
return event;
|
|
824
|
+
}
|
|
825
|
+
/**
|
|
826
|
+
* Returns a hash of the session ID. Used for logging (for security,
|
|
827
|
+
* the actual session ID is not logged)
|
|
828
|
+
* @param event the Sveltekit request event
|
|
829
|
+
* @returns hash of the session ID
|
|
830
|
+
*/
|
|
831
|
+
getHashOfSessionId(event) {
|
|
832
|
+
if (!event.locals.sessionId)
|
|
833
|
+
return "";
|
|
834
|
+
try {
|
|
835
|
+
return Crypto.hash(event.locals.sessionId);
|
|
836
|
+
}
|
|
837
|
+
catch (e) { }
|
|
838
|
+
return "";
|
|
839
|
+
}
|
|
840
|
+
/**
|
|
841
|
+
* Returns whether or not 2FA authentication was initiated as a result
|
|
842
|
+
* of visiting a page protected by it
|
|
843
|
+
* @param event the request event
|
|
844
|
+
* @returns true or false
|
|
845
|
+
*/
|
|
846
|
+
async factor2PageVisitStarted(event) {
|
|
847
|
+
try {
|
|
848
|
+
const pre2fa = this.getSessionData(event, "pre2fa");
|
|
849
|
+
return pre2fa != undefined;
|
|
850
|
+
}
|
|
851
|
+
catch (e) {
|
|
852
|
+
const ce = CrossauthError.asCrossauthError(e);
|
|
853
|
+
CrossauthLogger.logger.debug(j({ err: ce }));
|
|
854
|
+
CrossauthLogger.logger.error(j({ cerr: ce, msg: "Couldn't get pre2fa data from session" }));
|
|
855
|
+
return false;
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
/////////////////////////////////////////////////////////////
|
|
859
|
+
// login protected URLs
|
|
860
|
+
/**
|
|
861
|
+
* Returns whether a page being visited as part of a request event is
|
|
862
|
+
* configured to be protected by login.
|
|
863
|
+
*
|
|
864
|
+
* See {@link SvelteKitSessionServerOptions.loginProtectedPageEndpoints} and
|
|
865
|
+
* {@link SvelteKitSessionServerOptions.loginProtectedExceptionPageEndpoints}.
|
|
866
|
+
*
|
|
867
|
+
* @param event the request event
|
|
868
|
+
* @returns true or false
|
|
869
|
+
*/
|
|
870
|
+
isLoginPageProtected(event) {
|
|
871
|
+
const url = new URL(typeof event == "string" ? event : event.request.url);
|
|
872
|
+
// login page is never protected
|
|
873
|
+
if (url.pathname == this.loginUrl)
|
|
874
|
+
return false;
|
|
875
|
+
// return false for loginProtectedExceptionPageEndpoints
|
|
876
|
+
let isNotProtected = false;
|
|
877
|
+
isNotProtected = this.loginProtectedExceptionPageEndpoints.reduce((accumulator, currentValue) => accumulator || minimatch(url.pathname, currentValue), isNotProtected);
|
|
878
|
+
if (isNotProtected)
|
|
879
|
+
return false;
|
|
880
|
+
// set protected to true for any pages that are in loginProtectedPageEndpoints
|
|
881
|
+
let isProtected = false;
|
|
882
|
+
return this.loginProtectedPageEndpoints.reduce((accumulator, currentValue) => accumulator || minimatch(url.pathname, currentValue), isProtected);
|
|
883
|
+
}
|
|
884
|
+
/**
|
|
885
|
+
* Returns whether an API call is being visited as part of a request event is
|
|
886
|
+
* configured to be protected by login.
|
|
887
|
+
*
|
|
888
|
+
* See {@link SvelteKitSessionServerOptions.loginProtectedApiEndpoints}.
|
|
889
|
+
*
|
|
890
|
+
* @param event the request event
|
|
891
|
+
* @returns true or false
|
|
892
|
+
*/
|
|
893
|
+
isLoginApiProtected(event) {
|
|
894
|
+
const url = new URL(typeof event == "string" ? event : event.request.url);
|
|
895
|
+
// login page is never protected
|
|
896
|
+
if (url.pathname == this.loginUrl)
|
|
897
|
+
return false;
|
|
898
|
+
// return false for loginProtectedExceptionApiEndpoints
|
|
899
|
+
let isNotProtected = false;
|
|
900
|
+
isNotProtected = this.loginProtectedExceptionApiEndpoints.reduce((accumulator, currentValue) => accumulator || minimatch(url.pathname, currentValue), isNotProtected);
|
|
901
|
+
if (isNotProtected)
|
|
902
|
+
return false;
|
|
903
|
+
let isProtected = false;
|
|
904
|
+
return this.loginProtectedApiEndpoints.reduce((accumulator, currentValue) => accumulator || minimatch(url.pathname, currentValue), isProtected);
|
|
905
|
+
}
|
|
906
|
+
/**
|
|
907
|
+
* Returns whether a page being visited as part of a request event is
|
|
908
|
+
* configured to be protected by 2FA.
|
|
909
|
+
*
|
|
910
|
+
* See {@link SvelteKitSessionServerOptions.factor2ProtectedPageEndpoints}.
|
|
911
|
+
*
|
|
912
|
+
* @param event the request event
|
|
913
|
+
* @returns true or false
|
|
914
|
+
*/
|
|
915
|
+
isFactor2PageProtected(event) {
|
|
916
|
+
const url = new URL(typeof event == "string" ? event : event.request.url);
|
|
917
|
+
let isProtected = false;
|
|
918
|
+
return this.factor2ProtectedPageEndpoints.reduce((accumulator, currentValue) => accumulator || minimatch(url.pathname, currentValue), isProtected);
|
|
919
|
+
//return (this.loginProtectedPageEndpoints.includes(url.pathname));
|
|
920
|
+
}
|
|
921
|
+
/**
|
|
922
|
+
* Returns whether an API call is being visited as part of a request event is
|
|
923
|
+
* configured to be protected by 2FA.
|
|
924
|
+
*
|
|
925
|
+
* See {@link SvelteKitSessionServerOptions.factor2ProtectedApiEndpoints}.
|
|
926
|
+
*
|
|
927
|
+
* @param event the request event
|
|
928
|
+
* @returns true or false
|
|
929
|
+
*/
|
|
930
|
+
isFactor2ApiProtected(event) {
|
|
931
|
+
const url = new URL(typeof event == "string" ? event : event.request.url);
|
|
932
|
+
//return (this.loginProtectedApiEndpoints.includes(url.pathname));
|
|
933
|
+
let isProtected = false;
|
|
934
|
+
return this.factor2ProtectedApiEndpoints.reduce((accumulator, currentValue) => accumulator || minimatch(url.pathname, currentValue), isProtected);
|
|
935
|
+
}
|
|
936
|
+
/**
|
|
937
|
+
* Returns whether a page being visited as part of a request event is
|
|
938
|
+
* configured to be protected as admin only.
|
|
939
|
+
*
|
|
940
|
+
* See {@link SvelteKitSessionServerOptions.adminPageEndpoints}.
|
|
941
|
+
*
|
|
942
|
+
* @param event the request event
|
|
943
|
+
* @returns true or false
|
|
944
|
+
*/
|
|
945
|
+
isAdminPageEndpoint(event) {
|
|
946
|
+
const url = new URL(typeof event == "string" ? event : event.request.url);
|
|
947
|
+
//return (this.adminEndpoints.includes(url.pathname));
|
|
948
|
+
// return false for adminProtectedExceptionPageEndpoints
|
|
949
|
+
let isNotProtected = false;
|
|
950
|
+
isNotProtected = this.adminProtectedExceptionPageEndpoints.reduce((accumulator, currentValue) => accumulator || minimatch(url.pathname, currentValue), isNotProtected);
|
|
951
|
+
if (isNotProtected)
|
|
952
|
+
return false;
|
|
953
|
+
isNotProtected = this.loginProtectedExceptionPageEndpoints.reduce((accumulator, currentValue) => accumulator || minimatch(url.pathname, currentValue), isNotProtected);
|
|
954
|
+
if (isNotProtected)
|
|
955
|
+
return false;
|
|
956
|
+
let isAdmin = false;
|
|
957
|
+
return this.adminPageEndpoints.reduce((accumulator, currentValue) => accumulator || minimatch(url.pathname, currentValue), isAdmin);
|
|
958
|
+
}
|
|
959
|
+
/**
|
|
960
|
+
* Returns whether an AP call being visited as part of a request event is
|
|
961
|
+
* configured to be protected as admin only.
|
|
962
|
+
*
|
|
963
|
+
* See {@link SvelteKitSessionServerOptions.adminApiEndpoints}.
|
|
964
|
+
*
|
|
965
|
+
* @param event the request event
|
|
966
|
+
* @returns true or false
|
|
967
|
+
*/
|
|
968
|
+
isAdminApiEndpoint(event) {
|
|
969
|
+
const url = new URL(typeof event == "string" ? event : event.request.url);
|
|
970
|
+
//return (this.adminEndpoints.includes(url.pathname));
|
|
971
|
+
// return false for adminProtectedExceptionApiEndpoints
|
|
972
|
+
let isNotProtected = false;
|
|
973
|
+
isNotProtected = this.adminProtectedExceptionApiEndpoints.reduce((accumulator, currentValue) => accumulator || minimatch(url.pathname, currentValue), isNotProtected);
|
|
974
|
+
if (isNotProtected)
|
|
975
|
+
return false;
|
|
976
|
+
isNotProtected = this.loginProtectedExceptionApiEndpoints.reduce((accumulator, currentValue) => accumulator || minimatch(url.pathname, currentValue), isNotProtected);
|
|
977
|
+
if (isNotProtected)
|
|
978
|
+
return false;
|
|
979
|
+
let isAdmin = false;
|
|
980
|
+
return this.adminApiEndpoints.reduce((accumulator, currentValue) => accumulator || minimatch(url.pathname, currentValue), isAdmin);
|
|
981
|
+
}
|
|
982
|
+
/**
|
|
983
|
+
* Creates an anonymous session, setting the `Set-Cookue` headers
|
|
984
|
+
* in the reply.
|
|
985
|
+
*
|
|
986
|
+
* An anonymous sessiin is a session cookie that is not associated
|
|
987
|
+
* with a user (`userid` is undefined). It can be used to persist
|
|
988
|
+
* data between sessions just like a regular user session ID.
|
|
989
|
+
*
|
|
990
|
+
* @param event the SvelteKit reqzest event
|
|
991
|
+
* @param data session data to save
|
|
992
|
+
* @returns the session cookie value
|
|
993
|
+
*/
|
|
994
|
+
async createAnonymousSession(event, data) {
|
|
995
|
+
CrossauthLogger.logger.debug(j({ msg: "Creating anonympous session ID " }));
|
|
996
|
+
// get custom fields from implentor-provided function
|
|
997
|
+
const formData = new JsonOrFormData();
|
|
998
|
+
await formData.loadData(event);
|
|
999
|
+
let extraFields = this.addToSession ? this.addToSession(event, formData.toObject()) : {};
|
|
1000
|
+
if (data)
|
|
1001
|
+
extraFields.data = JSON.stringify(data);
|
|
1002
|
+
// create session, setting the session cookie, CSRF cookie and CSRF token
|
|
1003
|
+
let { sessionCookie, csrfCookie, csrfFormOrHeaderValue } = await this.sessionManager.createAnonymousSession(extraFields);
|
|
1004
|
+
event.cookies.set(sessionCookie.name, sessionCookie.value, toCookieSerializeOptions(sessionCookie.options));
|
|
1005
|
+
if (this.enableCsrfProtection) {
|
|
1006
|
+
event.locals.csrfToken = csrfFormOrHeaderValue;
|
|
1007
|
+
event.cookies.set(csrfCookie.name, csrfCookie.value, toCookieSerializeOptions(csrfCookie.options));
|
|
1008
|
+
}
|
|
1009
|
+
event.locals.user = undefined;
|
|
1010
|
+
const sessionId = this.sessionManager.getSessionId(sessionCookie.value);
|
|
1011
|
+
event.locals.sessionId = sessionId;
|
|
1012
|
+
return sessionCookie.value;
|
|
1013
|
+
}
|
|
1014
|
+
;
|
|
1015
|
+
/**
|
|
1016
|
+
* Sets locals based on session and CSRF cookies.
|
|
1017
|
+
*
|
|
1018
|
+
* Sets things like `locals.user`. You can call this if you need them
|
|
1019
|
+
* updated based on cookie settings and a page load hasn't been done
|
|
1020
|
+
* (ie the hooks haven't run).
|
|
1021
|
+
*
|
|
1022
|
+
* @param event the Sveltekit request event.
|
|
1023
|
+
*/
|
|
1024
|
+
async refreshLocals(event) {
|
|
1025
|
+
try {
|
|
1026
|
+
const sessionCookieValue = this.getSessionCookieValue(event);
|
|
1027
|
+
if (sessionCookieValue) {
|
|
1028
|
+
const sessionId = this.sessionManager.getSessionId(sessionCookieValue);
|
|
1029
|
+
event.locals.sessionId = sessionId;
|
|
1030
|
+
const resp = await this.sessionManager.userForSessionId(sessionId);
|
|
1031
|
+
event.locals.user = resp.user;
|
|
1032
|
+
}
|
|
1033
|
+
else {
|
|
1034
|
+
event.locals.sessionId = undefined;
|
|
1035
|
+
event.locals.user = undefined;
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
catch (e) {
|
|
1039
|
+
CrossauthLogger.logger.error(j({ errr: e }));
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
////////////////////////////////////////////////////////////////
|
|
1043
|
+
// SessionAdapter interface
|
|
1044
|
+
csrfProtectionEnabled() {
|
|
1045
|
+
return this.enableCsrfProtection;
|
|
1046
|
+
}
|
|
1047
|
+
getCsrfToken(event) {
|
|
1048
|
+
return event.locals.csrfToken;
|
|
1049
|
+
}
|
|
1050
|
+
getUser(event) {
|
|
1051
|
+
return event.locals.user;
|
|
1052
|
+
}
|
|
1053
|
+
/**
|
|
1054
|
+
* Returns the data stored along with the session server-side, with the
|
|
1055
|
+
* given name
|
|
1056
|
+
* @param event the Sveltekit request event
|
|
1057
|
+
* @param name tjhe data name to return
|
|
1058
|
+
* @returns an object or undefined.
|
|
1059
|
+
*/
|
|
1060
|
+
async getSessionData(event, name) {
|
|
1061
|
+
try {
|
|
1062
|
+
const data = event.locals.sessionId ?
|
|
1063
|
+
await this.sessionManager.dataForSessionId(event.locals.sessionId) :
|
|
1064
|
+
undefined;
|
|
1065
|
+
if (data && name in data)
|
|
1066
|
+
return data[name];
|
|
1067
|
+
}
|
|
1068
|
+
catch (e) {
|
|
1069
|
+
CrossauthLogger.logger.error(j({
|
|
1070
|
+
msg: "Couldn't get " + name + "from session",
|
|
1071
|
+
cerr: e
|
|
1072
|
+
}));
|
|
1073
|
+
CrossauthLogger.logger.debug(j({ err: e }));
|
|
1074
|
+
}
|
|
1075
|
+
return undefined;
|
|
1076
|
+
}
|
|
1077
|
+
/**
|
|
1078
|
+
* Updates or sets the given field in the session `data` field.
|
|
1079
|
+
*
|
|
1080
|
+
* The `data` field in the session record is assumed to be JSON
|
|
1081
|
+
*
|
|
1082
|
+
* @param event the Sveltekit request event
|
|
1083
|
+
* @param name the name of the field to set
|
|
1084
|
+
* @param value the value to set it to.
|
|
1085
|
+
*/
|
|
1086
|
+
async updateSessionData(event, name, value) {
|
|
1087
|
+
if (!event.locals.sessionId)
|
|
1088
|
+
throw new CrossauthError(ErrorCode.Unauthorized, "No session present");
|
|
1089
|
+
await this.sessionManager.updateSessionData(event.locals.sessionId, name, value);
|
|
1090
|
+
}
|
|
1091
|
+
async updateManySessionData(event, dataArray) {
|
|
1092
|
+
if (!event.locals.sessionId)
|
|
1093
|
+
throw new CrossauthError(ErrorCode.Unauthorized, "No session present");
|
|
1094
|
+
await this.sessionManager.updateManySessionData(event.locals.sessionId, dataArray);
|
|
1095
|
+
}
|
|
1096
|
+
/**
|
|
1097
|
+
* Deletes the given field from the session `data` field.
|
|
1098
|
+
*
|
|
1099
|
+
* The `data` field in the session record is assumed to be JSON
|
|
1100
|
+
*
|
|
1101
|
+
* @param event the Sveltekit request event
|
|
1102
|
+
* @param name the name of the field to set
|
|
1103
|
+
*/
|
|
1104
|
+
async deleteSessionData(event, name) {
|
|
1105
|
+
if (!event.locals.sessionId) {
|
|
1106
|
+
CrossauthLogger.logger.debug(j({ msg: `Attempting to delete session data ${name} when no session is present` }));
|
|
1107
|
+
}
|
|
1108
|
+
else {
|
|
1109
|
+
await this.sessionManager.deleteSessionData(event.locals.sessionId, name);
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
}
|