Devise, how to override send_confirmation_instructions - ruby-on-rails

I'm trying to override the method 'send_confirmation_instructions' as shown here:
http://trackingrails.com/posts/devise-send-confirmation-mail-manually-or-delay-them
with:
def send_confirmation_instructions
generate_confirmation_token! if self.confirmation_token.nil?
::Devise.mailer.delay.confirmation_instructions(self)
end
This seems to no longer work with the latest version of devise. The devise docs show how to override a controller but not a model. Any suggestions on how to override a devise model? Thanks

When you set up Devise, you tell it which model it's working on (e.g. User); many/most of its methods then apply to that class. So that's where you'll want to override stuff.
Here's a comment from the Devise code at lib/devise/models/authenticatable.rb that describes almost exactly what you want to do, if I am reading correctly.
# This is an internal method called every time Devise needs
# to send a notification/mail. This can be overriden if you
# need to customize the e-mail delivery logic. For instance,
# if you are using a queue to deliver e-mails (delayed job,
# sidekiq, resque, etc), you must add the delivery to the queue
# just after the transaction was committed. To achieve this,
# you can override send_devise_notification to store the
# deliveries until the after_commit callback is triggered:
#
# class User
# devise :database_authenticatable, :confirmable
#
# after_commit :send_pending_notifications
#
# protected
#
# def send_devise_notification(notification)
# pending_notifications << notification
# end
#
# def send_pending_notifications
# pending_notifications.each do |n|
# devise_mailer.send(n, self).deliver
# end
# end
#
# def pending_notifications
# #pending_notifications ||= []
# end
# end
#
def send_devise_notification(notification)
devise_mailer.send(notification, self).deliver
end

Why not use devise-async?
Usage
Devise >= 2.1.1
Include Devise::Async::Model to your Devise model
class User < ActiveRecord::Base
devise :database_authenticatable, :confirmable # etc ...
include Devise::Async::Model # should be below call to `devise`
end
Devise < 2.1.1
Set Devise::Async::Proxy as Devise's mailer in config/initializers/devise.rb:
# Configure the class responsible to send e-mails.
config.mailer = "Devise::Async::Proxy"
All
Set your queuing backend by creating config/initializers/devise_async.rb:
# Supported options: :resque, :sidekiq, :delayed_job
Devise::Async.backend = :resque

Related

Secure active storage with devise

