Skip to content

Commit 798bd37

Browse files
Merge pull request #270 from PerimeterX/release/v3.8.0
Release/v3.8.0 - user Identifiers support (for AD)
2 parents 1ead16e + d43a571 commit 798bd37

11 files changed

+192
-40
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](http://keepachangelog.com/)
66
and this project adheres to [Semantic Versioning](http://semver.org/).
77

8+
## [3.8.0] - 2023-01-25
9+
10+
### Added
11+
- Support User Identifiers: CTS and JWT.
12+
813
## [3.7.0] - 2023-01-15
914

1015
### Added

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
[PerimeterX](http://www.perimeterx.com) Shared base for NodeJS enforcers
77
=============================================================
88

9-
> Latest stable version: [v3.7.0](https://www.npmjs.com/package/perimeterx-node-core)
9+
> Latest stable version: [v3.8.0](https://www.npmjs.com/package/perimeterx-node-core)
1010
1111
This is a shared base implementation for PerimeterX Express enforcer and future NodeJS enforcers. For a fully functioning implementation example, see the [Node-Express enforcer](https://github.com/PerimeterX/perimeterx-node-express/) implementation.
1212

lib/pxapi.js

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,16 @@ const { ModuleMode } = require('./enums/ModuleMode');
77
const PassReason = require('./enums/PassReason');
88
const ScoreEvaluateAction = require('./enums/ScoreEvaluateAction');
99
const S2SErrorReason = require('./enums/S2SErrorReason');
10-
const { CI_USERNAME_FIELD, CI_PASSWORD_FIELD, CI_VERSION_FIELD, CI_SSO_STEP_FIELD,
11-
GQL_OPERATIONS_FIELD
10+
11+
const {
12+
CI_USERNAME_FIELD,
13+
CI_PASSWORD_FIELD,
14+
CI_VERSION_FIELD,
15+
CI_SSO_STEP_FIELD,
16+
GQL_OPERATIONS_FIELD,
17+
JWT_ADDITIONAL_FIELDS_FIELD_NAME,
18+
APP_USER_ID_FIELD_NAME,
19+
CROSS_TAB_SESSION,
1220
} = require('./utils/constants');
1321
const { CIVersion } = require('./enums/CIVersion');
1422

@@ -78,11 +86,31 @@ function buildRequestData(ctx, config) {
7886
}
7987
}
8088

89+
if (ctx.jwt) {
90+
const { userID, additionalFields } = ctx.jwt;
91+
92+
if (userID) {
93+
data.additional[APP_USER_ID_FIELD_NAME] = userID;
94+
}
95+
96+
if (additionalFields) {
97+
data.additional[JWT_ADDITIONAL_FIELDS_FIELD_NAME] = additionalFields;
98+
}
99+
}
100+
101+
if (ctx.cts) {
102+
data.additional[CROSS_TAB_SESSION] = ctx.cts;
103+
}
104+
81105
if (ctx.s2sCallReason === 'cookie_decryption_failed') {
82106
data.additional.px_orig_cookie = ctx.getCookie(); //No need strigify, already a string
83107
}
84108

85-
if (ctx.s2sCallReason === 'cookie_expired' || ctx.s2sCallReason === 'cookie_validation_failed' || ctx.s2sCallReason === 'sensitive_route') {
109+
if (
110+
ctx.s2sCallReason === 'cookie_expired' ||
111+
ctx.s2sCallReason === 'cookie_validation_failed' ||
112+
ctx.s2sCallReason === 'sensitive_route'
113+
) {
86114
data.additional.px_cookie = JSON.stringify(ctx.decodedCookie);
87115
}
88116

lib/pxclient.js

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,10 @@ const {
99
CI_SSO_STEP_FIELD,
1010
CI_RAW_USERNAME_FIELD,
1111
CI_CREDENTIALS_COMPROMISED_FIELD,
12-
GQL_OPERATIONS_FIELD
12+
GQL_OPERATIONS_FIELD,
13+
APP_USER_ID_FIELD_NAME,
14+
JWT_ADDITIONAL_FIELDS_FIELD_NAME,
15+
CROSS_TAB_SESSION,
1316
} = require('./utils/constants');
1417

1518
class PxClient {
@@ -64,6 +67,22 @@ class PxClient {
6467
if (ctx.graphqlData) {
6568
details[GQL_OPERATIONS_FIELD] = ctx.graphqlData;
6669
}
70+
71+
if (ctx.jwt) {
72+
const { userID, additionalFields } = ctx.jwt;
73+
74+
if (userID) {
75+
details[APP_USER_ID_FIELD_NAME] = userID;
76+
}
77+
78+
if (additionalFields) {
79+
details[JWT_ADDITIONAL_FIELDS_FIELD_NAME] = additionalFields;
80+
}
81+
}
82+
83+
if (ctx.cts) {
84+
details[CROSS_TAB_SESSION] = ctx.cts;
85+
}
6786
}
6887

6988
/**
@@ -85,11 +104,11 @@ class PxClient {
85104

86105
sendEnforcerTelemetry(updateReason, config) {
87106
const details = {
88-
'enforcer_configs': pxUtil.filterConfig(config),
89-
'node_name': os.hostname(),
90-
'os_name': os.platform(),
91-
'update_reason': updateReason,
92-
'module_version': config.MODULE_VERSION
107+
enforcer_configs: pxUtil.filterConfig(config),
108+
node_name: os.hostname(),
109+
os_name: os.platform(),
110+
update_reason: updateReason,
111+
module_version: config.MODULE_VERSION,
93112
};
94113

95114
const pxData = {};
@@ -119,9 +138,9 @@ class PxClient {
119138

120139
createHeaders(config, additionalHeaders = {}) {
121140
return {
122-
'Authorization': 'Bearer ' + config.AUTH_TOKEN,
141+
Authorization: 'Bearer ' + config.AUTH_TOKEN,
123142
'Content-Type': 'application/json',
124-
...additionalHeaders
143+
...additionalHeaders,
125144
};
126145
}
127146

@@ -135,7 +154,7 @@ class PxClient {
135154
[CI_VERSION_FIELD]: loginCredentials && loginCredentials.version,
136155
[CI_RAW_USERNAME_FIELD]: loginCredentials && loginCredentials.rawUsername,
137156
[CI_SSO_STEP_FIELD]: loginCredentials && loginCredentials.ssoStep,
138-
...additionalDetails
157+
...additionalDetails,
139158
};
140159

141160
if (!config.SEND_RAW_USERNAME_ON_ADDITIONAL_S2S_ACTIVITY || !details.credentials_compromised) {

lib/pxconfig.js

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,12 @@ class PxConfig {
9292
['LOGIN_SUCCESSFUL_BODY_REGEX', 'px_login_successful_body_regex'],
9393
['LOGIN_SUCCESSFUL_CUSTOM_CALLBACK', 'px_login_successful_custom_callback'],
9494
['MODIFY_CONTEXT', 'px_modify_context'],
95+
['JWT_COOKIE_NAME', 'px_jwt_cookie_name'],
96+
['JWT_COOKIE_USER_ID_FIELD_NAME', 'px_jwt_cookie_user_id_field_name'],
97+
['JWT_COOKIE_ADDITIONAL_FIELD_NAMES', 'px_jwt_cookie_additional_field_names'],
98+
['JWT_HEADER_NAME', 'px_jwt_header_name'],
99+
['JWT_HEADER_USER_ID_FIELD_NAME', 'px_jwt_header_user_id_field_name'],
100+
['JWT_HEADER_ADDITIONAL_FIELD_NAMES', 'px_jwt_header_additional_field_names'],
95101
];
96102

97103
configKeyMapping.forEach(([targetKey, sourceKey]) => {
@@ -336,7 +342,13 @@ function pxDefaultConfig() {
336342
LOGIN_SUCCESSFUL_BODY_REGEX: '',
337343
LOGIN_SUCCESSFUL_CUSTOM_CALLBACK: null,
338344
MODIFY_CONTEXT: null,
339-
GRAPHQL_ROUTES: ['^/graphql$']
345+
GRAPHQL_ROUTES: ['^/graphql$'],
346+
JWT_COOKIE_NAME: '',
347+
JWT_COOKIE_USER_ID_FIELD_NAME: '',
348+
JWT_COOKIE_ADDITIONAL_FIELD_NAMES: [],
349+
JWT_HEADER_NAME: '',
350+
JWT_HEADER_USER_ID_FIELD_NAME: '',
351+
JWT_HEADER_ADDITIONAL_FIELD_NAMES: [],
340352
};
341353
}
342354

@@ -398,7 +410,13 @@ const allowedConfigKeys = [
398410
'px_login_successful_body_regex',
399411
'px_login_successful_custom_callback',
400412
'px_modify_context',
401-
'px_graphql_routes'
413+
'px_graphql_routes',
414+
'px_jwt_cookie_name',
415+
'px_jwt_cookie_user_id_field_name',
416+
'px_jwt_cookie_additional_field_names',
417+
'px_jwt_header_name',
418+
'px_jwt_header_user_id_field_name',
419+
'px_jwt_header_additional_field_names',
402420
];
403421

404422
module.exports = PxConfig;

lib/pxcontext.js

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ const { v4: uuidv4 } = require('uuid');
22

33
const { CookieOrigin } = require('./enums/CookieOrigin');
44
const pxUtil = require('./pxutil');
5+
const pxJWT = require('./pxjwt');
56

67
class PxContext {
78
constructor(config, req, additionalFields) {
@@ -30,6 +31,7 @@ class PxContext {
3031
this.cookieOrigin = CookieOrigin.COOKIE;
3132
this.additionalFields = additionalFields || {};
3233
this.signedFields = [this.userAgent];
34+
3335
const mobileHeader = this.headers[mobileSdkHeader];
3436
if (mobileHeader !== undefined) {
3537
this.signedFields = null;
@@ -51,23 +53,36 @@ class PxContext {
5153
} else if ((key === '_pxvid' || key === 'pxvid') && vidRegex.test(cookies[key])) {
5254
this.vid = cookies[key];
5355
this.vidSource = 'vid_cookie';
56+
} else if (key === 'pxcts') {
57+
this.cts = cookies[key];
5458
} else if (key.match(/^_px.+$/)) {
5559
this.cookies[key] = cookies[key];
5660
}
5761
});
5862
}
5963
if (pxUtil.isGraphql(req, config)) {
6064
config.logger.debug('Graphql route detected');
61-
this.graphqlData = this.getGraphqlDataFromBody(req.body).filter(x => x).map(
62-
operation => operation && {
63-
...operation,
64-
sensitive: pxUtil.isSensitiveGraphqlOperation(operation, config),
65-
});
66-
this.sensitiveGraphqlOperation = this.graphqlData.some(operation => operation && operation.sensitive);
65+
this.graphqlData = this.getGraphqlDataFromBody(req.body)
66+
.filter((x) => x)
67+
.map(
68+
(operation) =>
69+
operation && {
70+
...operation,
71+
sensitive: pxUtil.isSensitiveGraphqlOperation(operation, config),
72+
},
73+
);
74+
this.sensitiveGraphqlOperation = this.graphqlData.some((operation) => operation && operation.sensitive);
6775
}
6876
if (process.env.AWS_REGION) {
6977
this.serverInfoRegion = process.env.AWS_REGION;
7078
}
79+
80+
if (config.JWT_COOKIE_NAME || config.JWT_HEADER_NAME) {
81+
const token = req.cookies[config.JWT_COOKIE_NAME] || req.headers[config.JWT_HEADER_NAME];
82+
if (token) {
83+
this.jwt = pxJWT.extractJWTData(config, token);
84+
}
85+
}
7186
}
7287

7388
getGraphqlDataFromBody(body) {
@@ -77,9 +92,7 @@ class PxContext {
7792
} else if (typeof body === 'object') {
7893
jsonBody = body;
7994
}
80-
return Array.isArray(jsonBody) ?
81-
jsonBody.map(pxUtil.getGraphqlData) :
82-
[pxUtil.getGraphqlData(jsonBody)];
95+
return Array.isArray(jsonBody) ? jsonBody.map(pxUtil.getGraphqlData) : [pxUtil.getGraphqlData(jsonBody)];
8396
}
8497

8598
getCookie() {

lib/pxjwt.js

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
const { TOKEN_SEPARATOR } = require('./utils/constants');
2+
3+
function getJWTPayload(pxConfig, token) {
4+
try {
5+
const encodedPayload = token.split(TOKEN_SEPARATOR)[1];
6+
if (encodedPayload) {
7+
const base64Payload = encodedPayload.replace('-', '+').replace('_', '/');
8+
const payload = Buffer.from(base64Payload, 'base64').toString();
9+
return JSON.parse(payload);
10+
}
11+
} catch (e) {
12+
pxConfig.logger.debug(`Failed to parse JWT token ${token}: ${e.message} `);
13+
}
14+
15+
return null;
16+
}
17+
18+
function getJWTData(pxConfig, payload) {
19+
let additionalFields = null;
20+
21+
try {
22+
const userFieldName = pxConfig.JWT_COOKIE_USER_ID_FIELD_NAME || pxConfig.JWT_HEADER_USER_ID_FIELD_NAME;
23+
const userID = payload[userFieldName];
24+
25+
const additionalFieldsConfig =
26+
pxConfig.JWT_COOKIE_ADDITIONAL_FIELD_NAMES.length > 0
27+
? pxConfig.JWT_COOKIE_ADDITIONAL_FIELD_NAMES
28+
: pxConfig.JWT_HEADER_ADDITIONAL_FIELD_NAMES;
29+
30+
if (additionalFieldsConfig && additionalFieldsConfig.length > 0) {
31+
additionalFields = additionalFieldsConfig.reduce((matchedFields, fieldName) => {
32+
if (payload[fieldName]) {
33+
matchedFields[fieldName] = payload[fieldName];
34+
}
35+
return matchedFields;
36+
}, {});
37+
}
38+
39+
return { userID, additionalFields };
40+
} catch (e) {
41+
pxConfig.logger.debug(`Failed to extract JWT token ${payload}: ${e.message} `);
42+
}
43+
44+
return null;
45+
}
46+
47+
function extractJWTData(pxConfig, token) {
48+
const payload = getJWTPayload(pxConfig, token);
49+
50+
if (!payload) {
51+
return null;
52+
}
53+
54+
return getJWTData(pxConfig, payload);
55+
}
56+
57+
module.exports = {
58+
extractJWTData,
59+
};

lib/pxutil.js

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -249,7 +249,8 @@ function generateHMAC(cookieSecret, payload) {
249249

250250
function isReqInMonitorMode(pxConfig, pxCtx) {
251251
return (
252-
(pxConfig.MODULE_MODE === ModuleMode.MONITOR && !pxCtx.shouldBypassMonitor && !pxCtx.enforcedRoute) || (pxCtx.monitoredRoute && !pxCtx.shouldBypassMonitor)
252+
(pxConfig.MODULE_MODE === ModuleMode.MONITOR && !pxCtx.shouldBypassMonitor && !pxCtx.enforcedRoute) ||
253+
(pxCtx.monitoredRoute && !pxCtx.shouldBypassMonitor)
253254
);
254255
}
255256

@@ -277,7 +278,7 @@ function isGraphql(req, config) {
277278
return false;
278279
}
279280
try {
280-
return routes.some(r => new RegExp(r).test(req.baseUrl || '' + req.path));
281+
return routes.some((r) => new RegExp(r).test(req.baseUrl || '' + req.path));
281282
} catch (e) {
282283
config.logger.error(`Failed to process graphql routes. exception: ${e}`);
283284
return false;
@@ -311,8 +312,10 @@ function isSensitiveGraphqlOperation(graphqlData, config) {
311312
if (!graphqlData) {
312313
return false;
313314
} else {
314-
return (config.SENSITIVE_GRAPHQL_OPERATION_TYPES.includes(graphqlData.type) ||
315-
config.SENSITIVE_GRAPHQL_OPERATION_NAMES.includes(graphqlData.name));
315+
return (
316+
config.SENSITIVE_GRAPHQL_OPERATION_TYPES.includes(graphqlData.type) ||
317+
config.SENSITIVE_GRAPHQL_OPERATION_NAMES.includes(graphqlData.name)
318+
);
316319
}
317320
}
318321

@@ -328,19 +331,16 @@ function getGraphqlData(graphqlBodyObject) {
328331
return null;
329332
}
330333

331-
const selectedOperationName = graphqlBodyObject['operationName'] ||
332-
(Object.keys(parsedData).length === 1 && Object.keys(parsedData)[0]);
334+
const selectedOperationName =
335+
graphqlBodyObject['operationName'] || (Object.keys(parsedData).length === 1 && Object.keys(parsedData)[0]);
333336

334337
if (!selectedOperationName || !parsedData[selectedOperationName]) {
335338
return null;
336339
}
337340

338341
const variables = extractVariables(graphqlBodyObject.variables);
339342

340-
return new GraphqlData(parsedData[selectedOperationName],
341-
selectedOperationName,
342-
variables,
343-
);
343+
return new GraphqlData(parsedData[selectedOperationName], selectedOperationName, variables);
344344
}
345345

346346
// input: object representing variables

0 commit comments

Comments
 (0)