Skip to content

Commit

Permalink
Allow re-sending of magic link emails
Browse files Browse the repository at this point in the history
When the config setting Passwordless.allow_token_resend = true a visit
to /users/sign_in/TOKEN_URL for an expired or already claimed user will
return a page with a button which can be used to resend an email with
this token. Extra care is taken to record a query parameter
`destination_path` into the current session, so opening the new email
immediately from the same browser redirects you to the proper location.
  • Loading branch information
Daan van Vugt committed Apr 14, 2023
1 parent 6ed4155 commit 1237a8b
Show file tree
Hide file tree
Showing 5 changed files with 33 additions and 10 deletions.
27 changes: 19 additions & 8 deletions app/controllers/passwordless/sessions_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ def create
session = build_passwordless_session(@resource)

if session.save
if params[:passwordless][:destination_path]
save_passwordless_redirect_location!(@resource.class, params[:passwordless][:destination_path])
end

if Passwordless.after_session_save.arity == 2
Passwordless.after_session_save.call(session, request)
else
Expand All @@ -45,15 +49,18 @@ def create
def show
# Make it "slow" on purpose to make brute-force attacks more of a hassle
BCrypt::Password.create(params[:token])
sign_in(passwordless_session)
@email_field = email_field
@session = passwordless_session
@destination_path = params[:destination_path] || ''
sign_in(@session)

redirect_to(passwordless_success_redirect_path)
rescue Errors::TokenAlreadyClaimedError
flash[:error] = I18n.t(".passwordless.sessions.create.token_claimed")
redirect_to(passwordless_failure_redirect_path)
redirect_to(passwordless_failure_redirect_path) unless Passwordless.allow_token_resend
rescue Errors::SessionTimedOutError
flash[:error] = I18n.t(".passwordless.sessions.create.session_expired")
redirect_to(passwordless_failure_redirect_path)
redirect_to(passwordless_failure_redirect_path) unless Passwordless.allow_token_resend
end

# match '/sign_out', via: %i[get delete].
Expand Down Expand Up @@ -107,12 +114,16 @@ def email_field
end

def find_authenticatable
email = params[:passwordless][email_field].downcase.strip

if authenticatable_class.respond_to?(:fetch_resource_for_passwordless)
authenticatable_class.fetch_resource_for_passwordless(email)
if params[:passwordless][:token]
Passwordless::Session.find_by(token: params[:passwordless][:token]).authenticatable
else
authenticatable_class.where("lower(#{email_field}) = ?", email).first
email = params[:passwordless][email_field].downcase.strip

if authenticatable_class.respond_to?(:fetch_resource_for_passwordless)
authenticatable_class.fetch_resource_for_passwordless(email)
else
authenticatable_class.where("lower(#{email_field}) = ?", email).first
end
end
end

Expand Down
7 changes: 7 additions & 0 deletions app/views/passwordless/sessions/show.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<%= form_with model: @session, method: :post, url: send(Passwordless.mounted_as).sign_in_path, data: { turbo: 'false' } do |f| %>
<%= hidden_field_tag 'passwordless[destination_path]', @destination_path %>
<%= hidden_field_tag 'passwordless[token]', @session.token %>
<%= I18n.t('passwordless.sessions.show.explanation') %>
<%= f.submit I18n.t('passwordless.sessions.show.submit') %>
<% end %>
<%= link_to I18n.t('passwordless.sessions.show.new'), send(Passwordless.mounted_as).sign_in_path %>
4 changes: 4 additions & 0 deletions config/locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ en:
token_claimed: "This link has already been used, try requesting the link again"
new:
submit: 'Send magic link'
show:
explanation: 'Your magic link is no longer valid, but you can request a new one.'
submit: 'Resend magic link'
new: 'Or go to the sign in page.'
mailer:
subject: "Your magic link ✨"
magic_link: "Here's your link: %{link}"
1 change: 1 addition & 0 deletions lib/passwordless.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ module Passwordless
mattr_accessor(:default_from_address) { "[email protected]" }
mattr_accessor(:token_generator) { UrlSafeBase64Generator.new }
mattr_accessor(:restrict_token_reuse) { false }
mattr_accessor(:allow_token_resend) { false }
mattr_accessor(:redirect_back_after_sign_in) { true }
mattr_accessor(:mounted_as) { :configured_when_mounting_passwordless }

Expand Down
4 changes: 2 additions & 2 deletions lib/passwordless/controller_helpers.rb
Original file line number Diff line number Diff line change
Expand Up @@ -121,8 +121,8 @@ def sign_out(authenticatable_class)
# passwordless Model.
# @param (see #authenticate_by_session)
# @return [String] the redirect url that was just saved.
def save_passwordless_redirect_location!(authenticatable_class)
session[redirect_session_key(authenticatable_class)] = request.original_url
def save_passwordless_redirect_location!(authenticatable_class, location = request.original_url)
session[redirect_session_key(authenticatable_class)] = location
end

# Resets the redirect_location to root_path by deleting the redirect_url
Expand Down

0 comments on commit 1237a8b

Please sign in to comment.