I think you know the situation pretty good. You want to try some app but then comes the registration. Big form, password requirements, email confirmation… Just why?!
For both users and developers, passwords, registrations, sing-ins and everything related means just trouble.
What i hate as a user:
What i hate as a developer:
I always wished there was some simpler way. And finally i found it! Do you know Asciinema.org? No? Go check it out and try it’s login. This is it. You just type your email, get your sing-in link and that’s all! No forms, no requirements, no confirmations, no reset, no nothing!
The workflow is incredibly simple:
First of all some scaffolds:
rails new passwordless_authentication rails g controller users index edit update rails g model user email:string name:string login_token:string login_token_valid_until:datetime rails db:migrate rails g controller logins create rails g controller sessions create destroy rails g mailer login login_link
And code with comments:
Rails.application.routes.draw do post 'logins/create' get 'sessions/create' delete 'sessions/destroy' resources :users root 'users/index' end # Here is just some basic example for authenticated/non-authenticated user restrictions class UsersController < ApplicationController before_action :authenticate_user!, only: [:edit, :update] def index @users = User.all end def edit @user = User.find(params[:id]) end def update User.find(params[:id]).update!(name: params[:user][:name]) redirect_to users_path end private def authenticate_user! if current_user.anonymous? redirect_to root_path, alert: 'Not authenticated' end end end class ApplicationController < ActionController::Base protect_from_forgery with: :exception helper_method :current_user def current_user=(user) session[:user_id] = user.id end # If i don't find a user from session i return null object def current_user User.find_by(id: session[:user_id]) || NullUser.new end end class LoginsController < ApplicationController # This is the action triggered by login form # if we don't find user by given email we create new one def create user = User.find_or_create_by!(email: params[:email]) do |user| user.name = 'Edit me!' end # Here we set unique login token which is valid only for next 15 minutes user.update!(login_token: SecureRandom.urlsafe_base64, login_token_valid_until: Time.now + 15.minutes) LoginMailer.login_link(user).deliver redirect_to root_path, notice: 'Login link sended to your email' end end class SessionsController < ApplicationController # This is the action triggered by login link def create # We don't sign in user with token which expired user = User.where(login_token: params[:token]) .where('login_token_valid_until > ?', Time.now).first if user # Here we nullify login token so it can't be reused user.update!(login_token: nil, login_token_valid_until: 1.year.ago) self.current_user = user redirect_to root_path, notice: 'Signed-in sucesfully' else redirect_to root_path, alert: 'Invalid or expired login link' end end # Simple sign-out. Just set current user to NullUser def destroy self.current_user = NullUser.new redirect_to root_path, notice: 'Sucesfully signed-out' end end class User < ApplicationRecord def anonymous? false end end class NullUser def anonymous? true end def id nil end end class LoginMailer < ApplicationMailer def login_link(user) @user = user mail to: @user.email, subject: 'Sign-in into someapp.com' end end
That’s pretty much it without views. Views are just some tables and forms. Check them out in the example app repo.
With this passwordless login you don’t need to bother yourself and your users with:
Some cons were mentioned in discussion under the post. I consider these two the worst: