Ensemble Docs

JWT signing secrets

Managing the shared secrets your backend signs user tokens with, activating and revealing one, and why a secret per integrator is worth it.

A JWT signing secret lets your backend mint tokens that reach the platform as your own users. Your users keep their identity, you never hand them a platform credential, and nothing long-lived reaches the browser.

How to sign a token, which claims it needs and what the audience must be is covered in Authentication. This page is about managing the secrets themselves.

Active and inactive

A secret is created inactive and has to be activated before tokens signed with it are accepted. That order is deliberate: it lets you create the secret, deploy it to your backend, and only then make it live, rather than having a window where the secret exists and half your fleet has it.

Deactivating is the reverse, and it is reversible. Tokens signed with a deactivated secret stop verifying immediately, which is the fastest way to cut off an integrator without deleting anything.

Revealing

Unlike an API key, a signing secret can be read back. It has to be: your backend needs the same value to sign with, and a secret only one side knows cannot be a shared secret.

It is withheld from listings and returned only by an explicit reveal, so the value does not turn up in a screen anyone happens to be sharing.

Reveal is the one place a credential here comes back in full. Treat it the way you would any secret retrieval: do it when you need to configure a backend, not to check whether it is still there.

One secret per integrator

Several secrets can be active at once, and that is the useful configuration rather than a transitional state.

Give each integrator, or each environment of your own, its own secret. Three things follow:

You can cut one off without touching the others. Deactivate that secret and only that integrator's tokens stop.

You can rotate without a flag day. Create the new secret, activate it, move the integrator, deactivate the old one. Both are valid in between.

Configuration can differ per integrator. Environment variables can hold an override keyed to a signing key, so ${secrets.API_TOKEN} resolves to that integrator's token in the same tool definition everyone else uses. That mechanism is keyed on the signing secret and is the main reason to split them. See Environments.

Testing one

Two helpers exist so you can check a secret works before wiring a backend to it:

Generate mints a token from a secret for a user id you name, with optional extra claims and a lifetime. Use it to exercise the platform as one of your users without writing signing code first.

Verify takes a token and tells you whether it is valid and what it resolved to. That is the endpoint to reach for when your own signing code produces something the platform rejects: it says whether the signature, the audience or the claims are at fault.

curl -X POST https://service.ensembleapp.dev/api/tenants/secrets/verify-jwt \
  -H 'Authorization: Bearer <jwt>' \
  -H 'Content-Type: application/json' \
  -d '{ "token": "<the token your backend signed>" }'

The API

GET    /api/tenants/secrets                          List secrets, without their values
POST   /api/tenants/secrets                          Create one, inactive
PATCH  /api/tenants/secrets/{secretId}/activate      Accept tokens signed with it
PATCH  /api/tenants/secrets/{secretId}/deactivate    Stop accepting them, reversibly
GET    /api/tenants/secrets/{secretId}/reveal        The value, for configuring a backend
POST   /api/tenants/secrets/{secretId}/generate-jwt  Mint a token for testing
POST   /api/tenants/secrets/verify-jwt               Check a token and see what it resolved to
DELETE /api/tenants/secrets/{secretId}               Delete it

Deleting

Deleting is permanent, and tokens signed with that secret stop verifying at once.

Prefer deactivating while you are still investigating. A deleted secret takes with it any environment overrides keyed to it, which is a quieter loss than the tokens failing: the tool definitions stay, silently resolving to the tenant-wide default instead.

On this page