---
title: "Authentication"
description: "How the CloudPDF server authenticates requests with document-scoped and admin JWTs, and how to manage the signing secret."
source: "https://www.cloudpdf.com/docs/server/configuration/authentication"
---

# Authentication

The server trusts no request by default. Every document and admin route requires
a valid **JWT**, signed with a secret shared between the server and the backend
that mints tokens. Only the health probes (`/healthz`, `/readyz`) are public.

## The signing secret

One secret signs and verifies every token:

```sh
CLOUDPDF_JWT_SECRET=a-long-random-string
```

Under a production license this is required (at least 32 bytes; the server
refuses publicly-known dev fallbacks). The server uses HS256, so the **same**
secret must be configured on the server and used by your backend to sign
tokens. Development license keys allow a built-in dev fallback — with a loud
warning — so local try-outs boot with zero configuration.

```sh
# Generate a strong secret
openssl rand -hex 32
```

> Treat the secret like a password. Keep it out of source control, inject it
> through your secret manager (or Kubernetes Secret), and rotate it if it leaks.
> Anyone with the secret can mint tokens for any document.

## Two kinds of token

| Token               | Who holds it                                   | What it allows                                                                                        |
| ------------------- | ---------------------------------------------- | ----------------------------------------------------------------------------------------------------- |
| **Document-scoped** | The end user's browser, via `@cloudpdf/engine` | Acting on **one** document — render, read text, manage annotations — limited to the scope you encode. |
| **Admin**           | Your trusted backend                           | Managing documents — create, upload, and mint or revoke tokens — via `/v1/admin/*`.                   |

The design principle: the browser only ever holds a **narrow, short-lived,
single-document** token. Your tenant secret and storage credentials never leave
the server side.

## The token flow

```
User opens a doc
      │
      ▼
Your backend  ──(verifies the user, then signs a doc-scoped JWT)──▶  JWT
      │
      ▼
Frontend (@cloudpdf/engine)  ──Authorization: Bearer <JWT>──▶  CloudPDF server
      │                                                              │
      └──────────────── rendered pages, text, annotations ◀─────────┘
```

1. Your backend authenticates the user with your own auth system.
2. It mints a short-lived JWT scoped to the document the user may access, signed
   with `CLOUDPDF_JWT_SECRET`.
3. The frontend passes that token to the engine, which sends it as a Bearer
   token on every request.
4. The server verifies the signature and scope, then serves the request.

## API tokens (server-to-server)

For trusted backends, CI, and operational tooling there is a second
credential: **static API tokens**, configured as a comma-separated list:

```sh
CLOUDPDF_API_AUTH_TOKENS="$(openssl rand -hex 32)"
```

An API token is a root credential for the admin plane
(`/v1/tenants/...` — uploads, listing, minting and revoking tokens, deployment
status). It is what the quick start's `curl` calls use, and what you'd give a
backfill script. Treat it like a database password: server-side only, never in
a browser, at least 32 bytes under a production license. Document-plane routes
still require document-scoped JWTs — an API token deliberately does not
replace the per-document token flow.

## Encrypted-PDF secrets

Two further server-side secrets exist, and they are easy to miss because they
have nothing to do with authenticating your API: they protect what the server
remembers **after a user unlocks a password-protected PDF**.

| Variable                                     | What it protects                | Required when                                                                                                            |
| -------------------------------------------- | ------------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
| `CLOUDPDF_PASSWORD_VERIFICATION_HMAC_SECRET` | The password-verification cache | Always, under a non-development license                                                                                  |
| `CLOUDPDF_PASSWORD_SESSION_SERVER_SECRET`    | Encrypted-PDF unlock sessions   | Under a non-development license, when a [KMS](https://www.cloudpdf.com/docs/server/configuration/adapters) is configured |

### The verification secret

Checking a PDF password is expensive, and the answer — which permissions that
password unlocks — is worth caching. The server never stores the password
itself: it stores an **HMAC proof** of it, keyed by this secret. The secret is
the pepper that keeps those proofs non-reversible. With a weak or
publicly-known value, a leaked database becomes an offline dictionary attack
against your customers' document passwords, which is why the server refuses to
boot without a strong one under a production license — even if you never open
an encrypted PDF.

Rotating it is safe: existing proofs stop matching and are simply re-verified
on next use. It is a cache, not a source of truth.

### The session secret

When a viewer unlocks an encrypted document, the server can keep that unlock
alive for the session instead of prompting on every request. This secret signs
the session renewal grants and feeds the key derivation that protects the
stored unlock key — together with your KMS, which is why it is only used, and
only enforced, when one is configured.

`CLOUDPDF_PASSWORD_SESSION_SERVER_SECRET_ID` (default `dev-v1`) labels the
active secret and is recorded on every session. Changing either the secret or
its id invalidates existing sessions: viewers re-enter their password once.

Generate both the same way as the JWT secret:

```sh
openssl rand -hex 32
```

> A **development** license lets all three secrets fall back to
> publicly-known dev values, so local runs need no configuration. Every other
> license kind requires real ones of at least 32 bytes and the server
> **fails closed at boot** — the error names the exact variable to
> set. See [Licensing](https://www.cloudpdf.com/docs/server/configuration/licensing).

## Minting a token

Use any JWT library. The token is signed with `CLOUDPDF_JWT_SECRET` and carries
the document scope your server expects. (For local experiments, the npm
package exports a `signDevToken` helper that produces a correctly-shaped
token in one call.)

```ts
import { SignJWT } from 'jose';

const secret = new TextEncoder().encode(process.env.CLOUDPDF_JWT_SECRET);

export async function mintDocToken(documentId: string) {
  return new SignJWT({ documentId /* + the scope your policy allows */ })
    .setProtectedHeader({ alg: 'HS256' })
    .setIssuedAt()
    .setExpirationTime('5m') // keep it short
    .sign(secret);
}
```

Hand short-lived tokens to the engine through a **factory**, so they refresh
transparently as your backend re-issues them:

```ts
import { createCloudEngine } from '@cloudpdf/engine';

const engine = createCloudEngine({
  baseUrl: 'https://pdf.your-app.com',
  token: () => fetchFreshDocToken(), // called on every request
});
```

> Prefer **short** expirations and a token **factory**
> over long-lived tokens. The browser never needs a token that outlives the
> current view. See the engine's
> [Authentication guide](https://www.cloudpdf.com/docs/engine/getting-started/authentication)
> for opening by id vs. by token and the public-share flow.

## Revoking tokens

For cases where you must cut off access before a token naturally expires, the
admin API can revoke tokens. Combined with short expirations, this gives you
both fast issuance and a hard kill switch. See the
[CLI](https://www.cloudpdf.com/docs/server/operations/cli) and admin routes for details.

## Next steps

- [The engine client](https://www.cloudpdf.com/docs/engine/getting-started/authentication) — Drive the server with doc-scoped tokens from the frontend.
- [Configuration reference](https://www.cloudpdf.com/docs/server/configuration) — Every setting, including the auth secret.
