@semapps/auth 1.1.3 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/dist/index.d.ts +8 -0
  2. package/dist/index.js +9 -0
  3. package/dist/index.js.map +1 -0
  4. package/dist/middlewares/localLogout.d.ts +2 -0
  5. package/dist/middlewares/localLogout.js +6 -0
  6. package/dist/middlewares/localLogout.js.map +1 -0
  7. package/dist/middlewares/redirectToFront.d.ts +2 -0
  8. package/dist/middlewares/redirectToFront.js +15 -0
  9. package/dist/middlewares/redirectToFront.js.map +1 -0
  10. package/dist/middlewares/saveRedirectUrl.d.ts +2 -0
  11. package/dist/middlewares/saveRedirectUrl.js +9 -0
  12. package/dist/middlewares/saveRedirectUrl.js.map +1 -0
  13. package/dist/middlewares/sendToken.d.ts +2 -0
  14. package/dist/middlewares/sendToken.js +6 -0
  15. package/dist/middlewares/sendToken.js.map +1 -0
  16. package/dist/mixins/auth.d.ts +98 -0
  17. package/dist/mixins/auth.js +235 -0
  18. package/dist/mixins/auth.js.map +1 -0
  19. package/dist/mixins/auth.sso.d.ts +76 -0
  20. package/dist/mixins/auth.sso.js +82 -0
  21. package/dist/mixins/auth.sso.js.map +1 -0
  22. package/dist/services/account.d.ts +122 -0
  23. package/dist/services/account.js +324 -0
  24. package/dist/services/account.js.map +1 -0
  25. package/dist/services/auth.cas.d.ts +100 -0
  26. package/dist/services/auth.cas.js +43 -0
  27. package/dist/services/auth.cas.js.map +1 -0
  28. package/dist/services/auth.local.d.ts +143 -0
  29. package/dist/services/auth.local.js +229 -0
  30. package/dist/services/auth.local.js.map +1 -0
  31. package/dist/services/auth.oidc.d.ts +102 -0
  32. package/dist/services/auth.oidc.js +63 -0
  33. package/dist/services/auth.oidc.js.map +1 -0
  34. package/dist/services/jwt.d.ts +50 -0
  35. package/dist/services/jwt.js +111 -0
  36. package/dist/services/jwt.js.map +1 -0
  37. package/dist/services/mail.d.ts +31 -0
  38. package/dist/services/mail.js +52 -0
  39. package/dist/services/mail.js.map +1 -0
  40. package/dist/services/migration.d.ts +18 -0
  41. package/dist/services/migration.js +33 -0
  42. package/dist/services/migration.js.map +1 -0
  43. package/dist/tsconfig.tsbuildinfo +1 -0
  44. package/index.ts +17 -0
  45. package/middlewares/localLogout.ts +6 -0
  46. package/middlewares/{redirectToFront.js → redirectToFront.ts} +2 -2
  47. package/middlewares/{saveRedirectUrl.js → saveRedirectUrl.ts} +2 -2
  48. package/middlewares/{sendToken.js → sendToken.ts} +2 -2
  49. package/mixins/auth.sso.ts +100 -0
  50. package/mixins/{auth.js → auth.ts} +91 -67
  51. package/package.json +16 -10
  52. package/services/account.ts +382 -0
  53. package/services/auth.cas.ts +56 -0
  54. package/services/auth.local.ts +276 -0
  55. package/services/{auth.oidc.js → auth.oidc.ts} +21 -9
  56. package/services/jwt.ts +127 -0
  57. package/services/mail.ts +67 -0
  58. package/services/migration.ts +43 -0
  59. package/tsconfig.json +10 -0
  60. package/index.js +0 -9
  61. package/middlewares/localLogout.js +0 -6
  62. package/mixins/auth.sso.js +0 -93
  63. package/services/account.js +0 -315
  64. package/services/auth.cas.js +0 -45
  65. package/services/auth.local.js +0 -238
  66. package/services/jwt.js +0 -101
  67. package/services/mail.js +0 -49
  68. package/services/migration.js +0 -29
