refactor(auth): extract sso-origins SSOT + harden drift test

TRUSTED_ORIGINS was defined inside better-auth.config.ts, which pulls
in the whole Better Auth stack just to read a list of hostnames. Anyone
who wants to consume the list (infra tooling, compose-env generators,
monitoring) had to either duplicate it or pay the import cost.

- New `sso-origins.ts` — zero-dep module exposing
  `PRODUCTION_TRUSTED_ORIGINS` + `LOCAL_TRUSTED_ORIGINS` + the combined
  `TRUSTED_ORIGINS` list. This is now the canonical place to add a new
  top-level SSO origin.
- `better-auth.config.ts` imports + re-exports so existing consumers
  keep working without a touch.
- `sso-config.spec.ts` imports directly from `./sso-origins` (cleaner
  coupling) and now HARD-FAILS when mana-auth CORS_ORIGINS contains a
  production origin that isn't in trustedOrigins. Previously this was
  a `console.warn` only, meaning dead-drift could silently accumulate
  and then surface as a confusing runtime auth rejection.
- Root CLAUDE.md "Adding an app to SSO" updated to point at the SSOT
  and mention the new hard-fail direction.

No current drift — the mana-auth CORS_ORIGINS already match. The
hardened assertion is defensive for future changes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-04-19 19:45:42 +02:00
parent 2bcc3954ea
commit a7fe828d32
4 changed files with 51 additions and 41 deletions

View file

@ -112,9 +112,9 @@ Default new user-typed fields to **encrypt**; default new IDs/timestamps/sort-ke
All servers use `@mana/shared-hono` with `authMiddleware()`. Tokens are EdDSA JWTs issued by `mana-auth` with claims `{sub, email, role, sid, tier, exp, iss, aud}`. Cross-app SSO works across `*.mana.how`. See [`.claude/guidelines/authentication.md`](.claude/guidelines/authentication.md) and `services/mana-auth/`.
**Adding an app to SSO** requires updating *all three*:
1. `trustedOrigins` in `services/mana-auth/src/auth/better-auth.config.ts`
1. `PRODUCTION_TRUSTED_ORIGINS` in `services/mana-auth/src/auth/sso-origins.ts` (the SSOT — better-auth.config.ts re-exports from here)
2. `CORS_ORIGINS` for mana-auth in `docker-compose.macmini.yml`
3. Run `pnpm test -- src/auth/sso-config.spec.ts` from `services/mana-auth/`
3. Run `bun test src/auth/sso-config.spec.ts` from `services/mana-auth/` — now hard-fails on drift in either direction
### Access tiers

View file

@ -37,33 +37,11 @@ import {
sendMagicLinkEmail,
} from '../email/send';
import { sourceAppStore, passwordResetRedirectStore } from './stores';
import { TRUSTED_ORIGINS } from './sso-origins';
/**
* Single source of truth for SSO trusted origins.
*
* Better Auth rejects any cross-origin auth request whose Origin header
* isn't in this list silent login failure on mis-configured apps. When
* adding a new top-level domain (NOT a path under mana.how), update both:
*
* 1. This array
* 2. The `mana-auth` `CORS_ORIGINS` env var in
* `docker-compose.macmini.yml` (must be a superset of this list)
*
* `sso-config.spec.ts` enforces both invariants. The unified app under
* `mana.how` does NOT need per-module subdomains here modules are routed
* by path on the same origin.
*/
export const TRUSTED_ORIGINS: string[] = [
// Unified app — all productivity apps live under mana.how
'https://mana.how',
'https://auth.mana.how',
// Separate apps (not part of the unified app)
'https://arcade.mana.how', // Games
'https://whopxl.mana.how', // Games
// Local development
'http://localhost:3001',
'http://localhost:5173',
];
// Re-export so existing imports (`import { TRUSTED_ORIGINS } from './better-auth.config'`)
// keep working. New code should import from './sso-origins' directly.
export { TRUSTED_ORIGINS };
/**
* JWT Custom Payload Interface

View file

@ -26,7 +26,7 @@
import { describe, it, expect } from 'bun:test';
import { readFileSync } from 'node:fs';
import { join } from 'node:path';
import { TRUSTED_ORIGINS } from './better-auth.config';
import { TRUSTED_ORIGINS } from './sso-origins';
const REPO_ROOT = join(import.meta.dir, '../../../..');
const COMPOSE_FILE = join(REPO_ROOT, 'docker-compose.macmini.yml');
@ -106,21 +106,18 @@ describe('SSO ↔ docker-compose CORS_ORIGINS consistency', () => {
expect(missing).toEqual([]);
});
it('reports CORS_ORIGINS entries that are NOT in trustedOrigins (dead drift)', () => {
// This is a soft assertion — extra entries don't break the SSO
// loop, but they're a sign of stale config that should be cleaned
// up. We log them so they're visible in CI without failing the
// build. Convert to a hard expect() once the cleanup ships.
it('mana-auth CORS_ORIGINS contains NO entries outside trustedOrigins (no dead drift)', () => {
// Hard-fail on extras: if CORS lists an origin Better Auth doesn't
// trust, the server accepts the preflight but then silently rejects
// the auth request — worst-of-both-worlds. Tightened from a warning
// to a hard assertion on 2026-04-19 per audit.
// Fix: either add the origin to TRUSTED_ORIGINS (in sso-origins.ts)
// or remove it from the mana-auth CORS_ORIGINS in
// docker-compose.macmini.yml.
const extras = corsOrigins.filter(
(o) =>
o.startsWith('https://') && !TRUSTED_ORIGINS.includes(o as (typeof TRUSTED_ORIGINS)[number])
);
if (extras.length > 0) {
console.warn(
`[sso-config.spec] mana-auth CORS_ORIGINS contains ${extras.length} origin(s) not in trustedOrigins (likely stale post-consolidation): ${extras.join(', ')}`
);
}
// No assertion — see comment above. Will be tightened once
// audit item from REFACTORING_AUDIT_2026_04.md lands.
expect(extras).toEqual([]);
});
});

View file

@ -0,0 +1,35 @@
/**
* Single source of truth for SSO trusted origins.
*
* Extracted into a standalone module (no Better Auth imports) so it can
* also be consumed by infra tooling (compose env generators, monitoring
* jobs, etc.) without pulling in the full auth stack.
*
* Better Auth rejects any cross-origin auth request whose Origin header
* isn't in this list silent login failure on mis-configured apps. When
* adding a new top-level domain (NOT a path under mana.how), update both:
*
* 1. `PRODUCTION_TRUSTED_ORIGINS` below
* 2. The `mana-auth` `CORS_ORIGINS` env var in
* `docker-compose.macmini.yml` (must be a superset of this list)
*
* `sso-config.spec.ts` enforces both invariants. The unified app under
* `mana.how` does NOT need per-module subdomains here modules are routed
* by path on the same origin.
*/
/** HTTPS origins Better Auth accepts in production. */
export const PRODUCTION_TRUSTED_ORIGINS = [
// Unified app — all productivity apps live under mana.how
'https://mana.how',
'https://auth.mana.how',
// Separate apps (not part of the unified app)
'https://arcade.mana.how', // Games
'https://whopxl.mana.how', // Games
] as const;
/** Local dev origins — web dev server + the auth server itself. */
export const LOCAL_TRUSTED_ORIGINS = ['http://localhost:3001', 'http://localhost:5173'] as const;
/** Full trusted-origins list passed to Better Auth. */
export const TRUSTED_ORIGINS: string[] = [...PRODUCTION_TRUSTED_ORIGINS, ...LOCAL_TRUSTED_ORIGINS];