# 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/sessions to a Rails app, when web and native clients need to share authentication, when you need revocable server-side sessions (logout/kick a device) instead of stateless JWTs, or when auditing existing auth for user-enumeration, plaintext tokens, readable cookies, or opt-in-auth mistakes. Provides templates and a security audit.

- Skill: `davidteren/rails-token-auth` (Agent Skill, multi-file: 8 files)
- Install (CLI): `npx skillmds@latest add davidteren/rails-token-auth`
- Raw SKILL.md: https://api.skillmd.com/api/skills/davidteren/rails-token-auth/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Security
- Author: davidteren (https://skillmd.com/u/davidteren)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/davidteren/rails-token-auth

---


# 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`](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

```bash
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.

