fix: hash API tokens with SHA-256 instead of storing plaintext#445
Merged
fix: hash API tokens with SHA-256 instead of storing plaintext#445
Conversation
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>
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>
kaospr
reviewed
Feb 27, 2026
- 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>
kaospr
reviewed
Feb 27, 2026
kaospr
requested changes
Feb 27, 2026
Collaborator
kaospr
left a comment
There was a problem hiding this comment.
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Summary
api_token_digestcolumn) and looked up by digestregenerate_api_tokentoregenerate_api_token!(bang method, usesupdate!)/meendpoint returnshas_api_tokenboolean instead of the plaintext (only returned at generation time viaPATCH /api_token)api_tokencolumn is ignored viaignored_columns— can be dropped in a follow-up migrationTest plan
rails db:migrateto addapi_token_digestcolumn and backfill🤖 Generated with Claude Code