Badger Sett Federation¶
Overview¶
Shop-operator ("admin") sign-in is moving to Badger Sett, Badger's own operator platform. Badger Commerce federates to it rather than being replaced by it:
- Badger Sett becomes an upstream OIDC identity provider.
auth.bdgr.co.ukremains the token issuer for the web and admin apps, and keeps owning tenant binding and per-tenant roles.- Clerk sits behind Badger Sett and never talks to Badger Commerce directly.
Shoppers and POS agents are not part of this. They keep signing in with a password, passkey, magic link or social login exactly as they do today.
Rollout is per shop¶
| Setting | Where | Default |
|---|---|---|
Badger Sett Federation Enabled (sett-federation-enabled) |
Admin → Site Configuration → auth-config |
Off |
Turning it on for a shop does two things:
- The auth portal shows Continue with Badger Sett on that shop's branded login page.
- That shop's operators are invited to link their Badger Sett account after they next sign in.
Nothing about how anyone signs in changes at the moment you flip it. The invitation is skippable and the old credentials keep working.
Who gets invited to migrate¶
An operator is a migration candidate when they hold an admin-level role
(MERCHANDISER, CONTENT_EDITOR, FULFILLER, FINANCE, ADMIN, SUPERADMIN) on at least one
shop that has sett-federation-enabled turned on.
POS_AGENTis not an admin-level role. POS agents are the shop's front-line staff, not Badger customers — they stay on Badger Commerce credentials permanently and are never invited.- Someone who holds both an admin role and
POS_AGENTdoes migrate, on the strength of the admin role. - Shoppers are never invited.
How linking works (and why it's done this way)¶
Linking happens in band, inside a single session:
- The operator signs in to
auth.bdgr.co.uknormally, with their password or passkey. This proves they own the Badger account. - On their way into the admin UI they are offered "Link your Badger Sett account".
- Accepting sends them out to Badger Sett and back. The returning identity is attached to the account they already authenticated as — not to whatever account happens to share the email address in the token.
That ordering is the security property. An account is never linked on the strength of a provider asserting an email address, because an account here carries every role its owner holds across every shop. The upstream email still has to be verified and has to match the Badger account's email, but that is a consistency check on top of the session proof, not the thing that selects the account.
Declining ("Not now") is offered once per session and changes nothing.
Naming the account the link is for¶
The hop out to Badger Sett carries login_hint — the address of the account the operator has just
proved they own.
Without it the request said no more than "sign somebody in", so a browser already holding a Badger Sett session for a different person — a second shop, a colleague's machine, the operator's own personal account — got that person asserted straight back, and the link was refused for a mismatch the operator had no way to fix. Badger Sett compares the hint against the live session and, when they disagree, stops and asks rather than answering with whoever is signed in.
The hint travels on this hop only. The ordinary Continue with Badger Sett button on the login page carries none, because nobody has authenticated yet and there is no account we could honestly name. It is advisory in any case: the email match on the way back in is still what actually holds.
If the operator has no Badger Sett account yet¶
Most won't, at the point their shop is flagged. They take the same route.
The link hop reaches Badger Sett's sign-in with the address already filled in, and the sign-up link
beside it carries the address and the destination onward — so the account they create is the one
this link is expecting, and finishing it drops them back into the authorization request they came
from. On the way back, sett-organisation-id puts them in the organisation that pays for their
shop, so a self-registered account does not sit outside it.
They can still type a different address at sign-up, and the link is then refused for a mismatch — correctly, since the account being joined is the one the session proved. The login page says so, and says which account to choose instead.
The organisation does not have to exist first. It is resolved on the first link — see below — so there is nothing to provision in advance and no order to get right.
Invitations remain the tidier route where you would rather choose the role up front: an invited operator arrives already inside the organisation, with a role somebody chose, rather than claiming a new one by being first.
The two flags, and why they behave differently¶
sett-federation-enabled is per shop. The federatedOnly marker that says "this person now
signs in with Badger Sett" is per person, and a person can operate several shops.
So the two decisions are deliberately asymmetric:
| Step | Condition |
|---|---|
| Link the Badger Sett account | any shop the operator administers has the flag on |
| Flip the operator to federated-only | every shop the operator administers has the flag on |
Linking is additive and harmless, so it starts early. Flipping changes how someone signs in everywhere, so it waits for the last shop. Without this, switching federation on for one small tenant could silently change how an operator signs in to a large one.
Enforcement¶
Once an operator is federated-only, every local credential — password, passkey and magic link — can be refused for them. That enforcement is built but shipped disabled, and should stay disabled until every operator has linked; enabling it early locks operators out of every shop they run at the same moment.
All three routes share a single gate (FederatedOnlyPolicy) rather than each carrying its own copy
of the switch and the exemption list. Magic link in particular has to go through it: it is a local
credential, and left ungated it would simply be the way around the refusal that password and passkey
respect.
There is a break-glass list of addresses that keep local sign-in regardless. It comes from deployment configuration, not the database, so it does not depend on Badger Sett or Clerk being reachable.
Shoppers and POS agents are never affected — federatedOnly is only ever set on identities holding
an admin-level role, so an identity that never qualified has the flag unset and falls straight
through.
Who gets invited to link¶
The invitation follows the per-shop sett-federation-enabled flag — there is no list of client ids
to maintain, because the tenant client ids already live in site configuration and duplicating them
would make opting a shop in a redeploy rather than a setting.
Only a shop's storefront client is ever interrupted. Its POS and MCP clients resolve to the same
shop, and neither can render an HTML interstitial — an MCP client is not a browser at all — so the
client id must match the shop's configured oauth2-client-id exactly. The exclusion list above is a
safety net on top of that, not the mechanism.
Joining an operator to their organisation¶
Badger Sett's SSO flow gives a user a personal organisation when they have none — not the one
paying for the shop they run. Badger auth knows the shop and, through sett-organisation-id, the
organisation behind it, so it is the side that can join them up: on linking it calls Badger Sett's
POST /api/internal/organisations/members with the operator's email.
It authenticates with the tool's x-badger-api-key, like every other Badger Commerce call to
Sett. That is not only convention: the key identifies the tool, so Sett scopes the attach to that
tool's own customers — a Badger Commerce key cannot add people to an organisation that never bought
Badger Commerce. Because the scope is per edition and badger auth does not know which one a shop
bought, it tries each configured key and stops at the first accepted.
It runs on every Badger Sett sign-in, not only at link time, so a mapping filled in later heals itself. Badger Sett's end is idempotent and never changes a role someone already holds — a repeat can't demote an owner. Attaching is best-effort: the operator has already proved both accounts and the link is written, so a failure is logged rather than costing them the sign-in.
New members are added as MEMBER. Badger Sett roles govern billing and subscription rights, which
a Badger Commerce role says nothing about, so promotion stays a human decision made in Badger Sett.
When the shop has no organisation yet¶
Which is every shop that predates Badger Sett — most of them, when their operators start migrating.
sett-organisation-id used to have to be filled in before a shop was flagged. Forget it and
operators linked successfully into no organisation at all: no error, no sign-in failure, nothing to
see until somebody asked why they couldn't find their subscription. An ordering requirement that is
silent when broken is a bad way to run a rollout.
So an unmapped shop is now resolved rather than skipped. Badger auth calls
POST /api/internal/organisations/resolve with the shop, its security context and the operator's
email, and Badger Sett answers with the organisation billing that shop — the one billing a sibling
shop in the same security context, if the group is already mapped, so a tenant running several
shops stays one organisation rather than fragmenting into one per shop. If there is no such
organisation it creates one, and the operator who triggered it becomes its OWNER. The id
comes back and is written into sett-organisation-id, so every later sign-in takes the direct
path.
Why the first linker owns it. Sett roles govern billing and subscription rights, and a Badger
Commerce admin role says nothing about who pays — so inferring an owner from "is an admin over
there" is a guess, and the wrong guess hands someone's billing to the wrong person. Proving control
of both accounts, which in-band linking already requires, is the strongest claim available without
asking a human. Everyone after them joins as MEMBER, and no existing role is ever revisited.
The organisation is never created ownerless. It comes into existence with its claimant attached and its site mapping written, in a single statement — separately would let two operators of one shop each create an organisation and only then discover that the unique site mapping leaves one of them owning nothing. Losing that race is treated as success: the loser joins the winner's organisation.
An id already in sett-organisation-id always wins, and resolution is not attempted. Somebody may
have pointed a shop at a particular organisation deliberately.
If resolution fails — Sett unreachable, no key accepted — nothing is recorded. A placeholder would be worse than an empty mapping, because the next sign-in would take the direct path to an organisation that isn't there.
A backfill is deliberately not offered. Creating an organisation for every existing shop would mint a Stripe customer for each of them, including the ones that never come back, and it would still have to guess at owners. Resolving on demand costs nothing until somebody actually arrives, and the path goes quiet by itself once the pre-Sett population has migrated.
When an upstream login is refused¶
Once an operator is federated-only, social login is refused too — not just password, passkey and magic link. Google and Apple travel a different path, and leaving them out would make enforcement decorative: the operator could simply click Google and skip Badger Sett entirely. The login page tells them which route to use instead.
The Badger Sett button itself is shown wherever the provider is configured, not gated on the
per-shop flag. federatedOnly is global to an identity, so gating the button per shop locked
people out: an operator migrated on the shops they run could arrive at an unrelated shop — one
where they are only a customer — find no button, and have their password refused. The flag governs
who is invited and who is flipped, never who may use the route.
Signing in with Badger Sett against an email that already owns a local identity is refused on purpose: that account must be joined in-band, where the operator authenticates to both sides inside one session. The login page explains this rather than reporting a bad password. Only an error code travels on the redirect — the wording is chosen locally, so nothing an upstream provider says is ever rendered.
Each refusal the operator can act on has wording of its own, and the two that come up during
migration both say what to do next: the chosen Badger Sett account using a different email address
to the Badger account (email_mismatch), and it already being linked to another Badger account
(account_already_linked). Anything without wording falls back to the generic message, which is
why a new refusal should arrive with its own.
Deployment configuration¶
Set on the auth service. Leave issuer-url or client-secret blank and the whole provider is
skipped — local development works without a running Badger Sett.
| Variable | Default | Purpose |
|---|---|---|
SETT_ISSUER_URL |
(empty — disabled) | Badger Sett issuer, e.g. https://sett.bdgr.co.uk |
SETT_CLIENT_ID |
badger-auth |
Client id registered with Badger Sett |
SETT_CLIENT_SECRET |
(empty — disabled) | Client secret (client_secret_basic) |
SETT_INTERNAL_API_BASE_URL |
(falls back to the issuer) | Badger Sett's internal API, for the organisation attach |
SETT_API_KEY_COMMERCE_LITE / _ENTERPRISE |
(empty — attach disabled) | The tool API keys from Sett admin, shared with the REST module |
SETT_MIGRATION_PROMPT_EXCLUDE_CLIENT_IDS |
badger-pos-app |
Clients that must never see the link invitation, whatever their shop's flag says |
SETT_ENFORCE_FEDERATED_ONLY |
false |
Refuse password, passkey and magic link for federated-only operators |
SETT_BREAK_GLASS_EMAILS |
(empty) | Addresses exempt from the above |
BADGER_AUTH_AUTO_LINK_PROVIDERS |
google,apple |
Upstream providers allowed to attach to an existing account by verified email |
BADGER_AUTH_EMAIL_VERIFIED_VOUCHED_PROVIDERS |
apple |
Providers whose absent email_verified claim is treated as verified |
BADGER_AUTH_EMAIL_VERIFIED_VOUCHED_PROVIDERS exists because Apple can omit email_verified
altogether on private-relay addresses, and a gate that fails closed on silence would break Apple
sign-in for shoppers. It relaxes silence only — an explicit email_verified: false is still
rejected for every provider without exception. Badger Sett is deliberately not on the list: it
always sends the claim, so silence from it means something has gone wrong and should fail closed.
The redirect URI to register with Badger Sett is
https://auth.bdgr.co.uk/login/oauth2/code/sett, with scopes openid profile email.
BADGER_AUTH_AUTO_LINK_PROVIDERS deliberately excludes Badger Sett: admin migration is in-band, and
any provider on that list can claim an existing account — and its roles — by asserting a verified
email address. Keep it short.
Suggested rollout order¶
- Register
badger-authwith Badger Sett and set the issuer, client id and secret on the auth service. Nothing changes for anyone yet. - Turn on Badger Sett Federation Enabled for one shop. Its operators start being invited to link.
- Work through the remaining shops. Operators are flipped to federated-only automatically as the last of their shops comes online.
- Only once every operator has linked, consider
SETT_ENFORCE_FEDERATED_ONLY, with break-glass addresses configured first.