From 680d8078c4f60bb041a5b5dbb0078aba68bd5ddf Mon Sep 17 00:00:00 2001 From: reluc Date: Tue, 5 Sep 2023 17:03:11 +0200 Subject: [PATCH 1/5] feat(http-binding): add supports for multiple security schemes Note that this means that the server is able to expose Things with different security requirements. For example, it is possibile to now have a Thing with `nosec` security scheme and one with `basic` security scheme. As a side effect, the OAuth example now works as explained in #873. Fix #204 #873 --- examples/security/oauth/consumer.js | 2 +- .../oauth/wot-server-servient-conf.json | 16 +- packages/binding-http/README.md | 14 +- packages/binding-http/src/http-server.ts | 164 +++++++++++------- packages/binding-http/src/http.ts | 2 +- packages/binding-http/src/routes/action.ts | 15 +- packages/binding-http/src/routes/common.ts | 21 ++- packages/binding-http/src/routes/event.ts | 9 +- .../binding-http/src/routes/properties.ts | 11 +- .../src/routes/property-observe.ts | 9 +- packages/binding-http/src/routes/property.ts | 15 +- .../test/http-server-oauth-tests.ts | 4 +- .../binding-http/test/http-server-test.ts | 21 ++- .../bindings/http/example-server-secure.ts | 8 +- .../examples/src/security/oauth/consumer.ts | 2 +- .../oauth/wot-server-servient-conf.json | 16 +- 16 files changed, 210 insertions(+), 119 deletions(-) diff --git a/examples/security/oauth/consumer.js b/examples/security/oauth/consumer.js index 5726a0186..4fffe323d 100644 --- a/examples/security/oauth/consumer.js +++ b/examples/security/oauth/consumer.js @@ -16,7 +16,7 @@ WoTHelpers.fetch("https://localhost:8080/oauth").then((td) => { WoT.consume(td).then(async (thing) => { try { const resp = await thing.invokeAction("sayOk"); - const result = resp === null || resp === void 0 ? void 0 : resp.value(); + const result = await (resp === null || resp === void 0 ? void 0 : resp.value()); console.log("oAuth token was", result); } catch (error) { console.log("It seems that I couldn't access the resource"); diff --git a/examples/security/oauth/wot-server-servient-conf.json b/examples/security/oauth/wot-server-servient-conf.json index 04ca44c57..b34eed731 100644 --- a/examples/security/oauth/wot-server-servient-conf.json +++ b/examples/security/oauth/wot-server-servient-conf.json @@ -4,14 +4,16 @@ "allowSelfSigned": true, "serverKey": "../privatekey.pem", "serverCert": "../certificate.pem", - "security": { - "scheme": "oauth2", - "method": { - "name": "introspection_endpoint", - "endpoint": "https://localhost:3000/introspect", - "allowSelfSigned": true + "security": [ + { + "scheme": "oauth2", + "method": { + "name": "introspection_endpoint", + "endpoint": "https://localhost:3000/introspect", + "allowSelfSigned": true + } } - } + ] }, "credentials": { "urn:dev:wot:oauth:test": { diff --git a/packages/binding-http/README.md b/packages/binding-http/README.md index 6357dbd3b..a3274097c 100644 --- a/packages/binding-http/README.md +++ b/packages/binding-http/README.md @@ -132,9 +132,11 @@ let httpConfig = { allowSelfSigned: true, // client configuration serverKey: "privatekey.pem", serverCert: "certificate.pem", - security: { - scheme: "basic", // (username & password) - }, + security: [ + { + scheme: "basic", // (username & password) + }, + ], }; // add HTTPS binding with configuration servient.addServer(new HttpServer(httpConfig)); @@ -182,7 +184,7 @@ The protocol binding can be configured using his constructor or trough servient allowSelfSigned?: boolean; // Accept self signed certificates serverKey?: string; // HTTPs server secret key file serverCert?: string; // HTTPs server certificate file - security?: TD.SecurityScheme; // Security scheme of the server + security?: TD.SecurityScheme[]; // A list of possible security schemes to be used by things exposed by this servient. baseUri?: string // A Base URI to be used in the TD in cases where the client will access a different URL than the actual machine serving the thing. [See Using BaseUri below] middleware?: MiddlewareRequestHandler; // the MiddlewareRequestHandler function. See [Adding a middleware] section below. } @@ -225,9 +227,9 @@ The http protocol binding supports a set of security protocols that can be enabl allowSelfSigned: true, serverKey: "privatekey.pem", serverCert: "certificate.pem", - security: { + security: [{ scheme: "basic" // (username & password) - } + }] } credentials: { "urn:dev:wot:org:eclipse:thingweb:my-example-secure": { diff --git a/packages/binding-http/src/http-server.ts b/packages/binding-http/src/http-server.ts index b19e95baf..aa3a4541d 100644 --- a/packages/binding-http/src/http-server.ts +++ b/packages/binding-http/src/http-server.ts @@ -66,7 +66,7 @@ export default class HttpServer implements ProtocolServer { private readonly address?: string = undefined; private readonly baseUri?: string = undefined; private readonly urlRewrite?: Record = undefined; - private readonly httpSecurityScheme: string = "NoSec"; // HTTP header compatible string + private readonly supportedSecurityScheme: string[] = ["nosec"]; private readonly validOAuthClients: RegExp = /.*/g; private readonly server: http.Server | https.Server; private readonly middleware: MiddlewareRequestHandler | null = null; @@ -174,30 +174,28 @@ export default class HttpServer implements ProtocolServer { // Auth if (config.security) { - // storing HTTP header compatible string - switch (config.security.scheme) { - case "nosec": - this.httpSecurityScheme = "NoSec"; - break; - case "basic": - this.httpSecurityScheme = "Basic"; - break; - case "digest": - this.httpSecurityScheme = "Digest"; - break; - case "bearer": - this.httpSecurityScheme = "Bearer"; - break; - case "oauth2": - { - this.httpSecurityScheme = "OAuth"; - const oAuthConfig = config.security as OAuth2ServerConfig; - this.validOAuthClients = new RegExp(oAuthConfig.allowedClients ?? ".*"); - this.oAuthValidator = createValidator(oAuthConfig.method); - } - break; - default: - throw new Error(`HttpServer does not support security scheme '${config.security.scheme}`); + if (config.security.length > 1) { + // clear the default + this.supportedSecurityScheme = []; + } + for (const securityScheme of config.security) { + switch (securityScheme.scheme) { + case "nosec": + case "basic": + case "digest": + case "bearer": + break; + case "oauth2": + { + const oAuthConfig = securityScheme as OAuth2ServerConfig; + this.validOAuthClients = new RegExp(oAuthConfig.allowedClients ?? ".*"); + this.oAuthValidator = createValidator(oAuthConfig.method); + } + break; + default: + throw new Error(`HttpServer does not support security scheme '${securityScheme.scheme}`); + } + this.supportedSecurityScheme.push(securityScheme.scheme); } } } @@ -263,10 +261,6 @@ export default class HttpServer implements ProtocolServer { } } - public getHttpSecurityScheme(): string { - return this.httpSecurityScheme; - } - private updateInteractionNameWithUriVariablePattern( interactionName: string, uriVariables: PropertyElement["uriVariables"] = {}, @@ -326,9 +320,11 @@ export default class HttpServer implements ProtocolServer { // media types } // addresses - if (this.scheme === "https") { - this.fillSecurityScheme(thing); + if (this.scheme === "http" && Object.keys(thing.securityDefinitions).length !== 0) { + warn(`HTTP Server will attempt to use your security schemes even if you are not using HTTPS.`); } + + this.fillSecurityScheme(thing); } } } @@ -506,24 +502,25 @@ export default class HttpServer implements ProtocolServer { throw new Error("Servient not set"); } - const creds = this.servient.getCredentials(thing.id); - - switch (this.httpSecurityScheme) { - case "NoSec": + const credentials = this.servient.retrieveCredentials(thing.id); + // Multiple security schemes are deprecated we are not supporting them. We are only supporting one security value. + const selected = Helpers.toStringArray(thing.security)[0]; + const thingSecurityScheme = thing.securityDefinitions[selected]; + debug(`Verifying credentials with security scheme '${thingSecurityScheme.scheme}'`); + switch (thingSecurityScheme.scheme) { + case "nosec": return true; - case "Basic": { + case "basic": { const basic = bauth(req); - const basicCreds = creds as { username: string; password: string }; - return ( - creds !== undefined && - basic !== undefined && - basic.name === basicCreds.username && - basic.pass === basicCreds.password - ); + if (basic === undefined) return false; + if (!credentials || credentials.length === 0) return false; + + const basicCredentials = credentials as { username: string; password: string }[]; + return basicCredentials.some((cred) => basic.name === cred.username && basic.pass === cred.password); } - case "Digest": + case "digest": return false; - case "OAuth": { + case "oauth2": { const oAuthScheme = thing.securityDefinitions[thing.security[0] as string] as OAuth2SecurityScheme; // TODO: Support security schemes defined at affordance level @@ -549,8 +546,12 @@ export default class HttpServer implements ProtocolServer { if (req.headers.authorization === undefined) return false; // TODO proper token evaluation const auth = req.headers.authorization.split(" "); - const bearerCredentials = creds as { token: string }; - return auth[0] === "Bearer" && creds !== undefined && auth[1] === bearerCredentials.token; + + if (auth.length !== 2 || auth[0] !== "Bearer") return false; + if (!credentials || credentials.length === 0) return false; + + const bearerCredentials = credentials as { token: string }[]; + return bearerCredentials.some((cred) => cred.token === auth[1]); } default: return false; @@ -558,22 +559,66 @@ export default class HttpServer implements ProtocolServer { } private fillSecurityScheme(thing: ExposedThing) { + // User selected one security scheme + if (thing.security) { + // multiple security schemes are deprecated we are not supporting them + const securityScheme = Helpers.toStringArray(thing.security)[0]; + const secCandidate = Object.keys(thing.securityDefinitions).find((key) => { + return key === securityScheme; + }); + + if (!secCandidate) { + throw new Error( + "Security scheme not found in thing security definitions. Thing security definitions: " + + Object.keys(thing.securityDefinitions).join(", ") + ); + } + + const isSupported = this.supportedSecurityScheme.find((supportedScheme) => { + const thingScheme = thing.securityDefinitions[secCandidate].scheme; + return thingScheme === supportedScheme.toLocaleLowerCase(); + }); + + if (!isSupported) { + throw new Error( + "Servient does not support thing security schemes. Current scheme supported: " + + this.supportedSecurityScheme.join(", ") + ); + } + // We don't need to do anything else, the user has selected one supported security scheme. + return; + } + + // The user let the servient choose the security scheme + if (!thing.securityDefinitions || Object.keys(thing.securityDefinitions).length === 0) { + // We are using the first supported security scheme as default + thing.securityDefinitions = { + [this.supportedSecurityScheme[0]]: { scheme: this.supportedSecurityScheme[0] }, + }; + thing.security = [this.supportedSecurityScheme[0]]; + return; + } + if (thing.securityDefinitions) { + // User provided a bunch of security schemes but no thing.security + // we select one for him. We select the first supported scheme. const secCandidate = Object.keys(thing.securityDefinitions).find((key) => { - let scheme = thing.securityDefinitions[key].scheme as string; + let scheme = thing.securityDefinitions[key].scheme; // HTTP Authentication Scheme for OAuth does not contain the version number // see https://www.iana.org/assignments/http-authschemes/http-authschemes.xhtml // remove version number for oauth2 schemes scheme = scheme === "oauth2" ? scheme.split("2")[0] : scheme; - return scheme === this.httpSecurityScheme.toLowerCase(); + return this.supportedSecurityScheme.includes(scheme.toLocaleLowerCase()); }); if (!secCandidate) { throw new Error( - "Servient does not support thing security schemes. Current scheme supported: " + - this.httpSecurityScheme + - " secCandidate " + - Object.keys(thing.securityDefinitions).join(", ") + "Servient does not support any of thing security schemes. Current scheme supported: " + + this.supportedSecurityScheme.join(",") + + " thing security schemes: " + + Object.values(thing.securityDefinitions) + .map((schemeDef) => schemeDef.scheme) + .join(", ") ); } @@ -582,11 +627,6 @@ export default class HttpServer implements ProtocolServer { thing.securityDefinitions[secCandidate] = selectedSecurityScheme; thing.security = [secCandidate]; - } else { - thing.securityDefinitions = { - noSec: { scheme: "nosec" }, - }; - thing.security = ["noSec"]; } } @@ -606,14 +646,6 @@ export default class HttpServer implements ProtocolServer { ); }); - // Set CORS headers - if (this.httpSecurityScheme !== "NoSec" && req.headers.origin) { - res.setHeader("Access-Control-Allow-Origin", req.headers.origin); - res.setHeader("Access-Control-Allow-Credentials", "true"); - } else { - res.setHeader("Access-Control-Allow-Origin", "*"); - } - const contentTypeHeader = req.headers["content-type"]; let contentType: string = Array.isArray(contentTypeHeader) ? contentTypeHeader[0] : contentTypeHeader; diff --git a/packages/binding-http/src/http.ts b/packages/binding-http/src/http.ts index 260716ce0..f1300573f 100644 --- a/packages/binding-http/src/http.ts +++ b/packages/binding-http/src/http.ts @@ -44,7 +44,7 @@ export interface HttpConfig { allowSelfSigned?: boolean; serverKey?: string; serverCert?: string; - security?: TD.SecurityScheme; + security?: TD.SecurityScheme[]; middleware?: MiddlewareRequestHandler; } diff --git a/packages/binding-http/src/routes/action.ts b/packages/binding-http/src/routes/action.ts index 5112e3b99..82a93c07b 100644 --- a/packages/binding-http/src/routes/action.ts +++ b/packages/binding-http/src/routes/action.ts @@ -14,7 +14,13 @@ ********************************************************************************/ import { IncomingMessage, ServerResponse } from "http"; import { Content, Helpers, ProtocolHelpers, createLoggers } from "@node-wot/core"; -import { isEmpty, respondUnallowedMethod, validOrDefaultRequestContentType } from "./common"; +import { + isEmpty, + respondUnallowedMethod, + securitySchemeToHTTPHeader, + setCORSForThing, + validOrDefaultRequestContentType, +} from "./common"; import HttpServer from "../http-server"; const { error, warn } = createLoggers("binding-http", "routes", "action"); @@ -56,12 +62,15 @@ export default async function actionRoute( return; } // TODO: refactor this part to move into a common place + setCORSForThing(req, res, thing); let corsPreflightWithCredentials = false; - if (this.getHttpSecurityScheme() !== "NoSec" && !(await this.checkCredentials(thing, req))) { + const securityScheme = thing.securityDefinitions[Helpers.toStringArray(thing.security)[0]].scheme; + + if (securityScheme !== "nosec" && !(await this.checkCredentials(thing, req))) { if (req.method === "OPTIONS" && req.headers.origin) { corsPreflightWithCredentials = true; } else { - res.setHeader("WWW-Authenticate", `${this.getHttpSecurityScheme()} realm="${thing.id}"`); + res.setHeader("WWW-Authenticate", `${securitySchemeToHTTPHeader(securityScheme)} realm="${thing.id}"`); res.writeHead(401); res.end(); return; diff --git a/packages/binding-http/src/routes/common.ts b/packages/binding-http/src/routes/common.ts index d7012347c..cef63fdf5 100644 --- a/packages/binding-http/src/routes/common.ts +++ b/packages/binding-http/src/routes/common.ts @@ -12,7 +12,7 @@ * * SPDX-License-Identifier: EPL-2.0 OR W3C-20150513 ********************************************************************************/ -import { ContentSerdes, Helpers, createLoggers } from "@node-wot/core"; +import { ContentSerdes, ExposedThing, Helpers, createLoggers } from "@node-wot/core"; import { IncomingMessage, ServerResponse } from "http"; const { debug, warn } = createLoggers("binding-http", "routes", "common"); @@ -79,3 +79,22 @@ export function isEmpty(obj: Record): boolean { } return true; } + +export function securitySchemeToHTTPHeader(scheme: string): string { + const [first, ...rest] = scheme; + // HTTP Authentication Scheme for OAuth does not contain the version number + // see https://www.iana.org/assignments/http-authschemes/http-authschemes.xhtml + if (scheme === "oauth2") return "OAuth"; + return first.toUpperCase() + rest.join("").toLowerCase(); +} + +export function setCORSForThing(req: IncomingMessage, res: ServerResponse, thing: ExposedThing): void { + const securityScheme = thing.securityDefinitions[Helpers.toStringArray(thing.security)[0]].scheme; + // Set CORS headers + if (securityScheme !== "nosec" && req.headers.origin) { + res.setHeader("Access-Control-Allow-Origin", req.headers.origin); + res.setHeader("Access-Control-Allow-Credentials", "true"); + } else { + res.setHeader("Access-Control-Allow-Origin", "*"); + } +} diff --git a/packages/binding-http/src/routes/event.ts b/packages/binding-http/src/routes/event.ts index 6996f143b..ba4e545da 100644 --- a/packages/binding-http/src/routes/event.ts +++ b/packages/binding-http/src/routes/event.ts @@ -14,7 +14,7 @@ ********************************************************************************/ import { IncomingMessage, ServerResponse } from "http"; import { Content, Helpers, ProtocolHelpers, createLoggers } from "@node-wot/core"; -import { isEmpty, respondUnallowedMethod } from "./common"; +import { isEmpty, respondUnallowedMethod, securitySchemeToHTTPHeader, setCORSForThing } from "./common"; import HttpServer from "../http-server"; const { warn, debug } = createLoggers("binding-http", "routes", "event"); @@ -42,12 +42,15 @@ export default async function eventRoute( return; } // TODO: refactor this part to move into a common place + setCORSForThing(req, res, thing); let corsPreflightWithCredentials = false; - if (this.getHttpSecurityScheme() !== "NoSec" && !(await this.checkCredentials(thing, req))) { + const securityScheme = thing.securityDefinitions[Helpers.toStringArray(thing.security)[0]].scheme; + + if (securityScheme !== "nosec" && !(await this.checkCredentials(thing, req))) { if (req.method === "OPTIONS" && req.headers.origin) { corsPreflightWithCredentials = true; } else { - res.setHeader("WWW-Authenticate", `${this.getHttpSecurityScheme()} realm="${thing.id}"`); + res.setHeader("WWW-Authenticate", `${securitySchemeToHTTPHeader(securityScheme)} realm="${thing.id}"`); res.writeHead(401); res.end(); return; diff --git a/packages/binding-http/src/routes/properties.ts b/packages/binding-http/src/routes/properties.ts index a7b0d6a84..3595f6e91 100644 --- a/packages/binding-http/src/routes/properties.ts +++ b/packages/binding-http/src/routes/properties.ts @@ -13,8 +13,8 @@ * SPDX-License-Identifier: EPL-2.0 OR W3C-20150513 ********************************************************************************/ import { IncomingMessage, ServerResponse } from "http"; -import { ContentSerdes, PropertyContentMap, createLoggers } from "@node-wot/core"; -import { respondUnallowedMethod } from "./common"; +import { ContentSerdes, Helpers, PropertyContentMap, createLoggers } from "@node-wot/core"; +import { respondUnallowedMethod, securitySchemeToHTTPHeader, setCORSForThing } from "./common"; import HttpServer from "../http-server"; const { error } = createLoggers("binding-http", "routes", "properties"); @@ -33,12 +33,15 @@ export default async function propertiesRoute( } // TODO: refactor this part to move into a common place + setCORSForThing(req, res, thing); let corsPreflightWithCredentials = false; - if (this.getHttpSecurityScheme() !== "NoSec" && !(await this.checkCredentials(thing, req))) { + const securityScheme = thing.securityDefinitions[Helpers.toStringArray(thing.security)[0]].scheme; + + if (securityScheme !== "nosec" && !(await this.checkCredentials(thing, req))) { if (req.method === "OPTIONS" && req.headers.origin) { corsPreflightWithCredentials = true; } else { - res.setHeader("WWW-Authenticate", `${this.getHttpSecurityScheme()} realm="${thing.id}"`); + res.setHeader("WWW-Authenticate", `${securitySchemeToHTTPHeader(securityScheme)} realm="${thing.id}"`); res.writeHead(401); res.end(); return; diff --git a/packages/binding-http/src/routes/property-observe.ts b/packages/binding-http/src/routes/property-observe.ts index d1b8ddfcd..f902d41e0 100644 --- a/packages/binding-http/src/routes/property-observe.ts +++ b/packages/binding-http/src/routes/property-observe.ts @@ -14,7 +14,7 @@ ********************************************************************************/ import { IncomingMessage, ServerResponse } from "http"; import { Content, Helpers, ProtocolHelpers, createLoggers } from "@node-wot/core"; -import { isEmpty, respondUnallowedMethod } from "./common"; +import { isEmpty, respondUnallowedMethod, securitySchemeToHTTPHeader, setCORSForThing } from "./common"; import HttpServer from "../http-server"; const { debug, warn } = createLoggers("binding-http", "routes", "property", "observe"); @@ -51,12 +51,15 @@ export default async function propertyObserveRoute( } // TODO: refactor this part to move into a common place + setCORSForThing(req, res, thing); let corsPreflightWithCredentials = false; - if (this.getHttpSecurityScheme() !== "NoSec" && !(await this.checkCredentials(thing, req))) { + const securityScheme = thing.securityDefinitions[Helpers.toStringArray(thing.security)[0]].scheme; + + if (securityScheme !== "nosec" && !(await this.checkCredentials(thing, req))) { if (req.method === "OPTIONS" && req.headers.origin) { corsPreflightWithCredentials = true; } else { - res.setHeader("WWW-Authenticate", `${this.getHttpSecurityScheme()} realm="${thing.id}"`); + res.setHeader("WWW-Authenticate", `${securitySchemeToHTTPHeader(securityScheme)} realm="${thing.id}"`); res.writeHead(401); res.end(); return; diff --git a/packages/binding-http/src/routes/property.ts b/packages/binding-http/src/routes/property.ts index 47cc72b7e..ad4ffe1ac 100644 --- a/packages/binding-http/src/routes/property.ts +++ b/packages/binding-http/src/routes/property.ts @@ -14,7 +14,13 @@ ********************************************************************************/ import { IncomingMessage, ServerResponse } from "http"; import { Content, Helpers, ProtocolHelpers, createLoggers } from "@node-wot/core"; -import { isEmpty, respondUnallowedMethod, validOrDefaultRequestContentType } from "./common"; +import { + isEmpty, + respondUnallowedMethod, + securitySchemeToHTTPHeader, + setCORSForThing, + validOrDefaultRequestContentType, +} from "./common"; import HttpServer from "../http-server"; const { error, warn } = createLoggers("binding-http", "routes", "property"); @@ -65,12 +71,15 @@ export default async function propertyRoute( } // TODO: refactor this part to move into a common place + setCORSForThing(req, res, thing); let corsPreflightWithCredentials = false; - if (this.getHttpSecurityScheme() !== "NoSec" && !(await this.checkCredentials(thing, req))) { + const securityScheme = thing.securityDefinitions[Helpers.toStringArray(thing.security)[0]].scheme; + + if (securityScheme !== "nosec" && !(await this.checkCredentials(thing, req))) { if (req.method === "OPTIONS" && req.headers.origin) { corsPreflightWithCredentials = true; } else { - res.setHeader("WWW-Authenticate", `${this.getHttpSecurityScheme()} realm="${thing.id}"`); + res.setHeader("WWW-Authenticate", `${securitySchemeToHTTPHeader(securityScheme)} realm="${thing.id}"`); res.writeHead(401); res.end(); return; diff --git a/packages/binding-http/test/http-server-oauth-tests.ts b/packages/binding-http/test/http-server-oauth-tests.ts index d61a89bbc..b9baae5a3 100644 --- a/packages/binding-http/test/http-server-oauth-tests.ts +++ b/packages/binding-http/test/http-server-oauth-tests.ts @@ -42,7 +42,7 @@ class OAuthServerTests { method: method, }; this.server = new HttpServer({ - security: authConfig, + security: [authConfig], }); await this.server.start(new MockServient()); @@ -79,7 +79,7 @@ class OAuthServerTests { @test async "should configure oauth"() { /* eslint-disable dot-notation */ - this.server["httpSecurityScheme"].should.be.equal("OAuth"); + this.server["supportedSecurityScheme"].should.contain("oauth2"); expect(this.server["oAuthValidator"]).to.be.instanceOf(EndpointValidator); /* eslint-enable dot-notation */ } diff --git a/packages/binding-http/test/http-server-test.ts b/packages/binding-http/test/http-server-test.ts index afd275b0d..79bc59fd3 100644 --- a/packages/binding-http/test/http-server-test.ts +++ b/packages/binding-http/test/http-server-test.ts @@ -411,11 +411,12 @@ class HttpServerTest { // https://github.com/eclipse-thingweb/node-wot/issues/181 @test async "should start and stop a server with no security"() { - const httpServer = new HttpServer({ port, security: { scheme: "nosec" } }); + const httpServer = new HttpServer({ port, security: [{ scheme: "nosec" }] }); await httpServer.start(new Servient()); expect(httpServer.getPort()).to.eq(port); // port test - expect(httpServer.getHttpSecurityScheme()).to.eq("NoSec"); // HTTP security scheme test (nosec -> NoSec) + // eslint-disable-next-line dot-notation + expect(httpServer["supportedSecurityScheme"][0]).to.eq("nosec"); await httpServer.stop(); } @@ -425,9 +426,11 @@ class HttpServerTest { port: port2, serverKey: "./test/server.key", serverCert: "./test/server.cert", - security: { - scheme: "bearer", - }, + security: [ + { + scheme: "bearer", + }, + ], }); await httpServer.start(new Servient()); const testThing = new ExposedThing(new Servient()); @@ -449,9 +452,11 @@ class HttpServerTest { port: port2, serverKey: "./test/server.key", serverCert: "./test/server.cert", - security: { - scheme: "bearer", - }, + security: [ + { + scheme: "bearer", + }, + ], }); await httpServer.start(new Servient()); diff --git a/packages/examples/src/bindings/http/example-server-secure.ts b/packages/examples/src/bindings/http/example-server-secure.ts index c12e20feb..ee69cd675 100644 --- a/packages/examples/src/bindings/http/example-server-secure.ts +++ b/packages/examples/src/bindings/http/example-server-secure.ts @@ -29,9 +29,11 @@ const httpConfig = { allowSelfSigned: true, // client configuration serverKey: "privatekey.pem", serverCert: "certificate.pem", - security: { - scheme: "basic", // (username & password) - }, + security: [ + { + scheme: "basic", // (username & password) + }, + ], }; // add HTTPS binding with configuration servient.addServer(new HttpServer(httpConfig)); diff --git a/packages/examples/src/security/oauth/consumer.ts b/packages/examples/src/security/oauth/consumer.ts index ccb16fb29..8b18127bb 100644 --- a/packages/examples/src/security/oauth/consumer.ts +++ b/packages/examples/src/security/oauth/consumer.ts @@ -21,7 +21,7 @@ WoTHelpers.fetch("https://localhost:8080/oauth").then((td) => { WoT.consume(td as ThingDescription).then(async (thing) => { try { const resp = await thing.invokeAction("sayOk"); - const result = resp?.value(); + const result = await resp?.value(); console.log("oAuth token was", result); } catch (error) { console.log("It seems that I couldn't access the resource"); diff --git a/packages/examples/src/security/oauth/wot-server-servient-conf.json b/packages/examples/src/security/oauth/wot-server-servient-conf.json index 04ca44c57..b34eed731 100644 --- a/packages/examples/src/security/oauth/wot-server-servient-conf.json +++ b/packages/examples/src/security/oauth/wot-server-servient-conf.json @@ -4,14 +4,16 @@ "allowSelfSigned": true, "serverKey": "../privatekey.pem", "serverCert": "../certificate.pem", - "security": { - "scheme": "oauth2", - "method": { - "name": "introspection_endpoint", - "endpoint": "https://localhost:3000/introspect", - "allowSelfSigned": true + "security": [ + { + "scheme": "oauth2", + "method": { + "name": "introspection_endpoint", + "endpoint": "https://localhost:3000/introspect", + "allowSelfSigned": true + } } - } + ] }, "credentials": { "urn:dev:wot:oauth:test": { From 33897149843e99183b7003cfe3e72412c96eaffb Mon Sep 17 00:00:00 2001 From: reluc Date: Wed, 6 Sep 2023 18:36:45 +0200 Subject: [PATCH 2/5] refactor(binding-http/http-server): rename supportedSecuritySchemes --- packages/binding-http/src/http-server.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/binding-http/src/http-server.ts b/packages/binding-http/src/http-server.ts index aa3a4541d..772925cb6 100644 --- a/packages/binding-http/src/http-server.ts +++ b/packages/binding-http/src/http-server.ts @@ -66,7 +66,7 @@ export default class HttpServer implements ProtocolServer { private readonly address?: string = undefined; private readonly baseUri?: string = undefined; private readonly urlRewrite?: Record = undefined; - private readonly supportedSecurityScheme: string[] = ["nosec"]; + private readonly supportedSecuritySchemes: string[] = ["nosec"]; private readonly validOAuthClients: RegExp = /.*/g; private readonly server: http.Server | https.Server; private readonly middleware: MiddlewareRequestHandler | null = null; @@ -176,7 +176,7 @@ export default class HttpServer implements ProtocolServer { if (config.security) { if (config.security.length > 1) { // clear the default - this.supportedSecurityScheme = []; + this.supportedSecuritySchemes = []; } for (const securityScheme of config.security) { switch (securityScheme.scheme) { @@ -195,7 +195,7 @@ export default class HttpServer implements ProtocolServer { default: throw new Error(`HttpServer does not support security scheme '${securityScheme.scheme}`); } - this.supportedSecurityScheme.push(securityScheme.scheme); + this.supportedSecuritySchemes.push(securityScheme.scheme); } } } @@ -574,7 +574,7 @@ export default class HttpServer implements ProtocolServer { ); } - const isSupported = this.supportedSecurityScheme.find((supportedScheme) => { + const isSupported = this.supportedSecuritySchemes.find((supportedScheme) => { const thingScheme = thing.securityDefinitions[secCandidate].scheme; return thingScheme === supportedScheme.toLocaleLowerCase(); }); @@ -582,7 +582,7 @@ export default class HttpServer implements ProtocolServer { if (!isSupported) { throw new Error( "Servient does not support thing security schemes. Current scheme supported: " + - this.supportedSecurityScheme.join(", ") + this.supportedSecuritySchemes.join(", ") ); } // We don't need to do anything else, the user has selected one supported security scheme. @@ -593,9 +593,9 @@ export default class HttpServer implements ProtocolServer { if (!thing.securityDefinitions || Object.keys(thing.securityDefinitions).length === 0) { // We are using the first supported security scheme as default thing.securityDefinitions = { - [this.supportedSecurityScheme[0]]: { scheme: this.supportedSecurityScheme[0] }, + [this.supportedSecuritySchemes[0]]: { scheme: this.supportedSecuritySchemes[0] }, }; - thing.security = [this.supportedSecurityScheme[0]]; + thing.security = [this.supportedSecuritySchemes[0]]; return; } @@ -608,13 +608,13 @@ export default class HttpServer implements ProtocolServer { // see https://www.iana.org/assignments/http-authschemes/http-authschemes.xhtml // remove version number for oauth2 schemes scheme = scheme === "oauth2" ? scheme.split("2")[0] : scheme; - return this.supportedSecurityScheme.includes(scheme.toLocaleLowerCase()); + return this.supportedSecuritySchemes.includes(scheme.toLocaleLowerCase()); }); if (!secCandidate) { throw new Error( "Servient does not support any of thing security schemes. Current scheme supported: " + - this.supportedSecurityScheme.join(",") + + this.supportedSecuritySchemes.join(",") + " thing security schemes: " + Object.values(thing.securityDefinitions) .map((schemeDef) => schemeDef.scheme) From 47554e5e9ac059b8f8d8718f664a3786535654f2 Mon Sep 17 00:00:00 2001 From: reluc Date: Wed, 6 Sep 2023 18:37:14 +0200 Subject: [PATCH 3/5] refactor(binding-http/routes/common): rename utility functions --- packages/binding-http/src/routes/action.ts | 8 ++++---- packages/binding-http/src/routes/common.ts | 4 ++-- packages/binding-http/src/routes/event.ts | 6 +++--- packages/binding-http/src/routes/properties.ts | 6 +++--- packages/binding-http/src/routes/property-observe.ts | 6 +++--- packages/binding-http/src/routes/property.ts | 8 ++++---- packages/binding-http/test/http-server-oauth-tests.ts | 2 +- packages/binding-http/test/http-server-test.ts | 2 +- 8 files changed, 21 insertions(+), 21 deletions(-) diff --git a/packages/binding-http/src/routes/action.ts b/packages/binding-http/src/routes/action.ts index 82a93c07b..ae5ab7696 100644 --- a/packages/binding-http/src/routes/action.ts +++ b/packages/binding-http/src/routes/action.ts @@ -17,8 +17,8 @@ import { Content, Helpers, ProtocolHelpers, createLoggers } from "@node-wot/core import { isEmpty, respondUnallowedMethod, - securitySchemeToHTTPHeader, - setCORSForThing, + securitySchemeToHttpHeader, + setCorsForThing, validOrDefaultRequestContentType, } from "./common"; import HttpServer from "../http-server"; @@ -62,7 +62,7 @@ export default async function actionRoute( return; } // TODO: refactor this part to move into a common place - setCORSForThing(req, res, thing); + setCorsForThing(req, res, thing); let corsPreflightWithCredentials = false; const securityScheme = thing.securityDefinitions[Helpers.toStringArray(thing.security)[0]].scheme; @@ -70,7 +70,7 @@ export default async function actionRoute( if (req.method === "OPTIONS" && req.headers.origin) { corsPreflightWithCredentials = true; } else { - res.setHeader("WWW-Authenticate", `${securitySchemeToHTTPHeader(securityScheme)} realm="${thing.id}"`); + res.setHeader("WWW-Authenticate", `${securitySchemeToHttpHeader(securityScheme)} realm="${thing.id}"`); res.writeHead(401); res.end(); return; diff --git a/packages/binding-http/src/routes/common.ts b/packages/binding-http/src/routes/common.ts index cef63fdf5..5a13c5bf5 100644 --- a/packages/binding-http/src/routes/common.ts +++ b/packages/binding-http/src/routes/common.ts @@ -80,7 +80,7 @@ export function isEmpty(obj: Record): boolean { return true; } -export function securitySchemeToHTTPHeader(scheme: string): string { +export function securitySchemeToHttpHeader(scheme: string): string { const [first, ...rest] = scheme; // HTTP Authentication Scheme for OAuth does not contain the version number // see https://www.iana.org/assignments/http-authschemes/http-authschemes.xhtml @@ -88,7 +88,7 @@ export function securitySchemeToHTTPHeader(scheme: string): string { return first.toUpperCase() + rest.join("").toLowerCase(); } -export function setCORSForThing(req: IncomingMessage, res: ServerResponse, thing: ExposedThing): void { +export function setCorsForThing(req: IncomingMessage, res: ServerResponse, thing: ExposedThing): void { const securityScheme = thing.securityDefinitions[Helpers.toStringArray(thing.security)[0]].scheme; // Set CORS headers if (securityScheme !== "nosec" && req.headers.origin) { diff --git a/packages/binding-http/src/routes/event.ts b/packages/binding-http/src/routes/event.ts index ba4e545da..393e10e67 100644 --- a/packages/binding-http/src/routes/event.ts +++ b/packages/binding-http/src/routes/event.ts @@ -14,7 +14,7 @@ ********************************************************************************/ import { IncomingMessage, ServerResponse } from "http"; import { Content, Helpers, ProtocolHelpers, createLoggers } from "@node-wot/core"; -import { isEmpty, respondUnallowedMethod, securitySchemeToHTTPHeader, setCORSForThing } from "./common"; +import { isEmpty, respondUnallowedMethod, securitySchemeToHttpHeader, setCorsForThing } from "./common"; import HttpServer from "../http-server"; const { warn, debug } = createLoggers("binding-http", "routes", "event"); @@ -42,7 +42,7 @@ export default async function eventRoute( return; } // TODO: refactor this part to move into a common place - setCORSForThing(req, res, thing); + setCorsForThing(req, res, thing); let corsPreflightWithCredentials = false; const securityScheme = thing.securityDefinitions[Helpers.toStringArray(thing.security)[0]].scheme; @@ -50,7 +50,7 @@ export default async function eventRoute( if (req.method === "OPTIONS" && req.headers.origin) { corsPreflightWithCredentials = true; } else { - res.setHeader("WWW-Authenticate", `${securitySchemeToHTTPHeader(securityScheme)} realm="${thing.id}"`); + res.setHeader("WWW-Authenticate", `${securitySchemeToHttpHeader(securityScheme)} realm="${thing.id}"`); res.writeHead(401); res.end(); return; diff --git a/packages/binding-http/src/routes/properties.ts b/packages/binding-http/src/routes/properties.ts index 3595f6e91..c1ddd2e2c 100644 --- a/packages/binding-http/src/routes/properties.ts +++ b/packages/binding-http/src/routes/properties.ts @@ -14,7 +14,7 @@ ********************************************************************************/ import { IncomingMessage, ServerResponse } from "http"; import { ContentSerdes, Helpers, PropertyContentMap, createLoggers } from "@node-wot/core"; -import { respondUnallowedMethod, securitySchemeToHTTPHeader, setCORSForThing } from "./common"; +import { respondUnallowedMethod, securitySchemeToHttpHeader, setCorsForThing } from "./common"; import HttpServer from "../http-server"; const { error } = createLoggers("binding-http", "routes", "properties"); @@ -33,7 +33,7 @@ export default async function propertiesRoute( } // TODO: refactor this part to move into a common place - setCORSForThing(req, res, thing); + setCorsForThing(req, res, thing); let corsPreflightWithCredentials = false; const securityScheme = thing.securityDefinitions[Helpers.toStringArray(thing.security)[0]].scheme; @@ -41,7 +41,7 @@ export default async function propertiesRoute( if (req.method === "OPTIONS" && req.headers.origin) { corsPreflightWithCredentials = true; } else { - res.setHeader("WWW-Authenticate", `${securitySchemeToHTTPHeader(securityScheme)} realm="${thing.id}"`); + res.setHeader("WWW-Authenticate", `${securitySchemeToHttpHeader(securityScheme)} realm="${thing.id}"`); res.writeHead(401); res.end(); return; diff --git a/packages/binding-http/src/routes/property-observe.ts b/packages/binding-http/src/routes/property-observe.ts index f902d41e0..99587ff41 100644 --- a/packages/binding-http/src/routes/property-observe.ts +++ b/packages/binding-http/src/routes/property-observe.ts @@ -14,7 +14,7 @@ ********************************************************************************/ import { IncomingMessage, ServerResponse } from "http"; import { Content, Helpers, ProtocolHelpers, createLoggers } from "@node-wot/core"; -import { isEmpty, respondUnallowedMethod, securitySchemeToHTTPHeader, setCORSForThing } from "./common"; +import { isEmpty, respondUnallowedMethod, securitySchemeToHttpHeader, setCorsForThing } from "./common"; import HttpServer from "../http-server"; const { debug, warn } = createLoggers("binding-http", "routes", "property", "observe"); @@ -51,7 +51,7 @@ export default async function propertyObserveRoute( } // TODO: refactor this part to move into a common place - setCORSForThing(req, res, thing); + setCorsForThing(req, res, thing); let corsPreflightWithCredentials = false; const securityScheme = thing.securityDefinitions[Helpers.toStringArray(thing.security)[0]].scheme; @@ -59,7 +59,7 @@ export default async function propertyObserveRoute( if (req.method === "OPTIONS" && req.headers.origin) { corsPreflightWithCredentials = true; } else { - res.setHeader("WWW-Authenticate", `${securitySchemeToHTTPHeader(securityScheme)} realm="${thing.id}"`); + res.setHeader("WWW-Authenticate", `${securitySchemeToHttpHeader(securityScheme)} realm="${thing.id}"`); res.writeHead(401); res.end(); return; diff --git a/packages/binding-http/src/routes/property.ts b/packages/binding-http/src/routes/property.ts index ad4ffe1ac..07830fc19 100644 --- a/packages/binding-http/src/routes/property.ts +++ b/packages/binding-http/src/routes/property.ts @@ -17,8 +17,8 @@ import { Content, Helpers, ProtocolHelpers, createLoggers } from "@node-wot/core import { isEmpty, respondUnallowedMethod, - securitySchemeToHTTPHeader, - setCORSForThing, + securitySchemeToHttpHeader, + setCorsForThing, validOrDefaultRequestContentType, } from "./common"; import HttpServer from "../http-server"; @@ -71,7 +71,7 @@ export default async function propertyRoute( } // TODO: refactor this part to move into a common place - setCORSForThing(req, res, thing); + setCorsForThing(req, res, thing); let corsPreflightWithCredentials = false; const securityScheme = thing.securityDefinitions[Helpers.toStringArray(thing.security)[0]].scheme; @@ -79,7 +79,7 @@ export default async function propertyRoute( if (req.method === "OPTIONS" && req.headers.origin) { corsPreflightWithCredentials = true; } else { - res.setHeader("WWW-Authenticate", `${securitySchemeToHTTPHeader(securityScheme)} realm="${thing.id}"`); + res.setHeader("WWW-Authenticate", `${securitySchemeToHttpHeader(securityScheme)} realm="${thing.id}"`); res.writeHead(401); res.end(); return; diff --git a/packages/binding-http/test/http-server-oauth-tests.ts b/packages/binding-http/test/http-server-oauth-tests.ts index b9baae5a3..4ab4f2131 100644 --- a/packages/binding-http/test/http-server-oauth-tests.ts +++ b/packages/binding-http/test/http-server-oauth-tests.ts @@ -79,7 +79,7 @@ class OAuthServerTests { @test async "should configure oauth"() { /* eslint-disable dot-notation */ - this.server["supportedSecurityScheme"].should.contain("oauth2"); + this.server["supportedSecuritySchemes"].should.contain("oauth2"); expect(this.server["oAuthValidator"]).to.be.instanceOf(EndpointValidator); /* eslint-enable dot-notation */ } diff --git a/packages/binding-http/test/http-server-test.ts b/packages/binding-http/test/http-server-test.ts index 79bc59fd3..144df2e4b 100644 --- a/packages/binding-http/test/http-server-test.ts +++ b/packages/binding-http/test/http-server-test.ts @@ -416,7 +416,7 @@ class HttpServerTest { await httpServer.start(new Servient()); expect(httpServer.getPort()).to.eq(port); // port test // eslint-disable-next-line dot-notation - expect(httpServer["supportedSecurityScheme"][0]).to.eq("nosec"); + expect(httpServer["supportedSecuritySchemes"][0]).to.eq("nosec"); await httpServer.stop(); } From bd3e055c86ce0eb763e80ad86b952fe6b4570156 Mon Sep 17 00:00:00 2001 From: reluc Date: Wed, 6 Sep 2023 18:55:30 +0200 Subject: [PATCH 4/5] fix(binding-http/routers): handle cors for no-thing paths --- examples/browser/counter.html | 2 +- packages/binding-http/src/routes/thing-description.ts | 2 +- packages/binding-http/src/routes/things.ts | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/examples/browser/counter.html b/examples/browser/counter.html index f5f4006d9..163c2d451 100644 --- a/examples/browser/counter.html +++ b/examples/browser/counter.html @@ -44,7 +44,7 @@

