Nets eID Broker: how it refuses things

There are four different ways this broker says no, and which one you get depends on the class of the problem rather than on the endpoint. Getting this wrong produces an emulator that passes every test a client library can express while sending your application somewhere it would never have gone.

An invalid request never reaches the client

A request the broker will not process does not redirect back to your redirect_uri. The browser goes to the broker's own error page with an opaque reference:

302 Location: https://pp.netseidbroker.dk/op/Error?errorId=CfDJ8GkI9WY4...

Your application sees nothing at all. No callback fires, no error handler runs, and the user is looking at a page you did not write. That is the behavior to reproduce, because "my callback never fired" is what the person debugging actually has to work with.

StubID emits a real protected payload for errorId, so it carries the same CfDJ8 prefix and round-trips through the same protector.

Recorded in CAP-008, CAP-009, CAP-011, CAP-040 and CAP-043.

A user-level failure does reach the client

If the request was fine and the login itself failed, the client is told:

?error=access_denied&error_description=mitid_user_aborted&state=...&session_state=...

error_description carries the broker's own error code, not a sentence. A test asserting on mitid_user_aborted needs that exact string. The response carries session_state and does not carry iss, even though discovery advertises authorization_response_iss_parameter_supported.

Recorded in CAP-023.

The token endpoint answers with a bare object

{"error":"invalid_client"}

No error_description, no error_uri, nothing else. Which code you get:

Situation Answer Recording
No parameters at all invalid_request CAP-042
Unknown client, or no secret invalid_client CAP-014
Unusable or already-redeemed code invalid_grant CAP-015
A grant the client may not use unauthorized_client CAP-016
PAR without client authentication invalid_client CAP-019
A request object that cannot be read (PAR) invalid_request_object CAP-046

Every one carries Cache-Control: no-store, no-cache, max-age=0.

The last row is the exception to "bare", and it is the reason to debug a signed request against PAR rather than authorize:

{"error":"invalid_request_object","error_description":"Invalid JWT request"}

400, and unlike every other row here it carries an error_description. That is why the PAR endpoint is the one to debug a signed request against: the authorize endpoint answers the same object with a 302 to /op/Error?errorId=… and says nothing, so every failure there looks the same.

CAP-046 sends a request parameter that is not a JWS at all, which is the case a client is most likely to produce. Three further causes earn the identical answer and are measured rather than recorded — a flipped signature byte, a random key, and a missing exp claim, the last being why a probe that omits exp fails its own negative control and reads as "signed requests do not work here". They stay a measurement because recording one means committing a request object signed HS256 with the client secret, and such a token is a known-plaintext HMAC tag over that secret: what the broker does with a signed request object.

StubID checks that the object can be read and not who signed it, so it answers this for a malformed object and accepts a forged one. See divergences.

StubID answers invalid_grant where the broker answers invalid_client for a wrong secret, because it does not check secrets. See divergences.

An unauthenticated call is challenged

Two endpoints on the same host, two different challenge strings, both real:

/op/connect/userinfo        WWW-Authenticate: Bearer realm="IdentityServer",error="invalid_token"
/op/api/v1/mitid/matchCpr   WWW-Authenticate: Bearer

Note the missing space after the comma in the first. Both answer 401 with an empty body.

Recorded in CAP-017 and CAP-018.

The endpoints that answer in their own shape

matchCpr is not an OAuth endpoint and does not use OAuth's error shape:

{"errorMessage":"Missing Cpr parameter"}

and after three attempts in one session:

{"errorMessage":"Cpr Match exceeded. Only 3 tries is allowed within a session."}

The first is recorded (CAP-021). The second is the broker's documented sentence — reaching it needs a fourth call inside one authenticated session, which no capture has.

A successful match answers {"cprNumberMatch":true}, with a JSON boolean. That typing comes from the pre-production swagger rather than a recording, and it is worth doubting: every value this broker returns from userinfo is a string, including two that are plainly booleans.

End session

Without a usable id_token_hint, post_logout_redirect_uri is ignored and the browser goes to /op/Account/Logout. A client that omits the hint never comes back, and there is no error to tell it why.

Recorded twice, in CAP-044 and CAP-045.