@1auth/authn 0.0.0-alpha.7 → 0.0.0-alpha.71

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 (3) hide show
  1. package/index.js +327 -123
  2. package/package.json +6 -6
  3. package/LICENSE +0 -21
package/index.js CHANGED
@@ -1,163 +1,365 @@
1
- import { setTimeout } from 'node:timers/promises'
2
- import { randomId, makeSymetricKey } from '@1auth/crypto'
1
+ import { setTimeout } from "node:timers/promises";
2
+ import {
3
+ makeRandomConfigObject,
4
+ symmetricGenerateEncryptionKey,
5
+ symmetricEncryptFields,
6
+ symmetricDecryptFields,
7
+ } from "@1auth/crypto";
3
8
 
4
- export const options = {
9
+ const id = "authn";
10
+
11
+ export const randomId = ({ prefix = "authn_", ...params } = {}) =>
12
+ makeRandomConfigObject({
13
+ id,
14
+ prefix,
15
+ ...params,
16
+ });
17
+
18
+ const defaults = {
19
+ id,
20
+ log: false,
5
21
  store: undefined,
6
22
  notify: undefined,
7
- table: 'credentials',
8
- authenticationDuration: 500, // min duration authentication should take (ms)
9
- usernameExists: [] // hooks to allow what to be used as a username
10
- }
11
- export default (params) => {
12
- Object.assign(options, params)
13
- }
14
- export const getOptions = () => options
15
-
16
- export const create = async (
17
- credentialType,
18
- { id, sub, value, ...rest },
19
- parentOptions
23
+ table: "authentications",
24
+ idGenerate: true,
25
+ randomId: randomId(),
26
+ authenticationDuration: 100, // minimum duration authentication should take (ms)
27
+ usernameExists: [], // hooks to allow what to be used as a username
28
+ encryptedFields: ["value"],
29
+ };
30
+ const options = {};
31
+ export default (opt = {}) => {
32
+ Object.assign(options, defaults, opt);
33
+ };
34
+ export const getOptions = () => options;
35
+
36
+ // export const exists = async (credentialOptions, sub, params) => {
37
+ // const type = makeType(credentialOptions);
38
+ // const list = await options.store.selectList(options.table, {
39
+ // ...params,
40
+ // sub,
41
+ // type,
42
+ // });
43
+ // return list.length > 1;
44
+ // };
45
+
46
+ export const count = async (credentialOptions, sub) => {
47
+ const type = makeType(credentialOptions);
48
+ const credentials = await options.store.selectList(
49
+ options.table,
50
+ { sub, type },
51
+ ["verify", "expire"],
52
+ );
53
+ let count = 0;
54
+ const now = nowInSeconds();
55
+ for (let i = credentials.length; i--; ) {
56
+ const credential = credentials[i];
57
+ if (credential.expire && credential.expire < now) {
58
+ continue;
59
+ }
60
+ if (!credential.verify) {
61
+ continue;
62
+ }
63
+ count += 1;
64
+ }
65
+ return count;
66
+ };
67
+
68
+ export const list = async (credentialOptions, sub, params, fields) => {
69
+ const type = makeType(credentialOptions);
70
+ const items = await options.store.selectList(
71
+ options.table,
72
+ {
73
+ ...params,
74
+ sub,
75
+ type,
76
+ },
77
+ fields,
78
+ );
79
+ // const now = nowInSeconds();
80
+ const list = [];
81
+ for (let i = items.length; i--; ) {
82
+ const item = items[i];
83
+ // TODO need filter for expire
84
+ // if (credential.expire < now) {
85
+ // continue;
86
+ // }
87
+ const { encryptionKey: encryptedKey } = item;
88
+ delete item.encryptionKey;
89
+ const decryptedItem = symmetricDecryptFields(
90
+ item,
91
+ { encryptedKey, sub },
92
+ options.encryptedFields,
93
+ );
94
+ list.push(decryptedItem);
95
+ }
96
+ return list;
97
+ };
98
+
99
+ const createCredential = async (
100
+ credentialOptions,
101
+ sub,
102
+ { id, value, ...values },
20
103
  ) => {
21
- const now = nowInSeconds()
22
- id ??= await randomId.create()
23
- const type = parentOptions.id + '-' + parentOptions[credentialType].type
24
- const otp = parentOptions[credentialType].otp
25
- const expire = parentOptions[credentialType].expire
26
- ? now + parentOptions[credentialType].expire
27
- : null
28
- const { encryptedKey } = makeSymetricKey(sub)
29
-
30
- const encryptedData = await parentOptions[credentialType].encode(
31
- value,
32
- encryptedKey,
33
- sub
34
- )
35
- await options.store.insert(options.table, {
36
- expire,
37
- ...rest,
38
- id,
104
+ const now = nowInSeconds();
105
+ const type = makeType(credentialOptions);
106
+ let { otp, expire } = credentialOptions;
107
+ expire &&= now + expire;
108
+
109
+ if (options.idGenerate) {
110
+ id ??= await options.randomId.create(options.idPrefix);
111
+ }
112
+ value ??= credentialOptions.create();
113
+ const encodedValue = await credentialOptions.encode(value);
114
+
115
+ const { encryptionKey, encryptedKey } = symmetricGenerateEncryptionKey(sub);
116
+ const encryptedValues = symmetricEncryptFields(
117
+ { ...values, value: encodedValue },
118
+ { encryptionKey, sub },
119
+ options.encryptedFields,
120
+ );
121
+ const params = {
122
+ ...encryptedValues,
39
123
  sub,
40
124
  type,
41
125
  otp,
42
126
  encryptionKey: encryptedKey,
43
- value: encryptedData,
44
127
  create: now,
45
- update: now
46
- })
47
- return id
48
- }
128
+ update: now,
129
+ expire,
130
+ };
131
+ if (options.idGenerate) {
132
+ params.id = id;
133
+ }
134
+ return params;
135
+ };
136
+
137
+ export const create = async (credentialOptions, sub, values) => {
138
+ const params = await createCredential(credentialOptions, sub, values);
139
+ const id = await options.store.insert(options.table, params);
140
+ return { ...params, id };
141
+ };
142
+
143
+ export const createList = async (credentialOptions, sub, list) => {
144
+ const rows = await Promise.all(
145
+ list.map((values) => createCredential(credentialOptions, sub, values)),
146
+ );
147
+ const params = rows[0];
148
+ const res = await options.store.insertList(options.table, rows);
149
+ return { ...params, id: res };
150
+ };
49
151
 