Counter Client Example in the Browser

readonly="readonly" id="td_addr" type="url" - value="http://plugfest.thingweb.io:8083/counter" + value="http://localhost:8080/counter" /> diff --git a/packages/binding-http/src/routes/thing-description.ts b/packages/binding-http/src/routes/thing-description.ts index 136787595..e600b5fd9 100644 --- a/packages/binding-http/src/routes/thing-description.ts +++ b/packages/binding-http/src/routes/thing-description.ts @@ -165,7 +165,7 @@ export default async function thingDescriptionRoute( const payload = await content.toBuffer(); negotiateLanguage(td, thing, req); - + res.setHeader("Access-Control-Allow-Origin", "*"); res.setHeader("Content-Type", contentType); res.writeHead(200); debug(`Sending HTTP response for TD with Content-Type ${contentType}.`); diff --git a/packages/binding-http/src/routes/things.ts b/packages/binding-http/src/routes/things.ts index 0a5464e97..93d4aa162 100644 --- a/packages/binding-http/src/routes/things.ts +++ b/packages/binding-http/src/routes/things.ts @@ -22,6 +22,7 @@ export default function thingsRoute( res: ServerResponse, _params: unknown ): void { + res.setHeader("Access-Control-Allow-Origin", "*"); res.setHeader("Content-Type", ContentSerdes.DEFAULT); res.writeHead(200); const list = []; From 26bf72ed4bb76afaed39e58c0a1a32697087cba5 Mon Sep 17 00:00:00 2001 From: reluc Date: Thu, 7 Sep 2023 11:48:50 +0200 Subject: [PATCH 5/5] fixup! fix(binding-http/routers): handle cors for no-thing paths --- examples/browser/counter.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/browser/counter.html b/examples/browser/counter.html index 163c2d451..f5f4006d9 100644 --- a/examples/browser/counter.html +++ b/examples/browser/counter.html @@ -44,7 +44,7 @@

Counter Client Example in the Browser

readonly="readonly" id="td_addr" type="url" - value="http://localhost:8080/counter" + value="http://plugfest.thingweb.io:8083/counter" />