I am using authlogic gem for the authentication and recently we implemented feature for logout_on_timeout.
So we have placed the below code:
class UserSession < Authlogic::Session::Base
before_persisting :reset_stale_state, :unless => :single_access?
after_persisting :enforce_timeout, :unless => :single_access?
logout_on_timeout true
end
The single access token works but then logout on timeout stops working. We have a discussion thread already on this:
https://github.com/binarylogic/authlogic/issues/64
But unfortunately, none of the solutions work.
Kindly advise on how to get timeout working while keeping single access token.
Related
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 just switched from Sorcery to Devise. Sorcery is semi-inactive and although authentication is working in rails 5---test helpers are not. So, I decided to make the switch the Devise. I've been getting the dreaded 401 error:
Completed 401 Unauthorized
I have seen many questions regarding this...but, none of those answers helped me.
My question is a bit misleading because you will immediately assume I have some remnants of the Sorcery install. I might, but Ive thoroughly checked it all.
1 - If I use the forgot password functionality within Devise I will be auto logged in after changing my password---However, I can not log in using those sme credentials...get the 401 error.
2 - If I completely clear existing passwords (because I'm pretty sure sorcery encryption is diff than devise encryption) and update password using devise OR
3 - I have traced the path taken for authentication inside Devise and my problem surfaces when the following method is hit:
def valid_password?(password)
Devise::Encryptor.compare(self.class, encrypted_password, password)
end
It seems that encrypted_password != password even though the password was successfully updated using devise.
As part of following the auth path inside Devise I should also point out that I am using the Devise method user.skip_confirmation! which manually confirms the user---so, user confirmation should not be part of this issue.
I am using custom Devise views (although have not altered them yet). I have also generated a custom sessions controller because I think/thought this issue might be related to Pundit policy. However, I am skipping policy verification in the application controller using the following...so, Pundit should not be a problem:
after_action :verify_authorized, except: :index, unless: :devise_controller?
after_action :verify_policy_scoped, only: :index, unless: :devise_controller?
cookies_serializer has been brought up a number of times in other answers...I have tried :json, :marshal, and :hybrid with no affect.
So, authentication is failing because Devise::Encryptor.compare(self.class, encrypted_password, password) returns false. But, if Devise saved the new password---how can compare not return true. And, yes---I have entering the credentials correctly.
Thanks for the help!
Ok. So, attr_accessor :password, :password_confirmation is no longer needed in Devise---and, was responsible my problem. By setting attr_accessor :password my app was creating a password=() method, which was overriding the same method in devise. The result was no matter what my password ended up being ''. I originally found this by creating my own password setter:
def password=(new_password)
#password = new_password
self.encrypted_password = password_digest(#password) if #password.present?
end
Which is the same exact method in Devise internals. With this method my password saved and I was able to log in. I thought this was strange...so, I removed my custom method, and the attr_accessor and authentication continued to work. So, basically the attr_accessor overrode the devise method, then I created a setter method that overrode the attr_accessor method.
It was a lot of work and in the end was something so simple. I hope this helps someone else. I have some additional testing to do and if I find anything else I will amend this.
I am using Devise to authenticate users to my Ruby on Rails application. Up to this point, I have been using the standard Cookie-based session to authenticate users, but now I have requirements to allow a token-based authentication, and I implemented this through a custom Warden strategy.
For the sake of this example, my custom strategy code is:
module Devise
module Strategies
class CustomAuthenticatable < Base
def valid?
params.has_key? :email
end
def authenticate!
success!(User.find_by(email: params[:email]))
#fail
end
end
end
end
So this works as expected for the first request: when I GET /api/my_controller/url?email=user#example.com the user is authenticated, and I get the expected response.
But wait: when I then make a second request: GET /api/my_controller/url, the user is still authenticated.
Upon further inspection, I see that a Set-Cookie is being sent, with a Devise session.
So here's my question:
How do I disable the Set-Cookie when using a custom strategy?
You can prevent the creation of a session, like described in here.
Prevent session creation on rails 3.2.2 for RESTful api
resource = warden.authenticate!(:scope => resource_name, :store => !(request.format.xml? || request.format.json?))
For some other options, please consider Rails 3 disabling session cookies.
I am using the doorkeeper gem to use tokens to authenticate requests.
When I have doorkeeper_for configured as defined below, tokens are required, which is good and expected:
doorkeeper_for :all
When I set a conditional for doorkeeper_for to be the condition I actually want (allowing requests to local deployment endpoints without tokens)
doorkeeper_for :all, :unless => lambda { request.url =~ /http:\/\/(localhost|127.0.0.1|0.0.0.0):8080/
Or if I set this condition to any number of junk conditions:
doorkeeper_for :all, :if => lambda { true }
doorkeeper_for :all, :unless => lambda { true }
Then doorkeeper allows all requests to go through without tokens, which is bad and not expected.
Noting that I have a require 'doorkeeper' statement and that I've fiddled around with various include statements in my API controller such as include Doorkeeper::Helpers::Filter
This may have to do with a problem with Doorkeeper working with Grape, though again it is strange that it works just fine without the conditional. Any insight into why this is happening would be appreciated!
After speaking with the main contributor to doorkeeper-gem we've determined that it has to do with the use of Grape. This feature should work just fine with Rails based controllers:
https://github.com/doorkeeper-gem/doorkeeper/issues/426
Grape itself is supposed to be a closer-to-the-metal API framework and isn't necessarily supposed to be used with Rails, which contributed to this issue.
As a final note, there is a gem specifically for integrating Grape and Doorkeeper gems:
https://github.com/fuCtor/grape-doorkeeper
with omniauth in my app, to have a user use Google oAuth2 to authenticate I redirect the user to:
/users/auth/google_oauth2
If the users approves the request, then the AuthenticationsController#create is called.
With AuthenticationsController#create - I can add event tracking to record the # of users who approve google auth. What I don't have is the number that I sent to approve meaning I don't have a conversion rate.
How can I track the # of people who hit the URL around making requests to connect.
A nasty solution would be to build a filter around the method Strategy#request_call and do the tracking there.
Inside an initializer:
OmniAuth::Strategy.class_eval do
def request_call_with_tracking
log :info, "Im running before the actual request_call"
Tracker.hit(name) #name will return the provider
request_call_without_tracking
end
alias_method_chain :request_call, :tracking
end
You can achieve this by using the OmniAuth setup phase. You can pass a :setup option to an OmniAuth provider, with a proc which will be executed before the authentication is performed. You can add event tracking inside this proc.
So if you have some tracker class, you can do this:
use OmniAuth::Builder do
provider :google_oauth2, ENV['GOOGLE_KEY'], ENV['GOOGLE_SECRET'],
:setup => lambda { |env|
Tracker.track
}
end
For more information check out Avdi Grimm's great blog post about the subject.