soajs.controller 4.3.7 → 4.3.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -27,6 +27,8 @@ registry.custom
|
|
|
27
27
|
│ │ └── [service] # Service-specific settings
|
|
28
28
|
│ ├── maintenanceMode # Gateway maintenance mode
|
|
29
29
|
│ ├── lastSeen # User activity tracking
|
|
30
|
+
│ ├── oauth
|
|
31
|
+
│ │ └── deviceIdCheck # Turn the deviceId check off
|
|
30
32
|
│ └── gotoService
|
|
31
33
|
│ ├── monitor # Request monitoring config
|
|
32
34
|
│ └── renewReqMonitorOff # Disable timeout renewal
|
|
@@ -448,6 +450,37 @@ Whitelists specific APIs that can be accessed when a user is PIN-locked (during
|
|
|
448
450
|
|
|
449
451
|
---
|
|
450
452
|
|
|
453
|
+
## 13. OAuth Device ID Check
|
|
454
|
+
|
|
455
|
+
**Path:** `registry.custom.gateway.value.oauth.deviceIdCheck`
|
|
456
|
+
|
|
457
|
+
**Middleware:** `mw/oauth/index.js`
|
|
458
|
+
|
|
459
|
+
Turns off the deviceId check done when authorising an access token. The check matches
|
|
460
|
+
`user.deviceId` on the access token record against the `device-id` header on every
|
|
461
|
+
private API call, and denies with error 156 on mismatch.
|
|
462
|
+
|
|
463
|
+
```javascript
|
|
464
|
+
{
|
|
465
|
+
"oauth": {
|
|
466
|
+
"deviceIdCheck": false // Turn off the deviceId check
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
```
|
|
470
|
+
|
|
471
|
+
| Field | Type | Default | Description |
|
|
472
|
+
|-------|------|---------|-------------|
|
|
473
|
+
| `deviceIdCheck` | boolean | `true` | Set to `false` to turn the deviceId check off |
|
|
474
|
+
|
|
475
|
+
Only the boolean `false` turns the check off. Any other value, and a missing or
|
|
476
|
+
malformed entry, leaves it on.
|
|
477
|
+
|
|
478
|
+
Note this is a `gateway` entry, not an `oauth` one. The `registry.custom.oauth.value.*`
|
|
479
|
+
entries above are consumed by `mw/mt/utils.js` and `mw/gotoService/roaming.js`, this one
|
|
480
|
+
is read by the oauth middleware itself.
|
|
481
|
+
|
|
482
|
+
---
|
|
483
|
+
|
|
451
484
|
## Complete Configuration Example
|
|
452
485
|
|
|
453
486
|
```javascript
|
|
@@ -514,6 +547,9 @@ Whitelists specific APIs that can be accessed when a user is PIN-locked (during
|
|
|
514
547
|
"api": "/user/last/seen",
|
|
515
548
|
"network": "production"
|
|
516
549
|
},
|
|
550
|
+
"oauth": {
|
|
551
|
+
"deviceIdCheck": false
|
|
552
|
+
},
|
|
517
553
|
"gotoService": {
|
|
518
554
|
"monitor": {
|
|
519
555
|
"blacklist": ["soamonitor", "urac"]
|
|
@@ -565,6 +601,7 @@ Custom registry configurations are processed in this order:
|
|
|
565
601
|
| 4 | gotoService/preRedirect | `gotoService.renewReqMonitorOff` |
|
|
566
602
|
| 5 | gotoService/redirectToService | `gotoService.monitor` |
|
|
567
603
|
| 6 | mt (security checks) | `mt.whitelist`, `oauth.value.pinWrapper`, `oauth.value.pinWhitelist` |
|
|
604
|
+
| 6a | oauth (called from mt) | `oauth.deviceIdCheck` |
|
|
568
605
|
| 7 | idempotency | `idempotency.model`, `idempotency.services.[name]` |
|
|
569
606
|
| 8 | traffic | `traffic.model`, `traffic.throttling` |
|
|
570
607
|
| 9 | cache | `cache.model`, `cache.defaultTTL`, `cache.services.[name]` |
|
package/docs/middleware/oauth.md
CHANGED
|
@@ -60,9 +60,53 @@ Uses oauth2-server library for token validation.
|
|
|
60
60
|
│ bearerToken: user, │
|
|
61
61
|
│ type: 2 │
|
|
62
62
|
│ } │
|
|
63
|
-
|
|
63
|
+
└────────┬────────────────┘
|
|
64
|
+
│
|
|
65
|
+
▼
|
|
66
|
+
┌─────────────────────────┐
|
|
67
|
+
│ deviceId check │ token user.deviceId vs device-id header
|
|
68
|
+
└────────┬────────────────┘
|
|
69
|
+
│
|
|
70
|
+
├─── Mismatch ──▶ Error 156
|
|
71
|
+
│
|
|
72
|
+
▼
|
|
73
|
+
next()
|
|
64
74
|
```
|
|
65
75
|
|
|
76
|
+
#### deviceId check
|
|
77
|
+
|
|
78
|
+
The access token record carries `user.deviceId`, set at login from the `device-id`
|
|
79
|
+
header and carried forward across refreshes. It is matched against the `device-id`
|
|
80
|
+
header on every private API call. The check runs on the record `authorise()` already
|
|
81
|
+
fetched, so it costs no extra database call.
|
|
82
|
+
|
|
83
|
+
The agent cannot be used for this, mobile sends the build number in the user-agent
|
|
84
|
+
(ie: `democav/142 CFNetwork/3826.400.120 Darwin/24.3.0`) which changes on every build.
|
|
85
|
+
|
|
86
|
+
Tokens with no `deviceId` are not checked. This covers tokens created before deviceId
|
|
87
|
+
was introduced and clients that do not send the header at all, both get checked once
|
|
88
|
+
the client logs in again with the header. A client that sends `device-id` on login but
|
|
89
|
+
omits it on API calls is denied, the header has to be sent consistently.
|
|
90
|
+
|
|
91
|
+
This check is only available for type 2, a type 0 JWT carries no stored token record.
|
|
92
|
+
|
|
93
|
+
##### Turning it off
|
|
94
|
+
|
|
95
|
+
The check is on by default. To turn it off for an environment, set `deviceIdCheck` to
|
|
96
|
+
`false` under the `gateway` custom registry entry:
|
|
97
|
+
|
|
98
|
+
```json
|
|
99
|
+
{
|
|
100
|
+
"oauth": {
|
|
101
|
+
"deviceIdCheck": false
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Read from `registry.custom.gateway.value.oauth.deviceIdCheck`. Only the boolean `false`
|
|
107
|
+
turns it off, any other value leaves it on, so a missing or malformed entry keeps the
|
|
108
|
+
check running.
|
|
109
|
+
|
|
66
110
|
### Type 0: JWT
|
|
67
111
|
|
|
68
112
|
Uses jsonwebtoken library for JWT validation.
|
|
@@ -155,6 +199,7 @@ Default OAuth service configuration:
|
|
|
155
199
|
| Code | Description |
|
|
156
200
|
|------|-------------|
|
|
157
201
|
| 143 | Invalid or missing JWT token |
|
|
202
|
+
| 156 | Device forbidden, the deviceId on the token does not match the device-id header |
|
|
158
203
|
| OAuth2Error | Various OAuth2 errors (invalid_token, expired, etc.) |
|
|
159
204
|
|
|
160
205
|
## Properties Set
|
package/mw/oauth/index.js
CHANGED
|
@@ -32,9 +32,57 @@ module.exports = (configuration) => {
|
|
|
32
32
|
});
|
|
33
33
|
|
|
34
34
|
let jwt = require('jsonwebtoken');
|
|
35
|
-
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The deviceId check is on unless it is explicitly turned off at the custom registry.
|
|
38
|
+
* Anything other than false leaves it on.
|
|
39
|
+
*
|
|
40
|
+
* req.soajs.registry.custom.gateway.value.oauth
|
|
41
|
+
* {
|
|
42
|
+
"oauth": {
|
|
43
|
+
"deviceIdCheck": false
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
*
|
|
47
|
+
* @param req
|
|
48
|
+
* @returns {boolean} true when the check is turned on
|
|
49
|
+
*/
|
|
50
|
+
let deviceIdCheckOn = (req) => {
|
|
51
|
+
if (req.soajs.registry &&
|
|
52
|
+
req.soajs.registry.custom &&
|
|
53
|
+
req.soajs.registry.custom.gateway &&
|
|
54
|
+
req.soajs.registry.custom.gateway.value &&
|
|
55
|
+
req.soajs.registry.custom.gateway.value.oauth &&
|
|
56
|
+
req.soajs.registry.custom.gateway.value.oauth.deviceIdCheck === false) {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
return true;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Matches the deviceId on the access token record against the device-id header.
|
|
64
|
+
*
|
|
65
|
+
* NOTE: we cannot check for agent, mobile is sending the build number
|
|
66
|
+
* (ie: "democav/142 CFNetwork/3826.400.120 Darwin/24.3.0") which changes on every build.
|
|
67
|
+
* we check deviceId instead. tokens with no deviceId (created before deviceId was
|
|
68
|
+
* introduced, or by a client that does not send the header) are not checked, they get
|
|
69
|
+
* checked once the client logs in again.
|
|
70
|
+
*
|
|
71
|
+
* @param req
|
|
72
|
+
* @returns {boolean} true when the request is allowed to proceed
|
|
73
|
+
*/
|
|
74
|
+
let deviceIdMatch = (req) => {
|
|
75
|
+
if (!deviceIdCheckOn(req)) {
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
if (!req.oauth || !req.oauth.bearerToken || !req.oauth.bearerToken.user || !req.oauth.bearerToken.user.deviceId) {
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
return req.oauth.bearerToken.user.deviceId === req.get('device-id');
|
|
82
|
+
};
|
|
83
|
+
|
|
36
84
|
return (req, res, next) => {
|
|
37
|
-
|
|
85
|
+
|
|
38
86
|
let oauthType = 2;
|
|
39
87
|
let tenantOauth = req.soajs.tenantOauth;
|
|
40
88
|
if (tenantOauth && Object.hasOwnProperty.call(tenantOauth, 'type')) {
|
|
@@ -45,7 +93,18 @@ module.exports = (configuration) => {
|
|
|
45
93
|
|
|
46
94
|
//0=oauth0, 2=oauth2
|
|
47
95
|
if (2 === oauthType) {
|
|
48
|
-
|
|
96
|
+
//NOTE: authorise() sets req.oauth.bearerToken to the token record it fetched from the
|
|
97
|
+
// model, the deviceId check is done on it here to avoid fetching the token again.
|
|
98
|
+
oauthObj.authorise()(req, res, (error) => {
|
|
99
|
+
if (error) {
|
|
100
|
+
return next(error);
|
|
101
|
+
}
|
|
102
|
+
if (!deviceIdMatch(req)) {
|
|
103
|
+
req.soajs.log.debug("Access denied, deviceId mismatch [deviceId: " + req.get('device-id') + "]");
|
|
104
|
+
return next(156);
|
|
105
|
+
}
|
|
106
|
+
return next();
|
|
107
|
+
});
|
|
49
108
|
} else {
|
|
50
109
|
let algorithms = req.soajs.registry.serviceConfig.oauth.algorithms || ["HS256"];
|
|
51
110
|
let audience = req.soajs.registry.serviceConfig.oauth.audience || "";
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "soajs.controller",
|
|
3
3
|
"description": "soajs multi tenant API gateway",
|
|
4
|
-
"version": "4.3.
|
|
4
|
+
"version": "4.3.9",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "soajs team",
|
|
7
7
|
"email": "team@soajs.org"
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
"oauth2-server": "2.4.1",
|
|
30
30
|
"path-to-regexp": "8.4.0",
|
|
31
31
|
"soajs.core.libs": "1.2.2",
|
|
32
|
-
"soajs.core.modules": "5.2.
|
|
32
|
+
"soajs.core.modules": "5.2.20",
|
|
33
33
|
"soajs.urac.driver": "3.2.15",
|
|
34
34
|
"useragent": "2.3.0"
|
|
35
35
|
},
|
|
@@ -99,3 +99,171 @@ describe("Unit test for: mw - oauth", () => {
|
|
|
99
99
|
});
|
|
100
100
|
});
|
|
101
101
|
});
|
|
102
|
+
|
|
103
|
+
describe("Unit test for: mw - oauth deviceId check", () => {
|
|
104
|
+
|
|
105
|
+
// builds a configuration whose model returns an access token carrying the given deviceId
|
|
106
|
+
let buildConfiguration = (deviceId) => {
|
|
107
|
+
let user = {"id": "1"};
|
|
108
|
+
if (deviceId !== undefined) {
|
|
109
|
+
user.deviceId = deviceId;
|
|
110
|
+
}
|
|
111
|
+
return {
|
|
112
|
+
"soajs": {
|
|
113
|
+
"param": {}
|
|
114
|
+
},
|
|
115
|
+
"serviceConfig": {
|
|
116
|
+
"oauth": {
|
|
117
|
+
grants: ["password", "refresh_token"],
|
|
118
|
+
debug: false,
|
|
119
|
+
accessTokenLifetime: 7200,
|
|
120
|
+
refreshTokenLifetime: 1209600
|
|
121
|
+
}
|
|
122
|
+
},
|
|
123
|
+
"model": {
|
|
124
|
+
"getAccessToken": (bearerToken, cb) => {
|
|
125
|
+
return cb(null, {
|
|
126
|
+
"token": bearerToken,
|
|
127
|
+
"expires": new Date(new Date().getTime() + 3600000),
|
|
128
|
+
"user": user
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
// oauthType 2 so that the access token is fetched from the model
|
|
136
|
+
let buildReq = (headerDeviceId) => {
|
|
137
|
+
return {
|
|
138
|
+
"soajs": {
|
|
139
|
+
"log": {
|
|
140
|
+
"debug": () => {
|
|
141
|
+
}
|
|
142
|
+
},
|
|
143
|
+
"tenantOauth": {
|
|
144
|
+
"type": 2
|
|
145
|
+
},
|
|
146
|
+
"servicesConfig": {},
|
|
147
|
+
"registry": {
|
|
148
|
+
"serviceConfig": {
|
|
149
|
+
"oauth": {
|
|
150
|
+
"type": 2
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
},
|
|
155
|
+
"query": {},
|
|
156
|
+
"body": {},
|
|
157
|
+
"get": (what) => {
|
|
158
|
+
if ('Authorization' === what) {
|
|
159
|
+
return "Bearer anAccessToken";
|
|
160
|
+
}
|
|
161
|
+
if ('device-id' === what) {
|
|
162
|
+
return headerDeviceId;
|
|
163
|
+
}
|
|
164
|
+
return undefined;
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
};
|
|
168
|
+
let res = {};
|
|
169
|
+
|
|
170
|
+
it("test oauth MW - deviceId matches", (done) => {
|
|
171
|
+
let functionMw = mw(buildConfiguration("2222222222"));
|
|
172
|
+
let req = buildReq("2222222222");
|
|
173
|
+
functionMw(req, res, (error) => {
|
|
174
|
+
assert.ifError(error);
|
|
175
|
+
assert.ok(req.oauth.bearerToken);
|
|
176
|
+
done();
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it("test oauth MW - deviceId does not match", (done) => {
|
|
181
|
+
let functionMw = mw(buildConfiguration("3333333333"));
|
|
182
|
+
let req = buildReq("4444444444");
|
|
183
|
+
functionMw(req, res, (error) => {
|
|
184
|
+
assert.deepStrictEqual(error, 156);
|
|
185
|
+
done();
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it("test oauth MW - deviceId on the token but no header", (done) => {
|
|
190
|
+
let functionMw = mw(buildConfiguration("3333333333"));
|
|
191
|
+
let req = buildReq(undefined);
|
|
192
|
+
functionMw(req, res, (error) => {
|
|
193
|
+
assert.deepStrictEqual(error, 156);
|
|
194
|
+
done();
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it("test oauth MW - no deviceId on the token, check is skipped", (done) => {
|
|
199
|
+
let functionMw = mw(buildConfiguration(undefined));
|
|
200
|
+
let req = buildReq("5555555555");
|
|
201
|
+
functionMw(req, res, (error) => {
|
|
202
|
+
assert.ifError(error);
|
|
203
|
+
assert.ok(req.oauth.bearerToken);
|
|
204
|
+
done();
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it("test oauth MW - null deviceId on the token, check is skipped", (done) => {
|
|
209
|
+
let functionMw = mw(buildConfiguration(null));
|
|
210
|
+
let req = buildReq("5555555555");
|
|
211
|
+
functionMw(req, res, (error) => {
|
|
212
|
+
assert.ifError(error);
|
|
213
|
+
assert.ok(req.oauth.bearerToken);
|
|
214
|
+
done();
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
// turns the check off at registry.custom.gateway.value.oauth
|
|
219
|
+
let turnCheckOff = (req, deviceIdCheck) => {
|
|
220
|
+
req.soajs.registry.custom = {
|
|
221
|
+
"gateway": {
|
|
222
|
+
"value": {
|
|
223
|
+
"oauth": {
|
|
224
|
+
"deviceIdCheck": deviceIdCheck
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
};
|
|
229
|
+
return req;
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
it("test oauth MW - deviceId mismatch but check turned off at registry", (done) => {
|
|
233
|
+
let functionMw = mw(buildConfiguration("3333333333"));
|
|
234
|
+
let req = turnCheckOff(buildReq("4444444444"), false);
|
|
235
|
+
functionMw(req, res, (error) => {
|
|
236
|
+
assert.ifError(error);
|
|
237
|
+
assert.ok(req.oauth.bearerToken);
|
|
238
|
+
done();
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it("test oauth MW - deviceId mismatch and check explicitly turned on at registry", (done) => {
|
|
243
|
+
let functionMw = mw(buildConfiguration("3333333333"));
|
|
244
|
+
let req = turnCheckOff(buildReq("4444444444"), true);
|
|
245
|
+
functionMw(req, res, (error) => {
|
|
246
|
+
assert.deepStrictEqual(error, 156);
|
|
247
|
+
done();
|
|
248
|
+
});
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it("test oauth MW - only false turns the check off, not a truthy value", (done) => {
|
|
252
|
+
let functionMw = mw(buildConfiguration("3333333333"));
|
|
253
|
+
let req = turnCheckOff(buildReq("4444444444"), "false");
|
|
254
|
+
functionMw(req, res, (error) => {
|
|
255
|
+
assert.deepStrictEqual(error, 156);
|
|
256
|
+
done();
|
|
257
|
+
});
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it("test oauth MW - empty custom registry leaves the check on", (done) => {
|
|
261
|
+
let functionMw = mw(buildConfiguration("3333333333"));
|
|
262
|
+
let req = buildReq("4444444444");
|
|
263
|
+
req.soajs.registry.custom = {};
|
|
264
|
+
functionMw(req, res, (error) => {
|
|
265
|
+
assert.deepStrictEqual(error, 156);
|
|
266
|
+
done();
|
|
267
|
+
});
|
|
268
|
+
});
|
|
269
|
+
});
|