Client-Side Javascript
If you are only interested in a client-side implementation, you can perform an API call to check access in the page itself using javascript.
Be sure to read about how to initialize the Piano javascript on your pages, as this code snippet assumes that you have already embedded the tinypass.min.js javascript file on the page and properly initialized the tp.
tp.push(["init", function() {
var params = {
rid : "PREMIUM_ACCESS"
};
var callback = function(response) {
if (response.access && response.access.granted) {
// the user has access
}
else {
// the user does not have access
}
}
tp.api.callApi("/access/check", params, callback);
}]);
A client-side implementation can be easier to get up and running than setting up server-side checks. However, client-side implementations are inherently less secure due to the necessity that the browser has JavaScript enabled and, in most cases, the full content will be delivered to the browser before being obfuscated.
Our metered paywall product in our dashboard provides the option “Don’t track users coming directly from external sites” that when checked ensures that users that arrive at your site from an external site (such as search and social sites) will not increment the meter. We commonly refer to this as the “first click free” feature.
If you would like to replicate the “first click free” feature in a client-side implementation of a hard paywall we provide the below javascript function tpFirstClickFree() as our suggested solution. It first checks to see if the user came from one of the allowed domains (google.com, twitter.com, facebook.com) and returns “true” if that is the case. When implementing this function, call it before doing the access checking, and if the result of this function is “true” do not perform the access check and allow the browser through to the content without showing the hard paywall.
/**
* Determine whether the content should be free based on referrer
* @returns {boolean}
*/
function tpFirstClickFree(){
// Specify allowed domains or referrer parameters here
var allowedDomains = [
/google\.com/,
/twitter\.com/,
/facebook\.com/
];
if (document.referrer.length > 0){
for (var i in allowedDomains){
if (document.referrer.match(allowedDomains[i])){
return true;
}
}
}
return false;
}
Server-Side REST
Checking access via server-side REST is a simple as making a REST call to Piano.
All available base URLs for Production are listed here.
Sending a request to publisher/user/access/check will check if the specified user's UID has access to the resource in question.
curl https://api.piano.io/api/v3/publisher/user/access/check \
-d 'aid=MODETWOAPP' \
-d 'rid=RID' \
-d 'uid=my_user_uid' \
-d 'api_token=4uRW8r31E3giOYnGLxfKq53yOOyziodoEpi6oBWw'
{
"code" : 0,
"ts" : 1432145735,
"access" : {
"access_id" : "hgSS9tAdKmKx",
"granted" : true,
"expire_date" : 1434822785,
"resource" : {
"rid" : "RID",
"aid" : "MODETWOAPP",
"deleted" : false,
"disabled" : false,
"create_date" : 1415130054,
"update_date" : 1418720853,
"publish_date" : 1415130054,
"name" : "Test Resource",
}
}
}
Note, that we do ignore charset when deciding which response format to choose for the 'application/json' media type.
Alternatively, here's an example of the same request using the PHP SDK:
// Include Tinypass - make sure to use the correct path
include_once "/path/to/api/TinypassClient.php";
// Init configuration object. This can be omitted if configuration object was initialised earlier.
$tinypassConfig = new TPConfig( $applicationId, $apiToken, $privateKey, $isSandbox );
// Init API client
$client = new TinypassClient( $client );
// Make access check request
$access = $client->PublisherUserAccess()
->checkAccessRequest()
->aid( $applicationId )
->rid( $resourceId )
->uid( $userId )
->execute();
if ( $access->granted ) {
// Access is granted, content can be displayed
} else {
// Access is denied, content should be hidden, and offer should be displayed
}
Additionally, to see a complete list of accesses for a user, query the publisher/user/access/list endpoint.
curl https://api.piano.io/api/v3/publisher/user/access/list \
-d 'aid=AIDAIDAID' \
-d 'uid=my_user_uid' \
-d 'api_token=4uRW8r31E3giOYnGLxfKq53yOOyziodoEpi6oBWw'
{
"code": 0,
"ts": 1432144388,
"limit": 1,
"offset": 0,
"total": 1,
"count": 1,
"accesses": [
{
"access_id": "hgSS9tAdKmKx",
"granted": true,
"expire_date": 1434822785
"resource": {
"rid": "RID",
"aid": "AIDAIDAID",
"deleted": false,
"disabled": false,
"create_date": 1415130054,
"update_date": 1418720853,
"publish_date": 1415130054,
"name": "Test Resource"
}
}
]
}
Server-Side Cookie
Using the Piano SDK, the publisher can also check access via the Access Token List cookie, which gets set on the browser as the __tac cookie. Some integrations might forbid making a server side REST call. In these situations, using the Access Token List is the best solution.
The Access Token List is an encrypted cookie, set on the publisher's domain, that contains the user's most recent access items. This secure cookie can be parsed and then access can be checked by inspecting the contents of the lists.
The AccessTokenStore library provides a simple way to access the Access Token List. The SDK/libraries can be sent upon request.
Below is an example of accessing the Access Token List
// Include Tinypass - make sure to use the correct path
include_once "/path/to/api/TinypassClient.php";
// Init configuration object. This can be omitted if configuration object was initialised earlier.
$tinypassConfig = new TPConfig( $applicationId, $apiToken, $privateKey, $isSandbox );
// Init Acccess token store object with created configuration
$tokenStore = new TPAccessTokenStore( $tinypassConfig );
// Parse cookies to get tokens
$tokenStore->loadTokensFromCookie( $_COOKIE );
// Get access token for some resource
$accessToken = $tokenStore->getAccessToken( $resourceId );
if ( $accessToken->isAccessGranted() ) {
// Access is granted, content can be displayed
} else {
// Access is denied, content should be hidden, and offer should be displayed
}
Local Database
If your users are stored in a local database - either in Wordpress, Drupal, or your own identity management system - and you're not using a third-party database like Identity Management, or Gigya, you can perform an access check by querying your local database for entitlements.
This type of integration involves listening to all Piano webhooks so that user access details are up-to-date locally. Since your local datastore is always up-to-date, there is no need to query Piano via a REST call or check the encrypted cookie when the page loads.
The UID that you use internally should be the UID that you provide to us on checkout. As a result, all REST endpoints are available for you to query as long as the appropriate UID is provided.
JWT-Based Access Check
General description
Compared to the server-side REST approach with an API call sent from the client to Piano to check access, the JWT-based access check provides:
-
Faster page loading (fewer queries to Piano’s server).
-
Lower costs of external traffic (even more considerable with CDN Edge).
Quite similar to the server-side REST approach, this solution is based on the usage of a JWT and does not require API calls. With the JWT-based access check, Composer checks the validity of a user’s access and sets (or updates) a __tac cookie on the client’s domain after every new pageview. And when the same page is loaded the next time, this __tac cookie is automatically passed to your backend. There, a user’s access to a specific resource can be checked without calling the API.
The signature of the __tac cookie is generated using the Private Key, accessible from the Piano dashboard's home page to Team Members with admin permissions.
Something to keep in mind when using the JWT-based access check is a one-page delay while the __tac cookie is updated AFTER Composer’s execution. This means that a user - after they complete a purchase - will be redirected to a page other than the target content page. There, Composer will be executed and the __tac cookie updated. Otherwise, when trying to open the content page immediately after a successful purchase, a user will not see any content since the JWT will not contain the required data. The page will have to be refreshed.
JWT payload details
-
Issuer (iss claim) – contains a party that issued the token, it will always be https://www.piano.io.
-
Audience (aud claim) – contains the Piano application ID for which Composer’s execution happened.
-
Token expiration time (exp claim) – contains a UNIX timestamp indicating the expiration of a user’s last access; if a user has endless access, expiration will be set to 1 month.
-
Token creation time (iat claim) – contains a UNIX timestamp showing when the token was issued.
-
Subject (sub claim) – a stringified JSON object containing information about the user access. The scheme of a JSON object is the following:
-
u (uid) - String, represents the user ID.
-
al (access list) - a map of the Piano application IDs to the list* of access objects:
-
map key (aid) - String, represents the Piano application ID;
-
map value (access object)
-
r (rid) - String, the resource ID from the Piano dashboard.
-
e (access expiration time in seconds) - a UNIX timestamp indicating when the access will expire, if the value is -1 then it is an access without expiration (endless).
-
ia (is active) - Boolean, in most cases "true" for any resource in this cookie. However, it can be "false" in case of a pending upgrade where the new resource access only starts on the next billing date.
-
t (term_id) - String, represents the term ID.
*Note that Piano’s Cross Application Access allows providing access to one resource from multiple applications within the same merchant (organization). If the goal is to check access only against the current application which has been configured on the page, then the access list of this exact application can be retrieved with an AID claim from JWT.
-
Example JWT:
eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwczovL3d3dy5waWFuby5pbyIsImF1ZCI6IkFJREhlcmUiLCJzdWIiOiJ7XCJ1XCI6XCJUaGlzLWlzLXRoZS11c2VyLWlkXCIsXCJhbFwiOntcIkFJREhlcmVcIjpbe1wiclwiOlwiUmVzb3VyY2VJRFwiLFwiZVwiOjE2OTA0MzAzOTksXCJpYVwiOnRydWUsXCJ0XCI6XCJUZXJtSURcIn1dfX0iLCJleHAiOjE2OTA0MzAzOTksImlhdCI6MTY4ODM4NDIyNH0.kT1h5GzJDsh-Q5KIraPA5xkCEg4Uboqdnd3B4RtAELE
Example JWT payload value:
{
"iss": "https://www.piano.io",
"aud": "AIDHere",
"sub": "{\"u\":\"This-is-the-user-id\",\"al\":{\"AIDHere\":[{\"r\":\"ResourceID\",\"e\":1690430399,\"ia\":true,\"t\":\"TermID\"}]}}",
"exp": 1690430399,
"iat": 1688384224
}
Access check on CDN Edge
General description
To save even more external traffic, you can check access not on your backend (like CMS) but with the Edge Workers of your CDN.
An Edge Worker is a service that accommodates serverless calculation of business logic in distributed networks. The feature is available for most large CDNs.
Following the same idea, you may use your Edge Worker to check access with a JWT from a __tac cookie and then either return the content or redirect a user to a paywall page or stud.
Checking access on Edge stands out due to the remarkably quick response a user gets. Since Edge Workers are distributed among CDN locations, the code will be run on the server closest to the user, allowing the quickest page load possible.
Implementation
1. Receive a __tac cookie on the CDN Edge (e.g. CloudFlare Workers or CloudFront Edge Lambdas). After a Composer execution, the cookie will be automatically set on your domain by Piano JS SDK. If necessary, you can forcibly refresh this cookie using the following JS API:
tp.user.refreshAccessToken(true, function (accessTokenList) {
// Optionally, run additional logic here after the access token cookie is refreshed
});
2. If there is no cookie, a user is considered to have no access.
3. If the cookie is present, then you extract its value (JWT) and verify the signature. Most CDN workers either have a built-in API to work with JWT or allow to add an external dependency (e.g. an NPM module) to do so.
To verify the JWT, the Piano client will have to use their Private Key from Piano Dashboard.
Piano uses HMAC_SHA256 to encode JWT which means that the client would have to use this algorithm and Private Key as a secret to verify that the JWT is valid.
4. If the signature is invalid or the token is expired, a user is considered to have no access.
5. Extract payload from the sub JWT claim and transform its value to a JSON object.
6. Check whether the access list meets the requirements to decide whether a user has access to a given article. There’re a couple of strategies that could be used:
a. If the access list contains any active access, then consider that a user has access. This option would work perfectly if you have a single resource or a single bundle used to provide your audience with premium content.
b. Prepare a map of resource IDs vs URL masks matching different site sections or articles; and check whenever any non-expired resource ID from the JWT matches the current URL by mask (e.g. if the current URL is www.publisher.com/premium-news/* and there’s a respective active resource ID in the JWT, then a user has access). This option will work if you have various site sections which require a distinct subscription tier to access them and change rarely.
c. Retrieve and cache the requested content metadata on CDN Edge to make a dynamic decision whenever a given page is eligible for the access check logic. This option would require a significant implementation effort from your development team; Consult your Piano representative for further recommendations.
7. As a result of previous steps you may get the following outcomes:
a. The page is not eligible for access check (e.g. it’s free content) – no further actions required, a user will get a full version of the page from CDN cache.
b. The page is eligible for access check and a user has active access to it – no further actions required, a user will get a full version of the page from CDN cache.
c. The page is eligible for access check and a user has no active access to it – an action should be take to prevent a user from accessing the content, a couple of strategies available:
i. Temporarily redirect a user to a generic offer page to encourage them to subscribe.
ii. Temporary redirect a user to the same page but with an extra query parameter attached, e.g. if the request page is com/premium/hot-content then redirect them to publisher.com/premium/hot-content?locked=true. This extra parameter should be handled by your CMS and return a locked/truncated version of the same page (a common functionality provided by popular CMS). Both versions will be cached on CDN, hence no extra call to your CMS will be made if both page versions have already been cached.
iii. Instead of redirecting, manually append the ?locked=true query to the request URL using an Edge Worker. In this case, CDN will transparently check if the cache already contains the page, and will make a call to the origin otherwise. As for option ii), your CMS should be ready to process such a request. The difference is that, from the user perspective, there won’t be any redirection or indication in the URL bar saying that this is a locked version. This approach is not ideal from the HTTP RFC because the same requested URL will return different content and it could be considered an issue by some SEO; however, it can be used if explicit redirection is not an option.
Note: For examples of the code parsing JWT on the CDN edge, please refer to the documentation of your CDN.
-
Akamai: