From 22770cc77292c5e0e3856bd391e38c3cc08a27df Mon Sep 17 00:00:00 2001 From: Scott Sundahl Date: Wed, 7 Feb 2024 16:18:40 -0700 Subject: [PATCH 1/5] initial technical implementation --- integrationExamples/gpt/cstg_example.html | 323 ++++++++++++++++++++++ modules/euidIdSystem.js | 12 +- modules/euidIdSystem.md | 4 +- modules/uid2IdSystem_shared.js | 16 ++ 4 files changed, 346 insertions(+), 9 deletions(-) create mode 100644 integrationExamples/gpt/cstg_example.html diff --git a/integrationExamples/gpt/cstg_example.html b/integrationExamples/gpt/cstg_example.html new file mode 100644 index 00000000000..bc41148fde1 --- /dev/null +++ b/integrationExamples/gpt/cstg_example.html @@ -0,0 +1,323 @@ + + + + + UID2 and EUID Prebid.js Integration Example + + + + +

UID2 and EUID Prebid.js Integration Examples

+ +

+ This example demonstrates how a content publisher can integrate with UID2 and Prebid.js using the UID2 Client-Side Integration Guide for Prebid.js, which includes generating UID2 tokens within the browser.
+ This example is configured to hit endpoints at https://operator-integ.uidapi.com. Calls to this endpoint will be rejected if made from localhost.
+ A working sample subscription_id and client_key are declared in the javascript. Please override them in set[Uid2|Euid]Config() to test with your own CSTG credentials.
+ Note Generation of UID2 after EUID will fail due to consent settings on pbjs config. + +

+ +

UID2 Example

+
+ + + + + + + + + + + + + +
CSTG Subscription Id:
CSTG Public Key:
Email Address (DII): + +
+ +
+
+ + + + + + + + + +
Ready for Targeted Advertising:
UID2 Advertising Token:
+
+ +
+
+
+

EUID Example

