PasskeyAuth

A Rails engine that adds passwordless authentication methods to your Rails app using WebAuthn passkeys and magic links with email verification codes. Works seamlessly alongside Rails' built-in authentication system.

Features

  • Complements Rails Authentication: Works with rails generate authentication - doesn't replace it
  • Passwordless Options: Give users alternatives to password authentication
  • WebAuthn/Passkey Support: Modern biometric and hardware-key authentication
  • Magic Links: Email-based authentication with short verification codes
  • Rate Limiting: Built-in protection against abuse
  • Customizable Hooks: Add your own behavior to authentication events
  • I18n Support: Fully internationalized with English translations included
  • Rails 8+ Compatible: Designed for modern Rails applications

Installation

Add this line to your application's Gemfile:

gem 'passkey_auth'

And then execute:

bundle install

Or install it yourself as:

gem install passkey_auth

Setup

1. Install Rails Authentication (if not already done)

PasskeyAuth requires Rails' authentication system. If you haven't already:

rails generate authentication

This creates the base authentication system with sessions, User model, and the Authentication concern.

Important: Ensure your Rails app has a root route defined in config/routes.rb. PasskeyAuth redirects users to the root URL after authentication.

2. Install PasskeyAuth

rails generate passkey_auth:install

This will:

  • Create migrations for magic links and WebAuthn credentials
  • Add passwordless fields to your users table
  • Add an initializer at config/initializers/passkey_auth.rb
  • Mount the engine routes
  • Copy JavaScript controllers

3. Add to your User model

class User < ApplicationRecord
  has_secure_password  # From Rails authentication
  include PasskeyAuth::Concerns::Passwordless  # Add passwordless methods

  # Optional: If you prefer using :email instead of :email_address
  alias_attribute :email, :email_address
end

Note: Rails 8's authentication uses email_address as the attribute name. PasskeyAuth defaults to this, but you can configure it in the initializer if your app uses a different attribute name.

4. Register Stimulus Controllers

Add the PasskeyAuth Stimulus controllers to your app/javascript/controllers/application.js:

import { application } from "./application"
import { PasskeyAuthenticationController, WebauthnRegisterController, WebauthnAuthController }
  from "passkey_auth/controllers"

// Register PasskeyAuth controllers
application.register("passkey-authentication", PasskeyAuthenticationController)
application.register("webauthn-register", WebauthnRegisterController)
application.register("webauthn-auth", WebauthnAuthController)

Note: The installer automatically adds the required JavaScript dependencies to your importmap. If you're using a different bundler (esbuild, webpack, etc.), install the WebAuthn dependency:

npm install @github/webauthn-json

5. Run migrations

rails db:migrate

Usage

How It Works

PasskeyAuth provides alternative authentication methods that work alongside password-based auth:

  • Password auth (Rails): SessionsController handles traditional email/password login
  • Magic links (PasskeyAuth): Users can request email-based login codes
  • Passkeys (PasskeyAuth): Users can register biometric/hardware keys for login

All three methods use the same session management from Rails' Authentication concern. You simply give users more options for how they authenticate.

Session Management

PasskeyAuth uses Rails' session management methods from the Authentication concern:

  • current_user - Access the authenticated user
  • start_new_session_for(user) - Log a user in
  • terminate_session - Log the user out
  • allow_unauthenticated_access - Allow public access to actions

These are provided by Rails' Authentication concern in your ApplicationController.

Offer magic link authentication as an alternative to passwords:

<!-- In your login page -->
<%= link_to "Sign in with email code", passkey_auth.new_magic_link_path %>

Users can then:

  1. Enter their email address
  2. Receive an email with a magic link and short code
  3. Click the link or enter the code to authenticate

The magic link flow:

  • GET /auth/magic_links/new - Request form
  • POST /auth/magic_links - Send magic link email
  • GET /auth/magic_links/verify_code - Code entry form
  • POST /auth/magic_links/verify_with_code - Verify code
  • GET /auth/magic_links/:token - Verify token from email link

Passkeys (WebAuthn)

Configure the WebAuthn origin in the initializer:

PasskeyAuth.setup do |config|
  config.webauthn_origin = ENV.fetch("WEBAUTHN_ORIGIN", "http://localhost:3000")
  config.webauthn_rp_name = "Your App Name"
end

Adding Passkey Sign-In Button

Add the passkey authentication UI to your sign-in page:

