Skip to content

fix(api): refuse to run without a strong JWT secret - #230

Merged
bordumb merged 2 commits into
mainfrom
claude/optimistic-swartz-jwt-secret
Sep 28, 2026
Merged

bordumb merged 2 commits into
mainfrom
claude/optimistic-swartz-jwt-secret

Conversation

@bordumb

@bordumb bordumb commented Sep 28, 2026

Copy link
Copy Markdown
Owner

Security fix. The API signed and verified every login token (HS256) with the key "dev-secret-change-in-production" whenever JWT_SECRET_KEY was unset, and nothing warned about it. On any deployment that never set the variable, anyone who has read the public source could mint an access token for any user, org and role.

⚠️ Operator action before upgrading

  1. Set JWT_SECRET_KEY to 32+ random bytes: openssl rand -hex 32. Without it the API exits at startup with:
    JWTSecretKeyError: JWT_SECRET_KEY is not set; it must be at least 32 random bytes. Generate one with: openssl rand -hex 32
  2. Expect users to sign in again. Tokens signed with the old default stop verifying.

What changed

  • No fallback. core/auth/jwt.py has a new jwt_secret_key(). It returns JWT_SECRET_KEY, or raises JWTSecretKeyError if the key is unset or shorter than 32 bytes (the HS256 minimum from RFC 7518 §3.2; pyjwt 2.15 also warns below it). All token paths call it: issuing, verifying, refresh, and the SSE ?token= query path.
  • Fails at startup. create_app() checks the key first. The CE and EE apps (create_ee_app() builds on create_app()) both refuse to start without one. The Temporal worker signs no tokens and needs no key.
  • Local flows still work:
    • just dev, just dev-backend and just dev-backend-ce keep a 32+ byte key from .env or the shell. Otherwise they use a random key for that run.
    • just demo generates a key if neither the shell nor .env has a valid one. just demo-infra starts no API and needs no key.
    • docker-compose.yml passes JWT_SECRET_KEY to the api service. demo/docker-compose.demo.yml reuses that service unchanged.
    • .env.example, infra/check-env.sh, docs/test-quickstart.sh, the quickstart and the deployment docs all require the key.
  • CLI, SDK and notebook: no change needed. They only carry tokens issued by the API. The EE OIDC jwt.decode verifies IdP tokens against the IdP's keys, so it is unaffected.

Tests

  • The CE and EE conftests set a 32+ byte test key, which removes pyjwt's InsecureKeyLengthWarning: 0 in all four runs below.
  • New tests:
    • Issuing an access or refresh token and decoding a token all raise with an unset, empty, 31-byte or old-default key.
    • A 32-byte key round-trips.
    • A token forged with the old default key is rejected.
    • create_app() and create_ee_app() refuse to start.
  • Red check: with the old fallback put back, 6 of the new tests fail.
Run Result
uv run pytest python-packages/dataing/tests 3167 passed, 64 skipped
uv run pytest python-packages/dataing-ee/tests 715 passed
CE -m integration, pgvector:pg16 throwaway container 130 passed
EE -m integration, same container 16 passed
mypy, ruff check and format, detect-secrets clean

🤖 Generated with Claude Code

core/auth/jwt.py fell back to the key "dev-secret-change-in-production"
when JWT_SECRET_KEY was unset. Every login token is HS256-signed with that
key, so on any deployment that never set the variable, anyone who has read
the source could mint an access token for any user, org and role. Nothing
warned that the variable was missing.

There is no fallback now. jwt_secret_key() returns JWT_SECRET_KEY, or raises
JWTSecretKeyError when it is unset or shorter than 32 bytes (the HS256
minimum from RFC 7518 section 3.2, which pyjwt 2.15 also warns about).
Issuing and verifying tokens both call it, including the SSE ?token= path.
create_app() calls it first, so the CE and EE APIs refuse to start without
a key. The Temporal worker signs no tokens and needs none.

Local flows keep working:
- just dev, dev-backend and dev-backend-ce keep a 32+ byte key from .env or
  the shell, and otherwise use a random key for that run
- just demo generates a key when neither the shell nor .env has a good one
- docker-compose passes JWT_SECRET_KEY to the api service
- .env.example, infra/check-env.sh, the quickstart and the deployment docs
  say to set it (openssl rand -hex 32)

The CE and EE test conftests set a 32+ byte key, so pyjwt's
InsecureKeyLengthWarning is gone. New tests check that:
- no token is issued or verified with an unset, empty, 31-byte or
  old-default key
- a token forged with the old default key is rejected
- create_app() and create_ee_app() refuse to start without a key

