rails-token-auth

Build secure DB-backed token session auth in Rails — one mechanism for web, Action Cable, and Hotwire Native, with Current attributes and secure-by-default controller concerns. Use when adding login/s

SKILL.md

GitHub

Rails token-backed session auth

A revocable, DB-backed session model (AppSession) that authenticates web and native clients with one mechanism — plus Current attributes and secure-by-default controller concerns. Built without a gem (plain has_secure_password + authenticate_by). Full design + rationale: references/token-auth-guide.md.

When to use

  • Adding login/logout to a Rails app and you want it done securely from the start.
  • Web + Hotwire Native (and Action Cable) need to share one auth.
  • You need revocable sessions (real logout, kick a device) — not stateless JWTs.
  • Auditing existing auth for the expensive mistakes.

The shape

File Role
AppSession (app/models/app_session.rb) one row per login; token stored as a digest (has_secure_password :token)
User::Authentication (app/models/user/authentication.rb) has_secure_password; authenticate_by login; mint/verify sessions
Current (app/models/current.rb) request-scoped user / app_session / organization
Authenticate (app/controllers/concerns/authenticate.rb) secure-by-default before_action; skip_authentication / allow_unauthenticated; log_in/log_out
SessionsController create_app_session → log_in; destroy → log_out

Copy the templates from templates/ (they're generic, ready to adapt). Add the migration (app_sessions: user ref + token_digest), the routes (login/logout), and include User::Authentication + include Authenticate in ApplicationController.

The non-negotiables (why this design)

  • authenticate_by, not find_by(email:)&.authenticate — timing-safe, no account enumeration.
  • Token hashed at rest (has_secure_password :token) — plaintext token lives only in the encrypted cookie. A DB leak yields no usable sessions.
  • cookies.encrypted for the session payload — confidential + tamper-proof.
  • Secure-by-default: global before_action :authenticate; public actions opt out explicitly. A forgotten controller stays locked.
  • Real logout destroys the AppSession row — server-side revocation.

Audit an app

scripts/audit_token_auth.sh path/to/rails-app

Checks all six properties above and flags: find_by(email:)+authenticate (enumeration), plaintext token storage, token/session in a plain cookie, opt-in auth (no global before_action), cookie-only logout, missing has_secure_password / password length. Heuristic grep scan — it names its ceiling; a clean run is not a formal proof, so still review custom logic. Verified: passes the secure Piazza app, flags a synthetic insecure one on every check.

Testing the concerns

The Authenticate concern is tested in isolation with a TestController + draw_test_routes harness. That harness has a Rails 8 LazyRouteSet flake — use the fixed helper and the detector from the rails-8-upgrade skill.