.offrails/generators/authentication.md
Accounts (rails generate authentication / phx.gen.auth): the auth parts
TL;DR
- Accounts are parts, not a recipe to type:
cargo offrails add auth-core auth-password(email + password),auth-core auth-magic-link(passwordless), both, andauth-totpfor two-factor. They bring users, database sessions, sign-in, the account security page, security emails, the audit trail, throttling and their own tests (the parts, samples).- Before: write
product/features/accounts.md, and pick the variant from the interview (ACCOUNTS: password | magic-link | password+magic-link, plusinvitewhen sign-up is closed). After: the header (ACCOUNTS,TENANCY),APP_SECRET_KEY, where people land after sign-in, your own handlers scoped to the user, the tenancy suites (the first scopedg resourcewrites them), and the browser smoke.- What's left is product, not plumbing: invite-only sign-up, roles, teams, the spec's own acceptance tests. Each is listed below with where it goes.
Read it all when adding accounts to an app.
Design: guides/auth.md (the why, the flows, the threat model) and guides/security.md. The parts implement it, tested; this page is the order of work and what the parts leave to you. Before 0.7.0 this page was a 14-step checklist for writing it all by hand; don't.
0. Spec and variant
Write product/features/accounts.md from blueprints/feature-spec.md: one AC-accounts-<n> per behavior the product promises (who may sign up, how people sign in, where they land, what an admin can do), and the STRIDE ## Security table. The parts' suites already prove the generic contract (cookie flags, rotation, throttling, enumeration safety, single-use links); your criteria cite their tests where they cover one, and get a test of their own where they don't.
ACCOUNTS (interview) | Parts | Then |
|---|---|---|
password | auth-core auth-password | done: sign-up, verification, reset, sign-in |
magic-link | auth-core auth-magic-link | done: sign-up on first link |
password+magic-link | auth-core auth-password auth-magic-link | done: either method, one account |
… invite (password invite, magic-link invite) | the same | one switch: DEFAULT_SIGNUP = Signup::Invite (below) |
| any, plus two-factor | + auth-totp | optional per user; required for admins is yours (below) |
auth-core needs mailer and dev-mailbox; add installs them first.
1. Add the parts
cargo offrails add auth-core auth-password --dry-run # what it will do
cargo offrails add auth-core auth-password
cargo run -q -- db migrate # then prove it: db rollback --steps <n> && db migrateadd updates the ACCOUNTS header line; set TENANCY yourself (per-user for records people own, teams with teams). Read each part's README, "After installing" and "Adapting it", and its CHECKS.md.
2. What the parts leave to you
- Configuration.
APP_SECRET_KEY(≥ 32 characters,openssl rand -base64 48) in the deploy config: production refuses to boot without it.SMTP_URLfor real email (verification, resets and links are emails). Scheduleapp task auth_cleanupdaily (the jobs part'srecurring.rs, or cron). - Where people land.
paths::after_sign_in()(// APP:insrc/routes.rs): the app's home for signed-in people. - Your handlers.
RequireUser(user)on every private page,RequireRecentAuthon anything that changes credentials or money, and an owner-scoped query for every record (<model>::find_for(db, &user, id)). With accounts installed,cargo offrails g resource …writes exactly that, plustests/app/guard_test.rsandtests/app/tenancy_test.rson the first scoped resource (scaffold); hand-written handlers follow the same shape (models). - The nav. The parts add "Sign in" and the account link at the
samples: nav linksanchor; the rest of the signed-in nav is yours. - The browser smoke.
tests/e2e/smoke.scenario.mjswithlogin(page, base)(the real form; magic links: read the link from/dev/mailbox/latest.json) andloggedIn(page); thenSCENARIO=tests/e2e/smoke.scenario.mjs BASE=<the URL bin/dev printed> bash .offrails/tools/browser-smoke/run.sh loginwith no certificate overrides, and… run.sh a11yin light and dark (test-standards §9).verify --fullsigns in asdemo@example.comfromcargo run -- task auth_demo, or asSMOKE_EMAIL/SMOKE_PASSWORD. - Seeds.
cargo run -- task auth_demomakes the development demo account; your own seed accounts go insrc/tasks/seed.rs, development only (refuse in production).
Invite-only
ACCOUNTS: password invite (or magic-link invite): no public sign-up, and it is a switch, not code. cargo offrails new … --parts auth-password --invite sets it; in an existing app, set pub const DEFAULT_SIGNUP: Signup = Signup::Invite; in src/config.rs (auth-core's // APP: line; APP_AUTH_SIGNUP=open|invite overrides it per deploy, but keep the default closed so a forgotten variable can't open sign-up). Then the sign-up pages answer 404 and their links are hidden (ctx.signup_open), magic links go only to existing accounts and never create one, and people get in with cargo run -- task auth_invite <email> (production: app task auth_invite <email>; with passwords the invitation links to "Choose your password", which also verifies the address), or crate::tasks::auth_invite::invite from your admin screen. Keep the parts' routes and tests: the sign-up tests ask for Signup::Open themselves. Add the AC-accounts-<n> that a stranger can't create an account and cite invite_only_has_no_public_sign_up (passwords) or invite_only_magic_links_never_create_accounts (links).
Roles and teams
Not in the parts (they're product decisions):
- Roles: a
rolecolumn onusers(a migration),RequireAdminnext toRequireUserinsrc/controllers/auth.rs, andAdminAccess(the kernel's admin pages:/admin/jobs, the dashboard) switched to it (auth, security). Two-factor for admins: refuse admin pages untiltotp_credential::enabled_foris true. - Teams:
accountsandmembershipstables,CurrentAccount, every query scoped to the account instead of the user, and the tenancy suite'sTenantholding the account (test-standards §4).
3. Done
Log the variant and any deviation in docs/decisions.md (add already logged the parts). Run the security checklist, then the Definition of Done on SQLite and Postgres, and smoke the flows in a real browser: sign up (or request a link), sign out, sign in, a reset opened from /dev/mailbox.