Discovery
https://login.sajn.se and it will find everything below.
Endpoints
What we support
Deliberately absent: refresh tokens (a login mints claims; your app owns the session),
an end-session endpoint (there is no sajn-side session to end), and implicit/hybrid
flows.
Authorization request
GET https://login.sajn.se/oidc/authorize
Where errors go
This distinction matters when you are debugging:- Before
redirect_uriis verified to belong to the client (unknownclient_id, unregistered URI), the request fails with a 400 in the browser. Nothing is redirected — otherwise the endpoint would be an open redirector. - After it is verified, errors go back to your
redirect_urias?error=…&error_description=…&state=…, per RFC 6749.
Token request
POST https://login.sajn.se/oidc/token — application/x-www-form-urlencoded
Response
Tokens
id_token
An ES256 JWT carrying the scope-projected claims. This is the product — validate it and read it.iss, aud, exp, and nonce if you sent one.
access_token
Opaque, not a JWT. It exists only to call/oidc/userinfo and lives ~10 minutes.
It is deliberately opaque. Under the
nin scope the claims include a personnummer, and a
self-contained token would carry that into your logs and every proxy in between. Because it is
server-backed we can also revoke it, which a JWT could not offer.UserInfo
id_token — the projection is shared, so no channel reveals more
than another. An expired or unknown token returns 401 with a WWW-Authenticate header.
Most integrations never need this: the id_token already contains everything.
JWKS and key rotation
kid. Rotation is additive: a new key is published alongside
the previous one, which stays until the last token signed with it has expired. Nothing breaks
mid-rotation.
Use a library that caches JWKS and refetches on an unknown kid (createRemoteJWKSet in
jose, or your framework’s equivalent) rather than pinning a key.
Security model
PKCE is mandatory for every client
PKCE is mandatory for every client
Including confidential ones. It costs an integrator nothing and removes authorization-code
interception as a class of attack. There is no public/PKCE-only client type — every client
has a secret and uses PKCE.
Redirect URIs match strictly
Redirect URIs match strictly
https URIs match exactly — no subpaths, no trailing-slash leniency. An open redirect
anywhere under your domain would otherwise let someone else receive your codes.Custom schemes for native apps (acme://auth) match on a path boundary, so acme://auth/done
is accepted but acme://authelse is not. Fragments are rejected everywhere.The completion redirect is pinned
The completion redirect is pinned
redirect_uri is validated when the login starts and stored server-side. The final redirect
uses that stored value, never a request parameter — so a login in flight cannot be steered
somewhere else.A leaked login URL is worthless
A leaked login URL is worthless
Login URLs are single-use, expire in 15 minutes, and contain nothing about the user. The worst
an attacker with one can do is authenticate as themselves — your server then receives a
different
sub, and the code is useless without your client secret and PKCE verifier.
