Saphan StudioDocs
Identity

Trusting an issuer

The act performed by hand on the machine holding the root key, the four values it turns on, and the mistakes that surface months later.

Telling this product that proofs signed by a particular authorization server may be honoured is an act performed by a human hand, on the machine holding the installation's root key. Nothing in the product does it for you, and nothing ever will.

That is deliberate. Which servers may mint proofs I honour is exactly the decision that should cost a person a deliberate command, on a machine only that person can reach, so that there is somebody to point at afterwards.

It is done once per authorization server. Reversing it is the same shape of act in the other direction, and it is also yours.

Before you start, give the installation its own name

The first step is not the enrolment. It is setting the address this installation calls itself by:

saphan config set server.public_base_url <the https address this installation answers on>

Do not skip this and do not leave it to be inferred. With it unset, the installation names itself from whatever address it happened to observe on its own socket — a loopback address on a laptop. Three things follow from that, and the third is the expensive one:

  • the name you would have to enrol is a loopback address, which is not something you can write into a record other machines read;
  • the value changes with the bind address, so a record enrolled today stops matching the day the installation moves or its port changes;
  • every proof minted for it would carry that same loopback name, so it would be spendable at any installation that happened to bind the same port.

⇒ What actually breaks until this is set is that nothing enrols cleanly at all: the enrolment appears to succeed and every single proof is then refused for naming the wrong recipient.

The value must be an origin — a scheme and a host and nothing else. A path, a trailing slash, a query, a fragment or credentials in the address are each refused when the configuration is read, by name.

Read the values off the running server, not off your own notes

The enrolment needs four values, and where you get them from decides whether the record is right.

Fetch the authorization server's own published description and read the values back out of it. What you enrol has to be what the server actually says, byte for byte — a trailing slash or one scheme in place of another is a difference, and it is a difference that is accepted here and refused at every proof afterwards.

The signing algorithm is the one value the published description a client is pointed at does not carry. Neither does the published key. There are exactly two honest places to read it: the server's other published description, which lists the algorithms it supports, or the published key's own type, from which the algorithm follows.

Do not compose that value from the configuration file you wrote. A trust record composed from intent rather than from what the running server publishes is a record that can be quietly wrong, and it will be wrong in the way this page keeps warning about.

Enrol the issuer

Run this on the machine holding the root key. It is a signed act.

saphan identity enroll --kind issuer \
  --subject   <the authorization server name, exactly as it publishes it> \
  --jwks-uri  <the address it publishes its keys at> \
  --audience  <this installation name, as set above, with the service path> \
  --alg       <the signing algorithm, read from the running server> \
  --workspace <the absolute path of your workspace>

All four values are checked at every request and none of them is checked here. That is the single most important sentence on this page. Enrolment confirms that the fields are filled in; it confirms nothing about whether they are right. A wrong algorithm, a wrong recipient name, an address the product will never agree to open — each is accepted, signed, and then produces a refusal weeks later with nothing anywhere near it to explain why.

In particular, the key address is not tested for a scheme the product will accept. A plain, unencrypted address is accepted and signed here, and then refused before a connection is even opened, every time, forever. There is no flag, no configuration key and no environment variable that relaxes that. Check the scheme yourself before you sign.

The audit table you can read afterwards is a projection of the signed record, not the record. Read it to check your work. ⛔ Never hand-edit it — the product reads the signed record, and an edit there changes nothing except your belief about what is enrolled.

The act on the machine, which is not an act in the product

If the authorization server presents a certificate the machine does not already trust — a self-signed certificate on a single on-premises host is the ordinary case — the machine has to be told to trust it. The authorization server presents; this product verifies, and it verifies against the trust store of the host it runs on.

A certificate the host does not trust fails when the keys are fetched, and the refusal you get back says nothing whatever about certificates. Do this before you enrol anything.

A command-line tool that succeeds is not proof by itself. Some of them carry their own certificate bundle and never consult the machine's, so they answer a different question from the one you are asking. Check it with a client that uses the machine's trust store the same way this product does.

Trusting it for the signed-in user is usually enough and costs no administrator rights. Prefer the smaller change and find out.

Enrol the person, too

This is a second act and it is not optional. The issuer record says that server may mint proofs I honour. It says nothing about who. A person nobody enrolled is refused even when their issuer is perfectly trusted.

saphan identity enroll --kind binding \
  --subject   <the identifier the authorization server puts inside the proof> \
  --actor     <the person name that will appear in the record> \
  --stream    <each stream this person may read, or a deliberate wildcard> \
  --workspace <the absolute path of your workspace>

The binding is keyed on the identifier inside the proof — not on the person's name, and not on the program's. The flag reads as though it wants a program, and with some identity providers those two values happen to be the same, which is why this has been invisible. With this product's own authorization server they are different. A binding written with the wrong one is a record nothing will ever look up, and it is refused with the same shape as no binding at all — which is to say, without a reason.

Adding a person to the authorization server does not print the identifier this enrolment needs. The only place it is written plainly is inside a proof that person's own sign-in produces. So the order is awkward and it is fixed:

  1. add the person on the authorization server;
  2. have them sign in once;
  3. read the identifier out of the proof that comes back;
  4. only then write the binding.

The administrative panel also shows it, and that is the shorter road — but the panel refuses everybody until somebody holds the administrative role, which is itself one of these acts.

The person also has to be in the roster and active, which is its own ceremony. Naming the streams they may read is mandatory and has no default: name each one, or say the wildcard deliberately.

Check that it took

Make one real request with a real proof, and read the answer against this:

What comes backWhat it meansWhat to do
a real resultenrolled and workingnothing
refused as an unaccepted proofthe proof was not accepted — unknown issuer, wrong recipient, wrong algorithm, or a signature that does not verifyre-read the four values. The algorithm and the recipient are the two that are accepted at enrolment and refused at every proof
refused as a denial of accessthe issuer is trusted and the person is notenrol the binding
refused briefly, right after a key changeexpected. The product bounds how often it re-fetches a key set, so a new key is adopted up to that late — never laterwait, then retry

A refusal carries a class and a short reference, and that reference does not currently lead anywhere you can follow. Until it does, diagnose by narrowing rather than by reading: present a proof with a deliberately broken signature and compare the two refusals. A broken signature is refused as an authentication failure with a challenge; a good signature that is not yet authorised is refused without one. That single difference tells you whether the key fetch, the certificate and the signature all succeeded, which is most of what can go wrong here.

Changing the signing key without an outage

Three steps, and the middle one is the point:

  1. Mint a second key. The first key on a new installation goes straight into use; a later one is staged — published, but not yet signing.
  2. Rotate. The staged key starts signing. The previous key stands down but stays published, so every proof it already signed keeps verifying.
  3. Retire. The stood-down key leaves the published set. ⛔ From that moment every proof it signed is refused. Run it only once the last of them has expired — which is the ten minutes in What a token carries, and it is the wait an operator owes between rotating and retiring.

Rotating does not need a restart. ⚠ A service does not pick up a new key instantly — it caches what it fetched, and that delay is a designed floor rather than a fault. Rotating faster does not make the new key arrive sooner.

No key is ever created for you, and a server with no published key refuses to start. That is deliberate: a key that appeared by itself is a key nobody decided to trust.

Two things this act is not

Pointing clients at an authorization server is a different decision from trusting it, and the product keeps them apart on purpose: advertising a server to clients must never by itself grant that server the power to mint proofs this product honours. They are separate settings and separate acts.

Trusting a third-party identity provider is the same ceremony, not a different one. Only the algorithm and the address its keys are published at differ. What you are enrolling is a server, and the product does not care who wrote it.