Protected Content

Protected Content keeps the full text of an article on your server until a reader has access. Your pages show only the preview. When a reader signs up or subscribes, their browser fetches the rest of the article from your server with a UniSignIn access token, and your server checks the token before returning the text. UniSignIn never stores or sees your articles.

Use it with a hard Access Wall in sign-up or subscription mode. Metered and dismissible walls cut the article in the browser instead.

How it works

  1. A reader opens an article. Your page contains the preview and a marker where the rest of the article goes.
  2. The Access Wall shows in place of the rest. Readers who already have access skip this step.
  3. Once the reader has access, the UniSignIn tag requests an access token for that article.
  4. The tag calls your protected address with the token: Authorization: Bearer <token>.
  5. Your server checks the token and returns the rest of the article as HTML. The tag shows it and removes the wall.

Set up in UniSignIn

  1. Go to Connect -> Protected Content.
  2. For each project, enter the address that returns an article's full text, with {id} where the article id goes, for example https://www.example.com/unis-content/{id}. It must use https and be on the project's domain.
  3. Copy the secret. Your server uses it to check tokens. Keep it on your server only.
  4. Open your Access Wall, set Access to Hard, and set Withhold the text on the server to Yes.

Change your article pages

Render only the preview paragraphs, then a marker with the article id:

<div class="article-body">
  <p>The first paragraph…</p>
  <p>The second paragraph…</p>
  <div data-unis-content="ARTICLE_ID"></div>
</div>

The id is passed to your protected address in place of {id}, so use an id your server can look up.

So that search engines understand the article is paywalled, add Google's paywalled content markup to the page:

<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "NewsArticle",
  "isAccessibleForFree": false,
  "hasPart": {
    "@type": "WebPageElement",
    "isAccessibleForFree": false,
    "cssSelector": ".article-body"
  }
}
</script>

Build the protected address

Your protected address receives GET /unis-content/ARTICLE_ID with an Authorization: Bearer <token> header. The token is a JSON Web Token signed with HS256 using your project's secret. Check it with any JWT library, then:

  • Refuse the request with 401 if the signature or expiry check fails.
  • Refuse it with 403 if the token's cid claim is not the requested article id.
  • Otherwise return the rest of the article as an HTML fragment, with Cache-Control: private, no-store so shared caches never keep it.

The token's claims:

ClaimMeaning
subThe reader's UniSignIn user id
cidThe article id from your page's marker
audYour project
iat, expWhen the token was issued and when it expires
jtiA unique id for the token

Tokens expire after a few hours, and the tag keeps a reader's token for their visit, so reloading the article doesn't ask UniSignIn again.

PHP

use Firebase\JWT\JWT;
use Firebase\JWT\Key;

$token = preg_replace('/^Bearer\s+/', '', $_SERVER['HTTP_AUTHORIZATION'] ?? '');
try {
    $claims = JWT::decode($token, new Key(getenv('UNISIGNIN_CONTENT_SECRET'), 'HS256'));
} catch (Exception $e) {
    http_response_code(401);
    exit;
}
if ($claims->cid !== $articleId) {
    http_response_code(403);
    exit;
}
header('Cache-Control: private, no-store');
echo renderArticleRest($articleId);

Node.js

const jwt = require('jsonwebtoken')

app.get('/unis-content/:id', (req, res) => {
  const token = (req.get('Authorization') || '').replace(/^Bearer\s+/, '')
  let claims
  try {
    claims = jwt.verify(token, process.env.UNISIGNIN_CONTENT_SECRET, { algorithms: ['HS256'] })
  } catch (e) {
    return res.sendStatus(401)
  }
  if (claims.cid !== req.params.id) return res.sendStatus(403)
  res.set('Cache-Control', 'private, no-store').send(renderArticleRest(req.params.id))
})

Without a JWT library

If your stack can't check a JWT, ask UniSignIn instead: send the token to GET https://<your UniSignIn account domain>/api/v1/content/verify?token=<token>. The answer is {"valid": true, "claims": {...}} for a good token, or {"valid": false}. Still compare claims.cid with the requested article id.

Rotating the secret

Select Rotate next to the secret to make a new one. Tokens signed with the previous secret keep working for a short time, so update your server's secret promptly. If your server refuses a reader's kept token after a rotation, the tag fetches a new one automatically.

If your address is on another domain

The protected address normally sits on your site's own domain, so the browser needs nothing extra. If it is on a different host from your pages, allow your pages' origin with CORS, including the Authorization header.

Stay ahead in first-party data

Identity, consent, audience, and subscription insights for publishers. A few emails a month, no spam.

By subscribing you agree to our privacy policy.