screentinker/server/routes
ScreenTinker e97228a502 SSO: per-organization providers, configured by the customer
Instance-wide providers belong to whoever runs the server. These belong to a
CUSTOMER: an organization points ScreenTinker at its own identity provider from
Settings → Single sign-on, with no environment variable and no restart.

The login flow is unchanged. An org provider is resolved through the same
oidc-providers.get(slug) the env ones go through, so there is one authorization
request builder, one token exchange and one verifier — not a second, less
tested path for tenants. That seam is why Phase 1 put provider lookup behind a
single function.

⚠️ An org provider is NEVER published. It is not in /api/auth/providers, because
listing a customer's IdP would both offer it to people it does not belong to and
leak the customer list from the login page. It surfaces only when someone types
an address at one of that organization's domains; otherwise the instance-wide
buttons are what you get.

The discovery endpoint answers with a BOOLEAN and nothing else — no slug, no
display name. Returning "yes, Acme Corp SSO" would turn a guessed domain into
confirmation that Acme buys this product, and the slug would hand out a working
entry point to their tenant. POST /sso/start repeats the lookup server-side and
redirects, so the browser never learns which provider it is being sent to until
the provider says so, and the address travels in a body rather than in a URL
that lands in history, proxy logs and a Referer. Both endpoints rate limited to
10/min.

Other properties, each with a test:
  - slugs are RANDOM, not chosen, so two customers cannot collide on or guess
    each other's URL
  - a domain may be claimed by ONE organization; a second claim is refused, so a
    tenant cannot capture another company's logins
  - the issuer is verified by live discovery BEFORE the row is written, so a
    typo is caught at configuration rather than by a user staring at a failed
    login
  - client secrets are optional (PKCE), stored AES-256-GCM via lib/secretbox,
    never returned; an absent secret on update leaves the stored one alone,
    which is how a settings form that cannot show it avoids blanking it
  - cross-org access answers 404, not 403, so an outsider cannot confirm that an
    organization id exists
  - signing in through an org provider grants membership of that organization,
    but never changes an existing member's role

Verified live end to end: creation against a real issuer, domain normalisation
(`@Acme.CO.UK` → `acme.co.uk`), boolean-only discovery, a rejected domain
squat, a rejected bad issuer, and 404 for a foreign organization.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bvjey4FNam49MN7ybjcq6A
2026-08-10 17:33:32 -05:00
..
activity.js Phase 2.1: tenancy middleware, permission helpers, JWT workspace context, frontend + backend role-rename compat 2026-05-11 20:02:00 -05:00
admin.js Show every plan on the admin tab, with who is on each 2026-07-29 19:36:10 -05:00
agency.js Default a video playlist item to the clip's own length (#237) 2026-08-06 09:36:53 -05:00
ai.js feat(ai): separate optional image API key (#41) 2026-06-09 13:47:47 -05:00
assignments.js Default a video playlist item to the clip's own length (#237) 2026-08-06 09:36:53 -05:00
auth.js SSO: per-organization providers, configured by the customer 2026-08-10 17:33:32 -05:00
billing.js feat(#146): billing:read scoped token — dual-path auth for the Usage Report (Option C) 2026-07-01 21:16:21 -05:00
contact.js fix(landing): replace broken Custom pricing card with enterprise contact form 2026-05-14 13:52:24 -05:00
content.js QA: close four ways a control or an asset lied about itself 2026-08-06 16:12:29 -05:00
device-groups.js Default a video playlist item to the clip's own length (#237) 2026-08-06 09:36:53 -05:00
devices.js BrightSign: report IPv6, the attached display and the active video mode 2026-08-10 15:30:40 -05:00
folders.js Phase 2.2c: content_folders gets workspace_id (schema + backfill); folders.js scoped; content.js folder-move strict same-workspace 2026-05-11 21:04:03 -05:00
kiosk.js Validate kiosk style values as CSS rather than as HTML 2026-07-27 22:28:47 -05:00
layouts.js Save a layout by diffing its zones, not by deleting and re-inserting them 2026-07-30 20:51:28 -05:00
media.js feat: transition engine — GL wipes across web, Tizen & Android (+ image↔video) (#204) 2026-07-20 16:45:32 -05:00
org-sso.js SSO: per-organization providers, configured by the customer 2026-08-10 17:33:32 -05:00
pip.js Hold overlay pushes to the same write check as every other fleet action 2026-07-30 20:46:03 -05:00
player-debug.js Add player debug overlay and server-side error telemetry sink 2026-05-15 15:20:42 -05:00
playlists.js Default a video playlist item to the clip's own length (#237) 2026-08-06 09:36:53 -05:00
provisioning.js fix(api): consolidate device pairing to /pair, remove vestigial bare endpoint (#90) 2026-06-12 20:13:16 -05:00
reports.js Phase 2.2g: reports.js scoped to workspace_id; fixes pre-existing /export and /uptime cross-tenant leaks 2026-05-11 21:36:54 -05:00
schedules.js Default a video playlist item to the clip's own length (#237) 2026-08-06 09:36:53 -05:00
status.js #240: stop the morning wave buying itself a blocking checkpoint 2026-08-06 20:22:21 -05:00
stripe.js feat(stripe): enable promotion codes on checkout sessions (#227) 2026-07-23 23:44:18 -05:00
subscription.js Initial open source release 2026-04-08 12:14:53 -05:00
teams.js feat(teams): temporarily disable Teams API while feature is redesigned 2026-05-12 13:30:55 -05:00
tokens.js feat(agency): per-token upload folder — auto-created, subtree-confined (#158) (#171) 2026-07-12 21:23:25 -05:00
video-walls.js Describe a portrait video wall as portrait, and stop a wall hiding its screens 2026-08-06 09:46:31 -05:00
white-label.js fix(security): patch quick-win findings from the codebase review 2026-06-08 19:02:19 -05:00
widgets.js Stop shrinking hand-written text widgets into illegibility 2026-07-30 20:28:28 -05:00
workspaces.js fix(workspaces): use APP_URL env var for invite-accept URL generation 2026-05-17 15:26:07 -05:00