Token-Based Preview Authentication for Headless CMS

Token-based preview authentication lets editors inspect unpublished content without exposing draft endpoints or weakening production access controls. It binds a cryptographically signed, short-lived credential to each preview request. Unlike session cookies or IP allowlists, stateless tokens scale across edge networks and serverless runtimes, so you keep a zero-trust boundary while editors validate layout, typography, and interactive components in real time. It’s the backbone of the broader Preview & Draft Workflow Patterns section, and this topic covers the whole credential lifecycle: minting, validation, handoff, scoping, sharing, rotation and revocation.

Integration Contract

Preview authentication involves three parties, and each owns one secret. The CMS knows who the editor is and decides whether they may preview an entry. The frontend verifies that a preview request really came from the CMS and turns it into a short browser session. The CMS draft API trusts only the frontend’s server, which holds the preview API token. Keeping these three secrets separate is what makes the design safe: a leaked preview link exposes one entry for a few minutes, not the whole draft API.

The contract between them is a signed token with a small, fixed set of claims: which entry or slug may be previewed, in which locale, by whom, until when, and for which audience. The CMS, or a small minting function the CMS calls, signs it; the frontend’s validation route verifies it and never forwards it anywhere else.

Bash
# .env: three secrets, three owners
PREVIEW_JWT_SECRET=32_random_bytes_base64       # shared by the minting function and the validation route
PREVIEW_JWT_SECRET_PREVIOUS=                    # set during rotation windows only
CMS_PREVIEW_API_TOKEN=server_only_draft_read     # frontend server -> CMS draft API
PREVIEW_COOKIE_NAME=__preview_session
PREVIEW_SESSION_SECONDS=1800

Token Format & Lifecycle

The pattern rests on a standard token format, usually a JWT (RFC 7519). Each preview credential carries exp (expiry), iss (issuing CMS), aud (target frontend domain), and a custom preview_mode boolean or scope array. The frontend verifies the signature against a shared secret or public key, then switches the data-fetching layer to draft mode before rendering. For algorithm selection, claim validation, and secret rotation, see Securing headless CMS preview endpoints with JWT tokens.

Give tokens a 15–30 minute TTL. Transmit them via query parameter on the CMS redirect, then immediately exchange for an httpOnly, Secure, SameSite=Lax cookie to close XSS and CSRF vectors. Never store preview credentials in localStorage or sessionStorage — they’re readable by client-side script. Revoke the token when an editor logs out or the draft is published, so orphaned sessions can’t be replayed.

Implementation

The token’s lifecycle runs from CMS mint through edge validation to a scoped cookie and explicit teardown:

The preview token lifecycleThe editor clicks preview; the CMS redirects with a signed token; the validation route verifies signature and claims, sets an httpOnly session cookie and redirects to the draft; draft fetches use the server-held API token; exit-preview clears the cookie; invalid tokens receive a 401.EditorCMSValidation routeCMS draft APIclick Previewredirect ?token=<signed>verify signatureiss, aud, exp, scopeSet-Cookie session, 302 to draftGET /preview/slug (cookie)fetch draft (server token)draft, no-store/api/exit-preview clears cookie
The signed token lives for one redirect; after that, the browser only holds a short httpOnly session.

1. CMS Configuration & URL Generation

Configure the CMS to build a preview URL that routes through a validation endpoint, not directly to a page template — that intermediary route is the gatekeeper. On “Preview,” the CMS signs a payload with the target slug, locale, and draft revision ID, encodes it into a token, and appends it to the validation URL. With ISR, pair this with Webhook Triggered Rebuilds so the preview reflects the latest content without a full deploy.

2. Token Validation Middleware

Intercept the request at the edge or in an API route. Validate the signature, extract claims, and establish a session. A Next.js App Router implementation using jose:

TypeScript
// app/api/preview/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { jwtVerify } from 'jose';

export async function GET(request: NextRequest) {
  const token = request.nextUrl.searchParams.get('token');
  if (!token) {
    return NextResponse.json({ error: 'Missing authentication token' }, { status: 400 });
  }

  try {
    const { payload } = await jwtVerify(
      token,
      new TextEncoder().encode(process.env.PREVIEW_SHARED_SECRET)
    );

    if (payload.preview_mode !== true || !payload.target_slug) {
      throw new Error('Invalid token claims');
    }

    // Set secure httpOnly cookie for subsequent draft requests
    const response = NextResponse.redirect(new URL(`/preview/${payload.target_slug}`, request.url));
    response.cookies.set('preview_session', token, {
      httpOnly: true,
      secure: process.env.NODE_ENV === 'production',
      sameSite: 'lax',
      maxAge: 1800, // 30 minutes
      path: '/',
    });

    return response;
  } catch (error) {
    return NextResponse.json({ error: 'Invalid or expired token' }, { status: 401 });
  }
}

