Custom Devise/Warden strategy that requires intermediate step with user interaction - ruby-on-rails

I have a Spree application and I’m trying to implement a custom authentication strategy with Devise/Warden. The idea is that users are mapped to one or more companies. The first step in authenticating a user is to hit an external api endpoint that returns a list of associated companies. If no companies are returned, the user is invalid and I call fail!. However, if one or more companies are returned, my goal is to require the user to select an active company to use before they are authenticated and can continue to the site. Here is what I've got so far:
def valid?
params[:email]
end
def authenticate!
email = params[:email]
companies = get_user_companies(email)
if (companies.length > 0)
user = User.find_by :email => email
# have user select the active company here
success!(user)
else
fail!("User has no companies")
end
end
def get_user_companies(email)
url = "http://localhost/user/#{email}/companies"
JSON.parse(RestClient.get url, :content_type => :json, :accept => :json)
end
A user is not valid without a company, and the active company cannot be changed during a session. Is this possible? How would I go about implementing this? Would this step make more sense outside of an auth strategy? If so, where? Thanks!

Related

Is there a way to restrict access to certain applications by specific users?

I have create several applications that communicate with our central auth server via doorkeeper. I want to make some applications accessible/inaccessible for specific users.
Is there a way to restrict access to specific oauth_applications and return a 401?
I believe the easiest way to achieve this would be the following:
In your doorkeeper application, change the Users table to include a permissions relationship. Something like, User -> has many -> permissions
And those permissions could contain just the name of the application you want to give them access to, (Or the ID of the application, you choose)
Then, in your config/initializer/doorkeeper.rb - inside Doorkeeper::JWT.configure - you add which applications that particular user can access inside the token payload, something like:
token_payload do |opts|
...
token[:permissions] = user.permissions.pluck(:application_name)
end
If you are using Doorkeeper without JWT, you can still pass extra information to the token by prepending a custom response to the ResponseToken object like so:
Doorkeeper::OAuth::TokenResponse.send :prepend, CustomTokenResponse
and CustomTokenResponse just need to implement the methods body, like so:
module CustomTokenResponse
def body
additional_data = {
'username' => env[:clearance].current_user.username,
'userid' => #token.resource_owner_id # you have an access to the #token object
# any other data
}
# call original `#body` method and merge its result with the additional data hash
super.merge(additional_data)
end
end
extra information can be found in Doorkeepers' wiki: https://github.com/doorkeeper-gem/doorkeeper/wiki/Customizing-Token-Response
and in the Doorkeeper JWT gem: https://github.com/doorkeeper-gem/doorkeeper-jwt#usage
On 9 Feb 2020 a new configuration option was introduced in Doorkeeper to exactly do this.
Therefore, you can configure config/initializer/doorkeeper.rb:
authorize_resource_owner_for_client do |client, resource_owner|
resource_owner.admin? || client.owners_whitelist.include?(resource_owner)
end
I wanted the same behaviour. I use the resource_owner_authenticator block in config/initializer/doorkeeper.rb. When a user has one or more groups which are connected with an Oauth application it can continue.
rails g model UserGroup user:references group:references
rails g model GroupApplications group:references oauth_application:references
resource_owner_authenticator do
app = OauthApplication.find_by(uid: request.query_parameters['client_id'])
user_id = session["warden.user.user.key"][0][0] rescue nil
user = User.find_by_id(user_id)
if !app && user
user
elsif app && user
if !(user.groups & app.groups).empty?
user
else
redirect_to main_app.root_url, notice: "You are not authorized to access this application."
end
else
begin
session['user_return_to'] = request.url
redirect_to(new_user_session_url)
end
end
end

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

How to create authentication (email-token/digest) link when two models are involved?

