Signing people in
The reference for products: the request, the ID token, passkeys, sign-out and logout tokens.
https://accounts.praecise.com
For whoever builds a Praecise product. Praecise is the one gate: invitation, sign-up, sign-in, the second step and sign-out exist here and nowhere else. A product has no sign-in page, no sign-up page, no invitation code and no request-an-invitation form of its own. It sends the person here and is told who they are; whoever holds a Praecise account and arrives at a product is in, and the product decides only what they may do there. This page is the reference for that; tutorial.md builds it step by step with working code; machines, agents and assistants are in agents.md; tokens for an API, and the shared services, are in api.md.
The service is an OpenID Connect provider. Everything below is also in its
metadata at /.well-known/openid-configuration, which a library can read by
itself.
| Issuer | https://accounts.praecise.com (compare exactly) |
| Authorization | GET /auth |
| Token | POST /token |
| Keys | GET /jwks |
| Userinfo | GET /me |
| End session | GET /session/end |
| Signing | RS256; each key in the JWKS has a kid |
Becoming a client
Clients are a fixed list in src/clients.js: adding a product is one entry
there (its id, its base address, the path the code is returned to, optionally
a path for logout tokens and a theme) and one secret. A client exists only
while its secret setting is present; the service reads its settings at start.
- The secret is 32 random bytes as base64url, the same value on both sides, set in each deployment's settings and nowhere else.
- The return address is the product's host plus the path in its entry,
compared exactly. The address after sign-out is the host plus
/, compared exactly, slash included. - More than one host.
<NAME>_CALLBACK_BASE(the secret's name without_SECRET) may list several hosts separated by commas, each as scheme and host only. Every listed host gets the return address and the sign-out address; the first listed is where logout tokens are sent. This is how a product lives on two hosts, or moves from one to another, as one client.
Signing in
Send the browser to:
GET https://accounts.praecise.com/auth
?client_id=<id>
&response_type=code
&redirect_uri=<the registered return address>
&scope=openid%20email%20profile
&state=<random, kept by you>
&code_challenge=<base64url(sha256(verifier))>
&code_challenge_method=S256
PKCE with S256 is compulsory for every client; a request without it is
refused. There is no implicit flow. nonce is optional and comes back
unchanged in the ID token.
The person signs in here if they are not already (password, then their
passkey if they hold one), and the browser returns to your address with
code, your state, and iss. Check state, then:
POST https://accounts.praecise.com/token
Authorization: Basic base64(<id>:<secret>)
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code&code=<code>&redirect_uri=<the same>&code_verifier=<verifier>
The answer holds id_token, an access_token for /me, and, when
offline_access was asked for, a refresh_token. Verify the ID token's
signature against /jwks, and its iss, aud (your client id, a single
string) and exp. Then make your own session. A code is good for ten minutes
and once.
What the ID token says
The claims are in the ID token itself; /me gives the same for whoever
prefers to ask.
| Claim | Scope | Meaning |
|---|---|---|
sub | openid | The account: a UUID that never changes. Bind your own record of the person to this, not to the address |
email | email | The account's address |
email_verified | email | true only when the person has shown they read that mailbox (a code at sign-up, at recovery, or confirmed once later). It can be false for an account made before sign-up sent a code. Do not link an existing record of yours to a Praecise identity by address when it is false; say in one sentence that the address has to be confirmed at https://accounts.praecise.com/account#email |
name | profile | What the person calls themselves; absent until they set one |
amr | openid | How they signed in, every method used: pwd, otp (a password set through the mailbox), and for a passkey hwk (bound to one device) or swk (synced), with user and mfa |
acr | openid | phr when a passkey was verified in this sign-in; otherwise absent |
auth_time | When the person last signed in or confirmed; present when the request carried max_age or prompt=login | |
nonce | Yours, unchanged | |
sid | Only for a client that asks to be told which session ended; none does today |
Lifetimes: the Praecise session thirty days; an ID token and an access token one hour; a refresh token is replaced as it is used and cannot be kept alive beyond a year.
One session, and no sign-in page of your own
Signed in to the Praecise account means signed in to every product. So when
somebody needs to be signed in and has no session of yours, send the browser
to /auth with the ordinary request above, at once, with no page of yours in
between. A person signed in at Praecise comes straight back with a code and
never sees a page; anybody else signs in, or makes their account, here and
then comes back. Make your own record of the person quietly on their first
arrival.
What keeps this from looping is your public page, not a sign-in page: after a
sign-out, or when the request comes back with an error (the person
declined), show the public page, which sends nobody anywhere by itself.
| What you link to | Address |
|---|---|
| "Sign in" / "Open" | your own protected address, which starts the ordinary request |
| "Create account" | https://accounts.praecise.com/signup?to=<your client id> (add &invite=<code> to fill the code in). The account is made here and the person lands back at your product. The sign-in page reached by the ordinary request has the same way in, so "Sign in" alone also serves |
| "Request an invitation" | no address yet: write the words without a link. It will be one page here, for every product |
| "Your account" | https://accounts.praecise.com/account |
Looking without asking (prompt=none) is the same request for when you
must not show Praecise's page: a public page that wants to say "Open" where
it would say "Sign in" (or use /session/check, below), or a check that your
session has not outlived the Praecise one.
| Comes back | Means |
|---|---|
code | They are signed in at Praecise |
error=login_required | Nobody is signed in in this browser (or max_age has passed) |
any other error | Something is wrong with the request; do not loop |
To keep a session of yours from outliving the Praecise one, either take logout tokens (below) or look again from time to time; do both if a missed sign-out would matter.
Asking for a passkey
A person who holds a passkey is asked for it after their password at every
new sign-in, and the ID token then has acr: "phr". A product that needs to
know the person is at their device now, before something sensitive, asks
again:
&acr_values=phr&max_age=0&prompt=login&nonce=<a value tied to the action>
They are asked for a passkey even though they are signed in; only a passkey
of the account already signed in is accepted; a person who holds none is led
through adding one first. Check in the ID token that comes back: acr is
phr, auth_time is recent, nonce is yours, sub is who you expect. With
prompt=none the answer is login_required unless the session already used
a passkey.
There is no sign-in by passkey alone, and a password alone never yields
acr: "phr".
Signing out
From your product. End your own session, then send the browser to:
GET https://accounts.praecise.com/session/end
?client_id=<id>
&post_logout_redirect_uri=<the registered address after sign-out>
The Praecise session ends, with no second question for our own clients, and the browser comes back to you. A person who signs out of one product has signed out of Praecise.
Being told of a sign-out made elsewhere (OpenID Connect Back-Channel Logout). A product that names a path for it in its entry is sent, server to server:
POST <your host><your path>
Content-Type: application/x-www-form-urlencoded
logout_token=<JWT>
when a session that had signed in to it ends: at /session/end from any
product, from the account page, or by "Sign out everywhere". Verify, in this
order, and refuse on any failure:
- the signature, against
/jwks; the header'stypislogout+jwt; issis the issuer andaudis your client id;eventshas the memberhttp://schemas.openid.net/event/backchannel-logout;iatis recent andjtihas not been seen;- there is a
suband nononce.
Then end every session you hold for that sub, and answer 200 or 204.
You are sent one for a sign-out you started yourself as well. Delivery is
tried once: a product that cannot be reached is named in the service's log and
keeps its session until it next looks.
What the person manages here, not in your product
https://accounts.praecise.com/account: their name, their address and its
confirmation (#email), their password, their passkeys and devices
(#passkeys), their sign-in history, and signing out everywhere. A product's
"Your account" links there for all of it and keeps only its own preferences.
Accounts are made by invitation, here: https://accounts.praecise.com/signup
asks for the code, and ?invite=<code> fills it in. A product has no
invitation to have an account and no check of invited addresses; what it may
have is membership (who belongs to which team or organisation), which decides
what a person may do, never whether they may sign in.
For a public page: is this browser signed in?
GET https://accounts.praecise.com/session/check, with credentials, from one
of our own origins (the list is in src/server.js), answers
{"signedIn": true|false} and nothing about who. It lets a public page say
"Open" where it would say "Sign in".
The sign-in pages in a product's own brand
A client that names a theme (src/themes.js) gets the same pages with its
mark, its wording and its tokens; the flow is not changed by it. Without one
the pages are Praecise's own.
Not a person in a browser
A machine with no browser (device grant), an agent acting alone (client credentials), an outside assistant that registers itself or is described by a metadata document, tokens bound to a key (DPoP), and access tokens for an API with claims from that API's own hook: all in api.md.
Before a product goes live
- Its own sign-in is gone only after Praecise sign-in is set up on live and a real member has come in through it.
- It binds to
sub, and treatsemail_verified: falseas above. - It has no sign-in, sign-up or invitation page: its protected addresses start the ordinary request, and its public page is where a refusal or a sign-out lands.
- Its sign-out goes through
/session/end. - It either takes logout tokens or looks again on a schedule.
Written and kept by Praecise account. Read from https://accounts.praecise.com/guides/sign-in.md.