Skip to content

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.

By default (SIGNUP=invite):

  • the first account on a fresh server, which is the admin of everything: with SETUP_TOKEN set, 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.

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, with APP_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).

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.

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.

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:

  1. 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
  2. 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
  3. proves the domain: a TXT record at _artbucket-challenge.acme.com holding 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.

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.

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:

Terminal window
OIDC_ISSUER=https://accounts.example.com
OIDC_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:

Terminal window
EMAIL_PROVIDER=resend
EMAIL_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.

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.