@digitalbazaar/ezcap-express 4.1.1 → 4.4.0
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/CHANGELOG.md +45 -0
- package/lib/authorize.js +49 -181
- package/lib/helpers.js +225 -0
- package/lib/main.js +1 -0
- package/lib/revoke.js +189 -0
- package/package.json +18 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,50 @@
|
|
|
1
1
|
# @digitalbazaar/ezcap-express Changelog
|
|
2
2
|
|
|
3
|
+
## 4.4.0 - 2021-12-15
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- Add additional tests.
|
|
7
|
+
|
|
8
|
+
## 4.3.1 - 2021-12-13
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
- Fix `expectedAction` to be `write` for `DELETE` method.
|
|
12
|
+
- Throw error when no `expectedAction` is given for a given HTTP method and
|
|
13
|
+
provide defaults for all common HTTP methods.
|
|
14
|
+
|
|
15
|
+
## 4.3.0 - 2021-12-10
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
- Allow any controller in a delegated zcap's chain to revoke it. This authority
|
|
19
|
+
is inherent in delegation and is now reflected in code. This feature gives
|
|
20
|
+
delegators more fine-grained control to revoke zcaps that they did not
|
|
21
|
+
delegate directly but one of their delegates did, allowing them to stop
|
|
22
|
+
specific zcap usage without having to revoke more of the chain. It also
|
|
23
|
+
gives zcap controllers the ability to revoke their own zcaps (if desired)
|
|
24
|
+
and adds a sanity check to prevent the revocation of root zcaps that use
|
|
25
|
+
the `urn:zcap:root:` ID scheme.
|
|
26
|
+
|
|
27
|
+
## 4.2.0 - 2021-08-26
|
|
28
|
+
|
|
29
|
+
### Added
|
|
30
|
+
- Add `suiteFactory` parameter to middleware creation functions. A
|
|
31
|
+
`suiteFactory` function should be passed and return the supported LD proof
|
|
32
|
+
suite (or an array of supported LD proof suites) that is supported for
|
|
33
|
+
authorizing zcap invocations and verifying capability chains.
|
|
34
|
+
- Add `authorizeZcapRevocation` middleware that can be attached to root
|
|
35
|
+
container/object endpoints to enable revocation of zcaps that have been
|
|
36
|
+
delegated to use them. This version assumes that the revocations endpoint
|
|
37
|
+
will follow this RESTful format: `<rootObjectUrl>/revocations/<zcapId>`
|
|
38
|
+
and that the body will be JSON and include a `capability` member with
|
|
39
|
+
the zcap to revoke. Future versions may allow for greater flexibility.
|
|
40
|
+
|
|
41
|
+
### Changed
|
|
42
|
+
- Deprecate passing a `suite` to any middleware creation functions. Instead,
|
|
43
|
+
`suiteFactory` should be passed. The next major version will remove `suite`.
|
|
44
|
+
This approach allows this library to remove npm dependencies that provide
|
|
45
|
+
cryptographic suites preventing this library from being affected when those
|
|
46
|
+
dependencies need to change.
|
|
47
|
+
|
|
3
48
|
## 4.1.1 - 2021-07-21
|
|
4
49
|
|
|
5
50
|
### Changed
|
package/lib/authorize.js
CHANGED
|
@@ -4,13 +4,9 @@
|
|
|
4
4
|
import assert from 'assert-plus';
|
|
5
5
|
import asyncHandler from 'express-async-handler';
|
|
6
6
|
import {Ed25519Signature2020} from '@digitalbazaar/ed25519-signature-2020';
|
|
7
|
-
import
|
|
8
|
-
import {verifyHeaderValue} from '@digitalbazaar/http-digest-header';
|
|
9
|
-
import * as sec from 'security-context';
|
|
7
|
+
import * as helpers from './helpers.js';
|
|
10
8
|
import {verifyCapabilityInvocation} from 'http-signature-zcap-verify';
|
|
11
9
|
|
|
12
|
-
const ZCAP_ROOT_PREFIX = 'urn:zcap:root:';
|
|
13
|
-
|
|
14
10
|
/**
|
|
15
11
|
* Authorizes an incoming request.
|
|
16
12
|
*
|
|
@@ -42,129 +38,56 @@ const ZCAP_ROOT_PREFIX = 'urn:zcap:root:';
|
|
|
42
38
|
* inspect a capability chain, e.g., to check for revocations.
|
|
43
39
|
* @param {Function} [options.onError] - An error handler handler for
|
|
44
40
|
* customizable error handling.
|
|
45
|
-
* @param {object} [options.suite] - The expected cryptography suite to use
|
|
46
|
-
* verifying digital signatures
|
|
41
|
+
* @param {object} [options.suite] - The expected cryptography suite to use
|
|
42
|
+
* when verifying digital signatures (deprecated; use `suiteFactory`
|
|
43
|
+
* instead).
|
|
44
|
+
* @param {object} [options.suiteFactory] - A factory for creating the
|
|
45
|
+
* supported suite(s) to use when verifying digital signatures.
|
|
47
46
|
*
|
|
48
|
-
* @returns {Function} Returns an Express.js middleware route handler.
|
|
47
|
+
* @returns {Function} Returns an Express.js style middleware route handler.
|
|
49
48
|
*/
|
|
50
49
|
export function authorizeZcapInvocation({
|
|
51
50
|
documentLoader, expectedHost, getExpectedTarget, getRootController,
|
|
52
51
|
allowTargetAttenuation = true, expectedAction, getExpectedAction,
|
|
53
52
|
getExpectedRootCapabilityId, inspectCapabilityChain,
|
|
54
|
-
onError, suite
|
|
53
|
+
onError, suite, suiteFactory
|
|
55
54
|
} = {}) {
|
|
55
|
+
// `helpers.createExpectationMiddleware` handles type checks on other params
|
|
56
56
|
assert.func(documentLoader, 'options.documentLoader');
|
|
57
|
-
assert.optionalString(expectedAction, 'options.expectedAction');
|
|
58
|
-
assert.optionalFunc(
|
|
59
|
-
getExpectedAction, 'options.getExpectedAction');
|
|
60
|
-
assert.string(expectedHost, 'options.expectedHost');
|
|
61
|
-
assert.func(getExpectedTarget, 'options.getExpectedTarget');
|
|
62
57
|
assert.func(getRootController, 'options.getRootController');
|
|
63
|
-
assert.optionalFunc(
|
|
64
|
-
getExpectedRootCapabilityId, 'options.getExpectedRootCapabilityId');
|
|
65
58
|
assert.optionalFunc(inspectCapabilityChain, 'options.inspectCapabilityChain');
|
|
66
|
-
assert.optionalFunc(
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
59
|
+
assert.optionalFunc(suiteFactory, 'options.suiteFactory');
|
|
60
|
+
|
|
61
|
+
// this code block is to be removed the next major release (5.0); `suite`
|
|
62
|
+
// should be removed as a parameter
|
|
63
|
+
if(!(suite && suiteFactory)) {
|
|
64
|
+
suiteFactory = () => new Ed25519Signature2020();
|
|
65
|
+
} else if(!suiteFactory) {
|
|
66
|
+
// backwards compatibility
|
|
67
|
+
suiteFactory = () => suite;
|
|
70
68
|
}
|
|
71
69
|
|
|
72
|
-
return
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
}
|
|
84
|
-
const {keyId} = params;
|
|
85
|
-
|
|
86
|
-
// if body is present, ensure header digest value matches digest of body
|
|
87
|
-
if(_hasBody({req})) {
|
|
88
|
-
const {digest: expectedDigest} = headers;
|
|
89
|
-
if(!expectedDigest) {
|
|
90
|
-
const error = new Error(
|
|
91
|
-
'A "digest" header must be present when an HTTP body is present.');
|
|
92
|
-
error.name = 'DataError';
|
|
93
|
-
error.httpStatusCode = 400;
|
|
94
|
-
return _handleError({error, onError});
|
|
95
|
-
}
|
|
96
|
-
const {verified} = await verifyHeaderValue({
|
|
97
|
-
data: req.body, headerValue: expectedDigest});
|
|
98
|
-
if(!verified) {
|
|
99
|
-
const error = new Error(
|
|
100
|
-
'The "digest" header value does not match digest of body.');
|
|
101
|
-
error.name = 'DataError';
|
|
102
|
-
error.httpStatusCode = 400;
|
|
103
|
-
return _handleError({error, onError});
|
|
104
|
-
}
|
|
105
|
-
} else {
|
|
106
|
-
// prevent any unhandled `req.body` from being erroneously used
|
|
107
|
-
req.body = undefined;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
// use `getExpectedAction` if provided
|
|
111
|
-
if(getExpectedAction) {
|
|
112
|
-
expectedAction = await getExpectedAction({req});
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
let expectedTarget;
|
|
116
|
-
try {
|
|
117
|
-
// getExpectedTarget may throw an error
|
|
118
|
-
({expectedTarget} = await getExpectedTarget({req}));
|
|
119
|
-
if(!(typeof expectedTarget === 'string' ||
|
|
120
|
-
Array.isArray(expectedTarget))) {
|
|
121
|
-
throw new Error(
|
|
122
|
-
'Return value from "getExpectedTarget" must be an object with ' +
|
|
123
|
-
'"expectedTarget" set to a string or an array.');
|
|
124
|
-
}
|
|
125
|
-
} catch(error) {
|
|
126
|
-
return _handleError({error, onError});
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
let _expectedAction = expectedAction;
|
|
130
|
-
|
|
131
|
-
// set expected action if it has not been specified
|
|
132
|
-
/* Note: This is safe as long as the server's request handling
|
|
133
|
-
infrastructure differentiates based on HTTP method (as is typical practice
|
|
134
|
-
with express/connect routing. So, while the client specifies the HTTP
|
|
135
|
-
method, the server specifies the handler for that HTTP method. For example,
|
|
136
|
-
this middleware will ensure that if a client specifies "POST" then it
|
|
137
|
-
must be invoking a zcap that grants "write" action authority. Then, provided
|
|
138
|
-
that the server's router ensures that only the "POST" handler will be
|
|
139
|
-
executed (typical routing practice), all is well. If the handler code is
|
|
140
|
-
chosen via some other means, e.g., via the request body, then the caller
|
|
141
|
-
MUST provide the expected action and not rely on default behavior. */
|
|
142
|
-
if(_expectedAction === undefined) {
|
|
143
|
-
_expectedAction = 'read';
|
|
144
|
-
if(req.method === 'POST') {
|
|
145
|
-
_expectedAction = 'write';
|
|
146
|
-
}
|
|
147
|
-
}
|
|
70
|
+
return [
|
|
71
|
+
helpers.createExpectationMiddleware({
|
|
72
|
+
expectedHost, expectedAction, getExpectedAction, getExpectedTarget,
|
|
73
|
+
getExpectedRootCapabilityId, onError
|
|
74
|
+
}),
|
|
75
|
+
authorizeZcapInvocationAfterParse({
|
|
76
|
+
documentLoader, getRootController, suiteFactory,
|
|
77
|
+
allowTargetAttenuation, inspectCapabilityChain, onError
|
|
78
|
+
})
|
|
79
|
+
];
|
|
80
|
+
}
|
|
148
81
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
} else if(Array.isArray(expectedTarget)) {
|
|
159
|
-
expectedRootCapability = expectedTarget.map(
|
|
160
|
-
t => `${ZCAP_ROOT_PREFIX}${encodeURIComponent(t)}`);
|
|
161
|
-
} else {
|
|
162
|
-
expectedRootCapability =
|
|
163
|
-
`${ZCAP_ROOT_PREFIX}${encodeURIComponent(expectedTarget)}`;
|
|
164
|
-
}
|
|
165
|
-
} catch(error) {
|
|
166
|
-
return _handleError({error, onError});
|
|
167
|
-
}
|
|
82
|
+
export function authorizeZcapInvocationAfterParse({
|
|
83
|
+
documentLoader, getRootController, suiteFactory,
|
|
84
|
+
allowTargetAttenuation = true, inspectCapabilityChain, onError
|
|
85
|
+
} = {}) {
|
|
86
|
+
return asyncHandler(async (req, res, next) => {
|
|
87
|
+
const {
|
|
88
|
+
expectedAction, expectedHost, expectedRootCapability, expectedTarget,
|
|
89
|
+
signature: {params: {keyId}}
|
|
90
|
+
} = req.ezcap;
|
|
168
91
|
|
|
169
92
|
// retrieves the root capability that was invoked
|
|
170
93
|
async function getInvokedCapability({id}) {
|
|
@@ -182,24 +105,26 @@ export function authorizeZcapInvocation({
|
|
|
182
105
|
actual: id,
|
|
183
106
|
expected: expectedRootCapability,
|
|
184
107
|
};
|
|
185
|
-
return
|
|
108
|
+
return helpers.handleError({error, onError});
|
|
186
109
|
}
|
|
187
|
-
return
|
|
110
|
+
return helpers.getRootCapability({
|
|
188
111
|
getRootController, req, expectedHost, expectedTarget,
|
|
189
|
-
expectedAction
|
|
112
|
+
expectedAction, rootCapabilityId
|
|
190
113
|
});
|
|
191
114
|
}
|
|
192
115
|
|
|
193
|
-
// perform the capability invocation
|
|
116
|
+
// perform the capability invocation...
|
|
117
|
+
// `originalUrl` must be used to support nested express routers
|
|
118
|
+
const {originalUrl: url, method, headers} = req;
|
|
194
119
|
const result = await verifyCapabilityInvocation({
|
|
195
120
|
url,
|
|
196
121
|
method,
|
|
197
|
-
suite,
|
|
122
|
+
suite: await suiteFactory({req}),
|
|
198
123
|
headers,
|
|
199
124
|
expectedHost,
|
|
200
|
-
documentLoader:
|
|
125
|
+
documentLoader: helpers.wrappedDocumentLoader({
|
|
201
126
|
documentLoader,
|
|
202
|
-
expectedAction
|
|
127
|
+
expectedAction,
|
|
203
128
|
expectedHost,
|
|
204
129
|
expectedTarget,
|
|
205
130
|
getRootController,
|
|
@@ -207,7 +132,7 @@ export function authorizeZcapInvocation({
|
|
|
207
132
|
}),
|
|
208
133
|
getInvokedCapability,
|
|
209
134
|
expectedTarget,
|
|
210
|
-
expectedAction
|
|
135
|
+
expectedAction,
|
|
211
136
|
expectedRootCapability,
|
|
212
137
|
inspectCapabilityChain,
|
|
213
138
|
keyId,
|
|
@@ -216,7 +141,7 @@ export function authorizeZcapInvocation({
|
|
|
216
141
|
|
|
217
142
|
// return HTTP 403 if verification fails
|
|
218
143
|
if(!result.verified) {
|
|
219
|
-
|
|
144
|
+
helpers.handleError({error: result.error, onError, throwError: false});
|
|
220
145
|
return res.status(403).send();
|
|
221
146
|
}
|
|
222
147
|
|
|
@@ -229,60 +154,3 @@ export function authorizeZcapInvocation({
|
|
|
229
154
|
process.nextTick(next);
|
|
230
155
|
});
|
|
231
156
|
}
|
|
232
|
-
|
|
233
|
-
function _handleError({error, onError, throwError = true}) {
|
|
234
|
-
if(onError) {
|
|
235
|
-
return onError({error});
|
|
236
|
-
}
|
|
237
|
-
if(throwError) {
|
|
238
|
-
throw error;
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
function _wrappedDocumentLoader({
|
|
243
|
-
req, documentLoader, expectedHost, expectedTarget, expectedAction,
|
|
244
|
-
getRootController
|
|
245
|
-
}) {
|
|
246
|
-
return async url => {
|
|
247
|
-
if(url.startsWith(ZCAP_ROOT_PREFIX)) {
|
|
248
|
-
const document = await _getRootCapability({
|
|
249
|
-
getRootController, req, expectedHost, expectedTarget, expectedAction,
|
|
250
|
-
rootCapabilityId: url
|
|
251
|
-
});
|
|
252
|
-
|
|
253
|
-
return {
|
|
254
|
-
contextUrl: null,
|
|
255
|
-
documentUrl: url,
|
|
256
|
-
document,
|
|
257
|
-
};
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
return documentLoader(url);
|
|
261
|
-
};
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
async function _getRootCapability({
|
|
265
|
-
getRootController, req, expectedHost, expectedTarget, expectedAction,
|
|
266
|
-
rootCapabilityId
|
|
267
|
-
}) {
|
|
268
|
-
const rootInvocationTarget = decodeURIComponent(
|
|
269
|
-
rootCapabilityId.substr(ZCAP_ROOT_PREFIX.length));
|
|
270
|
-
const controller = await getRootController({
|
|
271
|
-
req, expectedHost, expectedTarget, expectedAction,
|
|
272
|
-
rootCapabilityId, rootInvocationTarget
|
|
273
|
-
});
|
|
274
|
-
return {
|
|
275
|
-
'@context': sec.constants.SECURITY_CONTEXT_V2_URL,
|
|
276
|
-
id: rootCapabilityId,
|
|
277
|
-
invocationTarget: rootInvocationTarget,
|
|
278
|
-
controller
|
|
279
|
-
};
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
function _hasBody({req}) {
|
|
283
|
-
// a request has a body if `transfer-encoding` or `content-length` headers
|
|
284
|
-
// are set: http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.3
|
|
285
|
-
return req.body &&
|
|
286
|
-
(req.get('transfer-encoding') !== undefined ||
|
|
287
|
-
req.get('content-length') !== undefined);
|
|
288
|
-
}
|
package/lib/helpers.js
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Copyright (c) 2021 Digital Bazaar, Inc. All rights reserved.
|
|
3
|
+
*/
|
|
4
|
+
import assert from 'assert-plus';
|
|
5
|
+
import asyncHandler from 'express-async-handler';
|
|
6
|
+
import {constants as zCapConstants} from '@digitalbazaar/zcapld';
|
|
7
|
+
import * as helpers from './helpers.js';
|
|
8
|
+
import {parseSignatureHeader} from 'http-signature-header';
|
|
9
|
+
import {verifyHeaderValue} from '@digitalbazaar/http-digest-header';
|
|
10
|
+
|
|
11
|
+
const {ZCAP_CONTEXT_URL} = zCapConstants;
|
|
12
|
+
|
|
13
|
+
const DEFAULT_ACTION_FOR_METHOD = new Map([
|
|
14
|
+
['GET', 'read'],
|
|
15
|
+
['HEAD', 'read'],
|
|
16
|
+
['OPTIONS', 'read'],
|
|
17
|
+
['POST', 'write'],
|
|
18
|
+
['PUT', 'write'],
|
|
19
|
+
['PATCH', 'write'],
|
|
20
|
+
['DELETE', 'write'],
|
|
21
|
+
['CONNECT', 'write'],
|
|
22
|
+
['TRACE', 'write'],
|
|
23
|
+
['PATCH', 'write']
|
|
24
|
+
]);
|
|
25
|
+
|
|
26
|
+
export const ZCAP_ROOT_PREFIX = 'urn:zcap:root:';
|
|
27
|
+
|
|
28
|
+
// middleware used to collect expected values for zcap authorization
|
|
29
|
+
export function createExpectationMiddleware({
|
|
30
|
+
expectedHost, getExpectedTarget,
|
|
31
|
+
expectedAction, getExpectedAction,
|
|
32
|
+
getExpectedRootCapabilityId, onError
|
|
33
|
+
}) {
|
|
34
|
+
assert.string(expectedHost, 'options.expectedHost');
|
|
35
|
+
assert.func(getExpectedTarget, 'options.getExpectedTarget');
|
|
36
|
+
assert.optionalString(expectedAction, 'options.expectedAction');
|
|
37
|
+
assert.optionalFunc(getExpectedAction, 'options.getExpectedAction');
|
|
38
|
+
assert.optionalFunc(
|
|
39
|
+
getExpectedRootCapabilityId, 'options.getExpectedRootCapabilityId');
|
|
40
|
+
assert.optionalFunc(onError, 'options.onError');
|
|
41
|
+
|
|
42
|
+
if(getExpectedAction && expectedAction !== undefined) {
|
|
43
|
+
throw new Error('Use "getExpectedAction" or "expectedAction", not both.');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return asyncHandler(async (req, res, next) => {
|
|
47
|
+
// cache ezcap express info
|
|
48
|
+
req.ezcap = {expectedHost};
|
|
49
|
+
|
|
50
|
+
const {headers} = req;
|
|
51
|
+
try {
|
|
52
|
+
const {params} = parseSignatureHeader(headers.authorization);
|
|
53
|
+
req.ezcap.signature = {params};
|
|
54
|
+
} catch(e) {
|
|
55
|
+
const error = new Error('Missing or invalid "authorization" header.');
|
|
56
|
+
error.name = 'DataError';
|
|
57
|
+
error.cause = e;
|
|
58
|
+
return helpers.handleError({error, onError});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// if body is present, ensure header digest value matches digest of body
|
|
62
|
+
if(helpers.hasBody({req})) {
|
|
63
|
+
const {digest: expectedDigest} = headers;
|
|
64
|
+
if(!expectedDigest) {
|
|
65
|
+
const error = new Error(
|
|
66
|
+
'A "digest" header must be present when an HTTP body is present.');
|
|
67
|
+
error.name = 'DataError';
|
|
68
|
+
error.httpStatusCode = 400;
|
|
69
|
+
return helpers.handleError({error, onError});
|
|
70
|
+
}
|
|
71
|
+
const {verified} = await verifyHeaderValue({
|
|
72
|
+
data: req.body, headerValue: expectedDigest});
|
|
73
|
+
if(!verified) {
|
|
74
|
+
const error = new Error(
|
|
75
|
+
'The "digest" header value does not match digest of body.');
|
|
76
|
+
error.name = 'DataError';
|
|
77
|
+
error.httpStatusCode = 400;
|
|
78
|
+
return helpers.handleError({error, onError});
|
|
79
|
+
}
|
|
80
|
+
} else {
|
|
81
|
+
// prevent any unhandled `req.body` from being erroneously used
|
|
82
|
+
req.body = undefined;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
try {
|
|
86
|
+
// getExpectedTarget may throw an error
|
|
87
|
+
req.ezcap.expectedTarget = await helpers.getExpectedTarget(
|
|
88
|
+
{req, getExpectedTarget});
|
|
89
|
+
} catch(error) {
|
|
90
|
+
return helpers.handleError({error, onError});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// set expected action
|
|
94
|
+
req.ezcap.expectedAction = expectedAction;
|
|
95
|
+
|
|
96
|
+
// use `getExpectedAction` if provided
|
|
97
|
+
if(getExpectedAction) {
|
|
98
|
+
req.ezcap.expectedAction = await getExpectedAction({req});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/* Note: This is safe as long as the server's request handling
|
|
102
|
+
infrastructure differentiates based on HTTP method (as is typical practice
|
|
103
|
+
with express/connect routing). So, while the client specifies the HTTP
|
|
104
|
+
method, the server specifies the handler for that HTTP method. For example,
|
|
105
|
+
this middleware will ensure that if a client specifies "POST" then it
|
|
106
|
+
must be invoking a zcap that grants "write" action authority. Then,
|
|
107
|
+
provided that the server's router ensures that only the "POST" handler will
|
|
108
|
+
be executed (typical routing practice), all is well. If the handler code is
|
|
109
|
+
chosen via some other means, e.g., via the request body, then the caller
|
|
110
|
+
MUST provide the expected action and not rely on default behavior. */
|
|
111
|
+
if(req.ezcap.expectedAction === undefined) {
|
|
112
|
+
req.ezcap.expectedAction = DEFAULT_ACTION_FOR_METHOD.get(req.method);
|
|
113
|
+
if(req.ezcap.expectedAction === undefined) {
|
|
114
|
+
const error = new Error(
|
|
115
|
+
`The HTTP method ${req.method} has no expected capability action.`);
|
|
116
|
+
error.name = 'NotSupportedError';
|
|
117
|
+
error.httpStatusCode = 400;
|
|
118
|
+
return helpers.handleError({error, onError});
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
try {
|
|
123
|
+
req.ezcap.expectedRootCapability = await helpers
|
|
124
|
+
.getExpectedRootCapability({
|
|
125
|
+
req, expectedHost,
|
|
126
|
+
expectedTarget: req.ezcap.expectedTarget,
|
|
127
|
+
expectedAction: req.ezcap.expectedAction,
|
|
128
|
+
getExpectedRootCapabilityId
|
|
129
|
+
});
|
|
130
|
+
} catch(error) {
|
|
131
|
+
return helpers.handleError({error, onError});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// call `next` on the next tick to ensure the promise from this function
|
|
135
|
+
// resolves and does not reject because some subsequent middleware throws
|
|
136
|
+
// an error
|
|
137
|
+
process.nextTick(next);
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function handleError({error, onError, throwError = true}) {
|
|
142
|
+
if(onError) {
|
|
143
|
+
return onError({error});
|
|
144
|
+
}
|
|
145
|
+
if(throwError) {
|
|
146
|
+
throw error;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function wrappedDocumentLoader({
|
|
151
|
+
req, documentLoader, expectedHost, expectedTarget, expectedAction,
|
|
152
|
+
getRootController
|
|
153
|
+
}) {
|
|
154
|
+
return async url => {
|
|
155
|
+
if(url.startsWith(ZCAP_ROOT_PREFIX)) {
|
|
156
|
+
const document = await getRootCapability({
|
|
157
|
+
getRootController, req, expectedHost, expectedTarget, expectedAction,
|
|
158
|
+
rootCapabilityId: url
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
return {
|
|
162
|
+
contextUrl: null,
|
|
163
|
+
documentUrl: url,
|
|
164
|
+
document,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return documentLoader(url);
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export async function getExpectedRootCapability({
|
|
173
|
+
req, expectedHost, expectedTarget, expectedAction,
|
|
174
|
+
getExpectedRootCapabilityId
|
|
175
|
+
}) {
|
|
176
|
+
if(getExpectedRootCapabilityId) {
|
|
177
|
+
// retrieve the root capability given the request and expected params
|
|
178
|
+
// return value can be a string or an array of strings
|
|
179
|
+
return getExpectedRootCapabilityId({
|
|
180
|
+
req, expectedHost, expectedTarget, expectedAction
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
if(Array.isArray(expectedTarget)) {
|
|
184
|
+
return expectedTarget.map(
|
|
185
|
+
t => `${ZCAP_ROOT_PREFIX}${encodeURIComponent(t)}`);
|
|
186
|
+
}
|
|
187
|
+
return `${ZCAP_ROOT_PREFIX}${encodeURIComponent(expectedTarget)}`;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export async function getExpectedTarget({req, getExpectedTarget}) {
|
|
191
|
+
const {expectedTarget} = await getExpectedTarget({req});
|
|
192
|
+
if(!(typeof expectedTarget === 'string' ||
|
|
193
|
+
Array.isArray(expectedTarget))) {
|
|
194
|
+
throw new Error(
|
|
195
|
+
'Return value from "getExpectedTarget" must be an object with ' +
|
|
196
|
+
'"expectedTarget" set to a string or an array.');
|
|
197
|
+
}
|
|
198
|
+
return expectedTarget;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export async function getRootCapability({
|
|
202
|
+
getRootController, req, expectedHost, expectedTarget, expectedAction,
|
|
203
|
+
rootCapabilityId
|
|
204
|
+
}) {
|
|
205
|
+
const rootInvocationTarget = decodeURIComponent(
|
|
206
|
+
rootCapabilityId.substr(ZCAP_ROOT_PREFIX.length));
|
|
207
|
+
const controller = await getRootController({
|
|
208
|
+
req, expectedHost, expectedTarget, expectedAction,
|
|
209
|
+
rootCapabilityId, rootInvocationTarget
|
|
210
|
+
});
|
|
211
|
+
return {
|
|
212
|
+
'@context': ZCAP_CONTEXT_URL,
|
|
213
|
+
id: rootCapabilityId,
|
|
214
|
+
invocationTarget: rootInvocationTarget,
|
|
215
|
+
controller
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export function hasBody({req}) {
|
|
220
|
+
// a request has a body if `transfer-encoding` or `content-length` headers
|
|
221
|
+
// are set: http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.3
|
|
222
|
+
return req.body &&
|
|
223
|
+
(req.get('transfer-encoding') !== undefined ||
|
|
224
|
+
req.get('content-length') !== undefined);
|
|
225
|
+
}
|
package/lib/main.js
CHANGED
package/lib/revoke.js
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Copyright (c) 2021 Digital Bazaar, Inc. All rights reserved.
|
|
3
|
+
*/
|
|
4
|
+
import assert from 'assert-plus';
|
|
5
|
+
import asyncHandler from 'express-async-handler';
|
|
6
|
+
import {CapabilityDelegation} from '@digitalbazaar/zcapld';
|
|
7
|
+
import * as helpers from './helpers.js';
|
|
8
|
+
import * as jsigs from 'jsonld-signatures';
|
|
9
|
+
import {authorizeZcapInvocationAfterParse} from './authorize.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Authorizes a request to submit a zcap revocation.
|
|
13
|
+
*
|
|
14
|
+
* @param {object} options - Options hashmap.
|
|
15
|
+
* @param {object} options.documentLoader - Document loader used to load
|
|
16
|
+
* DID Documents, capability documents, and JSON-LD Contexts.
|
|
17
|
+
* @param {string} options.expectedHost - The expected host for the invoked
|
|
18
|
+
* capability.
|
|
19
|
+
* @param {Function} options.getExpectedTarget - Used to return the expected
|
|
20
|
+
* target(s) for the invoked capability.
|
|
21
|
+
* @param {Function} options.getRootController - Used to get the root
|
|
22
|
+
* capability controller for the given root capability ID.
|
|
23
|
+
* @param {Function} options.suiteFactory - A factory for creating the
|
|
24
|
+
* supported suite(s) to use when verifying digital signatures.
|
|
25
|
+
* @param {boolean} [options.allowTargetAttenuation=true] - Allow the
|
|
26
|
+
* invocationTarget of a delegation chain to be increasingly restrictive
|
|
27
|
+
* based on a hierarchical RESTful URL structure.
|
|
28
|
+
* @param {Function} [options.getExpectedRootCapabilityId] - Used to return the
|
|
29
|
+
* expected root capability identifiers for the expected targets.
|
|
30
|
+
* @param {Function} [options.inspectCapabilityChain] - A function that can
|
|
31
|
+
* inspect a capability chain, e.g., to check for revocations.
|
|
32
|
+
* @param {Function} [options.onError] - An error handler handler for
|
|
33
|
+
* customizable error handling.
|
|
34
|
+
*
|
|
35
|
+
* @returns {Function} Returns an Express.js style middleware route handler.
|
|
36
|
+
*/
|
|
37
|
+
export function authorizeZcapRevocation({
|
|
38
|
+
documentLoader, expectedHost, getExpectedTarget, getRootController,
|
|
39
|
+
allowTargetAttenuation = true, getExpectedRootCapabilityId,
|
|
40
|
+
inspectCapabilityChain, onError, suiteFactory
|
|
41
|
+
}) {
|
|
42
|
+
assert.func(suiteFactory, 'options.suiteFactory');
|
|
43
|
+
|
|
44
|
+
// expected action is always `write` for submitting a revocation
|
|
45
|
+
const expectedAction = 'write';
|
|
46
|
+
return [
|
|
47
|
+
helpers.createExpectationMiddleware({
|
|
48
|
+
expectedHost, expectedAction, getExpectedTarget,
|
|
49
|
+
getExpectedRootCapabilityId, onError
|
|
50
|
+
}),
|
|
51
|
+
verifyCapabilityDelegation({
|
|
52
|
+
documentLoader, getRootController, suiteFactory, inspectCapabilityChain,
|
|
53
|
+
onError
|
|
54
|
+
}),
|
|
55
|
+
authorizeZcapInvocationAfterParse({
|
|
56
|
+
documentLoader, getRootController: _getRevocationRootController,
|
|
57
|
+
suiteFactory, allowTargetAttenuation, inspectCapabilityChain, onError
|
|
58
|
+
})
|
|
59
|
+
];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function verifyCapabilityDelegation({
|
|
63
|
+
documentLoader, getRootController, inspectCapabilityChain, suiteFactory,
|
|
64
|
+
onError
|
|
65
|
+
}) {
|
|
66
|
+
return asyncHandler(async function getDelegator(req, res, next) {
|
|
67
|
+
const {
|
|
68
|
+
expectedAction, expectedHost, expectedTarget, expectedRootCapability
|
|
69
|
+
} = req.ezcap;
|
|
70
|
+
|
|
71
|
+
const {body: capability} = req;
|
|
72
|
+
|
|
73
|
+
// early-disallow revocation of root zcaps that follow ID convention
|
|
74
|
+
if(capability.id.startsWith(helpers.ZCAP_ROOT_PREFIX)) {
|
|
75
|
+
const error = new Error('A root capability cannot be revoked.');
|
|
76
|
+
error.name = 'NotAllowedError';
|
|
77
|
+
return helpers.handleError({error, onError});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// verify CapabilityDelegation
|
|
81
|
+
let delegator;
|
|
82
|
+
const chainControllers = [];
|
|
83
|
+
try {
|
|
84
|
+
const results = await _verifyDelegation({
|
|
85
|
+
capability,
|
|
86
|
+
documentLoader: helpers.wrappedDocumentLoader({
|
|
87
|
+
req, documentLoader, expectedHost, expectedTarget, expectedAction,
|
|
88
|
+
getRootController
|
|
89
|
+
}),
|
|
90
|
+
expectedRootCapability,
|
|
91
|
+
inspectCapabilityChain: _captureChainControllers({
|
|
92
|
+
inspectCapabilityChain,
|
|
93
|
+
chainControllers
|
|
94
|
+
}),
|
|
95
|
+
suiteFactory
|
|
96
|
+
});
|
|
97
|
+
({delegator} = results[0].purposeResult);
|
|
98
|
+
delegator = delegator.id || delegator;
|
|
99
|
+
} catch(e) {
|
|
100
|
+
const error = new Error('The provided capability delegation is invalid.');
|
|
101
|
+
error.name = 'DataError';
|
|
102
|
+
error.cause = e;
|
|
103
|
+
return helpers.handleError({error, onError});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
req.zcapRevocation = {delegator, chainControllers};
|
|
107
|
+
|
|
108
|
+
// proceed to next middleware on next tick to prevent subsequent
|
|
109
|
+
// middleware from potentially throwing here
|
|
110
|
+
process.nextTick(next);
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function _verifyDelegation({
|
|
115
|
+
req, capability, documentLoader,
|
|
116
|
+
expectedRootCapability, inspectCapabilityChain, suiteFactory
|
|
117
|
+
}) {
|
|
118
|
+
const {verified, error, results} = await jsigs.verify(capability, {
|
|
119
|
+
suite: await suiteFactory({req}),
|
|
120
|
+
purpose: new CapabilityDelegation({
|
|
121
|
+
allowTargetAttenuation: true,
|
|
122
|
+
expectedRootCapability,
|
|
123
|
+
inspectCapabilityChain,
|
|
124
|
+
suite: await suiteFactory({req})
|
|
125
|
+
}),
|
|
126
|
+
documentLoader
|
|
127
|
+
});
|
|
128
|
+
if(!verified) {
|
|
129
|
+
throw error;
|
|
130
|
+
}
|
|
131
|
+
return results;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async function _getRevocationRootController({
|
|
135
|
+
req, rootCapabilityId, rootInvocationTarget, getRootController,
|
|
136
|
+
revocationsSubPath = '/revocations/'
|
|
137
|
+
}) {
|
|
138
|
+
// if `revocations` is not in the root invocation target, then defer to
|
|
139
|
+
// `getRootController` to try and provide the root controller
|
|
140
|
+
if(!rootInvocationTarget.includes(revocationsSubPath)) {
|
|
141
|
+
return getRootController({req, rootCapabilityId, rootInvocationTarget});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/* Note: If the invocation target is a zcap-specific revocation endpoint,
|
|
145
|
+
we use all zcap controllers from the submitted zcap's chain as the root
|
|
146
|
+
controller value for the target.
|
|
147
|
+
|
|
148
|
+
This approach allows any party that has delegated a zcap or received one
|
|
149
|
+
to be able to send it for revocation. Subsequent code (in the revocation
|
|
150
|
+
route handler) will confirm that the delegation is proper and the zcap from
|
|
151
|
+
which it was delegated has not itself been revoked.
|
|
152
|
+
|
|
153
|
+
To be clear, if the delegation chain is:
|
|
154
|
+
|
|
155
|
+
root -> A -> B
|
|
156
|
+
|
|
157
|
+
Any zcap controller in the chain of B may invoke a root zcap with a
|
|
158
|
+
`target` of `<baseUrl>/revocations/<ID of B>` (and an ID of
|
|
159
|
+
`urn:zcap:root:encodeURIComponent(<baseUrl>/revocations/<ID of B>)`). This
|
|
160
|
+
means that `root`, `A`, or `B` may revoke `B`.
|
|
161
|
+
|
|
162
|
+
As long no other zcap in the chain of `B` (e.g., `A`) has already been
|
|
163
|
+
revoked, then `B` will be revoked and stored as a revocation until `B`
|
|
164
|
+
expires. */
|
|
165
|
+
|
|
166
|
+
// use all `chainControllers`
|
|
167
|
+
// presumes `verifyCapabilityDelegation` middleware already called
|
|
168
|
+
return req.zcapRevocation.chainControllers;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function _captureChainControllers({inspectCapabilityChain, chainControllers}) {
|
|
172
|
+
return async function _inspectCapabilityChain(chainDetails) {
|
|
173
|
+
// collect every controller in the chain
|
|
174
|
+
const {capabilityChain} = chainDetails;
|
|
175
|
+
for(const capability of capabilityChain.values()) {
|
|
176
|
+
chainControllers.push(..._getCapabilityControllers({capability}));
|
|
177
|
+
}
|
|
178
|
+
return inspectCapabilityChain(chainDetails);
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function _getCapabilityControllers({capability}) {
|
|
183
|
+
const {controller, id} = capability;
|
|
184
|
+
const result = controller || id;
|
|
185
|
+
if(!result) {
|
|
186
|
+
return [];
|
|
187
|
+
}
|
|
188
|
+
return Array.isArray(result) ? result : [result];
|
|
189
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@digitalbazaar/ezcap-express",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.4.0",
|
|
4
4
|
"main": "lib",
|
|
5
5
|
"module": "main.js",
|
|
6
6
|
"repository": {
|
|
@@ -16,26 +16,40 @@
|
|
|
16
16
|
"generate-readme": "jsdoc2md -t readme-template.hbs lib/*.js > README.md",
|
|
17
17
|
"lint": "eslint .",
|
|
18
18
|
"test": "npm run test-node",
|
|
19
|
-
"test-node": "cross-env NODE_ENV=test mocha -r esm --preserve-symlinks -t 30000 -A -R ${REPORTER:-spec} --require tests/test-mocha.js tests/*.spec.js"
|
|
19
|
+
"test-node": "cross-env NODE_ENV=test mocha -r esm --preserve-symlinks -t 30000 -A -R ${REPORTER:-spec} --require tests/test-mocha.js tests/*.spec.js",
|
|
20
|
+
"coverage": "cross-env NODE_ENV=test ESM_OPTIONS='{cache:false}' nyc --reporter=lcov --reporter=text-summary npm test",
|
|
21
|
+
"coverage-ci": "cross-env NODE_ENV=test ESM_OPTIONS='{cache:false}' nyc --reporter=lcovonly npm test",
|
|
22
|
+
"coverage-report": "nyc report"
|
|
20
23
|
},
|
|
21
24
|
"dependencies": {
|
|
22
25
|
"@digitalbazaar/ed25519-signature-2020": "^3.0.0",
|
|
23
26
|
"@digitalbazaar/http-digest-header": "^1.0.1",
|
|
27
|
+
"@digitalbazaar/zcapld": "^5.1.2",
|
|
24
28
|
"assert-plus": "^1.0.0",
|
|
25
29
|
"esm": "^3.2.25",
|
|
26
30
|
"express-async-handler": "^1.1.4",
|
|
27
31
|
"http-signature-header": "^2.0.2",
|
|
28
32
|
"http-signature-zcap-verify": "^8.1.1",
|
|
29
|
-
"
|
|
33
|
+
"jsonld-signatures": "^9.3.0"
|
|
30
34
|
},
|
|
31
35
|
"devDependencies": {
|
|
36
|
+
"@digitalbazaar/did-method-key": "^2.0.0",
|
|
37
|
+
"@digitalbazaar/ezcap": "^1.0.0",
|
|
38
|
+
"@digitalbazaar/http-client": "^2.0.1",
|
|
39
|
+
"@digitalbazaar/security-document-loader": "^1.1.1",
|
|
40
|
+
"bnid": "^2.1.0",
|
|
32
41
|
"chai": "^4.2.0",
|
|
42
|
+
"chai-http": "^4.3.0",
|
|
33
43
|
"cross-env": "^7.0.2",
|
|
34
44
|
"eslint": "^7.30.0",
|
|
35
45
|
"eslint-config-digitalbazaar": "^2.6.1",
|
|
36
46
|
"eslint-plugin-jsdoc": "^32.2.0",
|
|
47
|
+
"express": "^4.17.1",
|
|
48
|
+
"http-signature-zcap-invoke": "^3.1.0",
|
|
37
49
|
"jsdoc-to-markdown": "^6.0.1",
|
|
38
|
-
"mocha": "^8.1.3"
|
|
50
|
+
"mocha": "^8.1.3",
|
|
51
|
+
"nyc": "^15.1.0",
|
|
52
|
+
"zcap-context": "^1.2.1"
|
|
39
53
|
},
|
|
40
54
|
"engines": {
|
|
41
55
|
"node": ">=14.0.0"
|