Autenticación Passwordless con Rails
¿Qué es una autenticación passwordless?
Una autenticación passwordless es una manera de verificar un usuario en nuestra app sin necesidad de que requieran un nombre de usuario y contraseña. En cambio, vamos a generar un token de seguridad para autenticar al usuario.
Estos son algunos de los beneficios que otorgan tener una autenticación passwordless:
- una UX mejorada, los usuarios no tienen la necesidad de recordar sus contraseñas y generar una nueva en el caso de que se olviden la antigua.
- aumento en seguridad, se reduce la posibilidad de el reutilización de la misma contraseña en el caso de que surja un hackeo/leak en algunas de las plataformas en las que se haya registrado. También imposibilita la probabilidad de phishing.
- relativamente fácil de crear, se simplifica el proceso de login.
Escenario de uso:
Los casos de uso con esta funcionalidad son infinitos. En este ejemplo en particular lo voy a asemejar a un trabajo que tuve en un cliente, en donde hice uso de esta funcionalidad, pero con una versión simplificada porque sino el articulo se me va de las manos. No vamos a hacer uso de gemas como devise
o sorcery
, aunque se podría realizar de igual manera con ellas.
Setup:
Vamos a crear la app: rails new passwordless-auth-app -T --skip-turbokinks --database=postgresql
1. Instalación de gemas:
NOTA: las gemas que utilizo no son necesarias para implementar esta funcionalidad.
# ./Gemfile.rb
# formulario para ingreso del email
gem 'simple_form', '~> 5.1'
group :development, :test do
# para chequear el envio de mails en entorno de desarrollo
gem 'letter_opener', '~> 1.7'
end
bundle install
debería instalar todas las gemas. Luego de esto vamos a setear la configuración para letter_opener
.
# ./config/environments/development.rb
Rails.application.configure do
config.action_mailer.default_url_options = { host: "http://localhost:3000" }
config.action_mailer.delivery_method = :letter_opener
(...)
end
2. Creación del esqueleto de la app:
Migraciones de modelos y controllers:
# Modelos
rails g model User email:string login_token:string login_token_expires_at:datetime &&
rake db:create &&
rake db:migrate
# Controllers
rails g controller sessions index
rails g controller users new create
Modificación de rutas:
# ./config/routes.rb
Rails.application.routes.draw do
resources :sessions, only: [:index] do
get :undefined_login_token, on: :collection
end
resources :users, only: [:new, :create] do
post :update_new_access_token, on: :collection
get :generate_new_access_token, on: :collection
get :email_sent, on: :member
end
root to: 'users#new'
end
Modelo, callbacks, y, metodos de instancia:
Ahora viene la parte mas divertida, en donde nos vamos a ensuciar con un poco de lógica. Vamos a crear la función generate_login_token!
en donde actualizamos el login_token con un valor alfanumérico al azar y la fecha de expiración de ese token. A su vez vamos a setear un callback para que esta función se ejecute cada vez que creamos un nuevo record.
Por otro lado en login_token_url
ya dejamos seteado el path para que el usuario pueda acceder a nuestra app (recuerden cambiar esta función si van a subir su app a producción ya que va a romper porque localhost:3000
va a ser una url inexistente).
# ./models/user.rb
class User < ApplicationRecord
# Callbacks
before_create :generate_login_token!
# Instance Methods
def generate_login_token!
self.login_token = SecureRandom.urlsafe_base64
self.login_token_expires_at = 10.days.from_now
end
def login_token_valid?
login_token_expires_at > Time.now
end
def login_token_url
# NOTA: Cambiar la url cuando la app este en produccion, se puede hacer por medio de la gema dotenv o rails credentials.
"http://localhost:3000/sessions?login_token=#{login_token}"
end
end
Controller:
El controller no tiene mucha logica, paso a explicar cual sería el objetivo de cada path:
- new: va a ser donde el user ingrese su email para que le envíen el
login_token
. - create: Es un create básico.
- generate_new_access_token: En el caso de que el usuario se olvide/vence el
login_token
, puede entrar a esta vista donde se le pediría que introduzca el email de su usuario ya registrado. - update_new_access_token: Va a ser el path al que se va redireccionar desde la vista de
generate_new_access_token
para generar un nuevologin_token
con el refresh delogin_token_expires_at
.
# ./app/controllers/users_controller.rb
class UsersController < ApplicationController
before_action :set_user, only: %i[:email_sent]
def new
@user = User.new
end
def create
@user = User.new(user_params)
redirect_to email_sent_user_path(@user) if @user.save
end
def email_sent
end
def generate_new_access_token
end
def update_new_access_token
@user = User.find_by(user_params)
if @user.present?
@user.generate_login_token!
@user.save
redirect_to generate_new_access_token_users_path, notice: "Login token was successfully updated, check your email."
else
redirect_to undefined_login_token_sessions_path
end
end
private
def set_user
@user = User.find(params[:user_id])
end
def user_params
params.require(:user).permit(:email)
end
end
Vistas:
Las vistas las simplifique lo mas posible, con ver el contenido se entiende el objetivo que buscan en cuanto a la UX.
# ./app/views/users/new.html.erb
<h1>Bienvenido a passwordless-auth-app!</h1>
<h2>Introudce to email abajo en el formulario para registrarte.</h2>
<%= simple_form_for(@user) do |f| %>
<%= f.input :email %>
<%= f.button :submit %>
<% end %>
# ./app/views/sessions/email_sent.html.erb
<h1>Gracias por registrarte! Se te ha enviado un email a <%= @user.email %> con el accesso a la plataforma</h1>
# ./app/views/users/generate_new_access_token.html.erb
<h1>Genera un nuevo login token en el caso de que se te haya expirado o no lo recuerdes</h1>
<%= simple_form_for(:user, url: update_new_access_token_users_path, method: :post) do |f| %>
<%= f.input :email %>
<%= f.button :submit %>
<% end %>
# ./app/views/sessions/undefined_login_token.html.erb
<h1>No has podido acceder a tu cuenta?</h1>
<h2>Si se expiro tu login_token, genera uno nuevo <%= link_to 'aqui', generate_new_access_token_users_path %></h2>
<h2>No tienes cuenta? <%= link_to 'Registrate', new_user_path %> </h2>
# ./app/views/sessions/index.html.erb
<h1>Bienvenido <%= @user.email %></h1>
<p>Aqui puedes administrar y modificar todos tus datos.</p>
3. Agregado de notificaciones para enviar los emails:
La frutillita del postre, configurar el proceso de ejecución del mail en el que se envíe el login_token
.
###Iniciamos creando una el mailer:
rails generate mailer LoginMailer
Luego agregamos la función send_email
dentro de la clase LoginMailer
:
# ./app/mailers/login_mailer.rb
class LoginMailer < ApplicationMailer
def send_email(user, url)
@user = user
@url = url
mail to: @user.email, subject: 'Ingresar a paswordless-auth-app'
end
end
Y finalmente agregamos la vista de este mailer:
# ./app/views/login_mailer/send_email.html.erb
<!DOCTYPE html>
<html>
<head>
<meta content='text/html; charset=UTF-8' http-equiv='Content-Type' />
</head>
<body>
<h1>Bienvenido <%= @user.email %>,</h1>
<a href="<%= @url %>">Haz click para acceder a la app.</a>
</html>
Callback para la ejecución del mail:
Para tener el proceso completo sería generar una ejecución automática de este mail una vez que se crea el usuario/renueva el login_token
.
# ./app/models/user.rb
class User < ApplicationRecord
# Callback
after_save :send_new_login_token_notification, if: Proc.new { saved_change_to_attribute?(:login_token) }
# Instance Method
def send_new_login_token_notification
LoginMailer.send_email(self, login_token_url).deliver_now
end
end
4. Autenticación de la sesión:
Por ultimo paso queda validar que el login_token
sea el correcto dentro de SessionsController
:
class SessionsController < ApplicationController
def index
@user = User.find_by(login_token: login_token_params)
unless @user.present? && @user.login_token_valid?
redirect_to undefined_login_token_sessions_path
end
end
def undefined_login_token
end
private
def login_token_params
params.require(:login_token)
end
end
5. Et Voaià!
Ya tenemos funcionando la autenticación de usuarios sin necesidad de tener una contraseña 🎉.
El objetivo de este post es dar una idea básica de una de las muchas formas en las que se puede crear este tipo de autenticaciones, hay mucho mas en términos de seguridad y autorización para trabajar.
Les comparto los 2 principales posts en los que me sirvieron para realizar este trabajo: