Signing in from your own application

The other guides put StubID inside a test suite. This one is the shortest path from nothing to a MitID login you can look at: an application that starts, a browser, and a page of claims at the end of it. What you sign in to is a stock ASP.NET Core application that names StubID in three lines.

You need Docker, the .NET SDK, and the development certificate most .NET machines already have (dotnet dev-certs https --trust). Then:

docker run -d --name stubid -p 8080:8080 -p 8443:8443 -v stubid-keys:/keys \
  -e StubId__Tls=self-signed -e StubId__PublicBaseUrl=https://localhost:8443 \
  ghcr.io/benne/stubid

cd samples/aspnetcore && dotnet run

Open https://localhost:5099, follow the sign-in link, and you are looking at what MitID would have told your application about the person who signed in. Nobody approves anything: a fresh instance already has a citizen, and it approves by default.

That sample is not decoration. The_sample_signs_a_citizen_in in tests/StubId.Testing.Tests/SampleApplicationTests.cs hosts that same Program.cs - not a copy of it - against a container, on every change. A copied example that has quietly stopped working is worse than no example.

The test beside it matters as much. A login that succeeds proves nothing about the certificate handling below, because a sample that trusted every certificate would pass it too. That was measured rather than assumed: replacing the check with one that accepts anything leaves the login green, and only The_sample_refuses_a_certificate_it_did_not_pin notices.

The three lines that name StubID

Two of them are the authority and the client, which is the whole adoption claim: an application changes its authority and its credentials, and nothing else.

The third is a certificate. StubID generates its own, so nothing on your machine vouches for it, and the .NET handler will not fetch metadata over https it cannot verify. The sample reads the certificate from the plain-HTTP control port - a bootstrap that needs no trust, because nothing has to be believed in order to fetch it - and tells its back channel to accept that one certificate and no other.

What it deliberately does not do is turn RequireHttpsMetadata off. That is the usual advice for testing against a stub, and it is one copied line away from a production client that accepts an unsecured metadata document. The line is never written here, so it cannot be copied.

In your own project, the project reference in StubId.Sample.AspNetCore.csproj is dotnet add package StubId.Client.

Why the claims come from userinfo

The id_token this broker issues says idtoken_type: strict, and carries no mitid claim at all: no name, no CPR flag, no date of birth. Those are on the userinfo endpoint. So the sample sets GetClaimsFromUserInfoEndpoint, and against the real broker you would need it for the same reason. What the two responses each carry is in the claims reference.

That alone still shows you nothing, because the handler keeps only the claims it has a mapping for and it has never heard of any claim this broker names. ClaimActions.MapAll() is what puts them on the page. It also stops the handler discarding the protocol claims it normally drops, so iss, exp and at_hash end up in your authentication cookie as well - fine for a page whose whole job is to show what arrived, and worth narrowing in an application that needs three claims and a smaller cookie.

The client is one of three, and you cannot bring your own

StubID registers three clients, in one organization, and refuses any other client_id outright. The sample uses the one that asks for a code. They are the same three every guide here uses, and the secret is not checked - StubID accepts any, which is the same trade as not verifying an id_token_hint it did not issue.

The browser will warn you once

The authority is https://localhost:8443, secured by a certificate StubID generated a moment ago, so the browser will say so before it lets you through. Accepting it once is enough for a look around. Making it stop happening - per browser, per stack, and on the operating system - is its own guide, and three of those recipes run in CI on every change.

Watching a login be decided instead

Approving by default is what a test wants and not what a demonstration wants. Start the container with -e StubId__ApproveAutomatically=false and the login parks instead: the browser lands on StubID's own page, and nothing continues until somebody approves or aborts there. That page is deliberately StubID's own, with no MitID logo on it.

Abort it once, because the refused path is the one applications get wrong. The browser comes back to the application with error=access_denied and error_description=mitid_user_aborted, which is the pair the real broker sends, and the sample renders both. The second one is the broker's own naming and the thing worth logging, so a client that answers a refusal with a bare status code has thrown away the only part that says what happened.

Nothing about that is StubID-specific. ASP.NET Core already separates the two: a refusal arrives at OnAccessDenied, and a genuine fault - a correlation cookie that did not survive, a token that failed validation - arrives at OnRemoteFailure. The sample answers them differently for the same reason a real application would, because somebody aborting is an outcome and not an error.

A test would decide the same login through the control API without a browser at all, and both go through the same store rather than two implementations that agree until one changes. How a login is decided, and how to ask why it went the way it did, is in its own guide.

There is also a page for watching it happen. http://localhost:8080/_stubid/admin lists the logins this instance has as they arrive, decides them, and says why each one went the way it did — watching and steering an instance is that page explained.

Signing out

The sign-out link ends both sessions: the application's cookie, and the session inside StubID, which is what an id_token_hint is for. The browser comes back to the application afterwards, and asking for the protected page again starts a fresh login.

Node, with openid-client

samples/node/signin.mjs is a complete sign-in with openid-client - discovery, PKCE, the token exchange, and userinfo - and it runs in CI on every change, over plain HTTP and again over TLS with nothing relaxed. It is a starting point to copy from and a check at the same time; the assertions in it are the half a sample would not have.

It wants its own instance, without TLS, so stop the secured one first:

docker rm -f stubid
docker run -d --name stubid -p 8080:8080 ghcr.io/benne/stubid

cd samples/node && npm install && node signin.mjs

A second instance rather than the first one, because there is exactly one issuer and it names the address the instance was told about. The instance above publishes https://localhost:8443/op and answers on 8080 as well - plain HTTP never stops - but everything it renders still names the secured port. openid-client compares the issuer it discovers against the authority it was configured with, character for character, so pointed at 8080 on that instance it refuses. Finding that refusal is most of why the Node check exists. Reaching the secured port properly needs the certificate, which the certificates guide covers for Node.

Java, with Spring Security

tests/interop-spring resolves StubID's metadata the way a Spring application does, and asserts that the issuer and all three endpoints come back right. It does not sign in, and there is no Spring sample to run: what it covers is the part Spring is strictest about, which is deriving candidate metadata locations from an issuer that carries a path segment and then checking the issuer it finds. Everything after that is ordinary OAuth that Spring does the same way against any provider.

A browser driving it

Driving the front channel from a real browser - three engines, each refusing the certificate first and then trusting it by its own mechanism - is its own guide, and the script it explains is what runs in CI.

Or from a test suite instead

Once you have seen it work, the useful thing is to have it in tests. Running StubID from a test suite is in the Testcontainers guide for the container, and in the in-process guide for a host inside the test process, which starts in about 150 milliseconds and needs no Docker at all.