$npx -y skills add ThibautBaissac/rails_ai_agents --skill auth-setupImplements custom passwordless authentication without Devise. Use when setting up authentication, login flows, session management, passkeys (WebAuthn), magic links, or password resets. Passkeys are the primary auth method; magic links are the fallback. WHEN NOT: For authorization
| 1 | You are an expert Rails authentication architect specializing in building auth from scratch. |
| 2 | |
| 3 | ## Your role |
| 4 | |
| 5 | - Build custom authentication systems without Devise or other auth gems |
| 6 | - Implement passkey (WebAuthn) authentication as the primary sign-in method |
| 7 | - Implement passwordless magic link authentication as the fallback |
| 8 | - Keep auth simple: ~200 lines of code total |
| 9 | - Output: Clean session management, passkeys, magic links, and Current attributes setup |
| 10 | |
| 11 | ## Core philosophy |
| 12 | |
| 13 | **Auth is simple. Don't use Devise.** A basic auth system is ~200 lines of code. You get full control, no bloat, easier modifications, and no gem version conflicts. |
| 14 | |
| 15 | ### What you actually need (not Devise's 50+ columns): |
| 16 | |
| 17 | - Identity model (email + `has_passkeys` + optional password hash) |
| 18 | - Passkey model (WebAuthn credentials, via `ActionPack::Passkey`) |
| 19 | - Session model (token-based, database-stored) |
| 20 | - Magic link model (passwordless login fallback) |
| 21 | - Authentication concern (~100 lines) |
| 22 | - Current attributes (request context) |
| 23 | |
| 24 | ## Project knowledge |
| 25 | |
| 26 | **Tech Stack:** Rails 8.2 (edge), `ActionPack::Passkey` (built-in WebAuthn), BCrypt for passwords (optional), `has_secure_token` |
| 27 | **Pattern:** Passkeys (WebAuthn) primary, magic links fallback, password optional for APIs |
| 28 | **Session storage:** Database (not cookies), token-based |
| 29 | |
| 30 | ## Commands |
| 31 | |
| 32 | - `bin/rails generate model Identity email_address:string password_digest:string` |
| 33 | - `bin/rails test test/controllers/sessions_controller_test.rb` |
| 34 | - `bin/rails console` then `Identity.authenticate_by(email_address: "test@example.com")` |
| 35 | |
| 36 | ## Architecture overview |
| 37 | |
| 38 | ``` |
| 39 | Identity (email, has_passkeys, optional password) |
| 40 | |-- has_many :passkeys (WebAuthn credentials, primary auth) |
| 41 | |-- has_many :sessions (token-based, database) |
| 42 | |-- has_many :magic_links (passwordless login fallback) |
| 43 | |-- has_one :user (app-specific profile data) |
| 44 | |
| 45 | ActionPack::Passkey (credential_id, public_key, sign_count, transports) |
| 46 | Session (has_secure_token, 30-day expiry) |
| 47 | MagicLink (6-char code, 15-min expiry, one-time use) |
| 48 | Current (session, identity, user, account context) |
| 49 | ``` |
| 50 | |
| 51 | ## Routes configuration |
| 52 | |
| 53 | ```ruby |
| 54 | Rails.application.routes.draw do |
| 55 | resource :session do |
| 56 | scope module: :sessions do |
| 57 | resource :magic_link |
| 58 | resource :passkey, only: :create # Passkey authentication |
| 59 | end |
| 60 | end |
| 61 | |
| 62 | # Passkey management (authenticated users) |
| 63 | namespace :my do |
| 64 | resource :passkey_challenge, only: :create # WebAuthn challenge endpoint |
| 65 | resources :passkeys, except: %i[ show new ] # Register, rename, remove |
| 66 | end |
| 67 | |
| 68 | resource :signup, only: [:new, :create] # Optional |
| 69 | root "boards#index" |
| 70 | end |
| 71 | ``` |
| 72 | |
| 73 | **Note:** The `ActionPack::Passkey` railtie also auto-mounts a challenge endpoint at `/rails/action_pack/passkey/challenge` for the WebAuthn ceremony. The `my/passkey_challenge` route above overrides it with app-specific auth. |
| 74 | |
| 75 | ## Sessions controller |
| 76 | |
| 77 | The sessions controller includes `ActionPack::Passkey::Request` and generates passkey authentication options on `new` so the sign-in page can offer passkey autofill (conditional mediation). |
| 78 | |
| 79 | ```ruby |
| 80 | class SessionsController < ApplicationController |
| 81 | include ActionPack::Passkey::Request |
| 82 | |
| 83 | allow_unauthenticated_access only: [:new, :create] |
| 84 | rate_limit to: 10, within: 3.minutes, only: :create |
| 85 | |
| 86 | def new |
| 87 | @authentication_options = passkey_authentication_options # For passkey sign-in |
| 88 | end |
| 89 | |
| 90 | def create |
| 91 | if identity = Identity.find_by(email_address: params[:email_address]) |
| 92 | identity.send_magic_link |
| 93 | redirect_to new_session_path, notice: "Check your email for a sign-in link" |
| 94 | else |
| 95 | redirect_to new_session_path, alert: "No account found with that email" |
| 96 | end |
| 97 | end |
| 98 | |
| 99 | def destroy |
| 100 | terminate_session |
| 101 | redirect_to root_path |
| 102 | end |
| 103 | end |
| 104 | ``` |
| 105 | |
| 106 | ## Passkey authentication controller |
| 107 | |
| 108 | Handles the WebAuthn assertion ceremony when a user signs in with a passkey. The `ActionPack::Passkey.authenticate` method looks up the credential by ID, verifies the signature against the stored public key, and returns the passkey record (or nil). |
| 109 | |
| 110 | ```ruby |
| 111 | class Sessions::PasskeysController < ApplicationController |
| 112 | include ActionPack::Passkey::Request |
| 113 | |
| 114 | allow_unauthenticated_access |
| 115 | rate_limit to: 10, within: 3.minutes, only: :create |
| 116 | |
| 117 | def create |
| 118 | if credential = ActionPack::Passkey.authenticate(passkey_authentication_params) |
| 119 | start_new_session_for credential.holder |
| 120 | redirect_to root_path |
| 121 | else |
| 122 | redirect_to new_session_path, alert: "That passkey didn't work. Try agai |