@toa.io/extensions.exposition 1.0.0-alpha.2 → 1.0.0-alpha.3
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.federation/manifest.toa.yaml +12 -0
- package/components/identity.federation/operations/authenticate.js +4 -4
- package/components/identity.federation/operations/authenticate.js.map +1 -1
- package/components/identity.federation/operations/create.js +4 -4
- package/components/identity.federation/operations/create.js.map +1 -1
- package/components/identity.federation/operations/{assertions-as-values.cjs → lib/assertions-as-values.js} +1 -1
- package/components/identity.federation/operations/lib/assertions-as-values.js.map +1 -0
- package/components/identity.federation/operations/{jwt.d.cts → lib/jwt.d.ts} +5 -4
- package/components/identity.federation/operations/{jwt.cjs → lib/jwt.js} +35 -11
- package/components/identity.federation/operations/lib/jwt.js.map +1 -0
- package/components/identity.federation/operations/schemas.d.ts +16 -0
- package/components/identity.federation/operations/tsconfig.tsbuildinfo +1 -1
- package/components/identity.federation/operations/types.d.ts +1 -1
- package/components/identity.federation/source/authenticate.ts +2 -2
- package/components/identity.federation/source/create.ts +2 -2
- package/components/identity.federation/source/{assertions-as-values.cts → lib/assertions-as-values.ts} +1 -2
- package/components/identity.federation/source/lib/jwt.test.ts +56 -0
- package/components/identity.federation/source/{jwt.cts → lib/jwt.ts} +57 -29
- package/components/identity.federation/source/schemas.ts +16 -0
- package/components/identity.federation/source/types.ts +1 -1
- package/documentation/components.md +7 -0
- package/documentation/identity.md +7 -0
- package/documentation/octets.md +12 -0
- package/features/identity.federation.feature +31 -1
- package/features/octets.workflows.feature +38 -0
- package/features/steps/IdP.ts +29 -0
- package/package.json +8 -8
- package/schemas/octets/workflow.cos.yaml +12 -0
- package/source/directives/octets/Delete.ts +2 -2
- package/source/directives/octets/Octets.ts +3 -1
- package/source/directives/octets/Store.ts +17 -3
- package/source/directives/octets/Workflow.ts +41 -0
- package/source/directives/octets/schemas.test.ts +21 -0
- package/source/directives/octets/schemas.ts +2 -0
- package/source/directives/octets/{workflow → workflows}/Execution.ts +0 -2
- package/transpiled/directives/octets/Delete.d.ts +1 -1
- package/transpiled/directives/octets/Delete.js +2 -2
- package/transpiled/directives/octets/Delete.js.map +1 -1
- package/transpiled/directives/octets/Octets.js +3 -1
- package/transpiled/directives/octets/Octets.js.map +1 -1
- package/transpiled/directives/octets/Store.d.ts +2 -1
- package/transpiled/directives/octets/Store.js +11 -3
- package/transpiled/directives/octets/Store.js.map +1 -1
- package/transpiled/directives/octets/Workflow.d.ts +14 -0
- package/transpiled/directives/octets/Workflow.js +52 -0
- package/transpiled/directives/octets/Workflow.js.map +1 -0
- package/transpiled/directives/octets/schemas.d.ts +2 -0
- package/transpiled/directives/octets/schemas.js +2 -1
- package/transpiled/directives/octets/schemas.js.map +1 -1
- package/transpiled/directives/octets/{workflow → workflows}/Execution.js +0 -1
- package/transpiled/directives/octets/workflows/Execution.js.map +1 -0
- package/transpiled/directives/octets/workflows/Workflow.js.map +1 -0
- package/transpiled/directives/octets/workflows/index.js.map +1 -0
- package/transpiled/tsconfig.tsbuildinfo +1 -1
- package/components/identity.federation/operations/assertions-as-values.cjs.map +0 -1
- package/components/identity.federation/operations/jwt.cjs.map +0 -1
- package/transpiled/directives/octets/workflow/Execution.js.map +0 -1
- package/transpiled/directives/octets/workflow/Workflow.js.map +0 -1
- package/transpiled/directives/octets/workflow/index.js.map +0 -1
- /package/components/identity.federation/operations/{assertions-as-values.d.cts → lib/assertions-as-values.d.ts} +0 -0
- /package/source/directives/octets/{workflow → workflows}/Workflow.ts +0 -0
- /package/source/directives/octets/{workflow → workflows}/index.ts +0 -0
- /package/transpiled/directives/octets/{workflow → workflows}/Execution.d.ts +0 -0
- /package/transpiled/directives/octets/{workflow → workflows}/Workflow.d.ts +0 -0
- /package/transpiled/directives/octets/{workflow → workflows}/Workflow.js +0 -0
- /package/transpiled/directives/octets/{workflow → workflows}/index.d.ts +0 -0
- /package/transpiled/directives/octets/{workflow → workflows}/index.js +0 -0
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/* eslint-disable max-len */
|
|
2
|
+
/* eslint-disable @typescript-eslint/consistent-type-assertions */
|
|
3
|
+
import { validateSignature, decodeJwt } from './jwt'
|
|
4
|
+
|
|
5
|
+
describe('jwt', () => {
|
|
6
|
+
test('decode', () => {
|
|
7
|
+
const { header, payload } = decodeJwt('eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzb21lIjoicGF5bG9hZCJ9.4twFt5NiznN84AWoo1d7KO1T_yoc0Z6XOpOVswacPZg')
|
|
8
|
+
|
|
9
|
+
expect(header).toMatchObject({ alg: 'HS256' })
|
|
10
|
+
expect(payload).toEqual({ some: 'payload' })
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
test('symmetric pass', async () => {
|
|
14
|
+
// example from
|
|
15
|
+
// https://pyjwt.readthedocs.io/en/latest/usage.html#encoding-decoding-tokens-with-hs256
|
|
16
|
+
|
|
17
|
+
await expect(validateSignature({
|
|
18
|
+
header: { alg: 'HS256', kid: 'k2' },
|
|
19
|
+
payload: { iss: 'test-issuer', aud: 'test', sub: '0', exp: 0, iat: 0 },
|
|
20
|
+
rawHeader: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9',
|
|
21
|
+
rawPayload: 'eyJzb21lIjoicGF5bG9hZCJ9',
|
|
22
|
+
signature: '4twFt5NiznN84AWoo1d7KO1T_yoc0Z6XOpOVswacPZg',
|
|
23
|
+
trusted: [
|
|
24
|
+
{
|
|
25
|
+
issuer: 'test-issuer',
|
|
26
|
+
secrets: {
|
|
27
|
+
HS256: {
|
|
28
|
+
k1: 'old-secret',
|
|
29
|
+
k2: 'secret'
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
]
|
|
34
|
+
} as Parameters<typeof validateSignature>[0])).resolves.toBeUndefined()
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
test('symmetric fail', async () => {
|
|
38
|
+
await expect(validateSignature({
|
|
39
|
+
header: { alg: 'HS256' },
|
|
40
|
+
payload: { iss: 'test-issuer', aud: 'test', sub: '0', exp: 0, iat: 0 },
|
|
41
|
+
rawHeader: 'header',
|
|
42
|
+
rawPayload: 'payload',
|
|
43
|
+
signature: 'signature',
|
|
44
|
+
trusted: [
|
|
45
|
+
{
|
|
46
|
+
issuer: 'test-issuer',
|
|
47
|
+
secrets: {
|
|
48
|
+
HS256: {
|
|
49
|
+
theKey: 'secret'
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
]
|
|
54
|
+
} as Parameters<typeof validateSignature>[0])).rejects.toThrow('Signature does not match')
|
|
55
|
+
})
|
|
56
|
+
})
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import crypto from 'node:crypto'
|
|
2
2
|
import * as assert from 'node:assert'
|
|
3
|
-
import type
|
|
4
|
-
import { type TrustConfiguration } from '
|
|
3
|
+
import { type JwtHeader, type IdToken } from '../types'
|
|
4
|
+
import { type TrustConfiguration } from '../schemas'
|
|
5
5
|
|
|
6
6
|
export function decodeJwt (token: string): {
|
|
7
7
|
header: unknown
|
|
@@ -20,20 +20,19 @@ export function decodeJwt (token: string): {
|
|
|
20
20
|
|
|
21
21
|
export function validateJwtHeader (header: unknown): asserts header is JwtHeader {
|
|
22
22
|
assert.ok(header !== null && typeof header === 'object', 'Header is not an object')
|
|
23
|
-
assert.ok('typ' in header, 'Header is missing typ')
|
|
24
|
-
assert.equal(header.typ, 'JWT')
|
|
25
23
|
assert.ok('alg' in header, 'Header is missing alg')
|
|
26
24
|
assert.ok(typeof header.alg === 'string', 'Header alg is not a string')
|
|
27
|
-
assert.
|
|
25
|
+
assert.match(header.alg, /^RS256|HS\d{3}$/, `Unknown algorithm ${header.alg}`)
|
|
26
|
+
assert.ok(!('kid' in header) || typeof header.kid === 'string', 'kid must be a string if present')
|
|
28
27
|
}
|
|
29
28
|
|
|
30
|
-
export function validateJwtPayload (
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
): asserts payload is IdToken {
|
|
29
|
+
export function validateJwtPayload (payload: unknown,
|
|
30
|
+
trusted: TrustConfiguration[] = [],
|
|
31
|
+
header: JwtHeader): asserts payload is IdToken {
|
|
34
32
|
assert.ok(trusted.length > 0, 'No trusted issuers provided')
|
|
35
33
|
|
|
36
|
-
// full list of validations is
|
|
34
|
+
// full list of validations is
|
|
35
|
+
// at https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation
|
|
37
36
|
assert.ok(payload !== null && typeof payload === 'object', 'Payload is not an object')
|
|
38
37
|
|
|
39
38
|
assert.ok('iss' in payload, 'Payload is missing iss')
|
|
@@ -41,9 +40,24 @@ export function validateJwtPayload (
|
|
|
41
40
|
assert.ok('aud' in payload, 'Payload is missing aud')
|
|
42
41
|
assert.ok(typeof payload.aud === 'string', 'Payload aud is not a string')
|
|
43
42
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
43
|
+
const issuer = trusted.find((config) => config.issuer === payload.iss)
|
|
44
|
+
|
|
45
|
+
assert.ok(issuer !== undefined &&
|
|
46
|
+
(issuer.audience === undefined || issuer.audience.some((a) => a === payload.aud),
|
|
47
|
+
`Unknown issuer / audience: ${payload.iss} / ${payload.aud}`))
|
|
48
|
+
|
|
49
|
+
if (header.alg.startsWith('HS')) {
|
|
50
|
+
const secrets = issuer.secrets
|
|
51
|
+
|
|
52
|
+
assert.ok(secrets, `We don't have known secrets for ${payload.iss}`)
|
|
53
|
+
|
|
54
|
+
const keys = secrets[header.alg]
|
|
55
|
+
|
|
56
|
+
assert.ok(keys, `No known secrets for ${header.alg}`)
|
|
57
|
+
|
|
58
|
+
if (typeof header.kid === 'string')
|
|
59
|
+
assert.ok(header.kid in keys, `No secret ${header.kid} provided for ${header.alg}`)
|
|
60
|
+
}
|
|
47
61
|
|
|
48
62
|
assert.ok('sub' in payload, 'Payload is missing sub')
|
|
49
63
|
assert.ok(typeof payload.sub === 'string', 'Payload sub is not a string')
|
|
@@ -68,20 +82,39 @@ export async function validateSignature ({
|
|
|
68
82
|
payload: { iss },
|
|
69
83
|
rawHeader,
|
|
70
84
|
rawPayload,
|
|
71
|
-
signature
|
|
85
|
+
signature,
|
|
86
|
+
trusted = []
|
|
72
87
|
}: {
|
|
73
88
|
readonly header: JwtHeader
|
|
74
89
|
rawHeader: string
|
|
75
90
|
readonly payload: IdToken
|
|
76
91
|
rawPayload: string
|
|
77
92
|
signature: string
|
|
93
|
+
trusted?: TrustConfiguration[]
|
|
78
94
|
}): Promise<void> {
|
|
95
|
+
if (alg.startsWith('HS')) {
|
|
96
|
+
// symmetric algorithm, issuer is validated at this point
|
|
97
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- `kid` is validated
|
|
98
|
+
const secrets = trusted.find((c) => c.issuer === iss)!.secrets![alg]
|
|
99
|
+
const secret = kid !== undefined ? secrets[kid] : Object.values(secrets)[0]
|
|
100
|
+
const algorithm = alg.replace(/^HS(\d{3})$/, 'sha$1') // HS256 -> sha256
|
|
101
|
+
const hmac = crypto.createHmac(algorithm, secret)
|
|
102
|
+
|
|
103
|
+
hmac.update(rawHeader)
|
|
104
|
+
hmac.update('.')
|
|
105
|
+
hmac.update(rawPayload)
|
|
106
|
+
assert.strictEqual(signature, hmac.digest('base64url'), 'Signature does not match')
|
|
107
|
+
|
|
108
|
+
return
|
|
109
|
+
}
|
|
110
|
+
|
|
79
111
|
// Getting issuer public keys
|
|
80
112
|
const oidcRequest = await fetch(`${iss}/.well-known/openid-configuration`, {
|
|
81
113
|
cache: 'default'
|
|
82
114
|
})
|
|
83
115
|
|
|
84
|
-
assert.ok(oidcRequest.ok,
|
|
116
|
+
assert.ok(oidcRequest.ok,
|
|
117
|
+
`Failed to fetch OpenID configuration: ${oidcRequest.statusText}`)
|
|
85
118
|
|
|
86
119
|
const { jwks_uri: jwksUri } = (await oidcRequest.json()) as { jwks_uri: string }
|
|
87
120
|
|
|
@@ -98,10 +131,8 @@ export async function validateSignature ({
|
|
|
98
131
|
|
|
99
132
|
assert.ok(signingKeys.length > 0, 'No acceptable signing keys found')
|
|
100
133
|
|
|
101
|
-
assert.ok(
|
|
102
|
-
|
|
103
|
-
'Signing key selection is not deterministic'
|
|
104
|
-
)
|
|
134
|
+
assert.ok(kid === undefined || signingKeys.length === 1,
|
|
135
|
+
'Signing key selection is not deterministic')
|
|
105
136
|
|
|
106
137
|
const signingKey = kid === undefined ? signingKeys.find((k) => k.kid === kid) : keys[0]
|
|
107
138
|
|
|
@@ -114,29 +145,26 @@ export async function validateSignature ({
|
|
|
114
145
|
verifyFunction.write(rawPayload)
|
|
115
146
|
verifyFunction.end()
|
|
116
147
|
|
|
117
|
-
const signatureValid = verifyFunction.verify(
|
|
118
|
-
{ format: 'jwk', key: signingKey },
|
|
148
|
+
const signatureValid = verifyFunction.verify({ format: 'jwk', key: signingKey },
|
|
119
149
|
signature,
|
|
120
|
-
'base64url'
|
|
121
|
-
)
|
|
150
|
+
'base64url')
|
|
122
151
|
|
|
123
152
|
assert.ok(signatureValid, 'Failed to validate signature')
|
|
124
153
|
}
|
|
125
154
|
|
|
126
|
-
export async function validateIdToken (
|
|
127
|
-
|
|
128
|
-
trusted?: TrustConfiguration[]
|
|
129
|
-
): Promise<IdToken> {
|
|
155
|
+
export async function validateIdToken (token: string,
|
|
156
|
+
trusted?: TrustConfiguration[]): Promise<IdToken> {
|
|
130
157
|
const { header, payload, rawHeader, rawPayload, signature } = decodeJwt(token)
|
|
131
158
|
|
|
132
159
|
validateJwtHeader(header)
|
|
133
|
-
validateJwtPayload(payload, trusted)
|
|
160
|
+
validateJwtPayload(payload, trusted, header)
|
|
134
161
|
await validateSignature({
|
|
135
162
|
header,
|
|
136
163
|
rawHeader,
|
|
137
164
|
payload,
|
|
138
165
|
rawPayload,
|
|
139
|
-
signature
|
|
166
|
+
signature,
|
|
167
|
+
trusted
|
|
140
168
|
})
|
|
141
169
|
|
|
142
170
|
return payload
|
|
@@ -42,4 +42,20 @@ export interface TrustConfiguration {
|
|
|
42
42
|
* @minItems 1
|
|
43
43
|
*/
|
|
44
44
|
audience?: [string, ...string[]];
|
|
45
|
+
/**
|
|
46
|
+
* Symmetric encryption secrets
|
|
47
|
+
*/
|
|
48
|
+
secrets?: {
|
|
49
|
+
/**
|
|
50
|
+
* This interface was referenced by `undefined`'s JSON-Schema definition
|
|
51
|
+
* via the `patternProperty` "^HS\d{3}$".
|
|
52
|
+
*/
|
|
53
|
+
[k: string]: {
|
|
54
|
+
/**
|
|
55
|
+
* This interface was referenced by `undefined`'s JSON-Schema definition
|
|
56
|
+
* via the `patternProperty` "^\w+$".
|
|
57
|
+
*/
|
|
58
|
+
[k: string]: string;
|
|
59
|
+
};
|
|
60
|
+
};
|
|
45
61
|
}
|
|
@@ -100,6 +100,8 @@ The configuration schema alongside default values is described in the [component
|
|
|
100
100
|
|
|
101
101
|
No federated tokens are accepted by default until at least one entry is added to the `trust` configuration.
|
|
102
102
|
|
|
103
|
+
Toa supports either asymmetric RS256 or symmetric HS256 / HS384 / HS512 tokens with pre-shared secrets.
|
|
104
|
+
|
|
103
105
|
```yaml
|
|
104
106
|
# context.toa.yaml
|
|
105
107
|
|
|
@@ -110,6 +112,11 @@ configuration:
|
|
|
110
112
|
audience:
|
|
111
113
|
- https://github.com/tinovyatkin
|
|
112
114
|
- https://github.com/temich
|
|
115
|
+
|
|
116
|
+
- issuer: some.private.issuer
|
|
117
|
+
secrets:
|
|
118
|
+
HS256:
|
|
119
|
+
k1: <secret-to-be-used-for-hs256>
|
|
113
120
|
```
|
|
114
121
|
|
|
115
122
|
## Stateless tokens
|
|
@@ -80,7 +80,14 @@ configuration:
|
|
|
80
80
|
- issuer: https://accounts.google.com
|
|
81
81
|
audience:
|
|
82
82
|
- <GOOGLE_CLIENT_ID>
|
|
83
|
+
|
|
83
84
|
- issuer: https://appleid.apple.com
|
|
85
|
+
|
|
86
|
+
- issuer: private.entity
|
|
87
|
+
secrets:
|
|
88
|
+
HS384:
|
|
89
|
+
key0: <THE-SECRET-STRING-FOR-HS384>
|
|
90
|
+
key1: <THE-SECRET-STRING-FOR-HS384> # selected by `kid` in the JWT header
|
|
84
91
|
```
|
|
85
92
|
|
|
86
93
|
## Identity inception
|
package/documentation/octets.md
CHANGED
|
@@ -209,6 +209,18 @@ under the request path.
|
|
|
209
209
|
|
|
210
210
|
The request body must be a list of entry identifiers.
|
|
211
211
|
|
|
212
|
+
## `octets:workflow`
|
|
213
|
+
|
|
214
|
+
Execute a [workflow](#workflows) on the entry under the request path.
|
|
215
|
+
|
|
216
|
+
```yaml
|
|
217
|
+
/images:
|
|
218
|
+
/*:
|
|
219
|
+
DELETE:
|
|
220
|
+
octets:workflow:
|
|
221
|
+
archive: images.archive
|
|
222
|
+
```
|
|
223
|
+
|
|
212
224
|
## Workflows
|
|
213
225
|
|
|
214
226
|
A workflow is a list of endpoints to be called.
|
|
@@ -54,6 +54,36 @@ Feature: Identity Federation
|
|
|
54
54
|
id: ${{ User.id }}
|
|
55
55
|
"""
|
|
56
56
|
|
|
57
|
+
Scenario: Getting identity for a user with symmetric tokens
|
|
58
|
+
Given the `identity.federation` configuration:
|
|
59
|
+
"""yaml
|
|
60
|
+
explicit_identity_creation: false
|
|
61
|
+
trust:
|
|
62
|
+
- issuer: http://localhost:44444
|
|
63
|
+
secrets:
|
|
64
|
+
HS384:
|
|
65
|
+
k1: the-secret
|
|
66
|
+
"""
|
|
67
|
+
And the IDP HS384 token for GoodUser is issued with following secret:
|
|
68
|
+
"""
|
|
69
|
+
the-secret
|
|
70
|
+
"""
|
|
71
|
+
When the following request is received:
|
|
72
|
+
"""
|
|
73
|
+
GET /identity/ HTTP/1.1
|
|
74
|
+
authorization: Bearer ${{ GoodUser.id_token }}
|
|
75
|
+
accept: application/yaml
|
|
76
|
+
content-type: application/yaml
|
|
77
|
+
"""
|
|
78
|
+
Then the following reply is sent:
|
|
79
|
+
"""
|
|
80
|
+
200 OK
|
|
81
|
+
authorization: Token ${{ GoodUser.token }}
|
|
82
|
+
|
|
83
|
+
id: ${{ GoodUser.id }}
|
|
84
|
+
scheme: bearer
|
|
85
|
+
"""
|
|
86
|
+
|
|
57
87
|
Scenario: Creating an Identity using inception with existing credentials
|
|
58
88
|
Given the `identity.federation` configuration:
|
|
59
89
|
"""yaml
|
|
@@ -67,7 +97,7 @@ Feature: Identity Federation
|
|
|
67
97
|
anonymous: true
|
|
68
98
|
POST:
|
|
69
99
|
incept: id
|
|
70
|
-
endpoint:
|
|
100
|
+
endpoint: create
|
|
71
101
|
"""
|
|
72
102
|
And the IDP token for Bill is issued
|
|
73
103
|
When the following request is received:
|
|
@@ -246,3 +246,41 @@ Feature: Octets storage workflows
|
|
|
246
246
|
concat: hello world
|
|
247
247
|
--cut--
|
|
248
248
|
"""
|
|
249
|
+
|
|
250
|
+
Scenario: Executing a workflow with `octets:workflow`
|
|
251
|
+
Given the `octets.tester` is running
|
|
252
|
+
And the annotation:
|
|
253
|
+
"""yaml
|
|
254
|
+
/:
|
|
255
|
+
auth:anonymous: true
|
|
256
|
+
octets:context: octets
|
|
257
|
+
POST:
|
|
258
|
+
octets:store: ~
|
|
259
|
+
/*:
|
|
260
|
+
DELETE:
|
|
261
|
+
octets:workflow:
|
|
262
|
+
echo: octets.tester.echo
|
|
263
|
+
"""
|
|
264
|
+
When the stream of `lenna.ascii` is received with the following headers:
|
|
265
|
+
"""
|
|
266
|
+
POST / HTTP/1.1
|
|
267
|
+
content-type: application/octet-stream
|
|
268
|
+
"""
|
|
269
|
+
Then the following reply is sent:
|
|
270
|
+
"""
|
|
271
|
+
201 Created
|
|
272
|
+
"""
|
|
273
|
+
When the following request is received:
|
|
274
|
+
"""
|
|
275
|
+
DELETE /10cf16b458f759e0d617f2f3d83599ff HTTP/1.1
|
|
276
|
+
accept: application/yaml
|
|
277
|
+
"""
|
|
278
|
+
Then the following reply is sent:
|
|
279
|
+
"""
|
|
280
|
+
202 Accepted
|
|
281
|
+
content-type: multipart/yaml; boundary=cut
|
|
282
|
+
|
|
283
|
+
--cut
|
|
284
|
+
echo: 10cf16b458f759e0d617f2f3d83599ff
|
|
285
|
+
--cut--
|
|
286
|
+
"""
|
package/features/steps/IdP.ts
CHANGED
|
@@ -117,4 +117,33 @@ export class IdP {
|
|
|
117
117
|
|
|
118
118
|
this.captures.set(`${user}.id_token`, idToken)
|
|
119
119
|
}
|
|
120
|
+
|
|
121
|
+
@given('the IDP {word} token for {word} is issued with following secret:')
|
|
122
|
+
public async issueSymmetricToken (alg: string, user: string, secret: string): Promise<void> {
|
|
123
|
+
console.log('Sym token for %s with secret "%s"', user, secret)
|
|
124
|
+
|
|
125
|
+
const jwt = [
|
|
126
|
+
{
|
|
127
|
+
typ: 'JWT',
|
|
128
|
+
alg
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
iss: IdP.issuer,
|
|
132
|
+
sub: `${user}-mock-id`,
|
|
133
|
+
aud: 'test',
|
|
134
|
+
iat: Math.floor(Date.now() / 1000),
|
|
135
|
+
exp: Math.floor((Date.now() + 1000 * 60 * 5) / 1000)
|
|
136
|
+
}
|
|
137
|
+
]
|
|
138
|
+
.map((v) => Buffer.from(JSON.stringify(v)).toString('base64url'))
|
|
139
|
+
.join('.')
|
|
140
|
+
|
|
141
|
+
const signature = crypto.createHmac(alg.replace(/^HS(\d{3})$/, 'sha$1'), secret)
|
|
142
|
+
.update(jwt)
|
|
143
|
+
.digest('base64url')
|
|
144
|
+
|
|
145
|
+
const idToken = `${jwt}.${signature}`
|
|
146
|
+
|
|
147
|
+
this.captures.set(`${user}.id_token`, idToken)
|
|
148
|
+
}
|
|
120
149
|
}
|
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.3",
|
|
4
4
|
"description": "Toa Exposition",
|
|
5
5
|
"author": "temich <tema.gurtovoy@gmail.com>",
|
|
6
6
|
"homepage": "https://github.com/toa-io/toa#readme",
|
|
@@ -17,10 +17,10 @@
|
|
|
17
17
|
"access": "public"
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
|
20
|
-
"@toa.io/core": "1.0.0-alpha.
|
|
21
|
-
"@toa.io/generic": "1.0.0-alpha.
|
|
22
|
-
"@toa.io/schemas": "1.0.0-alpha.
|
|
23
|
-
"@toa.io/streams": "1.0.0-alpha.
|
|
20
|
+
"@toa.io/core": "1.0.0-alpha.3",
|
|
21
|
+
"@toa.io/generic": "1.0.0-alpha.3",
|
|
22
|
+
"@toa.io/schemas": "1.0.0-alpha.3",
|
|
23
|
+
"@toa.io/streams": "1.0.0-alpha.3",
|
|
24
24
|
"bcryptjs": "2.4.3",
|
|
25
25
|
"error-value": "0.3.0",
|
|
26
26
|
"express": "4.18.2",
|
|
@@ -45,12 +45,12 @@
|
|
|
45
45
|
"features": "cucumber-js"
|
|
46
46
|
},
|
|
47
47
|
"devDependencies": {
|
|
48
|
-
"@toa.io/agent": "1.0.0-alpha.
|
|
49
|
-
"@toa.io/extensions.storages": "1.0.0-alpha.
|
|
48
|
+
"@toa.io/agent": "1.0.0-alpha.3",
|
|
49
|
+
"@toa.io/extensions.storages": "1.0.0-alpha.3",
|
|
50
50
|
"@types/bcryptjs": "2.4.3",
|
|
51
51
|
"@types/cors": "2.8.13",
|
|
52
52
|
"@types/express": "4.17.17",
|
|
53
53
|
"@types/negotiator": "0.6.1"
|
|
54
54
|
},
|
|
55
|
-
"gitHead": "
|
|
55
|
+
"gitHead": "e36ac7871fc14d15863aaf8f9bbdeace8bdfa9f0"
|
|
56
56
|
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { Readable } from 'stream'
|
|
2
2
|
import { NotFound } from '../../HTTP'
|
|
3
3
|
import * as schemas from './schemas'
|
|
4
|
-
import { Workflow } from './
|
|
4
|
+
import { Workflow } from './workflows'
|
|
5
5
|
import type { Parameter } from '../../RTD'
|
|
6
|
-
import type { Unit } from './
|
|
6
|
+
import type { Unit } from './workflows'
|
|
7
7
|
import type { Maybe } from '@toa.io/types'
|
|
8
8
|
import type { Component } from '@toa.io/core'
|
|
9
9
|
import type { Output } from '../../io'
|
|
@@ -5,6 +5,7 @@ import { Fetch } from './Fetch'
|
|
|
5
5
|
import { List } from './List'
|
|
6
6
|
import { Delete } from './Delete'
|
|
7
7
|
import { Permute } from './Permute'
|
|
8
|
+
import { WorkflowDirective } from './Workflow'
|
|
8
9
|
import type { Output } from '../../io'
|
|
9
10
|
import type { Component } from '@toa.io/core'
|
|
10
11
|
import type { Remotes } from '../../Remotes'
|
|
@@ -63,7 +64,8 @@ const DIRECTIVES: Record<string, Constructor> = {
|
|
|
63
64
|
fetch: Fetch,
|
|
64
65
|
list: List,
|
|
65
66
|
delete: Delete,
|
|
66
|
-
permute: Permute
|
|
67
|
+
permute: Permute,
|
|
68
|
+
workflow: WorkflowDirective
|
|
67
69
|
}
|
|
68
70
|
|
|
69
71
|
type Constructor = new (value: any, discovery: Promise<Component>, remotes: Remotes) => Directive
|
|
@@ -1,10 +1,12 @@
|
|
|
1
|
+
import { PassThrough } from 'node:stream'
|
|
1
2
|
import { match } from 'matchacho'
|
|
2
3
|
import { BadRequest, UnsupportedMediaType } from '../../HTTP'
|
|
3
4
|
import { cors } from '../cors'
|
|
4
5
|
import * as schemas from './schemas'
|
|
5
|
-
import { Workflow } from './
|
|
6
|
+
import { Workflow } from './workflows'
|
|
7
|
+
import type { Readable } from 'stream'
|
|
6
8
|
import type { Parameter } from '../../RTD'
|
|
7
|
-
import type { Unit } from './
|
|
9
|
+
import type { Unit } from './workflows'
|
|
8
10
|
import type { Entry } from '@toa.io/extensions.storages'
|
|
9
11
|
import type { Remotes } from '../../Remotes'
|
|
10
12
|
import type { ErrorType } from 'error-value'
|
|
@@ -60,11 +62,23 @@ export class Store implements Directive {
|
|
|
60
62
|
private reply (request: Input, storage: string, entry: Entry, parameters: Parameter[]): Output {
|
|
61
63
|
const body = this.workflow === undefined
|
|
62
64
|
? entry
|
|
63
|
-
: this.
|
|
65
|
+
: this.execute(request, storage, entry, parameters)
|
|
64
66
|
|
|
65
67
|
return { body }
|
|
66
68
|
}
|
|
67
69
|
|
|
70
|
+
// eslint-disable-next-line max-params
|
|
71
|
+
private execute
|
|
72
|
+
(request: Input, storage: string, entry: Entry, parameters: Parameter[]): Readable {
|
|
73
|
+
const stream = new PassThrough({ objectMode: true })
|
|
74
|
+
|
|
75
|
+
stream.push(entry)
|
|
76
|
+
|
|
77
|
+
this.workflow!.execute(request, storage, entry, parameters).pipe(stream)
|
|
78
|
+
|
|
79
|
+
return stream
|
|
80
|
+
}
|
|
81
|
+
|
|
68
82
|
private throw (error: ErrorType): never {
|
|
69
83
|
throw match(error.code,
|
|
70
84
|
'NOT_ACCEPTABLE', () => new UnsupportedMediaType(),
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { NotFound } from '../../HTTP'
|
|
2
|
+
import * as schemas from './schemas'
|
|
3
|
+
import { Workflow } from './workflows'
|
|
4
|
+
import type { Unit } from './workflows'
|
|
5
|
+
import type { Directive, Input } from './types'
|
|
6
|
+
import type { Component } from '@toa.io/core'
|
|
7
|
+
import type { Output } from '../../io'
|
|
8
|
+
import type { Remotes } from '../../Remotes'
|
|
9
|
+
import type { Maybe } from '@toa.io/types'
|
|
10
|
+
import type { Entry } from '@toa.io/extensions.storages'
|
|
11
|
+
import type { Parameter } from '../../RTD'
|
|
12
|
+
|
|
13
|
+
export class WorkflowDirective implements Directive {
|
|
14
|
+
public readonly targeted = true
|
|
15
|
+
|
|
16
|
+
private readonly workflow: Workflow
|
|
17
|
+
private readonly discovery: Promise<Component>
|
|
18
|
+
private storage: Component | null = null
|
|
19
|
+
|
|
20
|
+
public constructor (units: Unit[] | Unit, discovery: Promise<Component>, remotes: Remotes) {
|
|
21
|
+
schemas.workflow.validate(units)
|
|
22
|
+
|
|
23
|
+
this.workflow = new Workflow(units, remotes)
|
|
24
|
+
this.discovery = discovery
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
public async apply (storage: string, request: Input, parameters: Parameter[]): Promise<Output> {
|
|
28
|
+
this.storage ??= await this.discovery
|
|
29
|
+
|
|
30
|
+
const entry = await this.storage.invoke<Maybe<Entry>>('get',
|
|
31
|
+
{ input: { storage, path: request.url } })
|
|
32
|
+
|
|
33
|
+
if (entry instanceof Error)
|
|
34
|
+
throw new NotFound()
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
status: 202,
|
|
38
|
+
body: this.workflow.execute(request, storage, entry, parameters)
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import * as schemas from './schemas'
|
|
2
|
+
|
|
3
|
+
describe('workflow', () => {
|
|
4
|
+
const ok = [
|
|
5
|
+
{ echo: 'hello world' },
|
|
6
|
+
[{ echo: 'hello world' }, { ok: 'ok' }]
|
|
7
|
+
]
|
|
8
|
+
|
|
9
|
+
const oh = [
|
|
10
|
+
{ echo: [] },
|
|
11
|
+
{ echo: 'hello world', ok: { not: 'ok' } }
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
it.each(ok)('should be valid', (workflow) => {
|
|
15
|
+
expect(() => schemas.workflow.validate(workflow)).not.toThrow()
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
it.each(oh)('should not be valid', (workflow) => {
|
|
19
|
+
expect(() => schemas.workflow.validate(workflow)).toThrow()
|
|
20
|
+
})
|
|
21
|
+
})
|
|
@@ -5,6 +5,7 @@ import type { Permissions as ListPermissions } from './List'
|
|
|
5
5
|
import type { Options as StoreOptions } from './Store'
|
|
6
6
|
import type { Options as DeleteOptions } from './Delete'
|
|
7
7
|
import type { Schema } from '@toa.io/schemas'
|
|
8
|
+
import type { Unit } from './workflows'
|
|
8
9
|
|
|
9
10
|
const path = resolve(__dirname, '../../../schemas/octets')
|
|
10
11
|
const namespace = schemas.namespace(path)
|
|
@@ -15,3 +16,4 @@ export const fetch: Schema<FetchPermissions | null> = namespace.schema('fetch')
|
|
|
15
16
|
export const remove: Schema<DeleteOptions | null> = namespace.schema('delete')
|
|
16
17
|
export const list: Schema<ListPermissions | null> = namespace.schema('list')
|
|
17
18
|
export const permute: Schema<null> = namespace.schema('permute')
|
|
19
|
+
export const workflow: Schema<Unit[] | Unit> = namespace.schema('workflow')
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Parameter } from '../../RTD';
|
|
2
|
-
import type { Unit } from './
|
|
2
|
+
import type { Unit } from './workflows';
|
|
3
3
|
import type { Component } from '@toa.io/core';
|
|
4
4
|
import type { Output } from '../../io';
|
|
5
5
|
import type { Directive, Input } from './types';
|
|
@@ -27,7 +27,7 @@ exports.Delete = void 0;
|
|
|
27
27
|
const stream_1 = require("stream");
|
|
28
28
|
const HTTP_1 = require("../../HTTP");
|
|
29
29
|
const schemas = __importStar(require("./schemas"));
|
|
30
|
-
const
|
|
30
|
+
const workflows_1 = require("./workflows");
|
|
31
31
|
class Delete {
|
|
32
32
|
targeted = true;
|
|
33
33
|
workflow;
|
|
@@ -36,7 +36,7 @@ class Delete {
|
|
|
36
36
|
constructor(options, discovery, remotes) {
|
|
37
37
|
schemas.remove.validate(options);
|
|
38
38
|
if (options?.workflow !== undefined)
|
|
39
|
-
this.workflow = new
|
|
39
|
+
this.workflow = new workflows_1.Workflow(options.workflow, remotes);
|
|
40
40
|
this.discovery = discovery;
|
|
41
41
|
}
|
|
42
42
|
async apply(storage, request, parameters) {
|