Devise reconfirmation hook - ruby-on-rails

This question is very similar to Rails Devise: after_confirmation except I'm looking for more specifically a reconfirmation hook
My use case : I need to synchronise a new user email on some third party service (Intercom to be precise).
I have a first implementation using an API where I have put the logic I need there (all tidied up in a service)
However, I often end up doing a lot of maintenance using the console, and the most obvious things that come to mind is to perform
user.email = 'newemail#example.com'
user.confirm # or skip_reconfirmation
user.save
Using this, I do not fire my resynchronisation logic automatically. Is there a way to force some reconfirmation callback ? overriding after_confirmation does not seem to work

Not really an answer, but in the meantime I have monkeypatched my user class with the following (where xxx represents some custome methods added on the user class to sync to third party services the new email)
class User
include User::Console if defined?('Rails::Console')
end
module User::Console
extend ActiveSupport::Concern
included do
## Override Devise confirmable to add warnings
def confirmation_warning
puts "WARNING !!".red
puts "calling `#confirm` or `#skip_reconfirmation!` on the user does not sync to xxx !\n"\
'Please either use [Name of custom confirmation service], '\
'or use `user.sync_to_xxx` to propagate changes'.red
end
def confirm
confirmation_warning
super
end
def skip_reconfirmation!
confirmation_warning
super
end
end
end

I have the exact same use case, where I need to sync a new user email on a third party service.
The way I solved it is a before_update filter on the User model, and using email_changed?:
class User < ApplicationRecord
before_update :update_email_in_third_party_service
private
def update_email_in_third_party_service
return unless self.valid? && self.email_changed?
# We passed our check, update email in the third party service (preferably in a background job)
end
end

Related

Devise - bypass_sign_in without active_for_authentication? callback

I have functionality of inactive account in my application for handling this i override active_for_authentication? method as below
def active_for_authentication?
super && activated?
end
But In my application super admin can also directly login in to other user account, whether it is active or not active
bypass_sign_in(User.find(resource.id))
I used above method for by pass sign in, it allows me to directly sign in only for activated user, when i login for non activated user it goes in infinite loop .
Any solutions to over come this issue or don't run active_for_authentication? callback when bypass_sign_in?
When admin logs in to another user account you can store some additional data in session, that makes it clear that this is the super admin mode.
def login_as(another_user)
return unless current_user.super_admin?
session[:super_admin_mode] = true
bypass_sign_in(another_user)
end
Unfortunately, you can't access session in Rails models, but you can store needed session information in some per-request global variable that is available in models. The solution might be like this:
module SessionInfo
def self.super_user_mode?
!!Thread.current[:super_user_mode]
end
def self.super_user_mode=(value)
Thread.current[:super_user_mode] = value
end
end
In the ApplicationController:
class ApplicationController < ActionController::Base
before_filter :store_session_info
private
def store_session_info
SessionInfo.super_user_mode = session[:super_admin_mode]
end
end
In the model:
def active_for_authentication?
super && (activated? || SessionInfo.super_user_mode?)
end
Also, you should make sure that the :super_admin_mode flag is removed from session when the super user logs out. Maybe it happens automatically, I am not sure. Maybe you will need to do it manually overriding Devise::SessionsController#destroy method (see the example below)
def destroy
session[:super_admin_mode] = nil
super
end
Also read this for better understanding of how devise handles session Stop Devise from clearing session
I recently came across a similar issue where I needed to allow an Admin to sign in as regular Users who were not active in Devise. I came up with the following solution that doesn't involve using Thread.current (which after looking into further online it seems like using Thread.current could be a precarious solution to this problem).
You can create a subclass of User called ProxyUser that has the active_for_authentication? return true. Something like this:
class ProxyUser < User
# If you have a type column on User then uncomment this line below
# as you dont want to expect ProxyUser to have type 'ProxyUser'
#
# self.inheritance_column = :_type_disabled
devise :database_authenticatable
def active_for_authentication?
true
end
end
Then in the controller you want something like this:
proxy_user = ProxyUser.find(params[:user_id])
sign_in :proxy_user, proxy_user
Also in your routes you will need devise to expect ProxyUser so include:
devise_for :proxy_users
And finally when you sign this user out (assuming you can sign the user out in your controller code) make sure to tell devise the scope of the sign out, so you would do
sign_out :proxy_user
And then finally note that in your app you may be expecting current_user in different places (such as if you use CanCanCan for authorization) and now when you sign in as a proxy_user your app will return current_user as nil. Your app will instead have an object called current_proxy_user that will be your signed-in ProxyUser object. There are many ways to handle the issues resulting from your current_user returning nil in this case (including overwriting current_user in your application controller).