50
152
  export const update = async (
51
- credentialType,
52
- { id, sub, encryptionKey, value, ...rest },
53
- parentOptions
153
+ credentialOptions,
154
+ { id, sub, encryptionKey, encryptedKey, value, ...values },
54
155
  ) => {
55
- const now = nowInSeconds()
56
- const encryptedData = await parentOptions[credentialType].encode(
57
- value,
58
- encryptionKey,
59
- sub
60
- )
156
+ const now = nowInSeconds();
157
+ // const type = makeType(credentialOptions);
158
+
159
+ const encodedValue = await credentialOptions.encode(value);
160
+
161
+ const encryptedValues = symmetricEncryptFields(
162
+ { ...values, value: encodedValue },
163
+ { encryptionKey, encryptedKey, sub },
164
+ options.encryptedFields,
165
+ );
166
+
61
167
  return options.store.update(
62
168
  options.table,
63
- { id, sub },
169
+ { sub, id },
64
170
  {
65
- ...rest,
66
- value: encryptedData,
67
- update: now
68
- }
69
- )
70
- }
171
+ ...encryptedValues,
172
+ update: now,
173
+ },
174
+ );
175
+ };
71
176
 
72
177
  export const subject = async (username) => {
73
178
  return Promise.all(
74
- options.usernameExists.map((exists) => exists(username))
179
+ options.usernameExists.map((exists) => {
180
+ return exists(username);
181
+ }),
75
182
  ).then((identities) => {
76
- return identities.filter((lookup) => lookup)?.[0]
77
- })
78
- }
183
+ return identities.filter((lookup) => lookup)?.[0];
184
+ });
185
+ };
79
186
 
