Verified, Attested, Handed Off
email_verified: true
trusted: true
auth_time: 2 minutes ago
That is what the first version of our sign-in debugging view showed for a session. It showed the same thing for the next session, and the one after that. Every user had a verified email. Every user was trusted. Every user had signed in two minutes before the page loaded.
None of that was a lie on purpose. When the door had a value, the page showed it. When it did not, the page filled the gap
with something plausible, so the screen would look complete during development. A missing sign-in time became “now, minus
two minutes”. A missing email check became true. A client address that was never captured became a fixed example address.
The view looked like evidence. Half of it was decoration.
Some setup, in one sentence: the system is a sign-in door, a small server that sits between a browser, an identity provider and a cloud identity service, carries the user through the sign-in, and then shows an operator exactly what happened on the way. The point of the view is to answer one question for whoever has to review a sign-in later: what did this door actually see? A view that invents values cannot answer it. It answers a different question, what would a normal sign-in look like?, and it answers in the same font.
Two colours for three kinds of fact
The fabricated values were the loud failure. The quiet one was in the hop list.
The door’s trace shows each step of a sign-in as a hop with an outcome. The first design had two outcomes, passed and failed, and two colours, green and red. That turned out to be one state short, and the missing state was being absorbed into the other two.
Some hops the door checks itself. It sends a random value out and requires the same value back. It compares a returned session id against the one it sealed into its own cookie. Green is honest there.
Some hops it does not check. Once the door stopped holding each provider’s client secret, the cloud identity service ran the code exchange with the provider and checked the provider’s signature. The door receives the result over TLS and decodes it. It does not re-check the signature. Showing that hop green says the door verified something it did not.
And one hop the door cannot see at all. The last step sends the browser on to the application’s own access proxy. The door emits the redirect, and whether the browser completes that step is visible only at the application. The two-colour design showed it in amber as “unverified”, which reads like a warning on every successful sign-in.
So a hop now has four outcomes:
- verified: the door checked it here, itself.
- attested: the platform vouches for it. The door received it over TLS and did not re-check the signature at this hop.
- handed off: the next system’s job. The door only redirects.
- failed: the step did not succeed, with the reason.
Each has its own mark. Attested has its own colour and is never green. Handed off is a neutral grey, not amber, with a hint that completion can only be observed at the target. Neither counts toward the number of checks the door reports as its own. Two more labels cover what the trace itself lacks: a value the door never captured reads “not observed”, and when the trace has to be shortened to fit in the session cookie, the dropped hops read “trace truncated”. A hop that was checked and then dropped for space is never shown as not observed.
Each label says one thing: who did the checking. That is the only thing a reviewer cannot reconstruct afterwards.
The claim that was not there
The sharpest case was the sign-in time.
A reviewer wants to know when the user last actually proved who they are at the identity provider. OpenID Connect has a
claim for it, auth_time. In testing, the claim kept coming back missing.
The reason is that two identity providers differ in what they omit. One leaves the original sign-in time out of its token unless the request asks for a maximum age. Another issues no ID token at all, so there is nothing to carry the time. The door now asks for a maximum age from the providers that honour one, and the time comes back. For the others it does not.
When the claim is missing, a substitute is sitting right next to it. Every token has an issued-at time, and it is usually close to the sign-in time. Falling back to it would fill the field, and almost always with a number that looks right.
We refused the fallback. Issued-at is when the platform minted its token. It is not when the user proved themselves to the provider. A sign-in that rides on an old provider session still gets a fresh issued-at, and that is exactly the case a reviewer is looking for. A field that holds the issue time under the label “sign-in time” makes a statement nobody made. So when the provider omits the claim, the view says the claim is absent. The empty field is the true record.
When the claim is present, the view does not promote it either. It is shown as attested, with the chain it came through written beside it, because the door decoded it and checked that it was a plausible time, and did nothing more.
What changed
Each change is a mechanism:
- Every invented default was removed. The view renders only values the door captured. Everything else reads “not observed”. A page test checks that no fabricated value renders in the claims view, the sign-in card or the step list.
- The hop outcome is a closed set. Verified, attested, handed off, failed, plus not observed. The trace records which one applied as the hop runs, not afterwards.
- The count tells the truth. The number of checks the door reports covers only what it checked itself. Attested and handed-off hops are listed but never counted.
- Truncation says so. A trace that had to be cut carries a flag, and the view shows where it was cut.
- No fallback for the sign-in time. If the provider omits it, the field is empty and labelled.
For the person signing in, none of this changes anything: the same buttons, the same redirect, the same speed. For the person reviewing a sign-in a month later, it is the difference between a record and a mock-up.
The same rule applied to a whole service, not one hop, was the previous post: missing evidence adds nothing. And it is the same lesson we had learned about one word on a trust screen: once “verified” means something, it is reserved, and any use it has not earned spends it.
The line that stays
A relay can be honest about checking very little, as long as it says exactly what it checked. Each fabricated default was a fallback with no label. It filled the gap with a value that nobody had observed.
A fallback you cannot label misleads.