Skip to content

Entitlement documents in Amazon S3

When Vimond Monetise is used for subscription management and payments, the current entitlements for a user is stored in a per-user document in Amazon S3.
When updates to entitlements happens, either as a result of new subscription or PPV purchases or by updates using the API of the vimond-entitlements-sync-service, the new entitlements will be reflected in the users entitlements document in S3.

The name of the entitlements document in S3 will be based on the issuer of the identity provider holding the user, piped with the subject of the user from the same identity provider.
Given an identity provider with the issuer of https://vimond-dev-next-enduser.vimond.auth0.com/ and a user with the subject of auth0|61d2f18a4ce6b16d7afbdb6d
the basis for the filename will be https://vimond-dev-next-enduser.vimond.auth0.com/|auth0|61d2f18a4ce6b16d7afbdb6d

This string is then base 64 URL encoded to give the file name of the entitlements document for the user:

https://vimond-dev-next-enduser.vimond.auth0.com/|auth0|61d2f18a4ce6b16d7afbdb6d → base 64 URL encode → aHR0cHM6Ly92aW1vbmQtZGV2LW5leHQtZW5kdXNlci52aW1vbmQuYXV0aDAuY29tL3xhdXRoMHw2MWQyZjE4YTRjZTZiMTZkN2FmYmRiNmQ.json

const base64url = require('base64-url');
const iss = 'https://vimond-dev-next-enduser.vimond.auth0.com/';
const sub = 'auth0|61d2f18a4ce6b16d7afbdb6d';
const issAndSub = iss + '|' + sub;
const encodedUserId = base64url.encode(issAndSub);
console.log(encodedUserId);
// aHR0cHM6Ly92aW1vbmQtZGV2LW5leHQtZW5kdXNlci52aW1vbmQuYXV0aDAuY29tL3xhdXRoMHw2MWQyZjE4YTRjZTZiMTZkN2FmYmRiNmQ
import com.google.common.io.BaseEncoding;
class Encoder {
public void encode() {
final BaseEncoding encoder = BaseEncoding.base64Url().omitPadding();
final String iss = "https://vimond-dev-next-enduser.vimond.auth0.com/";
final String sub = "auth0|61d2f18a4ce6b16d7afbdb6d";
final String issAndSub = iss + '|' + sub;
final String fileName = encoder.encode(issAndSub.getBytes()) + ".json";
System.out.println(fileName);
// aHR0cHM6Ly92aW1vbmQtZGV2LW5leHQtZW5kdXNlci52aW1vbmQuYXV0aDAuY29tL3xhdXRoMHw2MWQyZjE4YTRjZTZiMTZkN2FmYmRiNmQ.json
}
}

The document stored on S3 will look like the following example:

{
"version": 2,
"entitlements": {
"vimond-rest-api": {
"productGroupIds": [
4352
],
"assetIds": [
339054,
339075
],
"categoryIds": []
},
"external-entitlements": {
"productGroupIds": [
4353,
4354
],
"globalUser": true,
"assetIds": [
339083
]
}
},
"mergedEntitlements": {
"productGroupIds": [
4352,
4353,
4354
],
"globalUser": true,
"assetIds": [
339054,
339083
],
"categoryIds": [],
"hasMoreTVOD": true
},
"mergedUnfilteredEntitlements": {
"productGroupIds": [
4352,
4353,
4354
],
"globalUser": true,
"assetIds": [
339054,
339083,
339075
],
"categoryIds": []
}
}

On the root of the object the following properties will be found:

  • version: The version of this document. Current version is 2
  • entitlements: A map of source to entitlements containing the entitlements entered from different sources of truth. Default is vimond-rest-api which are entitlements synchronised from subscriptions and single programs in the vimond rest API. The format of the entitlements object is described below.
  • mergedEntitlements: An entitlements object with the additional boolean property hasMoreTVOD. This is the entitlement object that should be the base for the entitlements added to the JWT token for the end user. The hasMoreTVOD property indicates that the assetIds array have been truncated because of the size. Default this is done at more than 100 asset ids.
  • mergedUnfilteredEntitlements: An entitlements object with the full list of asset ids.

The entitlements object are defined as follows:

  • productGroupIds: An array of products the user have bought access to either as subscription or single period.
  • assetIds: An array of pay-per-view assets the user have bought access to as single program (TVOD)
  • categoryIds: An array of pay-per-view categories the user have bought access to as single program (TVOD). Not yet supported
  • globalUser: A boolean property for bypassing geo-blocking for this user

How to map the entitlements into the entitlements claim in the tokens is described here:

Authentication - Custom Entitlement Claims

The entitlements document should be fetched from S3 using etag so that data is not transferred from S3 unless changed. Resulting entitlements should be stored/cached based on user and etag.

const AWS = require("aws-sdk@2.291.0");
const crypto = require("crypto");
const moment = require("moment@2.11.2");
const { URL } = require("url");
const request = require("superagent@3.8.3");
const _ = require("lodash@4.17.10");
const base64 = require("base64-url@1.2.1");
const config = {
userId: base64.encode(`${oidc.issuer}|${oidc.subject}`),
s3Etag: <stored etag>,
s3_entitlements_region: <aws region>,
s3_entitlements_bucket: <s3 entitlements bucket>
};
getEntitlementsFromS3()
.then((data) => {
if (data) {
// update cached data for user/etag
}
}).catch((err) => {
//handle error
});
function getEntitlements({userIds}){
if(userIds.vimond && configuration.s3_entitlements_access_key_id){
return getEntitlementsFromS3({
userIds,
eTags
});
}else{
return getEntitlementsFromApi({
userIds,
eTag: eTags.api,
updateMemberId : true});
}
}
function getEntitlementsFromS3(){
return new AWS.S3({
apiVersion: "2006-03-01",
region: config.s3_entitlements_region
// The bucket will not be open, so the request must be properly authorised
}).getObject({
Bucket: config.s3_entitlements_bucket,
Key: `${config.userId}.json`,
...(eTags.s3 && { IfNoneMatch: eTags.s3 })
})
.promise()
.then((data) => {
return filterAndConvertEntitlements({
entitlements: JSON.parse(data.Body.toString("utf-8")),
source: "s3",
eTag: data.ETag,
lastModified: data.LastModified
});
}, (err) => {
switch (err.code) {
case "NoSuchKey":
console.log(`Could not find entitlements for user '${userId}' on s3`);
return null;
case "NotModified":
console.log(`The entitlements for user ${oidc.subject} has not been modified`);
return null;
default:
throw new Error("AWS S3 getObject failed. Error: " + err + ". AWS.VERSION " + AWS.VERSION);
}
});
}
function filterAndConvertEntitlements({entitlements, source, eTag, lastModified}){
return {
entitlements: {
data: Object.entries(entitlements.hasOwnProperty("mergedEntitlements") ? entitlements.mergedEntitlements : entitlements)
.filter(([key, value]) => value === true || (Array.isArray(value) && value.length > 0))
.filter(([key]) => ["assetIds",
"categoryIds",
"productGroupIds",
"globalUser",
"serviceOriginEUCitizen",
"skipDeviceRegistration",
"hasMoreTVOD"].includes(key))
.reduce((values, [key, value]) => {
return Object.assign(values, {
[key]: Array.isArray(value) ? value.map(String) : value
});
}, {}),
source: {
[source]: {
eTag: eTag,
...(lastModified && {lastModified: lastModified})
}
}
}
}
}