Passwordless
🗝 Authentication for your Rails app without the icky-ness of passwords
Install / Use
/learn @mikker/PasswordlessREADME
Add authentication to your Rails app without all the icky-ness of passwords. Magic link authentication, if you will. We call it passwordless.
Installation
Add to your bundle and copy over the migrations:
$ bundle add passwordless
$ bin/rails passwordless_engine:install:migrations
Upgrading
See Upgrading to Passwordless 1.0 for more details.
Usage
Passwordless creates a single model called Passwordless::Session, so it doesn't come with its own user model. Instead, it expects you to provide one, with an email field in place. If you don't yet have a user model, check out the wiki on creating the user model.
Enable Passwordless on your user model by pointing it to the email field:
class User < ApplicationRecord
# your other code..
passwordless_with :email # <-- here! this needs to be a column in `users` table
# more of your code..
end
Then mount the engine in your routes:
Rails.application.routes.draw do
passwordless_for :users
# other routes
end
Getting the current user, restricting access, the usual
Passwordless doesn't give you current_user automatically. Here's how you could add it:
class ApplicationController < ActionController::Base
include Passwordless::ControllerHelpers # <-- This!
# ...
helper_method :current_user
private
def current_user
@current_user ||= authenticate_by_session(User)
end
def require_user!
return if current_user
save_passwordless_redirect_location!(User) # <-- optional, see below
redirect_to root_path, alert: "You are not worthy!"
end
end
Et voilà:
class VerySecretThingsController < ApplicationController
before_action :require_user!
def index
@things = current_user.very_secret_things
end
end
Providing your own templates
To make Passwordless look like your app, override the bundled views by adding your own. You can manually copy the specific views that you need or copy them to your application with rails generate passwordless:views.
Passwordless has 2 action views and 1 mailer view:
# the form where the user inputs their email address
app/views/passwordless/sessions/new.html.erb
# the form where the user inputs their just received token
app/views/passwordless/sessions/show.html.erb
# the email with the token and magic link
app/views/passwordless/mailer/sign_in.text.erb
See the bundled views.
Registering new users
Because your User record is like any other record, you create one like you normally would. Passwordless provides a helper method to sign in the created user after it is saved – like so:
class UsersController < ApplicationController
include Passwordless::ControllerHelpers # <-- This!
# (unless you already have it in your ApplicationController)
def create
@user = User.new(user_params)
if @user.save
sign_in(create_passwordless_session(@user)) # <-- This!
redirect_to(@user, flash: { notice: 'Welcome!' })
else
render(:new)
end
end
# ...
end
URLs and links
By default, Passwordless uses the resource name given to passwordless_for to generate its routes and helpers.
passwordless_for :users
# <%= users_sign_in_path %> # => /users/sign_in
passwordless_for :users, at: '/', as: :auth
# <%= auth_sign_in_path %> # => /sign_in
Also be sure to
specify ActionMailer's default_url_options.host and tell the routes as well:
# config/application.rb for example:
config.action_mailer.default_url_options = {host: "www.example.com"}
routes.default_url_options[:host] ||= "www.example.com"
Note as well that passwordless_for accepts a custom controller. One possible application of this
is to add a before_action that redirects authenticated users from the sign-in routes, as in this example:
# config/routes.rb
passwordless_for :users, controller: "sessions"
# app/controllers/sessions_controller.rb
class SessionsController < Passwordless::SessionsController
before_action :require_unauth!, only: %i[new show]
private
def require_unauth!
return unless current_user
redirect_to("/", notice: "You are already signed in.")
end
end
Route constraints
With constraints you can restrict access to certain routes.
Passwordless provides Passwordless::Constraint and it's negative counterpart Passwordless::ConstraintNot for this purpose.
To limit a route to only authenticated Users:
constraints Passwordless::Constraint.new(User) do
# ...
end
The constraint takes a second if: argument, that expects a block and is passed the authenticatable record, (ie. User):
constraints Passwordless::Constraint.new(User, if: -> (user) { user.email.include?("john") }) do
# ...
end
The negated version has the same API but with the opposite result, ie. ensuring authenticated user don't have access:
constraints Passwordless::ConstraintNot.new(User) do
get("/no-users-allowed", to: "secrets#index")
end
Configuration
To customize Passwordless, create a file config/initializers/passwordless.rb.
The default values are shown below. It's recommended to only include the ones that you specifically want to modify.
Passwordless.configure do |config|
config.default_from_address = "CHANGE_ME@example.com"
config.parent_controller = "ApplicationController"
config.parent_mailer = "ActionMailer::Base"
config.restrict_token_reuse = true # Can a token/link be used multiple times?
config.token_generator = Passwordless::ShortTokenGenerator.new # Used to generate magic link tokens.
config.expires_at = lambda { 1.year.from_now } # How long until a signed in session expires.
config.timeout_at = lambda { 10.minutes.from_now } # How long until a token/magic link times out.
config.redirect_back_after_sign_in = true # When enabled the user will be redirected to their previous page, or a page specified by the `destination_path` query parameter, if available.
config.redirect_to_response_options = {} # Additional options for redirects.
config.success_redirect_path = '/' # After a user successfully signs in
config.failure_redirect_path = '/' # After a sign in fails
config.sign_out_redirect_path = '/' # After a user signs out
config.paranoid = false # Display email sent notice even when the resource is not found.
config.after_session_confirm = ->(session, request) {} # Called after a session is confirmed.
end
Delivery method
By default, Passwordless sends emails. See Providing your own templates. If you need to customize this further, you can do so in the after_session_save callback.
In config/initializers/passwordless.rb:
Passwordless.configure do |config|
config.after_session_save = lambda do |session, request|
# Default behavior is
# Passwordless::Mailer.sign_in(session, session.token).deliver_now
# You can change behavior to do something with session model. For example,
# SmsApi.send_sms(session.authenticatable.phone_number, session.token)
end
end
After Session Confirm Hook
An after_session_confirm hook is called after a successful session confirmation – in other words: after a user signs in successfully.
Passwordless.configure do |config|
config.after_session_confirm = ->(session, request) {
user = session.authenticatable
user.update!(
email_verified: true,
last_login_ip: request.remote_ip
)
}
end
Token generation
By default Passwordless generates short, 6-digit, alpha numeric tokens. You can change the generator using Passwordless.config.token_generator to something else that responds to call(session) eg.:
Passwordless.configure do |config|
config.token_generator = lambda do |session|
"probably-stupid-token-#{session.user_agent}-#{Time.current}"
end
end
Passwordless will keep generating tokens until it finds one that hasn't been used yet. So be sure to use some kind of method where matches are unlikely.
Timeout and Expiry
The timeout is the time by which the generated token and magic link is invalidated. After this the token cannot be used to sign in to your app and the user will need to request a new token.
The expiry is
