Skip to content

Commit

Permalink
Merge pull request #270 from PerimeterX/release/v3.8.0
Browse files Browse the repository at this point in the history
Release/v3.8.0 - user Identifiers support (for AD)
  • Loading branch information
chen-zimmer-px authored Jan 26, 2023
2 parents 1ead16e + d43a571 commit 798bd37
Show file tree
Hide file tree
Showing 11 changed files with 192 additions and 40 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/).

## [3.8.0] - 2023-01-25

### Added
- Support User Identifiers: CTS and JWT.

## [3.7.0] - 2023-01-15

### Added
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
[PerimeterX](http://www.perimeterx.com) Shared base for NodeJS enforcers
=============================================================

> Latest stable version: [v3.7.0](https://www.npmjs.com/package/perimeterx-node-core)
> Latest stable version: [v3.8.0](https://www.npmjs.com/package/perimeterx-node-core)
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.

Expand Down
34 changes: 31 additions & 3 deletions lib/pxapi.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,16 @@ const { ModuleMode } = require('./enums/ModuleMode');
const PassReason = require('./enums/PassReason');
const ScoreEvaluateAction = require('./enums/ScoreEvaluateAction');
const S2SErrorReason = require('./enums/S2SErrorReason');
const { CI_USERNAME_FIELD, CI_PASSWORD_FIELD, CI_VERSION_FIELD, CI_SSO_STEP_FIELD,
GQL_OPERATIONS_FIELD

const {
CI_USERNAME_FIELD,
CI_PASSWORD_FIELD,
CI_VERSION_FIELD,
CI_SSO_STEP_FIELD,
GQL_OPERATIONS_FIELD,
JWT_ADDITIONAL_FIELDS_FIELD_NAME,
APP_USER_ID_FIELD_NAME,
CROSS_TAB_SESSION,
} = require('./utils/constants');
const { CIVersion } = require('./enums/CIVersion');

Expand Down Expand Up @@ -78,11 +86,31 @@ function buildRequestData(ctx, config) {
}
}

if (ctx.jwt) {
const { userID, additionalFields } = ctx.jwt;

if (userID) {
data.additional[APP_USER_ID_FIELD_NAME] = userID;
}

if (additionalFields) {
data.additional[JWT_ADDITIONAL_FIELDS_FIELD_NAME] = additionalFields;
}
}

if (ctx.cts) {
data.additional[CROSS_TAB_SESSION] = ctx.cts;
}

if (ctx.s2sCallReason === 'cookie_decryption_failed') {
data.additional.px_orig_cookie = ctx.getCookie(); //No need strigify, already a string
}

if (ctx.s2sCallReason === 'cookie_expired' || ctx.s2sCallReason === 'cookie_validation_failed' || ctx.s2sCallReason === 'sensitive_route') {
if (
ctx.s2sCallReason === 'cookie_expired' ||
ctx.s2sCallReason === 'cookie_validation_failed' ||
ctx.s2sCallReason === 'sensitive_route'
) {
data.additional.px_cookie = JSON.stringify(ctx.decodedCookie);
}

Expand Down
37 changes: 28 additions & 9 deletions lib/pxclient.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@ const {
CI_SSO_STEP_FIELD,
CI_RAW_USERNAME_FIELD,
CI_CREDENTIALS_COMPROMISED_FIELD,
GQL_OPERATIONS_FIELD
GQL_OPERATIONS_FIELD,
APP_USER_ID_FIELD_NAME,
JWT_ADDITIONAL_FIELDS_FIELD_NAME,
CROSS_TAB_SESSION,
} = require('./utils/constants');

class PxClient {
Expand Down Expand Up @@ -64,6 +67,22 @@ class PxClient {
if (ctx.graphqlData) {
details[GQL_OPERATIONS_FIELD] = ctx.graphqlData;
}

if (ctx.jwt) {
const { userID, additionalFields } = ctx.jwt;

if (userID) {
details[APP_USER_ID_FIELD_NAME] = userID;
}

if (additionalFields) {
details[JWT_ADDITIONAL_FIELDS_FIELD_NAME] = additionalFields;
}
}

if (ctx.cts) {
details[CROSS_TAB_SESSION] = ctx.cts;
}
}

/**
Expand All @@ -85,11 +104,11 @@ class PxClient {

sendEnforcerTelemetry(updateReason, config) {
const details = {
'enforcer_configs': pxUtil.filterConfig(config),
'node_name': os.hostname(),
'os_name': os.platform(),
'update_reason': updateReason,
'module_version': config.MODULE_VERSION
enforcer_configs: pxUtil.filterConfig(config),
node_name: os.hostname(),
os_name: os.platform(),
update_reason: updateReason,
module_version: config.MODULE_VERSION,
};

const pxData = {};
Expand Down Expand Up @@ -119,9 +138,9 @@ class PxClient {

createHeaders(config, additionalHeaders = {}) {
return {
'Authorization': 'Bearer ' + config.AUTH_TOKEN,
Authorization: 'Bearer ' + config.AUTH_TOKEN,
'Content-Type': 'application/json',
...additionalHeaders
...additionalHeaders,
};
}

Expand All @@ -135,7 +154,7 @@ class PxClient {
[CI_VERSION_FIELD]: loginCredentials && loginCredentials.version,
[CI_RAW_USERNAME_FIELD]: loginCredentials && loginCredentials.rawUsername,
[CI_SSO_STEP_FIELD]: loginCredentials && loginCredentials.ssoStep,
...additionalDetails
...additionalDetails,
};

if (!config.SEND_RAW_USERNAME_ON_ADDITIONAL_S2S_ACTIVITY || !details.credentials_compromised) {
Expand Down
22 changes: 20 additions & 2 deletions lib/pxconfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,12 @@ class PxConfig {
['LOGIN_SUCCESSFUL_BODY_REGEX', 'px_login_successful_body_regex'],
['LOGIN_SUCCESSFUL_CUSTOM_CALLBACK', 'px_login_successful_custom_callback'],
['MODIFY_CONTEXT', 'px_modify_context'],
['JWT_COOKIE_NAME', 'px_jwt_cookie_name'],
['JWT_COOKIE_USER_ID_FIELD_NAME', 'px_jwt_cookie_user_id_field_name'],
['JWT_COOKIE_ADDITIONAL_FIELD_NAMES', 'px_jwt_cookie_additional_field_names'],
['JWT_HEADER_NAME', 'px_jwt_header_name'],
['JWT_HEADER_USER_ID_FIELD_NAME', 'px_jwt_header_user_id_field_name'],
['JWT_HEADER_ADDITIONAL_FIELD_NAMES', 'px_jwt_header_additional_field_names'],
];

configKeyMapping.forEach(([targetKey, sourceKey]) => {
Expand Down Expand Up @@ -336,7 +342,13 @@ function pxDefaultConfig() {
LOGIN_SUCCESSFUL_BODY_REGEX: '',
LOGIN_SUCCESSFUL_CUSTOM_CALLBACK: null,
MODIFY_CONTEXT: null,
GRAPHQL_ROUTES: ['^/graphql$']
GRAPHQL_ROUTES: ['^/graphql$'],
JWT_COOKIE_NAME: '',
JWT_COOKIE_USER_ID_FIELD_NAME: '',
JWT_COOKIE_ADDITIONAL_FIELD_NAMES: [],
JWT_HEADER_NAME: '',
JWT_HEADER_USER_ID_FIELD_NAME: '',
JWT_HEADER_ADDITIONAL_FIELD_NAMES: [],
};
}

Expand Down Expand Up @@ -398,7 +410,13 @@ const allowedConfigKeys = [
'px_login_successful_body_regex',
'px_login_successful_custom_callback',
'px_modify_context',
'px_graphql_routes'
'px_graphql_routes',
'px_jwt_cookie_name',
'px_jwt_cookie_user_id_field_name',
'px_jwt_cookie_additional_field_names',
'px_jwt_header_name',
'px_jwt_header_user_id_field_name',
'px_jwt_header_additional_field_names',
];

module.exports = PxConfig;
31 changes: 22 additions & 9 deletions lib/pxcontext.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ const { v4: uuidv4 } = require('uuid');

const { CookieOrigin } = require('./enums/CookieOrigin');
const pxUtil = require('./pxutil');
const pxJWT = require('./pxjwt');

class PxContext {
constructor(config, req, additionalFields) {
Expand Down Expand Up @@ -30,6 +31,7 @@ class PxContext {
this.cookieOrigin = CookieOrigin.COOKIE;
this.additionalFields = additionalFields || {};
this.signedFields = [this.userAgent];

const mobileHeader = this.headers[mobileSdkHeader];
if (mobileHeader !== undefined) {
this.signedFields = null;
Expand All @@ -51,23 +53,36 @@ class PxContext {
} else if ((key === '_pxvid' || key === 'pxvid') && vidRegex.test(cookies[key])) {
this.vid = cookies[key];
this.vidSource = 'vid_cookie';
} else if (key === 'pxcts') {
this.cts = cookies[key];
} else if (key.match(/^_px.+$/)) {
this.cookies[key] = cookies[key];
}
});
}
if (pxUtil.isGraphql(req, config)) {
config.logger.debug('Graphql route detected');
this.graphqlData = this.getGraphqlDataFromBody(req.body).filter(x => x).map(
operation => operation && {
...operation,
sensitive: pxUtil.isSensitiveGraphqlOperation(operation, config),
});
this.sensitiveGraphqlOperation = this.graphqlData.some(operation => operation && operation.sensitive);
this.graphqlData = this.getGraphqlDataFromBody(req.body)
.filter((x) => x)
.map(
(operation) =>
operation && {
...operation,
sensitive: pxUtil.isSensitiveGraphqlOperation(operation, config),
},
);
this.sensitiveGraphqlOperation = this.graphqlData.some((operation) => operation && operation.sensitive);
}
if (process.env.AWS_REGION) {
this.serverInfoRegion = process.env.AWS_REGION;
}

if (config.JWT_COOKIE_NAME || config.JWT_HEADER_NAME) {
const token = req.cookies[config.JWT_COOKIE_NAME] || req.headers[config.JWT_HEADER_NAME];
if (token) {
this.jwt = pxJWT.extractJWTData(config, token);
}
}
}

getGraphqlDataFromBody(body) {
Expand All @@ -77,9 +92,7 @@ class PxContext {
} else if (typeof body === 'object') {
jsonBody = body;
}
return Array.isArray(jsonBody) ?
jsonBody.map(pxUtil.getGraphqlData) :
[pxUtil.getGraphqlData(jsonBody)];
return Array.isArray(jsonBody) ? jsonBody.map(pxUtil.getGraphqlData) : [pxUtil.getGraphqlData(jsonBody)];
}

getCookie() {
Expand Down
59 changes: 59 additions & 0 deletions lib/pxjwt.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
const { TOKEN_SEPARATOR } = require('./utils/constants');

function getJWTPayload(pxConfig, token) {
try {
const encodedPayload = token.split(TOKEN_SEPARATOR)[1];
if (encodedPayload) {
const base64Payload = encodedPayload.replace('-', '+').replace('_', '/');
const payload = Buffer.from(base64Payload, 'base64').toString();
return JSON.parse(payload);
}
} catch (e) {
pxConfig.logger.debug(`Failed to parse JWT token ${token}: ${e.message} `);
}

return null;
}

function getJWTData(pxConfig, payload) {
let additionalFields = null;

try {
const userFieldName = pxConfig.JWT_COOKIE_USER_ID_FIELD_NAME || pxConfig.JWT_HEADER_USER_ID_FIELD_NAME;
const userID = payload[userFieldName];

const additionalFieldsConfig =
pxConfig.JWT_COOKIE_ADDITIONAL_FIELD_NAMES.length > 0
? pxConfig.JWT_COOKIE_ADDITIONAL_FIELD_NAMES
: pxConfig.JWT_HEADER_ADDITIONAL_FIELD_NAMES;

if (additionalFieldsConfig && additionalFieldsConfig.length > 0) {
additionalFields = additionalFieldsConfig.reduce((matchedFields, fieldName) => {
if (payload[fieldName]) {
matchedFields[fieldName] = payload[fieldName];
}
return matchedFields;
}, {});
}

return { userID, additionalFields };
} catch (e) {
pxConfig.logger.debug(`Failed to extract JWT token ${payload}: ${e.message} `);
}

return null;
}

function extractJWTData(pxConfig, token) {
const payload = getJWTPayload(pxConfig, token);

if (!payload) {
return null;
}

return getJWTData(pxConfig, payload);
}

module.exports = {
extractJWTData,
};
20 changes: 10 additions & 10 deletions lib/pxutil.js
Original file line number Diff line number Diff line change
Expand Up @@ -249,7 +249,8 @@ function generateHMAC(cookieSecret, payload) {

function isReqInMonitorMode(pxConfig, pxCtx) {
return (
(pxConfig.MODULE_MODE === ModuleMode.MONITOR && !pxCtx.shouldBypassMonitor && !pxCtx.enforcedRoute) || (pxCtx.monitoredRoute && !pxCtx.shouldBypassMonitor)
(pxConfig.MODULE_MODE === ModuleMode.MONITOR && !pxCtx.shouldBypassMonitor && !pxCtx.enforcedRoute) ||
(pxCtx.monitoredRoute && !pxCtx.shouldBypassMonitor)
);
}

Expand Down Expand Up @@ -277,7 +278,7 @@ function isGraphql(req, config) {
return false;
}
try {
return routes.some(r => new RegExp(r).test(req.baseUrl || '' + req.path));
return routes.some((r) => new RegExp(r).test(req.baseUrl || '' + req.path));
} catch (e) {
config.logger.error(`Failed to process graphql routes. exception: ${e}`);
return false;
Expand Down Expand Up @@ -311,8 +312,10 @@ function isSensitiveGraphqlOperation(graphqlData, config) {
if (!graphqlData) {
return false;
} else {
return (config.SENSITIVE_GRAPHQL_OPERATION_TYPES.includes(graphqlData.type) ||
config.SENSITIVE_GRAPHQL_OPERATION_NAMES.includes(graphqlData.name));
return (
config.SENSITIVE_GRAPHQL_OPERATION_TYPES.includes(graphqlData.type) ||
config.SENSITIVE_GRAPHQL_OPERATION_NAMES.includes(graphqlData.name)
);
}
}

Expand All @@ -328,19 +331,16 @@ function getGraphqlData(graphqlBodyObject) {
return null;
}

const selectedOperationName = graphqlBodyObject['operationName'] ||
(Object.keys(parsedData).length === 1 && Object.keys(parsedData)[0]);
const selectedOperationName =
graphqlBodyObject['operationName'] || (Object.keys(parsedData).length === 1 && Object.keys(parsedData)[0]);

if (!selectedOperationName || !parsedData[selectedOperationName]) {
return null;
}

const variables = extractVariables(graphqlBodyObject.variables);

return new GraphqlData(parsedData[selectedOperationName],
selectedOperationName,
variables,
);
return new GraphqlData(parsedData[selectedOperationName], selectedOperationName, variables);
}

// input: object representing variables
Expand Down
Loading

0 comments on commit 798bd37

Please sign in to comment.