<div data-controller="passkey-authentication"
     data-passkey-authentication-challenge-path-value="<%= passkey_auth.webauthn_credentials_challenge_path(format: :json) %>"
     data-passkey-authentication-authentication-path-value="<%= passkey_auth.webauthn_authentications_path(format: :json) %>">

  <button type="button"
          data-passkey-authentication-target="signinButton"
          data-action="click->passkey-authentication#signIn">
    Sign in with Passkey
  </button>
</div>

Registering a New Passkey

Allow users to register new passkeys in their profile:

<%= form_with url: passkey_auth.webauthn_credentials_path,
              data: {
                controller: "webauthn-register",
                webauthn_register_callback_url_value: passkey_auth.webauthn_credentials_path,
                action: "submit->webauthn-register#submit"
              } do |form| %>
  <%= form.text_field :nickname, placeholder: "Name this passkey", data: { webauthn_register_target: "nickname" } %>
  <%= form.submit "Add Passkey" %>
<% end %>

Configuration

Edit config/initializers/passkey_auth.rb:

PasskeyAuth.setup do |config|
  # User model (default: "User")
  config.user_model_name = "User"

  # Email attribute on User model (default: :email_address)
  # Rails 8 uses :email_address, but you can change this if your app uses :email
  # Or use alias_attribute in your User model instead
  config.user_email_attribute = :email_address

  # Magic link expiration (default: 10.minutes)
  config.magic_link_expiration = 10.minutes

  # Rate limit for magic links (default: 1.minute)
  config.magic_link_rate_limit = 1.minute

  # WebAuthn settings
  config.webauthn_origin = ENV.fetch("WEBAUTHN_ORIGIN")
  config.webauthn_rp_name = "Your App"
end

Email Attribute Configuration

Rails 8's authentication uses email_address as the default attribute name. PasskeyAuth respects this default, but you have two options if you prefer using email:

Option 1: Use alias_attribute in your User model (recommended)

class User < ApplicationRecord
  has_secure_password
  include PasskeyAuth::Concerns::Passwordless

  alias_attribute :email, :email_address
end

Option 2: Configure PasskeyAuth to use :email

PasskeyAuth.setup do |config|
  config.user_email_attribute = :email
end

Hooks

Add custom behavior to authentication events:

PasskeyAuth.setup do |config|
  config.on_magic_link_requested = ->(user, magic_link) {
    # Track magic link requests
    Analytics.track(user_id: user.id, event: 'magic_link_requested')
  }

  config.on_passkey_created = ->(user, credential) {
    # Track passkey creation
    Analytics.track(user_id: user.id, event: 'passkey_created')
  }
end

Customization

Views

Override any view by creating a file at the corresponding path in your app:

app/views/passkey_auth/magic_links/new.html.erb
app/views/passkey_auth/magic_links/verify_code.html.erb
app/views/passkey_auth/webauthn/credentials/index.html.erb

Mailers

Override the magic link email template:

app/views/passkey_auth/magic_link_mailer/.html.erb

Routes

The engine is mounted at /auth by default. Change this in config/routes.rb:

mount PasskeyAuth::Engine => "/authentication"

Security Considerations

  • Magic links expire after 10 minutes by default
  • Magic links can only be used once
  • Rate limiting prevents abuse (1 request per minute by default)
  • Sessions are managed by Rails' Authentication concern
  • WebAuthn credentials use public key cryptography
  • All sensitive operations use database transactions

Architecture

PasskeyAuth is designed to complement, not replace, Rails authentication:

What Rails Provides:

  • Authentication concern for session management
  • SessionsController for password-based login
  • current_user, start_new_session_for, etc.
  • User model with has_secure_password

What PasskeyAuth Adds:

  • Passwordless concern with magic link and passkey methods
  • Controllers for magic link and WebAuthn flows
  • Models for storing magic links and WebAuthn credentials
  • JavaScript for browser WebAuthn integration

Users can authenticate with any of these methods, and all use the same session system.

Requirements

  • Ruby >= 3.2
  • Rails >= 8.0 (with rails generate authentication installed)
  • PostgreSQL, MySQL, or SQLite

Development

After checking out the repo, run bin/setup to install dependencies. Then, run rake test to run the tests. You can also run bin/console for an interactive prompt that will allow you to experiment.

To install this gem onto your local machine, run bundle exec rake install. To release a new version, update the version number in version.rb, and then run bundle exec rake release, which will create a git tag for the version, push git commits and the created tag, and push the .gem file to rubygems.org.

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/passkey_auth. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the code of conduct.

License

The gem is available as open source under the terms of the MIT License.

Code of Conduct

Everyone interacting in the PasskeyAuth project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the code of conduct.