> ## Documentation Index
> Fetch the complete documentation index at: https://docs.publica.la/llms.txt
> Use this file to discover all available pages before exploring further.

# 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`.

```mermaid
sequenceDiagram
    participant User
    participant Platform as Your Platform
    participant PLA as Publica.la

    User->>Platform: Clicks "Go to library"
    Platform->>Platform: Generates signed JWT
    Platform->>User: Redirects with token
    User->>PLA: GET /auth/token?external-auth-token={JWT}
    PLA->>PLA: Validates token + upserts user
    PLA->>User: Redirects to library or intended_url
```

**Not the right fit?** Consider [IP Authentication](./auth-ip-integrations-tenants.md) for IP-based access, [LTI](./auth-lti-integrations.md) for LMS integrations, or [URL Referrer](./auth-referrer-integrations-tenants.md) for domain or URL-based access.

---

## Setup

1. **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.

2. **Contact support** with the following configuration:

   | Parameter      | Description                                                                         |
   | -------------- | ----------------------------------------------------------------------------------- |
   | `key`          | The secret you generated (at least 32 bytes / 256 bits)                             |
   | `issuer`       | Your platform identifier (e.g., `platform-name`). Must match `iss` claim in tokens. |
   | `redirect_url` | Where to send users on auth errors                                                  |
   | `logout_url`   | Where to send users after logging out                                               |

3. **Wait for confirmation** before testing your integration.

---

## Token format

Tokens must be signed with **HS256** (HMAC-SHA256). Other algorithms are rejected.

### Example payload

```json
{
  "iss": "platform-name",
  "aud": "farfalla",
  "sub": "user",
  "jti": "550e8400-e29b-41d4-a716-446655440000",
  "exp": 1699999960,
  "user": {
    "uuid": "user-123",
    "email": "user@example.com"
  },
  "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. |

:::caution Clock skew
`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:**

```
https://{store-domain}/auth/token?external-auth-token={JWT}
```

Recommended for browser redirects. JWT tokens are URL-safe, no encoding needed.

:::caution Security consideration
Tokens in URLs appear in browser history, server logs, and may leak via Referrer headers. Use short expiration times (60 seconds) to mitigate risk.
:::

---

**HTTP Header:**

```http
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.

---

**Cookie:**

```http
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

```mermaid
flowchart TD
Start([User arrives with token]) --> Validate{Valid token?}
Validate -->|No| ErrorRedirect[Redirect to redirect_url with error]
Validate -->|Yes| UserCheck{User exists?}
UserCheck -->|No| Create[Create user]
UserCheck -->|Yes| Update[Update email if changed]
Create --> Login[Log user in]
Update --> Login
Login --> HasIntended{intended_url present?}
HasIntended -->|Yes| IntendedRedirect[Redirect to intended_url]
HasIntended -->|No| LibraryRedirect[Redirect to library]
```

### New users

When a user authenticates for the first time (no existing user with that `uuid`):

1. A new user account is created with the provided `uuid` as `external_id`
2. `email` is set if provided in the token
3. If `accept_terms_and_policies: true`, terms are automatically accepted
4. User is logged in and redirected to their destination

### Returning users

When a user with an existing `uuid` authenticates:

1. User is found by their `external_id` (your `uuid`)
2. `email` is **updated** if the token contains a different value
3. 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):**

```php
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;
```

---

**Node.js:**

```javascript
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}`);
```

---

**Python:**

```python
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:**

```javascript
const details = JSON.parse(atob(errorDetails));
console.log(details);
```

---

**PHP:**

```php
$details = json_decode(base64_decode($errorDetails), true);
print_r($details);
```

---

**Python:**

```python
import base64
import json

details = json.loads(base64.b64decode(error_details))
print(details)
```

### Example error responses

**Token validation error:**

```json
{
  "token": {
    "exp": "Token is expired or `exp` attribute not present."
  }
}
```

**User validation error:**

```json
{
  "email": ["The email must be a valid email address."]
}
```

**Email conflict error:**

```json
{
  "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

1. **Inspect your token** — Paste it at [jwt.io](https://jwt.io) to see decoded header and payload
2. **Check expiration** — Ensure `exp` is in the future when the request reaches Publica.la
3. **Verify fixed values** — `aud` must be `"farfalla"`, `sub` must be `"user"`
4. **Decode errors** — Use [base64decode.org](https://www.base64decode.org/) to read error details

---

## JWT Generator

Generate test tokens to verify your integration:



---

## Security best practices

1. **Use short expiration times** — 60 seconds is recommended for tokens passed in URLs
2. **Store secrets securely** — Use environment variables, never commit keys to version control
3. **Use HTTPS only** — Never transmit tokens over unencrypted connections
4. **Prefer headers when possible** — HTTP headers don't appear in browser history or server logs
5. **Generate unique `jti` values** — Use UUID v4 for each token to enable replay detection
6. **Rotate keys periodically** — Contact support to update your signing key

---

## Tips for integrators

- **Anonymous users** — Omit `email` to create anonymous user accounts
- **Direct content access** — Use `intended_url` to send users directly to a specific book or page
- **Custom reader exit** — Use `reader_exit_url` to 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
