Skip to content

Authentication

Almost every endpoint requires a signed-in user. There is no anonymous read surface and no separate API-key system: a client authenticates as a person, with exactly the access that person has.

Two transports carry the same credential — a signed JWT. Which one you use depends on whether you are a browser.

Signing in

bash
curl -X POST https://api.kancil.nl/auth/login \
  -H 'Content-Type: application/json' \
  -d '{"username": "you", "password": "…"}'

A successful response carries the public user record plus the token:

json
{
  "username": "you",
  "role": "viewer",
  "access_token": "eyJhbGciOiJIUzI1NiIs…",
  "token_type": "bearer"
}

It also sets a kancil_session cookie: HttpOnly, SameSite=Lax, and Secure in any deployment worth the name. The token is valid for seven days.

Which one do I use?

In a browser, ignore access_token and let the cookie do the work — that is what the web app does, and an HttpOnly cookie cannot be read by page JavaScript, so an XSS bug cannot walk off with the session.

Everywhere else — scripts, native apps, integrations — store access_token and send it as a header. Cookies you cannot read are no use to you.

Sending the credential

Cookie, which any HTTP client with a cookie jar does for you:

bash
curl -c jar -X POST https://api.kancil.nl/auth/login -d '…'
curl -b jar https://api.kancil.nl/tasks

Or the header:

bash
curl -H 'Authorization: Bearer eyJhbGciOiJIUzI1NiIs…' \
  https://api.kancil.nl/tasks

If both are present the cookie wins — the header is only consulted when no session cookie is sent. Send neither and you get 401.

Multi-factor authentication

When the account has MFA enabled, POST /auth/login does not return a session. It returns an MFA challenge instead — as an mfa_token in the body, and as a short-lived kancil_mfa cookie for browsers. Send the code back to POST /auth/login/2fa along with that challenge to exchange it for a real session.

The challenge is valid for five minutes. It carries a distinct typ claim, so it cannot be replayed as a session token even though both are JWTs.

Roles

Two roles, in privilege order: viewer and admin. Endpoints tagged admin require the latter; everything else needs only a signed-in user. Self-registration, where enabled, always creates a viewer.

The role is not carried in the token — it is read from the database on every request, so a demotion takes effect immediately rather than at the user's next login.

Signing out

POST /auth/logout clears the cookie. A bearer token is not revocable: it stays valid until it expires, so treat a stored access_token with the same care as the password that produced it.

Kancil — one workspace for contacts, work and money.