Skip to content

Commit 65ab0b9

Browse files
Merge pull request #2169 from IABTechLab/vse-UID2-6322-allow-any-dii-token-validate
Allow any DII to be validated in token validate
2 parents cbca178 + 15758ed commit 65ab0b9

File tree

11 files changed

+263
-62
lines changed

11 files changed

+263
-62
lines changed

.trivyignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,9 @@ CVE-2025-59375 exp:2025-12-15
1010

1111
# UID2-6128
1212
CVE-2025-55163 exp:2025-10-30
13+
14+
# UID2-6340
15+
CVE-2025-64720 exp:2025-12-16
16+
17+
# UID2-6340
18+
CVE-2025-65018 exp:2025-12-16

src/main/java/com/uid2/operator/model/AdvertisingToken.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,21 @@ public class AdvertisingToken extends VersionedToken {
77
public final OperatorIdentity operatorIdentity;
88
public final PublisherIdentity publisherIdentity;
99
public final UserIdentity userIdentity;
10+
public Integer siteKeyId;
1011

1112
public AdvertisingToken(TokenVersion version, Instant createdAt, Instant expiresAt, OperatorIdentity operatorIdentity,
1213
PublisherIdentity publisherIdentity, UserIdentity userIdentity) {
1314
super(version, createdAt, expiresAt);
1415
this.operatorIdentity = operatorIdentity;
1516
this.publisherIdentity = publisherIdentity;
1617
this.userIdentity = userIdentity;
18+
this.siteKeyId = null;
19+
}
20+
21+
public AdvertisingToken(TokenVersion version, Instant createdAt, Instant expiresAt, OperatorIdentity operatorIdentity,
22+
PublisherIdentity publisherIdentity, UserIdentity userIdentity, Integer siteKeyId) {
23+
this(version, createdAt, expiresAt, operatorIdentity, publisherIdentity, userIdentity);
24+
this.siteKeyId = siteKeyId;
1725
}
1826
}
1927

src/main/java/com/uid2/operator/model/KeyManager.java

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
package com.uid2.operator.model;
22

3+
import com.uid2.operator.util.Tuple;
34
import com.uid2.operator.vertx.UIDOperatorVerticle;
45
import com.uid2.shared.Const;
56
import com.uid2.shared.auth.Keyset;
67
import com.uid2.shared.model.KeysetKey;
78
import com.uid2.shared.store.IKeysetKeyStore;
89
import com.uid2.shared.store.reader.RotatingKeysetProvider;
10+
import io.micrometer.core.instrument.Counter;
11+
import io.micrometer.core.instrument.Metrics;
912
import org.slf4j.Logger;
1013
import org.slf4j.LoggerFactory;
1114

@@ -17,6 +20,10 @@
1720

1821
public class KeyManager {
1922
private static final Logger LOGGER = LoggerFactory.getLogger(KeyManager.class);
23+
private static final String SITE_KEYSET_STATUS = "site_keyset_found";
24+
private static final String FALLBACK_KEYSET_STATUS = "fallback_keyset_found";
25+
private static final String KEYSET_NOT_FOUND_STATUS = "keyset_not_found";
26+
2027
private final IKeysetKeyStore keysetKeyStore;
2128
private final RotatingKeysetProvider keysetProvider;
2229

@@ -34,12 +41,45 @@ public KeyManagerSnapshot getKeyManagerSnapshot(int siteId) {
3441
this.getDefaultKeysetBySiteId(siteId));
3542
}
3643

