Skip to content

Commit fad2a65

Browse files
author
Marc Graham
authored
Add jwt support for thing-url-adapter (#96)
* Add ability to provide jwts for adapter urls and wss If url -- set auth to bearer with jwt If wss -- Add to query string Load jwt_auth object at start from jwt_auth.json file. In the same dir as addon hostname.local:port : token * refactor code -- use a function to get a configured header. move jwt_auth.json to .mozilla-iot/config * Update readme. * Update readme. * Update readme. * Update readme. * modified mainfest to add config option for adding jwts on the adapter config page. added secureThings property add to db as: <hostname>:<port> <jwt> with a space between * update read me * add check to see if secureThings is in config * #96 (comment) Renamed getHeaders to getHeaders(). * #96 (comment) Change query param for websocket to ?jwt=<token> * #96 (comment) remove unneeded ws==null check * #96 (comment) Modify manifest.json to collect parameters for jwt, basic and digest auth. Store in config. Update load thing to pull data for auth from config url data. Massaged functions as needed to accomodate the auth stuff. Only JWT is implemented in the adapter code. Stubs are in place to add basic and digest. Will have to add code at lines 52, 297, and 856 * #96 (comment) #96 (comment) #96 (comment) Proper naming, remove unneeded comments, remove console.log of AUTH_DATA * #96 (comment) Add config file update to loadThingURLAdapter. * #96 (comment) Add 'none' to auth mehtods. On upgrade of config and authenticaton object is added with a method property of 'none' * #96 (comment) move getHeaders function and authData to ThingURLAdapter. Added adapter property to ThingURLDevice and set it in the constructor. Changed all the getHeader() call accordingly. * #96 (comment) Update read me with instructions for secure thing usage. Change rev to 5.0 in manifest and package.jsnon * #96 (comment) Types and grammar. * #96 (comment) Types and grammar. * Latest polishing of code Remove redundant this.adapter Use part of authentication object for authData Typos and capitolization * Remove unneeded console.log statement * Remove un-needed comments. Correct getHeaders for eventsUrl and perfromAction Rename variable url_stub * Refactor for loops * correct incorrect spelling
1 parent 06dc9e4 commit fad2a65

File tree

8 files changed

+180
-32
lines changed

8 files changed

+180
-32
lines changed

README.md

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,29 @@
33
This is an adapter add-on for the [Mozilla WebThings Gateway](https://github.com/mozilla-iot/gateway) that allows a user to discover native web things on their network.
44

55
## Adding Web Things to Gateway
6-
76
* Usually, your custom web things should be auto-detected on the network via mDNS, so they should appear in the usual "Add Devices" screen.
87
* If they're not auto-detected, you can click "Add by URL..." at the bottom of the page and use the URL of your web thing.
98
* If you're trying to add a server that contains multiple web things, i.e. the "multiple-things" examples from the [webthing-python](https://github.com/mozilla-iot/webthing-python), [webthing-node](https://github.com/mozilla-iot/webthing-node), or [webthing-java](https://github.com/mozilla-iot/webthing-java) libraries, you'll have to add them individually. You can do so by addressing them numerically, i.e. `http://myserver.local:8888/0` and `http://myserver.local:8888/1`.
9+
10+
## Secure Web Things
11+
* Web Things that require jwt, basic or digest authentication can be supported by adding configuration options on the adapter page.
12+
* To add a secure Web Thing:
13+
14+
* Use the hamburger to open the side menu and select "Settings".
15+
16+
![select_settings.png](images/select_settings.png)
17+
18+
* Select "Add-ons".
19+
20+
![select_addons.png](images/select_addons.png)
21+
22+
* Find the Web Thing adapter and select "Configure".
23+
24+
![select_configure.png](images/select_configure.png)
25+
26+
* Enter data relevant to the desired authentication method.
27+
* To manually enter device URLs with no authentication select 'none' for the method.
28+
29+
![enter_data.png](images/enter_data.png)
30+
31+
* Save the entry by pressing "Apply" at the bottom of the page.

images/enter_data.png

36 KB
Loading

images/select_addons.png

13.3 KB
Loading

images/select_configure.png

19.8 KB
Loading

images/select_settings.png

23.6 KB
Loading

manifest.json

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,50 @@
3030
"urls": {
3131
"type": "array",
3232
"items": {
33-
"type": "string"
33+
"type": "object",
34+
"required": [
35+
"href"
36+
],
37+
"properties": {
38+
"href": {
39+
"description": "Base URL of web thing",
40+
"type": "string"
41+
},
42+
"authentication": {
43+
"description": "Authentication credentials",
44+
"type": "object",
45+
"required": [
46+
"method"
47+
],
48+
"properties": {
49+
"method": {
50+
"type": "string",
51+
"enum": [
52+
"none",
53+
"basic",
54+
"digest",
55+
"jwt"
56+
]
57+
},
58+
"username": {
59+
"description": "Username (for basic and digest)",
60+
"type": "string"
61+
},
62+
"password": {
63+
"description": "Password (for basic and digest)",
64+
"type": "string"
65+
},
66+
"realm": {
67+
"description": "Realm (for digest)",
68+
"type": "string"
69+
},
70+
"token": {
71+
"description": "Token (for JWT)",
72+
"type": "string"
73+
}
74+
}
75+
}
76+
}
3477
}
3578
},
3679
"pollInterval": {
@@ -41,5 +84,5 @@
4184
}
4285
},
4386
"short_name": "Web Thing",
44-
"version": "0.4.8"
87+
"version": "0.5.0"
4588
}

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "thing-url-adapter",
3-
"version": "0.4.8",
3+
"version": "0.5.0",
44
"description": "Native web thing support",
55
"author": "Mozilla IoT",
66
"main": "index.js",

