Skip to main content
sajn Login is a standard OpenID Connect provider implementing Authorization Code flow with PKCE. Any conformant library works — you should rarely need this page, but here is everything it does.

Discovery

Point your library at the issuer https://login.sajn.se and it will find everything below.
The issuer is compared byte-for-byte when your library validates a token. It is https://login.sajn.se — no trailing slash, no per-customer subdomain.

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_uri is verified to belong to the client (unknown client_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_uri as ?error=…&error_description=…&state=…, per RFC 6749.

Token request

POST https://login.sajn.se/oidc/token — application/x-www-form-urlencoded
Response
The code is single-use with a 60-second lifetime, and it is consumed even by a failed exchange. An attacker who steals one gets no retries.

Tokens

id_token

An ES256 JWT carrying the scope-projected claims. This is the product — validate it and read it.
Validate: signature against JWKS, 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

Returns the same claims as the 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

Public ES256 keys, each with a 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

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.
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.
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.
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.