Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion px_metadata.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
"sensitive_headers",
"sensitive_routes",
"telemetry_command",
"user_identifiers",
"vid_extraction"
],
"excluded_tests": [
Expand All @@ -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"
]
}
Original file line number Diff line number Diff line change
@@ -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<String> additionalPaths) {
if (isEmpty(token)) return;
try {
Map<String, Object> payload = decodePayload(token);
String appUserId = asString(getByDotPath(payload, userPath));
Map<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> collectByDotPaths(Map<String, Object> json, List<String> paths) {
Map<String, Object> out = new LinkedHashMap<>();
for (String p : paths) {
Object v = getByDotPath(json, p);
if (v != null) out.put(p, v);
}
return out;
}

private static List<String> safeList(List<String> 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); }
}


11 changes: 11 additions & 0 deletions src/main/java/com/perimeterx/models/PXContext.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -235,6 +236,10 @@ public class PXContext {
private boolean isSensitiveRequest;
private String additionalTokenInfo;

// JWT user identifiers
private String jwtAppUserId;
private Map<String, Object> jwtAdditionalFields;

/**
* The cookie key used to decrypt the cookie
*/
Expand Down Expand Up @@ -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(){
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import com.perimeterx.models.httpmodels.Additional;
import lombok.Getter;

import java.util.Map;
import java.util.UUID;

@Getter
Expand Down Expand Up @@ -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<String, Object> jwtAdditionalFields;

@JsonProperty("is_sensitive_route")
public Boolean isSensitiveRoute;

Expand Down Expand Up @@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,27 @@ public static void setPxLoggerSeverity(LoggerSeverity severity) {

@Builder.Default
private Predicate<HttpServletRequest> 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<String> 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<String> pxJwtHeaderAdditionalFieldNames = new ArrayList<>();
/**
* @return Configuration Object clone without cookieKey and authToken
**/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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<String, Object> jwtAdditionalFields;

@JsonProperty("is_sensitive_route")
public Boolean isSensitiveRoute;

Expand Down Expand Up @@ -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();

Expand Down
27 changes: 27 additions & 0 deletions web/src/main/java/com/web/Config.java
Original file line number Diff line number Diff line change
Expand Up @@ -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":
Expand Down Expand Up @@ -213,5 +231,14 @@ private int[] extractStatusCode(String key) {
}
return statusCode;
}

private java.util.List<String> extractStringList(String key) {
final JSONArray jsonField = enforcerConfig.getJSONArray(key);
final java.util.List<String> out = new java.util.ArrayList<>(jsonField.length());
for (int i = 0; i < jsonField.length(); i++) {
out.add(jsonField.getString(i));
}
return out;
}
}