Skip to content

fix(auth): require JWT_SECRET_KEY in prod + run config validation (C1/C2)#56

Open
PenguinzTech wants to merge 1 commit into
release/v0.1.xfrom
fix/auth-secrets-config
Open

fix(auth): require JWT_SECRET_KEY in prod + run config validation (C1/C2)#56
PenguinzTech wants to merge 1 commit into
release/v0.1.xfrom
fix/auth-secrets-config

Conversation

@PenguinzTech

@PenguinzTech PenguinzTech commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Security fixes (Track A1)

Targets release/v0.1.x (branched off v1.x — the active branch; main is stale).

C1 — JWT signing key no longer falls back to the dev default in production

ProductionConfig now requires JWT_SECRET_KEY via env var (no fallback to the base dev-secret-key-change-in-production), and validate() rejects the dev default. Closes an admin-token forgery vector where prod silently signed tokens with a public key.

C2 — Production config validation actually runs

create_app() now calls ProductionConfig.validate() at startup (guarded by hasattr so Dev/Testing configs are unaffected). Previously the guard was dead code (only called from tests), so prod could boot with default secrets.

Tests

19 config tests pass; mypy --strict compatible; Dev/Testing boot unaffected.

Part of the security-hardening series on release/v0.1.x.

🤖 Generated with Claude Code

Summary by Sourcery

Enforce production-time configuration validation and require an explicit JWT secret in production to prevent insecure defaults.

Bug Fixes:

  • Require JWT_SECRET_KEY to be explicitly set in production and reject the development default key value.
  • Run production configuration validation at application startup to prevent booting with missing or insecure secrets.

…startup

Fixes two critical findings:
- C1: JWT_SECRET_KEY no longer falls back to the dev SECRET_KEY default in
  production. ProductionConfig now requires it via env var and validate()
  rejects the dev default, closing an admin-token forgery vector.
- C2: create_app() now calls ProductionConfig.validate() at startup (guarded
  by hasattr so Dev/Testing configs are unaffected), so the dead secret-guard
  actually runs and the app boot-fails on default/missing secrets.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@PenguinzTech PenguinzTech added this to the v0.1.x milestone Jul 14, 2026
@PenguinzTech PenguinzTech added the type:security Security fix label Jul 14, 2026
@sourcery-ai

sourcery-ai Bot commented Jul 14, 2026

Copy link
Copy Markdown
Reviewer's guide (collapsed on small PRs)

Reviewer's Guide

This PR hardens production authentication by requiring a dedicated JWT secret key and ensuring production config validation is executed at app startup, causing startup to fail if required secrets are missing or insecure.

Sequence diagram for production config validation during app startup

sequenceDiagram
    participant Caller
    participant create_app
    participant ProductionConfig
    participant QuartApp

    Caller->>create_app: create_app(config_class)
    create_app->>QuartApp: Quart(__name__)
    alt config_class is None
        create_app->>create_app: get_config()
    end
    create_app->>QuartApp: config.from_object(config_class)
    alt hasattr(config_class, validate)
        create_app->>ProductionConfig: validate()
        alt [missing or insecure secrets]
            ProductionConfig-->>Caller: ValueError
        else [valid secrets]
            ProductionConfig-->>create_app: None
            create_app->>create_app: _setup_logging(app)
        end
    else no validate attribute
        create_app->>create_app: _setup_logging(app)
    end
Loading

File-Level Changes

Change Details Files
Run production config validation during app startup so invalid or missing secrets fail fast.
  • Updated create_app to call a validate classmethod on the active config if present.
  • Documented that create_app may raise ValueError when production config validation fails.
  • Left dev/testing configs unaffected by guarding the validation call with hasattr.
services/flask-backend/app/__init__.py
Require a non-default JWT secret in production and extend validation to enforce it.
  • Added JWT_SECRET_KEY as a required environment-provided setting in ProductionConfig.
  • Extended ProductionConfig.validate to reject missing JWT_SECRET_KEY or the known dev default value.
  • Kept existing SECRET_KEY and SECURITY_PASSWORD_SALT validation logic intact.