Using devise gem to authenticate all users of an application.
I'm trying to implement Active Storage.
Let's say that all users must be authenticated as soon as they reach the app:
class ApplicationController < ActionController::Base
before_action :authenticate_user!
...
end
How to secure the Active Storage generated routes?
URL of an uploaded file can be accessed without having to authenticate first. The unauthenticated user can get the file url generated by Active Storage.
This is not a full answer but a starting point:
The gist: You would need to override the redirect controller.
The docs for activestorage/app/controllers/active_storage/blobs_controller.rb say:
If you need to enforce access protection beyond the
security-through-obscurity factor of the signed blob references,
you'll need to implement your own authenticated redirection
controller.
Also if you plan to use previews the docs for activestorage/app/models/active_storage/blob/representable.rb say
Active Storage provides one [controller action for previews], but you may want to create your own (for
example, if you need authentication).
Also you might find some relevant information in this rails github issue
Update:
Here is a minimal example that "should" work for preventing unauthorised access to the redirects when using the devise gem.
How the url, that the user will be redirected to if logged, is then secured is still another story I guess. By default they expire after 5 minutes but this could be set to a shorter period like 10 seconds (if you replace line 6 in example below with expires_in 10.seconds)
Create a file app/controllers/active_storage/blobs_controller.rb with the following code:
class ActiveStorage::BlobsController < ActiveStorage::BaseController
before_action :authenticate_user!
include ActiveStorage::SetBlob
def show
expires_in ActiveStorage::Blob.service.url_expires_in
redirect_to #blob.service_url(disposition: params[:disposition])
end
end
Please note that the only thing that changed from the original code is that the second line is added
before_action :authenticate_user!
Update 2:
Here is a concern that you can include in ActiveStorage::RepresentationsController and ActiveStorage::BlobsController to enable devise authentication for ActiveStorage
See gist is at https://gist.github.com/dommmel/4e41b204b97238e9aaf35939ae8e1666 also included here:
# Rails controller concern to enable Devise authentication for ActiveStorage.
# Put it in +app/controllers/concerns/blob_authenticatable.rb+ and include it when overriding
# +ActiveStorage::BlobsController+ and +ActiveStorage::RepresentationsController+.
#
# Optional configuration:
#
# Set the model that includes devise's database_authenticatable.
# Defaults to Devise.default_scope which defaults to the first
# devise role declared in your routes (usually :user)
#
# blob_authenticatable resource: :admin
#
# To specify how to determine if the current_user is allowed to access the
# blob, override the can_access_blob? method
#
# Minimal example:
#
# class ActiveStorage::BlobsController < ActiveStorage::BaseController
# include ActiveStorage::SetBlob
# include AdminOrUserAuthenticatable
#
# def show
# expires_in ActiveStorage::Blob.service.url_expires_in
# redirect_to #blob.service_url(disposition: params[:disposition])
# end
# end
#
# Complete example:
#
# class ActiveStorage::RepresentationsController < ActiveStorage::BaseController
# include ActiveStorage::SetBlob
# include AdminOrUserAuthenticatable
#
# blob_authenticatable resource: :admin
#
# def show
# expires_in ActiveStorage::Blob.service.url_expires_in
# redirect_to #blob.representation(params[:variation_key]).processed.service_url(disposition: params[:disposition])
# end
#
# private
#
# def can_access_blob?(current_user)
# #blob.attachments.map(&:record).all? { |record| record.user == current_user }
# end
# end
module BlobAuthenticatable
extend ActiveSupport::Concern
included do
around_action :wrap_in_authentication
end
module ClassMethods
def auth_resource
#auth_resource || Devise.default_scope
end
private
def blob_authenticatable(resource:)
#auth_resource = resource
end
end
private
def wrap_in_authentication
is_signed_in_and_authorized = send("#{self.class.auth_resource}_signed_in?") \
& can_access_blob?(send("current_#{self.class.auth_resource}"))
if is_signed_in_and_authorized
yield
else
head :unauthorized
end
end
def can_access_blob?(_user)
true
end
end
If you want to implement authentication for all endpoints provided by active storage, you can override the ActiveStorage::BaseController based on the original implementation:
# app/controllers/active_storage/base_controller.rb
# frozen_string_literal: true
# The base class for all Active Storage controllers.
class ActiveStorage::BaseController < ActionController::Base
before_action :authenticate_user!
include ActiveStorage::SetCurrent
protect_from_forgery with: :exception
end

DRY Devise models that need to send emails asynchronously

I have multiple Devise models in my app (User, Admin, etc). I can successfully send Devise notification emails asynchronously by overriding #send_devise_notification in each of the models.
# user.rb, admin.rb, etc
private
##
# Uses Active::Job to send Devise's emails asynchronously.
##
def send_devise_notification(notification, *args)
devise_mailer.send(notification, self, *args).deliver_later
end
Since this code is exactly the same in all of the models, I figured I could make a Concern called DeviseAsyncCapable which implements #send_devise_notification the same way as I describe above.
# devise_async_capable.rb
module DeviseAsyncCapable
extend ActiveSupport::Concern
def send_devise_notification(notification, *args)
devise_mailer.send(notification, self, *args).deliver_later
end
end
# user.rb
class User < ActiveRecord::Base
include DeviseAsyncCapable
end
After including the concern in one of the models and testing it, the method defined in the concern was not called, instead Devise sent emails inline. How can I DRY this code?
Update - Answer
As bcd replied, you need to include the concern after you call devise :database_autheticable
#user.rb
class User < ActiveRecord::Base
devise :database_authenticatable#, ...
include DeviseAsyncCapable
end

