Skip to content

proven on the hosted testnet instance · 2026-09-13 · QA-14

Bring your own issuer

Your users authenticating to Permafrost with tokens your own issuer signed. A tenant per user, created on first use and keyed by that user’s Sui address. Nothing shown once, nothing to store, nothing to mail: no API key is ever minted for a user on this lane.

Everything downstream is identical to the key lane. The edge injects the same headers with the same values, so the storage layer cannot tell the two lanes apart, and every command in the Quickstart works unchanged with a token in the bearer slot.

  • An issuer you control that signs compact JWTs and publishes a JWKS over HTTPS. If you already sign anything for your own users, you have one.
  • Ed25519 signing keys. See the token rules below — this is the only algorithm accepted.
  • A Sui address per user. The token’s subject is that address, so your system has to know it at signing time.
  • One email to hello@permafrost.live with the JSON below. We enable it; you cannot enable it yourself.

One object per issuer:

{
"issuer": "https://issuer.example/",
"jwks_url": "https://issuer.example/.well-known/jwks.json",
"audience": "permafrost-edge"
}

audience is ours and is the same for everyone: permafrost-edge. The other two are yours. Send the exact strings your tokens will carry — iss is matched against issuer literally.

All of them, on every request:

  • alg is EdDSA (Ed25519). Every other algorithm is rejected before a key is loaded — including none, and including every HMAC algorithm.
  • kid is required and must name a key in your JWKS with kty: OKP and crv: Ed25519. A kid we have not seen forces exactly one refresh of your JWKS, never a loop.
  • iss is the issuer you registered, and aud is your audience — either as a string, or as an array containing it.
  • exp is present and has not passed, and nbf, if you send it, has. Both are checked with up to a minute of clock skew allowed, so an issuer whose clock has drifted will start failing.
  • sub is the user’s Sui address in canonical form: 0x followed by 64 lowercase hex characters. It must arrive canonical. The subject is signed by you, so nothing folds case on this path — an upper-case address in a signed token is refused, not repaired.

The subject is the tenant. The first request carrying a valid token for an address provisions that tenant with the same defaults a redeemed invite gets, and every later request reuses the same row. Two first requests racing each other converge on one tenant, because the address is the primary key.

No key is issued at any point in that. There is nothing to show once, nothing to rotate, and nothing for a user to lose. Revoking is one call on our side against the tenant, and it denies both lanes at once.

A refused token is answered 401, and that 401 is identical whatever went wrong: expired, not yet valid, wrong audience, unknown kid, wrong algorithm, subject that is not a canonical address, an issuer we have never heard of. No claim, no issuer name and no reason is echoed back. A failure to fetch your JWKS denies as well, rather than answering a server error.

This is deliberate. The response cannot be used to map what we have configured — and it has a practical consequence: you cannot debug a token against our answer. Verify it against your own JWKS first, then ask us.

We add your issuer to this instance’s roster and restart the edge. Your keys are fetched before the port opens, and refreshed in the background before the cache goes stale, so a user request never waits on a fetch of your JWKS.

Until we have done that, a token from your issuer is refused exactly like an unknown key — the same identical 401. There is no state you can query to find out whether we have enabled you yet, so the sequence is: send the JSON, wait for us to confirm, then try.

One case stays open after that. A key you rotate without announcing it, in the middle of a cache window, is discovered by a request that then has to wait for the fetch — and that one request is denied while the fetch completes and fills the cache. The next request succeeds. Tell us before you rotate and it does not happen at all.

A token-lane tenant has no self-service view. /portal and /me authenticate by pf_sk_, and this lane has no key to present, so neither answers for your users — no tier page, no usage page, no key list, nothing to rotate. See Keys, rotation, and the portal for what your users are not getting.

Their usage is visible to us on the operator surface instead, and we will tell you on request. Teaching those two routes this lane is not built.

Put the token where the quickstart puts the key — it is the same bearer slot:

Terminal window
export AUTH="Authorization: Bearer <the token your issuer signed>"

Then run the Quickstart from the top. Nothing else in it changes.

The bucket call at the top of the quickstart answers 200 under a token your issuer signed. Then sign a second, fresh token for the same subject and list that bucket with it: the object you stored under the first token is there. Same subject, same tenant.

Before we have enabled you, the same call answers the identical 401 an unknown key gets. That is the signal to ask us rather than to keep trying.

Permafrost runs on Sui testnet and Walrus testnet. Everything here describes a shipped testnet instance, not a production service.