Skip to content

feat: add security hardening and fix vetted backlog issues - #56

Merged
nejdetkadir merged 1 commit into
mainfrom
feature/add-refresh-rotation-and-security-hardening
Aug 25, 2026
Merged

nejdetkadir merged 1 commit into
mainfrom
feature/add-refresh-rotation-and-security-hardening

Conversation

@nejdetkadir

@nejdetkadir nejdetkadir commented Aug 25, 2026 •

Copy link
Copy Markdown
Owner

Summary

  • Implements the actionable findings from the vetted security/quality backlog (docs/analysis/): refresh-token rotation with reuse detection (SEC-2), DB-enforced token uniqueness (SEC-4), account-enumeration hardening (SEC-5/SEC-7), and token-secret log filtering (Filtering logging of the access_token #51)
  • Fixes the API-wart bugs: unreachable invalid_refresh_token now used by refresh, new invalid_login error for non-email authentication keys, canonical failed_attempts key emitted alongside the deprecated failed_attemps typo, duplicate refresh-token helper consolidated
  • All new behavior is opt-in via config flags with backward-compatible defaults; suite now at 100% line and branch coverage with zero rubocop disables

Changes

  • lib/devise/api/configuration.rb — new settings: refresh_token.rotation_enabled (default false), paranoid (default false), error_response.verbose_account_state (default true)
  • app/controllers/devise/api/tokens_controller.rb — refresh reuse detection (revokes the whole token family via Token#revoke_family! when a rotated/revoked refresh token is replayed); unknown refresh token now returns invalid_refresh_token (400) instead of invalid_token (401); actions rewritten around shared render_token_response/render_error_response helpers (all rubocop:disable comments removed)
  • app/services/.../tokens_service/refresh.rb — with rotation on, mints the new token and revokes the presented one in a single transaction
  • app/services/.../tokens_service/create.rb — rescues ActiveRecord::RecordNotUnique and retries with fresh tokens (3 attempts)
  • app/services/.../resource_owner_service/authenticate.rb — unknown account returns invalid_email only when :email is an authentication key, invalid_login otherwise, and generic invalid_authentication in paranoid mode
  • lib/devise/api/responses/error_response.rb — failed_attempts + deprecated failed_attemps; lockable/confirmable blocks gated by verbose_account_state/paranoid; status mapping via constants
  • lib/devise/api/token.rb — revoke!, revoke_family!, filter_attributes redaction of token secrets, Time.current
  • lib/devise/api/rails/engine.rb — initializer adds access_token/refresh_token/previous_refresh_token to the host app's filter_parameters
  • lib/devise/api/generators/templates/migration.rb.erb — unique indexes on access_token/refresh_token
  • Docs — README security recommendations section + fixes (KI-11), CHANGELOG backfilled 0.1.0→0.2.0 + Unreleased (KI-9), all contractual docs updated, SEC/KI items moved to resolved, RBS stub removed (KI-6)
  • Specs — new refresh_token_rotation_spec.rb, paranoid_mode_spec.rb, engine_spec.rb; extended token/config/error-response/service/generator/helpers specs

Architecture Impact

  • DB migration: new installs get unique indexes from the generator template; existing installs need the migration documented in the CHANGELOG (remove_index + add_index unique: true on both token columns). Dummy app migration + schema updated accordingly.
  • Breaking-ish: POST /<scope>/tokens/refresh with an unknown refresh token now responds invalid_refresh_token (400) instead of invalid_token (401) — changelogged.
  • No changes to routes, default response shapes (beyond the additive failed_attempts key), or default token flow; rotation/paranoid/verbose flags default to current behavior.

Test Plan

  • Unit tests added/updated (service, model, config, response, generator, helper specs)
  • Integration tests pass (bundle exec rake: 321 examples, 0 failures, 100% line + branch coverage, rubocop clean)
  • Manual testing completed (verified unique indexes applied to the dummy sqlite DB)
  • Migration tested on clean DB (dummy app schema + migration in sync)

Related

🤖 Generated with Claude Code

Resolves SEC-2/4/5/7 and KI-1/2/3/4/6/7/9/10/11 from docs/analysis, plus
GH-51 (token values in logs):

- refresh token rotation + family-revocation reuse detection behind
  refresh_token.rotation_enabled (default off)
- unique DB indexes on token secrets with RecordNotUnique retry
- paranoid mode and error_response.verbose_account_state flags to stop
  account enumeration and lockable-state leakage
- filter token secrets from request logs and Token#inspect
- refresh with unknown token now returns invalid_refresh_token (400);
  new invalid_login error for non-email authentication keys
- emit failed_attempts alongside the deprecated failed_attemps typo
- consolidate memoized current_devise_api_refresh_token helper
- drop rubocop-disable scar tissue, RBS stub; standardize Time.current
- backfill CHANGELOG, update contractual docs, 100% line+branch coverage

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@nejdetkadir nejdetkadir self-assigned this Aug 25, 2026
@nejdetkadir
nejdetkadir merged commit 3a86049 into main Aug 25, 2026
8 checks passed
@nejdetkadir
nejdetkadir deleted the feature/add-refresh-rotation-and-security-hardening branch August 25, 2026 16:15
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.

Filtering logging of the access_token

1 participant