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 proto = firstValue(req.headers["x-forwarded-proto"]);
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
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "soajs.controller",
3
3
  "description": "soajs multi tenant API gateway",
4
- "version": "4.3.26",
4
+ "version": "4.3.28",
5
5
  "author": {
6
6
  "name": "soajs team",
7
7
  "email": "team@soajs.org"
@@ -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
- assert.strictEqual(received.headers.authorization, "Bearer abc");
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
  });