← Changelog

Reader sign-in: PATCH, same-origin redeem and sign-out, named JWT refusals

2026.10.14-reader-auth
The Forumulate team
3 min read
apireader-auth

Added

PATCH …/reader-auth. On the org (/orgs/{orgId}/reader-auth) and on a space override. A deep merge: absent keeps, null deletes, providers are matched by method (plus order when there are two of one method), unmatched providers are added, remove: true drops one, and a provider's config merges key by key. Change a label without resending — or risking — a client secret. See Reader sign-in: the API.

secretSet on every provider. Admin reads of reader-auth now say, per provider, whether a secret is stored.

readerEndpoints. GET /orgs/{orgId}/spaces/{spaceId}/reader-auth and its /setup list, for every address the space is served from (custom domain first), the same-origin jwtRedeemUrl and signOutUrl. A domain's …/domains/{domainId}/setup answers the one for that domain. startUrl for external_jwt now points at the primary address. A reader session is a host-only cookie, so redeeming on forumulate.com never signed anybody in on your domain.

Reader sign-out. POST <mount>/_api/public/reader/sign-out (or /api/public/reader/sign-out on our addresses) revokes the reader session and clears every reader cookie on that host. Optional {orgId, spaceId, returnTo} answers where to go next.

signOutUrl. On reader-auth, beside signInUrl: Forumulate's reader Sign out clears its own session, then sends the reader to your page with returnTo, so one click ends both sessions.

error.reason on external_jwt refusals. One stable word per cause — iat_too_old, iat_in_future, bad_signature, bad_issuer, bad_audience, expired, not_yet_valid, missing_claim, unknown_key, jwks_unavailable, and more — listed in Sign readers in from your own app with a JWT. reason is a new optional field on the error envelope.

Changed

30 seconds of clock skew on external_jwt. iat, nbf and exp are checked with a ±30 s allowance; the iat freshness window stays 300 seconds. A token minted a second "in the future" by a clock slightly ahead of ours is no longer refused.

returnTo is capped at 2048 characters everywhere. visitor-oidc/start now refuses a longer one with a 422 naming returnTo, as the JWT redeem already did; the signInUrl / signOutUrl hand-offs send a longer page address without its query and fragment.

Fixed

A save can no longer erase a secret by leaving it out. On every configuration endpoint that hides a secret on read — reader-auth, a space's settings, Cloudflare, mail, AI and embedding keys, captcha, SMS, DeepL, Discourse Connect, SSO, SIEM, consent webhooks, SDK registry tokens, extensions, group mailboxes and status-monitor headers — a secret that is absent, empty or a redaction mask (********, ••••abcd) keeps the stored value, and the <key>Set markers a read returns are ignored. Reader-auth providers are matched by method + order, then by method, so reordering no longer drops a secret. Previously a reader-auth PUT without the OIDC client secret saved the provider without it, and reader sign-in failed until it was re-sent; a manifest apply did the same to every space it touched; a mail key hint sent back was stored as the key.

A space's legacy visitor-auth JWT secret and password hash are redacted on read. GET /orgs/{orgId}/spaces/{spaceId} returned settings.visitorAuth.jwtSecret and passwordHash; they now read jwtSecretSet: true / passwordHashSet: true.

Forumulate's reader Sign out clears the visitor pass. It cleared only the member session, so a gated space stayed open until the 12-hour pass expired.

GET/PUT /orgs/{orgId}/siem answer tokenSet, and an empty token no longer erases the stored one (null still clears it).