Skip to content

fix: standardize API token format across all creation paths#44

Merged
wicky-zipstack merged 5 commits intomainfrom
fix/consistent-api-token-format
Apr 9, 2026
Merged

fix: standardize API token format across all creation paths#44
wicky-zipstack merged 5 commits intomainfrom
fix/consistent-api-token-format

Conversation

@wicky-zipstack
Copy link
Copy Markdown
Contributor

@wicky-zipstack wicky-zipstack commented Apr 7, 2026

What

  • Standardize API token format to use vtk_ prefix across all 3 token creation paths
  • All auto-generated tokens now follow the same pattern as KeyManagement's create_api_key()

Why

  • user.py (get_or_create_valid_token) was generating tokens with uuid4().hex — plain hex, no vtk_ prefix, no label
  • views.py (update_user_token) stored raw frontend token with no format enforcement
  • api_tokens/views.py (generate_token legacy endpoint) generated vtk_ but never saved to DB
  • Inconsistent token formats cause issues when tokens are displayed in KeyManagement UI

How

  • user.py: Replaced uuid4().hex with generate_api_key(), added label="Default", generate_signature(), and API_KEY_EXPIRY_DAYS
  • views.py: Now generates fresh vtk_ key via generate_api_key() instead of storing raw frontend value
  • api_tokens/views.py: Legacy /token/generate now creates a proper APIToken DB record with label, signature, and expiry

Can this PR break any existing features. If yes, please list possible items. If no, please explain why. (PS: Admins do not merge the PR without this section filled)

  • No — existing tokens in DB are unaffected, only new tokens will use the standardized format
  • Legacy uuid4().hex tokens already in DB will continue to work until they expire
  • KeyManagement create_api_key flow is completely unchanged

Database Migrations

  • None — no schema changes, only the token value format and additional fields populated on creation

Env Config

  • Uses existing API_KEY_EXPIRY_DAYS setting (default: 90 days)
  • No new env vars required

Relevant Docs

  • N/A

Related Issues or PRs

  • N/A

Dependencies Versions

  • None — uses existing api_key_service.py functions

Notes on Testing

  • Verify get_or_create_valid_token creates tokens with vtk_ prefix
  • Verify Profile page regenerate produces vtk_ token saved to DB
  • Verify update_user_token creates proper APIToken record
  • Verify tokens appear in KeyManagement list with "Default" label
  • Verify existing create_api_key flow (KeyManagement) still works unchanged

Screenshots

N/A — backend-only changes

Checklist

Three places were creating API tokens with inconsistent formats:
- user.py: used uuid4().hex (no vtk_ prefix, no label, no signature)
- views.py: stored raw frontend token (no format enforcement)
- api_tokens/views.py: generated vtk_ but never persisted to DB

All now use generate_api_key() for consistent vtk_ prefix, with label
"Default", proper signature, and configurable expiry via API_KEY_EXPIRY_DAYS.
@greptile-apps
Copy link
Copy Markdown

greptile-apps bot commented Apr 7, 2026

Greptile Summary

This PR standardizes API token generation across three previously inconsistent code paths (user.py, views.py, and api_tokens/views.py). All paths now produce vtk_-prefixed tokens via generate_api_key(), store a signature, carry a "Default" label, and respect API_KEY_EXPIRY_DAYS. Additionally, the model's save() hook now always recalculates token_hash (removing the stale-hash bug in the old regenerate_api_key path), and regenerate_api_key was changed from a partial update_fields save to a full save() so the hash is always persisted.

Previous review concerns have been addressed:

  • generate_token now uses an upsert (update-in-place) rather than a destructive delete-all-Default approach.
  • update_user_token scopes its filter to label=\"Default\" so user-managed keys are never touched.
  • Audit events are emitted on both paths for create and regenerate actions.