Operators: set JWT_SECRET_KEY before upgrading, or the API will not start.
Tokens signed with the old default stop verifying, so users sign in again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Claude <noreply@anthropic.com>
@bordumb bordumb added the bug Something isn't working label Sep 28, 2026
@vercel

vercel Bot commented Sep 28, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
dataing Ready Ready Preview Sep 28, 2026 7:16pm UTC
dataing-app Ready Ready Preview Sep 28, 2026 7:16pm UTC
dataing-docs Ready Ready Preview Sep 28, 2026 7:16pm UTC

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Claude <noreply@anthropic.com>
@vercel

vercel Bot commented Sep 28, 2026

Copy link
Copy Markdown

Deployment failed for project dataing-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/bordumbs-projects?upgradeToPro=build-rate-limit

@vercel

vercel Bot commented Sep 28, 2026

Copy link
Copy Markdown

Deployment failed for project dataing 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/bordumbs-projects?upgradeToPro=build-rate-limit

@vercel

vercel Bot commented Sep 28, 2026

Copy link
Copy Markdown

Deployment failed for project dataing-app 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/bordumbs-projects?upgradeToPro=build-rate-limit

@bordumb
bordumb merged commit 58e44b0 into main Sep 28, 2026
4 of 7 checks passed
github-actions Bot pushed a commit that referenced this pull request Sep 28, 2026
## [1.25.4](v1.25.3...v1.25.4) (2026-09-28)

### Bug Fixes

* **api:** refuse to run without a strong JWT secret ([#230](#230)) ([58e44b0](58e44b0))
bordumb added a commit that referenced this pull request Sep 29, 2026
…ate it (#232)

* fix(frontend): keep orval 8 from turning GET hooks into mutations

#221 bumped orval from 6.31 to 8 without regenerating the client. Run
on the committed spec, orval 8 rewrote 293 files: every GET hook became
a useMutation and every POST/PUT/DELETE hook a useQuery, request
functions switched to the fetch client's (url, init) call that
customInstance does not accept, imports gained a ".ts" suffix, and
models lost their property order and named nullable types.

Orval 8 applies override.query.useQuery and useMutation to every verb,
and a GET with both becomes a mutation. Leave them unset: GETs then get
query hooks and the other verbs mutation hooks, as under orval 6.

Set the orval 6 behaviour that orval 8 changed:
- httpClient axios: customInstance takes one request config object
- propertySortOrder Alphabetical: model properties keep their order
- aliasCombinedTypes: nullable properties keep their named types
- a tsconfig without allowImportingTsExtensions, so imports stay
  extensionless

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Claude <noreply@anthropic.com>

* chore(frontend): regenerate the API client with orval 8

Output of `pnpm orval` on the committed openapi.json with the orval 8
config from the previous commit. A second run changes nothing.

The hooks consumers use keep their shape. The 28 regenerated tag files
remove or rename no export; their 76 GET hooks stay useQuery and their
90 other hooks useMutation; all 76 query keys are unchanged. Model
types match except for JSDoc and three tighter types
(InvestigationBrief.version is the literal 1, an upload is
Blob | File, UpdateUserRequestRole also exports its values).

Orval 8's React Query v5 output makes most of the size:
- query hooks gain initialData overloads, a DataTag query key and an
  optional queryClient argument
- mutations gain a mutation key and a named variables type
- non-GET request functions take an optional AbortSignal
- path-param queries are enabled when the param is not null or
  undefined (it was truthy), so an empty string now runs the query;
  no caller passes one
- headers drop the orval version, so upgrades stop touching every file

Orval 8 also writes generated/index.ts, which re-exports every tag.
Nothing imports it; it is kept so a regeneration leaves no untracked
file. Turning it off (indexFiles: false) would also stop orval
maintaining model/index.ts.

The 125 files the committed spec no longer produces (the audit,
feedback, runs, scim, settings and sso tags and 119 models) are left
alone and keep their orval 6 header.

The detect-secrets hook moved the baseline entry for configFieldType.ts
up a line, since orval 8 drops an eslint-disable comment there.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Claude <noreply@anthropic.com>

* fix(infra): let just generate-client export the schema without a JWT key

Since #230 the app refuses to build without a JWT signing key of 32+
bytes, so export_openapi.py, which imports it, fails before orval runs.
The schema doesn't depend on the key: use a throwaway one when none is
set, as the dev recipes do.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Claude <noreply@anthropic.com>

---------

Signed-off-by: Claude <noreply@anthropic.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
github-actions Bot pushed a commit that referenced this pull request Sep 29, 2026
## [1.25.5](v1.25.4...v1.25.5) (2026-09-29)

### Bug Fixes

* **frontend:** keep the API client's shapes under orval 8 and regenerate it ([#232](#232)) ([5831074](5831074)), closes [#221](#221) [#230](#230)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant