Storing Consent Preferences in the Preference Store

You can persist your users' consent preferences to the Preference Store, and synchronize consent preferences across devices and apps.

When a user has signed in on your application, your backend will serve a token to your frontend, which allows Transcend's SDK to save this authenticated user's consent preferences.

The token you'll serve is a JSON Web Token (JWT) containing a unique identifier for this user.

The identifier is encrypted on your backend server, placed in a JWT, and signed by your backend server. The encryption and signing keys can be found in the Admin Dashboard. More info about these keys can be found here.

The encryption algorithm is AES-KWP. The signing algorithm is HMAC-SHA-384.

In addition to the user ID, you may include a creation or expiry date to the token's payload (see examples below). Ultimately this JWT token can be stored locally on the user's device, and we recommend creating a new one for each logged in session. Note: some libraries (ex. JWT in TypeScript) will automatically insert an issue time (iat) to your payload.

import * as crypto from 'crypto';
import * as jwt from 'jsonwebtoken';

function createConsentToken(
  userId: string,
  base64EncryptionKey: string,
  base64SigningKey: string
): string {
  // Read on for where to find these keys
  const signingKey = Buffer.from(base64SigningKey, 'base64');
  const encryptionKey = Buffer.from(base64EncryptionKey, 'base64');

  // NIST's AES-KWP implementation { aes 48 } - see https://tools.ietf.org/html/rfc5649
  const encryptionAlgorithm = 'id-aes256-wrap-pad';
  // Initial Value for AES-KWP integrity check - see https://tools.ietf.org/html/rfc5649#section-3
  const iv = Buffer.from('A65959A6', 'hex');
  // Set up encryption algorithm
  const cipher = crypto.createCipheriv(encryptionAlgorithm, encryptionKey, iv);

  // Set an issued date for the token
  const issued: Date = new Date();

  // Encrypt the userId and base64-encode the result
  const encryptedIdentifier = Buffer.concat([
    cipher.update(userId),
    cipher.final(),
  ]).toString('base64');

  // Create the JWT content - jwt.sign will add a 'iat' (issued at) field to the payload
  // If you wanted to add something manually, consider
  // const issued: Date = new Date();
  // const isoDate = issued.toISOString();
  const jwtPayload = {
    encryptedIdentifier,
  };

  // Create a JSON web token and HMAC it with SHA-384
  const consentToken = jwt.sign(jwtPayload, signingKey, {
    algorithm: 'HS384',
  });

  return consentToken;
}

Notes:

  • PyJWT has namespace conflicts with JWT.
  • Avoid naming your scripts the same name as a library (for ex jwt.py).

N.B. pip install cryptography pyjwt

import base64
import jwt
from cryptography.hazmat.primitives.keywrap import aes_key_wrap_with_padding
from datetime import datetime

def create_consent_token(user_id: str, base64_encryption_key: str, base64_signing_key: str) -> str:
    # Read on for where to find these keys
    encryption_key = base64.b64decode(base64_encryption_key)
    signing_key = base64.b64decode(base64_signing_key)

    # encode user_id as bytes
    user_id_bytes = str.encode(user_id)

    # NIST's AES-KWP implementation { aes 48 } - see https://tools.ietf.org/html/rfc5649
    encrypted_identifier = aes_key_wrap_with_padding(encryption_key, user_id_bytes)

    # base64-encode the result
    base64_encrypted_identifier = base64.b64encode(encrypted_identifier)

    # Create the JWT content
    jwt_payload = { 'encryptedIdentifier': base64_encrypted_identifier.decode(),
                    'iat': datetime.now()}

    # Create a JSON web token and HMAC it with SHA-384
    consent_token = jwt.encode(jwt_payload, signing_key, algorithm="HS384")

    return consent_token

Your server will generate the token and return it to the frontend (likely as part of the response payload for the login flow):

// When a user has authenticated
const consentToken = createConsentToken(
  userId,
  base64EncryptionKey,
  base64SigningKey
);

return {
  status: 200,
  body: {
    user: authenticatedUserData,
    consentToken,
  }
}

On the frontend, you will then pass this token to Transcend:

// Authenticate the user to airgap, and sync their consent with Transcend's Preference Store
airgap.sync({ auth: consentToken });

