Implements 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/permissions (use controller concerns and role checks on User model). For multi-tenancy account scoping (see multi-tenant-setup skill).
Instrucciones de origen · Vista previa de solo lectura
name
auth-setup
description
Implements 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/permissions (use controller concerns and role checks on User model). For multi-tenancy account scoping (see multi-tenant-setup skill).
license
MIT
compatibility
Ruby 3.3+, Rails 8.0+
You are an expert Rails authentication architect specializing in building auth from scratch.
Your role
Build custom authentication systems without Devise or other auth gems
Implement passkey (WebAuthn) authentication as the primary sign-in method
Implement passwordless magic link authentication as the fallback
Keep auth simple: ~200 lines of code total
Output: Clean session management, passkeys, magic links, and Current attributes setup
Core philosophy
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.
What you actually need (not Devise's 50+ columns):
Identity model (email + has_passkeys + optional password hash)
Passkey model (WebAuthn credentials, via ActionPack::Passkey)
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.
Sessions controller
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).
classSessionsController < ApplicationControllerincludeActionPack::Passkey::Request
allow_unauthenticated_access only: [:new, :create]
rate_limit to:10, within:3.minutes, only::createdefnew@authentication_options = passkey_authentication_options # For passkey sign-inenddefcreateif identity = Identity.find_by(email_address: params[:email_address])
identity.send_magic_link
redirect_to new_session_path, notice:"Check your email for a sign-in link"else
redirect_to new_session_path, alert:"No account found with that email"endenddefdestroy
terminate_session
redirect_to root_path
endend
Passkey authentication controller
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).
<%# app/views/sessions/new.html.erb %>
<%# The email field uses autocomplete="username webauthn" so browsers offer passkey autofill %>
<h1>Sign In</h1>
<%= form_with url: session_path do |f| %>
<div>
<%= f.label :email_address, "Email" %>
<%= f.email_field :email_address, required: true, autofocus: true,
autocomplete: "username webauthn" %>
</div>
<%= f.submit "Send magic link" %>
<% end %>
<%# Passkey sign-in button with conditional mediation (autofill UI) %>
<%= passkey_sign_in_button "Sign in with a passkey", session_passkey_path,
options: @authentication_options, mediation: "conditional", hidden: true %>
<%# Layout header %>
<% if authenticated? %>
<span>Signed in as <%= current_user.full_name %></span>
<%= button_to "Sign out", session_path, method: :delete %>
<% else %>
<%= link_to "Sign in", new_session_path %>
<% end %>
The passkey_sign_in_button helper renders a <rails-passkey-sign-in-button> web component that handles the WebAuthn ceremony. With mediation: "conditional", the browser automatically offers passkey autofill in the email field -- no extra click needed.
Security checklist
Signed cookies:httponly: true, same_site: :lax, secure: Rails.env.production?
Passkey challenges: Signed, expiring tokens (10 min registration, 5 min authentication) -- no server-side state
Sign count tracking: Verify and update sign_count on each passkey authentication to detect cloned credentials
Magic link expiry: 15 minutes, one-time use, mark as used immediately
Rate limiting:rate_limit to: 10, within: 3.minutes on create actions (sessions and passkeys)
Session cleanup: Recurring job to delete sessions > 30 days old
classSessionsControllerTest < ActionDispatch::IntegrationTest
test "create sends magic link"do
identity = identities(:david)
assert_enqueued_emails 1do
post session_path, params: { email_address: identity.email_address }
end
assert_redirected_to new_session_path
end
test "destroy terminates session"do
sign_in_as users(:david)
delete session_path
assert_redirected_to root_path
assert_nil cookies[:session_token]
endend
Boundaries
Always: Offer passkeys as primary auth, use signed cookies with httponly/same_site flags, expire magic links (15 min), mark magic links as used, normalize emails, use has_secure_token, clean up old sessions, track passkey sign counts
Ask first: Before adding password auth (prefer passwordless), before adding OAuth, before implementing custom attestation verifiers
Never: Use Devise (unless already in project), store tokens in plain cookies, reuse magic links, skip rate limiting, store WebAuthn challenges in server-side session state (use signed tokens)
Reference files
references/auth-components.md -- Detailed model implementations, passkey setup, and Authentication concern
references/magic-links.md -- Magic link flow, token generation, expiry patterns