3. Draft Data Fetching & Session Handoff

Once the cookie is set, every fetch must inspect the session before querying the CMS. Frameworks ship built-in draft-mode toggles; custom setups inject the header explicitly. Pass the validated token as Authorization: Bearer (or read it from the cookie) to request draft content from the CMS GraphQL/REST API. The data layer must respect the preview_mode claim and never cache draft responses in shared CDNs — set Cache-Control: private, no-store on all preview routes to stop leakage to public caches.

4. Session Termination

Terminate preview sessions explicitly so stale drafts don’t persist. Expose /api/exit-preview to clear the cookie and redirect to the live URL, and check token expiry on every route transition. If a token expires mid-session, prompt re-authentication via the CMS rather than throwing — that keeps editorial continuity without exposing the API to unauthenticated fallback requests.

Choosing a Token Design

Three designs cover almost every preview setup, and they differ in who can mint, who can verify and what a leak costs.

Three preview credential designs, from weakest to strongestA static shared secret in the URL, a symmetric signed token minted per click, and an asymmetric signed token with per-entry scope and revocation, with the blast radius of a leak for each.Static secret in URLleak: every draft, foreverone secretno expiryall draftsHS256 token per clickleak: one entry, minutesshared secretexpiryentry claimEdDSA token + scope + revocationleak: one entry, minutes, revocableCMS signs, site verifiesrolesjti list
Each step up costs little code and shrinks what a leaked link can reveal.

A static secret is where most sites start, and it is the design this topic exists to replace: it never expires, it grants every draft, and it ends up in logs and tickets. A symmetric token minted per click fixes expiry and scope, at the cost of sharing one secret between the minting function and the frontend. An asymmetric token, signed by the CMS side with a private key and verified by the frontend with a public key, removes the frontend’s ability to mint, which matters when several frontends, agencies or preview deployments verify tokens and none of them should be able to create new ones. Add a revocation list for share links and long sessions, and the design covers the cases security reviews usually ask about.

Handing Off to the Framework

The validation route’s last job is to turn a verified token into the framework’s own preview state. In the Next.js App Router, that means calling draftMode().enable() and storing the verified claims in a separate, signed session cookie that the fetch layer reads to enforce scope. In Nuxt, it means setting the preview state in a server middleware and exposing it through a composable. In Astro or SvelteKit with server rendering, it means setting a cookie and reading it in hooks or middleware before rendering. The pattern is the same everywhere: the token is verified once, the framework’s preview switch is flipped once, and every fetch after that asks one helper whether and what it may preview.

What the handoff must not do is forward the token to the client or to the CMS. The CMS draft API is called with the server’s own preview API token; the editor’s token only proves that this particular browser may see this particular draft for a short time.

Scoping Tokens to Entries and Roles

A preview token should grant the least access that serves the review. Most CMS preview integrations mint one token per preview click, which makes per-entry scoping natural: the sub claim names the entry, and the frontend refuses to render any other draft with that session. Pages that combine many entries, such as a homepage, need a scope that names the page and lets the page’s own references through, which the frontend can enforce by fetching the page entry with the scope’s id and resolving its references normally.

Roles add a second dimension. An editor who may preview marketing pages may not be allowed to see drafts of a legal section or an embargoed product. Put the CMS role or a list of permitted content types into the token, and check it on the server before fetching drafts. The CMS already knows the editor’s permissions at the moment it mints the token, so this costs nothing extra and closes the gap where the frontend would otherwise show any draft to anyone with a valid session.

Claims in a preview token and what the frontend checksEach claim in a preview JWT, its purpose, and the check the validation route performs before granting a draft session.ClaimExampleFrontend checkisscms-preview-serviceequals the configured issueraudwww.example.comequals this site's audienceexpnow + 900 snot expired, small clock tolerancesubentry:7Ht2only this entry and its references renderlocalede-DEfetch drafts in this locale onlyscopepreview:read, types:[page]content type allowed for this sessionjtirandom idnot in the revocation list
Every claim maps to one check; a claim the frontend never checks is decoration.

Caching & Invalidation Considerations

Preview responses must never be cached anywhere shared. Send Cache-Control: private, no-store and X-Robots-Tag: noindex from the preview route, bypass the CDN for requests that carry the preview cookie, and make the draft fetch itself use cache: "no-store", so the framework’s data cache never stores a draft. Revocation has a caching dimension too: if the frontend keeps a revocation list, keep it in a fast store shared by all instances, and check it on every preview request rather than caching the result of a check.

Sharing Previews with External Reviewers

Editors often need to show a draft to someone without a CMS account: a legal reviewer, a client, an agency partner. Handing them a CMS preview link with a long-lived secret is the most common way drafts leak. A share link is a separate token type, minted on request from inside the CMS, scoped to a single entry and locale, with a longer but still bounded lifetime such as 48 hours, and recorded in an audit log with its creator and recipient. The share link guide covers the minting, the revocation list and the watermark that marks shared previews on screen.

