Skip to content

Commit c1c35f3

Browse files
Allow any DII to be validated in token validate
1 parent fca4cee commit c1c35f3

9 files changed

Lines changed: 137 additions & 45 deletions

File tree

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

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: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -225,7 +225,8 @@ public AdvertisingToken decodeAdvertisingTokenV2(Buffer b) {
225225
Instant.ofEpochMilli(expiresMillis),
226226
new OperatorIdentity(0, OperatorType.Service, 0, masterKeyId),
227227
new PublisherIdentity(siteId, siteKeyId, 0),
228-
new UserIdentity(IdentityScope.UID2, IdentityType.Email, advertisingId, privacyBits, Instant.ofEpochMilli(establishedMillis), null)
228+
new UserIdentity(IdentityScope.UID2, IdentityType.Email, advertisingId, privacyBits, Instant.ofEpochMilli(establishedMillis), null),
229+
siteKeyId
229230
);
230231

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

264265
return new AdvertisingToken(
265266
tokenVersion, createdAt, expiresAt, operatorIdentity, publisherIdentity,
266-
new UserIdentity(identityScope, identityType, id, privacyBits, establishedAt, refreshedAt)
267+
new UserIdentity(identityScope, identityType, id, privacyBits, establishedAt, refreshedAt),
268+
siteKeyId
267269
);
268270
}
269271

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: 40 additions & 17 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();
@@ -807,6 +809,22 @@ private void recordOperatorServedSdkUsage(RoutingContext rc, Integer siteId, Str
807809
}
808810
}
809811

