Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Magic Login submodule #8

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
50 changes: 50 additions & 0 deletions lib/generators/sorcery/templates/initializer.rb
Expand Up @@ -347,6 +347,56 @@
#
# user.reset_password_time_between_emails =

# -- magic_login --
# magic login code attribute name.
# Default: `:magic_login_token`
#
# user.magic_login_token_attribute_name =


# expires at attribute name.
# Default: `:magic_login_token_expires_at`
#
# user.magic_login_token_expires_at_attribute_name =


# when was email sent, used for hammering protection.
# Default: `:magic_login_email_sent_at`
#
# user.magic_login_email_sent_at_attribute_name =


# mailer class. Needed.
# Default: `nil`
#
# user.magic_login_mailer =


# magic login email method on your mailer class.
# Default: `:magic_login_email`
#
# user.magic_login_email_method_name =


# when true sorcery will not automatically
# email magic login details and allow you to
# manually handle how and when email is sent
# Default: `false`
#
# user.magic_login_mailer_disabled =


# how many seconds before the request expires. nil for never expires.
# Default: `nil`
#
# user.magic_login_expiration_period =


# hammering protection, how long in seconds to wait before allowing another email to be sent.
# Default: `5 * 60`
#
# user.magic_login_time_between_emails =

# -- brute_force_protection --
# Failed logins attribute name.
# Default: `:failed_logins_count`
Expand Down
9 changes: 9 additions & 0 deletions lib/generators/sorcery/templates/migration/magic_login.rb
@@ -0,0 +1,9 @@
class SorceryMagicLogin < ActiveRecord::Migration
def change
add_column :<%= model_class_name.tableize %>, :magic_login_token, :string, :default => nil
add_column :<%= model_class_name.tableize %>, :magic_login_token_expires_at, :datetime, :default => nil
add_column :<%= model_class_name.tableize %>, :magic_login_email_sent_at, :datetime, :default => nil

add_index :<%= model_class_name.tableize %>, :magic_login_token
end
end
1 change: 1 addition & 0 deletions lib/sorcery.rb
Expand Up @@ -18,6 +18,7 @@ module Submodules
require 'sorcery/model/submodules/activity_logging'
require 'sorcery/model/submodules/brute_force_protection'
require 'sorcery/model/submodules/external'
require 'sorcery/model/submodules/magic_login'
end
end

Expand Down
128 changes: 128 additions & 0 deletions lib/sorcery/model/submodules/magic_login.rb
@@ -0,0 +1,128 @@
module Sorcery
module Model
module Submodules
# This submodule adds the ability to login via email without password.
# When the user requests an email is sent to him with a url.
# The url includes a token, which is also saved with the user's record in the db.
# The token has configurable expiration.
# When the user clicks the url in the email, providing the token has not yet expired,
# he will be able to login.
#
# When using this submodule, supplying a mailer is mandatory.
module MagicLogin
def self.included(base)
base.sorcery_config.class_eval do
attr_accessor :magic_login_token_attribute_name, # magic login code attribute name.
:magic_login_token_expires_at_attribute_name, # expires at attribute name.
:magic_login_email_sent_at_attribute_name, # when was email sent, used for hammering
# protection.

:magic_login_mailer, # mailer class. Needed.

:magic_login_mailer_disabled, # when true sorcery will not automatically
# email magic login details and allow you to
# manually handle how and when email is sent

:magic_login_email_method_name, # magic login email method on your
# mailer class.

:magic_login_expiration_period, # how many seconds before the request
# expires. nil for never expires.

:magic_login_time_between_emails # hammering protection, how long to wait
# before allowing another email to be sent.

end

base.sorcery_config.instance_eval do
@defaults.merge!(:@magic_login_token_attribute_name => :magic_login_token,
:@magic_login_token_expires_at_attribute_name => :magic_login_token_expires_at,
:@magic_login_email_sent_at_attribute_name => :magic_login_email_sent_at,
:@magic_login_mailer => nil,
:@magic_login_mailer_disabled => false,
:@magic_login_email_method_name => :magic_login_email,
:@magic_login_expiration_period => 15 * 60,
:@magic_login_time_between_emails => 5 * 60 )

reset!
end

base.extend(ClassMethods)

base.sorcery_config.after_config << :validate_mailer_defined
base.sorcery_config.after_config << :define_magic_login_fields

base.send(:include, InstanceMethods)

end

module ClassMethods
# Find user by token, also checks for expiration.
# Returns the user if token found and is valid.
def load_from_magic_login_token(token)
token_attr_name = @sorcery_config.magic_login_token_attribute_name
token_expiration_date_attr = @sorcery_config.magic_login_token_expires_at_attribute_name
load_from_token(token, token_attr_name, token_expiration_date_attr)
end

protected

# This submodule requires the developer to define his own mailer class to be used by it
# when magic_login_mailer_disabled is false
def validate_mailer_defined
msg = "To use magic_login submodule, you must define a mailer (config.magic_login_mailer = YourMailerClass)."
raise ArgumentError, msg if @sorcery_config.magic_login_mailer.nil? and @sorcery_config.magic_login_mailer_disabled == false
end

def define_magic_login_fields
sorcery_adapter.define_field sorcery_config.magic_login_token_attribute_name, String
sorcery_adapter.define_field sorcery_config.magic_login_token_expires_at_attribute_name, Time
sorcery_adapter.define_field sorcery_config.magic_login_email_sent_at_attribute_name, Time
end

end

module InstanceMethods
# generates a reset code with expiration
def generate_magic_login_token!
config = sorcery_config
attributes = {config.magic_login_token_attribute_name => TemporaryToken.generate_random_token,
config.magic_login_email_sent_at_attribute_name => Time.now.in_time_zone}
attributes[config.magic_login_token_expires_at_attribute_name] = Time.now.in_time_zone + config.magic_login_expiration_period if config.magic_login_expiration_period

self.sorcery_adapter.update_attributes(attributes)
end

# generates a magic login code with expiration and sends an email to the user.
def deliver_magic_login_instructions!
mail = false
config = sorcery_config
# hammering protection
return false if config.magic_login_time_between_emails.present? && self.send(config.magic_login_email_sent_at_attribute_name) && self.send(config.magic_login_email_sent_at_attribute_name) > config.magic_login_time_between_emails.seconds.ago.utc
self.class.sorcery_adapter.transaction do
generate_magic_login_token!
mail = send_magic_login_email! unless config.magic_login_mailer_disabled
end
mail
end

# Clears the token.
def clear_magic_login_token!
config = sorcery_config
self.sorcery_adapter.update_attributes({
config.magic_login_token_attribute_name => nil,
config.magic_login_token_expires_at_attribute_name => nil
})
end

protected

def send_magic_login_email!
generic_send_email(:magic_login_email_method_name, :magic_login_mailer)
end
end

end
end
end
end