@1auth/authn 0.0.0-alpha.5 → 0.0.0-alpha.51

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 +271 -87
  2. package/package.json +5 -5
  3. package/LICENSE +0 -21
package/index.js CHANGED
@@ -1,68 +1,152 @@
1
1
  import { setTimeout } from 'node:timers/promises'
2
- import { randomId, makeSymetricKey } from '@1auth/crypto'
2
+ import {
3
+ randomId,
4
+ symmetricGenerateEncryptionKey,
5
+ symmetricEncryptFields,
6
+ symmetricDecryptFields
7
+ } from '@1auth/crypto'
3
8
 
4
- export const options = {
9
+ const id = 'authn'
10
+
11
+ const defaults = {
12
+ id,
13
+ log: false,
5
14
  store: undefined,
6
15
  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
16
+ table: 'authentications',
17
+ idGenerate: true,
18
+ idPrefix: 'authn',
19
+ randomId: { ...randomId },
20
+ authenticationDuration: 500, // minimum duration authentication should take (ms)
21
+ usernameExists: [], // hooks to allow what to be used as a username
22
+ encryptedFields: ['value']
10
23
  }
11
- export default (params) => {
12
- Object.assign(options, params)
24
+ const options = {}
25
+ export default (opt = {}) => {
26
+ Object.assign(options, defaults, opt)
13
27
  }
14
28
  export const getOptions = () => options
15
29
 
30
+ export const exists = async (credentialOptions, sub, params) => {
31
+ const type = makeType(credentialOptions)
32
+ const list = await options.store.selectList(options.table, {
33
+ ...params,
34
+ sub,
35
+ type
36
+ })
37
+ return list.length > 1
38
+ }
39
+
40
+ export const count = async (credentialOptions, sub) => {
41
+ const type = makeType(credentialOptions)
42
+ const credentials = await options.store.selectList(
43
+ options.table,
44
+ { sub, type },
45
+ ['verify', 'expire']
46
+ )
47
+ let count = 0
48
+ const now = nowInSeconds()
49
+ for (let i = credentials.length; i--;) {
50
+ const credential = credentials[i]
51
+ if (credential.expire && credential.expire < now) {
52
+ continue
53
+ }
54
+ if (!credential.verify) {
55
+ continue
56
+ }
57
+ count += 1
58
+ }
59
+ return count
60
+ }
61
+
62
+ export const list = async (credentialOptions, sub, params, fields) => {
63
+ const type = makeType(credentialOptions)
64
+ const items = await options.store.selectList(
65
+ options.table,
66
+ {
67
+ ...params,
68
+ sub,
69
+ type
70
+ },
71
+ fields
72
+ )
73
+ // const now = nowInSeconds();
74
+ const list = []
75
+ for (let i = items.length; i--;) {
76
+ const item = items[i]
77
+ // TODO need filter for expire
78
+ // if (credential.expire < now) {
79
+ // continue;
80
+ // }
81
+ const { encryptionKey: encryptedKey } = item
82
+ delete item.encryptionKey
83
+ const decryptedItem = symmetricDecryptFields(
84
+ item,
85
+ { encryptedKey, sub },
86
+ options.encryptedFields
87
+ )
88
+ list.push(decryptedItem)
89
+ }
90
+ return list
91
+ }
92
+
16
93
  export const create = async (
17
- credentialType,
18
- { id, sub, value, ...rest },
19
- parentOptions
94
+ credentialOptions,
95
+ sub,
96
+ { id, value, ...values }
20
97
  ) => {
21
98
  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
99
+ const type = makeType(credentialOptions)
100
+ let { otp, expire } = credentialOptions
101
+ expire &&= now + expire
102
+
103
+ if (options.idGenerate) {
104
+ id ??= await options.randomId.create(options.idPrefix)
105
+ }
106
+ value ??= credentialOptions.create()
107
+ const encodedValue = await credentialOptions.encode(value)
108
+
109
+ const { encryptionKey, encryptedKey } = symmetricGenerateEncryptionKey(sub)
110
+ const encryptedValues = symmetricEncryptFields(
111
+ { ...values, value: encodedValue },
112
+ { encryptionKey, sub },
113
+ options.encryptedFields
34
114
  )
35
- await options.store.insert(options.table, {
36
- expire,
37
- ...rest,
38
- id,
115
+ const params = {
116
+ ...encryptedValues,
39
117
  sub,
40
118
  type,
41
119
  otp,
42
120
  encryptionKey: encryptedKey,
43
- value: encryptedData,
44
121
  create: now,
45
- update: now
46
- })
47
- return id
122
+ update: now,
123
+ expire
124
+ }
125
+ if (options.idGenerate) {
126
+ params.id = id
127
+ }
128
+ const row = await options.store.insert(options.table, params)
129
+ return { type, id: row.id, value, otp, expire }
48
130
  }
49
131
 
50
132
  export const update = async (
51
- credentialType,
52
- { id, sub, encryptionKey, value, ...rest },
53
- parentOptions
133
+ credentialOptions,
134
+ { id, sub, encryptionKey, encryptedKey, value, ...values }
54
135
  ) => {
55
136
  const now = nowInSeconds()
56
- const encryptedData = await parentOptions[credentialType].encode(
137
+ // const type = makeType(credentialOptions);
138
+
139
+ const encryptedData = await credentialOptions.encode(
57
140
  value,
58
141
  encryptionKey,
142
+ encryptedKey,
59
143
  sub
60
144
  )
61
145
  return options.store.update(
62
146
  options.table,
63
- { id, sub },
147
+ { sub, id },
64
148
  {
65
- ...rest,
149
+ ...values,
66
150
  value: encryptedData,
67
151
  update: now
68
152
  }
@@ -71,92 +155,190 @@ export const update = async (
71
155
 
72
156
  export const subject = async (username) => {
73
157
  return Promise.all(
74
- options.usernameExists.map((exists) => exists(username))
158
+ options.usernameExists.map((exists) => {
159
+ return exists(username)
160
+ })
75
161
  ).then((identities) => {
76
162
  return identities.filter((lookup) => lookup)?.[0]
77
163
  })
78
164
  }
79
165
 
80
- export const authenticate = async (username, secret, parentOptions) => {
81
- const timeout = setTimeout(() => {}, options.authenticationDuration)
82
- const type = parentOptions.id + '-' + parentOptions.secret.type
83
-
166
+ export const authenticate = async (credentialOptions, username, secret) => {
84
167
  const sub = await subject(username)
85
168
 
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
169
+ const timeout = setTimeout(options.authenticationDuration)
170
+ const type = makeType(credentialOptions)
171
+
172
+ const credentials = await options.store.selectList(
173
+ options.table,
174
+ {
175
+ sub,
176
+ type
177
+ },
178
+ ['id', 'encryptionKey', 'value', 'otp', 'verify', 'expire', 'sourceId']
179
+ )
180
+ const now = nowInSeconds()
181
+ let valid
182
+ let skipIgnoredCount = 0
183
+ let skipExpiredCount = 0
91
184
  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)
185
+ // non-opt credentials must be verified before use
186
+ if (!credential.otp && !credential.verify) {
187
+ skipIgnoredCount += 1
188
+ continue
189
+ }
190
+ // skip expired
191
+ if (credential.expire < now) {
192
+ skipExpiredCount += 1
193
+ continue
194
+ }
195
+ const { encryptionKey: encryptedKey } = credential
196
+ const decryptedCredential = symmetricDecryptFields(
197
+ credential,
198
+ { encryptedKey, sub },
199
+ options.encryptedFields
200
+ )
201
+ let { value, ...values } = decryptedCredential
202
+ value = await credentialOptions.decode(value)
203
+ try {
204
+ valid = await credentialOptions.verify(secret, value, values)
205
+ } catch (e) {
206
+ if (options.log) {
207
+ options.log(e)
208
+ }
209
+ continue
210
+ }
95
211
  if (valid) {
96
- id ??= credential.id
97
- encryptionKey ??= encryptedKey
212
+ const { id, otp } = credential
213
+ if (otp) {
214
+ await options.store.update(
215
+ options.table,
216
+ { id, sub },
217
+ { update: now, expire: now, lastused: now }
218
+ )
219
+ } else if (credentialOptions.clean) {
220
+ await credentialOptions.clean(sub, value, values)
221
+ } else {
222
+ const now = nowInSeconds()
223
+ await options.store.update(
224
+ options.table,
225
+ { id, sub },
226
+ { update: now, lastused: now }
227
+ )
228
+ }
229
+
98
230
  break
99
231
  }
100
232
  }
101
233
 
102
- if (valid && parentOptions.secret.otp) {
103
- await options.store.remove(options.table, { id, sub })
104
- }
105
234
  await timeout
106
- if (!valid) throw new Error('401 Unauthorized')
107
- return { sub, id, encryptionKey, ...valid }
235
+ if (!valid) {
236
+ let cause = 'invalid'
237
+ const credentialsCount = credentials.length - skipIgnoredCount
238
+ if (credentialsCount === 0) {
239
+ cause = 'missing'
240
+ } else if (skipExpiredCount === credentialsCount) {
241
+ cause = 'expired'
242
+ }
243
+ throw new Error('401 Unauthorized', cause)
244
+ }
245
+ return sub
108
246
  }
109
247
 
110
- export const verifySecret = async (sub, id, parentOptions) => {
111
- // const type = parentOptions.id + '-' + parentOptions.secret.type
248
+ export const verifySecret = async (credentialOptions, sub, id) => {
249
+ // const type = makeType(credentialOptions);
250
+ const now = nowInSeconds()
112
251
  await options.store.update(
113
252
  options.table,
114
- { id, sub },
115
- { verify: nowInSeconds() }
253
+ { sub, id },
254
+ { update: now, verify: now }
116
255
  )
117
256
  }
118
257
 
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
258
+ export const verify = async (credentialOptions, sub, input) => {
259
+ const timeout = setTimeout(options.authenticationDuration)
260
+ const type = makeType(credentialOptions)
261
+
123
262
  const credentials = await options.store.selectList(options.table, {
124
263
  sub,
125
264
  type
126
265
  })
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
266
 
267
+ const now = nowInSeconds()
136
268
  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
269
+ let credential
270
+ let skipExpiredCount = 0
271
+ for (credential of credentials) {
272
+ // skip expired
273
+ if (credential.expire < now) {
274
+ skipExpiredCount += 1
275
+ continue
276
+ }
277
+ const { encryptionKey: encryptedKey } = credential
278
+ const decryptedCredential = symmetricDecryptFields(
279
+ credential,
280
+ { encryptedKey, sub },
281
+ options.encryptedFields
143
282
  )
144
- valid = await parentOptions[credentialType].verify(token, value, rest)
283
+ let { value, ...values } = decryptedCredential
284
+ value = await credentialOptions.decode(value)
285
+ try {
286
+ valid = await credentialOptions.verify(input, value, values)
287
+ } catch (e) {
288
+ if (options.log) {
289
+ options.log(e)
290
+ }
291
+ continue
292
+ }
145
293
  if (valid) {
146
- id = credential.id
294
+ const { id, otp } = credential
295
+ if (otp) {
296
+ await options.store.remove(options.table, { id, sub })
297
+ }
147
298
  break
148
299
  }
149
300
  }
150
- if (valid && parentOptions[credentialType].otp) {
151
- await options.store.remove(options.table, { id, sub })
152
- }
153
- if (!valid) throw new Error('401 Unauthorized')
301
+
154
302
  await timeout
155
- return { sub, id, ...valid }
303
+
304
+ if (!valid) {
305
+ let cause = 'invalid'
306
+ const credentialsCount = credentials.length
307
+ if (credentialsCount === 0) {
308
+ cause = 'missing'
309
+ } else if (skipExpiredCount === credentialsCount) {
310
+ cause = 'expired'
311
+ }
312
+ throw new Error('401 Unauthorized', { cause })
313
+ }
314
+ return { ...credential, ...valid }
156
315
  }
157
316
 
158
- export const expire = async (sub, id, parentOptions = options) => {
159
- await options.store.remove(options.table, { id, sub })
317
+ export const expire = async (credentialOptions, sub, id) => {
318
+ // const type = makeType(credentialOptions);
319
+ await options.store.update(
320
+ options.table,
321
+ { sub, id },
322
+ { expire: nowInSeconds() - 1 }
323
+ )
324
+ }
325
+
326
+ export const remove = async (credentialOptions, sub, id) => {
327
+ const type = makeType(credentialOptions)
328
+ await options.store.remove(options.table, { id, type, sub })
329
+ }
330
+
331
+ export const select = async (credentialOptions, sub, id) => {
332
+ const type = makeType(credentialOptions)
333
+ const item = await options.store.select(options.table, { id, type, sub })
334
+ const { encryptionKey: encryptedKey } = item
335
+ delete item.encryptionKey
336
+ const decryptedItem = symmetricDecryptFields(
337
+ item,
338
+ { encryptedKey, sub },
339
+ options.encryptedFields
340
+ )
341
+ return decryptedItem
160
342
  }
161
343
 
162
344
  // TODO manage onboard state
@@ -165,4 +347,6 @@ export const expire = async (sub, id, parentOptions = options) => {
165
347
 
166
348
  // TODO authorize management?
167
349
 
350
+ const makeType = (credentialOptions) =>
351
+ credentialOptions.id + '-' + credentialOptions.type
168
352
  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.5",
3
+ "version": "0.0.0-alpha.51",
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": {
@@ -48,8 +48,8 @@
48
48
  "url": "https://github.com/willfarrell/1auth/issues"
49
49
  },
50
50
  "homepage": "https://github.com/willfarrell/1auth",
51
- "gitHead": "32bb45598025fe664497e4ce8b9c1cf41c75d635",
51
+ "gitHead": "7a6c0fbb8ab71d6a2171e678697de9f237568431",
52
52
  "dependencies": {
53
- "@1auth/crypto": "0.0.0-alpha.5"
53
+ "@1auth/crypto": "0.0.0-alpha.51"
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.