[PR #7738] [MERGED] feat(crypto): non-destructive BETTER_AUTH_SECRET rotation #24426

Closed
opened 2026-04-15 22:21:52 -05:00 by GiteaMirror · 0 comments
Owner

📋 Pull Request Information

Original PR: https://github.com/better-auth/better-auth/pull/7738
Author: @jzila
Created: 2/1/2026
Status: Merged
Merged: 2/28/2026
Merged by: @Bekacru

Base: canaryHead: feat/secret-rotation


📝 Commits (10+)

  • a702cbd feat(crypto): non-destructive BETTER_AUTH_SECRET rotation
  • d67a8c0 docs: document secret rotation and BETTER_AUTH_SECRETS
  • c23bf80 fix(crypto): address code review findings for secret rotation
  • a2ee885 fix(crypto): enforce kid matching in JWE decode, harden legacy secret handling
  • 29345f3 fix(crypto): validate secret versions, detect duplicates, trim env whitespace
  • 715ecac fix(crypto): coerce secret versions, extract secret-utils, add validation tests
  • 2238e26 fix(crypto): import shared DEFAULT_SECRET, avoid mutation, strict decimal parsing
  • 8af6eea fix(crypto): resolve CI failures for secret rotation PR
  • 672339f fix(crypto): fix biome formatting in secret-rotation tests
  • 0881a6e fix(crypto): resolve remaining biome lint errors

📊 Changes

31 files changed (+924 additions, -86 deletions)

View changed files

📝 docs/content/docs/concepts/cookies.mdx (+1 -1)
📝 docs/content/docs/installation.mdx (+4 -0)
📝 docs/content/docs/reference/options.mdx (+26 -0)
📝 docs/content/docs/reference/security.mdx (+8 -0)
📝 packages/better-auth/src/api/routes/session.ts (+5 -1)
📝 packages/better-auth/src/context/create-context.ts (+28 -7)
packages/better-auth/src/context/secret-utils.ts (+108 -0)
📝 packages/better-auth/src/cookies/index.ts (+1 -1)
📝 packages/better-auth/src/cookies/session-store.ts (+2 -2)
📝 packages/better-auth/src/crypto/index.ts (+67 -10)
📝 packages/better-auth/src/crypto/jwt.ts (+101 -30)
packages/better-auth/src/crypto/secret-rotation.test.ts (+512 -0)
📝 packages/better-auth/src/oauth2/utils.test.ts (+1 -0)
📝 packages/better-auth/src/oauth2/utils.ts (+3 -2)
📝 packages/better-auth/src/plugins/email-otp/otp-token.ts (+2 -2)
📝 packages/better-auth/src/plugins/email-otp/routes.ts (+1 -1)
📝 packages/better-auth/src/plugins/jwt/sign.ts (+1 -1)
📝 packages/better-auth/src/plugins/jwt/utils.ts (+1 -1)
📝 packages/better-auth/src/plugins/oauth-proxy/index.ts (+6 -6)
📝 packages/better-auth/src/plugins/oidc-provider/index.ts (+2 -2)

...and 11 more files

📄 Description

Summary

  • Adds support for rotating BETTER_AUTH_SECRET without invalidating existing encrypted data
  • New secrets config option and BETTER_AUTH_SECRETS env var accept versioned secrets (2:base64secret,1:base64secret)
  • Encrypted data uses an envelope format ($ba$<kid>$<ciphertext>) embedding the key version; legacy bare-hex payloads fall back to the singular BETTER_AUTH_SECRET
  • All symmetricEncrypt/symmetricDecrypt and JWE encode/decode call sites updated to use the new secretConfig context property
  • Zero schema migrations, zero downtime — fully backwards compatible

Test plan

  • 17 new unit tests covering:
    • Single secret string (bare hex, no envelope) — identical to current behavior
    • SecretConfig with one key — envelope format
    • Key rotation (encrypt v2, decrypt with v1+v2 config)
    • Legacy bare-hex decryption with legacySecret fallback
    • Missing legacy secret error
    • Unknown/retired version error
    • Version gaps
    • JWE multi-secret encode/decode and legacy fallback
  • pnpm typecheck — no new errors (all failures pre-existing in unrelated packages)
  • pnpm format:check — passes
  • Verify existing test suites still pass in CI

Summary by cubic

Enables non-destructive rotation of BETTER_AUTH_SECRET with versioned keys. New data embeds a key version; legacy data stays readable. JWE decryption enforces kid matching and only falls back when tokens are kid-less. No downtime or schema changes.

  • New Features

    • secrets option and BETTER_AUTH_SECRETS env var (e.g., "2:base64secret,1:base64secret"); first entry is current.
    • Envelope format for symmetric encryption: "$ba$$"; legacy bare-hex falls back to BETTER_AUTH_SECRET.
    • Updated symmetricEncrypt/symmetricDecrypt and JWE encode/decode to accept SecretConfig; decode matches kid and tries other keys only for kid-less tokens.
    • All call sites now use secretConfig (cookies, session cache, OAuth tokens, OTP/2FA, OIDC/OAuth Proxy, SCIM); context.secret points to the current key when secrets are set.
    • Added secret-utils (parseSecretsEnv, validateSecretsArray, buildSecretConfig) with strict integer version parsing, duplicate detection, whitespace trimming, entropy warnings, and DEFAULT_SECRET filtered from legacy fallback.
    • Docs updated for rotation in Installation, Options, Security; tests cover rotation, legacy fallback, unknown versions, gaps, kid-less fallback, and kid mismatch.
  • Migration

    • Set BETTER_AUTH_SECRETS with the new key first, followed by previous keys; keep BETTER_AUTH_SECRET for legacy bare-hex data.
    • Rotate by prepending the new version; reads continue to work with old keys.
    • Remove retired keys only after confirming old data is gone; otherwise keep them (or BETTER_AUTH_SECRET) for compatibility.

Written for commit 8805a74bab. Summary will update on new commits.


🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.

## 📋 Pull Request Information **Original PR:** https://github.com/better-auth/better-auth/pull/7738 **Author:** [@jzila](https://github.com/jzila) **Created:** 2/1/2026 **Status:** ✅ Merged **Merged:** 2/28/2026 **Merged by:** [@Bekacru](https://github.com/Bekacru) **Base:** `canary` ← **Head:** `feat/secret-rotation` --- ### 📝 Commits (10+) - [`a702cbd`](https://github.com/better-auth/better-auth/commit/a702cbdba5b8a67ab525e303861f464f9003a483) feat(crypto): non-destructive BETTER_AUTH_SECRET rotation - [`d67a8c0`](https://github.com/better-auth/better-auth/commit/d67a8c007efb6452203fdbd2167018e49dc73aa2) docs: document secret rotation and BETTER_AUTH_SECRETS - [`c23bf80`](https://github.com/better-auth/better-auth/commit/c23bf80fad4f14f840322002fc57bd5f3bb4118d) fix(crypto): address code review findings for secret rotation - [`a2ee885`](https://github.com/better-auth/better-auth/commit/a2ee885503e634677a600f0e888b6782d7126623) fix(crypto): enforce kid matching in JWE decode, harden legacy secret handling - [`29345f3`](https://github.com/better-auth/better-auth/commit/29345f3ab2dbed34504b117c9a8603708e4cf03e) fix(crypto): validate secret versions, detect duplicates, trim env whitespace - [`715ecac`](https://github.com/better-auth/better-auth/commit/715ecac0d799e68551957ec8c7013ddb2cb37627) fix(crypto): coerce secret versions, extract secret-utils, add validation tests - [`2238e26`](https://github.com/better-auth/better-auth/commit/2238e26ef4674802459fbf8e4891a2bcc72d3632) fix(crypto): import shared DEFAULT_SECRET, avoid mutation, strict decimal parsing - [`8af6eea`](https://github.com/better-auth/better-auth/commit/8af6eeaa1a4f494ca02e717aca60eb2da12e3b1f) fix(crypto): resolve CI failures for secret rotation PR - [`672339f`](https://github.com/better-auth/better-auth/commit/672339fbbd270e2623c9e8996dd784f780c16efb) fix(crypto): fix biome formatting in secret-rotation tests - [`0881a6e`](https://github.com/better-auth/better-auth/commit/0881a6e08b2a986aaa2e1b927474750f0be363aa) fix(crypto): resolve remaining biome lint errors ### 📊 Changes **31 files changed** (+924 additions, -86 deletions) <details> <summary>View changed files</summary> 📝 `docs/content/docs/concepts/cookies.mdx` (+1 -1) 📝 `docs/content/docs/installation.mdx` (+4 -0) 📝 `docs/content/docs/reference/options.mdx` (+26 -0) 📝 `docs/content/docs/reference/security.mdx` (+8 -0) 📝 `packages/better-auth/src/api/routes/session.ts` (+5 -1) 📝 `packages/better-auth/src/context/create-context.ts` (+28 -7) ➕ `packages/better-auth/src/context/secret-utils.ts` (+108 -0) 📝 `packages/better-auth/src/cookies/index.ts` (+1 -1) 📝 `packages/better-auth/src/cookies/session-store.ts` (+2 -2) 📝 `packages/better-auth/src/crypto/index.ts` (+67 -10) 📝 `packages/better-auth/src/crypto/jwt.ts` (+101 -30) ➕ `packages/better-auth/src/crypto/secret-rotation.test.ts` (+512 -0) 📝 `packages/better-auth/src/oauth2/utils.test.ts` (+1 -0) 📝 `packages/better-auth/src/oauth2/utils.ts` (+3 -2) 📝 `packages/better-auth/src/plugins/email-otp/otp-token.ts` (+2 -2) 📝 `packages/better-auth/src/plugins/email-otp/routes.ts` (+1 -1) 📝 `packages/better-auth/src/plugins/jwt/sign.ts` (+1 -1) 📝 `packages/better-auth/src/plugins/jwt/utils.ts` (+1 -1) 📝 `packages/better-auth/src/plugins/oauth-proxy/index.ts` (+6 -6) 📝 `packages/better-auth/src/plugins/oidc-provider/index.ts` (+2 -2) _...and 11 more files_ </details> ### 📄 Description ## Summary - Adds support for rotating `BETTER_AUTH_SECRET` without invalidating existing encrypted data - New `secrets` config option and `BETTER_AUTH_SECRETS` env var accept versioned secrets (`2:base64secret,1:base64secret`) - Encrypted data uses an envelope format (`$ba$<kid>$<ciphertext>`) embedding the key version; legacy bare-hex payloads fall back to the singular `BETTER_AUTH_SECRET` - All `symmetricEncrypt`/`symmetricDecrypt` and JWE encode/decode call sites updated to use the new `secretConfig` context property - Zero schema migrations, zero downtime — fully backwards compatible ## Test plan - [x] 17 new unit tests covering: - Single secret string (bare hex, no envelope) — identical to current behavior - SecretConfig with one key — envelope format - Key rotation (encrypt v2, decrypt with v1+v2 config) - Legacy bare-hex decryption with `legacySecret` fallback - Missing legacy secret error - Unknown/retired version error - Version gaps - JWE multi-secret encode/decode and legacy fallback - [x] `pnpm typecheck` — no new errors (all failures pre-existing in unrelated packages) - [x] `pnpm format:check` — passes - [ ] Verify existing test suites still pass in CI <!-- This is an auto-generated description by cubic. --> --- ## Summary by cubic Enables non-destructive rotation of BETTER_AUTH_SECRET with versioned keys. New data embeds a key version; legacy data stays readable. JWE decryption enforces kid matching and only falls back when tokens are kid-less. No downtime or schema changes. - **New Features** - `secrets` option and `BETTER_AUTH_SECRETS` env var (e.g., "2:base64secret,1:base64secret"); first entry is current. - Envelope format for symmetric encryption: "$ba$<version>$<ciphertext>"; legacy bare-hex falls back to BETTER_AUTH_SECRET. - Updated symmetricEncrypt/symmetricDecrypt and JWE encode/decode to accept SecretConfig; decode matches kid and tries other keys only for kid-less tokens. - All call sites now use secretConfig (cookies, session cache, OAuth tokens, OTP/2FA, OIDC/OAuth Proxy, SCIM); context.secret points to the current key when secrets are set. - Added secret-utils (parseSecretsEnv, validateSecretsArray, buildSecretConfig) with strict integer version parsing, duplicate detection, whitespace trimming, entropy warnings, and DEFAULT_SECRET filtered from legacy fallback. - Docs updated for rotation in Installation, Options, Security; tests cover rotation, legacy fallback, unknown versions, gaps, kid-less fallback, and kid mismatch. - **Migration** - Set BETTER_AUTH_SECRETS with the new key first, followed by previous keys; keep BETTER_AUTH_SECRET for legacy bare-hex data. - Rotate by prepending the new version; reads continue to work with old keys. - Remove retired keys only after confirming old data is gone; otherwise keep them (or BETTER_AUTH_SECRET) for compatibility. <sup>Written for commit 8805a74bab578b45be3ef70462e95d45de24e229. Summary will update on new commits.</sup> <!-- End of auto-generated description by cubic. --> --- <sub>🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.</sub>
GiteaMirror added the pull-request label 2026-04-15 22:21:52 -05:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#24426