soajs.controller 4.3.26 → 4.3.28
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/docs/mcp.md
CHANGED
|
@@ -92,6 +92,9 @@ is yours, named by its endpoints.
|
|
|
92
92
|
- else `byHost`, by the host the request came in on (`X-Forwarded-Host`, else `Host`);
|
|
93
93
|
- else `default`.
|
|
94
94
|
- An entry needs `authorization_endpoint` and `token_endpoint`; `registration_endpoint` is optional.
|
|
95
|
+
- **`<origin>`** below is the scheme and host the request came in on: `X-Forwarded-Proto` and
|
|
96
|
+
`X-Forwarded-Host` when set. Behind a proxy that does not pass the scheme on, set
|
|
97
|
+
`"scheme": "https"` in `auth`; the host still comes from the request.
|
|
95
98
|
- **No token, or an unknown or expired one:** HTTP 401 with
|
|
96
99
|
`WWW-Authenticate: Bearer resource_metadata="<origin>/.well-known/oauth-protected-resource/soajs/mcp"`,
|
|
97
100
|
plus `?key=` when the client's URL had one. If nothing matches the request, the answer is as without
|
|
@@ -16,7 +16,8 @@
|
|
|
16
16
|
* at the endpoints the configuration names.
|
|
17
17
|
*
|
|
18
18
|
* Which endpoints: by the product of the key on the request (auth.byProduct), else by the host the
|
|
19
|
-
* request came in on (auth.byHost), else auth.default.
|
|
19
|
+
* request came in on (auth.byHost), else auth.default. The addresses the gateway gives out use the
|
|
20
|
+
* request's host and auth.scheme, else the scheme the request came in with.
|
|
20
21
|
*
|
|
21
22
|
* An MCP client sends the token only, no key. A token restricted to one key (user.restrictedTo.key)
|
|
22
23
|
* brings it: /soajs/mcp without a key uses it, and with a key of the same product the token's key
|
|
@@ -61,8 +62,16 @@ function requestHost(req) {
|
|
|
61
62
|
return (firstValue(req.headers["x-forwarded-host"]) || req.headers.host || "").toLowerCase();
|
|
62
63
|
}
|
|
63
64
|
|
|
65
|
+
/**
|
|
66
|
+
* The scheme is auth.scheme when set, for a proxy that does not pass X-Forwarded-Proto on, else
|
|
67
|
+
* X-Forwarded-Proto, else the connection's.
|
|
68
|
+
*/
|
|
64
69
|
function requestOrigin(req) {
|
|
65
|
-
let
|
|
70
|
+
let auth = getAuthConfig(req);
|
|
71
|
+
let proto = (auth && (auth.scheme === "https" || auth.scheme === "http")) ? auth.scheme : null;
|
|
72
|
+
if (!proto) {
|
|
73
|
+
proto = firstValue(req.headers["x-forwarded-proto"]);
|
|
74
|
+
}
|
|
66
75
|
if (!proto) {
|
|
67
76
|
proto = (req.socket && req.socket.encrypted) ? "https" : "http";
|
|
68
77
|
}
|
|
@@ -435,6 +435,18 @@ function callTool(req, configuration, config, entry, args) {
|
|
|
435
435
|
if (!headers.access_token && req.query && req.query.access_token) {
|
|
436
436
|
headers.access_token = req.query.access_token;
|
|
437
437
|
}
|
|
438
|
+
//NOTE: the token goes as access_token, never as Authorization: a service that falls back to the
|
|
439
|
+
// gateway for an interConnect peer passes access_token on, not a lowercase authorization
|
|
440
|
+
// header, and oauth refuses a call that carries both
|
|
441
|
+
if (!headers.access_token && headers.authorization) {
|
|
442
|
+
let matches = headers.authorization.match(/^Bearer\s+(\S+)/i);
|
|
443
|
+
if (matches) {
|
|
444
|
+
headers.access_token = matches[1];
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
if (headers.access_token) {
|
|
448
|
+
delete headers.authorization;
|
|
449
|
+
}
|
|
438
450
|
let clientIP = req.getClientIP ? req.getClientIP() : null;
|
|
439
451
|
let forwarded = req.headers["x-forwarded-for"];
|
|
440
452
|
if (clientIP) {
|
package/package.json
CHANGED
|
@@ -411,6 +411,23 @@ describe("Unit test for: mw - soajsRoutes mcp", () => {
|
|
|
411
411
|
|
|
412
412
|
let entry = {service: "urac", version: "2", tool: coreTools[1]};
|
|
413
413
|
|
|
414
|
+
it("a bearer token is passed on as access_token, not as authorization", (done) => {
|
|
415
|
+
reply = {status: 200, body: {result: true, data: {}}};
|
|
416
|
+
let req = makeReq({gatewayPort: gatewayPort, headers: {"authorization": "Bearer agentToken"}});
|
|
417
|
+
tools.callTool(req, configuration, config, entry, {id: "u1", name: "Ann"}).then(() => {
|
|
418
|
+
assert.strictEqual(received.headers.access_token, "agentToken");
|
|
419
|
+
assert.ok(!received.headers.authorization);
|
|
420
|
+
reply = {status: 200, body: {result: true, data: {}}};
|
|
421
|
+
let req2 = makeReq({gatewayPort: gatewayPort, headers: {"authorization": "Basic xyz"}});
|
|
422
|
+
return tools.callTool(req2, configuration, config, entry, {id: "u1", name: "Ann"});
|
|
423
|
+
}).then(() => {
|
|
424
|
+
// not a bearer token: left as it is
|
|
425
|
+
assert.strictEqual(received.headers.authorization, "Basic xyz");
|
|
426
|
+
assert.ok(!received.headers.access_token);
|
|
427
|
+
done();
|
|
428
|
+
}).catch(done);
|
|
429
|
+
});
|
|
430
|
+
|
|
414
431
|
it("the api call goes through the gateway port with the caller's credentials", (done) => {
|
|
415
432
|
reply = {status: 200, body: {result: true, data: {updated: true}}};
|
|
416
433
|
let req = makeReq({
|
|
@@ -423,7 +440,8 @@ describe("Unit test for: mw - soajsRoutes mcp", () => {
|
|
|
423
440
|
assert.strictEqual(received.url, "/urac/v2/user/u1");
|
|
424
441
|
assert.deepStrictEqual(JSON.parse(received.body), {profile: {name: "Ann"}});
|
|
425
442
|
assert.strictEqual(received.headers.key, "extKey");
|
|
426
|
-
|
|
443
|
+
// one token only, as access_token
|
|
444
|
+
assert.ok(!received.headers.authorization);
|
|
427
445
|
assert.strictEqual(received.headers.access_token, "tok");
|
|
428
446
|
assert.strictEqual(received.headers["device-id"], "d1");
|
|
429
447
|
// the mcp request's own key is not forwarded, a write gets a new one
|
|
@@ -276,5 +276,27 @@ describe("Unit test for: mw - soajsRoutes mcp auth", () => {
|
|
|
276
276
|
req.socket = {encrypted: false};
|
|
277
277
|
assert.strictEqual(auth.requestOrigin(req), "http://api.connect.example");
|
|
278
278
|
});
|
|
279
|
+
it("requestOrigin: auth.scheme wins over the forwarded proto, the host stays", () => {
|
|
280
|
+
let req = makeReq({headers: {"x-forwarded-proto": "http"}, mcp: {enabled: true, auth: Object.assign({scheme: "https"}, AUTH)}});
|
|
281
|
+
assert.strictEqual(auth.requestOrigin(req), "https://api.connect.example");
|
|
282
|
+
req = makeReq({headers: {"x-forwarded-proto": "http"}, mcp: {enabled: true, auth: Object.assign({scheme: "ftp"}, AUTH)}});
|
|
283
|
+
assert.strictEqual(auth.requestOrigin(req), "http://api.connect.example");
|
|
284
|
+
});
|
|
285
|
+
it("auth.scheme in the metadata and the 401", (done) => {
|
|
286
|
+
let mcp = {enabled: true, auth: Object.assign({scheme: "https"}, AUTH)};
|
|
287
|
+
let mw = auth.wellKnown({provision: provision});
|
|
288
|
+
run(mw, makeReq({method: "GET", url: "/.well-known/oauth-protected-resource", mcp: mcp, headers: {"x-forwarded-proto": "http"}}), (answer) => {
|
|
289
|
+
assert.strictEqual(answer.body.resource, "https://api.connect.example/soajs/mcp");
|
|
290
|
+
assert.deepStrictEqual(answer.body.authorization_servers, ["https://api.connect.example/soajs/mcp/auth/default"]);
|
|
291
|
+
run(mw, makeReq({method: "GET", url: "/.well-known/oauth-authorization-server/soajs/mcp/auth/default", mcp: mcp, headers: {"x-forwarded-proto": "http"}}), (answer) => {
|
|
292
|
+
assert.strictEqual(answer.body.issuer, "https://api.connect.example/soajs/mcp/auth/default");
|
|
293
|
+
run(auth.authorize({provision: provision}), makeReq({mcp: mcp, headers: {"x-forwarded-proto": "http"}}), (answer) => {
|
|
294
|
+
assert.strictEqual(answer.headers["WWW-Authenticate"],
|
|
295
|
+
'Bearer resource_metadata="https://api.connect.example/.well-known/oauth-protected-resource/soajs/mcp"');
|
|
296
|
+
done();
|
|
297
|
+
});
|
|
298
|
+
});
|
|
299
|
+
});
|
|
300
|
+
});
|
|
279
301
|
});
|
|
280
302
|
});
|