Skip to content

fix(backend): refresh claims, startup table defs, tz-aware datetime, logged errors, safe admin seed (OM4/M1/M2/M3/H2)#62

Open
PenguinzTech wants to merge 1 commit into
fix/token-revocation-ratelimitfrom
fix/backend-hygiene
Open

fix(backend): refresh claims, startup table defs, tz-aware datetime, logged errors, safe admin seed (OM4/M1/M2/M3/H2)#62
PenguinzTech wants to merge 1 commit into
fix/token-revocation-ratelimitfrom
fix/backend-hygiene

Conversation

@PenguinzTech

@PenguinzTech PenguinzTech commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Security fixes (Track A5b) — backend hygiene

Stacked on #61 (base = fix/token-revocation-ratelimit). Targets release/v0.1.x. (Companion to A5a #60, which covers licensing/OIDC-secret/provider gating on the OIDC branch.)

  • OM4 — refresh grant (both /refresh and /oauth/token) resolves tenant_id + scopes like the password grant, so refreshed tokens keep their claims (previously de-scoped/de-tenanted).
  • M1 — rbac + oidc tables (and the former inline create_user table) are defined once at startup in init_db, eliminating the request-time check-then-define race on the shared DAL across executor threads.
  • M2datetime.utcnow()/utcfromtimestamp() → timezone-aware datetime.now(timezone.utc) in token exp/iat + expiry comparisons.
  • M3 — silent except Exception: pass around tenant/scope resolution + revoke storage → logged warnings (no PII/secrets), so degraded-permission failures are visible.
  • H2 — production refuses to seed the default admin with the built-in weak password; requires DEFAULT_ADMIN_PASSWORD or skips seeding. Dev keeps the convenience default with a warning.

Tests

Suite: 467 passed, 90.44% coverage.

🤖 Generated with Claude Code

Summary by Sourcery

Harden backend auth and token handling by preserving claims on refresh, making token timestamps timezone-aware, removing request-time table definition races, logging previously silent failures, and enforcing safer default-admin seeding behavior.

Bug Fixes:

  • Ensure refresh and OAuth refresh grants re-resolve tenant and scopes so refreshed access tokens retain their claims.
  • Make token creation, validation, and expiry checks use timezone-aware UTC datetimes instead of naive utcnow/utcfromtimestamp.

Enhancements:

  • Define RBAC and OIDC tables upfront at startup to avoid request-time race conditions on table creation.
  • Replace silent exception swallowing in tenant/scope resolution and token revocation storage with structured logging for visibility.

Tests:

  • Add backend hardening tests covering token formats, timezone-aware timestamps, refresh behavior, and login error handling.

Chores:

  • Tighten default admin seeding by requiring explicit strong passwords in production while retaining a logged weak default only in development.

…time, logged errors, safe admin seed (OM4/M1/M2/M3/H2)

- OM4: refresh grant (both /refresh and /oauth/token) now resolves tenant_id + scopes
  like the password grant, so refreshed access tokens keep their tenant/scope claims.
- M1: define rbac + oidc tables (and the former inline create_user table) once at
  startup in init_db, eliminating the request-time check-then-define race on the
  shared DAL across executor threads.
- M2: datetime.utcnow()/utcfromtimestamp() -> timezone-aware datetime.now(timezone.utc)
  in token exp/iat and expiry comparisons.
- M3: replace silent except: pass around tenant/scope resolution and revoke storage
  with logged warnings (no PII/secrets), so degraded-permission failures are visible.
- H2: in production, refuse to seed the default admin with the built-in weak password;
  require DEFAULT_ADMIN_PASSWORD to be set or skip seeding. Dev keeps the convenience
  default with a prominent warning.

Suite: 467 passed, 90.44% coverage.

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

Backend hardening PR that makes refresh-token grants preserve tenant/scopes, defines RBAC/OIDC tables eagerly at startup, switches token timestamps and expiry checks to timezone-aware datetimes, replaces previously-silent exception swallowing with structured logging, and prevents seeding a production admin user with a built-in weak password, plus tests for these behaviors.

Sequence diagram for refresh token grant preserving tenant and scopes

sequenceDiagram
    actor Client
    participant AuthRefresh as refresh
    participant DB as get_db
    participant RBAC as _define_rbac_tables
    participant Scopes as get_user_scopes
    participant Tokens as create_access_token
    participant Refresh as create_refresh_token
    participant AsyncDB as run_sync
    participant Revoke as revoke_refresh_token

    Client->>AuthRefresh: POST /auth/refresh
    AuthRefresh->>AsyncDB: run_sync(revoke_refresh_token, token_hash)
    AsyncDB->>Revoke: revoke_refresh_token(token_hash)
    Revoke-->>AsyncDB: done
    AsyncDB-->>AuthRefresh: done

    AuthRefresh->>AsyncDB: run_sync(_resolve_tenant, user.id)
    AsyncDB->>DB: get_db()
    DB-->>AsyncDB: db
    AsyncDB->>RBAC: _define_rbac_tables(db)
    RBAC-->>AsyncDB: tables ready
    AsyncDB-->>AuthRefresh: tenant_id, tenant_slug

    AuthRefresh->>AsyncDB: run_sync(get_user_scopes, user.id, tenant_id)
    AsyncDB->>Scopes: get_user_scopes(user.id, tenant_id)
    Scopes-->>AsyncDB: user_scopes
    AsyncDB-->>AuthRefresh: user_scopes

    AuthRefresh->>Tokens: create_access_token(user.id, user.role, tenant_id, tenant_slug, scopes=user_scopes)
    Tokens-->>AuthRefresh: access_token

    AuthRefresh->>AsyncDB: run_sync(create_refresh_token, user.id)
    AsyncDB->>Refresh: create_refresh_token(user.id)
    Refresh-->>AsyncDB: new_refresh_token, refresh_expires
    AsyncDB-->>AuthRefresh: new_refresh_token, refresh_expires

    AuthRefresh-->>Client: JSON(access_token, new_refresh_token)
