|
| 1 | +package com.perimeterx.internals; |
| 2 | + |
| 3 | +import com.fasterxml.jackson.databind.ObjectMapper; |
| 4 | +import com.perimeterx.models.PXContext; |
| 5 | +import com.perimeterx.models.configuration.PXConfiguration; |
| 6 | + |
| 7 | +import javax.servlet.http.Cookie; |
| 8 | +import java.io.UnsupportedEncodingException; |
| 9 | +import java.net.URLDecoder; |
| 10 | +import java.nio.charset.StandardCharsets; |
| 11 | +import java.util.*; |
| 12 | + |
| 13 | +public final class JwtUserIdentifiersExtractor { |
| 14 | + private static final ObjectMapper OM = new ObjectMapper(); |
| 15 | + |
| 16 | + private JwtUserIdentifiersExtractor() {} |
| 17 | + |
| 18 | + public static void attachJwtIfConfigured(PXContext ctx, PXConfiguration cfg) { |
| 19 | + tryExtractFromCookie(ctx, cfg); |
| 20 | + if (ctx.getJwtAppUserId() != null || (ctx.getJwtAdditionalFields() != null && !ctx.getJwtAdditionalFields().isEmpty())) { |
| 21 | + return; |
| 22 | + } |
| 23 | + tryExtractFromHeader(ctx, cfg); |
| 24 | + } |
| 25 | + |
| 26 | + private static void tryExtractFromCookie(PXContext ctx, PXConfiguration cfg) { |
| 27 | + String cookieName = nullToEmpty(cfg.getPxJwtCookieName()); |
| 28 | + if (cookieName.isEmpty()) return; |
| 29 | + String token = findCookieValue(ctx, cookieName); |
| 30 | + buildAndSet(ctx, token, cfg.getPxJwtCookieUserIdFieldName(), safeList(cfg.getPxJwtCookieAdditionalFieldNames())); |
| 31 | + } |
| 32 | + |
| 33 | + private static void tryExtractFromHeader(PXContext ctx, PXConfiguration cfg) { |
| 34 | + String headerName = nullToEmpty(cfg.getPxJwtHeaderName()); |
| 35 | + if (headerName.isEmpty()) return; |
| 36 | + String raw = ctx.getRequest().getHeader(headerName); |
| 37 | + if (raw == null) return; |
| 38 | + String token = raw.startsWith("Bearer ") ? raw.substring(7).trim() : raw.trim(); |
| 39 | + buildAndSet(ctx, token, cfg.getPxJwtHeaderUserIdFieldName(), safeList(cfg.getPxJwtHeaderAdditionalFieldNames())); |
| 40 | + } |
| 41 | + |
| 42 | + private static void buildAndSet(PXContext ctx, String token, String userPath, List<String> additionalPaths) { |
| 43 | + if (isEmpty(token)) return; |
| 44 | + try { |
| 45 | + Map<String, Object> payload = decodePayload(token); |
| 46 | + String appUserId = asString(getByDotPath(payload, userPath)); |
| 47 | + Map<String, Object> additional = collectByDotPaths(payload, additionalPaths); |
| 48 | + if (isEmpty(appUserId) && additional.isEmpty()) return; |
| 49 | + ctx.setJwtAppUserId(appUserId); |
| 50 | + ctx.setJwtAdditionalFields(additional.isEmpty() ? null : additional); |
| 51 | + } catch (Exception e) { |
| 52 | + ctx.logger.debug("JWT extraction skipped: invalid token", e); |
| 53 | + } |
| 54 | + } |
| 55 | + |
| 56 | + private static Map<String, Object> decodePayload(String jwt) throws Exception { |
| 57 | + String[] parts = jwt.split("\\."); |
| 58 | + if (parts.length < 2) throw new IllegalArgumentException("Invalid JWT"); |
| 59 | + byte[] json = Base64.getUrlDecoder().decode(parts[1]); |
| 60 | + return OM.readValue(json, Map.class); |
| 61 | + } |
| 62 | + |
| 63 | + private static String findCookieValue(PXContext ctx, String name) { |
| 64 | + Cookie[] cookies = ctx.getRequest().getCookies(); |
| 65 | + if (cookies == null) return null; |
| 66 | + for (Cookie c : cookies) { |
| 67 | + if (name.equals(c.getName())) { |
| 68 | + String v = c.getValue(); |
| 69 | + try { |
| 70 | + return v != null ? URLDecoder.decode(v, StandardCharsets.UTF_8.toString()) : null; |
| 71 | + } catch (UnsupportedEncodingException e) { |
| 72 | + return v; |
| 73 | + } |
| 74 | + } |
| 75 | + } |
| 76 | + return null; |
| 77 | + } |
| 78 | + |
| 79 | + private static Object getByDotPath(Map<String, Object> json, String path) { |
| 80 | + if (json == null || isEmpty(path)) return null; |
| 81 | + Object cur = json; |
| 82 | + for (String seg : path.split("\\.")) { |
| 83 | + if (!(cur instanceof Map)) return null; |
| 84 | + cur = ((Map<?, ?>) cur).get(seg); |
| 85 | + if (cur == null) return null; |
| 86 | + } |
| 87 | + return cur; |
| 88 | + } |
| 89 | + |
| 90 | + private static Map<String, Object> collectByDotPaths(Map<String, Object> json, List<String> paths) { |
| 91 | + Map<String, Object> out = new LinkedHashMap<>(); |
| 92 | + for (String p : paths) { |
| 93 | + Object v = getByDotPath(json, p); |
| 94 | + if (v != null) out.put(p, v); |
| 95 | + } |
| 96 | + return out; |
| 97 | + } |
| 98 | + |
| 99 | + private static List<String> safeList(List<String> l) { return l == null ? Collections.emptyList() : l; } |
| 100 | + private static boolean isEmpty(String s) { return s == null || s.isEmpty(); } |
| 101 | + private static String nullToEmpty(String s) { return s == null ? "" : s; } |
| 102 | + private static String asString(Object v) { return v == null ? null : String.valueOf(v); } |
| 103 | +} |
| 104 | + |
| 105 | + |
0 commit comments