Authentication
People sign in with an email and a password, with the server’s OpenID Connect provider, or with their organization’s own. Single sign-on is free here, and stays free.
Who may make an account
Section titled “Who may make an account”By default (SIGNUP=invite):
- the first account on a fresh server, which is the admin of everything: with
SETUP_TOKENset, only with that token - anyone holding an invitation link, which an admin makes on Team
- anyone the OIDC provider vouches for, who arrives with no access until an admin gives them some
- anyone at an organization’s verified email domain its own provider vouches for, who joins that organization able to read one project
SIGNUP=open lets anyone make an account from the sign-in page. Each new
account gets an organization of its own, with a first project, and is its
admin. Invitations still work: someone signing up from one joins where it
points instead.
A check against scripted sign-ups
Section titled “A check against scripted sign-ups”With sign-up open, making an account with a password, and sending its email code, can take a check that a script can’t pass for free. Two kinds:
-
Cloudflare Turnstile, at
APP_URL’s address. Set a Turnstile widget’s keys, withAPP_URL’s host among its hostnames:Terminal window TURNSTILE_SITE_KEY=0x4AAAAAAA...TURNSTILE_SECRET_KEY=0x4AAAAAAA...The sign-up form (first run and invitations included) shows the widget above its button, and the server verifies its token with Cloudflare before the account is made.
-
A proof of work, built in: the browser spends a second or so finding a number the server asked for (the ALTCHA protocol), while the form is filled in, with “Checking you’re not a bot” under it until it is done. Nothing third party, nothing loaded from elsewhere, so it works on any domain.
Which one a sign-up takes depends on where the app is open:
APP_URL |
An organization’s own domain for the app | |
|---|---|---|
TURNSTILE_* set |
Turnstile | proof of work |
SIGNUP_CAPTCHA=pow alone |
proof of work | proof of work |
| neither | no check | no check |
A Turnstile widget only runs on the hostnames it lists, and organizations add
their domains as they go: they get the proof of work instead. Sending a new
email code takes the same check, out of sight unless Turnstile asks for a
click. Signing in, resetting a password, and signing up through Google, the
OIDC provider or an organization’s own are not checked. GET /api/v1/me
says which check the host asked takes (auth.captcha).
Email domains
Section titled “Email domains”An organization proves the domains its people have their email at in Settings,
Email domains (/api/v1/email-domains): a TXT record at
_artbucket-challenge.acme.com holding the token shown, then Check now. One
organization holds a domain; a claim left unproved for seven days can be taken
by another. Public suffixes (co.uk, github.io) are refused. These are not
the custom domains the app and portals answer at, and no limit counts them.
Keep the record: the server looks again every few hours, and a domain whose
record is gone at every look for 7 days is unverified, for single sign-on and
joining too, until it is proved again; another organization may then claim it.
Joining by domain
Section titled “Joining by domain”A proved domain can be opened: “Anyone at acme.com can join, able to read”. Whoever then signs up, or signs in, with an address at exactly that domain is offered to join the organization once the email code has confirmed the address: on the welcome page, beside starting an organization of their own, or in a banner across the app for an account made before. Not now is kept.
Joining is at read on one project, the domain’s landing project, never the
whole organization: an agency with a project per client that opens a
client’s domain lets that client’s people into their own project and no
other. An organization with more than one project picks it beside the
domain, once it is open (projectId in
PATCH /api/v1/email-domains/{domain}); unpicked, or once the project is
deleted, it is the organization’s oldest. Moving it moves where the next
person lands, not anyone who joined already. People who joined the whole
organization before this keep that access until an admin changes it in Team.
With sign-up closed, such an address may still make an account: the offer is
what it is for.
It is refused for domains that give addresses to the public (gmail.com,
orange.fr, comcast.net: some 14,000 of them), for a domain single sign-on
covers, whose people join through the provider, and on a server without its
own email (EMAIL_*), where nothing confirms the address. Joining by domain
doesn’t remove anyone who leaves the company: an admin does, or requires
single sign-on.
Single sign-on
Section titled “Single sign-on”An organization’s own
Section titled “An organization’s own”Each organization can bring its own provider in Settings, Single sign-on
(/api/v1/sso), with no restart and nothing in the environment. This is the
one for a server shared by several organizations. An organization admin:
- registers a web app with the provider (Okta, Entra ID, Google Workspace, or
any OpenID Connect one), with the redirect URIs the page shows:
{APP_URL}/api/auth/sso/callback/{organization id}, and the same path on each domain the organization uses for the app (Settings, Domains), since signing in there comes back there - saves its issuer URL, client ID and client secret, and the email domain its
people use, e.g.
acme.com. The endpoints are read from the issuer’s discovery document then, and must be public https addresses - proves the domain: a TXT record at
_artbucket-challenge.acme.comholding the token shown, then Check now. The domain is one of the organization’s email domains: one proved there already is proved here
From then on the sign-in page takes anyone at that domain, or under it, to
the provider once they type their email. The first time, they join the
organization able to read one project, the provider’s landing project
(picked beside the email domain when there is more than one, projectId in
PUT /api/v1/sso; else the oldest), as joining by domain does; an admin
raises their access in Team. An account
made before, with a password, joins at its next sign-in through the provider.
Signing up with a password at the domain is not on offer: the sign-in page
takes the address to the provider instead, so nobody there ends up in an
organization of their own by mistake.
The provider vouches for its own domain only: an address anywhere else is
refused. One domain belongs to one organization; a claim left unproved for
seven days can be taken by another. Turning it off keeps everyone’s account
and access: they sign in with a password, reset by email.
Requiring it
Section titled “Requiring it”Once the domain is proved, Require single sign-on (PATCH /api/v1/sso with
{"required": true}) holds everyone at the domain to the provider: password
sign-in sends them there, and no password reset goes out, so leaving the
provider means leaving Artbucket. Requiring it signs out everyone at the domain
at once. The organization’s admins are the exception: they keep their password,
so a provider that breaks later (a rotated secret, a changed app) never locks
the organization out of fixing it. Members at other domains, an agency’s or a
freelancer’s, are never held to it.
The server’s
Section titled “The server’s”For a server with one company on it, set all three, for Okta, Entra ID, Google Project, Keycloak, Authentik or any other OpenID Connect provider, internal ones included:
OIDC_ISSUER=https://accounts.example.comOIDC_CLIENT_ID=...OIDC_CLIENT_SECRET=...OIDC_NAME="Acme SSO"Register {APP_URL}/api/auth/callback/oidc as the redirect URI with the
provider, and /api/auth/callback/oidc on any organization’s domain used for
the app where people sign in with it.
Off by default. Email sends invitations, share links and password resets (“Forgot your password?” appears once some email can go out). Turn it on for the whole server:
EMAIL_PROVIDER=resendEMAIL_FROM="Acme Assets <assets@acme.example>"EMAIL_API_KEY=re_...Set this way, email is the server’s alone: every organization sends through
it, none can change the sender, the key or the provider, and Settings, Email sending
is not offered (PATCH /api/v1/settings/email answers 403). A hosted server
sends from its own domain with its own account, and no organization borrows it
to send as someone else.
Without it, each organization can turn on its own in Settings, Email.
Providers are HTTP APIs, no SMTP: resend, postmark, sendgrid, and
console, which prints to the log. A message that fails never fails what sent
it: the link is still shown, and the failure is in the audit log.
Confirming addresses
Section titled “Confirming addresses”With the server’s email set, a new account proves its address before it signs in: signing up mails a six-digit code, good for ten minutes and five tries, and the account signs in once it is entered. An account that never confirmed gets a fresh code the next time it signs in with its password, so accounts made before this confirm once too. Single sign-on needs no code: the provider vouched for the address. Without the server’s email there is nothing to send a code from before an account has an organization, so none is asked.