@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.
Files changed (47) hide show
  1. package/README.md +1 -1
  2. package/dist/index.d.ts +1 -1
  3. package/dist/index.js +16 -6181
  4. package/dist/sveltekitadminclientendpoints.d.ts +13 -12
  5. package/dist/sveltekitadminclientendpoints.js +187 -0
  6. package/dist/sveltekitadminendpoints.d.ts +5 -4
  7. package/dist/sveltekitadminendpoints.js +766 -0
  8. package/dist/sveltekitapikey.d.ts +4 -3
  9. package/dist/sveltekitapikey.js +81 -0
  10. package/dist/sveltekitoauthclient.d.ts +6 -4
  11. package/dist/sveltekitoauthclient.js +2309 -0
  12. package/dist/sveltekitoauthserver.d.ts +4 -4
  13. package/dist/sveltekitoauthserver.js +1350 -0
  14. package/dist/sveltekitresserver.d.ts +6 -4
  15. package/dist/sveltekitresserver.js +286 -0
  16. package/dist/sveltekitserver.d.ts +11 -9
  17. package/dist/sveltekitserver.js +393 -0
  18. package/dist/sveltekitsession.d.ts +6 -5
  19. package/dist/sveltekitsession.js +1112 -0
  20. package/dist/sveltekitsessionadapter.d.ts +2 -3
  21. package/dist/sveltekitsessionadapter.js +2 -0
  22. package/dist/sveltekitsharedclientendpoints.d.ts +7 -6
  23. package/dist/sveltekitsharedclientendpoints.js +630 -0
  24. package/dist/sveltekituserclientendpoints.d.ts +13 -12
  25. package/dist/sveltekituserclientendpoints.js +270 -0
  26. package/dist/sveltekituserendpoints.d.ts +6 -5
  27. package/dist/sveltekituserendpoints.js +1813 -0
  28. package/dist/tests/sveltekitadminclientendpoints.test.js +330 -0
  29. package/dist/tests/sveltekitadminendpoints.test.js +242 -0
  30. package/dist/tests/sveltekitapikeyserver.test.js +44 -0
  31. package/dist/tests/sveltekitoauthclient.test.d.ts +5 -5
  32. package/dist/tests/sveltekitoauthclient.test.js +1016 -0
  33. package/dist/tests/sveltekitoauthresserver.test.d.ts +4 -4
  34. package/dist/tests/sveltekitoauthresserver.test.js +185 -0
  35. package/dist/tests/sveltekitoauthserver.test.js +673 -0
  36. package/dist/tests/sveltekituserclientendpoints.test.js +244 -0
  37. package/dist/tests/sveltekituserendpoints.test.js +152 -0
  38. package/dist/tests/sveltemock.test.js +36 -0
  39. package/dist/tests/sveltemocks.d.ts +22 -8
  40. package/dist/tests/sveltemocks.js +114 -0
  41. package/dist/tests/sveltesessionhooks.test.js +224 -0
  42. package/dist/tests/testshared.d.ts +8 -8
  43. package/dist/tests/testshared.js +344 -0
  44. package/dist/utils.d.ts +1 -2
  45. package/dist/utils.js +123 -0
  46. package/package.json +23 -15
  47. package/dist/index.cjs +0 -1
