@1auth/authn 0.0.0-alpha.67 → 0.0.0-alpha.69

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 (2) hide show
  1. package/index.js +372 -0
  2. package/package.json +2 -2
package/index.js ADDED
@@ -0,0 +1,372 @@
1
+ import { setTimeout } from "node:timers/promises";
2
+ import {
3
+ makeRandomConfigObject,
4
+ symmetricGenerateEncryptionKey,
5
+ symmetricEncryptFields,
6
+ symmetricDecryptFields,
7
+ } from "@1auth/crypto";
8
+
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,
21
+ store: undefined,
22
+ notify: undefined,
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 },
103
+ ) => {
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,
123
+ sub,
124
+ type,
125
+ otp,
126
+ encryptionKey: encryptedKey,
127
+ create: now,
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
+ };
151
+
152
+ export const update = async (
153
+ credentialOptions,
154
+ { id, sub, encryptionKey, encryptedKey, value, ...values },
155
+ ) => {
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
+
167
+ return options.store.update(
168
+ options.table,
169
+ { sub, id },
170
+ {
171
+ ...encryptedValues,
172
+ update: now,
173
+ },
174
+ );
175
+ };
176
+
177
+ export const subject = async (username) => {
178
+ return Promise.all(
179
+ options.usernameExists.map((exists) => {
180
+ return exists(username);
181
+ }),
182
+ ).then((identities) => {
183
+ return identities.filter((lookup) => lookup)?.[0];
184
+ });
185
+ };
186
+
187
+ export const authenticate = async (credentialOptions, username, secret) => {
188
+ const sub = await subject(username);
189
+
190
+ const timeout = setTimeout(options.authenticationDuration);
191
+ const type = makeType(credentialOptions);
192
+
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;
205
+ for (const credential of credentials) {
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
+ }
232
+ if (valid) {
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;
249
+ }
250
+ }
251
+
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 });
262
+ }
263
+ return sub;
264
+ };
265
+
266
+ export const verifySecret = async (credentialOptions, sub, id) => {
267
+ // const type = makeType(credentialOptions);
268
+ const now = nowInSeconds();
269
+ await options.store.update(
270
+ options.table,
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
+
281
+ const credentials = await options.store.selectList(options.table, {
282
+ sub,
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
+ }
312
+ if (valid) {
313
+ const { id, otp } = credential;
314
+ if (otp) {
315
+ await remove(credentialOptions, sub, id);
316
+ }
317
+ break;
318
+ }
319
+ }
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 });
332
+ }
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
+ };
349
+
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
+ };
363
+
364
+ // TODO manage onboard state
365
+
366
+ // TODO save notification settings
367
+
368
+ // TODO authorize management?
369
+
370
+ const makeType = (credentialOptions) =>
371
+ credentialOptions.id + "-" + credentialOptions.type;
372
+ const nowInSeconds = () => Math.floor(Date.now() / 1000);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@1auth/authn",
3
- "version": "0.0.0-alpha.67",
3
+ "version": "0.0.0-alpha.69",
4
4
  "description": "",
5
5
  "type": "module",
6
6
  "engines": {
@@ -50,6 +50,6 @@
50
50
  "homepage": "https://github.com/willfarrell/1auth",
51
51
  "gitHead": "7a6c0fbb8ab71d6a2171e678697de9f237568431",
52
52
  "dependencies": {
53
- "@1auth/crypto": "0.0.0-alpha.67"
53
+ "@1auth/crypto": "0.0.0-alpha.69"
54
54
  }
55
55
  }