Our product is a Rails application; authentication is handled with Devise and OmniAuth. ~2000 users total. We've recently had reports of some users not being able to sign in, but we can't figure out why. Not getting any server errors or anything in our production logs to suggest anything is awry.
Let's look at some code…
class OmniauthCallbacksController < Devise::OmniauthCallbacksController
...
def twitter
oauthorize "twitter"
end
private
def oauthorize(provider)
if env['omniauth.auth']
#identity = Identity.from_omniauth(env['omniauth.auth'])
#person = #identity.person
# 1. failing here? Maybe?
if #person
PersonMeta.create_for_person(#person, session[:referrer], session[:landing_page]) if #person.first_visit?
# 2. PersonMetas *aren't* being created.
flash[:notice] = I18n.t("devise.omniauth_callbacks.success", kind: provider)
sign_in_and_redirect(#person, :event => :authentication)
# 3. Definitely failing by here…
else
redirect_to root_url
end
else
redirect_to root_url
end
end
end
class Identity < ActiveRecord::Base
belongs_to :person, counter_cache: true, touch: true
after_create :check_person
def self.from_omniauth(auth)
where(auth.slice("provider", "uid")).first_or_initialize.tap do |identity|
identity.oauth_token = auth['credentials']['token']
identity.oauth_secret = auth['credentials']['secret']
case auth['provider']
when "twitter"
identity.name = auth['info']['name']
identity.nickname = auth['info']['nickname']
identity.bio = auth['info']['description']
identity.avatar_address = auth['info']['image']
else
raise "Provider #{provider} not handled"
end
identity.save
end
end
def check_person
if person_id.nil?
p = create_person(nickname: nickname, name: name, remote_avatar_url: biggest_avatar)
p.identities << self
end
end
def biggest_avatar
avatar_address.gsub('_bigger', '').gsub('_normal', '') if avatar_address
end
end
class PersonMeta < ActiveRecord::Base
attr_accessible :landing_page, :mixpanel_id, :referrer_url, :person_id
belongs_to :person
def self.create_for_person(person, referrer, landing_page)
PersonMeta.create!(referrer_url: referrer, landing_page: landing_page, person_id: person.id)
end
end
So we have that, and we're not getting any errors in production.
Where do we start? Well, let's see if the point of failure is Identity.from_omniauth
This method searches for an existing identity (we've written extra code for more providers, but not implemented client-side yet). If no identity is found it will create one, and then create the associated Person model. If this was the point of failure we'd be able to see some suspiciously empty fields in the production console. But no - the Person & Identity models have all been created with all of the correct fields, and the relevant bits of the app have seen them (e.g. their 'user profile pages' have all been created).
I just added in the if #person to the #oauthorize - we had one 500 where #identity.person was nil, but haven't been able to replicate.
Anyway, the real-world users in question do have complete models with associations intact. Moving down the method we then create a PersonMeta record to record simple stuff like landing page. I'd have done this as an after_create on the Person but I figured it wasn't right to be passing session data to a model.
This isn't being created for our problematic users. At this point, I'm kind of stumped. I'm not sure how the create ! (with bang) got in there, but shouldn't this be throwing an exception if somthing's broken? It isn't.
That is only called if it's a person's first visit anyway - subsequent logins should bypass it. One of the problematic users is a friend so I've been getting him to try out various other things, including signing in again, trying different browsers etc, and it keeps happening
so anyway, after spending 45 minutes writing this post…
One of the users revoked access to the app via Twitter and reauthenticated. Everything works now.
What the hell?
His old identity had his OAuth tokens etc stored properly.
Luckily this is resolved for one user but it's obviously an ongoing problem.
What do we do?
Is it possible that the identity.save line in Identity.from_omniauth is failing silently? If so, your after_create hook won't run, #identity.person will be nil, and you'll just (silently) redirect.
Try identity.save! ?
Related
I have a question regarding the deletion of a User. Note that I'm not using Devise since my app is an API.
What I need to do
So I have a User model, I can delete this User with no issues. The user belongs to many other associations regarding Bank Accounts, Transactions, you name it.
When I delete the User, it's able to be deleted but its associations are not. Note that I'm using soft_deletion which means it gets in an INACTIVE state. And by saying that the "associations aren't being deleted" means that I just need to DISABLE specific associations when the User has been deleted or gets INACTIVE
What I currently have
user.rb model file
class User < ApplicationRecord
has_many :bank_accounts
def soft_deletion!
update!(status: "DELETED",
deleted_at: Time.now)
end
end
delete_user.rb interactor file
module UserRequests
class DeleteUser < BaseInteractor
def call
require_context(:id)
remove_user
context.message = 'user record deleted'
end
def remove_user
user = context.user
context.user.soft_deletion! #<- This is the method I have on my model, which works!
#below I removed all user invites in case there's one
user_invite = UserInvite.joined.where(
"lower(email) = '#{user.email.downcase}'"
)&.first
user_invite&.update(status: "CANCELLED")
return if user.user.present?
user.update!(status: "INACTIVE")
end
end
end
So giving a bit more context of what happened up there. My App is an API, on my frontend I remove the user and it actually works, and so what I need to do next is delete the user AND delete a bank_account that's associated with my user.
What I've been trying to do (This fails so hard, and I need some help )
I honestly don't know how to interact between interactions on Rails, that's the reason of my question here.
delete_user.rb interactor file
module UserRequests
class DeleteUser < BaseInteractor
def call
require_context(:id)
remove_user
soft_delete_transaction_account #method to delete bank account
context.message = 'user record deleted'
end
#since there's an association I believe in adding a method to verify if there's a bank
account.
def soft_delete_bank_account
context.account = context.user.bank_accounts.find_by_id(context.id)
fail_with_error!('account', 'does not exist') unless
context.account.present?
context.account.update!(deleted: true,
deleted_at: Time.now)
end
def remove_user
user = context.user
context.user.soft_deletion! #<- This is the method I have on my model, which works!
context.user.soft_delete_transaction_account #<- Then I add that method here so the bank account can be deleted while the user gets deleted!
#below I removed all user invites in case there's one
user_invite = UserInvite.joined.where(
"lower(email) = '#{user.email.downcase}'"
)&.first
user_invite&.update(status: "CANCELLED")
return if user.user.present?
user.update!(status: "INACTIVE")
end
end
end
ERROR LOG of my code:
NoMethodError - undefined method `soft_delete_bank_account' for #<User:0x00007f8284d660b0>:
app/interactors/admin_requests/delete_user.rb:47:in `remove_user'
app/interactors/admin_requests/delete_user.rb:9:in `call'
app/controllers/admin_controller.rb:18:in `destroy'
I would appreciate your help on this!
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
I have two websites that each connect to their own unique databases. I need to validate in website 'A' that an email address exists in the website 'B' database. I'm doing the validation as follows:
Called from website 'A's AccountController < ApplicationController class:
config = YAML::load(File.open("#{RAILS_ROOT}/config/database.yml"))
ActiveRecord::Base.establish_connection(config["database B"])
if ActiveRecord::Base.connection.select_values("SELECT "database B".X
FROM 'database B".X WHERE 'database B'.X.email = #member_email")
This call works when I test it in my development and QA environments but fails in my production environment. What appears to happen in production is that the value of the ActiveRecord and also the select get's co-mingled with currently logged-in user's active records, but only in production.
Okay so I've modified my files to the following, based on the feedback. Still not working... Could someone please review the files below and see what step(s) I'm missing? Thanks!
Thanks! I think that is what I did, but I created the 'model', and, being a newbie, I'm not sure if that would normally be generated by Rails...
Still failing, would you mind taking a look at the following and see if you see what I'm doing wrong?
First, this is the 'legacy' database model for the second database that I want to connect to in the existing application (Note that doing the 'Fileload' was the only way I could get this to not error out.):
class MMSDB < ActiveRecord::Base
self.abstract_class = true #important!
config = YAML::load(File.open("#{RAILS_ROOT}/config/database.yml"))
establish_connection(config["deals_qa"])
end
Second, this is the model that calls the 'MMSDB' Model (see above)
class Redirect < MMSDB
def initialize
end
Checking to see if the email address exists in the legacy database, and, if it does, the #increment the redirect count on the # database table 'members'
Do I somehow need to tell the application what table I want to pull from since the table # in the legacy database (members) would be different then in the current application #database (users)
def email_exists?(email)
if find_by_email(email)
user = find_by_email(email)
user.redirect_count += 1
user.save
end
end
end
Then this is the code snippet inside the account controller file.
else
if user == Redirect::User.email_exists?(#email)
#Redirect_Flag = true
else
flash.now[:error] = 'Invalid email or password; please try again.'
end
end
Subclassing ActiveRecord::Base will allow you to make multiple connections to different databases.
module DatabaseB
class Base < ActiveRecord::Base
#abstract_class = true
establish_connection(config["database B"])
end
end
class YourAwesomeModel < DatabaseB::Base
set_table_name "X"
# Use your regular active record magic
end
You will still be able to use your other models with the connection established using ActiveRecord::Base to your primary database.
I'm trying to build a registration module where user can only register if their e-mail is already in an existing database.
Models:
User
OldUser
The condition on User will be
if OldUser.find_by_email(params[:UserName]) exists, allow user registration.
If not, then indicate error message.
This is really simple to do in PHP where I can just run a function to execute a mysql query. However, I couldn't figure out how to do it on Rails. It looks like I have to create a custom validator function but seems to be overkilled for a such simple condition.
It should be pretty simple to do. What have I missed?
Any pointer?
Edit 1:
This solution by dku.rajkumar works with a slight modification:
validate :check_email_existence
def check_email_existence
errors.add(:base, "Your email does not exist in our database") if OldUser.find_by_email(self.UserName).nil?
end
For cases like this, is it better to do validation in the model or at the controller?
you can do it as
if OldUser.find_by_email(params[:UserName])
User.create(params) // something like this i guess
else
flash[:error] = "Your email id does not exist in our database."
redirect_to appropriate_url
end
UPDATE: validation in model, so the validation will be done while calling User.create
class User < ActiveRecord::Base
validates :check_mail_id_presence
// other code
// other code
private
def check_mail_id_presence
errors.add("Your email id does not exist in our database.") if OldUser.find_by_email(self.UserName).nil?
end
end
I'd recommend starting with Devise.
See https://github.com/plataformatec/devise
Even if you have unusual needs like these, you can normally adapt it. Once you get to know it, it's extremely powerful, solid and debugged, and you can do all sorts of things with it.
Bellow is just an initial implementation .../app/controller/UsersController for User registration related actions.
def new
#user = User.new
end
def create
#user = User.new(params[:user])
#old_user = User.find_by_email(user.email)
if #old_user
if #user.save
# Handle successful save
else
render 'new' # and render some error message telling why registration was not succeed
end
else
# render some page with some sort of error message of 'new' new users
end
end
Update:
Check out the following resources for more info:
Ruby on Rails Tutorial
Rails: User/Password Authentication from Scratch, Part I/II
I'm shifting code from an application built in a non-standard custom PHP framework into Ruby on Rails (version 3). In the PHP version all the controllers are really fat, with thin models, which I've always disagreed with, so I'm enjoying the way Rails does validation at the model level, which is probably 90% of what's happening in these fat controllers currently.
One problem I'm facing, and unsure how to resolve however, is that of differing validation rules based on who's making the change to the model. For example, an administrator, or the original creator of the record should be able to do things like flag a record as deleted (soft delete) whereas everybody else should not.
class Something < ActiveRecord::Base
...
validates :deleted, :owned_by_active_user => true
...
end
class OwnedByActiveUserValidator < ActiveModel::EachValidator
validate_each(record, attr_name, attr_value)
# Bad idea to have the model know about things such as sessions?
unless active_user.admin? || active_user.own?(record)
record.errors.add :base, "You do not have permission to delete this record"
end
end
end
Since the model itself is (in theory) unaware of the user who is making the change, what's the "rails way" to do this sort of thing? Should I set the active user as a virtual attribute on the record (not actually saved to DB), or should I just perform these checks in the controller? I have to admit, it does feel strange to have the model checking permissions on the active user, and it adds complexity when it comes to testing the model.
One reason I'm keen to keep as much of this as possible in the model, is because I want to provide both an API (accessed over OAuth) and a web site, without duplicating too much code, such as these types of permissions checks.
It is really the controller's job to handle authorization, or to delegate authorization to an authorization layer. The models should not know about, nor have to care about, who is currently logged in and what his/her permissions are - that's the job of the controller, or whatever auth helper layer the controller delegates that to.
You should make :deleted in-attr_accessible to mass assignment via new, create, or update_attributes. The controller should check the authenticated user's authorizations separately and call deleted= separately, if the authenticated user is authorized.
There are several authorization libraries and frameworks to help with authorization or to function as an authorization layer, such as cancan.
I would solve this with a before_filter in my controller, instead of with validations in my model.
class SomethingController < ApplicationController
before_filter :require_delete_permission, :only => [:destroy]
def destroy
# delete the record
end
private
def require_delete_permission
unless current_user.is_admin || record.owner == current_user
flash[:error] = 'You do not have delete permissions'
redirect_to somewhere
end
end
end
I have come across the same issue in Rails 2.3 and finally come up with this solution. In your model you define some atribute, depending on which you switch on/off validation. Than you your control you set this attribute depending on the date available to controller (such as user privileges in your case) as follows:
Class Model < ActiveRecord::Base
attr_accessor :perform_validation_of_field1 #This is an attribute which controller will use to turn on/off some validation logic depending on the current user
validates_presence_of :field1, :if => :perform_validation_of_field1
#This validation (or any similar one) will occur only if controller sets model.perform_validation_of_field1 to true.
end
Class MyController < ActionController::Base
def update
#item = Model.find(params[:id])
#item.update_attribute(params[:item])
#The controller decides whether to turn on optional validations depending on current user privileges (without the knowledge of internal implementation of this validation logic)
#item.perform_validation_of_field1 = true unless active_user.admin?
if #item.save
flash[:success] = 'The record has been saved'
redirect_to ...
else
flash.now[:error] = 'The record has not passed validation checks'
render :action => :edit
end
end
I think that in Rails 3 it can be done in similar manner.