Have the before_create and after_create methods been deprecated in Rails 4?

class User < ActiveRecord::Base
attr_accessor :password
Rails.logger.info "xxy From outside"
def before_create
Rails.logger.info "xxy From inside the before_create"
end
end
When calling User.save in a controller, my development log picks up xxy From outside but not xxy From inside the before_create so I would I be right in thinking that it's been deprecated?
If so, how can I call a model method before a save? Or been as xxy From outside was logged, does this mean all methods are automatically called when an instance of a model is saved?
They are still there. You seem to be doing it wrong. This is the correct way:
# Define callback:
before_create :method_name
# and then:
def method_name
Rails.logger.info "I am rad"
end
Not that I'm aware of. You might be able to get the result you're looking for by overriding the before_create method (why would you ever do this?) as described in the ActiveModel::Callbacks source.
# First, extend ActiveModel::Callbacks from the class you are creating:
#
# class MyModel
# extend ActiveModel::Callbacks
# end
#
# Then define a list of methods that you want callbacks attached to:
#
# define_model_callbacks :create, :update
#
# This will provide all three standard callbacks (before, around and after)
# for both the <tt>:create</tt> and <tt>:update</tt> methods. To implement,
# you need to wrap the methods you want callbacks on in a block so that the
# callbacks get a chance to fire:
#
# def create
# run_callbacks :create do
# # Your create action methods here
# end
# end
#
# Then in your class, you can use the +before_create+, +after_create+ and
# +around_create+ methods, just as you would in an Active Record module.
#
# before_create :action_before_create
#
# def action_before_create
# # Your code here
# end
They are still there. They just take a block instead of defining them as a method:
Rails.logger.info "xxy From outside"
before_create do
Rails.logger.info "xxy From inside the before_create"
end

Extending Devise to use remote login

