diff --git a/.env.sample.euid b/.env.sample.euid index 421bd81..49e5275 100644 --- a/.env.sample.euid +++ b/.env.sample.euid @@ -19,6 +19,7 @@ SESSION_KEY="your-session-key-here" # Client-Side Token Generation (CSTG) - Provided by your EUID integration representative UID_CSTG_SERVER_PUBLIC_KEY="your-euid-server-public-key" UID_CSTG_SUBSCRIPTION_ID="your-euid-subscription-id" +UID_CSTG_ORIGIN="your-app-url.com" # The public URL where this application is deployed (e.g., https://your-domain.com or http://localhost:3034 for local dev) # React Client-Side Examples - Provided by your EUID integration representative # Note: These are the same values as the variables above, prefixed with REACT_APP_ diff --git a/.env.sample.uid2 b/.env.sample.uid2 index f57c04b..350e602 100644 --- a/.env.sample.uid2 +++ b/.env.sample.uid2 @@ -19,6 +19,7 @@ SESSION_KEY="your-session-key-here" # Client-Side Token Generation (CSTG) UID_CSTG_SERVER_PUBLIC_KEY="UID2-X-I-MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEo+jcPlk8GWn3iG0R5Il2cbFQI9hR3TvHxaBUKHl5Vh+ugr+9uLMiXihka8To07ETFGghEifY96Hrpe5RnYko7Q==" UID_CSTG_SUBSCRIPTION_ID="DMr7uHxqLU" +UID_CSTG_ORIGIN="your-app-url.com" # The public URL where this application is deployed (e.g., https://your-domain.com or http://localhost:3034 for local dev) # React Client-Side Examples # Note: These are the same values as the variables above, prefixed with REACT_APP_ diff --git a/docker-compose.yml b/docker-compose.yml index 93cf174..fdf740f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -20,6 +20,16 @@ services: env_file: - .env + javascript-sdk-server-side: + build: + context: . + dockerfile: web-integrations/javascript-sdk/server-side-node/Dockerfile + ports: + - "3034:3034" + container_name: javascript-sdk-server-side + env_file: + - .env + # server-side integration (no SDK) server-side: build: diff --git a/web-integrations/javascript-sdk/server-side-node/.gitignore b/web-integrations/javascript-sdk/server-side-node/.gitignore new file mode 100644 index 0000000..708db9f --- /dev/null +++ b/web-integrations/javascript-sdk/server-side-node/.gitignore @@ -0,0 +1,17 @@ +# Dependencies +node_modules/ +package-lock.json + +# Logs +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Environment variables +.env +.env.local +.env.*.local + +# OS files +.DS_Store + diff --git a/web-integrations/javascript-sdk/server-side-node/Dockerfile b/web-integrations/javascript-sdk/server-side-node/Dockerfile new file mode 100644 index 0000000..fcb0d80 --- /dev/null +++ b/web-integrations/javascript-sdk/server-side-node/Dockerfile @@ -0,0 +1,17 @@ +FROM node:20.11.0-alpine3.18 + +WORKDIR /usr/src/app + +# Copy package files first for better caching +COPY web-integrations/javascript-sdk/server-side-node/package*.json ./ +RUN npm install + +# Copy application files +COPY web-integrations/javascript-sdk/server-side-node/server.js ./ +COPY web-integrations/javascript-sdk/server-side-node/public ./public/ +COPY web-integrations/javascript-sdk/server-side-node/views ./views/ + +ENV PORT=3034 +EXPOSE 3034 +CMD ["npm", "start"] + diff --git a/web-integrations/javascript-sdk/server-side-node/README.md b/web-integrations/javascript-sdk/server-side-node/README.md new file mode 100644 index 0000000..f2615dc --- /dev/null +++ b/web-integrations/javascript-sdk/server-side-node/README.md @@ -0,0 +1,101 @@ +# Server-Side UID2 or EUID Integration Example using JavaScript SDK via Node.js + +This example showcases how the UID2/EUID **JavaScript SDK works in Node.js** server environments. It uses the same `setIdentityFromEmail` method that runs in browsers, but executes it on the server. This uses **public credentials** (Subscription ID + Server Public Key) which are the same credentials used for client-side integrations. + +For more information on the JavaScript SDK, refer to the [UID2 SDK for JavaScript](https://unifiedid.com/docs/sdks/sdk-ref-javascript) or [EUID SDK for JavaScript](https://euid.eu/docs/sdks/sdk-ref-javascript) documentation. + +> **Note:** This example can be configured for either UID2 or EUID — the behavior is determined by your environment variable configuration. You cannot use both simultaneously. + +## How This Implementation Works + +Unlike the browser where the SDK runs natively in the DOM, this example uses **jsdom** to simulate a browser environment within Node.js: + +1. **Imports the SDK**: Uses npm packages [`@uid2/uid2-sdk`](https://www.npmjs.com/package/@uid2/uid2-sdk) or [`@unified-id/euid-sdk`](https://www.npmjs.com/package/@unified-id/euid-sdk) (selected dynamically based on the `IDENTITY_NAME` environment variable) +2. **Creates a virtual DOM**: Uses jsdom to provide `window`, `document`, and `navigator` objects that the SDK expects +3. **Polyfills browser APIs**: Adds Node.js equivalents for Web Crypto API (`crypto.subtle`) and text encoding APIs (`TextEncoder`/`TextDecoder`) +4. **Instantiates the SDK**: Creates a new instance of `UID2` or `EUID` class +5. **Runs SDK methods**: Calls `setIdentityFromEmail` just like in a browser, with the same public credentials + +This demonstrates that the client-side SDK can be compatible with server-side Node.js environments when given the proper browser-like context. + +## Build and Run the Example Application + +### Using Docker Compose (Recommended) + +From the repository root directory: + +```bash +# Start the service +docker compose up javascript-sdk-server-side +``` + +The application will be available at http://localhost:3034 + +To view logs or stop the service: + +```bash +# View logs (in another terminal) +docker compose logs javascript-sdk-server-side + +# Stop the service +docker compose stop javascript-sdk-server-side +``` + +### Using Docker Build + +```bash +# Build the image +docker build -f web-integrations/javascript-sdk/server-side/Dockerfile -t javascript-sdk-server-side . + +# Run the container +docker run -it --rm -p 3034:3034 --env-file .env javascript-sdk-server-side +``` + +## Environment Variables + +The following table lists the environment variables that you must specify to start the application. + +### Core Configuration + +| Variable | Description | Example Values | +|:---------|:------------|:---------------| +| `UID_SERVER_BASE_URL` | The base URL of the UID2/EUID service. For details, see [Environments](https://unifiedid.com/docs/getting-started/gs-environments) (UID2) or [Environments](https://euid.eu/docs/getting-started/gs-environments) (EUID). | UID2: `https://operator-integ.uidapi.com`
EUID: `https://integ.euid.eu/v2` | +| `UID_CSTG_SUBSCRIPTION_ID` | Your UID2/EUID subscription ID for Client-Side Token Generation. **These are public credentials.** | Your assigned subscription ID (e.g., `DMr7uHxqLU`) | +| `UID_CSTG_SERVER_PUBLIC_KEY` | Your UID2/EUID server public key for Client-Side Token Generation. **These are public credentials.** | Your assigned public key | +| `UID_CSTG_ORIGIN` | The public URL where this application is deployed. Must match your CSTG subscription's allowed origins. | `https://your-domain.com` (production)
`http://localhost:3034` (local dev default) | +| `SESSION_KEY` | Used by the cookie-session middleware to encrypt the session data stored in cookies. | Any secure random string | + +### Display/UI Configuration + +| Variable | Description | Example Values | +|:---------|:------------|:---------------| +| `IDENTITY_NAME` | Identity name for UI display | UID2: `UID2`
EUID: `EUID` | +| `DOCS_BASE_URL` | Documentation base URL | UID2: `https://unifiedid.com/docs`
EUID: `https://euid.eu/docs` | + +After you see output similar to the following, the example application is up and running: + +``` +Server listening at http://localhost:3034 +``` + +If needed, to close the application, terminate the docker container or use the `Ctrl+C` keyboard shortcut. + +## Test the Example Application + +The following table outlines and annotates the steps you may take to test and explore the example application. + +| Step | Description | Comments | +|:----:|:------------|:---------| +| 1 | In your browser, navigate to the application main page at `http://localhost:3034`. | The displayed main (index) page provides a login form for the user to complete the UID2/EUID login process.
IMPORTANT: A real-life application must also display a form for the user to express their consent to targeted advertising. | +| 2 | Enter the user email address that you want to use for testing and click **Log In**. | This is a call to the `/login` endpoint ([server.js](server.js)). The login initiated on the server side uses the JavaScript SDK's `setIdentityFromEmail` method to generate a token and processes the received response. The SDK handles all encryption/decryption automatically, just as it does in the browser. | +| | The main page is updated to display the established identity information. | The displayed identity information is the `body` property of the response from the SDK's `setIdentityFromEmail` call. If the response is successful, the returned identity is saved to a session cookie (a real-world application would use a different way to store session data) and the protected index page is rendered. | +| 3 | Review the displayed identity information. | The server reads the user session and extracts the current identity ([server.js](server.js)). The `advertising_token` on the identity can be used for targeted advertising. Note that the identity contains several timestamps that determine when the advertising token becomes invalid (`identity_expires`) and when the server should attempt to refresh it (`refresh_from`). The `verifyIdentity` function ([server.js](server.js)) uses the SDK to refresh the token as needed.
The user is automatically logged out in the following cases:
- If the identity expires without being refreshed and refresh attempt fails.
- If the refresh token expires.
- If the refresh attempt indicates that the user has opted out. | +| 4 | To exit the application, click **Log Out**. | This calls the `/logout` endpoint on the server ([server.js](server.js)), which clears the session and presents the user with the login form again.
NOTE: The page displays the **Log Out** button as long as the user is logged in. | + +## Key Benefits + +This example demonstrates the advantages of using the JavaScript SDK on the server: + +- **Secure credential handling**: Public credentials (server public key and subscription ID) remain on the server and are not exposed to the browser +- **Simplified implementation**: The SDK handles the full token lifecycle including encryption, decryption, and refresh logic automatically +- **No manual cryptography**: Unlike traditional server-side integrations, there's no need to manually implement encryption/decryption processes diff --git a/web-integrations/javascript-sdk/server-side-node/package.json b/web-integrations/javascript-sdk/server-side-node/package.json new file mode 100644 index 0000000..fee0603 --- /dev/null +++ b/web-integrations/javascript-sdk/server-side-node/package.json @@ -0,0 +1,33 @@ +{ + "name": "uid2-javascript-sdk-server-side", + "version": "1.0.0", + "description": "Server-Side UID2/EUID Integration Example using JavaScript SDK", + "main": "server.js", + "scripts": { + "start": "node server.js", + "dev": "nodemon server.js" + }, + "keywords": [ + "uid2", + "euid", + "identity", + "server-side" + ], + "author": "", + "license": "Apache-2.0", + "dependencies": { + "@uid2/uid2-sdk": "^4.0.1", + "@unified-id/euid-sdk": "^4.0.1", + "cookie-session": "^2.0.0", + "dotenv": "^16.0.3", + "ejs": "^3.1.9", + "express": "^4.18.2", + "jsdom": "^23.0.0", + "nocache": "^4.0.0", + "xhr2": "^0.2.1" + }, + "devDependencies": { + "nodemon": "^3.0.1" + } +} + diff --git a/web-integrations/javascript-sdk/server-side-node/public/images/favicon.png b/web-integrations/javascript-sdk/server-side-node/public/images/favicon.png new file mode 100644 index 0000000..48885e0 Binary files /dev/null and b/web-integrations/javascript-sdk/server-side-node/public/images/favicon.png differ diff --git a/web-integrations/javascript-sdk/server-side-node/public/stylesheets/app.css b/web-integrations/javascript-sdk/server-side-node/public/stylesheets/app.css new file mode 100644 index 0000000..58b2cf8 --- /dev/null +++ b/web-integrations/javascript-sdk/server-side-node/public/stylesheets/app.css @@ -0,0 +1,99 @@ +body { + padding: 50px; + font: 14px "Lucida Grande", Helvetica, Arial, sans-serif; +} + +a { + color: #00B7FF; +} + +.button { + border-style: none; + cursor: pointer; + align-items: center; + height: 40px; + width: 401px; + text-align: center; + position: absolute; + letter-spacing: 0.28px; + box-sizing: border-box; + color: white; + font-family: 'Raleway', Helvetica, Arial, serif; + font-size: 14px; + font-style: normal; + font-weight: 700; + text-transform: none; + text-indent: 0px; + text-shadow: none; + margin: 0em; + padding: 1px 6px; + background-color: rgba(2,10,64,1.0); + border-image: initial +} + +.form { + margin-top: 40px; +} + +.email_prompt { + align-items: center; + align-self: center; + background-color: white; + border: 1px solid rgba(2,10,64,1.0); + border-radius: 2px; + box-sizing: border-box; + display: inline-flex; + flex-direction: row; + flex-shrink: 0; + height: 40px; + justify-content: flex-start; + margin-right: 1.0px; + margin-bottom: 20px; + min-width: 399px; + padding: 0 16.0px; + position: relative; + width: auto; +} + +#email { + background-color: white; + flex-shrink: 0; + height: auto; + letter-spacing: 0.12px; + line-height: 16px; + min-height: 16px; + position: relative; + text-align: left; + white-space: nowrap; + width: 351px; + color: rgba(2,10,64,1.0); + font-family: 'Raleway', Helvetica, Arial, serif; + font-size: 12px; + font-style: normal; + font-weight: 500; + padding: 1px 2px; + outline: none; +} + +h1 { + padding-bottom: 20px; +} + +#uid2_state .label { + white-space: nowrap; + padding-right: 20px; +} +#uid2_state tr { + margin-top: 10px; +} + +.message { + color: green; + padding: 20px; + margin-left: -22px; + font-size: 16px; + font-weight: 500; + border: 2px solid green; + border-radius: 5px; +} + diff --git a/web-integrations/javascript-sdk/server-side-node/server.js b/web-integrations/javascript-sdk/server-side-node/server.js new file mode 100644 index 0000000..2a9367d --- /dev/null +++ b/web-integrations/javascript-sdk/server-side-node/server.js @@ -0,0 +1,227 @@ +"use strict"; + +// Load environment variables +require('dotenv').config({ path: '../../../.env' }); + +const session = require('cookie-session'); +const ejs = require('ejs'); +const express = require('express'); +const crypto = require('crypto'); +const nocache = require('nocache'); + + +const app = express(); +const port = process.env.PORT || 3034; + +let uidBaseUrl = process.env.UID_SERVER_BASE_URL; +const subscriptionId = process.env.UID_CSTG_SUBSCRIPTION_ID; +const serverPublicKey = process.env.UID_CSTG_SERVER_PUBLIC_KEY; +const identityName = process.env.IDENTITY_NAME; +const docsBaseUrl = process.env.DOCS_BASE_URL; + + +// additional packages/variables needed to ensure compabitibility with the SDK +const { JSDOM } = require('jsdom'); // for simulating a browser environment +const util = require('util'); // for polyfilling TextEncoder and TextDecoder +const XMLHttpRequest = require('xhr2'); // for making HTTP requests +const clientOrigin = process.env.UID_CSTG_ORIGIN || `http://localhost:${port}`; // Client origin: the URL where this app is accessible + + +// Create a virtual DOM environment for the SDK to run in +let SdkClass = null; +let uidSdk = null; +let dom = null; + +async function initializeSDK() { + dom = new JSDOM('', { + url: clientOrigin, + runScripts: 'dangerously', + resources: 'usable', + pretendToBeVisual: true, + }); + + // Polyfills for Browser APIs, SDK uses them extensively (e.g., for token storage or making network requests + global.window = dom.window; + global.document = dom.window.document; + global.navigator = dom.window.navigator; + global.localStorage = dom.window.localStorage; + + // Polyfill Web Crypto API for jsdom (SDK uses crypto.subtle for AES-GCM encryption/decryption) + Object.defineProperty(dom.window, 'crypto', { + value: crypto.webcrypto, + writable: false, + configurable: true + }); + + // Polyfill TextEncoder and TextDecoder (required by SDK for string/byte conversion) + global.TextEncoder = util.TextEncoder; + global.TextDecoder = util.TextDecoder; + dom.window.TextEncoder = util.TextEncoder; + dom.window.TextDecoder = util.TextDecoder; + + // Polyfill XMLHttpRequest with Origin header support + const OriginalXHR = XMLHttpRequest; + class XMLHttpRequestWithOrigin extends OriginalXHR { + constructor() { + super(); + this._origin = clientOrigin; + this._customHeaders = {}; + } + + open(method, url, async) { + const result = super.open(method, url, async); + return result; + } + + setRequestHeader(header, value) { + // Allow 'Origin' header that xhr2 normally blocks + this._customHeaders[header] = value; + if (header.toLowerCase() !== 'origin') { + return super.setRequestHeader(header, value); + } + } + + send(body) { + if (!this._headers) { + this._headers = {}; + } + this._headers.origin = this._origin; + + return super.send(body); + } + } + + global.XMLHttpRequest = XMLHttpRequestWithOrigin; + dom.window.XMLHttpRequest = XMLHttpRequestWithOrigin; + + try { + const isEUID = identityName && identityName.toUpperCase() === 'EUID'; + if (isEUID) { + const { EUID } = await import('@unified-id/euid-sdk'); + SdkClass = EUID; + } else { + const { UID2 } = await import('@uid2/uid2-sdk'); + SdkClass = UID2; + } + + // Instantiate the SDK (UID2 or EUID based on config) with base URL + uidSdk = new SdkClass(); + uidSdk.init({ baseUrl: uidBaseUrl }); + + return uidSdk; + } catch (error) { + console.error('Failed to initialize SDK:', error); + throw error; + } +} + +// Express middleware setup +app.use(session({ + keys: [process.env.SESSION_KEY || 'default-session-key-change-me'], + maxAge: 24 * 60 * 60 * 1000 // 24 hours +})); + +app.use(express.static('public')); +app.use(express.urlencoded({ extended: true })); + +app.engine('.html', ejs.__express); +app.set('view engine', 'html'); + +app.use(nocache()); + +// Routes + +app.get('/', (req, res) => { + res.render('index', { + identity: req.session.identity || null, + identityName, + docsBaseUrl + }); +}); + +/** + * Handle login form submission + * Uses the JavaScript SDK's setIdentityFromEmail method on the server + */ +app.post('/login', async (req, res) => { + if (!uidSdk) { + return res.render('error', { + error: 'SDK not initialized. Server may still be starting up.', + response: null, + identityName, + docsBaseUrl + }); + } + + try { + // Call the SDK's setIdentityFromEmail method and wait for the result via callback + const identity = await new Promise((resolve, reject) => { + const timeout = setTimeout(() => { + reject(new Error('Token generation timed out after 10 seconds')); + }, 10000); + + const callbackHandler = (eventType, payload) => { + if ((eventType === 'InitCompleted' || eventType === 'IdentityUpdated') && payload?.identity) { + clearTimeout(timeout); + uidSdk.callbacks.splice(uidSdk.callbacks.indexOf(callbackHandler), 1); + resolve(payload.identity); + } + + if (eventType === 'OptoutReceived') { + clearTimeout(timeout); + uidSdk.callbacks.splice(uidSdk.callbacks.indexOf(callbackHandler), 1); + reject(new Error('Got unexpected token generate status: optout')); + } + }; + + uidSdk.callbacks.push(callbackHandler); + + uidSdk.setIdentityFromEmail( + req.body.email, + { + subscriptionId: subscriptionId, + serverPublicKey: serverPublicKey + } + ).catch(err => { + clearTimeout(timeout); + reject(err); + }); + }); + + if (!identity) { + throw new Error('No identity returned from SDK'); + } + + req.session.identity = identity; + res.redirect('/'); + + } catch (error) { + console.error('Token generation failed:', error.message); + req.session = null; + res.render('error', { + error: error.message || error.toString(), + response: error.response || null, + identityName, + docsBaseUrl + }); + } +}); + +app.get('/logout', (req, res) => { + if (uidSdk && uidSdk.disconnect) { + uidSdk.disconnect(); + } + req.session = null; + res.redirect('/'); +}); + +// Start server and initialize SDK +initializeSDK().then(() => { + app.listen(port, () => { + console.log(`Server listening at http://localhost:${port}`); + }); +}).catch(error => { + console.error('Failed to start server:', error); + process.exit(1); +}); + diff --git a/web-integrations/javascript-sdk/server-side-node/views/error.html b/web-integrations/javascript-sdk/server-side-node/views/error.html new file mode 100644 index 0000000..eb744be --- /dev/null +++ b/web-integrations/javascript-sdk/server-side-node/views/error.html @@ -0,0 +1,22 @@ + + + + + Server-Side <%- identityName %> Integration Example using JavaScript SDK + + + + +

