Skip to content

Private betaRequest early access →

Developers

Point any OIDC library at it. No bespoke code.

Perihelium's authentication layer is a standards OpenID Provider (OAuth 2.1 and OIDC Core). Give your library the issuer URL and it discovers the rest. Verify tokens offline, call the API with scoped keys, and receive signed webhooks.

DiscoveryFig. 1

GET /.well-known/openid-configuration

issuer
https://auth.zynthmedia.com
authorization_endpoint
/api/v1/oauth/authorize
token_endpoint
/api/v1/oauth/token
userinfo_endpoint
/api/v1/oauth/userinfo
jwks_uri
/.well-known/jwks.json
code_challenge_methods
["S256"]
id_token_signing_alg
["RS256"]

Start here

The issuer is the integration

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
openid-configuration9
  • 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.

  1. 01Register a client under Organization, OAuth Clients (or with the API). Confidential clients get a show-once secret; public clients rely on PKCE.
  2. 02Run the authorization-code flow with PKCE. PKCE is mandatory for every client.
  3. 03Verify the ID token against the issuer's keys.
  4. 04Call UserInfo if you need profile claims.
  5. 05Refresh for long-lived sessions, and end the session at the logout endpoint.
  6. 06For machine-to-machine, use client_credentials.

Tokens

Verify tokens offline

Access tokens are signed. Your API verifies them against the published keys, with no call to Perihelium per request.

  1. 01Fetch and cache the JWKS; refresh once on a miss.
  2. 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.
  3. 03Verify the signature with the algorithm pinned to RS256. Never accept an algorithm from the token.
  4. 04Validate iss, exp and aud, and confirm type is access.
  5. 05Use sub and tid as 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

TypeScript · @zynth/auth-client
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 humans

Verification 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.

The API

Call the API

Backend jobs, data syncs and CI pipelines use an API key. It is used exactly like an access token.

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

Events

React to events with signed webhooks

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.

Node.js · verify a delivery
// 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.

Tools

SDK and sign-in tools

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.

This page is the summary. The documentation has every endpoint, parameter and edge case.