@toa.io/extensions.exposition 1.0.0-alpha.330 → 1.0.0-alpha.331
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/components/identity.basic/manifest.toa.yaml +3 -1
- package/components/identity.basic/operations/transit.js +2 -2
- package/components/identity.basic/package.json +1 -1
- package/components/identity.basic/source/transit.ts +1 -1
- package/components/identity.basic/types/toa.d.ts +1 -0
- package/components/identity.otp/manifest.toa.yaml +30 -1
- package/components/identity.otp/operations/authenticate.js +14 -18
- package/components/identity.otp/operations/confirm.js +23 -0
- package/components/identity.otp/operations/issue.js +4 -10
- package/components/identity.otp/operations/lib/index.js +2 -2
- package/components/identity.otp/operations/lib/redeem.js +28 -0
- package/components/identity.otp/source/authenticate.ts +14 -23
- package/components/identity.otp/source/confirm.ts +41 -0
- package/components/identity.otp/source/issue.ts +3 -10
- package/components/identity.otp/source/lib/index.ts +1 -0
- package/components/identity.otp/source/lib/redeem.ts +49 -0
- package/components/identity.otp/types/toa.d.ts +8 -0
- package/package.json +4 -4
- package/ui/dist/_app/immutable/chunks/{CZaFFKON.js → BDckNUhP.js} +1 -1
- package/ui/dist/_app/immutable/chunks/{B_xKDxop.js → BEB0f03Q.js} +1 -1
- package/ui/dist/_app/immutable/chunks/{CpefnqEp.js → DbLTrkva.js} +1 -1
- package/ui/dist/_app/immutable/chunks/{Ct2RV88Z.js → xZbjwf2l.js} +2 -2
- package/ui/dist/_app/immutable/entry/{app.CqwgUhww.js → app.DobJ2Iee.js} +2 -2
- package/ui/dist/_app/immutable/entry/start.5KZbLLPV.js +1 -0
- package/ui/dist/_app/immutable/nodes/{0.D5Wva3zJ.js → 0.BcmlOx1v.js} +1 -1
- package/ui/dist/_app/immutable/nodes/{1.lNr__ktV.js → 1.BzMQT6JK.js} +1 -1
- package/ui/dist/_app/immutable/nodes/{2.BRk85phb.js → 2.CbX5khSR.js} +1 -1
- package/ui/dist/_app/immutable/nodes/{3.tmoa7-_k.js → 3.BKJAedfV.js} +1 -1
- package/ui/dist/_app/immutable/nodes/{4.C6CClEtZ.js → 4.C-NuO1rS.js} +1 -1
- package/ui/dist/_app/immutable/nodes/{5.CX8sVMjt.js → 5.C2tdHzXF.js} +1 -1
- package/ui/dist/_app/immutable/nodes/{6.D-kQrGiV.js → 6.BWMOa3IU.js} +1 -1
- package/ui/dist/_app/version.json +1 -1
- package/ui/dist/index.html +10 -10
- package/ui/dist/_app/immutable/entry/start.B46VRLuJ.js +0 -1
|
@@ -46,7 +46,7 @@ export class Transition {
|
|
|
46
46
|
if (invalid(input.password, this.passwordRx))
|
|
47
47
|
return ERR_INVALID_PASSWORD;
|
|
48
48
|
const spicy = input.password + this.pepper;
|
|
49
|
-
object.password = await hash(spicy, this.rounds);
|
|
49
|
+
object.password = await hash(spicy, { cost: this.rounds });
|
|
50
50
|
}
|
|
51
51
|
return { id: object.id };
|
|
52
52
|
}
|
|
@@ -61,4 +61,4 @@ const ERR_PRINCIPAL_LOCKED = new Error('PRINCIPAL_LOCKED');
|
|
|
61
61
|
const ERR_INVALID_USERNAME = new Error('INVALID_USERNAME');
|
|
62
62
|
const ERR_INVALID_PASSWORD = new Error('INVALID_PASSWORD');
|
|
63
63
|
const ERR_EXISTS = new Error('EXISTS');
|
|
64
|
-
//# sourceMappingURL=data:application/json;base64,
|
|
64
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidHJhbnNpdC5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uL3NvdXJjZS90cmFuc2l0LnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLE9BQU8sRUFBRSxJQUFJLEVBQUUsTUFBTSxpQkFBaUIsQ0FBQTtBQVd0QyxNQUFNLE9BQU8sVUFBVTtJQUNiLE1BQU0sR0FBVyxFQUFFLENBQUE7SUFDbkIsTUFBTSxHQUFXLEVBQUUsQ0FBQTtJQUNuQixTQUFTLENBQVk7SUFDckIsTUFBTSxHQUFXLFNBQThCLENBQUE7SUFDL0MsSUFBSSxHQUFTLFNBQTRCLENBQUE7SUFDekMsVUFBVSxHQUFhLEVBQUUsQ0FBQTtJQUN6QixVQUFVLEdBQWEsRUFBRSxDQUFBO0lBRTFCLEtBQUssQ0FBQyxPQUFnQjtRQUMzQixJQUFJLENBQUMsTUFBTSxHQUFHLE9BQU8sQ0FBQyxhQUFhLENBQUMsTUFBTSxDQUFBO1FBQzFDLElBQUksQ0FBQyxNQUFNLEdBQUcsT0FBTyxDQUFDLGFBQWEsQ0FBQyxNQUFNLEVBQUUsTUFBTSxFQUFFLElBQUksRUFBRSxDQUFBO1FBQzFELElBQUksQ0FBQyxTQUFTLEdBQUcsT0FBTyxDQUFDLGFBQWEsQ0FBQyxTQUFTLENBQUE7UUFDaEQsSUFBSSxDQUFDLE1BQU0sR0FBRyxPQUFPLENBQUMsTUFBTSxDQUFDLFFBQVEsQ0FBQyxNQUFNLENBQUE7UUFDNUMsSUFBSSxDQUFDLElBQUksR0FBRyxPQUFPLENBQUMsTUFBTSxDQUFDLFFBQVEsQ0FBQyxJQUFJLENBQUE7UUFFeEMsSUFBSSxDQUFDLFVBQVUsR0FBRyxJQUFJLENBQUMsT0FBTyxDQUFDLGFBQWEsQ0FBQyxRQUFRLENBQUMsQ0FBQTtRQUN0RCxJQUFJLENBQUMsVUFBVSxHQUFHLElBQUksQ0FBQyxPQUFPLENBQUMsYUFBYSxDQUFDLFFBQVEsQ0FBQyxDQUFBO0lBQ3hELENBQUM7SUFFTyxNQUFNLENBQUMsTUFBYztRQUMzQixPQUFPLENBQ0wsSUFBSSxDQUFDLFNBQVMsS0FBSyxT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
|
|
@@ -38,6 +38,30 @@ operations:
|
|
|
38
38
|
- authority
|
|
39
39
|
- username
|
|
40
40
|
type: object
|
|
41
|
+
confirm:
|
|
42
|
+
input:
|
|
43
|
+
properties:
|
|
44
|
+
authority:
|
|
45
|
+
type: string
|
|
46
|
+
username:
|
|
47
|
+
type: string
|
|
48
|
+
minLength: 1
|
|
49
|
+
maxLength: 256
|
|
50
|
+
code:
|
|
51
|
+
type: string
|
|
52
|
+
identity:
|
|
53
|
+
type: string
|
|
54
|
+
pattern: ^[0-9a-f]{32}$
|
|
55
|
+
required:
|
|
56
|
+
- authority
|
|
57
|
+
- username
|
|
58
|
+
- code
|
|
59
|
+
- identity
|
|
60
|
+
type: object
|
|
61
|
+
errors:
|
|
62
|
+
- EXPIRED
|
|
63
|
+
- TOO_MANY_ATTEMPTS
|
|
64
|
+
- EXISTS
|
|
41
65
|
authenticate:
|
|
42
66
|
input:
|
|
43
67
|
properties:
|
|
@@ -86,11 +110,16 @@ stash: ~
|
|
|
86
110
|
exposition:
|
|
87
111
|
/:identity:
|
|
88
112
|
auth:id: identity
|
|
89
|
-
auth:role: system:identity:otp
|
|
90
113
|
POST:
|
|
114
|
+
auth:role: system:identity:otp
|
|
91
115
|
io:input: []
|
|
92
116
|
io:output: [code]
|
|
93
117
|
map:authority: authority
|
|
94
118
|
map:segments:
|
|
95
119
|
username: identity
|
|
96
120
|
endpoint: issue
|
|
121
|
+
PUT:
|
|
122
|
+
io:input: [username, code]
|
|
123
|
+
io:output: false
|
|
124
|
+
map:authority: authority
|
|
125
|
+
endpoint: confirm
|
|
@@ -1,23 +1,21 @@
|
|
|
1
|
+
import { redeem, SIGN_IN } from './lib/index.js';
|
|
1
2
|
export async function effect(input, context) {
|
|
2
3
|
const { authority, credentials } = input;
|
|
3
4
|
const [username, code] = Buffer.from(credentials, 'base64').toString().split(':');
|
|
4
5
|
if (code === undefined)
|
|
5
6
|
return ERR_INVALID_CREDENTIALS;
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
if (
|
|
17
|
-
|
|
18
|
-
return ERR_EXPIRED;
|
|
19
|
-
}
|
|
20
|
-
await context.stash.del(attempts);
|
|
7
|
+
// a code issued with an identity signs in only where the username is that identity's own id:
|
|
8
|
+
// any other is for `confirm`, and would tie the username to an identity of someone else's choosing
|
|
9
|
+
const issued = await redeem({
|
|
10
|
+
authority,
|
|
11
|
+
username,
|
|
12
|
+
code,
|
|
13
|
+
accepts: (value) => value === SIGN_IN || value === username
|
|
14
|
+
}, context);
|
|
15
|
+
if (issued instanceof Error)
|
|
16
|
+
return issued;
|
|
17
|
+
if (issued === username)
|
|
18
|
+
return { identity: { id: issued } };
|
|
21
19
|
const entry = await context.local.ensure({
|
|
22
20
|
entity: {
|
|
23
21
|
authority,
|
|
@@ -30,7 +28,5 @@ export async function effect(input, context) {
|
|
|
30
28
|
return { identity: { id } };
|
|
31
29
|
}
|
|
32
30
|
const ERR_INVALID_CREDENTIALS = new Error('INVALID_CREDENTIALS');
|
|
33
|
-
const ERR_EXPIRED = new Error('EXPIRED');
|
|
34
|
-
const ERR_TOO_MANY_ATTEMPTS = new Error('TOO_MANY_ATTEMPTS');
|
|
35
31
|
const ERR_NOT_FOUND = new Error('NOT_FOUND');
|
|
36
|
-
//# sourceMappingURL=data:application/json;base64,
|
|
32
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiYXV0aGVudGljYXRlLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vc291cmNlL2F1dGhlbnRpY2F0ZS50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEVBQUUsTUFBTSxFQUFFLE9BQU8sRUFBRSxNQUFNLGdCQUFnQixDQUFBO0FBR2hELE1BQU0sQ0FBQyxLQUFLLFVBQVUsTUFBTSxDQUFDLEtBQVksRUFBRSxPQUFnQjtJQUN6RCxNQUFNLEVBQUUsU0FBUyxFQUFFLFdBQVcsRUFBRSxHQUFHLEtBQUssQ0FBQTtJQUN4QyxNQUFNLENBQUMsUUFBUSxFQUFFLElBQUksQ0FBQyxHQUFHLE1BQU0sQ0FBQyxJQUFJLENBQUMsV0FBVyxFQUFFLFFBQVEsQ0FBQyxDQUFDLFFBQVEsRUFBRSxDQUFDLEtBQUssQ0FBQyxHQUFHLENBQUMsQ0FBQTtJQUVqRixJQUFJLElBQUksS0FBSyxTQUFTO1FBQUUsT0FBTyx1QkFBdUIsQ0FBQTtJQUV0RCw2RkFBNkY7SUFDN0YsbUdBQW1HO0lBQ25HLE1BQU0sTUFBTSxHQUFHLE1BQU0sTUFBTSxDQUN6QjtRQUNFLFNBQVM7UUFDVCxRQUFRO1FBQ1IsSUFBSTtRQUNKLE9BQU8sRUFBRSxDQUFDLEtBQUssRUFBRSxFQUFFLENBQUMsS0FBSyxLQUFLLE9BQU8sSUFBSSxLQUFLLEtBQUssUUFBUTtLQUM1RCxFQUNELE9BQU8sQ0FDUixDQUFBO0lBRUQsSUFBSSxNQUFNLFlBQVksS0FBSztRQUFFLE9BQU8sTUFBTSxDQUFBO0lBRTFDLElBQUksTUFBTSxLQUFLLFFBQVE7UUFBRSxPQUFPLEVBQUUsUUFBUSxFQUFFLEVBQUUsRUFBRSxFQUFFLE1BQU0sRUFBRSxFQUFFLENBQUE7SUFFNUQsTUFBTSxLQUFLLEdBQUcsTUFBTSxPQUFPLENBQUMsS0FBSyxDQUFDLE1BQU0sQ0FBQztRQUN2QyxNQUFNLEVBQUU7WUFDTixTQUFTO1lBQ1QsUUFBUTtTQUNUO0tBQ0YsQ0FBQyxDQUFBO0lBRUYsSUFBSSxLQUFLLEtBQUssSUFBSTtRQUFFLE9BQU8sYUFBYSxDQUFBO0lBRXhDLE1BQU0sRUFBRSxHQUFHLEtBQUssQ0FBQyxRQUFRLElBQUksS0FBSyxDQUFDLEVBQUUsQ0FBQSxDQUFDLHFCQUFxQjtJQUUzRCxPQUFPLEVBQUUsUUFBUSxFQUFFLEVBQUUsRUFBRSxFQUFFLEVBQUUsQ0FBQTtBQUM3QixDQUFDO0FBRUQsTUFBTSx1QkFBdUIsR0FBRyxJQUFJLEtBQUssQ0FBQyxxQkFBcUIsQ0FBQyxDQUFBO0FBRWhFLE1BQU0sYUFBYSxHQUFHLElBQUksS0FBSyxDQUFDLFdBQVcsQ0FBQyxDQUFBIn0=
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { quote } from '@toa.io/generic';
|
|
2
|
+
import { redeem } from './lib/index.js';
|
|
3
|
+
export async function effect(input, context) {
|
|
4
|
+
const { authority, username, code, identity } = input;
|
|
5
|
+
const issued = await redeem({ authority, username, code, accepts: (value) => value === identity }, context);
|
|
6
|
+
if (issued instanceof Error)
|
|
7
|
+
return issued;
|
|
8
|
+
const entry = await context.local.ensure({
|
|
9
|
+
query: { criteria: `authority==${quote(authority)};username==${quote(username)}` },
|
|
10
|
+
entity: { authority, username, identity }
|
|
11
|
+
});
|
|
12
|
+
const bound = entry.identity ?? entry.id; // identity inception
|
|
13
|
+
if (bound !== identity) {
|
|
14
|
+
context.logs.debug('OTP username is tied to another identity', {
|
|
15
|
+
authority,
|
|
16
|
+
username
|
|
17
|
+
});
|
|
18
|
+
return ERR_EXISTS;
|
|
19
|
+
}
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
const ERR_EXISTS = new Error('EXISTS');
|
|
23
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY29uZmlybS5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uL3NvdXJjZS9jb25maXJtLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLE9BQU8sRUFBRSxLQUFLLEVBQUUsTUFBTSxpQkFBaUIsQ0FBQTtBQUN2QyxPQUFPLEVBQUUsTUFBTSxFQUFFLE1BQU0sZ0JBQWdCLENBQUE7QUFHdkMsTUFBTSxDQUFDLEtBQUssVUFBVSxNQUFNLENBQUMsS0FBWSxFQUFFLE9BQWdCO0lBQ3pELE1BQU0sRUFBRSxTQUFTLEVBQUUsUUFBUSxFQUFFLElBQUksRUFBRSxRQUFRLEVBQUUsR0FBRyxLQUFLLENBQUE7SUFFckQsTUFBTSxNQUFNLEdBQUcsTUFBTSxNQUFNLENBQ3pCLEVBQUUsU0FBUyxFQUFFLFFBQVEsRUFBRSxJQUFJLEVBQUUsT0FBTyxFQUFFLENBQUMsS0FBSyxFQUFFLEVBQUUsQ0FBQyxLQUFLLEtBQUssUUFBUSxFQUFFLEVBQ3JFLE9BQU8sQ0FDUixDQUFBO0lBRUQsSUFBSSxNQUFNLFlBQVksS0FBSztRQUFFLE9BQU8sTUFBTSxDQUFBO0lBRTFDLE1BQU0sS0FBSyxHQUFHLE1BQU0sT0FBTyxDQUFDLEtBQUssQ0FBQyxNQUFNLENBQUM7UUFDdkMsS0FBSyxFQUFFLEVBQUUsUUFBUSxFQUFFLGNBQWMsS0FBSyxDQUFDLFNBQVMsQ0FBQyxjQUFjLEtBQUssQ0FBQyxRQUFRLENBQUMsRUFBRSxFQUFFO1FBQ2xGLE1BQU0sRUFBRSxFQUFFLFNBQVMsRUFBRSxRQUFRLEVBQUUsUUFBUSxFQUFFO0tBQzFDLENBQUMsQ0FBQTtJQUVGLE1BQU0sS0FBSyxHQUFHLEtBQUssQ0FBQyxRQUFRLElBQUksS0FBSyxDQUFDLEVBQUUsQ0FBQSxDQUFDLHFCQUFxQjtJQUU5RCxJQUFJLEtBQUssS0FBSyxRQUFRLEVBQUUsQ0FBQztRQUN2QixPQUFPLENBQUMsSUFBSSxDQUFDLEtBQUssQ0FBQywwQ0FBMEMsRUFBRTtZQUM3RCxTQUFTO1lBQ1QsUUFBUTtTQUNULENBQUMsQ0FBQTtRQUVGLE9BQU8sVUFBVSxDQUFBO0lBQ25CLENBQUM7SUFFRCxPQUFPLFNBQVMsQ0FBQTtBQUNsQixDQUFDO0FBRUQsTUFBTSxVQUFVLEdBQUcsSUFBSSxLQUFLLENBQUMsUUFBUSxDQUFDLENBQUEifQ==
|
|
@@ -1,19 +1,13 @@
|
|
|
1
1
|
import { randomInt } from 'node:crypto';
|
|
2
|
+
import { SIGN_IN } from './lib/index.js';
|
|
2
3
|
export async function effect(input, context) {
|
|
3
4
|
const { authority, username, identity } = input;
|
|
4
5
|
const code = randomInt(100000, 1000000).toString();
|
|
5
6
|
const key = `${authority}:${username}:${code}`;
|
|
6
7
|
const lifetime = input.lifetime ?? context.configuration.lifetime;
|
|
7
|
-
if (identity !== undefined)
|
|
8
|
-
await context.local.ensure({
|
|
9
|
-
entity: {
|
|
10
|
-
authority,
|
|
11
|
-
username,
|
|
12
|
-
identity
|
|
13
|
-
}
|
|
14
|
-
});
|
|
15
8
|
context.logs.debug('Issue OTP', { authority, username, identity, lifetime });
|
|
16
|
-
|
|
9
|
+
// the username is tied to the identity once `confirm` is given the code, not before
|
|
10
|
+
await context.stash.set(key, identity ?? SIGN_IN, 'EX', lifetime);
|
|
17
11
|
return { code };
|
|
18
12
|
}
|
|
19
|
-
//# sourceMappingURL=data:application/json;base64,
|
|
13
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaXNzdWUuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi9zb3VyY2UvaXNzdWUudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsT0FBTyxFQUFFLFNBQVMsRUFBRSxNQUFNLGFBQWEsQ0FBQTtBQUN2QyxPQUFPLEVBQUUsT0FBTyxFQUFFLE1BQU0sZ0JBQWdCLENBQUE7QUFHeEMsTUFBTSxDQUFDLEtBQUssVUFBVSxNQUFNLENBQUMsS0FBWSxFQUFFLE9BQWdCO0lBQ3pELE1BQU0sRUFBRSxTQUFTLEVBQUUsUUFBUSxFQUFFLFFBQVEsRUFBRSxHQUFHLEtBQUssQ0FBQTtJQUMvQyxNQUFNLElBQUksR0FBRyxTQUFTLENBQUMsTUFBTSxFQUFFLE9BQU8sQ0FBQyxDQUFDLFFBQVEsRUFBRSxDQUFBO0lBQ2xELE1BQU0sR0FBRyxHQUFHLEdBQUcsU0FBUyxJQUFJLFFBQVEsSUFBSSxJQUFJLEVBQUUsQ0FBQTtJQUM5QyxNQUFNLFFBQVEsR0FBRyxLQUFLLENBQUMsUUFBUSxJQUFJLE9BQU8sQ0FBQyxhQUFhLENBQUMsUUFBUSxDQUFBO0lBRWpFLE9BQU8sQ0FBQyxJQUFJLENBQUMsS0FBSyxDQUFDLFdBQVcsRUFBRSxFQUFFLFNBQVMsRUFBRSxRQUFRLEVBQUUsUUFBUSxFQUFFLFFBQVEsRUFBRSxDQUFDLENBQUE7SUFFNUUsb0ZBQW9GO0lBQ3BGLE1BQU0sT0FBTyxDQUFDLEtBQUssQ0FBQyxHQUFHLENBQUMsR0FBRyxFQUFFLFFBQVEsSUFBSSxPQUFPLEVBQUUsSUFBSSxFQUFFLFFBQVEsQ0FBQyxDQUFBO0lBRWpFLE9BQU8sRUFBRSxJQUFJLEVBQUUsQ0FBQTtBQUNqQixDQUFDIn0=
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export {};
|
|
2
|
-
//# sourceMappingURL=data:application/json;base64,
|
|
1
|
+
export { redeem, SIGN_IN } from './redeem.js';
|
|
2
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi9zb3VyY2UvbGliL2luZGV4LnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUNBLE9BQU8sRUFBRSxNQUFNLEVBQUUsT0FBTyxFQUFFLE1BQU0sYUFBYSxDQUFBIn0=
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Spends a code that `accepts` takes and answers what it was issued with: `SIGN_IN`, or the
|
|
3
|
+
* identity the username is to be tied to. A code it does not take stays for the operation that
|
|
4
|
+
* does, and counts as a wrong one.
|
|
5
|
+
*/
|
|
6
|
+
export async function redeem({ authority, username, code, accepts }, context) {
|
|
7
|
+
const attempts = `${authority}:${username}:attempts`;
|
|
8
|
+
const attempt = await context.stash.incr(attempts);
|
|
9
|
+
if (attempt === 1)
|
|
10
|
+
await context.stash.expire(attempts, context.configuration.lifetime);
|
|
11
|
+
if (attempt > context.configuration.attempts) {
|
|
12
|
+
context.logs.debug('OTP attempts exceeded', { authority, username, attempt });
|
|
13
|
+
return ERR_TOO_MANY_ATTEMPTS;
|
|
14
|
+
}
|
|
15
|
+
const key = `${authority}:${username}:${code}`;
|
|
16
|
+
const issued = await context.stash.get(key);
|
|
17
|
+
// of two requests with one code, the one that deletes it is the one it is spent on
|
|
18
|
+
if (issued === null || !accepts(issued) || (await context.stash.del(key)) === 0) {
|
|
19
|
+
context.logs.debug('OTP code not found', { authority, username, attempt });
|
|
20
|
+
return ERR_EXPIRED;
|
|
21
|
+
}
|
|
22
|
+
await context.stash.del(attempts);
|
|
23
|
+
return issued;
|
|
24
|
+
}
|
|
25
|
+
export const SIGN_IN = '1';
|
|
26
|
+
const ERR_EXPIRED = new Error('EXPIRED');
|
|
27
|
+
const ERR_TOO_MANY_ATTEMPTS = new Error('TOO_MANY_ATTEMPTS');
|
|
28
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicmVkZWVtLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vc291cmNlL2xpYi9yZWRlZW0udHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBRUE7Ozs7R0FJRztBQUNILE1BQU0sQ0FBQyxLQUFLLFVBQVUsTUFBTSxDQUMxQixFQUFFLFNBQVMsRUFBRSxRQUFRLEVBQUUsSUFBSSxFQUFFLE9BQU8sRUFBYyxFQUNsRCxPQUFnQjtJQUVoQixNQUFNLFFBQVEsR0FBRyxHQUFHLFNBQVMsSUFBSSxRQUFRLFdBQVcsQ0FBQTtJQUNwRCxNQUFNLE9BQU8sR0FBRyxNQUFNLE9BQU8sQ0FBQyxLQUFLLENBQUMsSUFBSSxDQUFDLFFBQVEsQ0FBQyxDQUFBO0lBRWxELElBQUksT0FBTyxLQUFLLENBQUM7UUFBRSxNQUFNLE9BQU8sQ0FBQyxLQUFLLENBQUMsTUFBTSxDQUFDLFFBQVEsRUFBRSxPQUFPLENBQUMsYUFBYSxDQUFDLFFBQVEsQ0FBQyxDQUFBO0lBRXZGLElBQUksT0FBTyxHQUFHLE9BQU8sQ0FBQyxhQUFhLENBQUMsUUFBUSxFQUFFLENBQUM7UUFDN0MsT0FBTyxDQUFDLElBQUksQ0FBQyxLQUFLLENBQUMsdUJBQXVCLEVBQUUsRUFBRSxTQUFTLEVBQUUsUUFBUSxFQUFFLE9BQU8sRUFBRSxDQUFDLENBQUE7UUFFN0UsT0FBTyxxQkFBcUIsQ0FBQTtJQUM5QixDQUFDO0lBRUQsTUFBTSxHQUFHLEdBQUcsR0FBRyxTQUFTLElBQUksUUFBUSxJQUFJLElBQUksRUFBRSxDQUFBO0lBQzlDLE1BQU0sTUFBTSxHQUFHLE1BQU0sT0FBTyxDQUFDLEtBQUssQ0FBQyxHQUFHLENBQUMsR0FBRyxDQUFDLENBQUE7SUFFM0MsbUZBQW1GO0lBQ25GLElBQUksTUFBTSxLQUFLLElBQUksSUFBSSxDQUFDLE9BQU8sQ0FBQyxNQUFNLENBQUMsSUFBSSxDQUFDLE1BQU0sT0FBTyxDQUFDLEtBQUssQ0FBQyxHQUFHLENBQUMsR0FBRyxDQUFDLENBQUMsS0FBSyxDQUFDLEVBQUUsQ0FBQztRQUNoRixPQUFPLENBQUMsSUFBSSxDQUFDLEtBQUssQ0FBQyxvQkFBb0IsRUFBRSxFQUFFLFNBQVMsRUFBRSxRQUFRLEVBQUUsT0FBTyxFQUFFLENBQUMsQ0FBQTtRQUUxRSxPQUFPLFdBQVcsQ0FBQTtJQUNwQixDQUFDO0lBRUQsTUFBTSxPQUFPLENBQUMsS0FBSyxDQUFDLEdBQUcsQ0FBQyxRQUFRLENBQUMsQ0FBQTtJQUVqQyxPQUFPLE1BQU0sQ0FBQTtBQUNmLENBQUM7QUFFRCxNQUFNLENBQUMsTUFBTSxPQUFPLEdBQUcsR0FBRyxDQUFBO0FBRTFCLE1BQU0sV0FBVyxHQUFHLElBQUksS0FBSyxDQUFDLFNBQVMsQ0FBQyxDQUFBO0FBRXhDLE1BQU0scUJBQXFCLEdBQUcsSUFBSSxLQUFLLENBQUMsbUJBQW1CLENBQUMsQ0FBQSJ9
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { redeem, SIGN_IN } from './lib/index.ts'
|
|
1
2
|
import type { Context } from './lib/index.ts'
|
|
2
3
|
|
|
3
4
|
export async function effect(input: Input, context: Context): Promise<Output | Error> {
|
|
@@ -6,27 +7,21 @@ export async function effect(input: Input, context: Context): Promise<Output | E
|
|
|
6
7
|
|
|
7
8
|
if (code === undefined) return ERR_INVALID_CREDENTIALS
|
|
8
9
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
const key = `${authority}:${username}:${code}`
|
|
21
|
-
const n = await context.stash.del(key)
|
|
22
|
-
|
|
23
|
-
if (n === 0) {
|
|
24
|
-
context.logs.debug('OTP code not found', { authority, username, attempt })
|
|
10
|
+
// a code issued with an identity signs in only where the username is that identity's own id:
|
|
11
|
+
// any other is for `confirm`, and would tie the username to an identity of someone else's choosing
|
|
12
|
+
const issued = await redeem(
|
|
13
|
+
{
|
|
14
|
+
authority,
|
|
15
|
+
username,
|
|
16
|
+
code,
|
|
17
|
+
accepts: (value) => value === SIGN_IN || value === username
|
|
18
|
+
},
|
|
19
|
+
context
|
|
20
|
+
)
|
|
25
21
|
|
|
26
|
-
|
|
27
|
-
}
|
|
22
|
+
if (issued instanceof Error) return issued
|
|
28
23
|
|
|
29
|
-
|
|
24
|
+
if (issued === username) return { identity: { id: issued } }
|
|
30
25
|
|
|
31
26
|
const entry = await context.local.ensure({
|
|
32
27
|
entity: {
|
|
@@ -44,10 +39,6 @@ export async function effect(input: Input, context: Context): Promise<Output | E
|
|
|
44
39
|
|
|
45
40
|
const ERR_INVALID_CREDENTIALS = new Error('INVALID_CREDENTIALS')
|
|
46
41
|
|
|
47
|
-
const ERR_EXPIRED = new Error('EXPIRED')
|
|
48
|
-
|
|
49
|
-
const ERR_TOO_MANY_ATTEMPTS = new Error('TOO_MANY_ATTEMPTS')
|
|
50
|
-
|
|
51
42
|
const ERR_NOT_FOUND = new Error('NOT_FOUND')
|
|
52
43
|
|
|
53
44
|
interface Input {
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { quote } from '@toa.io/generic'
|
|
2
|
+
import { redeem } from './lib/index.ts'
|
|
3
|
+
import type { Context } from './lib/index.ts'
|
|
4
|
+
|
|
5
|
+
export async function effect(input: Input, context: Context): Promise<void | Error> {
|
|
6
|
+
const { authority, username, code, identity } = input
|
|
7
|
+
|
|
8
|
+
const issued = await redeem(
|
|
9
|
+
{ authority, username, code, accepts: (value) => value === identity },
|
|
10
|
+
context
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
if (issued instanceof Error) return issued
|
|
14
|
+
|
|
15
|
+
const entry = await context.local.ensure({
|
|
16
|
+
query: { criteria: `authority==${quote(authority)};username==${quote(username)}` },
|
|
17
|
+
entity: { authority, username, identity }
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
const bound = entry.identity ?? entry.id // identity inception
|
|
21
|
+
|
|
22
|
+
if (bound !== identity) {
|
|
23
|
+
context.logs.debug('OTP username is tied to another identity', {
|
|
24
|
+
authority,
|
|
25
|
+
username
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
return ERR_EXISTS
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return undefined
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const ERR_EXISTS = new Error('EXISTS')
|
|
35
|
+
|
|
36
|
+
interface Input {
|
|
37
|
+
authority: string
|
|
38
|
+
username: string
|
|
39
|
+
code: string
|
|
40
|
+
identity: string
|
|
41
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { randomInt } from 'node:crypto'
|
|
2
|
+
import { SIGN_IN } from './lib/index.ts'
|
|
2
3
|
import type { Context } from './lib/index.ts'
|
|
3
4
|
|
|
4
5
|
export async function effect(input: Input, context: Context): Promise<Output> {
|
|
@@ -7,18 +8,10 @@ export async function effect(input: Input, context: Context): Promise<Output> {
|
|
|
7
8
|
const key = `${authority}:${username}:${code}`
|
|
8
9
|
const lifetime = input.lifetime ?? context.configuration.lifetime
|
|
9
10
|
|
|
10
|
-
if (identity !== undefined)
|
|
11
|
-
await context.local.ensure({
|
|
12
|
-
entity: {
|
|
13
|
-
authority,
|
|
14
|
-
username,
|
|
15
|
-
identity
|
|
16
|
-
}
|
|
17
|
-
})
|
|
18
|
-
|
|
19
11
|
context.logs.debug('Issue OTP', { authority, username, identity, lifetime })
|
|
20
12
|
|
|
21
|
-
|
|
13
|
+
// the username is tied to the identity once `confirm` is given the code, not before
|
|
14
|
+
await context.stash.set(key, identity ?? SIGN_IN, 'EX', lifetime)
|
|
22
15
|
|
|
23
16
|
return { code }
|
|
24
17
|
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { Context } from './Context.ts'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Spends a code that `accepts` takes and answers what it was issued with: `SIGN_IN`, or the
|
|
5
|
+
* identity the username is to be tied to. A code it does not take stays for the operation that
|
|
6
|
+
* does, and counts as a wrong one.
|
|
7
|
+
*/
|
|
8
|
+
export async function redeem(
|
|
9
|
+
{ authority, username, code, accepts }: Redemption,
|
|
10
|
+
context: Context
|
|
11
|
+
): Promise<string | Error> {
|
|
12
|
+
const attempts = `${authority}:${username}:attempts`
|
|
13
|
+
const attempt = await context.stash.incr(attempts)
|
|
14
|
+
|
|
15
|
+
if (attempt === 1) await context.stash.expire(attempts, context.configuration.lifetime)
|
|
16
|
+
|
|
17
|
+
if (attempt > context.configuration.attempts) {
|
|
18
|
+
context.logs.debug('OTP attempts exceeded', { authority, username, attempt })
|
|
19
|
+
|
|
20
|
+
return ERR_TOO_MANY_ATTEMPTS
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const key = `${authority}:${username}:${code}`
|
|
24
|
+
const issued = await context.stash.get(key)
|
|
25
|
+
|
|
26
|
+
// of two requests with one code, the one that deletes it is the one it is spent on
|
|
27
|
+
if (issued === null || !accepts(issued) || (await context.stash.del(key)) === 0) {
|
|
28
|
+
context.logs.debug('OTP code not found', { authority, username, attempt })
|
|
29
|
+
|
|
30
|
+
return ERR_EXPIRED
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
await context.stash.del(attempts)
|
|
34
|
+
|
|
35
|
+
return issued
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export const SIGN_IN = '1'
|
|
39
|
+
|
|
40
|
+
const ERR_EXPIRED = new Error('EXPIRED')
|
|
41
|
+
|
|
42
|
+
const ERR_TOO_MANY_ATTEMPTS = new Error('TOO_MANY_ATTEMPTS')
|
|
43
|
+
|
|
44
|
+
interface Redemption {
|
|
45
|
+
authority: string
|
|
46
|
+
username: string
|
|
47
|
+
code: string
|
|
48
|
+
accepts: (issued: string) => boolean
|
|
49
|
+
}
|
|
@@ -29,6 +29,13 @@ export type IssueInput = {
|
|
|
29
29
|
lifetime?: number
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
export type ConfirmInput = {
|
|
33
|
+
authority: string
|
|
34
|
+
username: string
|
|
35
|
+
code: string
|
|
36
|
+
identity: string
|
|
37
|
+
}
|
|
38
|
+
|
|
32
39
|
export type AuthenticateInput = {
|
|
33
40
|
authority: string
|
|
34
41
|
credentials: string
|
|
@@ -42,6 +49,7 @@ export type AuthenticateOutput = {
|
|
|
42
49
|
|
|
43
50
|
export interface Component {
|
|
44
51
|
issue: (request: { input: IssueInput, entity?: Partial<Entity>, task?: boolean }) => Promise<unknown>
|
|
52
|
+
confirm: (request: { input: ConfirmInput, entity?: Partial<Entity>, task?: boolean }) => Promise<{} | null | undefined | CodedError<"EXPIRED" | "TOO_MANY_ATTEMPTS" | "EXISTS">>
|
|
45
53
|
authenticate: (request: { input: AuthenticateInput, entity?: Partial<Entity>, task?: boolean }) => Promise<AuthenticateOutput | CodedError<"INVALID_CREDENTIALS" | "EXPIRED" | "TOO_MANY_ATTEMPTS" | "NOT_FOUND">>
|
|
46
54
|
assign: (request: { input?: null, query?: Query<Entity>, task?: boolean }) => Promise<Entity>
|
|
47
55
|
ensure: (request: { input?: null, query?: Query<Entity>, entity?: Partial<Entity>, task?: boolean }) => Promise<Entity>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@toa.io/extensions.exposition",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.331",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Toa Exposition",
|
|
6
6
|
"author": "temich <tema.gurtovoy@gmail.com>",
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
],
|
|
30
30
|
"dependencies": {
|
|
31
31
|
"@toa.io/core": "1.0.0-alpha.330",
|
|
32
|
-
"@toa.io/definitions": "1.0.0-alpha.
|
|
32
|
+
"@toa.io/definitions": "1.0.0-alpha.331",
|
|
33
33
|
"@toa.io/generic": "1.0.0-alpha.330",
|
|
34
34
|
"@toa.io/pointer": "1.0.0-alpha.330",
|
|
35
35
|
"@toa.io/schemas": "1.0.0-alpha.330",
|
|
@@ -63,7 +63,7 @@
|
|
|
63
63
|
"features:octets": "TSX_TSCONFIG_PATH=features/steps/tsconfig.json NODE_OPTIONS=--import=tsx cucumber-js features/octets.*"
|
|
64
64
|
},
|
|
65
65
|
"devDependencies": {
|
|
66
|
-
"@node-rs/bcrypt": "
|
|
66
|
+
"@node-rs/bcrypt": "2.0.0",
|
|
67
67
|
"@simplewebauthn/server": "14.0.3",
|
|
68
68
|
"@toa.io/agent": "*",
|
|
69
69
|
"@toa.io/extensions.storages": "*",
|
|
@@ -74,5 +74,5 @@
|
|
|
74
74
|
"lru-cache": "11.5.3",
|
|
75
75
|
"paseto": "4.0.1"
|
|
76
76
|
},
|
|
77
|
-
"gitHead": "
|
|
77
|
+
"gitHead": "e7020d160910ca4f0c2ff9040b3cbb4fca8ea41f"
|
|
78
78
|
}
|