Skip to content

fix: hash API tokens with SHA-256 instead of storing plaintext#445

Merged
kaospr merged 4 commits intomainfrom
fix/hash-api-tokens
Feb 27, 2026
Merged

fix: hash API tokens with SHA-256 instead of storing plaintext#445
kaospr merged 4 commits intomainfrom
fix/hash-api-tokens

Conversation

@mmichelli
Copy link
Copy Markdown
Contributor

@mmichelli mmichelli commented Feb 27, 2026

Summary

  • API tokens were stored as plaintext — a DB leak would compromise every user's API access
  • Now tokens are hashed with SHA-256 before storage (api_token_digest column) and looked up by digest
  • Backfill migration hashes all existing plaintext tokens using pgcrypto so existing tokens keep working
  • Renamed regenerate_api_token to regenerate_api_token! (bang method, uses update!)
  • /me endpoint returns has_api_token boolean instead of the plaintext (only returned at generation time via PATCH /api_token)
  • Old api_token column is ignored via ignored_columns — can be dropped in a follow-up migration

Test plan

  • All 38 API tests pass (97 assertions, 0 failures)
  • Run rails db:migrate to add api_token_digest column and backfill
  • Verify existing users can authenticate with their current tokens
  • Verify token regeneration returns new plaintext and old token stops working

🤖 Generated with Claude Code

API tokens were stored as plaintext in the database, meaning a DB leak
would directly compromise every user's API access. Now tokens are
hashed with SHA-256 before storage and looked up by digest.

- Add `api_token_digest` column alongside existing `api_token`
- Backfill migration hashes existing plaintext tokens using pgcrypto
- Add `User.find_by_api_token` class method for digest-based lookup
- Rename `regenerate_api_token` to `regenerate_api_token!` (uses update!)
- Return plaintext token only at generation time (from controller)
- Replace `api_token` in `/me` response with `has_api_token` boolean
- Ignore old `api_token` column (can be dropped in a follow-up migration)

All 38 API tests pass (97 assertions).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@mmichelli mmichelli requested a review from kaospr February 27, 2026 13:04
Scoped endpoints (clients, tasks, etc.) crashed with a 500 when no
X-Organization-Id header was set because current_organization was nil.

Now resolve_organization falls back to the user's active (or first)
org when the header is omitted, and returns a 422 JSON error if the
user has no org at all.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Comment thread app/controllers/api/v1/base_controller.rb Outdated
- Add API endpoint table, auth instructions, and token security note
  to README.md
- Add API token generation and testing instructions to SELF_HOSTING.md

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Comment thread app/controllers/api/v1/base_controller.rb Outdated
Copy link
Copy Markdown
Collaborator

@kaospr kaospr left a comment

Choose a reason for hiding this comment

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

Over all looks good! See minor changes 😎

- Remove redundant `if token.present?` guard since
  `User.find_by_api_token` already handles blank tokens
- Use `User#access_info(organization)` for org resolution instead of
  manual access_infos queries, keeping 404 for non-member orgs

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@mmichelli mmichelli requested a review from kaospr February 27, 2026 14:34
Copy link
Copy Markdown
Collaborator

@kaospr kaospr left a comment

Choose a reason for hiding this comment

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

ship_it_squrrel

@kaospr kaospr merged commit a97f36a into main Feb 27, 2026
4 of 5 checks passed
@kaospr kaospr deleted the fix/hash-api-tokens branch February 27, 2026 15:00
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.

2 participants