@toa.io/extensions.exposition 1.0.0-alpha.130 → 1.0.0-alpha.132
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.passkeys/manifest.toa.yaml +7 -2
- package/components/identity.passkeys/operations/challenge.d.ts +7 -3
- package/components/identity.passkeys/operations/challenge.js +25 -16
- package/components/identity.passkeys/operations/challenge.js.map +1 -1
- package/components/identity.passkeys/operations/create.d.ts +2 -0
- package/components/identity.passkeys/operations/create.js +7 -1
- package/components/identity.passkeys/operations/create.js.map +1 -1
- package/components/identity.passkeys/operations/tsconfig.tsbuildinfo +1 -1
- package/components/identity.passkeys/operations/use.d.ts +2 -0
- package/components/identity.passkeys/operations/use.js +6 -1
- package/components/identity.passkeys/operations/use.js.map +1 -1
- package/components/identity.passkeys/source/challenge.ts +35 -21
- package/components/identity.passkeys/source/create.ts +7 -1
- package/components/identity.passkeys/source/use.ts +6 -1
- package/features/passkeys.feature +34 -0
- package/package.json +2 -2
- package/source/directives/auth/Incept.ts +12 -1
- package/transpiled/directives/auth/Incept.js +10 -1
- package/transpiled/directives/auth/Incept.js.map +1 -1
- package/transpiled/tsconfig.tsbuildinfo +1 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { randomBytes } from 'node:crypto'
|
|
2
|
+
import { newid } from '@toa.io/generic'
|
|
2
3
|
import { MAX_KEYS } from './lib/const'
|
|
3
4
|
import type { Operation } from '@toa.io/types'
|
|
4
5
|
import type { Context } from './types'
|
|
@@ -31,7 +32,7 @@ export class Effect implements Operation {
|
|
|
31
32
|
}
|
|
32
33
|
|
|
33
34
|
public async execute (input: Input): Promise<Output> {
|
|
34
|
-
const { identity, authority } = input
|
|
35
|
+
const { type, identity, authority } = input
|
|
35
36
|
const challenge = await this.createChallenge(authority)
|
|
36
37
|
|
|
37
38
|
const options: Output = {
|
|
@@ -39,23 +40,31 @@ export class Effect implements Operation {
|
|
|
39
40
|
timeout: this.timeout
|
|
40
41
|
}
|
|
41
42
|
|
|
42
|
-
if (
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
const keys =
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
43
|
+
if (type === 'creation')
|
|
44
|
+
options.identity = identity ?? newid()
|
|
45
|
+
|
|
46
|
+
const keys = identity === undefined
|
|
47
|
+
? undefined
|
|
48
|
+
: await this.enumerate({
|
|
49
|
+
query: {
|
|
50
|
+
criteria: `identity==${identity}`,
|
|
51
|
+
projection: ['kid', 'transports'],
|
|
52
|
+
limit: MAX_KEYS
|
|
53
|
+
}
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
if (type === 'creation')
|
|
57
|
+
return {
|
|
58
|
+
...options,
|
|
59
|
+
excludeCredentials: keys?.map(({ kid, transports }) => ({ id: kid, transports })),
|
|
60
|
+
pubKeyCredParams: this.credParams,
|
|
61
|
+
authenticatorSelection: this.authenticator
|
|
62
|
+
} satisfies CreationOptions
|
|
63
|
+
else
|
|
64
|
+
return {
|
|
65
|
+
...options,
|
|
66
|
+
allowCredentials: keys?.map(({ kid, transports }) => ({ id: kid, transports }))
|
|
67
|
+
} satisfies RequestOptions
|
|
59
68
|
}
|
|
60
69
|
|
|
61
70
|
private async createChallenge (authority: string): Promise<string> {
|
|
@@ -73,6 +82,7 @@ export class Effect implements Operation {
|
|
|
73
82
|
const EX_GAP = 1.5
|
|
74
83
|
|
|
75
84
|
interface Input {
|
|
85
|
+
type: 'creation' | 'request'
|
|
76
86
|
authority: string
|
|
77
87
|
identity?: string | null
|
|
78
88
|
}
|
|
@@ -80,6 +90,7 @@ interface Input {
|
|
|
80
90
|
type Output = CreationOptions | RequestOptions
|
|
81
91
|
|
|
82
92
|
interface CommonOptions {
|
|
93
|
+
identity?: string
|
|
83
94
|
challenge: string
|
|
84
95
|
timeout: number
|
|
85
96
|
}
|
|
@@ -91,7 +102,10 @@ interface KeyDescriptor {
|
|
|
91
102
|
|
|
92
103
|
type CreationOptions =
|
|
93
104
|
CommonOptions
|
|
94
|
-
& Omit<PublicKeyCredentialCreationOptions, 'rp' | 'user' | 'excludeCredentials'>
|
|
95
|
-
& { excludeCredentials
|
|
105
|
+
& Omit<PublicKeyCredentialCreationOptions, 'challenge' | 'rp' | 'user' | 'excludeCredentials'>
|
|
106
|
+
& { excludeCredentials?: KeyDescriptor[] }
|
|
96
107
|
|
|
97
|
-
type RequestOptions =
|
|
108
|
+
type RequestOptions =
|
|
109
|
+
CommonOptions
|
|
110
|
+
& Omit<PublicKeyCredentialRequestOptions, 'challenge' | 'allowCredentials'>
|
|
111
|
+
& { allowCredentials?: KeyDescriptor[] }
|
|
@@ -9,9 +9,13 @@ export class Transition implements Operation {
|
|
|
9
9
|
private algorithms!: number[]
|
|
10
10
|
private stash!: Context['stash']
|
|
11
11
|
private logs!: Context['logs']
|
|
12
|
+
private verification!: boolean
|
|
13
|
+
private presence!: boolean
|
|
12
14
|
|
|
13
15
|
public mount (context: Context): void {
|
|
14
16
|
this.algorithms = context.configuration.algorithms
|
|
17
|
+
this.verification = context.configuration.verification === 'required'
|
|
18
|
+
this.presence = context.configuration.residence === 'required'
|
|
15
19
|
this.stash = context.stash
|
|
16
20
|
this.logs = context.logs
|
|
17
21
|
}
|
|
@@ -26,7 +30,9 @@ export class Transition implements Operation {
|
|
|
26
30
|
response,
|
|
27
31
|
expectedOrigin: input.origin,
|
|
28
32
|
expectedChallenge: async (challenge) => this.verifyChallenge(authority, challenge),
|
|
29
|
-
supportedAlgorithmIDs: this.algorithms
|
|
33
|
+
supportedAlgorithmIDs: this.algorithms,
|
|
34
|
+
requireUserVerification: this.verification,
|
|
35
|
+
requireUserPresence: this.presence
|
|
30
36
|
}).catch((e) => {
|
|
31
37
|
this.logs.info('Failed to verify registration response', { message: e.message })
|
|
32
38
|
|
|
@@ -7,10 +7,14 @@ import type { Context, Passkey } from './types'
|
|
|
7
7
|
export class Transition implements Operation {
|
|
8
8
|
private stash!: Context['stash']
|
|
9
9
|
private logs!: Context['logs']
|
|
10
|
+
private verification!: boolean
|
|
11
|
+
private presence!: boolean
|
|
10
12
|
|
|
11
13
|
public mount (context: Context): void {
|
|
12
14
|
this.stash = context.stash
|
|
13
15
|
this.logs = context.logs
|
|
16
|
+
this.verification = context.configuration.verification === 'required'
|
|
17
|
+
this.presence = context.configuration.residence === 'required'
|
|
14
18
|
}
|
|
15
19
|
|
|
16
20
|
public async execute (input: Input, object: Passkey): Promise<Output> {
|
|
@@ -23,7 +27,8 @@ export class Transition implements Operation {
|
|
|
23
27
|
credential,
|
|
24
28
|
expectedOrigin: origin,
|
|
25
29
|
expectedRPID: new URL(origin).hostname,
|
|
26
|
-
expectedChallenge: async (challenge) => this.verifyChallenge(object.authority, challenge)
|
|
30
|
+
expectedChallenge: async (challenge) => this.verifyChallenge(object.authority, challenge),
|
|
31
|
+
requireUserVerification: this.verification
|
|
27
32
|
}).catch((e) => {
|
|
28
33
|
this.logs.info('Failed to verify authentication response', { message: e.message })
|
|
29
34
|
|
|
@@ -12,12 +12,46 @@ Feature: Web Authentication
|
|
|
12
12
|
host: nex.toa.io
|
|
13
13
|
authorization: Token ${{ identity.token }}
|
|
14
14
|
accept: application/yaml
|
|
15
|
+
content-type: application/yaml
|
|
16
|
+
|
|
17
|
+
type: creation
|
|
18
|
+
"""
|
|
19
|
+
Then the following reply is sent:
|
|
20
|
+
"""
|
|
21
|
+
201 Created
|
|
22
|
+
|
|
23
|
+
challenge: ${{ challenge }}
|
|
24
|
+
identity: ${{ identity.id }}
|
|
25
|
+
timeout: 60000
|
|
26
|
+
pubKeyCredParams:
|
|
27
|
+
- type: public-key
|
|
28
|
+
alg: -7
|
|
29
|
+
- type: public-key
|
|
30
|
+
alg: -257
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
Scenario: Create a challenge to create a passkey to new identity
|
|
34
|
+
Given the `identity.passkeys` configuration:
|
|
35
|
+
"""yaml
|
|
36
|
+
name: Nex
|
|
37
|
+
"""
|
|
38
|
+
And transient identity
|
|
39
|
+
When the following request is received:
|
|
40
|
+
"""
|
|
41
|
+
POST /identity/passkeys/challenges/ HTTP/1.1
|
|
42
|
+
host: nex.toa.io
|
|
43
|
+
accept: application/yaml
|
|
44
|
+
content-type: application/yaml
|
|
45
|
+
|
|
46
|
+
type: creation
|
|
15
47
|
"""
|
|
16
48
|
Then the following reply is sent:
|
|
17
49
|
"""
|
|
18
50
|
201 Created
|
|
51
|
+
authorization: Token ${{ token }}
|
|
19
52
|
|
|
20
53
|
challenge: ${{ challenge }}
|
|
54
|
+
identity: ${{ identity }}
|
|
21
55
|
timeout: 60000
|
|
22
56
|
pubKeyCredParams:
|
|
23
57
|
- type: public-key
|
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.132",
|
|
4
4
|
"description": "Toa Exposition",
|
|
5
5
|
"author": "temich <tema.gurtovoy@gmail.com>",
|
|
6
6
|
"homepage": "https://github.com/toa-io/toa#readme",
|
|
@@ -63,5 +63,5 @@
|
|
|
63
63
|
"@types/negotiator": "0.6.1",
|
|
64
64
|
"jest-esbuild": "0.3.0"
|
|
65
65
|
},
|
|
66
|
-
"gitHead": "
|
|
66
|
+
"gitHead": "0159315b61cab2a7ab70e8473e4b88dc666c28d5"
|
|
67
67
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import assert from 'node:assert'
|
|
2
|
+
import { console } from 'openspan'
|
|
2
3
|
import * as http from '../../HTTP'
|
|
3
4
|
import { split } from './split'
|
|
4
5
|
import { create } from './create'
|
|
@@ -35,12 +36,21 @@ export class Incept implements Directive {
|
|
|
35
36
|
public async settle (context: Context, response: http.OutgoingMessage): Promise<void> {
|
|
36
37
|
const id = response.body?.[this.property ?? 'id']
|
|
37
38
|
|
|
39
|
+
if (id === undefined) {
|
|
40
|
+
console.debug('Inception skipped: response does not contain expected property', {
|
|
41
|
+
property: this.property,
|
|
42
|
+
response
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
return
|
|
46
|
+
}
|
|
47
|
+
|
|
38
48
|
assert(typeof id === 'string', `Response body property "${this.property}" expected to be a string`)
|
|
39
49
|
|
|
40
50
|
if (context.request.headers.authorization !== undefined)
|
|
41
51
|
context.identity = await this.incept(context, id)
|
|
42
52
|
else
|
|
43
|
-
context.identity = { id, scheme: null, refresh: true }
|
|
53
|
+
context.identity = { id, scheme: null, refresh: true, roles: [] }
|
|
44
54
|
}
|
|
45
55
|
|
|
46
56
|
private async incept (context: Context, id: string): Promise<Identity> {
|
|
@@ -67,6 +77,7 @@ export class Incept implements Directive {
|
|
|
67
77
|
throw new http.UnprocessableEntity(identity)
|
|
68
78
|
|
|
69
79
|
identity.scheme = scheme
|
|
80
|
+
identity.roles = []
|
|
70
81
|
|
|
71
82
|
return identity
|
|
72
83
|
}
|
|
@@ -28,6 +28,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
28
28
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
29
29
|
exports.Incept = void 0;
|
|
30
30
|
const node_assert_1 = __importDefault(require("node:assert"));
|
|
31
|
+
const openspan_1 = require("openspan");
|
|
31
32
|
const http = __importStar(require("../../HTTP"));
|
|
32
33
|
const split_1 = require("./split");
|
|
33
34
|
const create_1 = require("./create");
|
|
@@ -52,11 +53,18 @@ class Incept {
|
|
|
52
53
|
}
|
|
53
54
|
async settle(context, response) {
|
|
54
55
|
const id = response.body?.[this.property ?? 'id'];
|
|
56
|
+
if (id === undefined) {
|
|
57
|
+
openspan_1.console.debug('Inception skipped: response does not contain expected property', {
|
|
58
|
+
property: this.property,
|
|
59
|
+
response
|
|
60
|
+
});
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
55
63
|
(0, node_assert_1.default)(typeof id === 'string', `Response body property "${this.property}" expected to be a string`);
|
|
56
64
|
if (context.request.headers.authorization !== undefined)
|
|
57
65
|
context.identity = await this.incept(context, id);
|
|
58
66
|
else
|
|
59
|
-
context.identity = { id, scheme: null, refresh: true };
|
|
67
|
+
context.identity = { id, scheme: null, refresh: true, roles: [] };
|
|
60
68
|
}
|
|
61
69
|
async incept(context, id) {
|
|
62
70
|
const [scheme, credentials] = (0, split_1.split)(context.request.headers.authorization);
|
|
@@ -76,6 +84,7 @@ class Incept {
|
|
|
76
84
|
if (identity instanceof Error)
|
|
77
85
|
throw new http.UnprocessableEntity(identity);
|
|
78
86
|
identity.scheme = scheme;
|
|
87
|
+
identity.roles = [];
|
|
79
88
|
return identity;
|
|
80
89
|
}
|
|
81
90
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Incept.js","sourceRoot":"","sources":["../../../source/directives/auth/Incept.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,8DAAgC;AAChC,iDAAkC;AAClC,mCAA+B;AAC/B,qCAAiC;AACjC,uCAAgD;AAIhD,MAAa,MAAM;IACA,QAAQ,CAAe;IACvB,SAAS,CAAW;IACpB,OAAO,GAAY,EAAwB,CAAA;IAE5D,YAAoB,QAAgB,EAAE,SAAoB;QACxD,qBAAM,CAAC,EAAE,CAAC,QAAQ,KAAK,IAAI,IAAI,OAAO,QAAQ,KAAK,QAAQ,EACzD,8CAA8C,CAAC,CAAA;QAEjD,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAA;QACxB,IAAI,CAAC,SAAS,GAAG,SAAS,CAAA;IAC5B,CAAC;IAEM,SAAS,CAAE,QAAyB;QACzC,OAAO,QAAQ,KAAK,IAAI,CAAA;IAC1B,CAAC;IAEM,KAAK,CAAE,OAAgB;QAC5B,IAAI,IAAI,CAAC,QAAQ,KAAK,IAAI;YACxB,OAAO,IAAI,CAAA;QAEb,MAAM,IAAI,GAAG,IAAA,eAAM,EAAC,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC,aAAa,CAAC,CAAA;QAE1D,OAAO,EAAE,IAAI,EAAE,CAAA;IACjB,CAAC;IAEM,KAAK,CAAC,MAAM,CAAE,OAAgB,EAAE,QAA8B;QACnE,MAAM,EAAE,GAAG,QAAQ,CAAC,IAAI,EAAE,CAAC,IAAI,CAAC,QAAQ,IAAI,IAAI,CAAC,CAAA;QAEjD,IAAA,qBAAM,EAAC,OAAO,EAAE,KAAK,QAAQ,EAAE,2BAA2B,IAAI,CAAC,QAAQ,2BAA2B,CAAC,CAAA;QAEnG,IAAI,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC,aAAa,KAAK,SAAS;YACrD,OAAO,CAAC,QAAQ,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,EAAE,CAAC,CAAA;;YAEjD,OAAO,CAAC,QAAQ,GAAG,EAAE,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,CAAA;
|
|
1
|
+
{"version":3,"file":"Incept.js","sourceRoot":"","sources":["../../../source/directives/auth/Incept.ts"],"names":[],"mappings":"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"}
|