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 JWT token.
The signed link has two query string parameters, the token containing a jwt and a se parameters that contains the expiry of the link.
Signed Link JWT Token Authentication
In the latest version, we use a JWT (JSON Web Token) for authentication instead of signed URLs. This token is signed with the same keys used in the previous version.
JWT Payload Structure
The JWT payload contains the following information:
json
Copy
{
"url": "https://example.com/article1",
"n": "Firstname Lastname",
"i": "google-oauth2|108791004651072817794",
"e": "customer@example.com.com",
"s": "url:d842745d1a55afc198580015ef713df0",
"p": "ingest_own_article_publisher_default_SEK3900",
"pid": "99258",
"iat": 1726583215,
"exp": 1726597615
}
url
: The URL of the purchased article
n
: The full name of the user
e
: The email of the user
s
: The SKU of the article
p
: The purchase option ID
pid
: The publisher content ID
iat
: The timestamp when the token was created
exp
: The expiration timestamp of the token
Validation of the JWT
The JWTs are signed with an RSA256 asymmetric 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
Example: Verifying the JWT in Node.js with TypeScript
import { verify, createPublicKey } from 'crypto';
import jwkToPem from 'jwk-to-pem';
import jwt from 'jsonwebtoken';
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 verifyJWT(token: string) {
const publicKey = await getPublicKey();
try {
const decoded = jwt.verify(token, publicKey, { algorithms: ['RS256'] });
return decoded;
} catch (error) {
throw new Error('Invalid JWT');
}
}
Example: Verifying the JWT in PHP
<?php
require __DIR__ . '/vendor/autoload.php';
use Jose\Component\Core\JWK;
use Jose\Component\Core\AlgorithmManager;
use Jose\Component\Signature\Algorithm\RS256;
use Jose\Component\Signature\JWSVerifier;
use Jose\Component\Signature\Serializer\CompactSerializer;
function verify_jwt($jwt)
{
$algorithm_manager = new AlgorithmManager([new RS256()]);
$jwk = JWK::createFromJson('{
"kty": "RSA",
"n": "sFATY4fG4n822Zn8bQpszyF9navI_O5lwEg12fEHJGq69EKEfX1xFBXYNj8xEg6ROe4Zl-ssG1Co3Mb3M8zSE9shGSNmMB86oqPOZ9RZTYmiGg_Uh6FqGuP_-SzUC6k8gGVzoo1gn06dqv_S06cT7GW616T57DVHS280FPZ1JLmu88VaBhY_8kgCAqEWgdveLYYWzJhuiTcocCUVRbIElKwWzLbze4BpUQtLQmW5QL-zwYOYXlbamnN-2VP7ZshTqRZEG-LCwI9DEWVUZsdSBdDtG0xH8aTf1BxCAcdcFdPJM2lNa9DmQnNlcB420jL3vKu2mFxxE1Zn_5PIu19pmQ",
"e": "AQAB"
}');
$jwsVerifier = new JWSVerifier($algorithm_manager);
$serializer = new CompactSerializer();
$jws = $serializer->unserialize($jwt);
$isVerified = $jwsVerifier->verifyWithKey($jws, $jwk, 0);
if (!$isVerified) {
throw new Exception('Invalid JWT');
}
$payload = json_decode($jws->getPayload(), true);
if ($payload['exp'] < time()) {
throw new Exception('JWT has expired');
}
return $payload;
}
Example: Verifying the JWT in .NET (C#)
using System;
using System.IdentityModel.Tokens.Jwt;
using System.Net.Http;
using System.Security.Cryptography;
using Microsoft.IdentityModel.Tokens;
using Newtonsoft.Json.Linq;
public class JwtValidator
{
private static readonly HttpClient client = new HttpClient();
private static readonly string jwksUrl = "https://assets.sesamy.com/vault-jwks.json";
public static async Task<JwtSecurityToken> VerifyJwtAsync(string token)
{
var jwks = await GetJwksAsync();
var key = GetRsaSecurityKey(jwks);
var tokenHandler = new JwtSecurityTokenHandler();
var validationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = key,
ValidateIssuer = false,
ValidateAudience = false,
ClockSkew = TimeSpan.Zero
};
try
{
tokenHandler.ValidateToken(token, validationParameters, out SecurityToken validatedToken);
return (JwtSecurityToken)validatedToken;
}
catch (Exception)
{
throw new Exception("Invalid JWT");
}
}
private static async Task<JObject> GetJwksAsync()
{
var response = await client.GetStringAsync(jwksUrl);
return JObject.Parse(response);
}
private static RsaSecurityKey GetRsaSecurityKey(JObject jwks)
{
var n = Base64UrlEncoder.DecodeBytes(jwks["n"].ToString());
var e = Base64UrlEncoder.DecodeBytes(jwks["e"].ToString());
var rsaParameters = new RSAParameters
{
Modulus = n,
Exponent = e
};
return new RsaSecurityKey(rsaParameters);
}
}
Example: Verifying the JWT in Java
import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.auth0.jwt.interfaces.JWTVerifier;
import org.json.JSONObject;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.security.KeyFactory;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
public class JwtValidator {
private static final String JWKS_URL = "https://assets.sesamy.com/vault-jwks.json";
public static DecodedJWT verifyJwt(String token) throws Exception {
JSONObject jwks = getJwks();
RSAPublicKey publicKey = getRsaPublicKey(jwks);
Algorithm algorithm = Algorithm.RSA256(publicKey, null);
JWTVerifier verifier = JWT.require(algorithm).build();
return verifier.verify(token);
}
private static JSONObject getJwks() throws Exception {
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(JWKS_URL))
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
return new JSONObject(response.body());
}
private static RSAPublicKey getRsaPublicKey(JSONObject jwks) throws Exception {
String modulusBase64 = jwks.getString("n");
String exponentBase64 = jwks.getString("e");
byte[] modulusBytes = Base64.getUrlDecoder().decode(modulusBase64);
byte[] exponentBytes = Base64.getUrlDecoder().decode(exponentBase64);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(modulusBytes);
return (RSAPublicKey) keyFactory.generatePublic(keySpec);
}
}
Example: Verifying the JWT in Go
package main
import (
"crypto/rsa"
"encoding/base64"
"encoding/json"
"fmt"
"math/big"
"net/http"
"github.com/golang-jwt/jwt"
)
const jwksURL = "https://assets.sesamy.com/vault-jwks.json"
type JWKS struct {
N string `json:"n"`
E string `json:"e"`
}
func verifyJWT(tokenString string) (*jwt.Token, error) {
jwks, err := getJWKS()
if err != nil {
return nil, err
}
key, err := getRSAPublicKey(jwks)
if err != nil {
return nil, err
}
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}
return key, nil
})
if err != nil {
return nil, err
}
if !token.Valid {
return nil, fmt.Errorf("invalid token")
}
return token, nil
}
func getJWKS() (*JWKS, error) {
resp, err := http.Get(jwksURL)
if err != nil {
return nil, err
}
defer resp.Body.Close()
var jwks JWKS
err = json.NewDecoder(resp.Body).Decode(&jwks)
if err != nil {
return nil, err
}
return &jwks, nil
}
func getRSAPublicKey(jwks *JWKS) (*rsa.PublicKey, error) {
nBytes, err := base64.RawURLEncoding.DecodeString(jwks.N)
if err != nil {
return nil, err
}
eBytes, err := base64.RawURLEncoding.DecodeString(jwks.E)
if err != nil {
return nil, err
}
n := new(big.Int).SetBytes(nBytes)
e := new(big.Int).SetBytes(eBytes)
return &rsa.PublicKey{
N: n,
E: int(e.Int64()),
}, nil
}
Serving Locked Content via API
To fetch locked content from the server, set the lock-mode
attribute of the sesamy-content-container
web component to "jwt"
. By default, it fetches the locked content from the article URL using the JWT. You can specify a custom access-url
property on the content-container to use a different URL for fetching the locked content:
<sesamy-content-container lock-mode="jwt" access-url="https://example.com/api/access/test-article" >
The JWT will be passed in the Authorization
header as a Bearer token:
Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...
Cross-Origin Resource Sharing (CORS)
When serving content from an API endpoint on a separate domain, the API needs to handle preflight requests to enable Cross-Origin Resource Sharing (CORS). Ensure that your API server is configured to respond to OPTIONS requests and includes the necessary CORS headers in its responses.
- 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&token=ey...
The https://test.example.com/test-article
is the url of the purchased article, se=1656098737770
is the expiration timestamp of the url and token=...
is the token.
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&token=ey...
Validation of the signature
The urls are signed with an RSA256
asymmetric 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.