Storing Consent Preferences
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.
- Toggle "on" the Preference Store feature (see screenshot)
- Click on View Encryption Key to see the
encryptionKey
in the code example above. Save the contents somewhere safe! - Click on View JWT Signing Key to see the
signingKey
in the code example above. Again, please save the contents somewhere safe!
- Make sure Reporting Only mode is off
- Your domain is included in the Domains List (e.g.
localhost:3033
if you're using the backend-consent-example) - 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.)
- 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 Key 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 Key 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 Key partition: 'ee1a0845-694e-4820-9d51-50c7d0a23467', // updatedBefore and updatedAfter filters are set to retrieve consent records that were synced in the last 24 hours updatedBefore: new Date().toISOString(), updatedAfter: (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!