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]` |
@@ -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
- oauthObj.authorise()(req, res, next);
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.7",
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.19",
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
+ });