Overview
The shape of the traffic rather than the traffic itself. Everything below the totals covers the last 30 days, counted from the sign-in trail — which outlives the rows it describes, because auth_event keeps person_id without a foreign key.
Sign-ins per day
Two numbers per day, and the gap between them is the useful part: one person signing in nine times is nine sign-ins and one person.
Directories
| Directory | Sign-ins | People | Tenant |
|---|
Doors, and what each client took
| Door | Sign-ins | People |
|---|
| Client | Assertions | People |
|---|
Why sign-ins were refused
A refusal is not an incident. Every Google account on earth reaches that door, and a directory we have never heard of knocking is the allowlist doing its job.
| Reason | Count |
|---|
Assurance of sessions opened
Tier 3 means MFA was proven in the token at that sign-in. Google never reaches it, because amr does not arrive from there — so this is also a picture of which door people use. Sessions are deleted with their person, so this counts what is still on record.
| Tier | Sessions |
|---|
Sign-in trail
Every attempt, refused ones included. The page a refused person sees says only that it did not work — the reason lives here and nowhere else.
| When | Event | Outcome | Door | Reason | IP |
|---|
Microsoft directories
One multi-tenant app registration serves every company, so this list is the only thing standing between Tribune and every Microsoft directory on earth. A directory that is refused records itself here, which is why most rows arrive without anyone typing them. Enabling one lets everybody in that directory sign in.
| State | Name | Tenant id | Domain seen | Knocks | Identities | Last seen | Decided by |
|---|
Two ways in. A company on Google Workspace is let in by its domain, the way a Microsoft directory is. Everybody else is invited one address at a time.
Workspace domains
Matched on the domain Google itself puts in the sign-in — present only on accounts that company's Workspace manages. A private Google account registered on the same address carries none and is still refused, so this is safe for a Workspace domain and does nothing for a company on Microsoft 365.
| State | Domain | People | Knocks | Last seen | Note | Decided by |
|---|
Invited addresses
| State | Address | Name | Knocks | Last seen | Note | Invited by |
|---|
Guests
The guest door: a six-digit code mailed to a private address. For people with no work account anywhere. Spobik's Academy arrived on 14 private domains, five of which host mail and nothing else, so no federated door would ever have reached them.
A row here is the invitation. Unlike the two tabs before it, nothing fills this table by knocking: an address that is not on it is told nothing at all when it tries, so there would be no knock to show. Everything here was typed by somebody.
| State | Address | Name | Sign-ins | Last code | Last used | Invited by |
|---|
Invite
One address or a list. Paste a column straight out of a spreadsheet,
separated by commas, spaces or newlines. A name is optional and goes
in front of the address as Lian Åberg <lian@gmail.com>,
one person per line — mixing named lines and bare addresses in the
same box is fine. Everybody new is mailed to say the door is open;
nobody already invited is mailed again.
People
One row per human — or two, when the same human has used both doors. Identities are never linked on a matching address, because that is exactly how an account is taken over by someone who registered the address first.
Clients
The services Tribune signs people in to. Read here, changed in
code. A client's redirect URIs decide where a usable sign-in
code may be sent, so every change is a line in
functions/_lib/clients.ts that CI tests and a revert
can undo — not something a console session can write.
Adding a client
-
Get two things from the service. The exact URL it wants
people sent back to — https, the full path, no query
string; it is matched character for character. And, for a
platform we do not write, which way it can authenticate at a
token endpoint: a signed JWT (
private_key_jwt), a client secret, or neither. -
Add an entry to
CLIENTSin functions/_lib/clients.ts, with a comment saying who it is, who asked, and the date. Pick the template below that fits. The id is permanent: it is theclient_id, the token's audience, and the name in the sign-in trail. -
Make its credential, if it needs one. Run it in the
repository; the private half never goes into it.
- Client secret:
node scripts/gen-client-secret.mjs <id>— the digest goes in the entry, the secret goes to the service's administrator once, by a channel that is not ordinary mail. - Signed JWT:
node scripts/gen-client-key.mjs <id>— the public key goes inkeys, the private value is the service's own secret.
- Client secret:
-
Commit and push to
main. CI runs the tests and deploys; the client appears in the table below once the deploy is done. Nothing else needs touching — no migration, no setting. -
Hand over what the service needs. An OpenID Connect
platform needs the discovery URL
, itsclient_idand its secret if it has one. One of our own services uses the handoff instead — see “The contract with clients” in the README.
Before an outside platform goes live
- It links accounts on the address only when
email_verifiedis true. Otherwise somebody who registers a colleague's address first inherits their account. - It keeps a local administrator account of its own, so a Tribune outage costs it new sign-ins and not its administration.
- Somebody has decided whether it may create accounts on first sign-in. If it does, it is open to everybody Tribune signs in — Tribune does not keep a list per client.
Changing one
- A redirect URI: edit
returnTo. The old one stops working at the deploy, so if the service moves later, list both until it has. - A secret or key: add the new one beside the old, deploy, let the service switch, then remove the old and deploy again. Two entries are only ever a rotation in progress.
- Removing a client: delete the entry. Codes already in flight die within sixty seconds. Its history stays here, marked retired.
Templates
One of our own services (the handoff)
An outside platform with a client secret
An outside platform that signs a JWT
Registered
| Client | Doors | Redirect URIs | Authenticates with | Sign-ins | People | Refused | Last sign-in | Last refusal |
|---|
Notifications
What sends mail, and to whom. There is no subscriber list to edit: notifications go to the addresses behind the enabled rows in the admin table, so the way to change who is told is to appoint or retire an administrator. This screen shows the consequence rather than offering a second list that could disagree with it. The one mail that goes elsewhere is the answer to a Google request, which goes to the address that asked.
Transport
Recipients
Read from admin through identity, fresh on
every load. An administrator appointed before their first sign-in
has no identity yet, therefore no address, and is absent here —
which makes this the delivery list, not the roster.
| Address |
|---|
Events that send mail
| Event | To | When | Note |
|---|