@@ -128,6 +128,18 @@ public void setupRoutes(Router router) {
128128 }
129129 }, new AuditParams (List .of (), Collections .emptyList ()), Role .MAINTAINER , Role .SECRET_ROTATION ));
130130
131+ router .post ("/api/salt/generate" ).blockingHandler (auth .handle (ctx -> {
132+ synchronized (writeLock ) {
133+ this .handleSaltGenerate (ctx );
134+ }
135+ }, new AuditParams (List .of (), Collections .emptyList ()), Role .MAINTAINER , Role .SECRET_ROTATION ));
136+
137+ router .post ("/api/salt/optout" ).blockingHandler (auth .handle (ctx -> {
138+ synchronized (writeLock ) {
139+ this .handleSaltOptout (ctx );
140+ }
141+ }, new AuditParams (List .of (), Collections .emptyList ()), Role .MAINTAINER , Role .SECRET_ROTATION ));
142+
131143 router .post ("/api/salt/simulateToken" ).blockingHandler (auth .handle (ctx -> {
132144 synchronized (writeLock ) {
133145 this .handleSaltSimulateToken (ctx );
@@ -401,6 +413,67 @@ private void handleSaltCompare(RoutingContext rc) {
401413 }
402414 }
403415
416+ private void handleSaltGenerate (RoutingContext rc ) {
417+ try {
418+ RotatingSaltProvider .SaltSnapshot snapshot = saltProvider .getSnapshots ().getLast ();
419+
420+ String v2Email = null ;
421+ String v4Email = null ;
422+ while (v2Email == null || v4Email == null ) {
423+ String email = randomEmail ();
424+ SaltEntry salt = getSalt (email , snapshot );
425+ if (salt .currentSalt () != null ) {
426+ v2Email = email ;
427+ } else {
428+ v4Email = email ;
429+ }
430+ }
431+
432+ JsonNode v2GenerateResponse = v2TokenGenerate (v2Email );
433+ JsonNode v4GenerateResponse = v2TokenGenerate (v4Email );
434+
435+ JsonNode v2MapResponse = v3IdentityMap (List .of (v2Email ));
436+ JsonNode v4MapResponse = v3IdentityMap (List .of (v4Email ));
437+
438+ String v2UidToken = v2GenerateResponse .at ("/body/advertising_token" ).asText ();
439+ String v4UidToken = v4GenerateResponse .at ("/body/advertising_token" ).asText ();
440+
441+ String v2RawUid = v2MapResponse .at ("/body/email" ).get (0 ).at ("/u" ).asText ();
442+ String v4RawUid = v4MapResponse .at ("/body/email" ).get (0 ).at ("/u" ).asText ();
443+
444+ rc .response ()
445+ .putHeader (HttpHeaders .CONTENT_TYPE , "application/json" )
446+ .end ();
447+ } catch (Exception e ) {
448+ LOGGER .error (e .getMessage (), e );
449+ rc .fail (500 , e );
450+ }
451+ }
452+
453+ private void handleSaltOptout (RoutingContext rc ) {
454+ try {
455+ final String candidateOperatorUrl = RequestUtil .getString (rc , "candidate_operator_url" ).orElse ("" );
456+ final String candidateApiKey = RequestUtil .getString (rc , "candidate_api_key" ).orElse ("" );
457+ final String candidateApiSecret = RequestUtil .getString (rc , "candidate_api_secret" ).orElse ("" );
458+ final String [] optoutEmails = RequestUtil .getString (rc , "emails" ).orElse ("" ).split ("," );
459+ final String [] optoutPhones = RequestUtil .getString (rc , "phones" ).orElse ("" ).split ("," );
460+
461+ for (String email : optoutEmails ) {
462+ v2TokenLogout ("email" , email , candidateOperatorUrl , candidateApiKey , candidateApiSecret );
463+ }
464+ for (String phone : optoutPhones ) {
465+ v2TokenLogout ("phone" , phone , candidateOperatorUrl , candidateApiKey , candidateApiSecret );
466+ }
467+
468+ rc .response ()
469+ .putHeader (HttpHeaders .CONTENT_TYPE , "application/json" )
470+ .end ();
471+ } catch (Exception e ) {
472+ LOGGER .error (e .getMessage (), e );
473+ rc .fail (500 , e );
474+ }
475+ }
476+
404477 private void handleSaltSimulateToken (RoutingContext rc ) {
405478 try {
406479 final double fraction = RequestUtil .getDouble (rc , "fraction" ).orElse (0.002740 );
@@ -932,9 +1005,10 @@ private JsonNode v3IdentityMap(List<String> emails, String baseUrl, String key,
9321005 }
9331006 }
9341007 reqBody .append ("] }" );
935-
9361008 V2Envelope envelope = v2CreateEnvelope (reqBody .toString (), secret );
1009+
9371010 Map <String , String > headers = Map .of ("Authorization" , String .format ("Bearer %s" , key ));
1011+
9381012 HttpResponse <String > response = HTTP_CLIENT .post (String .format ("%s/v3/identity/map" , baseUrl ), envelope .envelope (), headers );
9391013 return v2DecryptEncryptedResponse (response .body (), envelope .nonce (), secret );
9401014 }
@@ -956,6 +1030,16 @@ private JsonNode v2TokenRefresh(String refreshToken, String refreshResponseKey)
9561030 return v2DecryptRefreshResponse (response .body (), refreshResponseKey );
9571031 }
9581032
1033+ private JsonNode v2TokenLogout (String type , String identity , String baseUrl , String key , String secret ) throws Exception {
1034+ String reqBody = String .format ("{\" %s\" :\" %s\" }" .formatted (type , identity ));
1035+ V2Envelope envelope = v2CreateEnvelope (reqBody , secret );
1036+
1037+ Map <String , String > headers = Map .of ("Authorization" , String .format ("Bearer %s" , key ));
1038+
1039+ HttpResponse <String > response = HTTP_CLIENT .post (String .format ("%s/v2/token/logout" , baseUrl ), envelope .envelope (), headers );
1040+ return v2DecryptEncryptedResponse (response .body (), envelope .nonce (), secret );
1041+ }
1042+
9591043 private String randomEmail () {
9601044 return "email_" + Math .abs (SECURE_RANDOM .nextLong ()) + "@example.com" ;
9611045 }
0 commit comments