Limiting number of simultaneous logins (sessions) with Rails and Devise? [duplicate]

My app is using Rails 3.0.4 and Devise 1.1.7.
I'm looking for a way to prevent users from sharing accounts as the app is a subscription based service. I've been searching for over a week, and I still don't know how to implement a solution. I'm hoping someone has implemented a solution and can point me in the right direction.
Solution (Thank you everyone for your answers and insight!)
In application controller.rb
before_filter :check_concurrent_session
def check_concurrent_session
if is_already_logged_in?
sign_out_and_redirect(current_user)
end
end
def is_already_logged_in?
current_user && !(session[:token] == current_user.login_token)
end
In session_controller that overrides Devise Sessions controller:
skip_before_filter :check_concurrent_session
def create
super
set_login_token
end
private
def set_login_token
token = Devise.friendly_token
session[:token] = token
current_user.login_token = token
current_user.save
end
In migration AddLoginTokenToUsers
def self.up
change_table "users" do |t|
t.string "login_token"
end
end
def self.down
change_table "users" do |t|
t.remove "login_token"
end
end
This gem works well: https://github.com/devise-security/devise-security
Add to Gemfile
gem 'devise-security'
after bundle install
rails generate devise_security:install
Then run
rails g migration AddSessionLimitableToUsers unique_session_id
Edit the migration file
class AddSessionLimitableToUsers < ActiveRecord::Migration
def change
add_column :users, :unique_session_id, :string, limit: 20
end
end
Then run
rake db:migrate
Edit your app/models/user.rb file
class User < ActiveRecord::Base
devise :session_limitable # other devise options
... rest of file ...
end
Done. Now logging in from another browser will kill any previous sessions. The gem actual notifies the user that he is about to kill a current session before logging in.
You can't do it.
You can control IP addresses of user, so you can prevent presence of user from two IP at a time. ANd you can bind login and IP. You can try to check cities and other geolocation data through IP to block user.
You can set cookies to control something else.
But none of this will guarantee that only one user uses this login, and that those 105 IP from all over the world doesn't belong to only one unique user, which uses Proxy or whatever.
And the last: you never need this in the Internet.
UPD
However, what I'm asking is about limiting multiple users from using the same account simultaneously which I feel should be possible
So you can store some token, that will contain some encrypted data: IP + secret string + user agent + user browser version + user OS + any other personal info: encrypt(IP + "some secret string" + request.user_agent + ...). And then you can set a session or cookie with that token. And with each request you can fetch it: if user is the same? Is he using the same browser and the same browser version from the same OS etc.
Also you can use dynamic tokens: you change token each request, so only one user could use system per session, because each request token will be changed, another user will be logged out as far as his token will be expired.
This is how I solved the duplicate session problem.
routes.rb
devise_for :users, :controllers => { :sessions => "my_sessions" }
my_sessions controller
class MySessionsController < Devise::SessionsController
skip_before_filter :check_concurrent_session
def create
super
set_login_token
end
private
def set_login_token
token = Devise.friendly_token
session[:token] = token
current_user.login_token = token
current_user.save(validate: false)
end
end
application_controller
def check_concurrent_session
if duplicate_session?
sign_out_and_redirect(current_user)
flash[:notice] = "Duplicate Login Detected"
end
end
def duplicate_session?
user_signed_in? && (current_user.login_token != session[:token])
end
User model
Add a string field via a migration named login_token
This overrides the default Devise Session controller but inherits from it as well. On a new session a login session token is created and stored in login_token on the User model. In the application controller we call check_concurrent_session which signs out and redirects the current_user after calling the duplicate_session? function.
It's not the cleanest way to go about it, but it definitely works.
As far as actually implementing it in Devise, add this to your User.rb model.
Something like this will log them out automatically (untested).
def token_valid?
# Use fl00rs method of setting the token
session[:token] == cookies[:token]
end
## Monkey Patch Devise methods ##
def active_for_authentication?
super && token_valid?
end
def inactive_message
token_valid? ? super : "You are sharing your account."
end
I found that the solution in the original posting did not quite work for me. I wanted the first user to be logged out and a log-in page presented. Also, the sign_out_and_redirect(current_user) method does not seem to work the way I would expect. Using the SessionsController override in that solution I modified it to use websockets as follows:
def create
super
force_logout
end
private
def force_logout
logout_subscribe_address = "signout_subscribe_response_#{current_user[:id]}"
logout_subscribe_resp = {:message => "#{logout_subscribe_address }: #{current_user[:email]} signed out."}
WebsocketRails[:signout_subscribe].trigger(signout_subscribe_address, signout_subscribe_resp)
end
end
Make sure that all web pages subscribe to the signout channel and bind it to the same logout_subscribe_address action. In my application, each page also has a 'sign out' button, which signs out the client via the devise session Destroy action. When the websocket response is triggered in the web page, it simply clicks this button - the signout logic is invoked and the first user is presented with the sign in page.
This solution also does not require the skip_before_filter :check_concurrent_session and the model login_token since it triggers the forced logout without prejudice.
For the record, the devise_security_extension appears to provide the functionality to do this as well. It also puts up an appropriate alert warning the first user about what has happened (I haven't figured out how to do that yet).
Keep track of uniq IPs used per user. Now and then, run an analysis on those IPs - sharing would be obvious if a single account has simultaneous logins from different ISPs in different countries. Note that simply having a different IP is not sufficient grounds to consider it shared - some ISPs use round-robin proxies, so each hit would necessarily be a different IP.
While you can't reliably prevent users from sharing an account, what you can do (I think) is prevent more than one user being logged on at the same time to the same account. Not sure if this is sufficient for your business model, but it does get around a lot of the problems discussed in the other answers. I've implemented something that is currently in beta and seems to work reasonably well - there are some notes here

Devise: Create users without password

In our application we have normal users. However, we want to be able to make invitations, to invite certain people. Note that an invitation is directly coupled to a user, as we want to be able to set certain settings for these users already. (We are mitigating clients from our old software to the new).
So:
An admin should be able to create a new user and change its settings.
When someone follows a link with their invitation_token, they should see a form where they can set a password for their account.
What I am having trouble with, is how to enable the admin to create an user account, bypassing the normal password validation. It would be a horrible solution if a default password would need to be set, as this would create a severe security flaw.
How to create a new User in Devise without providing a password?
There are at least two ways to do what you want:
Method 1:
Overload Devise's password_required? method
class User < ActiveRecord::Base
attr_accessor :skip_password_validation # virtual attribute to skip password validation while saving
protected
def password_required?
return false if skip_password_validation
super
end
end
Usage:
#user.skip_password_validation = true
#user.save
Method 2:
Disable validation with validate: false option:
user.save(validate: false)
This will skip validation of all fields (not only password). In this case you should make sure that all other fields are valid.
...
But I advise you to not create users without password in your particular case. I would create some additional table (for example, invitations) and store all required information including the fields that you want to be assigned to a user after confirmation.
TL;DR:
user.define_singleton_method(:password_required?) { false }
Fiddle:
class MockDeviseUser
protected
def password_required?
true
end
end
class User < MockDeviseUser
def is_password_required?
puts password_required?
end
end
unrequired_password_user = User.new
unrequired_password_user.define_singleton_method(:password_required?) { false }
unrequired_password_user.is_password_required?
regular_user = User.new
regular_user.is_password_required?
#false
#true
You can now use the DeviseInvitable gem for this.
It allows you to do exactly what you're asking.
If you need to completely overwrite the password requirement just define the following on your model:
def password_required?
false
end

Customizing Devise gem (Rails)

I am trying to add a new field "registration code" along with the email/password in sign up form. (i have the registration code field in a separate database, only a valid registration code/email pair will have to work with the sign up)
I could not able to find any controller for actions done by devise gem.
How do i customize devise to achieve this?
Thanks in advance.
It seems like your question basically has nothing to do with Devise itself (besides the views). To validate your registration code/email pairs, you surely need to add this as validation.
The easy way to validate registration code could be:
class User
validate :validate_registration_code
private
def validate_registration_code
reg_code = RegistrationCode.find_by_code(registration_code)
unless reg_code.email == record.email
errors.add(:registration_code, "Invalid registration code for #{record.email}")
end
end
end
You also might want to write simple custom validator:
class RegistrationCodeValidator < ActiveModel::Validator
def validate(record)
# actual reg code validation
# might look like:
reg_code = RegistrationCode.find_by_code(record.registration_code)
unless reg_code.email == record.email
record.errors[:registration_code] << "Invalid registration code for #{record.email}"
end
end
end
# in your User model
class User
# include registration code validator
include RegistrationCodeValidator
validates_with MyValidator
end
Devise keeps it's controllers behind the scenes inside the gem. If you want to add an action or modify one, you have to subclass it and do a little work in routes to get the action to your controller.
However you shouldn't need to do that to add a field. See goshakkk's answer

How can I send a welcome email to newly registered users in Rails using Devise?

I am using Devise on Rails and I'm wondering if there is a hook or a filter that I can use to add a bit of code to Devise's user registration process and send a welcome email to the user after an account has been created. Without Devise it would be something like this...
respond_to do |format|
if #user.save
Notifier.welcome_email(#user).deliver # <=======
...
The next most popular answer assumes you're using using Devise's :confirmable module, which I'm not.
I didn't like the other solutions because you have to use model callbacks, which will always send welcome emails even when you create his account in the console or an admin interface. My app involves the ability to mass-import users from a CSV file. I don't want my app sending a surprise email to all 3000 of them one by one, but I do want users who create their own account to get a welcome email.
The solution:
1) Override Devise's Registrations controller:
#registrations_controller.rb
class RegistrationsController < Devise::RegistrationsController
def create
super
UserMailer.welcome(resource).deliver unless resource.invalid?
end
end
2) Tell Devise you overrode its Registrations controller:
# routes.rb
devise_for :users, controllers: { registrations: "registrations" }
https://stackoverflow.com/a/6133991/109618 shows a decent (not perfect) answer, but at least better than ones I'm seeing here. It overrides the confirm! method:
class User < ActiveRecord::Base
devise # ...
# ...
def confirm!
welcome_message # define this method as needed
super
end
# ...
end
This is better because it does not use callbacks. Callbacks are not great to the extent that they (1) make models hard to test; (2) put too much logic into models. Overusing them often means you have behavior in a model that belongs elsewhere. For more discussion on this, see: Pros and cons of using callbacks for domain logic in Rails.
The above approach ties into the confirm! method, which is preferable to a callback for this example. Like a callback though, the logic is still in the model. :( So I don't find the approach fully satisfactory.
I solved this by using a callback method. It's not the cleanest of solutions, not as clean as an observer, but I'll take it. I'm lucky Mongoid implemented the ActiveRecord callbacks!
after_create :send_welcome_mail
def send_welcome_mail
Contact.welcome_email(self.email, self.name).deliver
end
I would recommend using a ActiveRecord::Observer. The idea with the observer is that you would create a class with an after_save method that would call the notification. All you need to do is create the observer class and then modify the application configuration to register the observer. The documentation describes the process quite well.
Using the observer pattern means you do not need to change any logic in the controller.
Since a yield has been added to the Devise controller methods a while back, I think this is now probably the best way to do it.
class RegistrationsController < Devise::RegistrationsController
def create
super do |resource|
Notifier.welcome_email(resource).deliver if resource.persisted?
end
end
end

Resources