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.
Authorization: Bearer <token>.Connect -> Protected Content.{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.Access to Hard, and set Withhold the text on the server to Yes.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>
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:
401 if the signature or expiry check fails.403 if the token's cid claim is not the requested article id.Cache-Control: private, no-store so shared caches never keep it.The token's claims:
| Claim | Meaning |
|---|---|
sub | The reader's UniSignIn user id |
cid | The article id from your page's marker |
aud | Your project |
iat, exp | When the token was issued and when it expires |
jti | A 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.
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);
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))
})
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.
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.
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.
Identity, consent, audience, and subscription insights for publishers. A few emails a month, no spam.