At a glance
- A federated sign-in ticket that says "SSO is broken" can mean any of roughly eight different SAML conditions, and most of them look identical from the user's chair.
- I built Assertion Desk, a synthetic diagnosis lab, to test whether an AI-drafted explanation could name the specific one and cite the check that proved it, never a guess.
- In one recorded case, the model's draft was confident and well-formed. A deterministic gate rejected it anyway, because the check it cited had not actually failed.
make eval-replayreproduces the recorded run offline, no live model call required.
Eight things "SSO is broken" can mean
A user cannot sign in to one federated application. The ticket says "SSO is broken." That sentence is true and it tells you almost nothing, because a SAML sign-in has roughly eight different, independent ways to fail, and to the person typing the ticket, every one of them looks the same: a spinning wheel, then an error page.
An expired certificate on the identity provider looks like a broken sign-in. A signature that no longer validates because a certificate rotated on one side but not the other looks like a broken sign-in. An assertion that arrived a few seconds outside its validity window because two servers' clocks disagree looks like a broken sign-in. A missing or duplicated identity attribute, a mismatched audience restriction, a wrong destination URL after an application moved to a new address, all of it looks like a broken sign-in. Some of these are the identity provider's problem. Some are the application's. Several of them will resolve on their own in a few minutes and several will not resolve until someone finds and fixes the specific field that is wrong.
I spent real time in a help desk role learning that the fastest way to make a ticket like this worse is to guess at a category and start troubleshooting the wrong layer. I built Assertion Desk to see whether a deterministic verifier plus a supervised model could do what a careful analyst does: read the actual signed assertion, check it against the specific conditions that can fail, and say which one did, instead of restating that something, somewhere, is broken.
What a diagnosis is worth if you cannot say which one
A diagnosis that cannot name the failing condition is not a diagnosis. It is a longer way of restating the ticket. "SSO login failures after a bulk sync" is a summary of the symptom, not a finding, and a support answer built only on a summary sends the customer's IT admin down whatever path the wording happens to suggest, which is not necessarily the path the actual data supports.
Assertion Desk's pipeline never lets a model's summary stand in for a finding. A deterministic verifier checks the captured assertion against roughly twenty named conditions first, things like signature validity, certificate expiry, clock skew tolerance, audience restriction, and destination match, and every one of those checks resolves to one of six states before a model ever sees the case. Only then does a drafting step write the customer-facing explanation, and it is only allowed to cite a check that the verifier actually marked as failed or needing review. It cannot invent a finding. It cannot promote a passing check into the cause. The gap between "here is a plausible-sounding explanation" and "here is the specific check that failed and the evidence behind it" is the entire reason this lab exists.
A confident, wrong answer, and the gate that caught it
The clearest example I have is a recorded case named missing_nameid. The synthetic fault was a SAML assertion missing its NameID element, the field that identifies who is actually signing in. The verifier ran its full set of checks and correctly flagged the missing element, along with two related signature checks that failed as a downstream consequence of the same edit.
The model's draft did not cite any of that. It wrote a fluent, well-formed explanation: "The customer is experiencing SSO login failures after a bulk user directory sync," and named the root cause as a check about whether the response was replying to a request the service provider had actually sent. That check had not failed. It had not been checked at all, because nothing in this case's flow required it, which is an ordinary, unremarkable state for that particular condition. The model's claim was confident, grammatically clean, schema-valid, and pointed at a check that had nothing wrong with it.
A gate sitting between the draft and anything a customer would see caught that on its own terms, without ever being told the right answer. Its one job is to check whether a claim's cited state actually matches what the verifier recorded, and here it did not: the model had pointed at an unverified gap and called it a finding. The gate rejected the draft outright. Nothing published. No answer went to the customer that week, in that recorded run, because no answer had actually been earned yet.
That is the version of this lab I want a support reader to sit with. It is not a story about a model getting the SAML details wrong, most of the draft's language was reasonable. It is a story about a system that refuses to let confidence substitute for evidence, even when the confidence is well written.
Why a person still signs off
Even a draft that clears the grounding gate does not go out on its own. A human reviews the grounded explanation and the specific checks it rests on before anything is marked ready to publish, and the pipeline design treats an unrecognized or ambiguous state as needing that review by default, never as a silent pass-through. A gate that checks whether a claim matches a finding is not the same thing as a person deciding this is the right call for this customer, and I did not want to build something that quietly blurred the two.
I also do not want to overstate what "verification complete" means at the end of this flow. Having the IdP-side condition pass again is not proof the person can actually sign in, because the failure could be on the service provider's side of the trust relationship instead, an audience restriction or an access URL that the identity provider's own test would never see. The honest verification step is watching the affected user complete a real sign-in on the specific application that was failing, not trusting a green result from one side of a two-sided trust.
"Fixed" is not an answer. A check identifier and an artifact is.
The habit I most wanted this lab to reinforce in myself is a documentation habit, not a modeling one. Closing a ticket with "SSO was broken, it's fixed now" is not traceable to anything. It gives the next person who touches this application, or the same person the next time it happens, nothing to go on. Closing it with the specific check identifier that failed, the artifact that proved it, and the change that resolved that exact condition means the next occurrence of "SSO is broken" for this application starts from a known list of what it was not, instead of starting from zero.
That is the same discipline the rest of my support casework tries to hold to: name the check, not the vibe. It is also why this project belongs in a support portfolio and not only a security one. The identity provider here is a self-hosted Keycloak instance, not Entra, Okta, or Ping, and the customer narratives are authored text standing in for real tickets, not real conversations I have had. What is real is the standard: work the checks in order, cite the one that actually failed, and never publish a guess.
The full eight-stage pipeline behind this, intake through publish, is written up as a workflow lab on my casework page, in the same register as the rest of that page's documented problems. The architecture, the numbers, and the code sit on the Assertion Desk project page.
What this lab does not claim
Every case in this project runs against a self-hosted Keycloak identity provider with deliberately injected faults, not a production IdP, and every customer narrative is authored text, not a real support conversation.
- No claim of Entra, Okta, or Ping production experience is made anywhere in this project. Any vendor-specific behavior named in it is, at most, modeled from public documentation.
- There is no live customer intake in this build. A demo endpoint wraps one of 55 frozen, pre-built cases; there is no path yet for a customer to upload a real artifact bundle or reply to a request for more evidence.
- The published disposition accuracy is 94.5 percent across 55 cases, and 3 of the misses each have a specific, written structural reason rather than being unexplained noise. It is not a claim that every SAML failure resolves cleanly.
- This is a demonstration of a method, careful verification before a model speaks, and a gate that can refuse a fluent answer, not a production help desk tool.
Run the recorded case yourself
Python 3.11 or newer. No API key or live IdP needed to replay what was already recorded.
git clone https://github.com/RasheedFarhat/assertion-desk.git
cd assertion-desk
python3 -m venv .venv
.venv/bin/python3 -m pip install -r requirements.txt
make eval-replay
make ground runs pytest tests/ground/ -v, which exercises the grounding gate directly against a root cause pointing at an unverified gap, the same rejection kind that caught the missing_nameid draft in this article, without needing the full corpus run.