This is a short recipe how to add the functionality to a Hobo powered Rails application to login via access to an email address.
I thought about security concerns regarding this.
The only drawback compared to an application that lets you change a forgotten password is that this way, you wouldn't notice if somebody logged in with the stolen login link, because there is no need for a password change.
I'm no security expert. If you are :-), feel free to add comments to this gists, that help me improve security.
I'll walk you through the login process: I expect, that you have configured mailing already propertly.
First we need a form for providing the email-address. I put it into app/views/front/index.dryml
:
<page title="Home">
<body: class="front-page"/>
<content:>
<!-- ... -->
<section class="content-body">
<if test="¤t_user.name == 'Gast'" >
<form action="request_login_link" method="POST">
<fieldset>
<label><t key="attributes.email_address"/></label>
<input type="string" name="email_address"/>
<span class="help-block"><t key="mercator.offer_email_login"/></span>
</fieldset>
<p> </p>
<submit label="&t('mercator.request_email')"/>
</form>
</if>
</section>
</content:>
</page>
This form posts to an action that I configure in config/routes.rb
:
Mercator::Application.routes.draw do
# ...
post 'request_login_link(.:format)' => 'users#request_email_login', :as => 'request_email_login'
end
I define the action in the app/controllers/users_controller.rb
:
class UsersController < ApplicationController
hobo_user_controller
# ...
def request_email_login
user = User.find_by_email_address(params[:email_address])
user.lifecycle.create_key!(current_user)
UserMailer.login_link(user, user.lifecycle.key).deliver
end
end
I want a fresh key here, just to be on the safe side. For the email sending I use a new user mailer method in
app/controllers/mailers/user_mailer.rb
:
class UserMailer < ActionMailer::Base
# ...
def login_link(user, key)
@user, @key = user, key
mail( :subject => "#{app_name} -- Login Link",
:to => user.email_address )
end
end
that needs a view app/views/user_mailer/login_link.erb
:
<h3>Hello <%= @user %>,</h3>
<p>You can log in to <%= @app_name %> using the following link once :</p>
<p> <%= link_to login_via_email_user_url(:id => @user, :key => @key), login_via_email_user_url(:id => @user, :key => @key) %></p>
<p> The link is valid for 10 minutes (until <%= l(Time.now + 10.minutes, locale: :en) %>).
<p>The <%= @app_name %> team.</p>
<hr>
<!-- additional language texts go here -->
So the mail is sent and the user's browser is rendering the minimalistic view app/views/users/request_email_login.dryml
:
<page title="Home">
<body: class="front-page"/>
<content:>
<header class="content-header hero-unit">
<section class="welcome-message">
<t key="mercator.email_sent"/>
</section>
</header>
</content:>
</page>
The mail is delivered. If the user clicks on the link in it, a transition login_via_email is triggered.
In the models/user.rb
model we add the extra transition to the lifecycle:
class User < ActiveRecord::Base
# ...
lifecycle do
# ...
transition :login_via_email, {:active => :active},
available_to: :key_holder, if: "Time.now() - self.key_timestamp < 10.minutes"
end
end
I shortcutted the two steps of a transition into one (the login link is a get request) and we want to get to the PUT part of the transition as fast as possible.
So one more time in the app/controllers/users_controller.rb
:
class UsersController < ApplicationController
# ...
def login_via_email
do_transition_action :login_via_email do
self.current_user = User.find(params[:id])
create_auth_cookie
self.current_user.lifecycle.create_key!(current_user)
redirect_to home_page
end
end
create_auth_cookie
uses self.current_user
automatically and we create a new lifecycle key, so that our link is devalidated and we have a sort of a one time link/password.
The user is logged in and redirected to wherever we want. Yeah! That's all there is to it.