package/package.json CHANGED
@@ -1,18 +1,17 @@
1
1
  {
2
2
  "name": "@semapps/auth",
3
- "version": "1.1.3",
3
+ "version": "1.2.0",
4
4
  "description": "Authentification module for SemApps",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Virtual Assembly",
7
7
  "dependencies": {
8
- "@semapps/ldp": "1.1.3",
9
- "@semapps/middlewares": "1.1.3",
10
- "@semapps/mime-types": "1.1.3",
11
- "@semapps/triplestore": "1.1.3",
8
+ "@semapps/ldp": "1.2.0",
9
+ "@semapps/middlewares": "1.2.0",
10
+ "@semapps/mime-types": "1.2.0",
11
+ "@semapps/triplestore": "1.2.0",
12
12
  "bcrypt": "^5.0.1",
13
13
  "express-session": "^1.17.0",
14
- "jsonwebtoken": "^8.5.1",
15
- "moleculer": "^0.14.17",
14
+ "jsonwebtoken": "^9.0.2",
16
15
  "moleculer-db": "^0.8.16",
17
16
  "moleculer-mail": "^1.2.5",
18
17
  "moleculer-web": "^0.10.0-beta1",
@@ -25,10 +24,17 @@
25
24
  "url-join": "^4.0.1"
26
25
  },
27
26
  "publishConfig": {
28
- "access": "public"
27
+ "access": "public",
28
+ "main": "./dist/index.js"
29
29
  },
30
30
  "engines": {
31
- "node": ">=14"
31
+ "node": ">=22.10.0"
32
32
  },
33
- "gitHead": "cc54d6fb548a7c34d3007d159b31145a5f509898"
33
+ "type": "module",
34
+ "main": "./dist/index.js",
35
+ "peerDependencies": {
36
+ "moleculer": "^0.14.35"
37
+ },
38
+ "gitHead": "b2d4edd4ace7346c2452cc8831cb30888afda3a9",
39
+ "xDevMain": "./index.ts"
34
40
  }