I'm trying to follow this blog post to allow remote authentication with devise, but I can't figure out a few things.
What do I call my new files, and where do I put them?
The first one, Devise::Models::RemoteAuthenticatable I assume I call remote_authenticatable.rb and put in a devise folder in my models folder?
The second file, "Warden strategy for Devise" I have no idea what to call it, or where to put it.
Any ideas? I found the tutorial pretty incomplete. It's linked to from the Devise instructions as the way to do this.
EDIT:
I've been doing more reading, and I'm not sure I need to do what it says in that blog post. I've been trying to simply PUT data to my Rails app, but can't get anything to work. Doing a PUT request causes devise to loose authentication.
I know this question is years old, but I also found the tutorial to be incomplete, and I recently spent a couple days trying to get remote authentication to work. So I will provide my solution in case it helps someone in the future. I am using Ruby 2.2.2 and Rails 4.2.5.1.
First of all, this gist was an EXTREMELY helpful reference for me.
I also used the gem fakeweb to mock API calls.
Here are what my files look like:
app/models/user.rb
class User < include ActiveModel::Model
# required because some before_validations are defined in devise
include ActiveModel::Validations
# required to define callbacks
extend ActiveModel::Callbacks
# taken from http://stackoverflow.com/questions/8936906/whats-the-correct-way-to-make-before-validation-etc-work-in-an-activemodel
include ActiveModel::Validations::Callbacks
extend Devise::Models
# create getter and setter methods internally for the fields below
attr_accessor :email, :auth_token
#required by Devise
define_model_callbacks :validation
devise :remote_authenticatable, :timeoutable
# Latest devise tries to initialize this class with values
# ignore it for now
def initialize(options={})
end
end
lib/models/remote_authenticatable.rb
require 'fakeweb' #used for mocking API calls
module Devise
module Models
module RemoteAuthenticatable
extend ActiveSupport::Concern
#
# Here you do the request to the external webservice
#
# If the authentication is successful you should return
# a resource instance
#
# If the authentication fails you should return false
#
def remote_authentication(authentication_hash)
FakeWeb.register_uri(:get, "http://localhost:3000/webservice/login.json",
:body => "{ \"success\": \"true\", \"auth_token\": \"secure_token_123\", \"email\": \"bob#1123.com\"}")
# Your logic to authenticate with the external webservice
response = Net::HTTP.get(URI.parse("http://localhost:3000/webservice/login.json"))
self.email = JSON.parse(response)["email"]
self.auth_token = JSON.parse(response)["auth_token"]
return self
end
module ClassMethods
####################################
# Overriden methods from Devise::Models::Authenticatable
####################################
#
# This method is called from:
# Warden::SessionSerializer in devise
#
# It takes as many params as elements had the array
# returned in serialize_into_session
#
# Recreates a resource from session data
#
def serialize_from_session(data, salt)
resource = self.new
resource.email = data['email']
resource.auth_token = data['auth_token']
resource
end
#
# Here you have to return and array with the data of your resource
# that you want to serialize into the session
#
# You might want to include some authentication data
#
def serialize_into_session(record)
[
{
:email => record.email,
:auth_token => record.auth_token
},
nil
]
end
end
end
end
end
config/initializers/remote_authenticatable.rb
module Devise
module Strategies
class RemoteAuthenticatable < Authenticatable
#
# For an example check : https://github.com/plataformatec/devise/blob/master/lib/devise/strategies/database_authenticatable.rb
#
# Method called by warden to authenticate a resource.
#
def authenticate!
#
# authentication_hash doesn't include the password
#
auth_params = authentication_hash
auth_params[:password] = password
#
# mapping.to is a wrapper over the resource model
#
resource = mapping.to.new
return fail! unless resource
# remote_authentication method is defined in Devise::Models::RemoteAuthenticatable
#
# validate is a method defined in Devise::Strategies::Authenticatable. It takes
#a block which must return a boolean value.
#
# If the block returns true the resource will be logged in
# If the block returns false the authentication will fail!
#
# resource = resource.remote_authentication(auth_params)
if validate(resource){ resource = resource.remote_authentication(auth_params) }
success!(resource)
end
end
end
end
end
config/initializers/devise.rb
Devise.setup do |config|
# ...
# ...
# OTHER CONFIGURATION CODE HERE
# ...
# ...
# ==> Warden configuration
# If you want to use other strategies, that are not supported by Devise, or
# change the failure app, you can configure them inside the config.warden block.
#
# config.warden do |manager|
# manager.intercept_401 = false
# manager.default_strategies(scope: :user).unshift :some_external_strategy
# end
# BEGIN code that was added to this file
config.warden do |manager|
manager.strategies.add(:remote_authenticatable, Devise::Strategies::RemoteAuthenticatable)
manager.default_strategies(:scope => :user).unshift :remote_authenticatable
end
Devise.add_module :remote_authenticatable, :controller => :sessions, :route => { :session => :routes }
# END code that was added to this file
# ...
# ...
# OTHER CONFIGURATION CODE HERE
# ...
# ...
end
config/application.rb
# ...
# ...
# OTHER CODE HERE
# ...
# ...
module RemoteAuth
class Application < Rails::Application
# ...
# OTHER CODE HERE
# ...
# BEGIN code that was added to this file
config.autoload_paths += Dir["#{config.root}/lib/**/"]
config.autoload_paths += Dir["#{config.root}/app/models/**/"]
# END code that was added to this file
end
end
This Gist shows you exactly how to do this. There are a few extra steps, like activating the :token_authenticatable module in your user model, and in config/devise.rb

Devise Async / delayed jobs - send custom emails with delayed job too?

