Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

advertiser and data provider support #55

Merged
merged 16 commits into from
Feb 23, 2024
Merged
Show file tree
Hide file tree
Changes from 11 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
204 changes: 6 additions & 198 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,8 @@ This document includes:
* [Who Is this SDK for?](#who-is-this-sdk-for)
* [Requirements](#requirements)
* [Install](#install)
* [Usage for DSPs](#usage-for-dsps)
* [Usage for Publishers](#usage-for-publishers)
- [Basic Usage](#basic-usage)
- [Advanced Usage](#advanced-usage)
* [Usage for UID2 Sharers](#usage-for-uid2-sharers)
* [Usage](#usage)
* [Example usage for DSPs](#example-usage-for-dsps)

## Who Is this SDK for?

Expand All @@ -24,198 +21,9 @@ This SDK requires Java version 1.8 or later.

To install the SDK, add the `uid2-client` dependency to your project's `pom.xml` file [as shown here](https://central.sonatype.com/artifact/com.uid2/uid2-client).

## Usage for DSPs
## Usage

For an example of usage for DSPs, see [com.uid2.client.test.IntegrationExamples](https://github.com/IABTechLab/uid2-client-java/blob/master/src/test/java/com/uid2/client/test/IntegrationExamples.java).

## Usage for Publishers

As a publisher, there are two ways to use this SDK:
1. [**Basic Usage**](#basic-usage) is for publishers who want to use this SDK's HTTP implementation (synchronous [OkHttp](https://square.github.io/okhttp/)).
2. [**Advanced Usage**](#advanced-usage) is for publishers who prefer to use their own HTTP library.

For an example application that demonstrates both Basic and Advanced usage, see [Java UID2 Integration Example](https://github.com/UnifiedID2/uid2-examples/tree/main/publisher/uid2-java-test-site#readme).

### Basic Usage

If you're using the SDK's HTTP implementation, follow these steps.

1. Create an instance of PublisherUid2Client as an instance variable.

`private final PublisherUid2Client publisherUid2Client = new PublisherUid2Client(UID2_BASE_URL, UID2_API_KEY, UID2_SECRET_KEY);`

2. Call a function that takes the user's email address or phone number as input and generates a `TokenGenerateResponse` object. The following example uses an email address:

`TokenGenerateResponse tokenGenerateResponse = publisherUid2Client.generateTokenResponse(TokenGenerateInput.fromEmail(emailAddress).doNotGenerateTokensForOptedOut());`

>IMPORTANT: Be sure to call this function only when you have obtained legal basis to convert the user’s [directly identifying information (DII)](https://unifiedid.com/docs/ref-info/glossary-uid#gl-dii) to UID2 tokens for targeted advertising.

>IMPORTANT: Always apply `doNotGenerateTokensForOptedOut()`. This applies a parameter similar to setting `optout_check=1` in the call to the POST /token/generate endpoint (see [Unencrypted JSON Body Parameters](https://unifiedid.com/docs/endpoints/post-token-generate#unencrypted-json-body-parameters)).

#### Standard Integration

If you're using standard integration (client and server) (see [JavaScript Standard Integration Guide](https://unifiedid.com/docs/guides/integration-javascript-standard)), follow this step:

* Send this identity as a JSON string back to the client (to use in the [identity field](https://unifiedid.com/docs/sdks/client-side-identity#initopts-object-void)) using the following:

```
tokenGenerateResponse.getIdentityJsonString()
```

>NOTE: If the user has opted out, this method returns `null`, so be sure to handle that case.

#### Server-Only Integration

If you're using server-only integration (see [Publisher Integration Guide, Server-Only](https://unifiedid.com/docs/guides/custom-publisher-integration)):

1. Store this identity as a JSON string in the user's session, using the `tokenGenerateResponse.getIdentityJsonString()` function.

If the user has opted out, this method returns `null`, so be sure to handle that case.
2. To retrieve the user's UID2 token, use:

```
IdentityTokens identity = tokenGenerateResponse.getIdentity();
if (identity != null) { String advertisingToken = identity.getAdvertisingToken(); }
```
4. When the user accesses another page, or on a timer, determine whether a refresh is needed:
1. Retrieve the identity JSON string from the user's session, and then call the following function that takes the identity information as input and generates an `IdentityTokens` object:

`IdentityTokens identity = IdentityTokens.fromJsonString(identityJsonString);`
2. Determine if the identity can be refreshed (that is, the refresh token hasn't expired):

` if (identity == null || !identity.isRefreshable()) { we must no longer use this identity (for example, remove this identity from the user's session) }`
3. Determine if a refresh is needed:

`if (identity.isDueForRefresh()) {..}`
5. If needed, refresh the token and associated values:

`TokenRefreshResponse tokenRefreshResponse = publisherUid2Client.refreshToken(identity);`

6. Store `tokenRefreshResponse.getIdentityJsonString()` in the user's session.

If the user has opted out, this method returns `null`, indicating that the user's identity should be removed from the session. To confirm optout, you can use the `tokenRefreshResponse.isOptout()` function.

### Advanced Usage

1. Create an instance of PublisherUid2Helper as an instance variable:

`private final PublisherUid2Helper publisherUid2Helper = new PublisherUid2Helper(UID2_SECRET_KEY);`
2. Call a function that takes the user's email address or phone number as input and creates a secure request data envelope. See [Encrypting requests](https://unifiedid.com/docs/getting-started/gs-encryption-decryption#encrypting-requests). The following example uses an email address:

`EnvelopeV2 envelope = publisherUid2Helper.createEnvelopeForTokenGenerateRequest(TokenGenerateInput.fromEmail(emailAddress).doNotGenerateTokensForOptedOut());`
3. Using an HTTP client library of your choice, post this envelope to the [POST token/generate](https://unifiedid.com/docs/endpoints/post-token-generate) endpoint, including headers and body:
1. Headers: Depending on your HTTP library, this might look something like the following:

`.putHeader("Authorization", "Bearer " + UID2_API_KEY)`
`.putHeader("X-UID2-Client-Version", PublisherUid2Helper.getVersionHeader())`
2. Body: `envelope.getEnvelope()`
>IMPORTANT: Be sure to call this endpoint only when you have obtained legal basis to convert the user’s [directly identifying information (DII)](https://unifiedid.com/docs/ref-info/glossary-uid#gl-dii) to UID2 tokens for targeted advertising.

>IMPORTANT: Always apply `doNotGenerateTokensForOptedOut()`. This applies a parameter similar to setting `optout_check=1` in the call to the POST /token/generate endpoint (see [Unencrypted JSON Body Parameters](https://unifiedid.com/docs/endpoints/post-token-generate#unencrypted-json-body-parameters)).
For documentation on usage, see the [UID2 SDK for Java Reference Guide](https://unifiedid.com/docs/sdks/uid2-sdk-ref-java).

4. If the HTTP response status code is _not_ 200, see [Response Status Codes](https://unifiedid.com/docs/endpoints/post-token-generate#response-status-codes) to determine next steps. Otherwise, convert the UID2 identity response content into a `TokenGenerateResponse` object:

`TokenGenerateResponse tokenGenerateResponse = publisherUid2Helper.createTokenGenerateResponse({response body}, envelope);`

#### Standard Integration

If you're using standard integration (client and server) (see [JavaScript Standard Integration Guide](https://unifiedid.com/docs/guides/integration-javascript-standard)):

* Send this identity as a JSON string back to the client (to use in the [identity field](https://unifiedid.com/docs/sdks/client-side-identity#initopts-object-void)) using the following:

```
tokenGenerateResponse.getIdentityJsonString()
```

>NOTE: If the user has opted out, this method returns `null`, so be sure to handle that case.

#### Server-Only Integration

If you're using server-only integration (see [Publisher Integration Guide, Server-Only](https://unifiedid.com/docs/guides/custom-publisher-integration)):

1. Store this identity as a JSON string in the user's session, using: `tokenGenerateResponse.getIdentityJsonString()`.

If the user has opted out, this method returns null, so be sure to handle that case.
2. To retrieve the user's UID2 token, use:

```
IdentityTokens identity = tokenGenerateResponse.getIdentity();
if (identity != null) { String advertisingToken = identity.getAdvertisingToken(); }
```

3. When the user accesses another page, or on a timer, determine whether a refresh is needed:
1. Retrieve the identity JSON string from the user's session, and then call the following function that generates an `IdentityTokens` object:

```
IdentityTokens identity = IdentityTokens.fromJsonString(identityJsonString);
```

2. Determine if the identity can be refreshed (that is, the refresh token hasn't expired):

```
if (identity == null || !identity.isRefreshable()) { we must no longer use this identity (for example, remove this identity from the user's session) }
```

3. Determine if a refresh is needed:

```
if (identity.isDueForRefresh()) {..}
```

4. If a refresh is needed, call the [POST token/refresh](https://unifiedid.com/docs/endpoints/post-token-refresh) endpoint, with:
1. Headers (depending on your HTTP library, this might look something like):

`.putHeader("Authorization", "Bearer " + UID2_API_KEY)`
`.putHeader("X-UID2-Client-Version", PublisherUid2Helper.getVersionHeader())`.
2. Body: `identity.getRefreshToken()`
5. If the refresh HTTP response status code is 200:

```
TokenRefreshResponse tokenRefreshResponse = PublisherUid2Helper.createTokenRefreshResponse({response body}, identity);
```

6. Store `tokenRefreshResponse.getIdentityJsonString()` in the user's session.

If the user has opted out, this method returns null, indicating that the user's identity should be removed from the session. To confirm optout, you can use the `tokenRefreshResponse.isOptout()` function.

## Usage for UID2 Sharers

A UID2 sharer is a participant that wants to share UID2s or EUIDs with another participant. Raw UID2s must be encrypted into UID2 tokens before sending them to another participant. For an example of usage, see [com.uid2.client.test.IntegrationExamples](https://github.com/IABTechLab/uid2-client-java/blob/master/src/test/java/com/uid2/client/test/IntegrationExamples.java) (runSharingExample method).

1. Use UID2ClientFactory.create() to create an IUID2Client reference:

```
private final IUID2Client client = UID2ClientFactory.create(UID2_BASE_URL, UID2_API_KEY, UID2_SECRET_KEY);
```

2. Call IUID2Client.refresh once at startup, and then periodically (for example, every hour):

```
client.refresh();
```

3. Senders:
1. Call the following:

```
EncryptionDataResponse encrypted = client.encrypt(rawUid);
```

2. If encryption succeeded, send the UID2 token to the receiver:

```
if (encrypted.isSuccess()) {` send `encrypted.getEncryptedData()` to receiver`} else {`check `encrypted.getStatus()` for the failure reason}
```

4. Receivers:
1. Call the following:

```
DecryptionResponse decrypted = client.decrypt(uidToken);
```

2. If decryption succeeded, use the raw UID2:

```
if (decrypted.isSuccess()) {`use `decrypted.getUid() } else {`check `decrypted.getStatus()` for the failure reason `}
```
## Example usage for DSPs
For an example of usage for DSPs, see [com.uid2.client.test.IntegrationExamples](https://github.com/IABTechLab/uid2-client-java/blob/master/src/test/java/com/uid2/client/test/IntegrationExamples.java).
28 changes: 28 additions & 0 deletions src/main/java/com/uid2/client/IdentityMapClient.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.uid2.client;

public class IdentityMapClient {
/**
* @param uid2BaseUrl The <a href="https://unifiedid.com/docs/getting-started/gs-environments">UID2 Base URL</a>
* @param clientApiKey Your client API key
* @param base64SecretKey Your client secret key
*/
IdentityMapClient(String uid2BaseUrl, String clientApiKey, String base64SecretKey) {
identityMapHelper = new IdentityMapHelper(base64SecretKey);
uid2ClientHelper = new Uid2ClientHelper(uid2BaseUrl, clientApiKey);
}

/**
* @param identityMapInput represents the input required for <a href="https://unifiedid.com/docs/endpoints/post-identity-map">/identity/map</a>
* @return an IdentityMapResponse instance
* @throws Uid2Exception if the response did not contain a "success" status, or the response code was not 200, or there was an error communicating with the provided UID2 Base URL
*/
public IdentityMapResponse generateIdentityMap(IdentityMapInput identityMapInput) {
EnvelopeV2 envelope = identityMapHelper.createEnvelopeForIdentityMapRequest(identityMapInput);

String responseString = uid2ClientHelper.makeRequest(envelope, "/v2/identity/map");
return identityMapHelper.createIdentityMapResponse(responseString, envelope, identityMapInput);
}

private final IdentityMapHelper identityMapHelper;
private final Uid2ClientHelper uid2ClientHelper;
}
35 changes: 35 additions & 0 deletions src/main/java/com/uid2/client/IdentityMapHelper.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.uid2.client;

import com.google.gson.Gson;

import java.nio.charset.StandardCharsets;

public class IdentityMapHelper {
/**
* @param base64SecretKey your UID2 client secret
*/
public IdentityMapHelper(String base64SecretKey) {uid2Helper = new Uid2Helper(base64SecretKey);}

/**
* @param identityMapInput represents the input required for <a href="https://unifiedid.com/docs/endpoints/post-identity-map">/identity/map</a>
* @return an EnvelopeV2 instance to use in the POST body of <a href="https://unifiedid.com/docs/endpoints/post-identity-map">/identity/map</a>
*/
public EnvelopeV2 createEnvelopeForIdentityMapRequest(IdentityMapInput identityMapInput) {
byte[] jsonBytes = new Gson().toJson(identityMapInput).getBytes(StandardCharsets.UTF_8);
return uid2Helper.createEnvelopeV2(jsonBytes);
}


/**
* @param responseString the response body returned by a call to <a href="https://unifiedid.com/docs/endpoints/post-identity-map">/identity/map</a>
* @param envelope the EnvelopeV2 instance returned by {@link #createEnvelopeForIdentityMapRequest}
* @param identityMapInput the same instance that was passed to {@link #createEnvelopeForIdentityMapRequest}.
* @return an IdentityMapResponse instance
*/
public IdentityMapResponse createIdentityMapResponse(String responseString, EnvelopeV2 envelope, IdentityMapInput identityMapInput) {
String decryptedResponseString = uid2Helper.decrypt(responseString, envelope.getNonce());
return new IdentityMapResponse(decryptedResponseString, identityMapInput);
}

Uid2Helper uid2Helper;
}
88 changes: 88 additions & 0 deletions src/main/java/com/uid2/client/IdentityMapInput.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package com.uid2.client;

import com.google.gson.annotations.SerializedName;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;

public class IdentityMapInput {
/**
* @param emails a list of normalized or unnormalized email addresses
* @return a IdentityMapInput instance, to be used in {@link IdentityMapHelper#createEnvelopeForIdentityMapRequest}
*/
public static IdentityMapInput fromEmails(Iterable<String> emails) {
return new IdentityMapInput(IdentityType.Email, emails, false);
}

/**
* @param phones a <a href="https://unifiedid.com/docs/getting-started/gs-normalization-encoding#phone-number-normalization">normalized</a> phone number
* @return an IdentityMapInput instance
*/
public static IdentityMapInput fromPhones(Iterable<String> phones) {
return new IdentityMapInput(IdentityType.Phone, phones, false);
}

/**
* @param hashedEmails a <a href="https://unifiedid.com/docs/getting-started/gs-normalization-encoding#email-address-normalization">normalized</a> and <a href="https://unifiedid.com/docs/getting-started/gs-normalization-encoding#email-address-hash-encoding">hashed</a> email address
* @return an IdentityMapInput instance
*/
public static IdentityMapInput fromHashedEmails(Iterable<String> hashedEmails) {
return new IdentityMapInput(IdentityType.Email, hashedEmails, true);
}

/**
* @param hashedPhones a <a href="https://unifiedid.com/docs/getting-started/gs-normalization-encoding#phone-number-normalization">normalized</a> and <a href="https://unifiedid.com/docs/getting-started/gs-normalization-encoding#phone-number-hash-encoding">hashed</a> phone number
* @return an IdentityMapInput instance
*/
public static IdentityMapInput fromHashedPhones(Iterable<String> hashedPhones) {
return new IdentityMapInput(IdentityType.Phone, hashedPhones, true);
}

private IdentityMapInput(IdentityType identityType, Iterable<String> emailsOrPhones, boolean alreadyHashed) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens if emailsOrPhones is empty?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The request still works but getMappedIdentities() and getUnmappedIdentities return empty maps. Added test identityMapEmptyInput

if (identityType == IdentityType.Email) {
hashedNormalizedEmails = new ArrayList<>();
for (String email : emailsOrPhones) {
if (alreadyHashed) {
hashedNormalizedEmails.add(email);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the answer is yet, but does operator return hash exactly as passed without transformations? E.g. lowercase, etc. What happens if the same hash is specified multiple times on input?

Copy link
Contributor Author

@jon8787 jon8787 Feb 22, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, the hash is base64 encoded so it's case sensitive and is returned in the same case.
If same hash is specified multiple times, it is deduplicated in the response. I've added a test identityMapDuplicateHashedEmails

} else {
String hashedEmail = InputUtil.normalizeAndHashEmail(email);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens if input contains multiple instances of the same email or variants of it that result in the same hash?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks, good spot. Fixed and added identityMapDuplicateEmails test

hashedNormalizedEmails.add(hashedEmail);
hashedDiiToRawDii.put(hashedEmail, email);
}
}
} else { //phone
hashedNormalizedPhones = new ArrayList<>();
for (String phone : emailsOrPhones) {
if (alreadyHashed) {
hashedNormalizedPhones.add(phone);
} else {
if (!InputUtil.isPhoneNumberNormalized(phone)) {
throw new IllegalArgumentException("phone number is not normalized: " + phone);
}

String hashedNormalizedPhone = InputUtil.getBase64EncodedHash(phone);
hashedDiiToRawDii.put(hashedNormalizedPhone, phone);
hashedNormalizedPhones.add(hashedNormalizedPhone);
}
}
}
}


String getRawDii(String identifier) {
if (wasInputAlreadyHashed())
return identifier;
return hashedDiiToRawDii.get(identifier);
}

private boolean wasInputAlreadyHashed() {
return hashedDiiToRawDii.isEmpty();
}

@SerializedName("email_hash")
private List<String> hashedNormalizedEmails;
@SerializedName("phone_hash")
private List<String> hashedNormalizedPhones;

private final transient HashMap<String, String> hashedDiiToRawDii = new HashMap<>();
}
Loading
Loading