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 @@ export default class HttpServer implements ProtocolServer {
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 @@ 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.supportedSecuritySchemes = [];
}
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.supportedSecuritySchemes.push(securityScheme.scheme);
}
}
}
Expand Down Expand Up @@ -263,10 +261,6 @@ export default class HttpServer implements ProtocolServer {
}
}

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

private updateInteractionNameWithUriVariablePattern(
interactionName: string,
uriVariables: PropertyElement["uriVariables"] = {},
Expand Down Expand Up @@ -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);
}
}
}
Expand Down Expand Up @@ -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
Expand All @@ -549,31 +546,79 @@ 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;
}
}

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.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(", ")
);
}
// 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 @@ export default class HttpServer implements ProtocolServer {
thing.securityDefinitions[secCandidate] = selectedSecurityScheme;

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

Expand All @@ -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", "*");
}

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 @@ 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;
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 @@ export function isEmpty(obj: Record<string, unknown>): 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", "*");
}
}
Loading