37-
public KeysetKey getActiveKeyBySiteIdWithFallback(int siteId, int fallbackSiteId, Instant asOf) {
44+
private void recordSiteKeysetStatusMetrics(int siteId, Boolean keysetFound, Boolean isFallback, Map<Tuple.Tuple2<String, String>, Counter> siteKeysetStatusMetrics) {
45+
String status;
46+
if (!keysetFound) {
47+
status = KEYSET_NOT_FOUND_STATUS;
48+
} else if (isFallback) {
49+
status = FALLBACK_KEYSET_STATUS;
50+
} else {
51+
status = SITE_KEYSET_STATUS;
52+
}
53+
54+
siteKeysetStatusMetrics.computeIfAbsent(
55+
new Tuple.Tuple2<>(String.valueOf(siteId), status),
56+
tuple -> Counter
57+
.builder("uid2_site_keyset_status")
58+
.description("counts site keyset status by site ID")
59+
.tags(
60+
"site_id", tuple.getItem1(),
61+
"status", tuple.getItem2()
62+
)
63+
.register(Metrics.globalRegistry)
64+
).increment();
65+
}
66+
67+
public KeysetKey getActiveKeyBySiteIdWithFallback(int siteId, int fallbackSiteId, Instant asOf, Map<Tuple.Tuple2<String, String>, Counter> siteKeysetStatusMetrics) {
68+
boolean isFallback = false;
69+
3870
KeysetKey key = getActiveKeyBySiteId(siteId, asOf);
39-
if (key == null) key = getActiveKeyBySiteId(fallbackSiteId, asOf);
71+
4072
if (key == null) {
73+
isFallback = true;
74+
key = getActiveKeyBySiteId(fallbackSiteId, asOf);
75+
}
76+
77+
if (key == null) {
78+
recordSiteKeysetStatusMetrics(siteId, false, null, siteKeysetStatusMetrics);
4179
throw new NoActiveKeyException(String.format("Cannot get active key in default keyset with SITE ID %d or %d.", siteId, fallbackSiteId));
4280
}
81+
82+
recordSiteKeysetStatusMetrics(siteId, true, isFallback, siteKeysetStatusMetrics);
4383
return key;
4484
}
4585

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package com.uid2.operator.model;
2+
3+
public enum TokenValidateResult {
4+
MATCH,
5+
MISMATCH,
6+
UNAUTHORIZED,
7+
INVALID_TOKEN,
8+
}

src/main/java/com/uid2/operator/service/EncryptedTokenEncoder.java

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.uid2.operator.service;
22

33
import com.uid2.operator.model.*;
4+
import com.uid2.operator.util.Tuple;
45
import com.uid2.operator.vertx.ClientInputValidationException;
56
import com.uid2.shared.Const.Data;
67
import com.uid2.shared.encryption.AesCbc;
@@ -14,17 +15,20 @@
1415

1516
import java.time.Instant;
1617
import java.util.Base64;
18+
import java.util.HashMap;
19+
import java.util.Map;
1720

1821
public class EncryptedTokenEncoder implements ITokenEncoder {
1922
private final KeyManager keyManager;
23+
private final Map<Tuple.Tuple2<String, String>, Counter> siteKeysetStatusMetrics = new HashMap<>();
2024

2125
public EncryptedTokenEncoder(KeyManager keyManager) {
2226
this.keyManager = keyManager;
2327
}
2428

2529
public byte[] encode(AdvertisingToken t, Instant asOf) {
2630
final KeysetKey masterKey = this.keyManager.getMasterKey(asOf);
27-
final KeysetKey siteEncryptionKey = this.keyManager.getActiveKeyBySiteIdWithFallback(t.publisherIdentity.siteId, Data.AdvertisingTokenSiteId, asOf);
31+
final KeysetKey siteEncryptionKey = this.keyManager.getActiveKeyBySiteIdWithFallback(t.publisherIdentity.siteId, Data.AdvertisingTokenSiteId, asOf, siteKeysetStatusMetrics);
2832

2933
return t.version == TokenVersion.V2
3034
? encodeV2(t, masterKey, siteEncryptionKey)
@@ -225,7 +229,8 @@ public AdvertisingToken decodeAdvertisingTokenV2(Buffer b) {
225229
Instant.ofEpochMilli(expiresMillis),
226230
new OperatorIdentity(0, OperatorType.Service, 0, masterKeyId),
227231
new PublisherIdentity(siteId, siteKeyId, 0),
228-
new UserIdentity(IdentityScope.UID2, IdentityType.Email, advertisingId, privacyBits, Instant.ofEpochMilli(establishedMillis), null)
232+
new UserIdentity(IdentityScope.UID2, IdentityType.Email, advertisingId, privacyBits, Instant.ofEpochMilli(establishedMillis), null),
233+
siteKeyId
229234
);
230235

231236
} catch (Exception e) {
@@ -263,7 +268,8 @@ public AdvertisingToken decodeAdvertisingTokenV3orV4(Buffer b, byte[] bytes, Tok
263268

264269
return new AdvertisingToken(
265270
tokenVersion, createdAt, expiresAt, operatorIdentity, publisherIdentity,
266-
new UserIdentity(identityScope, identityType, id, privacyBits, establishedAt, refreshedAt)
271+
new UserIdentity(identityScope, identityType, id, privacyBits, establishedAt, refreshedAt),
272+
siteKeyId
267273
);
268274
}
269275

src/main/java/com/uid2/operator/service/IUIDOperatorService.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,5 +25,5 @@ void invalidateTokensAsync(UserIdentity userIdentity, Instant asOf, String uidTr
2525
String email, String phone, String clientIp,
2626
Handler<AsyncResult<Instant>> handler);
2727

28-
boolean advertisingTokenMatches(String advertisingToken, UserIdentity userIdentity, Instant asOf, IdentityEnvironment env);
28+
TokenValidateResult validateAdvertisingToken(int participantSiteId, String advertisingToken, UserIdentity userIdentity, Instant asOf, IdentityEnvironment env);
2929
}

src/main/java/com/uid2/operator/service/UIDOperatorService.java

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -57,16 +57,18 @@ public class UIDOperatorService implements IUIDOperatorService {
5757

5858
private final Handler<Boolean> saltRetrievalResponseHandler;
5959
private final UidInstanceIdProvider uidInstanceIdProvider;
60+
private final KeyManager keyManager;
6061

6162
public UIDOperatorService(IOptOutStore optOutStore, ISaltProvider saltProvider, ITokenEncoder encoder, Clock clock,
62-
IdentityScope identityScope, Handler<Boolean> saltRetrievalResponseHandler, boolean identityV3Enabled, UidInstanceIdProvider uidInstanceIdProvider) {
63+
IdentityScope identityScope, Handler<Boolean> saltRetrievalResponseHandler, boolean identityV3Enabled, UidInstanceIdProvider uidInstanceIdProvider, KeyManager keyManager) {
6364
this.saltProvider = saltProvider;
6465
this.encoder = encoder;
6566
this.optOutStore = optOutStore;
6667
this.clock = clock;
6768
this.identityScope = identityScope;
6869
this.saltRetrievalResponseHandler = saltRetrievalResponseHandler;
6970
this.uidInstanceIdProvider = uidInstanceIdProvider;
71+
this.keyManager = keyManager;
7072

7173
this.testOptOutIdentityForEmail = getFirstLevelHashIdentity(identityScope, IdentityType.Email,
7274
InputUtil.normalizeEmail(OptOutIdentityForEmail).getIdentityInput(), Instant.now());
@@ -197,12 +199,27 @@ public void invalidateTokensAsync(UserIdentity userIdentity, Instant asOf, Strin
197199
}
198200

199201
@Override
200-
public boolean advertisingTokenMatches(String advertisingToken, UserIdentity userIdentity, Instant asOf, IdentityEnvironment env) {
202+
public TokenValidateResult validateAdvertisingToken(int participantSiteId, String advertisingToken, UserIdentity userIdentity, Instant asOf, IdentityEnvironment env) {
201203
final UserIdentity firstLevelHashIdentity = getFirstLevelHashIdentity(userIdentity, asOf);
202204
final MappedIdentity mappedIdentity = getMappedIdentity(firstLevelHashIdentity, asOf, env);
203205

204-
final AdvertisingToken token = this.encoder.decodeAdvertisingToken(advertisingToken);
205-
return Arrays.equals(mappedIdentity.advertisingId, token.userIdentity.id);
206+
final AdvertisingToken token;
207+
try {
208+
token = this.encoder.decodeAdvertisingToken(advertisingToken);
209+
} catch (Exception e) {
210+
return TokenValidateResult.INVALID_TOKEN;
211+
}
212+
213+
int tokenSiteId = this.keyManager.getSiteIdFromKeyId(token.siteKeyId);
214+
if (tokenSiteId != participantSiteId) {
215+
return TokenValidateResult.UNAUTHORIZED;
216+
}
217+
218+
if (!Arrays.equals(mappedIdentity.advertisingId, token.userIdentity.id)) {
219+
return TokenValidateResult.MISMATCH;
220+
}
221+
222+
return TokenValidateResult.MATCH;
206223
}
207224

208225
private void validateTokenDurations(Duration refreshIdentityAfter, Duration refreshExpiresAfter, Duration identityExpiresAfter) {

src/main/java/com/uid2/operator/vertx/UIDOperatorVerticle.java

Lines changed: 53 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ public class UIDOperatorVerticle extends AbstractVerticle {
117117
private final Map<String, Tuple.Tuple2<Counter, Counter>> _identityMapUnmappedIdentifiers = new HashMap<>();
118118
private final Map<String, Counter> _identityMapRequestWithUnmapped = new HashMap<>();
119119
private final Map<Tuple.Tuple2<String, String>, Counter> _clientVersions = new HashMap<>();
120+
private final Map<Tuple.Tuple2<String, String>, Counter> _tokenValidateCounters = new HashMap<>();
120121

121122
private final Map<String, DistributionSummary> optOutStatusCounters = new HashMap<>();
122123
private final IdentityScope identityScope;
@@ -210,7 +211,8 @@ public void start(Promise<Void> startPromise) throws Exception {
210211
this.identityScope,
211212
this.saltRetrievalResponseHandler,
212213
this.identityV3Enabled,
213-
this.uidInstanceIdProvider
214+
this.uidInstanceIdProvider,
215+
this.keyManager
214216
);
215217

216218
final Router router = createRoutesSetup();
@@ -233,13 +235,8 @@ public void start(Promise<Void> startPromise) throws Exception {
233235

234236
}
235237

236-
private Router createRoutesSetup() throws IOException {
237-
final Router router = Router.router(vertx);
238-
239-
router.allowForward(AllowForwardHeaders.X_FORWARD);
240-
router.route().handler(new RequestCapturingHandler(siteProvider));
241-
router.route().handler(new ClientVersionCapturingHandler("static/js", "*.js", clientKeyProvider));
242-
router.route().handler(CorsHandler.create()
238+
private CorsHandler createCorsHandler() {
239+
return CorsHandler.create()
243240
.addRelativeOrigin(".*.")
244241
.allowedMethod(io.vertx.core.http.HttpMethod.GET)
245242
.allowedMethod(io.vertx.core.http.HttpMethod.POST)
@@ -249,7 +246,17 @@ private Router createRoutesSetup() throws IOException {
249246
.allowedHeader("Access-Control-Allow-Credentials")
250247
.allowedHeader("Access-Control-Allow-Origin")
251248
.allowedHeader("Access-Control-Allow-Headers")
252-
.allowedHeader("Content-Type"));
249+
.allowedHeader("Content-Type");
250+
}
251+
252+
private Router createRoutesSetup() throws IOException {
253+
final Router router = Router.router(vertx);
254+
255+
router.allowForward(AllowForwardHeaders.X_FORWARD);
256+
router.route().handler(new RequestCapturingHandler(siteProvider));
257+
router.route().handler(new ClientVersionCapturingHandler("static/js", "*.js", clientKeyProvider));
258+
router.route(V2_TOKEN_VALIDATE.toString()).handler(createCorsHandler().allowedHeader("Authorization"));
259+
router.route().handler(createCorsHandler());
253260
router.route().handler(new StatsCollectorHandler(_statsCollectorQueue, vertx));
254261
router.route("/static/*").handler(StaticHandler.create("static"));
255262
router.route().handler(ctx -> {
@@ -807,6 +814,22 @@ private void recordOperatorServedSdkUsage(RoutingContext rc, Integer siteId, Str
807814
}
808815
}
809816

817+
private void recordTokenValidateStats(Integer siteId, String result) {
818+
final String siteIdStr = siteId != null ? String.valueOf(siteId) : "unknown";
819+
_tokenValidateCounters.computeIfAbsent(
820+
new Tuple.Tuple2<>(siteIdStr, result),
821+
tuple -> Counter
822+
.builder("uid2_token_validate_total")
823+
.description("counter for token validate endpoint results")
824+
.tags(
825+
"site_id", tuple.getItem1(),
826+
"site_name", getSiteName(siteProvider, Integer.valueOf(tuple.getItem1())),
827+
"result", tuple.getItem2()
828+
)
829+
.register(Metrics.globalRegistry)
830+
).increment();
831+
}
832+
810833
private void handleTokenRefreshV2(RoutingContext rc) {
811834
Integer siteId = null;
812835
TokenResponseStatsCollector.PlatformType platformType = TokenResponseStatsCollector.PlatformType.Other;
@@ -848,33 +871,38 @@ private void handleTokenRefreshV2(RoutingContext rc) {
848871
private void handleTokenValidateV2(RoutingContext rc) {
849872
RuntimeConfig config = this.getConfigFromRc(rc);
850873
IdentityEnvironment env = config.getIdentityEnvironment();
874+
final Integer participantSiteId = AuthMiddleware.getAuthClient(rc).getSiteId();
851875

852876
try {
853877
final JsonObject req = (JsonObject) rc.data().get("request");
854878

855879
final InputUtil.InputVal input = getTokenInputV2(req);
856880
if (!isTokenInputValid(input, rc)) {
881+
recordTokenValidateStats(participantSiteId, "invalid_input");
857882
return;
858883
}
859-
if ((input.getIdentityType() == IdentityType.Email && Arrays.equals(ValidateIdentityForEmailHash, input.getIdentityInput()))
860-
|| (input.getIdentityType() == IdentityType.Phone && Arrays.equals(ValidateIdentityForPhoneHash, input.getIdentityInput()))) {
861-
try {
862-
final Instant now = Instant.now();
863-
final String token = req.getString("token");
864-
865-
if (this.idService.advertisingTokenMatches(token, input.toUserIdentity(this.identityScope, 0, now), now, env)) {
866-
ResponseUtil.SuccessV2(rc, Boolean.TRUE);
867-
} else {
868-
ResponseUtil.SuccessV2(rc, Boolean.FALSE);
869-
}
870-
} catch (Exception e) {
871-
ResponseUtil.SuccessV2(rc, Boolean.FALSE);
872-
}
873-
} else {
884+
885+
final Instant now = Instant.now();
886+
final String token = req.getString("token");
887+
888+
final TokenValidateResult result = this.idService.validateAdvertisingToken(participantSiteId, token, input.toUserIdentity(this.identityScope, 0, now), now, env);
889+
890+
if (result == TokenValidateResult.MATCH) {
891+
recordTokenValidateStats(participantSiteId, "match");
892+
ResponseUtil.SuccessV2(rc, Boolean.TRUE);
893+
} else if (result == TokenValidateResult.MISMATCH) {
894+
recordTokenValidateStats(participantSiteId, "mismatch");
874895
ResponseUtil.SuccessV2(rc, Boolean.FALSE);
896+
} else if (result == TokenValidateResult.UNAUTHORIZED) {
897+
recordTokenValidateStats(participantSiteId, "unauthorized");
898+
ResponseUtil.LogInfoAndSend400Response(rc, "Unauthorised to validate token");
899+
} else if (result == TokenValidateResult.INVALID_TOKEN) {
900+
recordTokenValidateStats(participantSiteId, "invalid_token");
901+
ResponseUtil.LogInfoAndSend400Response(rc, "Invalid token");
875902
}
876903
} catch (Exception e) {
877-
LOGGER.error("Unknown error while validating token v2", e);
904+
recordTokenValidateStats(participantSiteId, "error");
905+
LOGGER.error("Unknown error while validating token", e);
878906
rc.fail(500);
879907
}
880908
}

0 commit comments

Comments
 (0)