Error Handling & Resilience

Preview authentication should fail closed and explain itself. An expired token should lead to a friendly page that says the preview link expired and offers to open the entry in the CMS again, not to a bare 401. A signature failure should be logged with the token’s iss, aud and jti, never the token itself, because a flood of signature failures is the signature of someone trying to guess secrets. And if the CMS draft API is down, show the error inside the preview banner rather than falling back to published content, which would make editors believe their changes were lost.

Clock skew between the minting service and the edge is the most common source of spurious failures. Keep clockTolerance at 10 to 30 seconds and synchronize clocks on self-hosted minting services; do not “fix” skew by lengthening token lifetimes.

Testing & Observability

The validation route is security code and deserves its own tests: a valid token grants a session, an expired one is rejected, a token signed with another secret is rejected, a token for another audience is rejected, and a session for entry A cannot render entry B. These run in milliseconds with a test secret. End to end, a Playwright test can mint a token, follow the redirect, confirm the draft renders and confirm that a second browser context without the cookie sees published content. The automated testing topic covers the staging setup.

In production, log every session start with jti, sub, the editor id and expiry, every rejection with its reason, and every share link creation. Those three streams answer the questions that come up in an incident: who could see this draft, when, and through which link.

Production Hardening

A few things to get right at scale. Enforce strict CORS on the validation endpoints to reject unauthorized origins. Rate-limit to block token brute-forcing and replay. And make real-time channels inherit the same validation: when you pair this with Live Editing Integration Patterns, WebSocket and SSE connections must run the same token check or you’ve opened a draft-stream injection path.

Audit accessibility too: preview indicators (banners, watermarks, toolbars) need to meet WCAG contrast and keyboard-navigation standards so teams can validate inclusive design before publishing. The Next.js draft mode docs are a solid baseline for cookie handling and route interception to adapt into custom middleware.

Implementation Checklist

  • Replace every static preview secret with tokens minted per preview click.
  • Verify signature, issuer, audience, expiry and scope in one validation route, with an explicit algorithm list.
  • Exchange the token for an httpOnly session and never forward it to the client or the CMS.
  • Enforce entry, locale and role scope in the fetch helper, not only at the route.
  • Send private, no-store and noindex on preview responses and bypass the CDN for preview sessions.
  • Mint separate, revocable share links for reviewers outside the CMS.
  • Rotate signing keys on a schedule with an overlap window, and log session starts, rejections and share links.
  • Apply the same verification to live preview streams as to page requests.

Most of these items are small on their own; the value comes from having all of them, because each one closes a path that the others leave open. A token with perfect claims still leaks drafts if the CDN caches the preview response, and an uncached preview still leaks if a static secret remains valid somewhere. Treat the checklist as a single change rather than a menu.

Frequently Asked Questions

Why not reuse the CMS login session for previews?

The CMS session lives on the CMS domain and cannot be read by your frontend, which is a different origin. Tokens bridge the two origins deliberately and briefly, without sharing credentials.

Should the preview token be a JWT?

A JWT is convenient because libraries such as jose handle signing, expiry and audience checks. Any signed, expiring token works, including an HMAC over a few fields; what matters is the verification discipline, not the format.

How short should preview sessions be?

The token itself should live only long enough for the redirect, a few minutes at most. The session cookie it creates can last 30 to 60 minutes and be renewed while the editor keeps working. Share links for external reviewers are the exception, with lifetimes measured in days and explicit revocation.

What if the minting service is unavailable?

Preview stops working, which is the correct failure. Never fall back to a static secret or an unauthenticated draft route; show editors a clear message and alert on the outage.

Do previews inside the CMS iframe need different cookies?

Yes. The iframe is a third-party context, so the preview session cookie needs SameSite=None; Secure and, in browsers that partition third-party cookies, it will be partitioned per embedding site, which is fine for preview. Keep such cookies scoped to preview paths.

Can one token cover a whole release of entries?

Yes: put the release id in the scope instead of a single entry, and let the frontend fetch drafts for any entry in that release. That suits launch reviews, where stakeholders click through a set of pages that will go live together.

How do previews on branch deployments fit in?

Every preview deployment verifies tokens for its own audience. Put the deployment’s hostname in the aud claim, and have the CMS mint tokens for the deployment the editor chose, so a token for a feature branch cannot open drafts on production and vice versa.

Are tokens needed if previews run on an internal network?

Network boundaries help, but they do not tell you which person saw which draft, and they fail as soon as a contractor or remote reviewer needs access. Tokens give per-entry, per-person, auditable access that works anywhere.

Who should own the minting service?

The team that owns the CMS configuration, because minting encodes CMS permissions. The frontend team owns verification. Writing both halves against a shared claim specification keeps them from drifting.