# Signing people in with the Praecise account

`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](tutorial.md) builds it
step by step with working code; machines, agents and assistants are in
[agents.md](agents.md); tokens for an API, and the shared services, are in
[api.md](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:

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