thing-url-adapter.js

Lines changed: 111 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ const POLL_INTERVAL = 5 * 1000;
3838
const WS_INITIAL_BACKOFF = 1000;
3939
const WS_MAX_BACKOFF = 30 * 1000;
4040

41+
4142
class ThingURLProperty extends Property {
4243
constructor(device, name, url, propertyDescription) {
4344
super(device, name, propertyDescription);
@@ -71,12 +72,10 @@ class ThingURLProperty extends Property {
7172
return Promise.resolve(value);
7273
}
7374

75+
const headers = this.device.adapter.getHeaders(this.url, true);
7476
return fetch(this.url, {
7577
method: 'PUT',
76-
headers: {
77-
'Content-type': 'application/json',
78-
Accept: 'application/json',
79-
},
78+
headers: headers,
8079
body: JSON.stringify({
8180
[this.name]: value,
8281
}),
@@ -118,6 +117,7 @@ class ThingURLDevice extends Device {
118117
this.scheduledUpdate = null;
119118
this.closing = false;
120119

120+
121121
for (const actionName in description.actions) {
122122
const action = description.actions[actionName];
123123
if (action.hasOwnProperty('links')) {
@@ -165,11 +165,10 @@ class ThingURLDevice extends Device {
165165
propertyUrl = this.baseHref + propertyDescription.href;
166166
}
167167

168+
const headers = this.adapter.getHeaders(propertyUrl);
168169
this.propertyPromises.push(
169170
fetch(propertyUrl, {
170-
headers: {
171-
Accept: 'application/json',
172-
},
171+
headers: headers,
173172
}).then((res) => {
174173
return res.json();
175174
}).then((res) => {
@@ -265,7 +264,24 @@ class ThingURLDevice extends Device {
265264
return;
266265
}
267266

268-
this.ws = new WebSocket(this.wsUrl);
267+
let auth = '';
268+
for (const [url, authData] of Object.entries(this.adapter.authData)) {
269+
if (this.wsUrl.includes(url)) {
270+
switch (authData.method) {
271+
case 'jwt':
272+
auth = `?jwt=${authData.token}`;
273+
break;
274+
case 'basic':
275+
case 'digest':
276+
default:
277+
// not implemented
278+
break;
279+
}
280+
break;
281+
}
282+
}
283+
284+
this.ws = new WebSocket(`${this.wsUrl}${auth}`, '', {});
269285

270286
this.ws.on('open', () => {
271287
this.connectedNotify(true);
@@ -363,10 +379,9 @@ class ThingURLDevice extends Device {
363379

364380
// Update properties
365381
await Promise.all(Array.from(this.properties.values()).map((prop) => {
382+
const headers = this.adapter.getHeaders(prop.url);
366383
return fetch(prop.url, {
367-
headers: {
368-
Accept: 'application/json',
369-
},
384+
headers: headers,
370385
}).then((res) => {
371386
return res.json();
372387
}).then((res) => {
@@ -381,10 +396,9 @@ class ThingURLDevice extends Device {
381396
})).then(() => {
382397
// Check for new actions
383398
if (this.actionsUrl !== null) {
399+
const headers = this.adapter.getHeaders(this.actionsUrl);
384400
return fetch(this.actionsUrl, {
385-
headers: {
386-
Accept: 'application/json',
387-
},
401+
headers: headers,
388402
}).then((res) => {
389403
return res.json();
390404
}).then((actions) => {
@@ -408,10 +422,9 @@ class ThingURLDevice extends Device {
408422
}).then(() => {
409423
// Check for new events
410424
if (this.eventsUrl !== null) {
425+
const headers = this.adapter.getHeaders(this.eventsUrl);
411426
return fetch(this.eventsUrl, {
412-
headers: {
413-
Accept: 'application/json',
414-
},
427+
headers: headers,
415428
}).then((res) => {
416429
return res.json();
417430
}).then((events) => {
@@ -464,13 +477,10 @@ class ThingURLDevice extends Device {
464477

465478
performAction(action) {
466479
action.start();
467-
480+
const headers = this.adapter.getHeaders(this.actionsUrl, true);
468481
return fetch(this.actionsUrl, {
469482
method: 'POST',
470-
headers: {
471-
'Content-Type': 'application/json',
472-
Accept: 'application/json',
473-
},
483+
headers: headers,
474484
body: JSON.stringify({[action.name]: {input: action.input}}),
475485
}).then((res) => {
476486
return res.json();
@@ -488,11 +498,11 @@ class ThingURLDevice extends Device {
488498

489499
this.requestedActions.forEach((action, actionHref) => {
490500
if (action.name === actionName && action.id === actionId) {
501+
const headers = this.adapter.getHeaders(actionHref);
502+
491503
promise = fetch(actionHref, {
492504
method: 'DELETE',
493-
headers: {
494-
Accept: 'application/json',
495-
},
505+
headers: headers,
496506
}).catch((e) => {
497507
console.log(`Failed to cancel action: ${e}`);
498508
});
@@ -516,6 +526,30 @@ class ThingURLAdapter extends Adapter {
516526
this.knownUrls = {};
517527
this.savedDevices = new Set();
518528
this.pollInterval = POLL_INTERVAL;
529+
this.authData = {};
530+
}
531+
532+
getHeaders(_url, contentType = false) {
533+
const headers = {Accept: 'application/json'};
534+
if (contentType) {
535+
headers['Content-Type'] = 'application/json';
536+
}
537+
for (const [url, authData] of Object.entries(this.authData)) {
538+
if (_url.includes(url)) {
539+
switch (authData.method) {
540+
case 'jwt':
541+
headers.Authorization = `Bearer ${authData.token}`;
542+
break;
543+
case 'basic':
544+
case 'digest':
545+
default:
546+
// not implemented
547+
break;
548+
}
549+
break;
550+
}
551+
}
552+
return headers;
519553
}
520554

521555
async loadThing(url, retryCounter) {
@@ -537,8 +571,9 @@ class ThingURLAdapter extends Adapter {
537571
}
538572

539573
let res;
574+
const headers = this.getHeaders(url);
540575
try {
541-
res = await fetch(url, {headers: {Accept: 'application/json'}});
576+
res = await fetch(url, {headers: headers});
542577
} catch (e) {
543578
// Retry the connection at a 2 second interval up to 5 times.
544579
if (retryCounter >= 5) {
@@ -799,10 +834,58 @@ function loadThingURLAdapter(addonManager) {
799834
adapter.pollInterval = config.pollInterval * 1000;
800835
}
801836

802-
for (const url of config.urls) {
803-
adapter.loadThing(url);
837+
let modified = false;
838+
const urls = [];
839+
for (const entry of config.urls) {
840+
if (typeof entry === 'string') {
841+
urls.push({
842+
href: entry,
843+
authentication: {
844+
method: 'none',
845+
},
846+
});
847+
modified = true;
848+
} else {
849+
urls.push(entry);
850+
}
851+
}
852+
853+
if (modified) {
854+
config.urls = urls;
855+
db.saveConfig(config);
804856
}
805857

858+
for (const url of config.urls) {
859+
if ('authentication' in url) {
860+
// remove http(s) from url
861+
let urlStub = '';
862+
if (url.href.includes('http://')) {
863+
urlStub = url.href.substr(7);
864+
}
865+
if (url.href.includes('https://')) {
866+
urlStub = url.href.substr(8);
867+
}
868+
switch (url.authentication.method) {
869+
case 'jwt':
870+
adapter.authData[urlStub] = url.authentication;
871+
break;
872+
case 'basic':
873+
// adapter.authData[url_stub] = url.authentication;
874+
// break;
875+
// eslint-disable-next-line no-fallthrough
876+
case 'digest':
877+
// adapter.authData[url_stub] = url.authentication;
878+
// break;
879+
// eslint-disable-next-line no-fallthrough
880+
case 'none':
881+
break;
882+
default:
883+
console.log(`${url.authentication.method} is not implemented`);
884+
break;
885+
}
886+
}
887+
adapter.loadThing(url.href);
888+
}
806889
startDNSDiscovery(adapter);
807890
}).catch(console.error);
808891
}

0 commit comments

Comments
 (0)