Auth Token
Overview
The Auth Token integration enables single sign-on (SSO) between your platform and Publica.la stores. Users authenticated in your system (LMS, e-commerce, CMS, etc.) can access Publica.la content without logging in again.
How it works: Your platform generates a signed JWT containing user information, redirects the user to Publica.la with that token, and we validate it to grant access automatically. New users are created on first authentication; existing users are recognized by their uuid.
Not the right fit? Consider IP Authentication for IP-based access, LTI for LMS integrations, or URL Referrer for domain or URL-based access.
Setup
-
Generate a secret key — Create a secret of at least 32 bytes (256 bits) that you'll use to sign JWTs. Longer secrets are accepted.
-
Contact support with the following configuration:
Parameter Description keyThe secret you generated (at least 32 bytes / 256 bits) issuerYour platform identifier (e.g., platform-name). Must matchissclaim in tokens.redirect_urlWhere to send users on auth errors logout_urlWhere to send users after logging out -
Wait for confirmation before testing your integration.
Token format
Tokens must be signed with HS256 (HMAC-SHA256). Other algorithms are rejected.
Example payload
{
"iss": "platform-name",
"aud": "farfalla",
"sub": "user",
"jti": "550e8400-e29b-41d4-a716-446655440000",
"exp": 1699999960,
"user": {
"uuid": "user-123",
"email": "[email protected]"
},
"intended_url": "https://{store-domain}/reader/my-ebook"
}
Claims reference
Required JWT claims
| Claim | Type | Description |
|---|---|---|
iss | string | Your platform identifier. Must match configured issuer. |
aud | string | Must be "farfalla". |
sub | string | Must be "user". |
jti | string | Unique token ID. Use UUID v4 to prevent replay. |
exp | number | Expiration timestamp (Unix time). We recommend 60 seconds for real-time redirects. |
exp is validated with no tolerance: a token is rejected as soon as exp is in the past relative to Publica.la's server clock, with zero leeway. A token sent without exp is treated as expired. Keep your server clock synchronized (NTP), and account for network latency when choosing the expiration window.
User object (user claim)
| Field | Required | Type | Description |
|---|---|---|---|
uuid | Yes | string | Unique identifier for the user in your system. 1-200 characters. Stored as external_id in Publica.la. |
email | No | string | User's email address. Must be valid format if provided. Updated on each login if changed. |
accept_terms_and_policies | No | boolean | If true, automatically accepts terms on first login. |
Optional claims
| Claim | Type | Description |
|---|---|---|
intended_url | string | Where to redirect after successful login. Must be a valid URL. Defaults to /library. |
reader_exit_url | string | Custom URL for the reader's exit button. Stored in session for the reader to use. |
Endpoint
URL
https://{store-domain}/auth/token
Methods
Both GET and POST are supported.
Passing the token
The token can be provided in three ways. When more than one is present, Publica.la reads them in this order: header, then cookie, then query parameter.
- Query Parameter
- HTTP Header
- Cookie
https://{store-domain}/auth/token?external-auth-token={JWT}
Recommended for browser redirects. JWT tokens are URL-safe, no encoding needed.
Tokens in URLs appear in browser history, server logs, and may leak via Referrer headers. Use short expiration times (60 seconds) to mitigate risk.
GET /auth/token HTTP/1.1
Host: {store-domain}
external-auth-token: {JWT}
More secure as tokens don't appear in logs or browser history. Requires JavaScript or server-side implementation.
GET /auth/token HTTP/1.1
Host: {store-domain}
Cookie: external-auth-token={JWT}
Useful when an upstream proxy or your platform sets the cookie on the user's browser before the redirect.
Response behavior
| Scenario | HTTP Response | Destination |
|---|---|---|
| Valid token | 302 Redirect | intended_url from token, or /library if not specified |
| Invalid token | 302 Redirect | redirect_url with error query params |
| Invalid user data | 302 Redirect | redirect_url with error query params |
| Missing store configuration | 422 JSON | Error message returned |
User management
View detailed authentication flow
New users
When a user authenticates for the first time (no existing user with that uuid):
- A new user account is created with the provided
uuidasexternal_id emailis set if provided in the token- If
accept_terms_and_policies: true, terms are automatically accepted - User is logged in and redirected to their destination
Returning users
When a user with an existing uuid authenticates:
- User is found by their
external_id(youruuid) emailis updated if the token contains a different value- User is logged in and redirected
Email uniqueness
Each email address can only be associated with one uuid. If you send an email that already belongs to a different user, authentication will fail with an invalid-user error.
Code examples
- PHP (lcobucci/jwt v5)
- Node.js
- Python
use Lcobucci\JWT\Configuration;
use Lcobucci\JWT\Signer\Hmac\Sha256;
use Lcobucci\JWT\Signer\Key\InMemory;
use DateTimeImmutable;
$config = Configuration::forSymmetricSigner(
new Sha256(),
InMemory::plainText($_ENV['JWT_SECRET'])
);
$now = new DateTimeImmutable();
$token = $config->builder()
->issuedBy('platform-name') // iss - must match configured issuer
->permittedFor('farfalla') // aud - always "farfalla"
->relatedTo('user') // sub - always "user"
->identifiedBy(bin2hex(random_bytes(16))) // jti - unique token ID
->expiresAt($now->modify('+60 seconds')) // exp - short expiration for security
->withClaim('user', [
'uuid' => $userId,
'email' => $userEmail,
])
->withClaim('intended_url', 'https://{store-domain}/reader/my-ebook')
->getToken($config->signer(), $config->signingKey());
$jwt = $token->toString();
// Redirect user to Publica.la
header('Location: https://{store-domain}/auth/token?external-auth-token=' . $jwt);
exit;
const jwt = require('jsonwebtoken');
const crypto = require('crypto');
const token = jwt.sign(
{
iss: 'platform-name', // must match configured issuer
aud: 'farfalla', // always "farfalla"
sub: 'user', // always "user"
jti: crypto.randomUUID(), // unique token ID
user: {
uuid: userId,
email: userEmail,
},
intended_url: 'https://{store-domain}/reader/my-ebook',
},
process.env.JWT_SECRET,
{
algorithm: 'HS256',
expiresIn: 60, // 60 seconds
}
);
// Redirect user to Publica.la
res.redirect(`https://{store-domain}/auth/token?external-auth-token=${token}`);
import jwt
import os
from uuid import uuid4
from time import time
token = jwt.encode(
{
'iss': 'platform-name', # must match configured issuer
'aud': 'farfalla', # always "farfalla"
'sub': 'user', # always "user"
'jti': str(uuid4()), # unique token ID
'exp': int(time()) + 60, # 60 seconds from now
'user': {
'uuid': user_id,
'email': user_email,
},
'intended_url': 'https://{store-domain}/reader/my-ebook',
},
os.environ['JWT_SECRET'],
algorithm='HS256'
)
# Redirect user to Publica.la
return redirect(f'https://{store-domain}/auth/token?external-auth-token={token}')
Error handling
When authentication fails, users are redirected to your configured redirect_url with error information in query parameters:
https://your-redirect-url.com?external-auth-token-error={code}&external-auth-token-error-details={base64-json}
Error codes
| Code | Description |
|---|---|
invalid-token | Token validation failed (expired, bad signature, wrong issuer/audience/subject) |
invalid-user | User data validation failed (invalid email format, missing uuid, uuid too long) |
Decoding error details
The external-auth-token-error-details parameter contains Base64-encoded JSON with specific error messages:
- JavaScript
- PHP
- Python
const details = JSON.parse(atob(errorDetails));
console.log(details);
$details = json_decode(base64_decode($errorDetails), true);
print_r($details);
import base64
import json
details = json.loads(base64.b64decode(error_details))
print(details)
Example error responses
Token validation error:
{
"token": {
"exp": "Token is expired or `exp` attribute not present."
}
}
User validation error:
{
"email": ["The email must be a valid email address."]
}
Email conflict error:
{
"uuid": ["This email is already attached to UUID abc-123."]
}
Troubleshooting
| Problem | Cause | Solution |
|---|---|---|
| "Token is expired" | exp timestamp is in the past | Ensure server clock is synchronized (NTP). Use 60-second expiration. |
| "Invalid signature" | Signing key doesn't match | Verify key matches exactly what was configured in setup. |
| "Invalid issuer" | iss claim mismatch | Ensure iss matches the configured issuer exactly (case-sensitive). |
| "Invalid audience" | aud claim wrong | Must be exactly "farfalla". |
| "Email already attached to UUID" | Email registered to different user | Use the correct uuid for that email, or use a different email. |
| 422 error | Store not configured | Contact support to complete Auth Token setup. |
Debugging tips
- Inspect your token — Paste it at jwt.io to see decoded header and payload
- Check expiration — Ensure
expis in the future when the request reaches Publica.la - Verify fixed values —
audmust be"farfalla",submust be"user" - Decode errors — Use base64decode.org to read error details
JWT Generator
Generate test tokens to verify your integration:
Security best practices
- Use short expiration times — 60 seconds is recommended for tokens passed in URLs
- Store secrets securely — Use environment variables, never commit keys to version control
- Use HTTPS only — Never transmit tokens over unencrypted connections
- Prefer headers when possible — HTTP headers don't appear in browser history or server logs
- Generate unique
jtivalues — Use UUID v4 for each token to enable replay detection - Rotate keys periodically — Contact support to update your signing key
Tips for integrators
- Anonymous users — Omit
emailto create anonymous user accounts - Direct content access — Use
intended_urlto send users directly to a specific book or page - Custom reader exit — Use
reader_exit_urlto control where users go when closing the reader - No URL encoding needed — JWT tokens are URL-safe by design
- Email updates — Users are matched by
uuid, so if an email changes in your system, just send the new email and it will be updated automatically