Server-Side <%- identityName %> Integration Example using JavaScript SDK

+

+ This example demonstrates how a content publisher can use <%- identityName %> services and the JavaScript SDK on the server to implement the + server-side <%- identityName %> integration workflow. This proves that the JavaScript SDK (designed for browsers) also works in a Node.js server environment. + [Source Code] +

+ +

Something went wrong:

+
<%= error %>
+

Back to the main page

+ + + diff --git a/web-integrations/javascript-sdk/server-side-node/views/index.html b/web-integrations/javascript-sdk/server-side-node/views/index.html new file mode 100644 index 0000000..ff40360 --- /dev/null +++ b/web-integrations/javascript-sdk/server-side-node/views/index.html @@ -0,0 +1,39 @@ + + + + + Server-Side <%- identityName %> Integration Example using JavaScript SDK via Node.js + + + + +

Server-Side <%- identityName %> Integration Example using JavaScript SDK

+

+ This example demonstrates how a content publisher can use <%- identityName %> services and the JavaScript SDK on the server to implement the + server-side <%- identityName %> integration workflow. This showcases how the JavaScript SDK (designed for browsers) can also work in a Node.js server environment. + [Source Code] +

+ + <% if (identity) { %> + +

Current <%- identityName %> Identity:

+
<%= JSON.stringify(identity, null, 2) %>
+
+
+ +
+
+ <% } else { %> + +
+
+ +
+
+
+ <% } %> + + +