@crossauth/sveltekit 1.1.0 → 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/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 -4
- package/dist/sveltekitapikey.js +81 -0
- package/dist/sveltekitoauthclient.d.ts +6 -5
- 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 -5
- package/dist/sveltekitresserver.js +286 -0
- package/dist/sveltekitserver.d.ts +11 -10
- package/dist/sveltekitserver.js +393 -0
- package/dist/sveltekitsession.d.ts +5 -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 +2 -3
- 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 +6 -4
- 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
|
+
}
|