Unlocking content

When a user has purchased a post the unlocked content can be displayed using the sesamy-content-container component. The content can either be embedded directly on the page or fetched from the server using a signed url.

The signed Urls can be shared in an order confirmation email and render a page without any further authentication. To avoid the signed links being distributed they have a limited expiry.

The signed Urls require that the CMS hosting the content can validate the signature and serve the locked content for valid requests.

Signed link validation

The links consist of four parts:

  • Url of the content
  • Optional information, such as the Publisher's Content ID
  • Expiration timestamp
  • Signature of the previous parts of the url

This is a sample of a signed link: https://test.example.com/test-article?se=TIMESTAMP&si=PUBLISHER_CONTENT_ID&ss=RSA_256_HASH

The https://test.example.com/test-article is the url of the purchased article, se=1656098737770 is the expiration timestamp of the url and ss=... is the signature.

If a publisher provided a Publisher Content ID in the metatags this ID will also be passed as an sp querystring parameter in the signed link: https://test.example.com/test-article?sp=PUBLISHER_CONTENT_ID&se=TIMESTAMP&si=PUBLISHER_PRODUCT_ID&ss=RSA_256_HASH

Validation of the signature

The urls are signed with an RSA256 asymetric key that can be verified with the public Sesamy key. The public key is published in a jwks-format here: https://assets.sesamy.com/vault-jwks.json

This is an example of how to verify the signed url in node-js with typescript:

import { verify, createPublicKey, KeyObject } from 'crypto';
import jwkToPem from 'jwk-to-pem';

async function fetchPublicKey() {
const response = await fetch('https://assets.sesamy.com/vault-jwks.json');

    if (!response.ok) {
        throw new Error('Failed to fetch public key');
    }

    return response.json();

}

async function getPublicKey() {
const jwks = await fetchPublicKey();

    const pemKey = jwkToPem(jwks);
    return createPublicKey(pemKey);

}

async function verifySignature(signedUrl: string) {
const publicKey = await getPublicKey();

    const [url, signature] = signedUrl.split('&ss=');
    if (!verify('RSA-SHA256', Buffer.from(url), publicKey, Buffer.from(signature, 'base64'))) {
        throw new Error('Signature not valid');
    }

}

This is an example of how to verify the signed url in php:

<?php

require __DIR__ . '/vendor/autoload.php';

use Jose\Component\Core\JWK;
use Jose\Component\Core\AlgorithmManager;
use Jose\Component\Signature\Algorithm\RS256;

function verify_signature($url)
{
    $url_to_sign = explode('&ss=', $url)[0];
    $signature = explode('&ss=', $url)[1];

    $parsed_url = parse_url($url);
    parse_str($parsed_url['query'], $parsed_query);

    $expire = $parsed_query['se'];
    if ($expire  < time()) {
        return new Exception('Link is expired');
    }

    $algorithm_manager = new AlgorithmManager([
        new RS256()
    ]);

    $rs256 = $algorithm_manager->get('RS256');
    $json = '{
        "kty": "RSA",
        "n": "sFATY4fG4n822Zn8bQpszyF9navI_O5lwEg12fEHJGq69EKEfX1xFBXYNj8xEg6ROe4Zl-ssG1Co3Mb3M8zSE9shGSNmMB86oqPOZ9RZTYmiGg_Uh6FqGuP_-SzUC6k8gGVzoo1gn06dqv_S06cT7GW616T57DVHS280FPZ1JLmu88VaBhY_8kgCAqEWgdveLYYWzJhuiTcocCUVRbIElKwWzLbze4BpUQtLQmW5QL-zwYOYXlbamnN-2VP7ZshTqRZEG-LCwI9DEWVUZsdSBdDtG0xH8aTf1BxCAcdcFdPJM2lNa9DmQnNlcB420jL3vKu2mFxxE1Zn_5PIu19pmQ",
        "e": "AQAB"
        }';
    $jwk = JWK::createFromJson($json);
    return $rs256->verify($jwk, $url_to_sign, base64_decode($signature));
}

Signed link with pass

If an article is part of a pass (for instance a subscription) the signed link will be for the pass rather than the article. This way a signed link can be reused for all articles within a subscription until the link has expired. In this case the backend needs to validate that the article is part of the pass before serving the content.

The signed link is passed to the backend as a pass query string (the signature is shortened for brevity): https://test.example.com/test-article?pass=https%3A%2F%2Ftest.example.com%2Fsubscription%3Fse%3D1656098737770%26si%3D113%26ss%3DPTKUZ...

Serving locked content via API

By setting the lock mode of the content-container web component to signedUrl the content is fetched from the server. By default it fetches the locked content from the article url using the signed url, but by specifying the access-url property on the content-container it will use this url to fetch the locked content and pass the signed url in the x-sesamy-signed-url header instead:

<sesamy-content-container
    lock-mode="signedUrl"
    access-url="https://example.com/api/access/test-article"
>

Cross-Origin Resource Sharing (CORS)

Cross-Origin Resource Sharing (CORS) is a browser mechanism which enables controlled access to resources located outside of the browser domain. When serving content from an API endpoint on a separate domain the API needs to handle preflight requests.

Did this answer your question?
😞
😐
🤩