+
+ + + + + + + + + + + + + +
CSTG Subscription Id:
CSTG Public Key:
Email Address (DII): + +
+ +
+
+ + + + + + + + + +
Ready for Targeted Advertising:
EUID Advertising Token:
+
+ +
+ + diff --git a/modules/euidIdSystem.js b/modules/euidIdSystem.js index fa6113250a8..d98dc02cdce 100644 --- a/modules/euidIdSystem.js +++ b/modules/euidIdSystem.js @@ -14,13 +14,6 @@ import {MODULE_TYPE_UID} from '../src/activities/modules.js'; // eslint-disable-next-line prebid/validate-imports import { Uid2GetId, Uid2CodeVersion, extractIdentityFromParams } from './uid2IdSystem_shared.js'; -/** - * @typedef {import('../modules/userId/index.js').Submodule} Submodule - * @typedef {import('../modules/userId/index.js').SubmoduleConfig} SubmoduleConfig - * @typedef {import('../modules/userId/index.js').ConsentData} ConsentData - * @typedef {import('../modules/userId/index.js').euidId} euidId - */ - const MODULE_NAME = 'euid'; const MODULE_REVISION = Uid2CodeVersion; const PREBID_VERSION = '$prebid.version$'; @@ -110,6 +103,7 @@ export const euidIdSubmodule = { mappedConfig.cstg = { serverPublicKey: config?.params?.serverPublicKey, subscriptionId: config?.params?.subscriptionId, + optoutCheck: 1, ...extractIdentityFromParams(config?.params ?? {}) } } @@ -135,6 +129,10 @@ function decodeImpl(value) { const result = { euid: { id: value } }; return result; } + if (value.latestToken === 'optout') { + _logInfo('Found optout token. Refresh is unavailable for this token.'); + return { euid: { optout: true } }; + } if (Date.now() < value.latestToken.identity_expires) { return { euid: { id: value.latestToken.advertising_token } }; } diff --git a/modules/euidIdSystem.md b/modules/euidIdSystem.md index 72e40b8ce7b..82d943496cf 100644 --- a/modules/euidIdSystem.md +++ b/modules/euidIdSystem.md @@ -4,10 +4,10 @@ The EUID module handles storing, providing, and optionally refreshing tokens. Wh *Server Only* mode was originally referred to as *legacy mode*, but it is a popular mode for new integrations where publishers prefer to handle token refresh server-side. -*Client-Side Token Generation* mode is included in EUID module by default. However, it's important to note that this mode is created and made available recently. For publishers who do not intend to use it, you have the option to instruct the build to exclude the code related to this feature: +*Client-Side Token Generation* mode is included in EUID module by default. However, it's important to note that this mode was created and made available recently. For publishers who do not intend to use it, you have the option to instruct the build to exclude the code related to this feature: ``` - $ gulp build --modules=uid2IdSystem --disable UID2_CSTG + $ gulp build --modules=euidIdSystem --disable UID2_CSTG ``` If you do plan to use Client-Side Token Generation (CSTG) mode, please consult the EUID Team first as they will provide required configuration values for you to use (see the Client-Side Token Generation (CSTG) mode section below for details) diff --git a/modules/uid2IdSystem_shared.js b/modules/uid2IdSystem_shared.js index acc440eafc5..e6a9097b95a 100644 --- a/modules/uid2IdSystem_shared.js +++ b/modules/uid2IdSystem_shared.js @@ -254,6 +254,9 @@ if (FEATURES.UID2_CSTG) { isStoredTokenInvalid(cstgIdentity, storedTokens, _logInfo, _logWarn) { if (storedTokens) { + if (storedTokens.latestToken === 'optout') { + return true; + } const identity = Object.values(cstgIdentity)[0]; if (!this.isStoredTokenFromSameIdentity(storedTokens, identity)) { _logInfo( @@ -386,6 +389,7 @@ if (FEATURES.UID2_CSTG) { this._baseUrl = opts.baseUrl; this._serverPublicKey = opts.cstg.serverPublicKey; this._subscriptionId = opts.cstg.subscriptionId; + this._optoutCheck = opts.cstg.optoutCheck; this._logInfo = logInfo; this._logWarn = logWarn; } @@ -402,6 +406,12 @@ if (FEATURES.UID2_CSTG) { ); } + isCstgApiOptoutResponse(response) { + return ( + this.hasStatusResponse(response) && + response.status === 'optout'); + } + isCstgApiClientErrorResponse(response) { return ( this.hasStatusResponse(response) && @@ -460,6 +470,7 @@ if (FEATURES.UID2_CSTG) { ), timestamp: now, subscription_id: this._subscriptionId, + optout_check: this._optoutCheck, }; return this.callCstgApi(requestBody, box); } @@ -491,6 +502,11 @@ if (FEATURES.UID2_CSTG) { status: 'success', identity: response.body, }); + } else if (this.isCstgApiOptoutResponse(response)) { + resolvePromise({ + status: 'optout', + identity: 'optout', + }); } else { // A 200 should always be a success response. // Something has gone wrong. From 826a070dc74453ad422a58940ca81d6af454ebfd Mon Sep 17 00:00:00 2001 From: Scott Sundahl Date: Wed, 7 Feb 2024 16:24:19 -0700 Subject: [PATCH 2/5] initial technical implementation --- modules/euidIdSystem.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/modules/euidIdSystem.js b/modules/euidIdSystem.js index d98dc02cdce..6d8c4d2f1a7 100644 --- a/modules/euidIdSystem.js +++ b/modules/euidIdSystem.js @@ -14,6 +14,13 @@ import {MODULE_TYPE_UID} from '../src/activities/modules.js'; // eslint-disable-next-line prebid/validate-imports import { Uid2GetId, Uid2CodeVersion, extractIdentityFromParams } from './uid2IdSystem_shared.js'; +/** + * @typedef {import('../modules/userId/index.js').Submodule} Submodule + * @typedef {import('../modules/userId/index.js').SubmoduleConfig} SubmoduleConfig + * @typedef {import('../modules/userId/index.js').ConsentData} ConsentData + * @typedef {import('../modules/userId/index.js').euidId} euidId + */ + const MODULE_NAME = 'euid'; const MODULE_REVISION = Uid2CodeVersion; const PREBID_VERSION = '$prebid.version$'; From f01e00bc5a49eaa761d6e031c2834e944fe16da5 Mon Sep 17 00:00:00 2001 From: Scott Sundahl Date: Thu, 8 Feb 2024 15:11:20 -0700 Subject: [PATCH 3/5] test and doc update --- modules/euidIdSystem.js | 7 ------- modules/euidIdSystem.md | 5 +++++ test/spec/modules/euidIdSystem_spec.js | 14 ++++++++++++++ 3 files changed, 19 insertions(+), 7 deletions(-) diff --git a/modules/euidIdSystem.js b/modules/euidIdSystem.js index 6d8c4d2f1a7..d98dc02cdce 100644 --- a/modules/euidIdSystem.js +++ b/modules/euidIdSystem.js @@ -14,13 +14,6 @@ import {MODULE_TYPE_UID} from '../src/activities/modules.js'; // eslint-disable-next-line prebid/validate-imports import { Uid2GetId, Uid2CodeVersion, extractIdentityFromParams } from './uid2IdSystem_shared.js'; -/** - * @typedef {import('../modules/userId/index.js').Submodule} Submodule - * @typedef {import('../modules/userId/index.js').SubmoduleConfig} SubmoduleConfig - * @typedef {import('../modules/userId/index.js').ConsentData} ConsentData - * @typedef {import('../modules/userId/index.js').euidId} euidId - */ - const MODULE_NAME = 'euid'; const MODULE_REVISION = Uid2CodeVersion; const PREBID_VERSION = '$prebid.version$'; diff --git a/modules/euidIdSystem.md b/modules/euidIdSystem.md index 82d943496cf..9c3f730da83 100644 --- a/modules/euidIdSystem.md +++ b/modules/euidIdSystem.md @@ -157,6 +157,11 @@ The module stores a number of internal values. By default, all values are stored `{`
  `"advertising_token": "...",`
  `"refresh_token": "...",`
  `"identity_expires": 1633643601000,`
  `"refresh_from": 1633643001000,`
  `"refresh_expires": 1636322000000,`
  `"refresh_response_key": "wR5t6HKMfJ2r4J7fEGX9Gw=="`
`}` +## Optout response + +`{`
  `"optout": "true",`
`}` + + ### Notes If you are trying to limit the size of cookies, provide the token in configuration and use the default option of local storage. diff --git a/test/spec/modules/euidIdSystem_spec.js b/test/spec/modules/euidIdSystem_spec.js index 98770fa80bc..9ad2b69e89c 100644 --- a/test/spec/modules/euidIdSystem_spec.js +++ b/test/spec/modules/euidIdSystem_spec.js @@ -22,6 +22,7 @@ const refreshedToken = 'refreshed-advertising-token'; const auctionDelayMs = 10; const makeEuidIdentityContainer = (token) => ({euid: {id: token}}); +const makeEuidOptoutContainer = (token) => ({euid: {optout: true}}); const useLocalStorage = true; const makePrebidConfig = (params = null, extraSettings = {}, debug = false) => ({ @@ -30,12 +31,15 @@ const makePrebidConfig = (params = null, extraSettings = {}, debug = false) => ( const cstgConfigParams = { serverPublicKey: 'UID2-X-L-24B8a/eLYBmRkXA9yPgRZt+ouKbXewG2OPs23+ov3JC8mtYJBCx6AxGwJ4MlwUcguebhdDp2CvzsCgS9ogwwGA==', subscriptionId: 'subscription-id' } const clientSideGeneratedToken = 'client-side-generated-advertising-token'; +const optoutToken = 'optout-token'; const apiUrl = 'https://prod.euid.eu/v2/token/refresh'; const cstgApiUrl = 'https://prod.euid.eu/v2/token/client-generate'; const headers = { 'Content-Type': 'application/json' }; const makeSuccessResponseBody = (token) => btoa(JSON.stringify({ status: 'success', body: { ...apiHelpers.makeTokenResponse(initialToken), advertising_token: token } })); +const makeOptoutResponseBody = (token) => btoa(JSON.stringify({ status: 'optout', body: { ...apiHelpers.makeTokenResponse(initialToken), advertising_token: token } })); const expectToken = (bid, token) => expect(bid?.userId ?? {}).to.deep.include(makeEuidIdentityContainer(token)); +const expectOptout = (bid, token) => expect(bid?.userId ?? {}).to.deep.include(makeEuidOptoutContainer(token)); const expectNoIdentity = (bid) => expect(bid).to.not.haveOwnProperty('userId'); describe('EUID module', function() { @@ -146,5 +150,15 @@ describe('EUID module', function() { const bid = await runAuction(); expectToken(bid, clientSideGeneratedToken); }); + it('Should receive an optout response when the user has opted out.', async function() { + setGdprApplies(true); + const euidToken = apiHelpers.makeTokenResponse(initialToken, true, true); + configureEuidCstgResponse(200, makeOptoutResponseBody(optoutToken)); + config.setConfig(makePrebidConfig({ euidToken, ...cstgConfigParams, email: 'optout@test.com' })); + apiHelpers.respondAfterDelay(1, server); + + const bid = await runAuction(); + expectOptout(bid, optoutToken); + }); } }); From 7f7c316f727fc66e500e36bb3e0e7cb9966b571d Mon Sep 17 00:00:00 2001 From: Scott Sundahl Date: Fri, 9 Feb 2024 16:50:27 -0700 Subject: [PATCH 4/5] optout check in encrypted payload --- modules/uid2IdSystem_shared.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/uid2IdSystem_shared.js b/modules/uid2IdSystem_shared.js index e6a9097b95a..102d217a658 100644 --- a/modules/uid2IdSystem_shared.js +++ b/modules/uid2IdSystem_shared.js @@ -447,7 +447,8 @@ if (FEATURES.UID2_CSTG) { } async generateToken(cstgIdentity) { - const request = await this.generateCstgRequest(cstgIdentity); + const requestIdentity = await this.generateCstgRequest(cstgIdentity); + const request = { optout_check: this._optoutCheck, ...requestIdentity }; this._logInfo('Building CSTG request for', request); const box = await UID2CstgBox.build( this.stripPublicKeyPrefix(this._serverPublicKey) @@ -470,7 +471,6 @@ if (FEATURES.UID2_CSTG) { ), timestamp: now, subscription_id: this._subscriptionId, - optout_check: this._optoutCheck, }; return this.callCstgApi(requestBody, box); } From 36393a30814179e62f672b3e424f1b510a75a543 Mon Sep 17 00:00:00 2001 From: Scott Sundahl Date: Mon, 12 Feb 2024 09:11:13 -0700 Subject: [PATCH 5/5] fixed cstg example config --- integrationExamples/gpt/cstg_example.html | 30 +++++++++-------------- 1 file changed, 12 insertions(+), 18 deletions(-) diff --git a/integrationExamples/gpt/cstg_example.html b/integrationExamples/gpt/cstg_example.html index bc41148fde1..8ca049a0ed0 100644 --- a/integrationExamples/gpt/cstg_example.html +++ b/integrationExamples/gpt/cstg_example.html @@ -20,8 +20,8 @@ // *********************************** UID2 *********************************** console.log('Initializing example.') - window.uid2_example_settings = { - UID2_BASE_URL: undefined, + window.example_settings = { + BASE_URL: undefined, SERVER_PUBLIC_KEY: undefined, SUBSCRIPTION_ID: undefined, }; @@ -55,11 +55,11 @@ function setUid2Config(email) { // show credentials in the UI - document.querySelector('#uid2SubscriptionId').innerText = window.uid2_example_settings.SUBSCRIPTION_ID; - document.querySelector('#uid2PublicKey').innerText = window.uid2_example_settings.SERVER_PUBLIC_KEY; - const cstgParams = email ? {email, subscriptionId: window.uid2_example_settings.SUBSCRIPTION_ID, serverPublicKey: window.uid2_example_settings.SERVER_PUBLIC_KEY} : {}; + document.querySelector('#uid2SubscriptionId').innerText = window.example_settings.SUBSCRIPTION_ID; + document.querySelector('#uid2PublicKey').innerText = window.example_settings.SERVER_PUBLIC_KEY; + const cstgParams = email ? {email, subscriptionId: window.example_settings.SUBSCRIPTION_ID, serverPublicKey: window.example_settings.SERVER_PUBLIC_KEY} : {}; const uid2Params = { - uid2ApiBase: window.uid2_example_settings.UID2_BASE_URL, + uid2ApiBase: window.example_settings.BASE_URL, ...cstgParams, }; @@ -81,20 +81,14 @@ // *********************************** EUID *********************************** - window.euid_example_settings = { - EUID_BASE_URL: undefined, - SERVER_PUBLIC_KEY: undefined, - SUBSCRIPTION_ID: undefined, - } - function setEuidConfig(email) { // show credentials in the UI - document.querySelector('#euidSubscriptionId').innerText = window.euid_example_settings.SUBSCRIPTION_ID; - document.querySelector('#euidPublicKey').innerText = window.euid_example_settings.SERVER_PUBLIC_KEY; + document.querySelector('#euidSubscriptionId').innerText = window.example_settings.SUBSCRIPTION_ID; + document.querySelector('#euidPublicKey').innerText = window.example_settings.SERVER_PUBLIC_KEY; - const cstgParams = email ? {email, subscriptionId: window.euid_example_settings.SUBSCRIPTION_ID, serverPublicKey: window.euid_example_settings.SERVER_PUBLIC_KEY} : {}; + const cstgParams = email ? {email, subscriptionId: window.example_settings.SUBSCRIPTION_ID, serverPublicKey: window.example_settings.SERVER_PUBLIC_KEY} : {}; const euidParams = { - euidApiBase: window.euid_example_settings.EUID_BASE_URL, + euidApiBase: window.example_settings.BASE_URL, ...cstgParams, }; pbjs.setConfig({ @@ -200,8 +194,8 @@ pbjs.que = pbjs.que || []; pbjs.que.push(function () { // uncomment one to pull an existing token from localStorage on load - //setUid2Config(); - setEuidConfig() + setUid2Config(); + //setEuidConfig() pbjs.addAdUnits(adUnits); pbjs.requestBids(); });