@@ -0,0 +1,382 @@
1
+ // @ts-expect-error TS(7016): Could not find a declaration file for module 'bcry... Remove this comment to see the full error message
2
+ import bcrypt from 'bcrypt';
3
+ // @ts-expect-error TS(7016): Could not find a declaration file for module 'spea... Remove this comment to see the full error message
4
+ import createSlug from 'speakingurl';
5
+ import DbService from 'moleculer-db';
6
+ import { TripleStoreAdapter } from '@semapps/triplestore';
7
+ import crypto from 'crypto';
8
+ import { ServiceSchema } from 'moleculer';
9
+
10
+ // Taken from https://stackoverflow.com/a/9204568/7900695
11
+ const emailRegexp = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
12
+
13
+ const AuthAccountSchema = {
14
+ name: 'auth.account' as const,
15
+ mixins: [DbService],
16
+ adapter: new TripleStoreAdapter({ type: 'AuthAccount', dataset: 'settings' }),
17
+ settings: {
18
+ idField: '@id',
19
+ reservedUsernames: ['relay'],
20
+ minPasswordLength: 1,
21
+ minUsernameLength: 1
22
+ },
23
+ dependencies: ['triplestore'],
24
+ actions: {
25
+ create: {
26
+ async handler(ctx) {
27
+ let { uuid, username, password, email, webId, ...rest } = ctx.params;
28
+
29
+ // FORMAT AND VERIFY PASSWORD
30
+
31
+ if (password) {
32
+ if (password.length < this.settings.minPasswordLength) {
33
+ throw new Error('password.too-short');
34
+ }
35
+
36
+ password = await this.hashPassword(password);
37
+ }
38
+
39
+ // FORMAT AND VERIFY EMAIL
40
+
41
+ if (email) {
42
+ email = email.toLowerCase();
43
+
44
+ const emailExists = await ctx.call('auth.account.emailExists', { email });
45
+ if (emailExists) {
46
+ throw new Error('email.already.exists');
47
+ }
48
+
49
+ if (!emailRegexp.test(email)) {
50
+ throw new Error('email.invalid');
51
+ }
52
+ }
53
+
54
+ // FORMAT AND VERIFY USERNAME
55
+
56
+ if (username) {
57
+ // @ts-expect-error TS(2339): Property 'isSystemCall' does not exist on type '{}... Remove this comment to see the full error message
58
+ if (!ctx.meta.isSystemCall) {
59
+ const { isValid, error } = await this.isValidUsername(ctx, username);
60
+ if (!isValid) throw new Error(error);
61
+ }
62
+ } else if (email) {
63
+ // If username is not provided, find one automatically from the email (without errors)
64
+ username = createSlug(email.split('@')[0].toLowerCase());
65
+
66
+ let { isValid, error } = await this.isValidUsername(ctx, username);
67
+
68
+ if (!isValid) {
69
+ if (error === 'username.invalid' || error === 'username.too-short') {
70
+ // If username generated from email is invalid, use a generic name
71
+ username = 'user';
72
+ }
73
+
74
+ // If necessary, add a number after the username
75
+ let i = 0;
76
+ do {
77
+ username = i === 0 ? username : username + i;
78
+ ({ isValid } = await this.isValidUsername(ctx, username));
79
+ } while (!isValid);
80
+ }
81
+ } else {
82
+ throw new Error('You must provide at least a username or an email address');
83
+ }
84
+
85
+ return await this._create(ctx, {
86
+ ...rest,
87
+ uuid,
88
+ username,
89
+ email,
90
+ hashedPassword: password,
91
+ webId
92
+ });
93
+ }
94
+ },
95
+
96
+ attachWebId: {
97
+ async handler(ctx) {
98
+ const { accountUri, webId } = ctx.params;
99
+
100
+ return await this._update(ctx, {
101
+ '@id': accountUri,
102
+ webId
103
+ });
104
+ }
105
+ },
106
+
107
+ verify: {
108
+ async handler(ctx) {
109
+ const { username, password } = ctx.params;
110
+
111
+ // If the username includes a @, assume it is an email
112
+ const query = username.includes('@') ? { email: username } : { username };
113
+
114
+ const accounts = await this._find(ctx, { query });
115
+
116
+ if (accounts.length > 0) {
117
+ const passwordMatch = await this.comparePassword(password, accounts[0].hashedPassword);
118
+ if (passwordMatch) {
119
+ return accounts[0];
120
+ }
121
+ throw new Error('account.not-found');
122
+ } else {
123
+ throw new Error('account.not-found');
124
+ }
125
+ }
126
+ },
127
+
128
+ usernameExists: {
129
+ async handler(ctx) {
130
+ const { username } = ctx.params;
131
+ const accounts = await this._find(ctx, { query: { username } });
132
+ return accounts.length > 0;
133
+ }
134
+ },
135
+
136
+ emailExists: {
137
+ async handler(ctx) {
138
+ const { email } = ctx.params;
139
+ const accounts = await this._find(ctx, { query: { email } });
140
+ return accounts.length > 0;
141
+ }
142
+ },
143
+
144
+ find: {
145
+ /** Overwrite find method, to filter accounts with tombstone. */
146
+ async handler(ctx) {
147
+ /** @type {object[]} */
148
+ const accounts = await this._find(ctx, ctx.params);
149
+ return accounts.filter((account: any) => !account.deletedAt);
150
+ }
151
+ },
152
+
153
+ findByUsername: {
154
+ async handler(ctx) {
155
+ const { username } = ctx.params;
156
+ const accounts = await this._find(ctx, { query: { username } });
157
+ return accounts.length > 0 ? accounts[0] : null;
158
+ }
159
+ },
160
+
161
+ findByWebId: {
162
+ async handler(ctx) {
163
+ const { webId } = ctx.params;
164
+ const accounts = await this._find(ctx, { query: { webId } });
165
+ return accounts.length > 0 ? accounts[0] : null;
166
+ }
167
+ },
168
+
169
+ findByEmail: {
170
+ async handler(ctx) {
171
+ const { email } = ctx.params;
172
+ const accounts = await this._find(ctx, { query: { email } });
173
+ return accounts.length > 0 ? accounts[0] : null;
174
+ }
175
+ },
176
+
177
+ setPassword: {
178
+ async handler(ctx) {
179
+ const { webId, password } = ctx.params;
180
+ const hashedPassword = await this.hashPassword(password);
181
+ const account = await ctx.call('auth.account.findByWebId', { webId });
182
+
183
+ return await this._update(ctx, {
184
+ '@id': account['@id'],
185
+ hashedPassword
186
+ });
187
+ }
188
+ },
189
+
190
+ setNewPassword: {
191
+ async handler(ctx) {
192
+ const { webId, token, password } = ctx.params;
193
+ const hashedPassword = await this.hashPassword(password);
194
+ const account = await ctx.call('auth.account.findByWebId', { webId });
195
+
196
+ if (account.resetPasswordToken !== token) {
197
+ throw new Error('auth.password.invalid_reset_token');
198
+ }
199
+
200
+ return await this._update(ctx, {
201
+ '@id': account['@id'],
202
+ hashedPassword,
203
+ resetPasswordToken: undefined
204
+ });
205
+ }
206
+ },
207
+
208
+ generateResetPasswordToken: {
209
+ async handler(ctx) {
210
+ const { webId } = ctx.params;
211
+ const resetPasswordToken = await this.generateResetPasswordToken();
212
+ const account = await ctx.call('auth.account.findByWebId', { webId });
213
+
214
+ await this._update(ctx, {
215
+ '@id': account['@id'],
216
+ resetPasswordToken
217
+ });
218
+
219
+ return resetPasswordToken;
220
+ }
221
+ },
222
+
223
+ findDatasetByWebId: {
224
+ async handler(ctx) {
225
+ // @ts-expect-error TS(2339): Property 'webId' does not exist on type '{}'.
226
+ const webId = ctx.params.webId || ctx.meta.webId;
227
+ const account = await ctx.call('auth.account.findByWebId', { webId });
228
+ return account?.username;
229
+ }
230
+ },
231
+
232
+ findSettingsByWebId: {
233
+ async handler(ctx) {
234
+ // @ts-expect-error TS(2339): Property 'webId' does not exist on type '{}'.
235
+ const { webId } = ctx.meta;
236
+
237
+ const account = await ctx.call('auth.account.findByWebId', { webId });
238
+
239
+ return {
240
+ email: account.email,
241
+ preferredLocale: account.preferredLocale
242
+ };
243
+ }
244
+ },
245
+
246
+ updateAccountSettings: {
247
+ async handler(ctx) {
248
+ const { currentPassword, email, newPassword } = ctx.params;
249
+ // @ts-expect-error TS(2339): Property 'webId' does not exist on type '{}'.
250
+ const { webId } = ctx.meta;
251
+ const account = await ctx.call('auth.account.findByWebId', { webId });
252
+ const passwordMatch = await this.comparePassword(currentPassword, account.hashedPassword);
253
+ let params = {};
254
+
255
+ if (!passwordMatch) {
256
+ throw new Error('auth.account.invalid_password');
257
+ }
258
+
259
+ if (newPassword) {
260
+ const hashedPassword = await this.hashPassword(newPassword);
261
+ params = { ...params, hashedPassword };
262
+ }
263
+
264
+ if (email !== account.email) {
265
+ const existing = await ctx.call('auth.account.findByEmail', { email });
266
+ if (existing) {
267
+ throw new Error('email.already.exists');
268
+ }
269
+
270
+ params = { ...params, email };
271
+ }
272
+
273
+ return await this._update(ctx, {
274
+ '@id': account['@id'],
275
+ ...params
276
+ });
277
+ }
278
+ },
279
+
280
+ deleteByWebId: {
281
+ async handler(ctx) {
282
+ const { webId } = ctx.params;
283
+ const account = await ctx.call('auth.account.findByWebId', { webId });
284
+
285
+ if (account) {
286
+ await this._remove(ctx, { id: account['@id'] });
287
+ return true;
288
+ }
289
+
290
+ return false;
291
+ }
292
+ },
293
+
294
+ setTombstone: {
295
+ // Remove email and password from an account, set deletedAt timestamp.
296
+ async handler(ctx) {
297
+ const { webId } = ctx.params;
298
+ const account = await ctx.call('auth.account.findByWebId', { webId });
299
+
300
+ return await this._update(ctx, {
301
+ // Set all values to undefined...
302
+ ...Object.fromEntries(Object.keys(account).map(key => [key, null])),
303
+ '@id': account['@id'],
304
+ // ...except for
305
+ webId: account.webId,
306
+ username: account.username,
307
+ // And add a deletedAt date.
308
+ deletedAt: new Date().toISOString()
309
+ });
310
+ }
311
+ }
312
+ },
313
+ methods: {
314
+ async isValidUsername(ctx, username) {
315
+ let error;
316
+
317
+ // Ensure the username has no space or special characters
318
+ if (!/^[a-z0-9\-+_.]+$/.exec(username)) {
319
+ error = 'username.invalid';
320
+ }
321
+
322
+ if (username.length < this.settings.minUsernameLength) {
323
+ error = 'username.too-short';
324
+ }
325
+
326
+ // Ensure we don't use reservedUsernames
327
+ if (this.settings.reservedUsernames.includes(username)) {
328
+ error = 'username.reserved';
329
+ }
330
+
331
+ // Ensure username doesn't already exist
332
+ const usernameExists = await ctx.call('auth.account.usernameExists', { username });
333
+ if (usernameExists) {
334
+ error = 'username.already.exists';
335
+ }
336
+
337
+ return { isValid: !error, error };
338
+ },
339
+ async hashPassword(password) {
340
+ return new Promise((resolve, reject) => {
341
+ bcrypt.hash(password, 10, (err: any, hash: any) => {
342
+ if (err) {
343
+ reject(err);
344
+ } else {
345
+ resolve(hash);
346
+ }
347
+ });
348
+ });
349
+ },
350
+ async comparePassword(password, hash) {
351
+ return new Promise(resolve => {
352
+ bcrypt.compare(password, hash, (err: any, res: any) => {
353
+ if (res === true) {
354
+ resolve(true);
355
+ } else {
356
+ resolve(false);
357
+ }
358
+ });
359
+ });
360
+ },
361
+ async generateResetPasswordToken() {
362
+ return new Promise((resolve, reject) => {
363
+ crypto.randomBytes(32, (ex, buf) => {
364
+ if (ex) {
365
+ reject(ex);
366
+ }
367
+ resolve(buf.toString('hex'));
368
+ });
369
+ });
370
+ }
371
+ }
372
+ } satisfies ServiceSchema;
373
+
374
+ export default AuthAccountSchema;
375
+
376
+ declare global {
377
+ export namespace Moleculer {
378
+ export interface AllServices {
379
+ [AuthAccountSchema.name]: typeof AuthAccountSchema;
380
+ }
381
+ }
382
+ }
@@ -0,0 +1,56 @@
1
+ // @ts-expect-error TS(7016): Could not find a declaration file for module 'pass... Remove this comment to see the full error message
2
+ import { Strategy } from 'passport-cas2';
3
+ // @ts-expect-error TS(2614): Module '"moleculer-web"' has no exported member 'E... Remove this comment to see the full error message
4
+ import { Errors as E } from 'moleculer-web';
5
+ import { ServiceSchema } from 'moleculer';
6
+ import AuthSSOMixin from '../mixins/auth.sso.ts';
7
+
8
+ const AuthCASService = {
9
+ name: 'auth' as const,
10
+ mixins: [AuthSSOMixin],
11
+ settings: {
12
+ baseUrl: null,
13
+ jwtPath: null,
14
+ registrationAllowed: true,
15
+ reservedUsernames: [],
16
+ webIdSelection: [],
17
+ // SSO-specific settings
18
+ sessionSecret: 's€m@pps',
19
+ selectSsoData: null,
20
+ // Cas-specific settings
21
+ casUrl: null
22
+ },
23
+ async created() {
24
+ this.passportId = 'cas';
25
+ },
26
+ methods: {
27
+ getStrategy() {
28
+ return new Strategy(
29
+ {
30
+ casURL: this.settings.casUrl,
31
+ passReqToCallback: true
32
+ },
33
+ (req: any, username: any, profile: any, done: any) => {
34
+ req.$ctx
35
+ .call('auth.loginOrSignup', { ssoData: { username, ...profile } })
36
+ .then((loginData: any) => {
37
+ done(null, loginData);
38
+ })
39
+ .catch((e: any) => {
40
+ done(new E.UnAuthorizedError(e.message), false);
41
+ });
42
+ }
43
+ );
44
+ }
45
+ }
46
+ } satisfies ServiceSchema;
47
+
48
+ export default AuthCASService;
49
+
50
+ declare global {
51
+ export namespace Moleculer {
52
+ export interface AllServices {
53
+ [AuthCASService.name]: typeof AuthCASService;
54
+ }
55
+ }
56
+ }