The keys above, encryptionKey and signingKey, can be retrieved from the Admin Dashboard, under the "Preference Store" section on the Developer Settings page.

  1. Toggle "on" the Preference Store feature (see screenshot)
  2. Click on View Encryption Key to see the encryptionKey in the code example above. Save the contents somewhere safe!
  3. Click on View JWT Signing Key to see the signingKey in the code example above. Again, please save the contents somewhere safe!

  1. Make sure Reporting Only mode is off
  2. Your domain is included in the Domains List (e.g. localhost:3033 if you're using the backend-consent-example)
  3. Navigate to Regional Experiences and make sure that you are in a region with an experience defined. (e.g. if you are in New York, set an experience using either the region of New York or the New York time zone (or both) and add a purpose (e.g. Advertising) to it.)
  4. If you haven't already, copy the HTML Snippet into the page you want to test this on (using the Test bundle)

Now that you've collected the user consent from the web, you may need to look up their consent record as part of executing backend data processes. This can be easily done using our Sombra API.

Endpoint reference: Query User Consent

Your backend server may need to gate tracking/data-processing logic based on the user consent preferences. An example of how to do so for user with email 'foo@example.com':

const request = require('request');

const result = request.post(
  '{{yourOrganizationSombraURL}}/v1/consent-preferences',
  {
    headers: {
      // use the scope `View Preference Store Admin API`
      authorization: 'Bearer {{apiKey}}',
      // only required for single tenant sombra
      'x-sombra-authorization': 'Bearer {{sombraApiKey}}',
      'content-type': 'application/json',
    },
    body: {
      filterBy: {
        identifiers: 'foo@example.com',
        // grab this value from the Admin Dashboard > Consent > Developer Settings
        partition: 'ee1a0845-694e-4820-9d51-50c7d0a23467',
      },
    },
    json: true,
  }
);

const consentPreference = result.nodes[0];

// May be undefined/empty list if the user has never given consent
if (consentPreference?.purposes?.Advertising) {
  // The user has given consent to Advertising, perform some custom logic...
}

if (consentPreferences?.purposes?.Functional !== false) {
  // The user has NOT explicitly opted out of Functional tracking purpose, perform some custom logic...
}

A common scenario: your company uploads a list of users to Facebook once a week, and you need to filter this list to exclude users who have opted out of targeted advertising. Here's an example of how you might do so:

const request = require('request');

const { nodes: userConsentPreferences } = request.post(
  '{{yourOrganizationSombraURL}}/v1/consent-preferences',
  {
    headers: {
      // use the scope `View Preference Store Admin API`
      authorization: 'Bearer {{apiKey}}',
      'x-sombra-authorization': 'Bearer {{sombraApiKey}}',
      'content-type': 'application/json',
    },
    body: {
      filterBy: {
        identifiers: userIdentifiers,
        // grab this value from the Admin Dashboard > Consent > Developer Settings
        partition: 'ee1a0845-694e-4820-9d51-50c7d0a23467',
      },
    },
    json: true,
  }
);

const usersToUpload = userIdentifiers.filter((identifier) =>
  userConsentPreferences.find(
    (data) =>
      data.userId === identifier &&
      // This logic makes sure that the user has not explicitly opted out
      // You may also write your own logic here to check for explicit consent (opt-in) as well
      data.purposes?.Advertising !== false &&
      data.purposes?.SaleOfInfo !== false
  )
);

// Now you can safely upload `usersToUpload`

There might be the need to query user consent preferences daily to update/trigger certain workflows. Here's an example on how to query daily user consent preferences with pagination:

const request = require('request');

async function paginateThroughConsentPreferences() {
  let currentLastKey = undefined;
  const data = [];

  while (true) {
    const { nodes, lastKey } = await request.post(
      '{{yourOrganizationSombraURL}}/v1/consent-preferences',
      {
        headers: {
          // use the scope `View Preference Store Admin API`
          authorization: 'Bearer {{apiKey}}',
          'x-sombra-authorization': 'Bearer {{sombraApiKey}}',
          'content-type': 'application/json',
        },
        body: {
          filterBy: {
            // grab this value from the Admin Dashboard > Consent > Developer Settings
            partition: 'ee1a0845-694e-4820-9d51-50c7d0a23467',
            // timestampBefore and timestampAfter filters are set to retrieve consent preferences set in the last 24 hours
            timestampBefore: new Date().toISOString(),
            timestampAfter: (
              new Date() -
              1 * 24 * 60 * 60 * 1000
            ).toISOString(),
            // using lastKey to paginate if it exists (will not for first iteration)
            startKey: currentLastKey || undefined,
          },
        },
        json: true,
      }
    );

    if (!nodes || nodes.length === 0) {
      break;
    }

    // Process the data received from the API call
    // For example, push the new data into an array
    data.push(...nodes);

    // Extract the lastKey from the API response
    currentLastKey = lastKey;

    // If there is no lastKey, it means we have reached the end of the data
    if (!currentLastKey) {
      break;
    }
  }

  return data;
}
// Call the function to start paginating through the API data
const consentPreferences = await paginateThroughConsentPreferences();

// Now you can loop through user preferences and use the data to trigger any internal processes!