Praecise account/ Guides

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.

Issuerhttps://accounts.praecise.com (compare exactly)
AuthorizationGET /auth
TokenPOST /token
KeysGET /jwks
UserinfoGET /me
End sessionGET /session/end
SigningRS256; 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.

ClaimScopeMeaning
subopenidThe account: a UUID that never changes. Bind your own record of the person to this, not to the address
emailemailThe account's address
email_verifiedemailtrue 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
nameprofileWhat the person calls themselves; absent until they set one
amropenidHow 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
acropenidphr when a passkey was verified in this sign-in; otherwise absent
auth_timeWhen the person last signed in or confirmed; present when the request carried max_age or prompt=login
nonceYours, unchanged
sidOnly 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 toAddress
"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 backMeans
codeThey are signed in at Praecise
error=login_requiredNobody is signed in in this browser (or max_age has passed)
any other errorSomething 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:

  1. the signature, against /jwks; the header's typ is logout+jwt;
  2. iss is the issuer and aud is your client id;
  3. events has the member http://schemas.openid.net/event/backchannel-logout;
  4. iat is recent and jti has not been seen;
  5. there is a sub and no nonce.

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 treats email_verified: false as 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.

This page as Markdown