Loading

File-Level Changes

Change Details Files
Eagerly define RBAC and OIDC tables at startup and simplify user-role assignment to avoid request-time table-definition races.
  • Extend init_db to import and call internal _define_rbac_tables and _define_oidc_tables so RBAC/OIDC tables are created once at startup instead of lazily per request.
  • Adjust create_user to assume user_role_assignments table exists (created at startup) and only insert global role assignments when the table is present.
services/flask-backend/app/models.py
Harden default admin seeding so production never uses the built-in weak password while keeping a convenience path in development, with consistent logging/printing.
  • Change _seed_default_admin to accept the Quart app, read RELEASE_MODE, and require DEFAULT_ADMIN_PASSWORD in production while refusing the known weak default; in development, fall back to the weak default with warnings and structured logging.
  • Update run.create_default_admin to mirror the same RELEASE_MODE/DEFAULT_ADMIN_PASSWORD behavior, add logging, hash the password explicitly, and avoid printing the password warning in production.
services/flask-backend/app/models.py
services/flask-backend/run.py
Make token creation and validation timezone-aware and ensure refresh token expiry comparisons use aware datetimes.
  • Replace datetime.utcnow()/utcfromtimestamp() with datetime.now(timezone.utc) and datetime.fromtimestamp(..., tz=timezone.utc) in access/refresh token creation and revoked-token storage.
  • Update refresh token validity checks to compare expires_at against a timezone-aware now value.
services/flask-backend/app/models.py
services/flask-backend/app/auth.py
Expose previously-silent failures in tenant/scope resolution and revoke storage via logging while maintaining graceful degradation.
  • Add module-level loggers and convert bare except Exception: pass around tenant resolution, scope resolution, default-tenant assignment, and revoked-access-token storage into exception handlers that log warnings (with contextual IDs but no secrets) or errors, then continue.
  • Ensure these changes apply consistently across login, OAuth password grant, OAuth refresh grant, and revoke-token storage paths.
services/flask-backend/app/models.py
services/flask-backend/app/auth.py
services/flask-backend/app/oauth.py
Make refresh-token flows preserve tenant and scope claims the same way as the password grant and login flows (OM4).
  • Extend /api/v1/auth/refresh handler to resolve the user’s default tenant and scopes (mirroring login’s behavior) and pass tenant_id, tenant_slug, and scopes into create_access_token when issuing a new access token.
  • Update OAuth2 refresh grant handler to mirror the password-grant logic by resolving tenant membership and scopes on refresh and including those claims in the new access token.
services/flask-backend/app/auth.py
services/flask-backend/app/oauth.py
Add regression tests for backend hardening behaviors (OM4, M2, M3, H2).
  • Create test_backend_hardening.py with async tests that validate JWT format/claims for login and refresh, confirm timezone-aware token timestamps and non-expired refresh tokens, ensure login gracefully handles tenant-resolution failures, and implicitly exercise the new refresh behavior.
  • Use timezone-aware datetime operations in tests to match production code semantics and avoid naive/aware comparison issues.
services/flask-backend/tests/test_backend_hardening.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 left some high level feedback:

  • The default-admin seeding logic is now implemented both in app.models._seed_default_admin and run.create_default_admin with slightly different behaviors (logging vs prints); consider consolidating this into a single helper or ensuring one is the canonical path to avoid drift.
  • The tenant and scope resolution logic for refresh/password grants and login (e.g., _resolve_tenant in auth.refresh, auth.login, and oauth._handle_*_grant) is nearly identical; extracting a shared helper to encapsulate this behavior would reduce duplication and make future changes easier.
  • In create_user, the new guard on user_role_assignments silently skips inserting a global role when the table is missing; if this situation is unexpected in normal operation, you might want to log a warning to make misconfigured RBAC setups visible.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The default-admin seeding logic is now implemented both in `app.models._seed_default_admin` and `run.create_default_admin` with slightly different behaviors (logging vs prints); consider consolidating this into a single helper or ensuring one is the canonical path to avoid drift.
- The tenant and scope resolution logic for refresh/password grants and login (e.g., `_resolve_tenant` in `auth.refresh`, `auth.login`, and `oauth._handle_*_grant`) is nearly identical; extracting a shared helper to encapsulate this behavior would reduce duplication and make future changes easier.
- In `create_user`, the new guard on `user_role_assignments` silently skips inserting a global role when the table is missing; if this situation is unexpected in normal operation, you might want to log a warning to make misconfigured RBAC setups visible.

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.

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