API

Sign in with Tokn

Standard OpenID Connect: authorization code with PKCE. If your framework already speaks OpenID Connect, point it at the discovery address and you are finished. Everything below is what it will find.

Discovery

Give your library this one address and it configures itself.

…

Before your first sign-in

Ask the owner of this Room to add your site. That is one record in the sites table, and it is what makes your site real to the provider:

FieldWhat it is
client_idThe name your site signs in as. Yours alone.
redirect_uriThe exact address a person is returned to. Several are allowed, separated by spaces. Anything not on that list is refused.
secretOptional. A site that keeps one is a confidential client; a browser-only site keeps none and uses PKCE alone.

A sign-in for a site nobody registered is refused before anything is recorded, because the attempt has no site to be recorded against. There is no way to write one in by trying.

The flow

1 · Send the person to the sign-in page

A normal browser redirect. code_challenge is the SHA-256 of a verifier you keep, base64url without padding.

…

2 · They come back with a code

To your redirect_uri, with code and the state you sent. Check the state matches before you go on.

3 · Exchange it for an ID token

Form-encoded, and the secret goes in the body rather than a header. A code can be exchanged once: the second attempt is refused and no token comes back.

…

Endpoints

Method and pathWhat it does
GET /api/.well-known/openid-configurationThe discovery document.
POST /api/authorizeGrants a code. Takes the request fields plus either a session or an email and password. Answers with the address to send the person back to.
POST /api/tokenExchanges a code for an ID token. Form-encoded.
GET /api/userinfoThe claims behind a token.
GET /api/jwksPresent, and deliberately empty: tokens are signed HS256 with your own secret.
POST /api/signup · POST /api/loginAccounts, for this provider's own pages.
GET /api/mine?session=…A person's own record: where they signed in, what the reviewer said, what is on hold.
GET /api/healthWhether the service is answering.

Tokens

WhatValue
SigningHS256, with your site's secret. A site with no secret uses the service secret. Tell your library HS256; it will not find a public key, and there is not meant to be one.
SubjectPairwise. The same person is a different sub at every site, so two sites cannot line their users up against each other.
Claimssub, email, email_verified, name, iss, aud, exp, iat, nonce
Scopesopenid, email, profile
Grantauthorization_code only, with S256 proof. No implicit flow, no password grant.

When it says no

Every refusal arrives as JSON with an error and a detail you can show a person. These are rules the Room holds, not checks this page performs.

StatusErrorWhat happened
400unknown_siteNo site is registered under that client id.
400redirect_mismatchThe return address is not one the site registered.
400invalid_grantNot a code, already used, or issued to a different site.
401refusedWrong email or password. The attempt is still recorded.
403heldThe security reviewer has this site on hold for this person, with a reason.
403suspendedThe site itself is suspended.

What is kept

Every sign-in, granted or refused, is a record with its time, the site, the outcome and the device. Nobody can alter or erase it, the owner of the Room included. A separate reviewer reads those records and may add an alert or a hold; it can write to those two tables and nowhere else, and lifting a hold is a new record pointing at the old one rather than a deletion. A person sees all of it about themselves on the home page.