Everything is discovered from one URL. Grafana, Kubernetes, Backstage, openid-client, next-auth and Spring Security sign in against it with no custom code.
https://auth.zynthmedia.com- GET
/.well-known/openid-configurationDiscovery - GETPOST
/api/v1/oauth/authorizeAuthorization: code flow, PKCE required - POST
/api/v1/oauth/tokenToken - GETPOST
/api/v1/oauth/userinfoUserInfo - GET
/.well-known/jwks.jsonThe keys to verify tokens with - GET
/api/v1/oauth/logoutEnd session - POST
/api/v1/oauth/device_authorizationDevice authorization, for CLI sign-in - POST
/api/v1/oauth/introspectToken introspection - POST
/api/v1/oauth/revokeToken revocation
What it advertises is exactly what it accepts: response type code, PKCE S256, RS256 ID tokens, and the scopes openid, profile, email and roles. Grant types are authorization code, refresh token, client credentials and the device code.
- 01Register a client under Organization, OAuth Clients (or with the API). Confidential clients get a show-once secret; public clients rely on PKCE.
- 02Run the authorization-code flow with PKCE. PKCE is mandatory for every client.
- 03Verify the ID token against the issuer's keys.
- 04Call UserInfo if you need profile claims.
- 05Refresh for long-lived sessions, and end the session at the logout endpoint.
- 06For machine-to-machine, use
client_credentials.
Access tokens are signed. Your API verifies them against the published keys, with no call to Perihelium per request.
- 01Fetch and cache the JWKS; refresh once on a miss.
- 02Select the key by the token header's
kid. The endpoint publishes up to three keys so tokens verify across a rotation. Never take the first key as the active one. - 03Verify the signature with the algorithm pinned to
RS256. Never accept an algorithm from the token. - 04Validate
iss,expandaud, and confirmtypeisaccess. - 05Use
subandtidas the identity, and resolve permissions server-side.
ID tokenthe OIDC issuer URL
Audience (aud)your client_id
Access tokenzynth-auth
Audience (aud)zynth-services, or your registered resource
import { createVerifier, ZynthAuthError } from "@zynth/auth-client";
const verifier = createVerifier({ baseUrl: "https://auth.example.com" });
const claims = await verifier.verify(token);
// claims.sub the user id (or agent id)
// claims.tid the tenant
// claims.actor "agent" for AI-agent principals, absent for humansVerification is offline and cannot see a decision made after the token was minted: a revoked grant, a stopped agent, a password change. Introspection answers whether a token is live now, and revocation is how a client ends its own grant. Use introspection where that freshness matters.
Backend jobs, data syncs and CI pipelines use an API key. It is used exactly like an access token.
# zyn_live_<secret>_<checksum> (test keys: zyn_test_…)
curl https://auth.zynthmedia.com/api/v1/consent/admin \
-H "Authorization: Bearer zyn_live_…"- environmentA key is bound to the environment that issued it: a test key never authenticates against production.
- shown onceThe full value appears once, at creation or rotation. Store it immediately.
- scopesA key may do only what its scopes allow.
- rotationRotate without downtime. Revoke at any time.
- readbackWhat a key did can be read back, and undone.
- checksumMalformed keys are rejected instantly, and secret scanners recognise a leaked key.
For sender-constrained access, bind a token to the client's own key with DPoP: a stolen token is useless without the key that proves possession. Agents get this too.
Verify the signature before you trust the payload. Anyone can POST JSON at your URL; only the platform can sign it.
- Zynth-Signaturet=<unix-seconds>,v1=<hex-hmac-sha256>, signed over the timestamp, a dot and the raw body.
- timestampReject anything outside five minutes either way; the timestamp is inside the signed material.
- idUnique per event. Use it as your idempotency key: a delivery can arrive twice.
- Zynth-Delivery-Attempt1 on the first attempt, then 2, 3 on retries. Expected behaviour, not a fault.
retriesFifteen attempts over about a day and a half, with jitter, then the delivery moves to dead letter. It is kept, and you can replay it.
Fifteen attempts over about a day and a half, with jitter, then the delivery moves to dead letter. It is kept, and you can replay it.
- failuresAfter twenty consecutive failures an endpoint is disabled and shows why. Re-enable it once fixed.
rotationDuring a secret rotation the header carries more than one v1 value; accept the request if any matches.
During a secret rotation the header carries more than one v1 value; accept the request if any matches.
// Zynth-Signature: t=<unix-seconds>,v1=<hex-hmac-sha256>
// signed material: HMAC-SHA256( secret, "<t>.<raw body bytes>" )
const crypto = require("node:crypto");
function verify(header, body, secrets, tolerance = 300) {
if (!header || secrets.length === 0) return false;
const fields = {};
for (const chunk of header.split(",")) {
const idx = chunk.indexOf("=");
if (idx <= 0) continue;
(fields[chunk.slice(0, idx).trim()] ||= []).push(chunk.slice(idx + 1));
}
const stamps = fields.t || [];
const signatures = fields.v1 || [];
if (stamps.length !== 1 || signatures.length === 0) return false;
const ts = Number.parseInt(stamps[0], 10);
if (!Number.isInteger(ts)) return false;
if (Math.abs(Math.floor(Date.now() / 1000) - ts) > tolerance) return false;
const signed = Buffer.concat([Buffer.from(`${ts}.`), body]);
return secrets.some((s) => {
const valid = Buffer.from(crypto.createHmac("sha256", s).update(signed).digest("hex"));
return signatures.some((c) => {
const a = Buffer.from(c);
return a.length === valid.length && crypto.timingSafeEqual(a, valid);
});
});
}Sign the raw bytes, not a re-serialised body, and compare in constant time. Acknowledge first and process asynchronously: delivery is unordered, and a slow handler burns retries.
The pieces around the protocol, so you do not rebuild them.
@zynth/auth-clientThe TypeScript and Node SDK: a hardened token verifier, a client for building governed AI agents, and the client half of DPoP. Runs on Node 20+ and on edge runtimes.
The TypeScript and Node SDK: a hardened token verifier, a client for building governed AI agents, and the client half of DPoP. Runs on Node 20+ and on edge runtimes.
Loopback sign-inRFC 8252: a CLI on a machine with a browser registers a loopback address without a port and presents any port at run time. PKCE throughout.
RFC 8252: a CLI on a machine with a browser registers a loopback address without a port and presents any port at run time. PKCE throughout.
Device grantRFC 8628: an SSH session, a container or a remote box shows a code, and the person approves on another device.
RFC 8628: an SSH session, a container or a remote box shows a code, and the person approves on another device.
Verifiable signaturesA person signs a digest with their passkey; the assertion verifies offline, with no call to the platform.
A person signs a digest with their passkey; the assertion verifies offline, with no call to the platform.
Event ingestionReport what your own gateway or router observed about an agent, as a closed vocabulary of metadata, never content.
Report what your own gateway or router observed about an agent, as a closed vocabulary of metadata, never content.