Nets eID Broker: what the tokens carry
Everything here comes from recordings of real logins against pre-production, not from the broker's documentation. Where the two disagree the recording wins, and they disagreed often.
Member order is part of the contract, so the tables are in the order the broker sends.
id_token
| Member | Type | Notes |
|---|---|---|
iss |
string | |
nbf |
number | Not in the broker's claim tables |
iat |
number | |
exp |
number | 300 seconds after iat |
aud |
string | the client id |
amr |
array | bare values, not mitid.-prefixed |
nonce |
string | when the request carried one |
at_hash or c_hash |
string | one slot, see below |
sid |
string | Not in the broker's claim tables. Same value as neb_sid |
sub |
string | scoped to the organization, not the client |
auth_time |
number | a string in the userinfo token and in the transaction token |
idp |
string | mitid |
acr |
string | documented as belonging to the transaction token alone; it is here too |
neb_sid |
string | |
loa, aal, ial |
string | this order and the userinfo response; the two tokens differ |
identity_type |
string | private |
transaction_id |
string | |
idp_transaction_id |
string | undocumented. Is the mitid.transaction_id below |
session_expiry |
string | a unix timestamp, as a string |
idtoken_type |
string | undocumented. strict |
subject_type |
string | undocumented. org_mapped |
idp_environment is documented as an id_token claim and is not sent.
The hash slot
At most one, always after nonce:
at_hashin the back-channel token, over the access token.c_hashin the front-channel token of a hybrid response, over the code.- neither in a front-channel token from
response_type=id_tokenalone, because there is no access token and no code to cover.
ASP.NET Core rejects a front-channel id_token whose c_hash is missing or wrong.
The subject
Scoped to the organization. Two clients belonging to one service provider receive the same
subject for the same person, while mitid.uuid is the same everywhere. The id_token names
this itself: subject_type: org_mapped.
This matters more than it looks. Deriving a subject per client hands an application that signs users in through two of its own clients two different people.
userinfo
Every value is a JSON string, the age and both flags included. sub comes last.
session_is_active, session_expiry, idp, subject_type, idp_identity_id, loa, aal,
ial, mitid.transaction_id, mitid.uuid, mitid.age, mitid.date_of_birth,
mitid.has_cpr, mitid.identity_name, (scope-dependent members), mitid.psd2,
mitid.geo_ip_distance_km, (consent members), sub.
The documented session_status and session_identifier do not exist. mitid.psd2 and
mitid.geo_ip_distance_km are undocumented and always present.
Scope-dependent: dk.cpr (ssn), nemid.pid and nemid.pid_status (nemid.pid),
ssn.details.status (ssn.details_*), and mitid.cpr_consent_text /
mitid.cpr_consent_header after the address members.
userinfo token
Returned in the token response when the client has the setting enabled — not because a scope
asks for it. A recording with only openid mitid carried one.
Its header says at+jwt rather than JWT, its assurance claims come in a different order
from the id_token's, and its auth_time is a string where the id_token sends a number.
Same broker, same response, two answers.
transaction token
Returned when the request asked for the transaction_token scope. Signed by a different
key from the other three tokens of the same response: CN=NEB Transact PP, kid
7FF447FA0FB65A7E749E8B43AC635862381F0CC3, published in the same JWKS as the rest.
A transaction_token_ocsp_resp is always beside it in the body: a DER OCSP response in
standard base64, where every other encoded value here is base64url, signed with ECDSA on
P-256 where the token beside it is RS256. Its single answer names that same certificate and
says good. The certificate chain has the rest.
Three recordings: a login, a login that matched a CPR, and a transaction signing. Members that only some of them carry say so.
| Member | Type | Notes |
|---|---|---|
mitid.transaction_id |
string | the id_token's idp_transaction_id |
mitid.uuid |
string | |
mitid.age |
string | |
mitid.date_of_birth |
string | |
mitid.has_cpr |
string | |
mitid.identity_name |
string | |
amr |
string | an array in the other three tokens of the same response |
loa, ial, aal |
string | this order, as in the userinfo token |
identity_type |
string | underscored. private |
idp_identity_id |
string | |
idp |
string | |
dk.cpr, nemid.pid, nemid.pid_status |
string | scope-dependent |
acr |
string | |
ssn.details.status |
string | scope-dependent |
auth_time |
string | a number in the id_token |
sub |
string | |
transaction_id |
string | |
redirect_uri |
string | where recipient_info is documented |
nonce |
string | |
requested_scope |
string | |
mitid.psd2 |
string | the string "false" |
mitid.reference_text |
string | reference-text flow only, and one slot earlier |
mitid.geo_ip_distance_km |
string | |
| the transaction text | string | six members, below |
mitid.cpr_consent_text, mitid.cpr_consent_header |
string | scope-dependent |
transaction_actions |
string or array | below |
transaction_client_ip |
string | |
nbf |
number | equal to iat, which the broker sends after exp rather than beside it |
exp |
number | iat plus 189 388 800 seconds, which is six years |
iat |
number | |
iss |
string | |
aud |
string | the client id |
spec_ver and recipient_info are documented and not sent. Neither is
signing_cert_ocsp_nonce, on a login or on a signing transaction.
The transaction text arrives under both spellings
One recording sent one, inside a signed request object, which is the only way this broker takes it. Each value comes back twice, prefixed and unprefixed, underscored in both:
| Value | ||
|---|---|---|
mitid.transaction_text |
transaction_text |
the base64 that was sent, not decoded |
mitid.transaction_text_type |
transaction_text_type |
what was asked for |
mitid.transaction_text_sha256 |
transaction_text_sha256 |
below |
mitid.transactiontext does not exist. Six members carry three values, so a client matching
either spelling works and one matching both sees each value twice.
Six member names in the token, and none among the flow values MitID held for the same transaction — those carried the service provider's name, a reference text and a reference text header, not this text. The broker rendered it on its own page instead: what the screens showed.
The digest is over the decoded text rather than the base64 that was sent, and it is base64 of the hash — standard alphabet, padded — not hex.
The userinfo endpoint splits them. mitid.transaction_text_type and
mitid.transaction_text_sha256 are there; mitid.transaction_text is not, so that endpoint
hands over a digest without the text it is over. A reference text comes back whole there, as
mitid.reference_text, with no type and no digest beside it.
transaction_actions changes type
"mitid.login" on the login-only recording. ["mitid.login", "mitid.cpr_match"] and
["mitid.login", "mitid.transaction_signing"] on the two that did something as well. A client
reading this member has to handle a string and an array.
The authorization response
| Flow | Parameters, in order |
|---|---|
| code | code, state, session_state, iss |
| hybrid | code, id_token, state, session_state |
| implicit | id_token, state, session_state |
iss appears only when no id_token is returned. The id_token already carries the issuer,
so the broker omits the parameter, even though discovery advertises support for it.