services/flask-backend/app/config.py

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai 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.

Hey - I've found 1 issue, and left some high level feedback:

  • The create_app docstring and comment say validation is “production only”, but the hasattr(config_class, "validate") check will run it for any config that defines validate; consider tightening this to an explicit ProductionConfig (or subclass) check or updating the comment/docstring to match the behavior.
  • Instead of only checking hasattr(config_class, "validate"), you might also confirm it’s callable (e.g. callable(getattr(config_class, "validate", None))) to avoid surprising behavior if a non-callable attribute named validate is introduced on a config class.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The `create_app` docstring and comment say validation is “production only”, but the `hasattr(config_class, "validate")` check will run it for any config that defines `validate`; consider tightening this to an explicit `ProductionConfig` (or subclass) check or updating the comment/docstring to match the behavior.
- Instead of only checking `hasattr(config_class, "validate")`, you might also confirm it’s callable (e.g. `callable(getattr(config_class, "validate", None))`) to avoid surprising behavior if a non-callable attribute named `validate` is introduced on a config class.

## Individual Comments

### Comment 1
<location path="services/flask-backend/app/config.py" line_range="180-183" />
<code_context>
     # Strict security in production
     SECURITY_PASSWORD_SALT = os.getenv("SECURITY_PASSWORD_SALT")  # Required
     SECRET_KEY = os.getenv("SECRET_KEY")  # Required
+    JWT_SECRET_KEY = os.getenv("JWT_SECRET_KEY")  # Required — must not fall back to dev default

     @classmethod
</code_context>
<issue_to_address>
**suggestion:** The hardcoded dev default string in validation is brittle and may drift from the actual dev default.

This check couples production validation to the hardcoded string `"dev-secret-key-change-in-production"`, which can drift if the dev default changes elsewhere. Prefer referencing a single canonical dev/default value (e.g., `Config.DEFAULT_JWT_SECRET_KEY` or `DevelopmentConfig.JWT_SECRET_KEY`) rather than inlining the literal here.

Suggested implementation:

```python
        if (
            not cls.JWT_SECRET_KEY
            or cls.JWT_SECRET_KEY == DevelopmentConfig.JWT_SECRET_KEY
        ):

```

If `DevelopmentConfig.JWT_SECRET_KEY` is not yet defined or the development default is stored elsewhere, you should:
1. Define the canonical dev default in one place (e.g., `DevelopmentConfig.JWT_SECRET_KEY = "dev-secret-key-change-in-production"` or `Config.DEFAULT_JWT_SECRET_KEY = "dev-secret-key-change-in-production"`).
2. Update this production validation and any other dev default usages to reference that single canonical location.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +180 to 183
JWT_SECRET_KEY = os.getenv("JWT_SECRET_KEY") # Required — must not fall back to dev default

@classmethod
def validate(cls) -> None:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

suggestion: The hardcoded dev default string in validation is brittle and may drift from the actual dev default.

This check couples production validation to the hardcoded string "dev-secret-key-change-in-production", which can drift if the dev default changes elsewhere. Prefer referencing a single canonical dev/default value (e.g., Config.DEFAULT_JWT_SECRET_KEY or DevelopmentConfig.JWT_SECRET_KEY) rather than inlining the literal here.

Suggested implementation:

        if (
            not cls.JWT_SECRET_KEY
            or cls.JWT_SECRET_KEY == DevelopmentConfig.JWT_SECRET_KEY
        ):

If DevelopmentConfig.JWT_SECRET_KEY is not yet defined or the development default is stored elsewhere, you should:

  1. Define the canonical dev default in one place (e.g., DevelopmentConfig.JWT_SECRET_KEY = "dev-secret-key-change-in-production" or Config.DEFAULT_JWT_SECRET_KEY = "dev-secret-key-change-in-production").
  2. Update this production validation and any other dev default usages to reference that single canonical location.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

type:security Security fix

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant