The broker profile seam
StubID serves one broker today and is meant to serve several. The seam is where a broker's personality goes, and the risk it manages is that an abstraction designed around one example is usually wrong.
So it was designed against two: Signaturgruppen Broker, which is recorded and implemented, and
Idura, which is not implemented at all. Idura's route table is declared and served in
src/StubId.Profiles.Idura for one reason — to find out whether the seam can express a second
broker before anything is built on it.
What is behind the seam so far
Only the route table. A profile declares patterns relative to the tenant root, the methods each answers, how strictly each path is matched, and the handler.
That is deliberately less than the design allows. Claim composition, error envelopes, key rosters and the request grammar are still engine code, and stay there until a second profile actually needs them to differ. Each has exactly one implementation today, and the recordings that would justify a second shape do not exist. Moving them now would be guessing with extra steps.
What Idura forced that one broker never would
| Nets eID Broker | Idura | |
|---|---|---|
| Issuer | ends in /op |
the bare host, no path |
| Routes | under /op/... |
at the host root |
| Dynamic segment | none | base64 of an acr value, before .well-known |
| Path matching | first segment ordinal, rest not, trailing slash refused | case-insensitive, trailing slash tolerated |
| Configuration probe | none | status depends on the query string |
Four consequences, each of which would have been wrong if the seam had been cut around one broker:
Routes are relative, never absolute. Nets eID Broker's op is the first segment of its own
pattern, not a mount prefix. That is what lets a document served from under a path segment
declare an issuer without one, which is exactly what Idura's acr-scoped discovery does.
The dynamic segment applies to two routes, not all of them. Idura answers 404 for the same segment in front of its key set and its token endpoint. A stub that served them there would pass a client the real broker refuses — the same false pass this project exists to prevent, wearing different clothes.
The segment is standard base64, not base64url. Load-bearing rather than pedantic: - and
_ are not standard-base64 characters, which is what stops a root-mounted tenant's dynamic
first segment from ever swallowing StubID's own /_stubid/… surface.
Path strictness is per profile. Both brokers are right about themselves. Being stricter than a broker fails a client that works against it, which is the same class of error as being looser, pointing the other way. StubID got this wrong in the other direction first, and a test locked the mistake in until it was probed rather than assumed.
Collisions stop the boot
Two profiles claiming one path do not fail fast on their own. The matcher is built lazily on the first request, so the application starts happily and then throws on every request afterwards; the compiler's duplicate-route analyzer cannot help either, because it only sees literal registrations in source.
The route set is therefore scanned as it loads. The check is conservative rather than a proof: two parameters with different policies are allowed to share a position, because rejecting that would reject the Idura route the seam was built for, and two policies that some single value satisfies remain ambiguous at request time.