I've been following railstutorial.org. In this tutorial we a.o. learn how to create a token/digest together with a mailer for account activation. It saves a digest to the db and sends an email with a link that contains a token + email address. An authentication controller method then checks the email-token combination against the digest in the db.
Now I have a similar situation but with a complicating factor: There are users and organizations. My use case is that an organization can invite a user to become a member of that organization. The user will need to confirm this using an authentication link, before the user actually becomes a member of that organization.
Below is my current setup, which consists of a digest being saved to the User table and a link with the user's email address and token. The problem is that if the user clicks the authentication link, the authentication method still does not know to which organization it should add the user. It only has an email address and token.
How can I achieve that it is known which organization sent the invitation? Also, we of course don't want the user to be able to manipulate the link in such a way that the user can add himself to a different organization than the one the user was invited for.
Controller method:
def request
#organization = current_organization
#user = User.find(email: params[:user][:email])
#user.send_invitation(#organization)
end
Model method:
def send_invitation(organization)
create_invite_digest # Uses other model method to create digest and token.
update_columns(invite_digest: self.invite_digest)
UserMailer.add_user(self, organization).deliver_now
end
Mailer method:
def add_user(user, organization)
#user = user
#organization = organization
mail to: user.email, subject: "Please confirm"
end
Mailer View:
<%= adduser_url(#user.invite_token, email: #user.email) %>
Controller method to authenticate:
def adduser
user = User.find_by(email: params[:email])
if user && user.authenticated?(:invite, params[:id])
user.add_role(organization) # Should add user to organization, but then it needs to know which organization
flash[:success] = "User added"
redirect_to root_path
end
end
Model method to add user: requires information about the organization to add the user to.
def add_role(organization)
end
For a project I had that was similar, I had an invitation model and participant model (a membership is also a good name for this).
The invitation had a user_email, organization_id, and token. It had a related mailer that was created when the invitation was created.
The user then would receive the invitation by email and click to see it on the site, using the token for the invitation as the find parameter. The invitation had the opportunity to "accept", which was really the ability to create a participant record (user_id, organization_id). The user would have to be logged in for this, which may be a new registration or sign in. The participant would be created only if the invitation token was valid (exists and not yet used). I have a date on the invitation "accepted_at" which is used to know its state and if it can be used to create a participant or not. Users are not required to use a particular email address for registration, they just have to have access to the invitation to be able to accept it. Users can be a participant only once in an organization. The role on the participant is determined by the controller.
This all works really well for me. I am also planning to add a participant_request model that will allow the logged in user to request to be a participant of an organization. The organization would then have the option to view requests and "accept" or "deny" them - create the participant or decline the request. Datetimes will be on the request to know its state.
If you want more model details, I can give those. This may be enough to work from.
Probably the easiest thing to do would be to create an Invitation model of some sort that contains the token, user_id and organization_id. This will help you keep track of the references, and allow for flexibility in the future.
You could also add extra meta that way too (e.g. accepted_at), and possibly add the ability for the invite to only be valid for a certain amount of time (by using the invitation created_at, for example).

what is the "rails way" for enabling an admin to create privileges for an existing user?

I'm writing a ruby on rails website for the first time. I have a User model and a Manager model. The user has_one Manager and a Manager belongs_to a User. The Manager model contains more info and flags regarding privileges. I want to allow an admin while viewing a User (show) to be able to make him a manager.
This is what I wrote (probably wrong):
In the view: <%= link_to 'Make Manager', new_manager_path(:id => #user.id) %>
In the controller:
def new
#user = User.find(params[:id])
#manager = #user.build_manager
end
resulting in a managers/new?id=X Url.
I would separate roles and permissions from the User class. Here's why:
Managers are users too. They share the same characteristics of Users: Email address, first name, last name, password, etc...
What if a manager also has a higher level manager? You'll have create a ManagerManager class, and that's terrible. You might end up with a ManagerManagerManager.
You could use inheritance, but that would still be wrong. Managers are users except for their title and permissions, so extract these domains into their own classes. Then use an authorisation library to isolate permissions.
You can use Pundit or CanCan. I prefer Pundit because it's better maintained, and separates permissions into their own classes.
Once you have done that, allowing a manager to change a normal user to a manager becomes trivial and easy to test:
class UserPolicy
attr_reader :user, :other_user
def initialize(user, other_user)
#user = user
#other_user = other_user
end
def make_manager?
user.manager?
end
end
In your user class you can have something like:
def manager?
title == 'manager?'
# or
# roles.include?('manager')
# Or whatever way you choose to implement this
end
Now you can always rely on this policy, wherever you are in the application, to make a decision whether the current user can change another user's role. So, in your view, you can do something like this:
- if policy(#user).make_manager?
= link_to "Make Manager", make_manager_path(#user)
Then, in the controller you would fetch the current user, and the user being acted upon, use the same policy to otherwise the action, and run the necessary updates. Something like:
def make_manager
user = User.find(params[:id])
authorize #user, :make_manager?
user.update(role: 'manager')
# or better, extract the method to the user class
# user.make_manager!
end
So you can now see the advantage of taking this approach.

How to create a view which is only accessible through a randomly generated URL?

I want to set up a registration process in which the user initially requests more information from the site and then subsequently receives an e-mail containing a link to the actual registration page. The link should be a randomly generated URL, and access to the registration page should be otherwise restricted. In other words, the registration page shouldn't be accessible by manually typing a URL into the browser.
I would appreciate any advice on how best to implement these features.
I'm new to Rails, so I apologize in advance if this question is basic or if it has already been covered.
You need to store the random token in a database. Since you are sending it to an email address, you probably want to store the email as well so that you can add it to your User model when they register. Below are some of the relevant parts you can do (although you will need to fill in the gaps).
Essentially, you need to generate the registration token, with RegistrationToken.new(:email => "their email address"), in a controller.
This model implements the generation of the random token:
class RegistrationToken < ActiveRecord::Base
# the secret is just to make the random number generator more secure
##secret = 6345
def initialize
# info at http://www.ruby-doc.org/core-1.9.3/Random.html
seed = Random.new().integer(1000000000) + ##secret
token = Random.new(seed).integer(1000000000);
# you might want to generate random numbers larger than 1 billion
# for more security (maybe with 64 bit integers?)
end
end
and a migration for your database:
class CreateRegistrationTokens
def change
create_table :products do |t|
t.string :email
t.integer :token
t.timestamps
end
end
end
Then you just need to setup your controller and view.
For your registration controller, you just need something like:
class RegistrationsController < ActiveRecord::Base
def new
#registration_token = RegistrationToken.find(params[:token]).first
if #registration_token.nil?
raise 'Invalid token' # or whatever you want, eg. redirect to a page
end
# otherwise render the registration form (can do implicit render)
end
def create
#registration_token = RegistrationToken.find(params[:token]).first
if #registration_token.nil?
raise 'Invalid token' # or whatever you want, eg. redirect to a page
end
# otherwise create the user, eg.
User.create(params[:user].merge(:email => #registration_token.email))
#registration_token.destroy
end
end
The controller actions above essentially makes sure that they can only register if a matching token is found in the database.

Resources