One small gap remains: the else branch of update_user_token (when no token_value is provided) silently deletes an existing Default token without emitting a log_api_key_event(\"delete\", ...) call, making this the only deletion path in the codebase that goes unaudited.

Confidence Score: 5/5

Safe to merge — all previous P1 concerns are addressed, the only remaining finding is a P2 audit-log gap in an edge-case branch.

The PR resolves all prior P1/P0 issues (unscoped deletes, missing audit events on create/regenerate, stale token_hash after regeneration). The single new finding — a missing log_api_key_event call in the else branch of update_user_token — is P2: it affects traceability only, not correctness or security. All three token creation paths now produce consistent vtk_-prefixed tokens with labels and expiry, and the model's hash recalculation is correct.

backend/backend/core/views.py — else branch of update_user_token is missing an audit delete event.

Vulnerabilities

No security concerns identified. Tokens are generated with secrets.token_urlsafe(32) (cryptographically secure), stored as plaintext (by design, existing pattern), and returned once in plaintext on creation — consistent with the pre-existing create_api_key behaviour. No new input vectors, no new privilege escalation paths, and no secrets are leaked beyond what the endpoint intends to return.

Important Files Changed

Filename Overview
backend/backend/core/models/api_tokens.py Removed and not self.token_hash guard so token_hash is recalculated on every save() — intentional fix that ensures hash stays in sync after token regeneration and is always persisted on full saves.
backend/backend/core/routers/api_tokens/views.py Legacy generate_token endpoint refactored to upsert a real APIToken DB record with vtk_ prefix, label, expiry, and audit log; regenerate_api_key switched to full save() so token_hash is now correctly refreshed.
backend/backend/core/user.py Replaced uuid4().hex with generate_api_key() and added signature, label="Default", and API_KEY_EXPIRY_DAYS — clean, consistent with the other creation paths.
backend/backend/core/views.py update_user_token now generates a proper vtk_ token with audit logging on create/regenerate, but the else branch deletes an existing Default token without emitting a log_api_key_event("delete") call.

Sequence Diagram

sequenceDiagram
    participant FE as Frontend
    participant VP as views.py (update_user_token)
    participant AT as api_tokens/views.py (generate_token)
    participant UP as user.py (get_or_create_valid_token)
    participant DB as APIToken DB
    participant AUD as Audit Log

    Note over FE,AUD: Path 1 - Profile page token regeneration
    FE->>VP: PUT /profile
    VP->>DB: filter(user, label=Default)
    DB-->>VP: existing token or None
    alt token unchanged
        VP-->>FE: first_name, last_name only
    else token changed or new
        VP->>DB: existing.delete()
        VP->>DB: APIToken.create(vtk_ key, label=Default)
        DB-->>VP: new token
        VP->>AUD: log_api_key_event(create/regenerate)
        VP-->>FE: first_name, last_name, token
    else token_value empty
        VP->>DB: existing.delete()
        Note over VP,AUD: No audit event emitted
        VP-->>FE: first_name, last_name
    end

    Note over FE,AUD: Path 2 - Legacy /token/generate endpoint
    FE->>AT: POST /token/generate
    AT->>DB: filter(user, label=Default) order by -created_at
    DB-->>AT: existing or None
    alt existing found
        AT->>DB: existing.save() upsert vtk_ key
        AT->>AUD: log_api_key_event(regenerate)
    else no existing
        AT->>DB: APIToken.create(vtk_ key, label=Default)
        AT->>AUD: log_api_key_event(create)
    end
    AT-->>FE: token value

    Note over FE,AUD: Path 3 - Auto-provisioning on login
    UP->>DB: raw_objects.select_for_update().filter(user, org)
    DB-->>UP: token or None
    alt token invalid or expired
        UP->>DB: token.delete()
        UP->>DB: APIToken.create(vtk_ key, label=Default, org=org)
    end
    UP-->>FE: token
Loading
Prompt To Fix All With AI
This is a comment left during a code review.
Path: backend/backend/core/views.py
Line: 84-86

Comment:
**Missing audit event on token deletion (else branch)**

When `token_value` is falsy (the frontend sends an empty/absent token field), an existing `"Default"` token is deleted, but no `log_api_key_event` call is made. Every other deletion path in the codebase — `delete_api_key` in `api_tokens/views.py` and the `if token_value:` branch just above — emits an audit event. This branch is the only one that silently drops the record with no trail.

The fix is straightforward: capture the token metadata before calling `.delete()` (since the object is gone after), then call `log_api_key_event(request, action="delete", ...)` — mirroring the pattern already used in `delete_api_key`.

How can I resolve this? If you propose a fix, please make it concise.

Reviews (5): Last reviewed commit: "fix: address CodeQL SHA-256 finding and ..." | Re-trigger Greptile

- P1: Replace delete-all-by-label with upsert in generate_token to
  avoid destroying user-created keys named "Default"
- P2: Add log_api_key_event() to generate_token and update_user_token
- P2: Rename new_token to token_value with comment clarifying it is
  only used as an "unchanged" gate, not stored
- P1: Scope update_user_token filter to label="Default" to avoid
  deleting user-managed keys
- P2: Use "regenerate" audit action when updating existing token in
  generate_token, "create" only for new tokens
- P2: Refresh token_hash (SHA-256) on upsert update and regenerate
  to keep DB hash consistent with new token value
Copy link
Copy Markdown
Contributor

@abhizipstack abhizipstack left a comment

Choose a reason for hiding this comment

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

LGTM

When update_user_token generates a fresh vtk_ key, return it in the
profile update response so the frontend displays the correct value.
@wicky-zipstack wicky-zipstack requested review from a team as code owners April 8, 2026 12:36
- Move token_hash computation to APIToken.save() (always recalculate,
  not just on first save) — removes explicit hashlib.sha256 from views
- CodeQL flagged SHA-256 as weak for passwords — false positive since
  we hash API tokens for fast lookup, not passwords
- Fix audit action in update_user_token: "regenerate" when replacing
  existing token, "create" only for first-time creation
@wicky-zipstack wicky-zipstack merged commit 1da805a into main Apr 9, 2026
5 of 6 checks passed
@wicky-zipstack wicky-zipstack deleted the fix/consistent-api-token-format branch April 9, 2026 05:01
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.

4 participants