Skip to content

feat(oauth-server-kit): add hardened Better Auth defaults - #35

Merged
s3rot0n1n merged 2 commits into
mainfrom
feat/oauth-kit-security-defaults
Nov 13, 2026
Merged

s3rot0n1n merged 2 commits into
mainfrom
feat/oauth-kit-security-defaults

Conversation

@clbotdev

@clbotdev clbotdev commented Nov 13, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Every product configures Better Auth itself, so CourseLit, SendLit and MediaLit would each re-derive the same hardening. This moves the hardening CourseLit built locally into @codelitdev/oauth-server-kit as helpers that products spread into their own config. The kit still doesn't create the auth instance.

@codelitdev/oauth-server-kit/better-auth:

  • secureCookieOptions({ baseURL, cookiePrefix, crossSubDomainCookies?, clientIpHeader? }) chooses the cookie names:
    • Over https: __Host-<prefix> session cookies (Secure, Path=/, no Domain), so a sibling subdomain can't plant one.
    • With cross-subdomain cookies on (SendLit): __Secure-.
    • Over http: plain names.
    • With clientIpHeader: Better Auth reads the client address only from that header, never from X-Forwarded-For.
  • sessionCookieName(advanced) returns the resulting session cookie name, for credential selection.
  • secureSessionOptions() turns off the session cookie cache, so a revoked session stops working on the next request.
  • secureEmailAndPasswordOptions() keeps password sign-up off. A product that opts in with { enabled: true } also requires a verified email before sign-in.
  • authenticatedAt and isRecentlyAuthenticated: session identities now carry authenticatedAt, and isRecentlyAuthenticated(identity, { maxAgeMs }) allows 60 seconds of clock skew, for "signed in recently" checks.

@codelitdev/oauth-server-kit/express:

  • parseTrustProxy and DEFAULT_TRUST_PROXY: the default is private networks.
  • createClientIpMiddleware({ header }) writes req.ip into the header and overwrites any value the client sent. req.ip reads X-Forwarded-For from the right, so the left-hand side, which an attacker controls, never counts.

Also in this PR:

  • The reference product and template use the helpers.
  • ADR 0018 records the decision.
  • The AGENTS.md cookie-prefix rule now points products at secureCookieOptions.
  • The README and integration guide are updated.
  • A changeset adds a minor bump.

Known limitation

Better Auth's OAuth-provider plugin sets its RP-initiated logout confirmation cookie on a fixed narrow path. A __Host- cookie has to use Path=/, so browsers reject it, and that logout confirmation step stops working. Neither the plugin's options nor advanced.cookies can change the path. This is documented, and products that need that step should use cross-subdomain mode.

Product changes after release

  • Switching to __Host- renames the session cookie, so users sign in once more.
  • Products that select credentials by cookie name must pass sessionCookieName(...). The reference product needed this.

Testing

  • oauth-server-kit: 139 tests pass, including live Better Auth round trips:
    • cookie names and attributes in each mode, and a planted plain-name cookie being refused;
    • a revoked session refused at once;
    • the password default.
  • check-types, biome lint . and format:check are clean.
  • The reference product type-checks and its 16 tests pass.
  • CourseLit's API was switched to a packed build of this branch, with its own copies removed. A CourseLit test drives the real Express app and confirms forged client-IP headers don't get around API-key throttling.

Better Auth 1.7.22

The second commit sets the kit's minimum Better Auth version to ^1.7.16. That release fixes GHSA-fvxx-76fc-893x, GHSA-2f9c-7xj7-gh55 and GHSA-9hm7-xhfp-cx58, which affect these defaults.

  • Version pins: the kit, reference product, template and preset move to 1.7.22. @better-auth/utils (0.5.1), better-call (1.4.5) and @better-fetch/fetch (1.3.3) move with them. A mismatched utils pin had installed two copies of @better-auth/core, which broke the plugin types.
  • Resource-client type: it now matches newer @better-auth/oauth-provider, which needs the product's real Better Auth instance.
  • Reference product: account.issuer is dropped. Better Auth stopped writing it in 1.7.3, and 1.7.22 refuses to start if it is still required.
  • Trusted proxies: the docs now say to set TRUST_PROXY to the real proxy subnet when clients can reach the app from a private network.
  • Session cookie cache: it stays off by default. 1.7.16 only fixes some routes, and the advisory itself recommends turning the cache off when revocation must take effect immediately.
  • Checks: locally, bun run verify and template:smoke pass, along with format:check and lint.

Products configure Better Auth themselves, so each one re-derived the same
hardening. The kit now ships it as helpers products spread into their config:

- secureCookieOptions / sessionCookieName: __Host- session cookies over https
  (__Secure- with cross-subdomain cookies, plain names over http), and Better
  Auth reads the client address only from a header the edge sets.
- secureSessionOptions: no session cookie cache, so revoked sessions stop
  working on the next request.
- secureEmailAndPasswordOptions: password sign-up off; opting in also
  requires a verified email before sign-in.
- authenticatedAt on session identities and isRecentlyAuthenticated for
  step-up checks.
- Express: parseTrustProxy and createClientIpMiddleware, which resolve the
  client address from a trusted proxy chain instead of the leftmost
  X-Forwarded-For entry.

The reference product and template use the helpers. ADR 0018 records the
decision. Under __Host- the OAuth provider's RP-initiated logout confirmation
cookie is rejected by browsers (the plugin fixes its path); this is documented.
Better Auth 1.7.16 fixes advisories that touch the kit's defaults:
revoked sessions outliving the cookie cache on sensitive routes
(GHSA-fvxx-76fc-893x), email OTP attempt-limit races (GHSA-2f9c-7xj7-gh55)
and unverified OAuth emails counting as verified (GHSA-9hm7-xhfp-cx58).

- Peer floor for better-auth and @better-auth/oauth-provider is ^1.7.16;
  dev, reference product, template and preset pins move to 1.7.22 with the
  matching @better-auth/utils 0.5.1, better-call 1.4.5 and
  @better-fetch/fetch 1.3.3 (a mismatched utils pin split @better-auth/core
  into two copies and broke plugin types).
- The resource client type now matches newer oauth-provider: it needs the
  product's real Better Auth instance (options and a $context resolving
  baseURL).
- Reference product: type the database option and drop account.issuer,
  which Better Auth stopped writing in 1.7.3 and 1.7.22 refuses at startup
  when required.
- Docs: the default trusted proxies are private networks; set TRUST_PROXY to
  the real proxy subnet when clients can reach the app from a private
  network. ADR 0018 and the changeset record the floor and upgrade steps.

The session cookie cache stays off by default: 1.7.16 only guards some
routes, and Better Auth's advisory itself recommends disabling the cache
when revocation must be immediate.
@s3rot0n1n
s3rot0n1n merged commit cf93869 into main Nov 13, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants