From 9875ed88bfec80750185404fa98b5154e95504b9 Mon Sep 17 00:00:00 2001 From: Ori Gold Date: Tue, 28 Oct 2025 12:38:59 -0700 Subject: [PATCH 1/3] feat: jwt user identifiers feature added --- .../JwtUserIdentifiersExtractor.java | 105 ++++++++++++++++++ .../java/com/perimeterx/models/PXContext.java | 11 ++ .../activities/CommonActivityDetails.java | 9 ++ .../models/configuration/PXConfiguration.java | 21 ++++ .../models/httpmodels/Additional.java | 9 ++ 5 files changed, 155 insertions(+) create mode 100644 src/main/java/com/perimeterx/internals/JwtUserIdentifiersExtractor.java diff --git a/src/main/java/com/perimeterx/internals/JwtUserIdentifiersExtractor.java b/src/main/java/com/perimeterx/internals/JwtUserIdentifiersExtractor.java new file mode 100644 index 00000000..a1f8b545 --- /dev/null +++ b/src/main/java/com/perimeterx/internals/JwtUserIdentifiersExtractor.java @@ -0,0 +1,105 @@ +package com.perimeterx.internals; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.perimeterx.models.PXContext; +import com.perimeterx.models.configuration.PXConfiguration; + +import javax.servlet.http.Cookie; +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.util.*; + +public final class JwtUserIdentifiersExtractor { + private static final ObjectMapper OM = new ObjectMapper(); + + private JwtUserIdentifiersExtractor() {} + + public static void attachJwtIfConfigured(PXContext ctx, PXConfiguration cfg) { + tryExtractFromCookie(ctx, cfg); + if (ctx.getJwtAppUserId() != null || (ctx.getJwtAdditionalFields() != null && !ctx.getJwtAdditionalFields().isEmpty())) { + return; + } + tryExtractFromHeader(ctx, cfg); + } + + private static void tryExtractFromCookie(PXContext ctx, PXConfiguration cfg) { + String cookieName = nullToEmpty(cfg.getPxJwtCookieName()); + if (cookieName.isEmpty()) return; + String token = findCookieValue(ctx, cookieName); + buildAndSet(ctx, token, cfg.getPxJwtCookieUserIdFieldName(), safeList(cfg.getPxJwtCookieAdditionalFieldNames())); + } + + private static void tryExtractFromHeader(PXContext ctx, PXConfiguration cfg) { + String headerName = nullToEmpty(cfg.getPxJwtHeaderName()); + if (headerName.isEmpty()) return; + String raw = ctx.getRequest().getHeader(headerName); + if (raw == null) return; + String token = raw.startsWith("Bearer ") ? raw.substring(7).trim() : raw.trim(); + buildAndSet(ctx, token, cfg.getPxJwtHeaderUserIdFieldName(), safeList(cfg.getPxJwtHeaderAdditionalFieldNames())); + } + + private static void buildAndSet(PXContext ctx, String token, String userPath, List additionalPaths) { + if (isEmpty(token)) return; + try { + Map payload = decodePayload(token); + String appUserId = asString(getByDotPath(payload, userPath)); + Map additional = collectByDotPaths(payload, additionalPaths); + if (isEmpty(appUserId) && additional.isEmpty()) return; + ctx.setJwtAppUserId(appUserId); + ctx.setJwtAdditionalFields(additional.isEmpty() ? null : additional); + } catch (Exception e) { + ctx.logger.debug("JWT extraction skipped: invalid token", e); + } + } + + private static Map decodePayload(String jwt) throws Exception { + String[] parts = jwt.split("\\."); + if (parts.length < 2) throw new IllegalArgumentException("Invalid JWT"); + byte[] json = Base64.getUrlDecoder().decode(parts[1]); + return OM.readValue(json, Map.class); + } + + private static String findCookieValue(PXContext ctx, String name) { + Cookie[] cookies = ctx.getRequest().getCookies(); + if (cookies == null) return null; + for (Cookie c : cookies) { + if (name.equals(c.getName())) { + String v = c.getValue(); + try { + return v != null ? URLDecoder.decode(v, StandardCharsets.UTF_8.toString()) : null; + } catch (UnsupportedEncodingException e) { + return v; + } + } + } + return null; + } + + private static Object getByDotPath(Map json, String path) { + if (json == null || isEmpty(path)) return null; + Object cur = json; + for (String seg : path.split("\\.")) { + if (!(cur instanceof Map)) return null; + cur = ((Map) cur).get(seg); + if (cur == null) return null; + } + return cur; + } + + private static Map collectByDotPaths(Map json, List paths) { + Map out = new LinkedHashMap<>(); + for (String p : paths) { + Object v = getByDotPath(json, p); + if (v != null) out.put(p, v); + } + return out; + } + + private static List safeList(List l) { return l == null ? Collections.emptyList() : l; } + private static boolean isEmpty(String s) { return s == null || s.isEmpty(); } + private static String nullToEmpty(String s) { return s == null ? "" : s; } + private static String asString(Object v) { return v == null ? null : String.valueOf(v); } +} + + diff --git a/src/main/java/com/perimeterx/models/PXContext.java b/src/main/java/com/perimeterx/models/PXContext.java index 56b7ddd4..cab4b02a 100644 --- a/src/main/java/com/perimeterx/models/PXContext.java +++ b/src/main/java/com/perimeterx/models/PXContext.java @@ -13,6 +13,7 @@ import com.perimeterx.internals.cookie.cookieparsers.CookieHeaderParser; import com.perimeterx.internals.cookie.cookieparsers.HeaderParser; import com.perimeterx.internals.cookie.cookieparsers.MobileCookieHeaderParser; +import com.perimeterx.internals.JwtUserIdentifiersExtractor; import com.perimeterx.models.configuration.ModuleMode; import com.perimeterx.models.configuration.PXConfiguration; import com.perimeterx.models.enforcererror.EnforcerErrorReasonInfo; @@ -235,6 +236,10 @@ public class PXContext { private boolean isSensitiveRequest; private String additionalTokenInfo; + // JWT user identifiers + private String jwtAppUserId; + private Map jwtAdditionalFields; + /** * The cookie key used to decrypt the cookie */ @@ -306,6 +311,12 @@ private void postInitContext(final HttpServletRequest request, PXConfiguration p } catch (Exception e) { logger.debug("failed to extract custom parameters from custom function", e); } + + try { + JwtUserIdentifiersExtractor.attachJwtIfConfigured(this, pxConfiguration); + } catch (Exception e) { + logger.debug("jwt identifiers extraction failed", e); + } } private IPXLogger getLogger(){ diff --git a/src/main/java/com/perimeterx/models/activities/CommonActivityDetails.java b/src/main/java/com/perimeterx/models/activities/CommonActivityDetails.java index f268f5de..8fbde006 100644 --- a/src/main/java/com/perimeterx/models/activities/CommonActivityDetails.java +++ b/src/main/java/com/perimeterx/models/activities/CommonActivityDetails.java @@ -9,6 +9,7 @@ import com.perimeterx.models.httpmodels.Additional; import lombok.Getter; +import java.util.Map; import java.util.UUID; @Getter @@ -62,6 +63,12 @@ public class CommonActivityDetails implements ActivityDetails { @JsonProperty("cross_tab_session") public String pxCtsCookie; + @JsonProperty("app_user_id") + public String appUserId; + + @JsonProperty("jwt_additional_fields") + public Map jwtAdditionalFields; + @JsonProperty("is_sensitive_route") public Boolean isSensitiveRoute; @@ -91,6 +98,8 @@ public CommonActivityDetails(PXContext context) { this.riskStartTime = additional.riskStartTime; this.enforcerStartTime = additional.enforcerStartTime; this.pxCtsCookie = additional.pxCtsCookie; + this.appUserId = additional.appUserId; + this.jwtAdditionalFields = additional.jwtAdditionalFields; this.isSensitiveRoute = additional.isSensitiveRoute; this.additionalTokenInfo = additional.additionalTokenInfo; } diff --git a/src/main/java/com/perimeterx/models/configuration/PXConfiguration.java b/src/main/java/com/perimeterx/models/configuration/PXConfiguration.java index 8bb1baad..c1e30b89 100644 --- a/src/main/java/com/perimeterx/models/configuration/PXConfiguration.java +++ b/src/main/java/com/perimeterx/models/configuration/PXConfiguration.java @@ -334,6 +334,27 @@ public static void setPxLoggerSeverity(LoggerSeverity severity) { @Builder.Default private Predicate filterByCustomFunction = req -> false; + + // --- JWT user identifiers configuration --- + @JsonProperty("px_jwt_cookie_name") + private String pxJwtCookieName; + + @JsonProperty("px_jwt_cookie_user_id_field_name") + private String pxJwtCookieUserIdFieldName; + + @Builder.Default + @JsonProperty("px_jwt_cookie_additional_field_names") + private List pxJwtCookieAdditionalFieldNames = new ArrayList<>(); + + @JsonProperty("px_jwt_header_name") + private String pxJwtHeaderName; + + @JsonProperty("px_jwt_header_user_id_field_name") + private String pxJwtHeaderUserIdFieldName; + + @Builder.Default + @JsonProperty("px_jwt_header_additional_field_names") + private List pxJwtHeaderAdditionalFieldNames = new ArrayList<>(); /** * @return Configuration Object clone without cookieKey and authToken **/ diff --git a/src/main/java/com/perimeterx/models/httpmodels/Additional.java b/src/main/java/com/perimeterx/models/httpmodels/Additional.java index 46235630..aebce120 100644 --- a/src/main/java/com/perimeterx/models/httpmodels/Additional.java +++ b/src/main/java/com/perimeterx/models/httpmodels/Additional.java @@ -12,6 +12,7 @@ import com.perimeterx.utils.Constants; import java.util.Date; +import java.util.Map; import java.util.Objects; import java.util.UUID; @@ -95,6 +96,12 @@ public class Additional { @JsonProperty("cross_tab_session") public String pxCtsCookie; + @JsonProperty("app_user_id") + public String appUserId; + + @JsonProperty("jwt_additional_fields") + public Map jwtAdditionalFields; + @JsonProperty("is_sensitive_route") public Boolean isSensitiveRoute; @@ -122,6 +129,8 @@ public static Additional fromContext(PXContext ctx) { additional.enforcerStartTime = ctx.getEnforcerStartTime(); additional.riskStartTime = new Date().getTime(); additional.pxCtsCookie = ctx.getPxCtsCookie(); + additional.appUserId = ctx.getJwtAppUserId(); + additional.jwtAdditionalFields = ctx.getJwtAdditionalFields(); additional.isSensitiveRoute = ctx.isSensitiveRequest(); additional.additionalTokenInfo = ctx.getAdditionalTokenInfo(); From ad25a41dc8794f0220862a3f187bd6cf4bd85f1f Mon Sep 17 00:00:00 2001 From: Ori Gold Date: Tue, 28 Oct 2025 12:39:44 -0700 Subject: [PATCH 2/3] chore: add user_identifiers config values to example site --- web/src/main/java/com/web/Config.java | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/web/src/main/java/com/web/Config.java b/web/src/main/java/com/web/Config.java index 462b3a05..ce771c49 100644 --- a/web/src/main/java/com/web/Config.java +++ b/web/src/main/java/com/web/Config.java @@ -160,6 +160,24 @@ public PXConfiguration getPxConfiguration() { case "px_secured_pxhd_enabled": builder.securedPxhdEnabled(enforcerConfig.getBoolean(key)); break; + case "px_jwt_cookie_name": + builder.pxJwtCookieName(enforcerConfig.getString(key)); + break; + case "px_jwt_cookie_user_id_field_name": + builder.pxJwtCookieUserIdFieldName(enforcerConfig.getString(key)); + break; + case "px_jwt_cookie_additional_field_names": + builder.pxJwtCookieAdditionalFieldNames(extractStringList(key)); + break; + case "px_jwt_header_name": + builder.pxJwtHeaderName(enforcerConfig.getString(key)); + break; + case "px_jwt_header_user_id_field_name": + builder.pxJwtHeaderUserIdFieldName(enforcerConfig.getString(key)); + break; + case "px_jwt_header_additional_field_names": + builder.pxJwtHeaderAdditionalFieldNames(extractStringList(key)); + break; case "px_user_agent_max_length": case "px_risk_cookie_max_length": case "px_risk_cookie_max_iterations": @@ -213,5 +231,14 @@ private int[] extractStatusCode(String key) { } return statusCode; } + + private java.util.List extractStringList(String key) { + final JSONArray jsonField = enforcerConfig.getJSONArray(key); + final java.util.List out = new java.util.ArrayList<>(jsonField.length()); + for (int i = 0; i < jsonField.length(); i++) { + out.add(jsonField.getString(i)); + } + return out; + } } From 6b918bcda3a8e86479331981dea315dac6a26199 Mon Sep 17 00:00:00 2001 From: Ori Gold Date: Tue, 28 Oct 2025 12:39:59 -0700 Subject: [PATCH 3/3] chore: add user_identifiers to px_metadata.json file --- px_metadata.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/px_metadata.json b/px_metadata.json index fc12507f..56ee33fe 100644 --- a/px_metadata.json +++ b/px_metadata.json @@ -36,6 +36,7 @@ "sensitive_headers", "sensitive_routes", "telemetry_command", + "user_identifiers", "vid_extraction" ], "excluded_tests": [ @@ -51,6 +52,7 @@ "test_cookie_v3_cookie_validation_failed_big_cookie", "test_client_ip_extraction_order_risk_api", "test_telemetry_command_verify_custom_function_hash", - "test_first_party_timeout" + "test_first_party_timeout", + "test_huge_jwt_(cookie|header)_with_user_id_and_additional_fields" ] }