screentinker/frontend
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
..
assets Landing: replace iframe mock with dashboard screenshot 2026-04-21 19:47:13 -05:00
compare feat(seo): strengthen integrations internal linking + drop public status link (#178) 2026-07-13 12:11:30 -05:00
css Preview a rotated display the way people see it, not the way its framebuffer is 2026-08-06 09:38:31 -05:00
guides Pi installer: stop advertising what was never installed (#245) 2026-08-10 10:50:34 -05:00
integrations feat(seo): strengthen integrations internal linking + drop public status link (#178) 2026-07-13 12:11:30 -05:00
js SSO: per-organization providers, configured by the customer 2026-08-10 17:33:32 -05:00
legal docs(privacy): disclose error and diagnostic telemetry from players 2026-05-15 15:31:21 -05:00
vendor test(api): fix spec scope drift + guard it in CI; Redoc provenance 2026-06-12 18:45:09 -05:00
41498e6c84045d9d187f4504c122e54f.txt feat(seo): IndexNow + landing-page optimization (schema, FAQ, CWV, content) (#177) 2026-07-13 11:22:37 -05:00
agency.html feat(agency): per-token upload folder — auto-created, subtree-confined (#158) (#171) 2026-07-12 21:23:25 -05:00
api-docs.html docs(api): OpenAPI spec, Redoc at /docs, CI spec-lint 2026-06-12 18:45:09 -05:00
index.html Preview a rotated display the way people see it, not the way its framebuffer is 2026-08-06 09:38:31 -05:00
landing.html feat(seo): strengthen integrations internal linking + drop public status link (#178) 2026-07-13 12:11:30 -05:00
manifest.json Initial open source release 2026-04-08 12:14:53 -05:00
robots.txt SEO: add meta tags, sitemap, robots.txt, comparison pages, guides, internal linking 2026-04-28 20:54:32 -05:00
sitemap.xml feat(seo): IndexNow + landing-page optimization (schema, FAQ, CWV, content) (#177) 2026-07-13 11:22:37 -05:00
sw-admin.js fix(sw): stop the admin service worker from breaking video playback 2026-06-08 23:08:13 -05:00