80
- export const authenticate = async (username, secret, parentOptions) => {
81
- const timeout = setTimeout(() => {}, options.authenticationDuration)
82
- const type = parentOptions.id + '-' + parentOptions.secret.type
187
+ export const authenticate = async (credentialOptions, username, secret) => {
188
+ const sub = await subject(username);
83
189
 
84
- const sub = await subject(username)
190
+ const timeout = setTimeout(options.authenticationDuration);
191
+ const type = makeType(credentialOptions);
85
192
 
86
- const credentials = await options.store.selectList(options.table, {
87
- sub,
88
- type
89
- }) // TODO and verify is not null
90
- let valid, id, encryptionKey
193
+ const credentials = await options.store.selectList(
194
+ options.table,
195
+ {
196
+ sub,
197
+ type,
198
+ },
199
+ ["id", "encryptionKey", "value", "otp", "verify", "expire", "sourceId"],
200
+ );
201
+ const now = nowInSeconds();
202
+ let valid;
203
+ let skipIgnoredCount = 0;
204
+ let skipExpiredCount = 0;
91
205
  for (const credential of credentials) {
92
- let { value, encryptionKey: encryptedKey, ...rest } = credential
93
- value = await parentOptions.secret.decode(value, encryptedKey, sub)
94
- valid = await parentOptions.secret.verify(secret, value, rest)
206
+ // non-opt credentials must be verified before use
207
+ if (!credential.otp && !credential.verify) {
208
+ skipIgnoredCount += 1;
209
+ continue;
210
+ }
211
+ // skip expired
212
+ if (credential.expire < now) {
213
+ skipExpiredCount += 1;
214
+ continue;
215
+ }
216
+ const { encryptionKey: encryptedKey } = credential;
217
+ const decryptedCredential = symmetricDecryptFields(
218
+ credential,
219
+ { encryptedKey, sub },
220
+ options.encryptedFields,
221
+ );
222
+ let { value, ...values } = decryptedCredential;
223
+ value = await credentialOptions.decode(value);
224
+ try {
225
+ valid = await credentialOptions.verify(secret, value, values);
226
+ } catch (e) {
227
+ if (options.log) {
228
+ options.log(e);
229
+ }
230
+ continue;
231
+ }
95
232
  if (valid) {
96
- id ??= credential.id
97
- encryptionKey ??= encryptedKey
98
- break
233
+ const { id, otp } = credential;
234
+ if (otp) {
235
+ await expire(credentialOptions, sub, id, { lastused: now });
236
+ } else {
237
+ const now = nowInSeconds();
238
+ await options.store.update(
239
+ options.table,
240
+ { id, sub },
241
+ { lastused: now },
242
+ );
243
+ }
244
+
245
+ if (credentialOptions.cleanup) {
246
+ await credentialOptions.cleanup(sub, value, values);
247
+ }
248
+ break;
99
249
  }
100
250
  }
101
251
 
102
- if (valid && parentOptions.secret.otp) {
103
- await options.store.remove(options.table, { id, sub })
252
+ await timeout;
253
+ if (!valid) {
254
+ let cause = "invalid";
255
+ const credentialsCount = credentials.length - skipIgnoredCount;
256
+ if (credentialsCount === 0) {
257
+ cause = "missing";
258
+ } else if (skipExpiredCount === credentialsCount) {
259
+ cause = "expired";
260
+ }
261
+ throw new Error("401 Unauthorized", { cause });
104
262
  }
105
- await timeout
106
- if (!valid) throw new Error('401 Unauthorized')
107
- return { sub, id, encryptionKey, ...valid }
108
- }
263
+ return sub;
264
+ };
109
265
 
110
- export const verifySecret = async (sub, id, parentOptions) => {
111
- // const type = parentOptions.id + '-' + parentOptions.secret.type
266
+ export const verifySecret = async (credentialOptions, sub, id) => {
267
+ // const type = makeType(credentialOptions);
268
+ const now = nowInSeconds();
112
269
  await options.store.update(
113
270
  options.table,
114
- { id, sub },
115
- { verify: nowInSeconds() }
116
- )
117
- }
118
-
119
- export const verify = async (credentialType, sub, token, parentOptions) => {
120
- const timeout = setTimeout(() => {}, options.authenticationDuration)
121
- const type = parentOptions.id + '-' + parentOptions[credentialType].type
122
- let id
271
+ { sub, id },
272
+ { update: now, verify: now },
273
+ );
274
+ };
275
+
276
+ // TODO add in sourceId as filter for messengers
277
+ export const verify = async (credentialOptions, sub, input) => {
278
+ const timeout = setTimeout(options.authenticationDuration);
279
+ const type = makeType(credentialOptions);
280
+
123
281
  const credentials = await options.store.selectList(options.table, {
124
282
  sub,
125
- type
126
- })
127
- // TODO re-confirm when needed
128
- // .then((rows) => {
129
- // if (rows.length) {
130
- // return rows
131
- // }
132
- //
133
- // return options.store.select(options.table, { id: sub, type })
134
- // })
135
-
136
- let valid
137
- for (const credential of credentials) {
138
- let { value, encryptionKey, ...rest } = credential
139
- value = await parentOptions[credentialType].decode(
140
- value,
141
- encryptionKey,
142
- sub
143
- )
144
- valid = await parentOptions[credentialType].verify(token, value, rest)
283
+ type,
284
+ });
285
+
286
+ const now = nowInSeconds();
287
+ let valid;
288
+ let credential;
289
+ let skipExpiredCount = 0;
290
+ for (credential of credentials) {
291
+ // skip expired
292
+ if (credential.expire < now) {
293
+ skipExpiredCount += 1;
294
+ continue;
295
+ }
296
+ const { encryptionKey: encryptedKey } = credential;
297
+ const decryptedCredential = symmetricDecryptFields(
298
+ credential,
299
+ { encryptedKey, sub },
300
+ options.encryptedFields,
301
+ );
302
+ let { value, ...values } = decryptedCredential;
303
+ value = await credentialOptions.decode(value);
304
+ try {
305
+ valid = await credentialOptions.verify(input, value, values);
306
+ } catch (e) {
307
+ if (options.log) {
308
+ options.log(e);
309
+ }
310
+ continue;
311
+ }
145
312
  if (valid) {
146
- id = credential.id
147
- break
313
+ const { id, otp } = credential;
314
+ if (otp) {
315
+ await remove(credentialOptions, sub, id);
316
+ }
317
+ break;
148
318
  }
149
319
  }
150
- if (valid && parentOptions[credentialType].otp) {
151
- await options.store.remove(options.table, { id, sub })
320
+
321
+ await timeout;
322
+
323
+ if (!valid) {
324
+ let cause = "invalid";
325
+ const credentialsCount = credentials.length;
326
+ if (credentialsCount === 0) {
327
+ cause = "missing";
328
+ } else if (skipExpiredCount === credentialsCount) {
329
+ cause = "expired";
330
+ }
331
+ throw new Error("401 Unauthorized", { cause });
152
332
  }
153
- if (!valid) throw new Error('401 Unauthorized')
154
- await timeout
155
- return { sub, id, ...valid }
156
- }
333
+ return { ...credential, ...valid };
334
+ };
335
+
336
+ export const expire = async (credentialOptions, sub, id, values) => {
337
+ // const type = makeType(credentialOptions);
338
+ await options.store.update(
339
+ options.table,
340
+ { sub, id },
341
+ { ...values, expire: nowInSeconds() - 1 },
342
+ );
343
+ };
344
+
345
+ export const remove = async (credentialOptions, sub, id) => {
346
+ const type = makeType(credentialOptions);
347
+ await options.store.remove(options.table, { id, type, sub });
348
+ };
157
349
 