812+
private void recordTokenValidateStats(Integer siteId, String result) {
813+
final String siteIdStr = siteId != null ? String.valueOf(siteId) : "unknown";
814+
_tokenValidateCounters.computeIfAbsent(
815+
new Tuple.Tuple2<>(siteIdStr, result),
816+
tuple -> Counter
817+
.builder("uid2_token_validate_total")
818+
.description("counter for token validate endpoint results")
819+
.tags(
820+
"site_id", tuple.getItem1(),
821+
"site_name", getSiteName(siteProvider, Integer.valueOf(tuple.getItem1())),
822+
"result", tuple.getItem2()
823+
)
824+
.register(Metrics.globalRegistry)
825+
).increment();
826+
}
827+
810828
private void handleTokenRefreshV2(RoutingContext rc) {
811829
Integer siteId = null;
812830
TokenResponseStatsCollector.PlatformType platformType = TokenResponseStatsCollector.PlatformType.Other;
@@ -848,33 +866,38 @@ private void handleTokenRefreshV2(RoutingContext rc) {
848866
private void handleTokenValidateV2(RoutingContext rc) {
849867
RuntimeConfig config = this.getConfigFromRc(rc);
850868
IdentityEnvironment env = config.getIdentityEnvironment();
869+
final Integer participantSiteId = AuthMiddleware.getAuthClient(rc).getSiteId();
851870

852871
try {
853872
final JsonObject req = (JsonObject) rc.data().get("request");
854873

855874
final InputUtil.InputVal input = getTokenInputV2(req);
856875
if (!isTokenInputValid(input, rc)) {
876+
recordTokenValidateStats(participantSiteId, "invalid_input");
857877
return;
858878
}
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 {
879+
880+
final Instant now = Instant.now();
881+
final String token = req.getString("token");
882+
883+
final TokenValidateResult result = this.idService.validateAdvertisingToken(participantSiteId, token, input.toUserIdentity(this.identityScope, 0, now), now, env);
884+
885+
if (result == TokenValidateResult.MATCH) {
886+
recordTokenValidateStats(participantSiteId, "match");
887+
ResponseUtil.SuccessV2(rc, Boolean.TRUE);
888+
} else if (result == TokenValidateResult.MISMATCH) {
889+
recordTokenValidateStats(participantSiteId, "mismatch");
874890
ResponseUtil.SuccessV2(rc, Boolean.FALSE);
891+
} else if (result == TokenValidateResult.UNAUTHORIZED) {
892+
recordTokenValidateStats(participantSiteId, "unauthorized");
893+
ResponseUtil.LogInfoAndSend400Response(rc, "Unauthorised to validate token");
894+
} else if (result == TokenValidateResult.INVALID_TOKEN) {
895+
recordTokenValidateStats(participantSiteId, "invalid_token");
896+
ResponseUtil.LogInfoAndSend400Response(rc, "Invalid token");
875897
}
876898
} catch (Exception e) {
877-
LOGGER.error("Unknown error while validating token v2", e);
899+
recordTokenValidateStats(participantSiteId, "error");
900+
LOGGER.error("Unknown error while validating token", e);
878901
rc.fail(500);
879902
}
880903
}

src/test/java/com/uid2/operator/UIDOperatorServiceTest.java

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ class UIDOperatorServiceTest {
5454
@Mock private OperatorShutdownHandler shutdownHandler;
5555

5656
private EncryptedTokenEncoder tokenEncoder;
57+
private KeyManager keyManager;
5758
private UidInstanceIdProvider uidInstanceIdProvider;
5859
private JsonObject uid2Config;
5960
private JsonObject euidConfig;
@@ -62,8 +63,8 @@ class UIDOperatorServiceTest {
6263
private Instant now;
6364

6465
static class ExtendedUIDOperatorService extends UIDOperatorService {
65-
public ExtendedUIDOperatorService(IOptOutStore optOutStore, ISaltProvider saltProvider, ITokenEncoder encoder, Clock clock, IdentityScope identityScope, Handler<Boolean> saltRetrievalResponseHandler, boolean identityV3Enabled, UidInstanceIdProvider uidInstanceIdProvider) {
66-
super(optOutStore, saltProvider, encoder, clock, identityScope, saltRetrievalResponseHandler, identityV3Enabled, uidInstanceIdProvider);
66+
public ExtendedUIDOperatorService(IOptOutStore optOutStore, ISaltProvider saltProvider, ITokenEncoder encoder, Clock clock, IdentityScope identityScope, Handler<Boolean> saltRetrievalResponseHandler, boolean identityV3Enabled, UidInstanceIdProvider uidInstanceIdProvider, KeyManager keyManager) {
67+
super(optOutStore, saltProvider, encoder, clock, identityScope, saltRetrievalResponseHandler, identityV3Enabled, uidInstanceIdProvider, keyManager);
6768
}
6869
}
6970

@@ -88,7 +89,8 @@ void setup() throws Exception {
8889
"/com.uid2.core/test/salts/metadata.json");
8990
saltProvider.loadContent();
9091

91-
tokenEncoder = new EncryptedTokenEncoder(new KeyManager(keysetKeyStore, keysetProvider));
92+
keyManager = new KeyManager(keysetKeyStore, keysetProvider);
93+
tokenEncoder = new EncryptedTokenEncoder(keyManager);
9294

9395
setNow(Instant.now());
9496

@@ -108,7 +110,8 @@ void setup() throws Exception {
108110
IdentityScope.UID2,
109111
this.shutdownHandler::handleSaltRetrievalResponse,
110112
uid2Config.getBoolean(IdentityV3Prop),
111-
uidInstanceIdProvider
113+
uidInstanceIdProvider,
114+
keyManager
112115
);
113116

114117
euidConfig = new JsonObject();
@@ -125,7 +128,8 @@ void setup() throws Exception {
125128
IdentityScope.EUID,
126129
this.shutdownHandler::handleSaltRetrievalResponse,
127130
euidConfig.getBoolean(IdentityV3Prop),
128-
uidInstanceIdProvider
131+
uidInstanceIdProvider,
132+
keyManager
129133
);
130134
}
131135

@@ -154,7 +158,8 @@ private RotatingSaltProvider.SaltSnapshot setUpMockSalts() {
154158
IdentityScope.UID2,
155159
this.shutdownHandler::handleSaltRetrievalResponse,
156160
uid2Config.getBoolean(IdentityV3Prop),
157-
uidInstanceIdProvider
161+
uidInstanceIdProvider,
162+
keyManager
158163
);
159164

160165
return saltSnapshot;
@@ -820,7 +825,8 @@ void testExpiredSaltsNotifiesShutdownHandler(TestIdentityInputType type, String
820825
IdentityScope.UID2,
821826
this.shutdownHandler::handleSaltRetrievalResponse,
822827
uid2Config.getBoolean(IdentityV3Prop),
823-
uidInstanceIdProvider
828+
uidInstanceIdProvider,
829+
keyManager
824830
);
825831

826832
UIDOperatorService euidService = new UIDOperatorService(
@@ -831,7 +837,8 @@ void testExpiredSaltsNotifiesShutdownHandler(TestIdentityInputType type, String
831837
IdentityScope.EUID,
832838
this.shutdownHandler::handleSaltRetrievalResponse,
833839
euidConfig.getBoolean(IdentityV3Prop),
834-
uidInstanceIdProvider
840+
uidInstanceIdProvider,
841+
keyManager
835842
);
836843

837844
when(this.optOutStore.getLatestEntry(any())).thenReturn(null);

src/test/java/com/uid2/operator/UIDOperatorVerticleTest.java

Lines changed: 36 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1543,11 +1543,6 @@ void tokenGenerateOptOutToken(String policyParameterKey, String identity, Identi
15431543
assertArrayEquals(advertisingId, advertisingToken.userIdentity.id);
15441544
assertArrayEquals(firstLevelHash, refreshToken.userIdentity.id);
15451545

1546-
String advertisingTokenString = body.getString("advertising_token");
1547-
final Instant now = Instant.now();
1548-
final String token = advertisingTokenString;
1549-
final boolean matchedOptedOutIdentity = this.uidOperatorVerticle.getIdService().advertisingTokenMatches(token, optOutTokenInput.toUserIdentity(getIdentityScope(), 0, now), now, IdentityEnvironment.TEST);
1550-
assertTrue(matchedOptedOutIdentity);
15511546
assertFalse(PrivacyBits.fromInt(advertisingToken.userIdentity.privacyBits).isClientSideTokenGenerated());
15521547
assertTrue(PrivacyBits.fromInt(advertisingToken.userIdentity.privacyBits).isClientSideTokenOptedOut());
15531548

@@ -1931,6 +1926,36 @@ void tokenGenerateThenValidateWithEmailHash_Match(Vertx vertx, VertxTestContext
19311926
});
19321927
}
19331928

1929+
@Test
1930+
void tokenGenerateThenValidate_Unauthorized(Vertx vertx, VertxTestContext testContext) {
1931+
final int clientSiteId = 201;
1932+
fakeAuth(clientSiteId, Role.GENERATOR);
1933+
setupSalts();
1934+
setupKeys();
1935+
1936+
generateTokens(vertx, "email", ValidateIdentityForEmail, genRespJson -> {
1937+
assertEquals("success", genRespJson.getString("status"));
1938+
JsonObject genBody = genRespJson.getJsonObject("body");
1939+
assertNotNull(genBody);
1940+
1941+
String advertisingTokenString = genBody.getString("advertising_token");
1942+
1943+
// This site ID did not generate the token and is therefore not authorised to validate the token
1944+
fakeAuth(999, Role.GENERATOR);
1945+
1946+
JsonObject v2Payload = new JsonObject();
1947+
v2Payload.put("token", advertisingTokenString);
1948+
v2Payload.put("email", ValidateIdentityForEmail);
1949+
1950+
send(vertx, "v2/token/validate", v2Payload, 400, json -> {
1951+
assertEquals("client_error", json.getString("status"));
1952+
assertEquals("Unauthorised to validate token", json.getString("message"));
1953+
1954+
testContext.completeNow();
1955+
});
1956+
});
1957+
}
1958+
19341959
@Test
19351960
void tokenGenerateThenValidateWithBothEmailAndEmailHash(Vertx vertx, VertxTestContext testContext) {
19361961
final int clientSiteId = 201;
@@ -2258,10 +2283,10 @@ void tokenValidateWithEmail_Mismatch(String contentType, Vertx vertx, VertxTestC
22582283
setupKeys();
22592284

22602285
send(vertx, "v2/token/validate", new JsonObject().put("token", "abcdef").put("email", emailAddress),
2261-
200,
2286+
400,
22622287
respJson -> {
2263-
assertFalse(respJson.getBoolean("body"));
2264-
assertEquals("success", respJson.getString("status"));
2288+
assertEquals("client_error", respJson.getString("status"));
2289+
assertEquals("Invalid token", respJson.getString("message"));
22652290

22662291
testContext.completeNow();
22672292
},
@@ -2277,10 +2302,10 @@ void tokenValidateWithEmailHash_Mismatch(Vertx vertx, VertxTestContext testConte
22772302

22782303
send(vertx, "v2/token/validate",
22792304
new JsonObject().put("token", "abcdef").put("email_hash", EncodingUtils.toBase64String(ValidateIdentityForEmailHash)),
2280-
200,
2305+
400,
22812306
respJson -> {
2282-
assertFalse(respJson.getBoolean("body"));
2283-
assertEquals("success", respJson.getString("status"));
2307+
assertEquals("client_error", respJson.getString("status"));
2308+
assertEquals("Invalid token", respJson.getString("message"));
22842309

22852310
testContext.completeNow();
22862311
});

src/test/java/com/uid2/operator/benchmark/BenchmarkCommon.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,8 @@ public static IUIDOperatorService createUidOperatorService() throws Exception {
6969
"/com.uid2.core/test/salts/metadata.json");
7070
saltProvider.loadContent();
7171

72-
final EncryptedTokenEncoder tokenEncoder = new EncryptedTokenEncoder(new KeyManager(keysetKeyStore, keysetProvider));
72+
final KeyManager keyManager = new KeyManager(keysetKeyStore, keysetProvider);
73+
final EncryptedTokenEncoder tokenEncoder = new EncryptedTokenEncoder(keyManager);
7374
final List<String> optOutPartitionFiles = new ArrayList<>();
7475
final ICloudStorage optOutLocalStorage = make1mOptOutEntryStorage(
7576
saltProvider.getSnapshot(Instant.now()).getFirstLevelSalt(),
@@ -84,7 +85,8 @@ public static IUIDOperatorService createUidOperatorService() throws Exception {
8485
IdentityScope.UID2,
8586
shutdownHandler::handleSaltRetrievalResponse,
8687
false,
87-
new UidInstanceIdProvider("test-instance", "id")
88+
new UidInstanceIdProvider("test-instance", "id"),
89+
keyManager
8890
);
8991
}
9092

0 commit comments

Comments
 (0)