Skip to content

Commit c3c468f

Browse files
authored
Add supports for multiple security schemes in http server (#1070)
* 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 * refactor(binding-http/http-server): rename supportedSecuritySchemes * refactor(binding-http/routes/common): rename utility functions * fix(binding-http/routers): handle cors for no-thing paths * fixup! fix(binding-http/routers): handle cors for no-thing paths
1 parent 559978e commit c3c468f

File tree

18 files changed

+212
-120
lines changed

18 files changed

+212
-120
lines changed

examples/security/oauth/consumer.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ WoTHelpers.fetch("https://localhost:8080/oauth").then((td) => {
1616
WoT.consume(td).then(async (thing) => {
1717
try {
1818
const resp = await thing.invokeAction("sayOk");
19-
const result = resp === null || resp === void 0 ? void 0 : resp.value();
19+
const result = await (resp === null || resp === void 0 ? void 0 : resp.value());
2020
console.log("oAuth token was", result);
2121
} catch (error) {
2222
console.log("It seems that I couldn't access the resource");

examples/security/oauth/wot-server-servient-conf.json

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,16 @@
44
"allowSelfSigned": true,
55
"serverKey": "../privatekey.pem",
66
"serverCert": "../certificate.pem",
7-
"security": {
8-
"scheme": "oauth2",
9-
"method": {
10-
"name": "introspection_endpoint",
11-
"endpoint": "https://localhost:3000/introspect",
12-
"allowSelfSigned": true
7+
"security": [
8+
{
9+
"scheme": "oauth2",
10+
"method": {
11+
"name": "introspection_endpoint",
12+
"endpoint": "https://localhost:3000/introspect",
13+
"allowSelfSigned": true
14+
}
1315
}
14-
}
16+
]
1517
},
1618
"credentials": {
1719
"urn:dev:wot:oauth:test": {

packages/binding-http/README.md

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -132,9 +132,11 @@ let httpConfig = {
132132
allowSelfSigned: true, // client configuration
133133
serverKey: "privatekey.pem",
134134
serverCert: "certificate.pem",
135-
security: {
136-
scheme: "basic", // (username & password)
137-
},
135+
security: [
136+
{
137+
scheme: "basic", // (username & password)
138+
},
139+
],
138140
};
139141
// add HTTPS binding with configuration
140142
servient.addServer(new HttpServer(httpConfig));
@@ -182,7 +184,7 @@ The protocol binding can be configured using his constructor or trough servient
182184
allowSelfSigned?: boolean; // Accept self signed certificates
183185
serverKey?: string; // HTTPs server secret key file
184186
serverCert?: string; // HTTPs server certificate file
185-
security?: TD.SecurityScheme; // Security scheme of the server
187+
security?: TD.SecurityScheme[]; // A list of possible security schemes to be used by things exposed by this servient.
186188
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]
187189
middleware?: MiddlewareRequestHandler; // the MiddlewareRequestHandler function. See [Adding a middleware] section below.
188190
}
@@ -225,9 +227,9 @@ The http protocol binding supports a set of security protocols that can be enabl
225227
allowSelfSigned: true,
226228
serverKey: "privatekey.pem",
227229
serverCert: "certificate.pem",
228-
security: {
230+
security: [{
229231
scheme: "basic" // (username & password)
230-
}
232+
}]
231233
}
232234
credentials: {
233235
"urn:dev:wot:org:eclipse:thingweb:my-example-secure": {

packages/binding-http/src/http-server.ts

Lines changed: 98 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ export default class HttpServer implements ProtocolServer {
6666
private readonly address?: string = undefined;
6767
private readonly baseUri?: string = undefined;
6868
private readonly urlRewrite?: Record<string, string> = undefined;
69-
private readonly httpSecurityScheme: string = "NoSec"; // HTTP header compatible string
69+
private readonly supportedSecuritySchemes: string[] = ["nosec"];
7070
private readonly validOAuthClients: RegExp = /.*/g;
7171
private readonly server: http.Server | https.Server;
7272
private readonly middleware: MiddlewareRequestHandler | null = null;
@@ -174,30 +174,28 @@ export default class HttpServer implements ProtocolServer {
174174

175175
// Auth
176176
if (config.security) {
177-
// storing HTTP header compatible string
178-
switch (config.security.scheme) {
179-
case "nosec":
180-
this.httpSecurityScheme = "NoSec";
181-
break;
182-
case "basic":
183-
this.httpSecurityScheme = "Basic";
184-
break;
185-
case "digest":
186-
this.httpSecurityScheme = "Digest";
187-
break;
188-
case "bearer":
189-
this.httpSecurityScheme = "Bearer";
190-
break;
191-
case "oauth2":
192-
{
193-
this.httpSecurityScheme = "OAuth";
194-
const oAuthConfig = config.security as OAuth2ServerConfig;
195-
this.validOAuthClients = new RegExp(oAuthConfig.allowedClients ?? ".*");
196-
this.oAuthValidator = createValidator(oAuthConfig.method);
197-
}
198-
break;
199-
default:
200-
throw new Error(`HttpServer does not support security scheme '${config.security.scheme}`);
177+
if (config.security.length > 1) {
178+
// clear the default
179+
this.supportedSecuritySchemes = [];
180+
}
181+
for (const securityScheme of config.security) {
182+
switch (securityScheme.scheme) {
183+
case "nosec":
184+
case "basic":
185+
case "digest":
186+
case "bearer":
187+
break;
188+
case "oauth2":
189+
{
190+
const oAuthConfig = securityScheme as OAuth2ServerConfig;
191+
this.validOAuthClients = new RegExp(oAuthConfig.allowedClients ?? ".*");
192+
this.oAuthValidator = createValidator(oAuthConfig.method);
193+
}
194+
break;
195+
default:
196+
throw new Error(`HttpServer does not support security scheme '${securityScheme.scheme}`);
197+
}
198+
this.supportedSecuritySchemes.push(securityScheme.scheme);
201199
}
202200
}
203201
}
@@ -263,10 +261,6 @@ export default class HttpServer implements ProtocolServer {
263261
}
264262
}
265263

266-
public getHttpSecurityScheme(): string {
267-
return this.httpSecurityScheme;
268-
}
269-
270264
private updateInteractionNameWithUriVariablePattern(
271265
interactionName: string,
272266
uriVariables: PropertyElement["uriVariables"] = {},
@@ -326,9 +320,11 @@ export default class HttpServer implements ProtocolServer {
326320
// media types
327321
} // addresses
328322

329-
if (this.scheme === "https") {
330-
this.fillSecurityScheme(thing);
323+
if (this.scheme === "http" && Object.keys(thing.securityDefinitions).length !== 0) {
324+
warn(`HTTP Server will attempt to use your security schemes even if you are not using HTTPS.`);
331325
}
326+
327+
this.fillSecurityScheme(thing);
332328
}
333329
}
334330
}
@@ -506,24 +502,25 @@ export default class HttpServer implements ProtocolServer {
506502
throw new Error("Servient not set");
507503
}
508504

509-
const creds = this.servient.getCredentials(thing.id);
510-
511-
switch (this.httpSecurityScheme) {
512-
case "NoSec":
505+
const credentials = this.servient.retrieveCredentials(thing.id);
506+
// Multiple security schemes are deprecated we are not supporting them. We are only supporting one security value.
507+
const selected = Helpers.toStringArray(thing.security)[0];
508+
const thingSecurityScheme = thing.securityDefinitions[selected];
509+
debug(`Verifying credentials with security scheme '${thingSecurityScheme.scheme}'`);
510+
switch (thingSecurityScheme.scheme) {
511+
case "nosec":
513512
return true;
514-
case "Basic": {
513+
case "basic": {
515514
const basic = bauth(req);
516-
const basicCreds = creds as { username: string; password: string };
517-
return (
518-
creds !== undefined &&
519-
basic !== undefined &&
520-
basic.name === basicCreds.username &&
521-
basic.pass === basicCreds.password
522-
);
515+
if (basic === undefined) return false;
516+
if (!credentials || credentials.length === 0) return false;
517+
518+
const basicCredentials = credentials as { username: string; password: string }[];
519+
return basicCredentials.some((cred) => basic.name === cred.username && basic.pass === cred.password);
523520
}
524-
case "Digest":
521+
case "digest":
525522
return false;
526-
case "OAuth": {
523+
case "oauth2": {
527524
const oAuthScheme = thing.securityDefinitions[thing.security[0] as string] as OAuth2SecurityScheme;
528525

529526
// TODO: Support security schemes defined at affordance level
@@ -549,31 +546,79 @@ export default class HttpServer implements ProtocolServer {
549546
if (req.headers.authorization === undefined) return false;
550547
// TODO proper token evaluation
551548
const auth = req.headers.authorization.split(" ");
552-
const bearerCredentials = creds as { token: string };
553-
return auth[0] === "Bearer" && creds !== undefined && auth[1] === bearerCredentials.token;
549+
550+
if (auth.length !== 2 || auth[0] !== "Bearer") return false;
551+
if (!credentials || credentials.length === 0) return false;
552+
553+
const bearerCredentials = credentials as { token: string }[];
554+
return bearerCredentials.some((cred) => cred.token === auth[1]);
554555
}
555556
default:
556557
return false;
557558
}
558559
}
559560

560561
private fillSecurityScheme(thing: ExposedThing) {
562+
// User selected one security scheme
563+
if (thing.security) {
564+
// multiple security schemes are deprecated we are not supporting them
565+
const securityScheme = Helpers.toStringArray(thing.security)[0];
566+
const secCandidate = Object.keys(thing.securityDefinitions).find((key) => {
567+
return key === securityScheme;
568+
});
569+
570+
if (!secCandidate) {
571+
throw new Error(
572+
"Security scheme not found in thing security definitions. Thing security definitions: " +
573+
Object.keys(thing.securityDefinitions).join(", ")
574+
);
575+
}
576+
577+
const isSupported = this.supportedSecuritySchemes.find((supportedScheme) => {
578+
const thingScheme = thing.securityDefinitions[secCandidate].scheme;
579+
return thingScheme === supportedScheme.toLocaleLowerCase();
580+
});
581+
582+
if (!isSupported) {
583+
throw new Error(
584+
"Servient does not support thing security schemes. Current scheme supported: " +
585+
this.supportedSecuritySchemes.join(", ")
586+
);
587+
}
588+
// We don't need to do anything else, the user has selected one supported security scheme.
589+
return;
590+
}
591+
592+
// The user let the servient choose the security scheme
593+
if (!thing.securityDefinitions || Object.keys(thing.securityDefinitions).length === 0) {
594+
// We are using the first supported security scheme as default
595+
thing.securityDefinitions = {
596+
[this.supportedSecuritySchemes[0]]: { scheme: this.supportedSecuritySchemes[0] },
597+
};
598+
thing.security = [this.supportedSecuritySchemes[0]];
599+
return;
600+
}
601+
561602
if (thing.securityDefinitions) {
603+
// User provided a bunch of security schemes but no thing.security
604+
// we select one for him. We select the first supported scheme.
562605
const secCandidate = Object.keys(thing.securityDefinitions).find((key) => {
563-
let scheme = thing.securityDefinitions[key].scheme as string;
606+
let scheme = thing.securityDefinitions[key].scheme;
564607
// HTTP Authentication Scheme for OAuth does not contain the version number
565608
// see https://www.iana.org/assignments/http-authschemes/http-authschemes.xhtml
566609
// remove version number for oauth2 schemes
567610
scheme = scheme === "oauth2" ? scheme.split("2")[0] : scheme;
568-
return scheme === this.httpSecurityScheme.toLowerCase();
611+
return this.supportedSecuritySchemes.includes(scheme.toLocaleLowerCase());
569612
});
570613

571614
if (!secCandidate) {
572615
throw new Error(
573-
"Servient does not support thing security schemes. Current scheme supported: " +
574-
this.httpSecurityScheme +
575-
" secCandidate " +
576-
Object.keys(thing.securityDefinitions).join(", ")
616+
"Servient does not support any of thing security schemes. Current scheme supported: " +
617+
this.supportedSecuritySchemes.join(",") +
618+
" thing security schemes: " +
619+
Object.values(thing.securityDefinitions)
620+
.map((schemeDef) => schemeDef.scheme)
621+
.join(", ")
577622
);
578623
}
579624

@@ -582,11 +627,6 @@ export default class HttpServer implements ProtocolServer {
582627
thing.securityDefinitions[secCandidate] = selectedSecurityScheme;
583628

584629
thing.security = [secCandidate];
585-
} else {
586-
thing.securityDefinitions = {
587-
noSec: { scheme: "nosec" },
588-
};
589-
thing.security = ["noSec"];
590630
}
591631
}
592632

@@ -606,14 +646,6 @@ export default class HttpServer implements ProtocolServer {
606646
);
607647
});
608648

609-
// Set CORS headers
610-
if (this.httpSecurityScheme !== "NoSec" && req.headers.origin) {
611-
res.setHeader("Access-Control-Allow-Origin", req.headers.origin);
612-
res.setHeader("Access-Control-Allow-Credentials", "true");
613-
} else {
614-
res.setHeader("Access-Control-Allow-Origin", "*");
615-
}
616-
617649
const contentTypeHeader = req.headers["content-type"];
618650
let contentType: string = Array.isArray(contentTypeHeader) ? contentTypeHeader[0] : contentTypeHeader;
619651

packages/binding-http/src/http.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ export interface HttpConfig {
4444
allowSelfSigned?: boolean;
4545
serverKey?: string;
4646
serverCert?: string;
47-
security?: TD.SecurityScheme;
47+
security?: TD.SecurityScheme[];
4848
middleware?: MiddlewareRequestHandler;
4949
}
5050

packages/binding-http/src/routes/action.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,13 @@
1414
********************************************************************************/
1515
import { IncomingMessage, ServerResponse } from "http";
1616
import { Content, Helpers, ProtocolHelpers, createLoggers } from "@node-wot/core";
17-
import { isEmpty, respondUnallowedMethod, validOrDefaultRequestContentType } from "./common";
17+
import {
18+
isEmpty,
19+
respondUnallowedMethod,
20+
securitySchemeToHttpHeader,
21+
setCorsForThing,
22+
validOrDefaultRequestContentType,
23+
} from "./common";
1824
import HttpServer from "../http-server";
1925

2026
const { error, warn } = createLoggers("binding-http", "routes", "action");
@@ -61,12 +67,15 @@ export default async function actionRoute(
6167
return;
6268
}
6369
// TODO: refactor this part to move into a common place
70+
setCorsForThing(req, res, thing);
6471
let corsPreflightWithCredentials = false;
65-
if (this.getHttpSecurityScheme() !== "NoSec" && !(await this.checkCredentials(thing, req))) {
72+
const securityScheme = thing.securityDefinitions[Helpers.toStringArray(thing.security)[0]].scheme;
73+
74+
if (securityScheme !== "nosec" && !(await this.checkCredentials(thing, req))) {
6675
if (req.method === "OPTIONS" && req.headers.origin) {
6776
corsPreflightWithCredentials = true;
6877
} else {
69-
res.setHeader("WWW-Authenticate", `${this.getHttpSecurityScheme()} realm="${thing.id}"`);
78+
res.setHeader("WWW-Authenticate", `${securitySchemeToHttpHeader(securityScheme)} realm="${thing.id}"`);
7079
res.writeHead(401);
7180
res.end();
7281
return;

packages/binding-http/src/routes/common.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
*
1313
* SPDX-License-Identifier: EPL-2.0 OR W3C-20150513
1414
********************************************************************************/
15-
import { ContentSerdes, Helpers, createLoggers } from "@node-wot/core";
15+
import { ContentSerdes, ExposedThing, Helpers, createLoggers } from "@node-wot/core";
1616
import { IncomingMessage, ServerResponse } from "http";
1717

1818
const { debug, warn } = createLoggers("binding-http", "routes", "common");
@@ -79,3 +79,22 @@ export function isEmpty(obj: Record<string, unknown>): boolean {
7979
}
8080
return true;
8181
}
82+
83+
export function securitySchemeToHttpHeader(scheme: string): string {
84+
const [first, ...rest] = scheme;
85+
// HTTP Authentication Scheme for OAuth does not contain the version number
86+
// see https://www.iana.org/assignments/http-authschemes/http-authschemes.xhtml
87+
if (scheme === "oauth2") return "OAuth";
88+
return first.toUpperCase() + rest.join("").toLowerCase();
89+
}
90+
91+
export function setCorsForThing(req: IncomingMessage, res: ServerResponse, thing: ExposedThing): void {
92+
const securityScheme = thing.securityDefinitions[Helpers.toStringArray(thing.security)[0]].scheme;
93+
// Set CORS headers
94+
if (securityScheme !== "nosec" && req.headers.origin) {
95+
res.setHeader("Access-Control-Allow-Origin", req.headers.origin);
96+
res.setHeader("Access-Control-Allow-Credentials", "true");
97+
} else {
98+
res.setHeader("Access-Control-Allow-Origin", "*");
99+
}
100+
}

0 commit comments

Comments
 (0)