Skip to content

fix(auth): ship plugin-derived credential fields read-denied - #1013

Merged
borisno2 merged 3 commits into
mainfrom
claude/focused-rubin-ck653y
Aug 22, 2026
Merged

fix(auth): ship plugin-derived credential fields read-denied#1013
borisno2 merged 3 commits into
mainfrom
claude/focused-rubin-ck653y

Conversation

@borisno2

Copy link
Copy Markdown
Member

Summary

Implements ADR-0036: the derived auth lists' credential-bearing fields — Session.token, Verification.value, and Account.password/accessToken/refreshToken/idToken — now carry a field-level read deny that deriveAuthLists sets unconditionally when it derives the list. Previously, granting operation-level query access to a derived auth list (e.g. to build a "your active sessions" screen) had no way to withhold just the credential column — the safe configuration was unexpressible (see the issue for the full analysis).

  • Added CREDENTIAL_FIELDS + withCredentialAccess in derive-auth-lists.ts, applied in the scalar-field derivation loop. Keyed by better-auth's own model/field key (not the app's list key or column db.map), so the deny survives a modelName remap and a column fields override.
  • A denied field is stripped from the result, not an error — this is the existing field-level access engine behavior, unchanged.
  • sudo() still reads all six fields — the supported path for a genuine need.
  • Identifying fields (ipAddress, userAgent, providerId, accountId, expiresAt, identifier, every User field) stay open.
  • examples/starter's own password field (not the auth-plugin-derived one — this example doesn't use authPlugin) also gets a read deny, with a comment pointing at context.sudo() for an app's own auth implementation.
  • Updated packages/auth/CLAUDE.md and docs/content/reference/auth.md to document the deny.
  • Added a minor changeset for @opensaas/stack-auth (pre-1.0 breaking-behavior policy) naming each affected field.

Test plan

  • Unit tests in derive-auth-lists.test.ts: each of the six fields denies read; identifying fields and every User field stay open; the deny survives a modelName + column fields remap.
  • New live e2e test credential-field-read-deny-e2e.test.ts (gated behind RUN_CREDENTIAL_DENY_E2E=1, same offline-toolchain pattern as rate-limit-e2e.test.ts/mcp-oauth-cascade-e2e.test.ts): generates a real schema, pushes to real SQLite, and proves — against a real betterAuth() instance — that an opened context.db read strips token/password/accessToken/refreshToken/idToken/value while sudo() still reads them, and that sign-up, sign-in, session refresh, and password reset all still work end-to-end with the denies in place. Wired into .github/workflows/test.yml's e2e job.
  • examples/starter regenerates cleanly (opensaas generate) with the new field-level access.
  • pnpm --filter @opensaas/stack-auth test (275 passed), pnpm --filter @opensaas/stack-core test (1164 passed)
  • pnpm lint, pnpm manypkg fix, pnpm format

Closes #981


Generated by Claude Code

Session.token, Verification.value, and Account.password/accessToken/
refreshToken/idToken now carry a field-level read deny in deriveAuthLists,
so granting operation-level access to a derived auth list no longer also
exposes these live credentials through context.db or the admin UI. A denied
field is stripped, not an error; sudo() still reads all six. The deny is
keyed to better-auth's own model/field, so it survives a modelName or
column remap.

Closes #981

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L3qvRgej7jAvdbvNC3c8C3
@vercel

vercel Bot commented Aug 22, 2026

Copy link
Copy Markdown

Deployment failed for project stack-docs with the following error:

Resource is limited - try again in 24 hours (more than 100, code: "api-deployments-free-per-day").

Learn More: https://vercel.com/open-saas?upgradeToPro=build-rate-limit

@changeset-bot

changeset-bot Bot commented Aug 22, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 39d8cbc

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 9 packages
Name Type
@opensaas/stack-auth Minor
@opensaas/stack-cli Minor
@opensaas/stack-core Minor
@opensaas/stack-rag Minor
@opensaas/stack-storage-s3 Minor
@opensaas/stack-storage-vercel Minor
@opensaas/stack-storage Minor
@opensaas/stack-tiptap Minor
@opensaas/stack-ui Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: cff354b66c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread .changeset/six-plugins-hide.md Outdated

Copy link
Copy Markdown
Member Author

Review (self-review, high effort)

One finding surfaced, worth recording even though I'm treating it as out of scope for this PR:

Plugin-table credential fields (e.g. the mcp/oidc-provider plugin's oauthApplication.clientSecret, oauthAccessToken.accessToken/refreshToken, or the twoFactor() plugin's secret/backupCodes) are not covered by this deny. CREDENTIAL_FIELDS in derive-auth-lists.ts is keyed by BaseModelKey (user/session/account/verification/rateLimit), so withCredentialAccess never matches a better-auth plugin table — even though the same derivation loop derives plugin tables through the identical scalar-field path. An app that follows the documented context.extendList('OauthAccessToken', { access }) pattern (packages/auth/CLAUDE.md) to build something like an "connected OAuth apps" admin screen would get the live accessToken/refreshToken back in plaintext, which is the same class of exposure ADR-0036 describes.

This is real, but both the issue and ADR-0036 explicitly enumerate exactly six fields across the four base models (Session.token, Verification.value, Account.password/accessToken/refreshToken/idToken) — plugin tables aren't mentioned in either. Widening this PR's scope to cover every plugin table's credential fields would require its own design pass (which plugin fields qualify, sourced from where — better-auth doesn't mark plugin-table fields with anything like the returned: false flag issue #981 was able to lean on for the base account columns). I'm leaving this PR scoped to the issue's stated six fields and will file a follow-up issue for the plugin-table gap rather than block merge on it here.