@@ -0,0 +1,1813 @@
1
+ // Copyright (c) 2026 Matthew Baker. All rights reserved. Licenced under the Apache Licence 2.0. See LICENSE file
2
+ import { SvelteKitServer } from './sveltekitserver';
3
+ import { SvelteKitSessionServer } from './sveltekitsession';
4
+ import { toCookieSerializeOptions, setParameter, ParamType, } from '@crossauth/backend';
5
+ import { CrossauthError, CrossauthLogger, j, ErrorCode, UserState } from '@crossauth/common';
6
+ import { JsonOrFormData } from './utils';
7
+ //////////////////////////////////////////////////////////////////////
8
+ // Class
9
+ /**
10
+ * Provides endpoints for users to login, logout and maintain their
11
+ * own account.
12
+ *
13
+ * This is created automatically when {@link SvelteKitServer} is instantiated.
14
+ * The endpoints are available through `SvelteKitServer.sessionServer.userEndpoints`.
15
+ *
16
+ * The methods in this class are designed to be used in
17
+ * `+*_server.ts` files in the `load` and `actions` exports. You can
18
+ * either use the low-level functions such as {@link changePassword} or use
19
+ * the `action` and `load` members of the endpoint objects.
20
+ * For example, for {@link changePasswordEndpoint}
21
+ *
22
+ * ```
23
+ * export const load = crossauth.sessionServer?.userEndpoints.changeFactor2Endpoint.load ?? crossauth.dummyLoad;
24
+ * export const actions = crossauth.sessionServer?.userEndpoints.changeFactor2Endpoint.actions ?? crossauth.dummyActions;
25
+ * ```
26
+ * The `?? crossauth.dummyLoad` and `?? crossauth.dummyActions` is to stop
27
+ * typescript complaining as the `sessionServer` member of the
28
+ * {@link SvelteKitServer} object may be undefined, because
29
+ * some application do not have a session server.
30
+ *
31
+ * **Endpoints**
32
+ *
33
+ * | Name | Description | PageData (returned by load) | ActionData (return by actions) | Form fields expected by actions | URL param |
34
+ * | -------------------------- | ---------------------------------------------------------- | ---------------------------------------------------------------------------- | ---------------------------------------------------------------- | --------------------------------------------------------------- | --------- |
35
+ * | baseEndpoint | This PageData is returned by all endpoints' load function. | - `user` logged in {@link @crossauth/common!User} | *Not provided* | | |
36
+ * | | | - `csrfToken` CSRF token if enabled | | | | | loginPage |
37
+ * | -------------------------- | ---------------------------------------------------------- | ---------------------------------------------------------------------------- | ---------------------------------------------------------------- | --------------------------------------------------------------- | --------- |
38
+ * | signupEndpoint | Create a user and sign in | - `allowedFactor2` array of: | `default`: | `default`: | |
39
+ * | | | - `name` name that is in user's `factor2` | - see {@link SvelteKitUserEndpoints.signup} return | - see {@link SvelteKitUserEndpoints.signup} event | |
40
+ * | | | - `friendlyName` for showing in form | | | |
41
+ * | -------------------------- | ---------------------------------------------------------- | ---------------------------------------------------------------------------- | ---------------------------------------------------------------- | --------------------------------------------------------------- | --------- |
42
+ * | loginEndpoint | Logs a user in | - `next` page to redirect to on ok | `login`: starts login | `login`: | |
43
+ * | | | | - see {@link SvelteKitUserEndpoints.login} return | - see {@link SvelteKitUserEndpoints.login} event | |
44
+ * | | | | `factor2`: submit 2FA data to complete login | `factor2`: | |
45
+ * | | | | - see {@link SvelteKitUserEndpoints.loginFactor2} return | - see {@link SvelteKitUserEndpoints.loginFactor2} event | |
46
+ * | -------------------------- | ---------------------------------------------------------- | ---------------------------------------------------------------------------- | ---------------------------------------------------------------- | --------------------------------------------------------------- | --------- |
47
+ * | factor2Endpoint | Called when 2FA authentication is needed | See {@link SvelteKitUserEndpoints.requestFactor2} return | *Not provided* | | |
48
+ * | -------------------------- | ---------------------------------------------------------- | ---------------------------------------------------------------------------- | ---------------------------------------------------------------- | --------------------------------------------------------------- | --------- |
49
+ * | logoutEndpoint | Logs a user out | Just `baseEndpoint` data | `default`: | `default`: | |
50
+ * | | | | - see {@link SvelteKitUserEndpoints.logout} return | - see {@link SvelteKitUserEndpoints.logout} event | |
51
+ * | -------------------------- | ---------------------------------------------------------- | ---------------------------------------------------------------------------- | ---------------------------------------------------------------- | --------------------------------------------------------------- | --------- |
52
+ * | changeFactor2Endpoint | Change user's factor2 method or reconfigure existing | - `next` page to redirect to on ok | `change`: change to a different factor2 | `change`: | |
53
+ * | | | - `required` if true, this was called because the user must | - see {@link SvelteKitUserEndpoints.changeFactor2} return | - see {@link SvelteKitUserEndpoints.changeFactor2} event | |
54
+ * | | | eg if user's `state` set to `factor2ResetRequired` | `factor2`: submit 2FA data to complete login | `factor2`: | |
55
+ * | | | - `username` the user's username (`user` not set if not fully logged in yet) | - see {@link SvelteKitUserEndpoints.loginFactor2} return | - see {@link SvelteKitUserEndpoints.loginFactor2} event | |
56
+ * | | | - `allowedFactor2` see PageData for `signupEndpoint` | | | |
57
+ * | -------------------------- | ---------------------------------------------------------- | ---------------------------------------------------------------------------- | ---------------------------------------------------------------- | --------------------------------------------------------------- | --------- |
58
+ * | changePasswordEndpoint | Change user's factor2 method or reconfigure existing | - `next` page to redirect to on ok | `default`: | `default`: | |
59
+ * | | | - `required` if true, this was called because the user must | - see {@link SvelteKitUserEndpoints.changePassword} return | - see {@link SvelteKitUserEndpoints.changePassword} event | |
60
+ * | -------------------------- | ---------------------------------------------------------- | ---------------------------------------------------------------------------- | ---------------------------------------------------------------- | --------------------------------------------------------------- | --------- |
61
+ * | configureFactor2Endpoint | Configure secrets for user's factor2 | Just `baseEndpoint` data | `default`: | `default`: | |
62
+ * | | | | - see {@link SvelteKitUserEndpoints.configureFactor2} return | - see {@link SvelteKitUserEndpoints.configureFactor2} event | |
63
+ * | -------------------------- | ---------------------------------------------------------- | ---------------------------------------------------------------------------- | ---------------------------------------------------------------- | --------------------------------------------------------------- | --------- |
64
+ * | deleteUserEndpoint | Delete the logged in user | Just `baseEndpoint` data | `default`: | `default`: | |
65
+ * | | | | - see {@link SvelteKitUserEndpoints.deleteUser} return | - see {@link SvelteKitUserEndpoints.deleteUser} event | |
66
+ * | -------------------------- | ---------------------------------------------------------- | ---------------------------------------------------------------------------- | ---------------------------------------------------------------- | --------------------------------------------------------------- | --------- |
67
+ * | resetPasswordEndpoint | Requests and password reset and emails token to user | - `next` page to redirect to on ok | `default`: | `default`: | |
68
+ * | | | - `required` if true, this was called because the user must | - see {@link SvelteKitUserEndpoints.requestPasswordReset} return | - see {@link SvelteKitUserEndpoints.requestPasswordReset} event | |
69
+ * | | | | | | |
70
+ * | -------------------------- | ---------------------------------------------------------- | ---------------------------------------------------------------------------- | ---------------------------------------------------------------- | --------------------------------------------------------------- | --------- |
71
+ * | passwordResetTokenEndpoint | Validates emailed token and executes a password reset | - `tokenValidates` true if the token is valid | `default`: | `default`: | `token` |
72
+ * | | | - `error` error message if token is not valid | - see {@link SvelteKitUserEndpoints.resetPassword} return | - see {@link SvelteKitUserEndpoints.resetPassword} event | |
73
+ * | -------------------------- | ---------------------------------------------------------- | ---------------------------------------------------------------------------- | ---------------------------------------------------------------- | --------------------------------------------------------------- | --------- |
74
+ * | updateUserEndpoint | Update currently-logged in user's details | - `allowedFactor2` see PageData for `signupEndpoint` | `default`: | `default`: | |
75
+ * | | | - `required` if true, this was called because the user must | - see {@link SvelteKitUserEndpoints.updateUser} return | - see {@link SvelteKitUserEndpoints.updateUser} event | |
76
+ * | -------------------------- | ---------------------------------------------------------- | ---------------------------------------------------------------------------- | ---------------------------------------------------------------- | --------------------------------------------------------------- | --------- |
77
+ * | verifyEmailTokenEndpoint | Validates an email verification token emailed to user | - `user` corresponding {@link @crossauth/common!User} if token is valid | *None provided* | | `token` |
78
+ * | | | - `error` error message if token validation failed | | | |
79
+ * | | | - `ok` true if validation was successful, false otherwise | | | |
80
+ */
81
+ export class SvelteKitUserEndpoints {
82
+ sessionServer;
83
+ changePasswordUrl = undefined; //"/changepassword";
84
+ changeFactor2Url = undefined; //"/changefactor2";
85
+ configureFactor2Url = undefined; //"/configurefactor2";
86
+ requestPasswordResetUrl = undefined; //"/resetpassword";
87
+ loginRedirectUrl = "/";
88
+ loginUrl = "/login";
89
+ addToSession;
90
+ constructor(sessionServer, options) {
91
+ this.sessionServer = sessionServer;
92
+ setParameter("changePasswordUrl", ParamType.String, this, options, "CHANGE_PASSWORD_URL");
93
+ setParameter("requestPasswordResetUrl", ParamType.String, this, options, "REQUEST_PASSWORD_RESET_URL");
94
+ setParameter("changeFactor2Url", ParamType.String, this, options, "CHANGE_FACTOR2_URL");
95
+ setParameter("configureFactor2Url", ParamType.String, this, options, "CONFIGURE_FACTOR2_URL");
96
+ setParameter("loginRedirectUrl", ParamType.JsonArray, this, options, "LOGIN_REDIRECT_URL");
97
+ setParameter("loginUrl", ParamType.JsonArray, this, options, "LOGIN_URL");
98
+ if (options.addToSession)
99
+ this.addToSession = options.addToSession;
100
+ if (this.changePasswordUrl && !this.changePasswordUrl.startsWith("/")) {
101
+ throw new CrossauthError(ErrorCode.Configuration, "changePasswordUrl must be an absolute path");
102
+ }
103
+ if (this.requestPasswordResetUrl && !this.requestPasswordResetUrl.startsWith("/")) {
104
+ throw new CrossauthError(ErrorCode.Configuration, "requestPasswordResetUrl must be an absolute path");
105
+ }
106
+ if (this.changeFactor2Url && !this.changeFactor2Url.startsWith("/")) {
107
+ throw new CrossauthError(ErrorCode.Configuration, "changeFactor2Url must be an absolute path");
108
+ }
109
+ if (this.configureFactor2Url && !this.configureFactor2Url.startsWith("/")) {
110
+ throw new CrossauthError(ErrorCode.Configuration, "configureFactor2Url must be an absolute path");
111
+ }
112
+ if (!this.loginUrl.startsWith("/")) {
113
+ throw new CrossauthError(ErrorCode.Configuration, "loginUrl must be an absolute path");
114
+ }
115
+ }
116
+ /** Returns whether there is a user logged in with a cookie-based session
117
+ */
118
+ isSessionUser(event) {
119
+ return event.locals.user != undefined && event.locals.authType == "cookie";
120
+ }
121
+ /**
122
+ * A user can edit his or her account if they are logged in with
123
+ * session management, or are logged in with some other means and
124
+ * e`ditUserScope` has been set and is included in the user's scopes.
125
+ * @param event the SvelteKit request event
126
+ * @returns true or false
127
+ */
128
+ canEditUser(event) {
129
+ return this.isSessionUser(event) ||
130
+ (this.sessionServer.editUserScope && event.locals.scope &&
131
+ event.locals.scope.includes(this.sessionServer.editUserScope));
132
+ }
133
+ ////////////////////////////////////////////////////
134
+ // Functions for calling manually from own Actions or PageLoad
135
+ /**
136
+ * Log a user in if possible.
137
+ *
138
+ * Form data is returned unless there was
139
+ * an error extrafting it. User is returned if log in was successful.
140
+ * Error messge and exception are returned if not successful.
141
+ *
142
+ * @param event the Sveltekit event. The fields needed are:
143
+ *
144
+ * - `username`.
145
+ * - *secrets* (eg `password`).
146
+ * - `repeat_`*secrets* (eg `repeat_password`).
147
+ *
148
+ * The secrets are authenticator-dependent.
149
+ *
150
+ * @returns object with:
151
+ *
152
+ * - `success` true if login was successful, false otherwise.
153
+ * even if factor2 authentication is required, this will still
154
+ * be true if there was no error.
155
+ * - `user` the user if login was successful
156
+ * - `formData` the form fields extracted from the request
157
+ * - `error` an error message or undefined
158
+ * - `exception` a {@link @crossauth/common!CrossauthError} if an
159
+ * exception was raised
160
+ * - `factor2Required` if true, second factor authentication is needed
161
+ * to complete login
162
+ */
163
+ async login(event) {
164
+ let formData = undefined;
165
+ try {
166
+ // get form data
167
+ var data = new JsonOrFormData();
168
+ await data.loadData(event);
169
+ formData = data.toObject();
170
+ const username = data.get('username') ?? "";
171
+ const persist = data.getAsBoolean('persist') ?? false;
172
+ if (formData.next && formData.next.includes("/__data.json")) {
173
+ formData.next = formData.next.substring(0, formData.next.indexOf("/__data.json"));
174
+ }
175
+ let next = formData.next ?? this.loginRedirectUrl;
176
+ if (username == "")
177
+ throw new CrossauthError(ErrorCode.InvalidUsername, "Username field may not be empty");
178
+ // call implementor-provided hook to add additional fields to session key
179
+ let extraFields = this.addToSession ? this.addToSession(event, formData) : {};
180
+ // throw an exception if the CSRF token isn't valid
181
+ //await this.validateCsrfToken(request);
182
+ if (this.sessionServer.enableCsrfProtection && !event.locals.csrfToken)
183
+ throw new CrossauthError(ErrorCode.InvalidCsrf);
184
+ // keep the old session ID. If there was one, we will delete it after
185
+ const oldSessionId = this.sessionServer.getSessionCookieValue(event);
186
+ // log user in and get new session cookie, CSRF cookie and user
187
+ // if 2FA is enabled, it will be an anonymous session
188
+ let { sessionCookie, csrfCookie, user } = await this.sessionServer.sessionManager.login(username, data.toObject(), extraFields, persist);
189
+ // Set the new cookies in the reply
190
+ CrossauthLogger.logger.debug(j({
191
+ msg: "Login: set session cookie " + sessionCookie.name + " opts " + JSON.stringify(sessionCookie.options),
192
+ user: username
193
+ }));
194
+ event.cookies.set(sessionCookie.name, sessionCookie.value, toCookieSerializeOptions(sessionCookie.options));
195
+ CrossauthLogger.logger.debug(j({
196
+ msg: "Login: set csrf cookie " + csrfCookie.name + " opts " + JSON.stringify(sessionCookie.options),
197
+ user: username
198
+ }));
199
+ if (this.sessionServer.enableCsrfProtection) {
200
+ event.cookies.set(csrfCookie.name, csrfCookie.value, toCookieSerializeOptions(csrfCookie.options));
201
+ event.locals.csrfToken =
202
+ await this.sessionServer.sessionManager.createCsrfFormOrHeaderValue(csrfCookie.value);
203
+ }
204
+ // delete the old session key if there was one
205
+ if (oldSessionId) {
206
+ try {
207
+ await this.sessionServer.sessionManager.deleteSession(oldSessionId);
208
+ }
209
+ catch (e) {
210
+ CrossauthLogger.logger.warn(j({
211
+ msg: "Couldn't delete session ID from database",
212
+ hashOfSessionId: this.sessionServer.getHashOfSessionId(event)
213
+ }));
214
+ CrossauthLogger.logger.debug(j({ err: e }));
215
+ }
216
+ }
217
+ // XXX
218
+ if (user.state == UserState.passwordChangeNeeded) {
219
+ if (!this.changePasswordUrl)
220
+ throw new CrossauthError(ErrorCode.Configuration, "Must set changePasswordUrl in session server");
221
+ this.sessionServer.redirect(302, this.changePasswordUrl + "?required=true&next=" + encodeURIComponent("login?next=" + next));
222
+ }
223
+ else if (user.state == UserState.passwordResetNeeded) {
224
+ //this.sessionServer.redirect(302, this.requestPasswordResetUrl);
225
+ throw new CrossauthError(ErrorCode.PasswordResetNeeded, "Please click on the link we sent you to reset your password");
226
+ }
227
+ else if (user.state == UserState.passwordAndFactor2ResetNeeded) {
228
+ //this.sessionServer.redirect(302, this.requestPasswordResetUrl);
229
+ throw new CrossauthError(ErrorCode.PasswordResetNeeded, "Please click on the link we sent you to reset your password");
230
+ }
231
+ else if (this.sessionServer.allowedFactor2.length > 0 &&
232
+ user.state == UserState.factor2ResetNeeded ||
233
+ !this.sessionServer.allowedFactor2Names.includes(user.factor2 ? user.factor2 : "none")) {
234
+ if (!this.changeFactor2Url)
235
+ throw new CrossauthError(ErrorCode.Configuration, "Must set changeFactor2Url in session server");
236
+ this.sessionServer.redirect(302, this.changeFactor2Url + "?required=true&next=" + encodeURIComponent("login?next=" + next));
237
+ }
238
+ else {
239
+ if (!user.factor2 || user.factor2 == "")
240
+ event.locals.user = user;
241
+ }
242
+ return {
243
+ user,
244
+ formData,
245
+ factor2Required: user.factor2 && user.factor2 != "",
246
+ next: next,
247
+ ok: true,
248
+ };
249
+ }
250
+ catch (e) {
251
+ // hack - let Sveltekit redirect through
252
+ if (typeof e == "object" && e != null && "status" in e && "location" in e)
253
+ throw e;
254
+ let ce = CrossauthError.asCrossauthError(e, "Couldn't log in");
255
+ CrossauthLogger.logger.debug(j({ err: ce }));
256
+ CrossauthLogger.logger.error(j({ cerr: ce }));
257
+ return {
258
+ error: ce.message,
259
+ ok: false,
260
+ formData,
261
+ errorCode: ce.code,
262
+ errorCodeName: ce.codeName,
263
+ };
264
+ }
265
+ }
266
+ /**
267
+ * This is called after the user has been validated to log the user in
268
+ */
269
+ async loginWithUser(user, bypass2FA, event) {
270
+ // get old session ID so we can delete it after
271
+ const oldSessionId = event.locals.sessionId;
272
+ // call implementor-provided hook to add custom fields to session key
273
+ const data = new JsonOrFormData();
274
+ await data.loadData(event);
275
+ let extraFields = this.addToSession ? this.addToSession(event, data.toObject()) : {};
276
+ // log user in - this doesn't do any authentication
277
+ let { sessionCookie, csrfCookie, csrfFormOrHeaderValue } = await this.sessionServer.sessionManager.login("", {}, extraFields, undefined, user, bypass2FA);
278
+ // set the cookies
279
+ CrossauthLogger.logger.debug(j({
280
+ msg: "Login: set session cookie " + sessionCookie.name + " opts " + JSON.stringify(sessionCookie.options),
281
+ user: user.username
282
+ }));
283
+ event.cookies.set(sessionCookie.name, sessionCookie.value, toCookieSerializeOptions(sessionCookie.options));
284
+ CrossauthLogger.logger.debug(j({
285
+ msg: "Login: set csrf cookie " + csrfCookie.name + " opts " + JSON.stringify(sessionCookie.options),
286
+ user: user.username
287
+ }));
288
+ if (this.sessionServer.enableCsrfProtection)
289
+ event.cookies.set(csrfCookie.name, csrfCookie.value, toCookieSerializeOptions(csrfCookie.options));
290
+ // set locals
291
+ event.locals.user = user;
292
+ event.locals.csrfToken = csrfFormOrHeaderValue;
293
+ event.locals.sessionId = this.sessionServer.sessionManager.getSessionId(sessionCookie.value);
294
+ // delete the old session
295
+ if (oldSessionId) {
296
+ try {
297
+ await this.sessionServer.sessionManager.deleteSession(oldSessionId);
298
+ }
299
+ catch (e) {
300
+ CrossauthLogger.logger.warn(j({
301
+ msg: "Couldn't delete session ID from database",
302
+ hashOfSessionId: this.sessionServer.getHashOfSessionId(event)
303
+ }));
304
+ CrossauthLogger.logger.debug(j({ err: e }));
305
+ }
306
+ }
307
+ return {
308
+ user: user,
309
+ ok: true,
310
+ };
311
+ }
312
+ /**
313
+ * Log a user out.
314
+ *
315
+ * Deletes the session if the user was logged in and clears session
316
+ * and CSRF cookies (if CSRF protection is enabled)
317
+ *
318
+ * @param event the Sveltekit event
319
+ *
320
+ * @returns object with:
321
+ *
322
+ * - `success` true if logout was successful, false otherwise.
323
+ * - `error` an error message or undefined
324
+ * - `exception` a {@link @crossauth/common!CrossauthError} if an
325
+ * exception was raised
326
+ */
327
+ async logout(event) {
328
+ try {
329
+ // logout
330
+ if (event.locals.sessionId) {
331
+ await this.sessionServer.sessionManager.logout(event.locals.sessionId);
332
+ }
333
+ // clear cookies
334
+ CrossauthLogger.logger.debug(j({ msg: "Logout: clear cookie "
335
+ + this.sessionServer.sessionManager.sessionCookieName }));
336
+ event.cookies.delete(this.sessionServer.sessionManager.sessionCookieName, { path: "/" });
337
+ if (this.sessionServer.enableCsrfProtection)
338
+ event.cookies.delete(this.sessionServer.sessionManager.csrfCookieName, { path: "/" });
339
+ if (event.locals.sessionId) {
340
+ try {
341
+ await this.sessionServer.sessionManager.deleteSession(event.locals.sessionId);
342
+ }
343
+ catch (e) {
344
+ CrossauthLogger.logger.warn(j({
345
+ msg: "Couldn't delete session ID from database",
346
+ hashOfSessionId: this.sessionServer.getHashOfSessionId(event)
347
+ }));
348
+ CrossauthLogger.logger.debug(j({ err: e }));
349
+ }
350
+ }
351
+ // delete locals
352
+ event.locals.sessionId = undefined;
353
+ event.locals.user = undefined;
354
+ if (this.sessionServer.enableCsrfProtection) {
355
+ event.locals.csrfToken = undefined;
356
+ event.cookies.delete(this.sessionServer.sessionManager.csrfCookieName, { path: "/" });
357
+ // create new CSRF token
358
+ const { csrfCookie, csrfFormOrHeaderValue } = await this.sessionServer.sessionManager.createCsrfToken();
359
+ this.sessionServer.setCsrfCookie(csrfCookie, event);
360
+ event.locals.csrfToken = csrfFormOrHeaderValue;
361
+ }
362
+ return { ok: true };
363
+ }
364
+ catch (e) {
365
+ const ce = CrossauthError.asCrossauthError(e);
366
+ CrossauthLogger.logger.debug(j({ err: ce }));
367
+ CrossauthLogger.logger.error(j({ cerr: ce }));
368
+ return {
369
+ ok: false,
370
+ error: ce.message,
371
+ errorCode: ce.code,
372
+ errorCodeName: ce.codeName,
373
+ };
374
+ }
375
+ }
376
+ /**
377
+ * Creates an account.
378
+ *
379
+ * Form data is returned unless there was an error extrafting it.
380
+ *
381
+ * Initiates user login if creation was successful.
382
+ *
383
+ * If login was successful, no factor2 is needed
384
+ * and no email verification is needed, the user is returned.
385
+ *
386
+ * If email verification is needed, `emailVerificationRequired` is
387
+ * returned as `true`.
388
+ *
389
+ * If factor2 configuration is required, `factor2Required` is returned
390
+ * as `true`.
391
+ *
392
+ * @param event the Sveltekit event. The form fields used are
393
+ * - `username` the desired username
394
+ * - `factor2` which must be in the `allowedFactor2` option passed
395
+ * to the constructor.
396
+ * - *secrets* (eg `password`) which are factor1 authenticator specific
397
+ * - `repeat_`*secrets* (eg `repeat_password`)
398
+ * - `user_*` anything prefixed with `user` that is also in
399
+ * - the `userEditableFields` option passed when constructing the
400
+ * user storage object will be added to the {@link @crossauth/common!User}
401
+ * object (with `user_` removed).
402
+ *
403
+ * @returns object with:
404
+ *
405
+ * - `ok` true if creation and login were successful,
406
+ * false otherwise.
407
+ * even if factor2 authentication is required, this will still
408
+ * be true if there was no error.
409
+ * - `user` the user if login was successful
410
+ * - `formData` the form fields extracted from the request
411
+ * - `error` an error message or undefined
412
+ * - `exception` a {@link @crossauth/common!CrossauthError} if an
413
+ * exception was raised
414
+ * - `factor2Required` if true, second factor authentication is needed
415
+ * to complete login
416
+ * - `factor2Data` contains data that needs to be passed to the user's
417
+ * chosen factor2 authenticator
418
+ * - `emailVerificationRequired` if true, the user needs to click on
419
+ * the link emailed to them to complete signup.
420
+ */
421
+ async signup(event) {
422
+ let formData = undefined;
423
+ try {
424
+ if (!this.sessionServer.userStorage)
425
+ throw new CrossauthError(ErrorCode.Configuration, "Must provide user storage to use this function");
426
+ // get form data
427
+ var data = new JsonOrFormData();
428
+ await data.loadData(event);
429
+ formData = data.toObject();
430
+ const username = data.get('username') ?? "";
431
+ let user;
432
+ // throw an error if the CSRF token is invalid
433
+ if (this.isSessionUser(event) && this.sessionServer.enableCsrfProtection && !event.locals.csrfToken)
434
+ throw new CrossauthError(ErrorCode.InvalidCsrf);
435
+ if (username == "")
436
+ throw new CrossauthError(ErrorCode.InvalidUsername, "Username field may not be empty");
437
+ // get factor2 from user input
438
+ if (!formData.factor2) {
439
+ formData.factor2 = this.sessionServer.allowedFactor2Names[0];
440
+ }
441
+ if (formData.factor2 &&
442
+ !(this.sessionServer.allowedFactor2Names.includes(formData.factor2 ?? "none"))) {
443
+ throw new CrossauthError(ErrorCode.Forbidden, "Illegal second factor " + formData.factor2 + " requested");
444
+ }
445
+ if (formData.factor2 == "none" || formData.factor2 == "") {
446
+ formData.factor2 = undefined;
447
+ }
448
+ // call implementor-provided function to create the user object (or our default)
449
+ user =
450
+ this.sessionServer.createUserFn(event, formData, this.sessionServer.userStorage.userEditableFields, this.sessionServer.userAllowedFactor1);
451
+ // ask the authenticator to validate the user-provided secret
452
+ let passwordErrors = this.sessionServer.authenticators[user.factor1].validateSecrets(formData);
453
+ // get the repeat secrets (secret names prefixed with repeat_)
454
+ const secretNames = this.sessionServer.authenticators[user.factor1].secretNames();
455
+ let repeatSecrets = {};
456
+ for (let field in formData) {
457
+ if (field.startsWith("repeat_")) {
458
+ const name = field.replace(/^repeat_/, "");
459
+ // @ts-ignore as it complains about request.body[field]
460
+ if (secretNames.includes(name))
461
+ repeatSecrets[name] =
462
+ formData[field];
463
+ }
464
+ }
465
+ if (Object.keys(repeatSecrets).length === 0)
466
+ repeatSecrets = undefined;
467
+ // set the user's state to active, awaitingtwofactor or
468
+ // awaitingemailverification
469
+ // depending on settings for next step
470
+ user.state = "active";
471
+ if (formData.factor2 && formData.factor2 != "none") {
472
+ user.state = "awaitingtwofactor";
473
+ }
474
+ else if (this.sessionServer.enableEmailVerification) {
475
+ user.state = "awaitingemailverification";
476
+ }
477
+ // call the implementor-provided hook to validate the user fields
478
+ let userErrors = this.sessionServer.validateUserFn(user);
479
+ // report any errors
480
+ let errors = [...userErrors, ...passwordErrors];
481
+ if (errors.length > 0) {
482
+ throw new CrossauthError(ErrorCode.FormEntry, errors);
483
+ }
484
+ // See if the user was already created, with the correct password, and
485
+ // is awaiting 2FA
486
+ // completion. Send the same response as before, in case the user
487
+ // closed the browser
488
+ let twoFactorInitiated = false;
489
+ try {
490
+ const { user: existingUser, secrets: existingSecrets } = await this.sessionServer.userStorage.getUserByUsername(username);
491
+ await this.sessionServer.sessionManager.authenticators[user.factor1]
492
+ .authenticateUser(existingUser, existingSecrets, formData);
493
+ }
494
+ catch (e) {
495
+ const ce = CrossauthError.asCrossauthError(e);
496
+ if (ce.code == ErrorCode.TwoFactorIncomplete) {
497
+ twoFactorInitiated = true;
498
+ } // all other errors are legitimate ones - we ignore them
499
+ }
500
+ // login (this may be just first stage of 2FA)
501
+ if ((!formData.factor2) && !twoFactorInitiated) {
502
+ // not enabling 2FA
503
+ await this.sessionServer.sessionManager.createUser(user, formData, repeatSecrets);
504
+ if (!this.sessionServer.enableEmailVerification) {
505
+ return { ...await this.login(event), formData: formData };
506
+ }
507
+ // email verification sent - tell user
508
+ return { emailVerificationRequired: true, user: user, ok: true, formData: formData };
509
+ }
510
+ else {
511
+ // also enabling 2FA
512
+ let userData;
513
+ if (twoFactorInitiated) {
514
+ // account already created but 2FA setup not complete
515
+ if (!event.locals.sessionId)
516
+ throw new CrossauthError(ErrorCode.Unauthorized);
517
+ const resp = await this.sessionServer.sessionManager.repeatTwoFactorSignup(event.locals.sessionId);
518
+ userData = resp.userData;
519
+ }
520
+ else {
521
+ // account not created - create one with state awaiting 2FA setup
522
+ const sessionValue = await this.sessionServer.createAnonymousSession(event);
523
+ const sessionId = this.sessionServer.sessionManager.getSessionId(sessionValue);
524
+ const resp = await this.sessionServer.sessionManager.initiateTwoFactorSignup(user, formData, sessionId, repeatSecrets);
525
+ userData = resp.userData;
526
+ }
527
+ // pass caller back 2FA parameters
528
+ try {
529
+ let data = {
530
+ userData: userData,
531
+ username: username,
532
+ factor2: formData.factor2 ?? "none",
533
+ };
534
+ if (this.sessionServer.enableCsrfProtection)
535
+ data.csrfToken = event.locals.csrfToken;
536
+ return { factor2Data: data, ok: true, factor2Required: true, formData };
537
+ }
538
+ catch (e) {
539
+ // if there is an error, make sure we delete the user before returning
540
+ CrossauthLogger.logger.error(j({ err: e }));
541
+ try {
542
+ this.sessionServer.sessionManager.deleteUserByUsername(username);
543
+ }
544
+ catch (e) {
545
+ CrossauthLogger.logger.error(j({ err: e }));
546
+ }
547
+ }
548
+ }
549
+ return { user, formData, ok: true };
550
+ }
551
+ catch (e) {
552
+ let ce = CrossauthError.asCrossauthError(e, "Couldn't sign up");
553
+ CrossauthLogger.logger.debug(j({ err: ce }));
554
+ CrossauthLogger.logger.error(j({ cerr: ce }));
555
+ return {
556
+ error: ce.message,
557
+ ok: false,
558
+ formData,
559
+ errorCode: ce.code,
560
+ errorCodeName: ce.codeName
561
+ };
562
+ }
563
+ }
564
+ /**
565
+ * Takes email verification token from the params on the URL and attempts
566
+ * email verification.
567
+ *
568
+ * @param event the Sveltekit event. This should contain the URL
569
+ * parameter called `token`
570
+ *
571
+ * @returns object with:
572
+ *
573
+ * - `ok` true if creation and login were successful,
574
+ * false otherwise.
575
+ * even if factor2 authentication is required, this will still
576
+ * be true if there was no error.
577
+ * - `user` the user if successful
578
+ * - `formData` the form fields extracted from the request
579
+ * - `error` an error message or undefined
580
+ * - `exception` a {@link @crossauth/common!CrossauthError} if an
581
+ * exception was raised
582
+ * - `factor2Required` if true, second factor authentication is needed
583
+ * to complete login
584
+ * - `factor2Data` contains data that needs to be passed to the user's
585
+ * chosen factor2 authenticator
586
+ * - `emailVerificationRequired` if true, the user needs to click on
587
+ * the link emailed to them to complete signup.
588
+ */
589
+ async verifyEmail(event) {
590
+ try {
591
+ if (!this.sessionServer.userStorage)
592
+ throw new CrossauthError(ErrorCode.Configuration, "Must provide user storage to use this function");
593
+ const token = event.params.token;
594
+ if (!token)
595
+ throw new CrossauthError(ErrorCode.InvalidToken, "Invalid email verification token");
596
+ // validate the token and log the user in
597
+ const user = await this.sessionServer.sessionManager.applyEmailVerificationToken(token);
598
+ await this.loginWithUser(user, true, event);
599
+ if (event.locals.user) {
600
+ const resp = await this.sessionServer.userStorage.getUserById(event.locals.user?.id);
601
+ event.locals.user = resp.user;
602
+ }
603
+ return {
604
+ ok: true,
605
+ user: user,
606
+ };
607
+ }
608
+ catch (e) {
609
+ const ce = CrossauthError.asCrossauthError(e);
610
+ CrossauthLogger.logger.debug(j({ err: e }));
611
+ CrossauthLogger.logger.error(j({ cerr: e }));
612
+ return {
613
+ ok: false,
614
+ error: ce.message,
615
+ errorCode: ce.code,
616
+ errorCodeName: ce.codeName,
617
+ };
618
+ }
619
+ }
620
+ /**
621
+ * Completes factor2 configuration.
622
+ *
623
+ * 2FA configuration is initiated with {@link signup()}, or
624
+ * {@link changeFactor2()}. If these return successfully, call this
625
+ * function.
626
+ *
627
+ * @param event the Sveltekit event. This should contain fields
628
+ * required by the user's chosen authenticator.
629
+ *
630
+ * @returns object with:
631
+ *
632
+ * - `success` true if creation and login were successful,
633
+ * false otherwise.
634
+ * - `user` the user successful
635
+ * - `error` an error message or undefined
636
+ * - `exception` a {@link @crossauth/common!CrossauthError} if an
637
+ * exception was raised
638
+ * - `emailVerificationRequired` if true, the user needs to click on
639
+ * the link emailed to them to complete configuration.
640
+ */
641
+ async configureFactor2(event) {
642
+ let formData = undefined;
643
+ let factor2Data = undefined;
644
+ let factor2 = "";
645
+ try {
646
+ // get form data
647
+ var data = new JsonOrFormData();
648
+ await data.loadData(event);
649
+ formData = data.toObject();
650
+ // get factor2 type from session data
651
+ const sessionData = await this.sessionServer.getSessionData(event, "2fa");
652
+ if (sessionData?.factor2)
653
+ factor2 = sessionData?.factor2;
654
+ else
655
+ throw new CrossauthError(ErrorCode.BadRequest, "Two factor authentication was not started");
656
+ // throw an error if the CSRF token is invalid
657
+ if (this.isSessionUser(event) && this.sessionServer.enableCsrfProtection && !event.locals.csrfToken)
658
+ throw new CrossauthError(ErrorCode.InvalidCsrf);
659
+ // get the session - it may be a real user or anonymous
660
+ if (!event.locals.sessionId)
661
+ throw new CrossauthError(ErrorCode.Unauthorized, "No session active while enabling 2FA. Please enable cookies");
662
+ // finish 2FA setup - validate secrets and update user
663
+ let user = await this.sessionServer.sessionManager.completeTwoFactorSetup(formData, event.locals.sessionId);
664
+ /*if (!this.isSessionUser(event) && !this.sessionServer.enableEmailVerification) {
665
+ // we skip the login if the user is already logged in and we are not doing email verification
666
+ await this.loginWithUser(user, true, event);
667
+ }*/
668
+ if (!this.sessionServer.enableEmailVerification) {
669
+ // if email verification is enabled, the user will have
670
+ // to click on their link before logging in.
671
+ // completeTwoFactorSetup() already sent the email
672
+ await this.loginWithUser(user, true, event);
673
+ }
674
+ // log user in if they are not already
675
+ if (!event.locals.user) {
676
+ return await this.loginWithUser(user, true, event);
677
+ }
678
+ return {
679
+ ok: true,
680
+ user: user,
681
+ emailVerificationRequired: this.sessionServer.enableEmailVerification,
682
+ };
683
+ }
684
+ catch (e) {
685
+ const ce = CrossauthError.asCrossauthError(e);
686
+ // get user data for 2fa again so that we can show it to
687
+ // the user again
688
+ let userData = undefined;
689
+ try {
690
+ const resp = await this.sessionServer.sessionManager.repeatTwoFactorSignup(event.locals.sessionId ?? "");
691
+ userData = resp.userData;
692
+ }
693
+ catch (e2) { }
694
+ if (userData)
695
+ factor2Data = {
696
+ userData: userData,
697
+ csrfToken: event.locals.csrfToken,
698
+ username: userData.username ?? "",
699
+ factor2: factor2,
700
+ };
701
+ else {
702
+ factor2Data = {
703
+ userData: {},
704
+ csrfToken: event.locals.csrfToken,
705
+ username: "",
706
+ factor2: factor2,
707
+ };
708
+ }
709
+ CrossauthLogger.logger.debug(j({ err: e }));
710
+ CrossauthLogger.logger.error(j({ cerr: e }));
711
+ return {
712
+ ok: false,
713
+ error: ce.message,
714
+ errorCode: ce.code,
715
+ errorCodeName: ce.codeName,
716
+ formData: formData,
717
+ factor2Data: factor2Data,
718
+ emailVerificationRequired: this.sessionServer.enableEmailVerification,
719
+ };
720
+ }
721
+ }
722
+ /**
723
+ * Call this when `login()` returns `factor2Required = true`
724
+ *
725
+ * @param event the Sveltekit event. The fields needed are those
726
+ * required by the factor2 authenticator.
727
+ *
728
+ * @returns object with:
729
+ *
730
+ * - `success` true if login was successful, false otherwise.
731
+ * - `user` the user if login was successful
732
+ * - `formData` the form fields extracted from the request
733
+ * - `error` an error message or undefined
734
+ * - `exception` a {@link @crossauth/common!CrossauthError} if an
735
+ * exception was raised
736
+ */
737
+ async loginFactor2(event) {
738
+ if (event.locals.user) {
739
+ return {
740
+ user: event.locals.user,
741
+ ok: true,
742
+ };
743
+ }
744
+ let formData = undefined;
745
+ try {
746
+ // get form data
747
+ var data = new JsonOrFormData();
748
+ await data.loadData(event);
749
+ formData = data.toObject();
750
+ const persist = data.getAsBoolean('persist') ?? false;
751
+ // save the old session ID so we can delete it after (the anonymous session)
752
+ // If there isn't one it is an error - only allowed to this URL with a
753
+ // valid session
754
+ const oldSessionId = event.locals.sessionId;
755
+ if (!oldSessionId)
756
+ throw new CrossauthError(ErrorCode.Unauthorized);
757
+ // validate CSRF token - throw an exception if it is not valid
758
+ //await this.validateCsrfToken(request);
759
+ if (this.isSessionUser(event) && this.sessionServer.enableCsrfProtection &&
760
+ !event.locals.csrfToken)
761
+ throw new CrossauthError(ErrorCode.InvalidCsrf);
762
+ let extraFields = this.addToSession ? this.addToSession(event, formData) : {};
763
+ const { sessionCookie, csrfCookie, user } = await this.sessionServer.sessionManager.completeTwoFactorLogin(formData, oldSessionId, extraFields, persist);
764
+ CrossauthLogger.logger.debug(j({
765
+ msg: "Login: set session cookie " + sessionCookie.name + " opts " + JSON.stringify(sessionCookie.options),
766
+ user: user?.username
767
+ }));
768
+ event.cookies.set(sessionCookie.name, sessionCookie.value, toCookieSerializeOptions(sessionCookie.options));
769
+ CrossauthLogger.logger.debug(j({
770
+ msg: "Login: set csrf cookie " + csrfCookie.name + " opts " + JSON.stringify(sessionCookie.options),
771
+ user: user?.username
772
+ }));
773
+ event.cookies.set(csrfCookie.name, csrfCookie.value, toCookieSerializeOptions(csrfCookie.options));
774
+ if (this.sessionServer.enableCsrfProtection)
775
+ event.locals.csrfToken =
776
+ await this.sessionServer.sessionManager.createCsrfFormOrHeaderValue(csrfCookie.value);
777
+ event.locals.user = user;
778
+ return {
779
+ user: user,
780
+ ok: true,
781
+ formData: formData,
782
+ };
783
+ }
784
+ catch (e) {
785
+ const ce = CrossauthError.asCrossauthError(e);
786
+ CrossauthLogger.logger.debug(j({ err: e }));
787
+ CrossauthLogger.logger.error(j({ cerr: e }));
788
+ return {
789
+ ok: false,
790
+ error: ce.message,
791
+ errorCode: ce.code,
792
+ errorCodeName: ce.codeName,
793
+ formData: formData,
794
+ };
795
+ }
796
+ }
797
+ async requestPasswordReset(event) {
798
+ let formData = undefined;
799
+ try {
800
+ // get form data
801
+ var data = new JsonOrFormData();
802
+ await data.loadData(event);
803
+ formData = data.toObject();
804
+ const email = data.get('email') ?? "";
805
+ if (email == "")
806
+ throw new CrossauthError(ErrorCode.InvalidUsername, "Email field may not be empty");
807
+ // throw an error if the CSRF token is invalid
808
+ if (this.isSessionUser(event) && this.sessionServer.enableCsrfProtection && !event.locals.csrfToken)
809
+ throw new CrossauthError(ErrorCode.InvalidCsrf);
810
+ // this has to be enabled in configuration
811
+ if (!this.sessionServer.enablePasswordReset) {
812
+ throw new CrossauthError(ErrorCode.Configuration, "Password reset not enabled");
813
+ }
814
+ // Send password reset email
815
+ await this.sessionServer.sessionManager.requestPasswordReset(email);
816
+ return { formData, ok: true };
817
+ }
818
+ catch (e) {
819
+ let ce = CrossauthError.asCrossauthError(e, "Couldn't log in");
820
+ CrossauthLogger.logger.debug(j({ err: e }));
821
+ CrossauthLogger.logger.error(j({ cerr: e }));
822
+ return {
823
+ error: ce.message,
824
+ errorCode: ce.code,
825
+ errorCodeName: ce.codeName,
826
+ ok: false,
827
+ formData,
828
+ };
829
+ }
830
+ }
831
+ /**
832
+ * Call this from the GET url the user clicks on to reset their password.
833
+ *
834
+ * If it is enabled, fetches the user for the token to confirm the token
835
+ * is valid.
836
+
837
+ * @param event the Sveltekit event. This should a `token` URL parameter.
838
+
839
+ * @returns object with:
840
+ *
841
+ * - `ok` true if creation and login were successful,
842
+ * false otherwise.
843
+ * - `user` the user successful
844
+ * - `error` an error message or undefined
845
+ * - `exception` a {@link @crossauth/common!CrossauthError} if an
846
+ * exception was raised
847
+ * - `formData` the form fields extracted from the request
848
+ */
849
+ async validatePasswordResetToken(event) {
850
+ CrossauthLogger.logger.debug(j({ msg: "validatePasswordResetToken " + event.request.method }));
851
+ try {
852
+ const token = event.params.token;
853
+ if (!token)
854
+ throw new CrossauthError(ErrorCode.InvalidToken, "Invalid email verification token");
855
+ // validate the token and log the user in
856
+ const user = await this.sessionServer.sessionManager.userForPasswordResetToken(token);
857
+ return {
858
+ ok: true,
859
+ user: user,
860
+ formData: { token },
861
+ };
862
+ }
863
+ catch (e) {
864
+ const ce = CrossauthError.asCrossauthError(e);
865
+ CrossauthLogger.logger.debug(j({ err: ce }));
866
+ CrossauthLogger.logger.error(j({ cerr: ce }));
867
+ return {
868
+ ok: false,
869
+ error: ce.message,
870
+ errorCode: ce.code,
871
+ errorCodeName: ce.codeName,
872
+ };
873
+ }
874
+ }
875
+ /**
876
+ * Call this from the POST url the user uses to fill in a new password
877
+ * after validating the token in the GET url with
878
+ * {@link validatePasswordResetToken}.
879
+ *
880
+ * @param event the Sveltekit event. This should contain
881
+ * - `new_`*secrets` (eg `new_password`) the new secret.
882
+ * - `repeat_`*secrets` (eg `repeat_password`) repeat of the new secret.
883
+
884
+ * @returns object with:
885
+ *
886
+ * - `ok` true if creation and login were successful,
887
+ * false otherwise.
888
+ * - `user` the user if successful
889
+ * - `error` an error message or undefined
890
+ * - `exception` a {@link @crossauth/common!CrossauthError} if an
891
+ * exception was raised
892
+ * - `formData` the form fields extracted from the request
893
+ */
894
+ async resetPassword(event) {
895
+ CrossauthLogger.logger.debug(j({ msg: "resetPassword" }));
896
+ let formData = undefined;
897
+ try {
898
+ // get form data
899
+ var data = new JsonOrFormData();
900
+ await data.loadData(event);
901
+ formData = data.toObject();
902
+ // throw an error if the CSRF token is invalid
903
+ if (this.isSessionUser(event) && this.sessionServer.enableCsrfProtection && !event.locals.csrfToken)
904
+ throw new CrossauthError(ErrorCode.InvalidCsrf);
905
+ // this has to be enabled in configuration
906
+ if (!this.sessionServer.enablePasswordReset) {
907
+ throw new CrossauthError(ErrorCode.Configuration, "Password reset not enabled");
908
+ }
909
+ // get user for token
910
+ const token = event.params.token ?? "";
911
+ if (token == "")
912
+ throw new CrossauthError(ErrorCode.InvalidUsername, "No token provided");
913
+ const user = await this.sessionServer.sessionManager.userForPasswordResetToken(token);
914
+ // get secrets from the request body
915
+ // there should be new_{secret} and repeat_{secret}
916
+ const authenticator = this.sessionServer.authenticators[user.factor1];
917
+ const secretNames = authenticator.secretNames();
918
+ let newSecrets = {};
919
+ let repeatSecrets = {};
920
+ for (let field in formData) {
921
+ if (field.startsWith("new_")) {
922
+ const name = field.replace(/^new_/, "");
923
+ // @ts-ignore as it complains about formData[field]
924
+ if (secretNames.includes(name))
925
+ newSecrets[name] = formData[field];
926
+ }
927
+ else if (field.startsWith("repeat_")) {
928
+ const name = field.replace(/^repeat_/, "");
929
+ // @ts-ignore as it complains about formData[field]
930
+ if (secretNames.includes(name))
931
+ repeatSecrets[name] = formData[field];
932
+ }
933
+ }
934
+ if (Object.keys(repeatSecrets).length === 0)
935
+ repeatSecrets = undefined;
936
+ // validate the new secrets (with the implementor-provided function)
937
+ let errors = authenticator.validateSecrets(newSecrets);
938
+ if (errors.length > 0) {
939
+ throw new CrossauthError(ErrorCode.PasswordFormat);
940
+ }
941
+ // check new and repeat secrets are valid and update the user
942
+ const user1 = await this.sessionServer.sessionManager.resetSecret(token, 1, newSecrets, repeatSecrets);
943
+ // log the user in
944
+ if (user1.state == UserState.active)
945
+ return await this.loginWithUser(user1, true, event);
946
+ else {
947
+ if (!this.changeFactor2Url) {
948
+ throw new CrossauthError(ErrorCode.Configuration, "Must set changeFactor2Url in session server");
949
+ }
950
+ const sessionCookieValue = this.sessionServer.getSessionCookieValue(event);
951
+ const sessionId = this.sessionServer.sessionManager.getSessionId(sessionCookieValue ?? "");
952
+ await this.sessionServer.sessionManager.updateSessionData(sessionId, "factor2change", { username: user.username });
953
+ throw this.sessionServer.redirect(302, this.changeFactor2Url + "?required=true");
954
+ }
955
+ }
956
+ catch (e) {
957
+ if (SvelteKitServer.isSvelteKitRedirect(e))
958
+ throw e;
959
+ let ce = CrossauthError.asCrossauthError(e, "Couldn't log in");
960
+ CrossauthLogger.logger.debug(j({ err: ce }));
961
+ CrossauthLogger.logger.error(j({ cerr: ce }));
962
+ return {
963
+ error: ce.message,
964
+ errorCode: ce.code,
965
+ errorCodeName: ce.codeName,
966
+ ok: false,
967
+ formData,
968
+ };
969
+ }
970
+ }
971
+ /**
972
+ * Call this from your factor2 endpoint to fetch the data needed to
973
+ * display the factor2 form.
974
+ *
975
+ * This can only be called after 2FA has been initiated by visiting
976
+ * a page with factor2 protection, as defined by the
977
+ * `factor2ProtectedPageEndpoints` and `factor2ProtectedApiEndpoints`
978
+ * defined when constructing this class.
979
+ *
980
+ * @param event the Sveltekit event.
981
+
982
+ * @returns object with:
983
+ *
984
+ * - `ok` true if creation and login were successful,
985
+ * false otherwise.
986
+ * - `action` the URL to load on ok. This was the one originally
987
+ * requested by the user before being redirected to 2FA authentication.
988
+ * - `factor2` the user's factor2
989
+ * - `error` an error message or undefined
990
+ * - `exception` a {@link @crossauth/common!CrossauthError} if an
991
+ * exception was raised
992
+ */
993
+ async requestFactor2(event) {
994
+ try {
995
+ if (!event.locals.sessionId)
996
+ throw new CrossauthError(ErrorCode.Unauthorized, "No session cookie present");
997
+ const sessionCookieValue = this.sessionServer.getSessionCookieValue(event);
998
+ const sessionId = this.sessionServer.sessionManager.getSessionId(sessionCookieValue ?? "");
999
+ const sessionData = await this.sessionServer.sessionManager.dataForSessionId(sessionId);
1000
+ if (!sessionData?.pre2fa)
1001
+ throw new CrossauthError(ErrorCode.Unauthorized, "2FA not initiated");
1002
+ return {
1003
+ ok: true,
1004
+ csrfToken: event.locals.csrfToken,
1005
+ action: sessionData.pre2fa.url,
1006
+ factor2: sessionData.pre2fa.factor2
1007
+ };
1008
+ }
1009
+ catch (e) {
1010
+ let ce = CrossauthError.asCrossauthError(e, "2FA failed");
1011
+ CrossauthLogger.logger.debug(j({ err: ce }));
1012
+ CrossauthLogger.logger.error(j({ cerr: ce }));
1013
+ return {
1014
+ error: ce.message,
1015
+ errorCode: ce.code,
1016
+ errorCodeName: ce.codeName,
1017
+ ok: false,
1018
+ };
1019
+ }
1020
+ }
1021
+ /**
1022
+ * Call this with POST data to change the logged-in user's password
1023
+ *
1024
+ * @param event the Sveltekit event. This should contain
1025
+ * - `old_`*secrets` (eg `old_password`) the existing secret.
1026
+ * - `new_`*secrets` (eg `new_password`) the new secret.
1027
+ * - `repeat_`*secrets` (eg `repeat_password`) repeat of the new secret.
1028
+
1029
+ * @returns object with:
1030
+ *
1031
+ * - `ok` true if creation and login were successful,
1032
+ * false otherwise.
1033
+ * - `user` the user if successful
1034
+ * - `error` an error message or undefined
1035
+ * - `exception` a {@link @crossauth/common!CrossauthError} if an
1036
+ * exception was raised
1037
+ * - `formData` the form fields extracted from the request
1038
+ */
1039
+ async changePassword(event) {
1040
+ CrossauthLogger.logger.debug(j({ msg: "changePassword" }));
1041
+ let formData = undefined;
1042
+ try {
1043
+ if (!this.sessionServer.userStorage)
1044
+ throw new CrossauthError(ErrorCode.Configuration, "Must provide user storage to use this function");
1045
+ // get form data
1046
+ var data = new JsonOrFormData();
1047
+ await data.loadData(event);
1048
+ formData = data.toObject();
1049
+ // can only call this if logged in and CSRF token is valid,
1050
+ // or else if login has been initiated but a password change is
1051
+ // required
1052
+ let user;
1053
+ let required = false;
1054
+ if (!this.isSessionUser(event) || !event.locals.user) {
1055
+ // user is not logged on - check if there is an anonymous
1056
+ // session with passwordchange set (meaning the user state
1057
+ // was set to changepasswordneeded when logging on)
1058
+ const data = await this.sessionServer.getSessionData(event, "passwordchange");
1059
+ if (data?.username) {
1060
+ const resp = await this.sessionServer.userStorage.getUserByUsername(data?.username, {
1061
+ skipActiveCheck: true,
1062
+ skipEmailVerifiedCheck: true,
1063
+ });
1064
+ user = resp.user;
1065
+ required = true;
1066
+ if (this.sessionServer.enableCsrfProtection && !event.locals.csrfToken) {
1067
+ throw new CrossauthError(ErrorCode.InvalidCsrf);
1068
+ }
1069
+ }
1070
+ else {
1071
+ throw new CrossauthError(ErrorCode.Unauthorized);
1072
+ }
1073
+ }
1074
+ else if (!this.canEditUser(event)) {
1075
+ throw new CrossauthError(ErrorCode.InsufficientPriviledges);
1076
+ }
1077
+ else {
1078
+ //this.validateCsrfToken(request)
1079
+ if (this.isSessionUser(event) &&
1080
+ this.sessionServer.enableCsrfProtection && !event.locals.csrfToken) {
1081
+ throw new CrossauthError(ErrorCode.InvalidCsrf);
1082
+ }
1083
+ user = event.locals.user;
1084
+ }
1085
+ // get the authenticator for factor1 (passwords on factor2 are not supported)
1086
+ const authenticator = this.sessionServer.authenticators[user.factor1];
1087
+ // the form should contain old_{secret}, new_{secret} and repeat_{secret}
1088
+ // extract them, making sure the secret is a valid one
1089
+ const secretNames = authenticator.secretNames();
1090
+ let oldSecrets = {};
1091
+ let newSecrets = {};
1092
+ let repeatSecrets = {};
1093
+ for (let field in formData) {
1094
+ if (field.startsWith("new_")) {
1095
+ const name = field.replace(/^new_/, "");
1096
+ if (secretNames.includes(name))
1097
+ newSecrets[name] = formData[field];
1098
+ }
1099
+ else if (field.startsWith("old_")) {
1100
+ const name = field.replace(/^old_/, "");
1101
+ if (secretNames.includes(name))
1102
+ oldSecrets[name] = formData[field];
1103
+ }
1104
+ else if (field.startsWith("repeat_")) {
1105
+ const name = field.replace(/^repeat_/, "");
1106
+ if (secretNames.includes(name))
1107
+ repeatSecrets[name] = formData[field];
1108
+ }
1109
+ }
1110
+ if (Object.keys(repeatSecrets).length === 0)
1111
+ repeatSecrets = undefined;
1112
+ // validate the new secret - this is through an implementor-supplied function
1113
+ let errors = authenticator.validateSecrets(newSecrets);
1114
+ if (errors.length > 0) {
1115
+ throw new CrossauthError(ErrorCode.PasswordFormat);
1116
+ }
1117
+ // validate the old secrets, check the new and repeat ones match and
1118
+ // update if valid
1119
+ const oldState = user.state;
1120
+ try {
1121
+ if (required) {
1122
+ user.state = "active";
1123
+ await this.sessionServer.userStorage.updateUser({ id: user.id, state: user.state });
1124
+ }
1125
+ await this.sessionServer.sessionManager.changeSecrets(user.username, 1, newSecrets, repeatSecrets, oldSecrets);
1126
+ }
1127
+ catch (e) {
1128
+ const ce = CrossauthError.asCrossauthError(e);
1129
+ CrossauthLogger.logger.debug(j({ err: e }));
1130
+ if (required) {
1131
+ try {
1132
+ await this.sessionServer.userStorage.updateUser({ id: user.id, state: oldState });
1133
+ }
1134
+ catch (e2) {
1135
+ CrossauthLogger.logger.debug(j({ err: e2 }));
1136
+ }
1137
+ }
1138
+ throw ce;
1139
+ }
1140
+ if (required) {
1141
+ // this was a forced change - user is not actually logged on
1142
+ return await this.loginWithUser(user, false, event);
1143
+ }
1144
+ return {
1145
+ ok: true,
1146
+ formData: formData,
1147
+ };
1148
+ }
1149
+ catch (e) {
1150
+ let ce = CrossauthError.asCrossauthError(e, "Couldn't change password");
1151
+ CrossauthLogger.logger.debug(j({ err: ce }));
1152
+ CrossauthLogger.logger.error(j({ cerr: ce }));
1153
+ return {
1154
+ error: ce.message,
1155
+ ok: false,
1156
+ errorCode: ce.code,
1157
+ errorCodeName: ce.codeName,
1158
+ formData,
1159
+ };
1160
+ }
1161
+ }
1162
+ /**
1163
+ * Call this to delete the logged-in user
1164
+ *
1165
+ * @param event the Sveltekit event.
1166
+
1167
+ * @returns object with:
1168
+ *
1169
+ * - `ok` true if creation and login were successful,
1170
+ * false otherwise.
1171
+ * - `error` an error message or undefined
1172
+ * - `exception` a {@link @crossauth/common!CrossauthError} if an
1173
+ * exception was raised
1174
+ */
1175
+ async deleteUser(event) {
1176
+ CrossauthLogger.logger.debug(j({ msg: "deleteUser" }));
1177
+ try {
1178
+ if (!this.sessionServer.userStorage)
1179
+ throw new CrossauthError(ErrorCode.Configuration, "Must provide user storage to use this function");
1180
+ // throw an error if the CSRF token is invalid
1181
+ if (this.sessionServer.enableCsrfProtection && !event.locals.csrfToken) {
1182
+ throw new CrossauthError(ErrorCode.InvalidCsrf);
1183
+ }
1184
+ // throw an error if not logged in
1185
+ if (!event.locals.user) {
1186
+ throw new CrossauthError(ErrorCode.InsufficientPriviledges);
1187
+ }
1188
+ await this.sessionServer.userStorage.deleteUserById(event.locals.user.id);
1189
+ event.cookies.delete(this.sessionServer.sessionManager.sessionCookieName, { path: "/" });
1190
+ event.locals.sessionId = undefined;
1191
+ event.locals.user = undefined;
1192
+ return {
1193
+ ok: true,
1194
+ };
1195
+ }
1196
+ catch (e) {
1197
+ let ce = CrossauthError.asCrossauthError(e, "Couldn't delete account");
1198
+ CrossauthLogger.logger.debug(j({ err: ce }));
1199
+ CrossauthLogger.logger.error(j({ cerr: ce }));
1200
+ return {
1201
+ error: ce.message,
1202
+ errorCode: ce.code,
1203
+ errorCodeName: ce.codeName,
1204
+ ok: false,
1205
+ };
1206
+ }
1207
+ }
1208
+ /**
1209
+ * Call this to update a user's details (apart from password and factor2)
1210
+ *
1211
+ * @param event the Sveltekit event. The form fields used are
1212
+ * - `username` the desired username
1213
+ * - `user_*` anything prefixed with `user` that is also in
1214
+ * the `userEditableFields` option passed when constructing the
1215
+ * user storage object will be added to the {@link @crossauth/common!User}
1216
+ * object (with `user_` removed).
1217
+ *
1218
+ * @returns object with:
1219
+ *
1220
+ * - `ok` true if creation and login were successful,
1221
+ * false otherwise.
1222
+ * even if factor2 authentication is required, this will still
1223
+ * be true if there was no error.
1224
+ * - `user` the user if login was successful
1225
+ * - `formData` the form fields extracted from the request
1226
+ * - `error` an error message or undefined
1227
+ * - `exception` a {@link @crossauth/common!CrossauthError} if an
1228
+ * exception was raised
1229
+ * - `emailVerificationRequired` if true, the user needs to click on
1230
+ * the link emailed to them to complete signup.
1231
+ */
1232
+ async updateUser(event) {
1233
+ CrossauthLogger.logger.debug(j({ msg: "updateUser" }));
1234
+ let formData = undefined;
1235
+ try {
1236
+ if (!this.sessionServer.userStorage)
1237
+ throw new CrossauthError(ErrorCode.Configuration, "Must provide user storage to use this function");
1238
+ // get form data
1239
+ var data = new JsonOrFormData();
1240
+ await data.loadData(event);
1241
+ formData = data.toObject();
1242
+ // throw an error if the CSRF token is invalid
1243
+ if (this.sessionServer.enableCsrfProtection && !event.locals.csrfToken) {
1244
+ throw new CrossauthError(ErrorCode.InvalidCsrf);
1245
+ }
1246
+ // throw an error if not logged in
1247
+ if (!event.locals.user) {
1248
+ throw new CrossauthError(ErrorCode.InsufficientPriviledges);
1249
+ }
1250
+ // get new user fields from form, including from the
1251
+ // implementor-provided hook
1252
+ let user = {
1253
+ id: event.locals.user.id,
1254
+ username: event.locals.user.username,
1255
+ state: "active",
1256
+ };
1257
+ user = this.sessionServer.updateUserFn(user, event, formData, this.sessionServer.userStorage.userEditableFields);
1258
+ // validate the new user using the implementor-provided function
1259
+ let errors = this.sessionServer.validateUserFn(user);
1260
+ if (errors.length > 0) {
1261
+ throw new CrossauthError(ErrorCode.FormEntry, errors);
1262
+ }
1263
+ // update the user
1264
+ let { emailVerificationTokenSent } = await this.sessionServer.sessionManager.updateUser(event.locals.user, user);
1265
+ if (!emailVerificationTokenSent) {
1266
+ const resp = await this.sessionServer.userStorage.getUserById(event.locals.user.id);
1267
+ event.locals.user = resp.user;
1268
+ }
1269
+ return {
1270
+ ok: true,
1271
+ formData: formData,
1272
+ emailVerificationNeeded: emailVerificationTokenSent,
1273
+ };
1274
+ }
1275
+ catch (e) {
1276
+ let ce = CrossauthError.asCrossauthError(e, "Couldn't update account");
1277
+ CrossauthLogger.logger.debug(j({ err: ce }));
1278
+ CrossauthLogger.logger.error(j({ cerr: ce }));
1279
+ return {
1280
+ error: ce.message,
1281
+ errorCode: ce.code,
1282
+ errorCodeName: ce.codeName,
1283
+ ok: false,
1284
+ formData,
1285
+ emailVerificationNeeded: false,
1286
+ };
1287
+ }
1288
+ }
1289
+ /**
1290
+ * Call this to change the logged in user's factor2.
1291
+ *
1292
+ * @param event the Sveltekit event. The form fields used are
1293
+ * - `factor2` the new designed factor2, which must be in
1294
+ * the `allowedFactor2` option passed to the constructor.
1295
+ *
1296
+ * @returns object with:
1297
+ *
1298
+ * - `ok` true if creation and login were successful,
1299
+ * false otherwise.
1300
+ * even if factor2 authentication is required, this will still
1301
+ * be true if there was no error.
1302
+ * - `user` the user if login was successful
1303
+ * - `formData` the form fields extracted from the request
1304
+ * - `error` an error message or undefined
1305
+ * - `exception` a {@link @crossauth/common!CrossauthError} if an
1306
+ * exception was raised
1307
+ * - `factor2Data` the data to pass to the factor2 configuration page.
1308
+ */
1309
+ async changeFactor2(event) {
1310
+ CrossauthLogger.logger.debug(j({ msg: "updateUser" }));
1311
+ let formData = undefined;
1312
+ try {
1313
+ if (!this.sessionServer.userStorage)
1314
+ throw new CrossauthError(ErrorCode.Configuration, "Must provide user storage to use this function");
1315
+ // get form data
1316
+ var data = new JsonOrFormData();
1317
+ await data.loadData(event);
1318
+ formData = data.toObject();
1319
+ // throw an error if the CSRF token is invalid
1320
+ if (this.sessionServer.enableCsrfProtection && !event.locals.csrfToken) {
1321
+ throw new CrossauthError(ErrorCode.InvalidCsrf);
1322
+ }
1323
+ // see if the user is allowed to do this
1324
+ let username = event.locals.user?.username;
1325
+ if (!this.isSessionUser(event) || !event.locals.user) {
1326
+ // user is not logged on - check if there is an anonymous
1327
+ // session with passwordchange set (meaning the user state
1328
+ // was set to changepasswordneeded when logging on)
1329
+ const sessionData = await this.sessionServer.getSessionData(event, "factor2change");
1330
+ if (!sessionData?.username) {
1331
+ if (!this.isSessionUser(event)) {
1332
+ // as we create session data, user has to be logged in with cookies
1333
+ if (this.sessionServer.unauthorizedUrl) {
1334
+ this.sessionServer.redirect(302, this.sessionServer.unauthorizedUrl);
1335
+ }
1336
+ this.sessionServer.error(401, "Unauthorized");
1337
+ }
1338
+ }
1339
+ username = sessionData?.username;
1340
+ }
1341
+ let user = event.locals.user;
1342
+ if (!user && username) {
1343
+ const resp = await this.sessionServer.userStorage.getUserByUsername(username, {
1344
+ skipActiveCheck: true,
1345
+ skipEmailVerifiedCheck: true,
1346
+ });
1347
+ user = resp.user;
1348
+ }
1349
+ // throw an error if not logged in
1350
+ if (!user) {
1351
+ throw new CrossauthError(ErrorCode.InsufficientPriviledges);
1352
+ }
1353
+ if (!event.locals.sessionId) {
1354
+ throw new CrossauthError(ErrorCode.Unauthorized);
1355
+ }
1356
+ // validate the requested factor2
1357
+ let newFactor2 = formData.factor2;
1358
+ if (formData.factor2 &&
1359
+ !(this.sessionServer.allowedFactor2Names.includes(formData.factor2))) {
1360
+ throw new CrossauthError(ErrorCode.Forbidden, "Illegal second factor " + formData.factor2 + " requested");
1361
+ }
1362
+ if (formData.factor2 == "none" || formData.factor2 == "") {
1363
+ newFactor2 = undefined;
1364
+ if (!event.locals.user) {
1365
+ return await this.loginWithUser(user, true, event);
1366
+ }
1367
+ }
1368
+ // get data to show user to finish 2FA setup
1369
+ const userData = await this.sessionServer.sessionManager
1370
+ .initiateTwoFactorSetup(user, newFactor2, event.locals.sessionId);
1371
+ if (newFactor2) {
1372
+ return {
1373
+ ok: true,
1374
+ formData: formData,
1375
+ factor2Data: {
1376
+ username: user.username,
1377
+ factor2: newFactor2 ?? "",
1378
+ userData,
1379
+ csrfToken: event.locals.csrfToken,
1380
+ }
1381
+ };
1382
+ }
1383
+ return {
1384
+ ok: true,
1385
+ formData: formData,
1386
+ };
1387
+ }
1388
+ catch (e) {
1389
+ let ce = CrossauthError.asCrossauthError(e, "Couldn't update account");
1390
+ CrossauthLogger.logger.debug(j({ err: ce }));
1391
+ CrossauthLogger.logger.error(j({ cerr: ce }));
1392
+ return {
1393
+ error: ce.message,
1394
+ errorCode: ce.code,
1395
+ errorCodeName: ce.codeName,
1396
+ ok: false,
1397
+ formData,
1398
+ };
1399
+ }
1400
+ }
1401
+ /**
1402
+ * Call this to reconfigure the current factor2 type.
1403
+ *
1404
+ * @param event the Sveltekit event.
1405
+ *
1406
+ * @returns object with:
1407
+ *
1408
+ * - `ok` true if creation and login were successful,
1409
+ * false otherwise.
1410
+ * even if factor2 authentication is required, this will still
1411
+ * be true if there was no error.
1412
+ * - `user` the user if login was successful
1413
+ * - `formData` the form fields extracted from the request
1414
+ * - `error` an error message or undefined
1415
+ * - `exception` a {@link @crossauth/common!CrossauthError} if an
1416
+ * exception was raised
1417
+ * - `factor2Data` the data to pass to the factor2 configuration page.
1418
+ */
1419
+ async reconfigureFactor2(event) {
1420
+ CrossauthLogger.logger.debug(j({ msg: "updateUser" }));
1421
+ let formData = undefined;
1422
+ try {
1423
+ if (!this.sessionServer.userStorage)
1424
+ throw new CrossauthError(ErrorCode.Configuration, "Must provide user storage to use this function");
1425
+ // get form data
1426
+ var data = new JsonOrFormData();
1427
+ await data.loadData(event);
1428
+ formData = data.toObject();
1429
+ // throw an error if the CSRF token is invalid
1430
+ if (this.sessionServer.enableCsrfProtection && !event.locals.csrfToken) {
1431
+ throw new CrossauthError(ErrorCode.InvalidCsrf);
1432
+ }
1433
+ // see if the user is allowed to do this
1434
+ let username = event.locals.user?.username;
1435
+ if (!this.isSessionUser(event) || !event.locals.user) {
1436
+ // user is not logged on - check if there is an anonymous
1437
+ // session with passwordchange set (meaning the user state
1438
+ // was set to changepasswordneeded when logging on)
1439
+ const sessionData = await this.sessionServer.getSessionData(event, "factor2change");
1440
+ if (!sessionData?.username) {
1441
+ if (!this.isSessionUser(event)) {
1442
+ // as we create session data, user has to be logged in with cookies
1443
+ if (this.sessionServer.unauthorizedUrl) {
1444
+ this.sessionServer.redirect(302, this.sessionServer.unauthorizedUrl);
1445
+ }
1446
+ this.sessionServer.error(401, "Unauthorized");
1447
+ }
1448
+ }
1449
+ username = sessionData?.username;
1450
+ }
1451
+ let user = event.locals.user;
1452
+ if (!user && username) {
1453
+ const resp = await this.sessionServer.userStorage.getUserByUsername(username, {
1454
+ skipActiveCheck: true,
1455
+ skipEmailVerifiedCheck: true,
1456
+ });
1457
+ user = resp.user;
1458
+ }
1459
+ // throw an error if not logged in
1460
+ if (!user) {
1461
+ throw new CrossauthError(ErrorCode.InsufficientPriviledges);
1462
+ }
1463
+ if (!event.locals.sessionId) {
1464
+ throw new CrossauthError(ErrorCode.Unauthorized);
1465
+ }
1466
+ if (!event.locals.sessionId) {
1467
+ throw new CrossauthError(ErrorCode.Unauthorized);
1468
+ }
1469
+ // get second factor authenticator
1470
+ let factor2 = user.factor2;
1471
+ const authenticator = this.sessionServer.authenticators[factor2];
1472
+ if (!authenticator || authenticator.secretNames().length == 0) {
1473
+ throw new CrossauthError(ErrorCode.BadRequest, "Selected second factor does not have configuration");
1474
+ }
1475
+ // step one in 2FA setup - create secrets and get data to dispaly to user
1476
+ const userData = await this.sessionServer.sessionManager.initiateTwoFactorSetup(user, factor2, event.locals.sessionId);
1477
+ return {
1478
+ ok: true,
1479
+ formData: formData,
1480
+ factor2Data: {
1481
+ username: user.username,
1482
+ factor2: user.factor2 ?? "",
1483
+ userData,
1484
+ csrfToken: event.locals.csrfToken,
1485
+ }
1486
+ };
1487
+ }
1488
+ catch (e) {
1489
+ let ce = CrossauthError.asCrossauthError(e, "Couldn't update account");
1490
+ CrossauthLogger.logger.debug(j({ err: ce }));
1491
+ CrossauthLogger.logger.error(j({ cerr: ce }));
1492
+ return {
1493
+ error: ce.message,
1494
+ errorCode: ce.code,
1495
+ errorCodeName: ce.codeName,
1496
+ ok: false,
1497
+ formData,
1498
+ };
1499
+ }
1500
+ }
1501
+ ////////////////////////////////////////////////////////////////
1502
+ // Sveltekit user endpoints
1503
+ baseEndpoint(event) {
1504
+ return {
1505
+ user: event.locals.user,
1506
+ csrfToken: event.locals.csrfToken,
1507
+ };
1508
+ }
1509
+ signupEndpoint = {
1510
+ load: async (event) => {
1511
+ let allowedFactor2 = this.sessionServer?.allowedFactor2 ??
1512
+ [{ name: "none", friendlyName: "None" }];
1513
+ return {
1514
+ allowedFactor2,
1515
+ ...this.baseEndpoint(event),
1516
+ };
1517
+ },
1518
+ actions: {
1519
+ default: async (event) => {
1520
+ const resp = await this.signup(event);
1521
+ return resp;
1522
+ }
1523
+ }
1524
+ };
1525
+ loginEndpoint = {
1526
+ load: async (event) => {
1527
+ return {
1528
+ next: event.url.searchParams.get("next") ?? this.loginRedirectUrl,
1529
+ ...this.baseEndpoint(event),
1530
+ };
1531
+ },
1532
+ actions: {
1533
+ login: async (event) => {
1534
+ const resp = await this.login(event);
1535
+ if (resp?.ok == true && !resp?.factor2Required)
1536
+ this.sessionServer.redirect(302, resp.formData?.next ?? this.loginRedirectUrl);
1537
+ if (resp && (resp?.errorCode == ErrorCode.UserNotExist ||
1538
+ resp?.errorCode == ErrorCode.PasswordInvalid)) {
1539
+ resp.error = "Username or password is invalid";
1540
+ }
1541
+ return resp;
1542
+ },
1543
+ factor2: async (event) => {
1544
+ const resp = await this.loginFactor2(event);
1545
+ if (resp?.ok == true && !resp?.factor2Required)
1546
+ this.sessionServer.redirect(302, resp.formData?.next ?? this.loginRedirectUrl);
1547
+ return resp;
1548
+ },
1549
+ },
1550
+ };
1551
+ factor2Endpoint = {
1552
+ load: async (event) => {
1553
+ const resp = await this.requestFactor2(event);
1554
+ if (resp && !resp.error && event.url.searchParams.get("error"))
1555
+ resp.error = event.url.searchParams.get("error") ?? undefined;
1556
+ return resp;
1557
+ },
1558
+ };
1559
+ logoutEndpoint = {
1560
+ actions: {
1561
+ default: async (event) => {
1562
+ const resp = await this.logout(event);
1563
+ return resp;
1564
+ }
1565
+ },
1566
+ load: async (event) => {
1567
+ return {
1568
+ ...this.baseEndpoint(event),
1569
+ };
1570
+ },
1571
+ };
1572
+ changeFactor2Endpoint = {
1573
+ actions: {
1574
+ change: async (event) => {
1575
+ const resp = await this.changeFactor2(event);
1576
+ return resp;
1577
+ },
1578
+ reconfigure: async (event) => {
1579
+ const resp = await this.reconfigureFactor2(event);
1580
+ return resp;
1581
+ },
1582
+ },
1583
+ load: async (event) => {
1584
+ let username = event.locals.user?.username;
1585
+ // see if the user is allowed to do this
1586
+ if (!this.isSessionUser(event) || !event.locals.user) {
1587
+ // user is not logged on - check if there is an anonymous
1588
+ // session with passwordchange set (meaning the user state
1589
+ // was set to changepasswordneeded when logging on)
1590
+ const sessionData = await this.sessionServer.getSessionData(event, "factor2change");
1591
+ if (!sessionData?.username) {
1592
+ if (!this.isSessionUser(event)) {
1593
+ // as we create session data, user has to be logged in with cookies
1594
+ if (this.sessionServer.unauthorizedUrl) {
1595
+ this.sessionServer.redirect(302, this.sessionServer.unauthorizedUrl);
1596
+ }
1597
+ this.sessionServer.error(401, "Unauthorized");
1598
+ }
1599
+ }
1600
+ username = sessionData?.username;
1601
+ }
1602
+ let allowedFactor2 = this.sessionServer.allowedFactor2 ??
1603
+ [{ name: "none", friendlyName: "None", configurable: false }];
1604
+ let data = {};
1605
+ let requiredString = event.url.searchParams.get("required");
1606
+ let required = undefined;
1607
+ if (requiredString) {
1608
+ requiredString = requiredString.toLowerCase();
1609
+ required = requiredString == "true" || requiredString == "1";
1610
+ if (required == true)
1611
+ data.required = true;
1612
+ }
1613
+ let next = event.url.searchParams.get("next");
1614
+ if (next)
1615
+ data.next = next;
1616
+ return {
1617
+ allowedFactor2,
1618
+ ...data,
1619
+ username,
1620
+ ...this.baseEndpoint(event),
1621
+ };
1622
+ },
1623
+ };
1624
+ changePasswordEndpoint = {
1625
+ actions: {
1626
+ default: async (event) => {
1627
+ const resp = await this.changePassword(event);
1628
+ return resp;
1629
+ }
1630
+ },
1631
+ load: async (event) => {
1632
+ let data = {};
1633
+ let requiredString = event.url.searchParams.get("required");
1634
+ let required = undefined;
1635
+ let haveUser = event.locals.user != undefined;
1636
+ if (!haveUser) {
1637
+ const passwordchange = await this.sessionServer.getSessionData(event, "passwordchange");
1638
+ if (passwordchange?.username)
1639
+ haveUser = true;
1640
+ }
1641
+ if (!haveUser)
1642
+ this.sessionServer.redirect(302, this.loginUrl);
1643
+ if (requiredString) {
1644
+ requiredString = requiredString.toLowerCase();
1645
+ required = requiredString == "true" || requiredString == "1";
1646
+ if (required == true)
1647
+ data.required = true;
1648
+ }
1649
+ let next = event.url.searchParams.get("next");
1650
+ if (next)
1651
+ data.next = next;
1652
+ return {
1653
+ ...data,
1654
+ ...this.baseEndpoint(event),
1655
+ };
1656
+ },
1657
+ };
1658
+ configureFactor2Endpoint = {
1659
+ actions: {
1660
+ default: async (event) => {
1661
+ const resp = await this.configureFactor2(event);
1662
+ return resp;
1663
+ }
1664
+ },
1665
+ load: async (event) => {
1666
+ return {
1667
+ ...this.baseEndpoint(event),
1668
+ };
1669
+ },
1670
+ };
1671
+ deleteUserEndpoint = {
1672
+ actions: {
1673
+ default: async (event) => {
1674
+ const resp = await this.deleteUser(event);
1675
+ return resp;
1676
+ }
1677
+ },
1678
+ load: async (event) => {
1679
+ return {
1680
+ ...this.baseEndpoint(event),
1681
+ };
1682
+ },
1683
+ };
1684
+ resetPasswordEndpoint = {
1685
+ actions: {
1686
+ default: async (event) => {
1687
+ const resp = await this.requestPasswordReset(event);
1688
+ return resp;
1689
+ }
1690
+ },
1691
+ load: async (event) => {
1692
+ let data = {};
1693
+ let requiredString = event.url.searchParams.get("required");
1694
+ let required = undefined;
1695
+ if (requiredString) {
1696
+ requiredString = requiredString.toLowerCase();
1697
+ required = requiredString == "true" || requiredString == "1";
1698
+ if (required == true)
1699
+ data.required = true;
1700
+ }
1701
+ return {
1702
+ ...data,
1703
+ ...this.baseEndpoint(event),
1704
+ };
1705
+ },
1706
+ };
1707
+ passwordResetTokenEndpoint = {
1708
+ actions: {
1709
+ default: async (event) => {
1710
+ let resp = await this.validatePasswordResetToken(event);
1711
+ if (!resp?.user)
1712
+ throw new CrossauthError(ErrorCode.InvalidToken, "The password reset token is invalid");
1713
+ if (resp.user.factor2 != "" && !event.locals.sessionId) {
1714
+ // If we have 2FA, we need to create an anonymous session with
1715
+ // user.username set for the 2FA hook to pick up the 2FA config
1716
+ await this.sessionServer.createAnonymousSession(event, { user: { username: resp.user.username } });
1717
+ }
1718
+ if (resp?.error) {
1719
+ return {
1720
+ ok: false,
1721
+ tokenValidated: false,
1722
+ error: resp?.error,
1723
+ ...this.baseEndpoint(event),
1724
+ };
1725
+ }
1726
+ try {
1727
+ resp = await this.resetPassword(event);
1728
+ return resp;
1729
+ }
1730
+ catch (e) {
1731
+ const ce = CrossauthError.asCrossauthError(e);
1732
+ if (SvelteKitServer.isSvelteKitRedirect(e))
1733
+ throw e;
1734
+ if (SvelteKitServer.isSvelteKitError(e))
1735
+ throw e;
1736
+ CrossauthLogger.logger.debug(j({ err: ce }));
1737
+ CrossauthLogger.logger.error(j({ cerr: ce }));
1738
+ return {
1739
+ ok: false,
1740
+ tokenValidated: false,
1741
+ error: resp?.error,
1742
+ errorCode: ce.code,
1743
+ errorCodeName: ce.codeName,
1744
+ ...this.baseEndpoint(event),
1745
+ };
1746
+ }
1747
+ }
1748
+ },
1749
+ load: async (event) => {
1750
+ try {
1751
+ if (event.request.method != "POST") {
1752
+ const resp = await this.validatePasswordResetToken(event);
1753
+ if (!resp?.user)
1754
+ throw new CrossauthError(ErrorCode.InvalidToken, "The password reset token is invalid");
1755
+ if (resp.user.factor2 != "" && !event.locals.sessionId) {
1756
+ // If we have 2FA, we need to create an anonymous session with
1757
+ // user.username set for the 2FA hook to pick up the 2FA config
1758
+ await this.sessionServer.createAnonymousSession(event, { user: { username: resp.user.username } });
1759
+ }
1760
+ return {
1761
+ tokenValidated: resp?.ok ?? false,
1762
+ error: resp?.error,
1763
+ ...this.baseEndpoint(event),
1764
+ };
1765
+ }
1766
+ else {
1767
+ return {
1768
+ tokenValidated: false,
1769
+ ...this.baseEndpoint(event),
1770
+ };
1771
+ }
1772
+ }
1773
+ catch (e) {
1774
+ const ce = CrossauthError.asCrossauthError(e);
1775
+ CrossauthLogger.logger.debug(j({ err: ce }));
1776
+ CrossauthLogger.logger.error(j({ cerr: ce }));
1777
+ return {
1778
+ tokenValidated: false,
1779
+ error: ce.message,
1780
+ errorCode: ce.code,
1781
+ errorCodeName: ce.codeName,
1782
+ ...this.baseEndpoint(event),
1783
+ };
1784
+ }
1785
+ },
1786
+ };
1787
+ updateUserEndpoint = {
1788
+ actions: {
1789
+ default: async (event) => {
1790
+ const resp = await this.updateUser(event);
1791
+ return resp;
1792
+ }
1793
+ },
1794
+ load: async (event) => {
1795
+ //this.sessionServer?.refreshLocals(event);
1796
+ let allowedFactor2 = this.sessionServer.allowedFactor2 ??
1797
+ [{ name: "none", friendlyName: "None" }];
1798
+ return {
1799
+ allowedFactor2,
1800
+ ...this.baseEndpoint(event),
1801
+ };
1802
+ }
1803
+ };
1804
+ verifyEmailTokenEndpoint = {
1805
+ load: async (event) => {
1806
+ const resp = await this.verifyEmail(event);
1807
+ return {
1808
+ ...this.baseEndpoint(event),
1809
+ ...resp,
1810
+ };
1811
+ },
1812
+ };
1813
+ }