I have a solution where I be able to send the devise ' stock emails ' the mail messages that are defaultly included in Devise with Delayed jobs. In a async manner. Therefore I use the following code:
/initializers/devise_acync.rb
module Devise
module Models
module Confirmable
alias_method :send_confirmation_instructions_without_delay, :send_confirmation_instructions
handle_asynchronously :send_confirmation_instructions
end
module Recoverable
alias_method :send_reset_password_instructions_without_delay, :send_reset_password_instructions
handle_asynchronously :send_reset_password_instructions
end
module Lockable
alias_method :send_unlock_instructions_without_delay, :send_unlock_instructions
handle_asynchronously :send_unlock_instructions
end
module Invitable
alias_method :deliver_invitation_without_delay, :deliver_invitation
handle_asynchronously :deliver_invitation
end
end
end
In my User Model devise is linked to this model I do
def confirm!
welcome_message
super
end
private
def welcome_message
::Devise.mailer.welcome_instructions(self).deliver
end
The big question that keeps me dazzled:
How would I be able to send this welcome message true delayed_job?
And how would you add other emails that are custom and not devise included so they get send true delayed_job also?
I found this to be an elegant solution - http://danseaver.com/blog/2011/10/18/sending-devise-emails-with-delayed-job
I changed it slightly to handle all the outgoing devise emails, running rails 3.0.7 + DJ 3.0.1
config/initializers/devise.rb
# override Devise::Mailer to use queue outgoing mail into DJ
class DeviseMailer < Devise::Mailer
def confirmation_instructions(record)
Devise::Mailer.delay.confirmation_instructions(record)
end
def reset_password_instructions(record)
Devise::Mailer.delay.reset_password_instructions(record)
end
def unlock_instructions(record)
Devise::Mailer.delay.unlock_instructions(record)
end
end
# Use this hook to configure devise mailer, warden hooks and so forth. The first
# four configuration values can also be set straight in your models.
Devise.setup do |config|
# ==> Mailer Configuration
# Configure the e-mail address which will be shown in DeviseMailer.
config.mailer_sender = APP_CONFIG[:member_email]
# Configure the class responsible to send e-mails.
config.mailer = "DeviseMailer"
...
Better to use devise-aync gem or checkout this small blogpost
I found that none of the above worked for me. I'm using Devise 2.0.4 and Rails 3.2.2 with delayed_job_active_record 0.3.2
The way devise actually talks about doing something like this in the comments in the code is to override the methods in the User class. Thus, I solved it like so, and it works perfectly:
app/models/User.rb
def send_on_create_confirmation_instructions
Devise::Mailer.delay.confirmation_instructions(self)
end
def send_reset_password_instructions
Devise::Mailer.delay.reset_password_instructions(self)
end
def send_unlock_instructions
Devise::Mailer.delay.unlock_instructions(self)
end
Have you tried doing it like this:
def welcome_message
::Devise.mailer.delay.welcome_instructions(self)
end
(Question is almost 3 year old, but still easy to find through Google, So here is my contribution)
Best practice is to follow Device's instruction as describe in the source https://github.com/plataformatec/devise/blob/master/lib/devise/models/authenticatable.rb#L129
class User
devise :database_authenticatable, :confirmable
after_commit :send_pending_notifications
protected
def send_devise_notification(notification, *args)
# If the record is new or changed then delay the
# delivery until the after_commit callback otherwise
# send now because after_commit will not be called.
if new_record? || changed?
pending_notifications << [notification, args]
else
devise_mailer.send(notification, self, *args).deliver
end
end
def send_pending_notifications
pending_notifications.each do |notification, args|
devise_mailer.send(notification, self, *args).deliver
end
# Empty the pending notifications array because the
# after_commit hook can be called multiple times which
# could cause multiple emails to be sent.
pending_notifications.clear
end
def pending_notifications
#pending_notifications ||= []
end
end
In which you just need to replace the 2 occurrences of
devise_mailer.send(notification, self, *args).deliver
By
devise_mailer.delay.send(notification, self, *args)
This is actually quite easy to do by overriding a single method on your User model. Devise sends all emails through the send_devise_notification method, so just change the code there to be async-friendly.
def send_devise_notification(notification, opts = {})
devise_mailer.delay.send(notification, self, opts)
end
This code is assuming Sidekiq, though the syntax may be compatible with Resque or DelayedJob.

Resources