Generated by Claude Code

@github-actions

github-actions Bot commented Aug 22, 2026

Copy link
Copy Markdown
Contributor

Coverage Report for Core Package Coverage (./packages/core)

Status Category Percentage Covered / Total
🟢 Lines 94.05% (🎯 65%) 1631 / 1734
🟢 Statements 92.32% (🎯 65%) 1768 / 1915
🟢 Functions 97.41% (🎯 62%) 264 / 271
🟢 Branches 86.18% (🎯 50%) 1279 / 1484
File CoverageNo changed files found.
Generated in workflow #1787 for commit 39d8cbc by the Vitest Coverage Report Action

@github-actions

github-actions Bot commented Aug 22, 2026

Copy link
Copy Markdown
Contributor

Coverage Report for UI Package Coverage (./packages/ui)

Status Category Percentage Covered / Total
🔵 Lines 76.72% 244 / 318
🔵 Statements 76.29% 251 / 329
🔵 Functions 69.15% 74 / 107
🔵 Branches 64.25% 160 / 249
File CoverageNo changed files found.
Generated in workflow #1787 for commit 39d8cbc by the Vitest Coverage Report Action

@github-actions

github-actions Bot commented Aug 22, 2026

Copy link
Copy Markdown
Contributor

Coverage Report for CLI Package Coverage (./packages/cli)

Status Category Percentage Covered / Total
🔵 Lines 78.98% 1545 / 1956
🔵 Statements 78.79% 1613 / 2047
🔵 Functions 86.11% 217 / 252
🔵 Branches 69.5% 752 / 1082
File CoverageNo changed files found.
Generated in workflow #1787 for commit 39d8cbc by the Vitest Coverage Report Action

@github-actions

github-actions Bot commented Aug 22, 2026

Copy link
Copy Markdown
Contributor

Coverage Report for Auth Package Coverage (./packages/auth)

Status Category Percentage Covered / Total
🔵 Lines 99.4% 166 / 167
🔵 Statements 97.8% 178 / 182
🔵 Functions 100% 43 / 43
🔵 Branches 89.83% 159 / 177
File Coverage
File Stmts Branches Functions Lines Uncovered Lines
Changed Files
packages/auth/src/config/derive-auth-lists.ts 96.69% 87.32% 100% 99.08% 152, 153, 316-318, 621
Generated in workflow #1787 for commit 39d8cbc by the Vitest Coverage Report Action

@github-actions

github-actions Bot commented Aug 22, 2026

Copy link
Copy Markdown
Contributor

Coverage Report for Storage Package Coverage (./packages/storage)

Status Category Percentage Covered / Total
🔵 Lines 78.57% 220 / 280
🔵 Statements 80.06% 245 / 306
🔵 Functions 86.07% 68 / 79
🔵 Branches 75.88% 214 / 282
File CoverageNo changed files found.
Generated in workflow #1787 for commit 39d8cbc by the Vitest Coverage Report Action

@github-actions

github-actions Bot commented Aug 22, 2026

Copy link
Copy Markdown
Contributor

Coverage Report for RAG Package Coverage (./packages/rag)

Status Category Percentage Covered / Total
🔵 Lines 47.97% 355 / 740
🔵 Statements 48.14% 377 / 783
🔵 Functions 54.26% 70 / 129
🔵 Branches 42.55% 180 / 423
File CoverageNo changed files found.
Generated in workflow #1787 for commit 39d8cbc by the Vitest Coverage Report Action

@github-actions

github-actions Bot commented Aug 22, 2026

Copy link
Copy Markdown
Contributor

Coverage Report for Storage S3 Package Coverage (./packages/storage-s3)

Status Category Percentage Covered / Total
🔵 Lines 100% 40 / 40
🔵 Statements 100% 40 / 40
🔵 Functions 100% 9 / 9
🔵 Branches 100% 19 / 19
File CoverageNo changed files found.
Generated in workflow #1787 for commit 39d8cbc by the Vitest Coverage Report Action

@github-actions

github-actions Bot commented Aug 22, 2026

Copy link
Copy Markdown
Contributor

Coverage Report for Storage Vercel Package Coverage (./packages/storage-vercel)

Status Category Percentage Covered / Total
🔵 Lines 100% 68 / 68
🔵 Statements 100% 71 / 71
🔵 Functions 100% 15 / 15
🔵 Branches 97.87% 46 / 47
File CoverageNo changed files found.
Generated in workflow #1787 for commit 39d8cbc by the Vitest Coverage Report Action

claude added 2 commits August 22, 2026 07:54
A Codex review comment on the PR flagged the changeset's blanket "not an
error" claim. Verified the actual behavior: findMany's/count's where/orderBy
naming a denied credential field throws ValidationError up front
(validateQueryFieldReadAccess); findUnique's where only unique-selects the
row and is not walked by that check, so a lookup by the denied field still
succeeds with the field stripped from the result. Corrected the changeset,
docs/content/reference/auth.md, and packages/auth/CLAUDE.md to describe both
paths accurately, and added e2e coverage for the findMany-throws case.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L3qvRgej7jAvdbvNC3c8C3
…ck653y

# Conflicts:
#	packages/auth/src/config/derive-auth-lists.ts
@borisno2
borisno2 enabled auto-merge (squash) August 22, 2026 09:30
@borisno2
borisno2 merged commit 49687ea into main Aug 22, 2026
4 of 5 checks passed
@borisno2
borisno2 deleted the claude/focused-rubin-ck653y branch August 22, 2026 09:35
@github-actions github-actions Bot mentioned this pull request Aug 22, 2026
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.

auth: derived auth lists store live credentials in plain text() fields with no redaction and no field-level access seam

2 participants