Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add supports for multiple security schemes in http server #1070

Merged
merged 5 commits into from
Sep 8, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion examples/security/oauth/consumer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down
16 changes: 9 additions & 7 deletions examples/security/oauth/wot-server-servient-conf.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
14 changes: 8 additions & 6 deletions packages/binding-http/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand Down Expand Up @@ -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.
}
Expand Down Expand Up @@ -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": {
Expand Down
164 changes: 98 additions & 66 deletions packages/binding-http/src/http-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@
private readonly address?: string = undefined;
private readonly baseUri?: string = undefined;
private readonly urlRewrite?: Record<string, string> = undefined;
private readonly httpSecurityScheme: string = "NoSec"; // HTTP header compatible string
private readonly supportedSecuritySchemes: string[] = ["nosec"];
private readonly validOAuthClients: RegExp = /.*/g;
private readonly server: http.Server | https.Server;
private readonly middleware: MiddlewareRequestHandler | null = null;
Expand Down Expand Up @@ -174,30 +174,28 @@

// 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.supportedSecuritySchemes = [];
}

Check warning on line 180 in packages/binding-http/src/http-server.ts

View check run for this annotation

Codecov / codecov/patch

packages/binding-http/src/http-server.ts#L178-L180

Added lines #L178 - L180 were not covered by tests
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}`);

Check warning on line 196 in packages/binding-http/src/http-server.ts

View check run for this annotation

Codecov / codecov/patch

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

Added line #L196 was not covered by tests
}
this.supportedSecuritySchemes.push(securityScheme.scheme);
}
}
}
Expand Down Expand Up @@ -263,10 +261,6 @@
}
}

public getHttpSecurityScheme(): string {
return this.httpSecurityScheme;
}

private updateInteractionNameWithUriVariablePattern(
interactionName: string,
uriVariables: PropertyElement["uriVariables"] = {},
Expand Down Expand Up @@ -326,9 +320,11 @@
// 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);
}
}
}
Expand Down Expand Up @@ -506,24 +502,25 @@
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);

Check warning on line 519 in packages/binding-http/src/http-server.ts

View check run for this annotation

Codecov / codecov/patch

packages/binding-http/src/http-server.ts#L515-L519

Added lines #L515 - L519 were not covered by tests
}
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
Expand All @@ -549,31 +546,79 @@
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]);

Check warning on line 554 in packages/binding-http/src/http-server.ts

View check run for this annotation

Codecov / codecov/patch

packages/binding-http/src/http-server.ts#L549-L554

Added lines #L549 - L554 were not covered by tests
}
default:
return false;
}
}

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(", ")
);
}

Check warning on line 575 in packages/binding-http/src/http-server.ts

View check run for this annotation

Codecov / codecov/patch

packages/binding-http/src/http-server.ts#L571-L575

Added lines #L571 - L575 were not covered by tests

const isSupported = this.supportedSecuritySchemes.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.supportedSecuritySchemes.join(", ")
);
}

Check warning on line 587 in packages/binding-http/src/http-server.ts

View check run for this annotation

Codecov / codecov/patch

packages/binding-http/src/http-server.ts#L583-L587

Added lines #L583 - L587 were not covered by tests
// 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.supportedSecuritySchemes[0]]: { scheme: this.supportedSecuritySchemes[0] },
};
thing.security = [this.supportedSecuritySchemes[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.supportedSecuritySchemes.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.supportedSecuritySchemes.join(",") +
" thing security schemes: " +
Object.values(thing.securityDefinitions)
.map((schemeDef) => schemeDef.scheme)
.join(", ")
);
}

Expand All @@ -582,11 +627,6 @@
thing.securityDefinitions[secCandidate] = selectedSecurityScheme;

thing.security = [secCandidate];
} else {
thing.securityDefinitions = {
noSec: { scheme: "nosec" },
};
thing.security = ["noSec"];
}
}

Expand All @@ -606,14 +646,6 @@
);
});

// 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", "*");
}

danielpeintner marked this conversation as resolved.
Show resolved Hide resolved
const contentTypeHeader = req.headers["content-type"];
let contentType: string = Array.isArray(contentTypeHeader) ? contentTypeHeader[0] : contentTypeHeader;

Expand Down
2 changes: 1 addition & 1 deletion packages/binding-http/src/http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export interface HttpConfig {
allowSelfSigned?: boolean;
serverKey?: string;
serverCert?: string;
security?: TD.SecurityScheme;
security?: TD.SecurityScheme[];
middleware?: MiddlewareRequestHandler;
}

Expand Down
15 changes: 12 additions & 3 deletions packages/binding-http/src/routes/action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down Expand Up @@ -56,12 +62,15 @@
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}"`);

Check warning on line 73 in packages/binding-http/src/routes/action.ts

View check run for this annotation

Codecov / codecov/patch

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

Added line #L73 was not covered by tests
res.writeHead(401);
res.end();
return;
Expand Down
21 changes: 20 additions & 1 deletion packages/binding-http/src/routes/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down Expand Up @@ -79,3 +79,22 @@
}
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();
}

Check warning on line 89 in packages/binding-http/src/routes/common.ts

View check run for this annotation

Codecov / codecov/patch

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

Added line #L89 was not covered by tests

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");

Check warning on line 96 in packages/binding-http/src/routes/common.ts

View check run for this annotation

Codecov / codecov/patch

packages/binding-http/src/routes/common.ts#L95-L96

Added lines #L95 - L96 were not covered by tests
} else {
res.setHeader("Access-Control-Allow-Origin", "*");
}
}
Loading
Loading