158
- export const expire = async (sub, id, parentOptions = options) => {
159
- await options.store.remove(options.table, { id, sub })
160
- }
350
+ export const select = async (credentialOptions, sub, id) => {
351
+ const type = makeType(credentialOptions);
352
+ const item = await options.store.select(options.table, { id, type, sub });
353
+ if (!item) return item;
354
+ const { encryptionKey: encryptedKey } = item;
355
+ delete item.encryptionKey;
356
+ const decryptedItem = symmetricDecryptFields(
357
+ item,
358
+ { encryptedKey, sub },
359
+ options.encryptedFields,
360
+ );
361
+ return decryptedItem;
362
+ };
161
363
 
162
364
  // TODO manage onboard state
163
365
 
@@ -165,4 +367,6 @@ export const expire = async (sub, id, parentOptions = options) => {
165
367
 
166
368
  // TODO authorize management?
167
369
 
168
- const nowInSeconds = () => Math.floor(Date.now() / 1000)
370
+ const makeType = (credentialOptions) =>
371
+ credentialOptions.id + "-" + credentialOptions.type;
372
+ const nowInSeconds = () => Math.floor(Date.now() / 1000);
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@1auth/authn",
3
- "version": "0.0.0-alpha.7",
3
+ "version": "0.0.0-alpha.71",
4
4
  "description": "",
5
5
  "type": "module",
6
6
  "engines": {
7
- "node": ">=16"
7
+ "node": ">=20"
8
8
  },
9
9
  "engineStrict": true,
10
10
  "publishConfig": {
@@ -27,7 +27,7 @@
27
27
  ],
28
28
  "scripts": {
29
29
  "test": "npm run test:unit",
30
- "test:unit": "ava"
30
+ "test:unit": "node --test"
31
31
  },
32
32
  "license": "MIT",
33
33
  "funding": {
@@ -41,15 +41,15 @@
41
41
  },
42
42
  "repository": {
43
43
  "type": "git",
44
- "url": "github:willfarrell/1auth",
44
+ "url": "git+https://github.com/willfarrell/1auth.git",
45
45
  "directory": "packages/authn"
46
46
  },
47
47
  "bugs": {
48
48
  "url": "https://github.com/willfarrell/1auth/issues"
49
49
  },
50
50
  "homepage": "https://github.com/willfarrell/1auth",
51
- "gitHead": "39c704fcab5798f6bee8d00b2bcf574ba250a4cf",
51
+ "gitHead": "7a6c0fbb8ab71d6a2171e678697de9f237568431",
52
52
  "dependencies": {
53
- "@1auth/crypto": "0.0.0-alpha.7"
53
+ "@1auth/crypto": "0.0.0-alpha.71"
54
54
  }
55
55
  }
package/LICENSE DELETED
@@ -1,21 +0,0 @@
1
- MIT License
2
-
3
- Copyright (c) 2023 will Farrell
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.