Using Devise to auto login a user on a multi tenancy site with subdomains - ruby-on-rails

In my app a customer registers on mysite.com. Once registration is complete, they are given a site such as customer.mysite.com. I'm using Devise and would like to log the customer into their site immediately. I'm using multitenancy as explained in the RailsCast here. I'm not quite sure how to go about this. The standard solution of adding sign_in to an after_sign_up_path_for def isn't working. I'm assuming it's trying to log the customer into mysite.com, not customer.mysite.com. I'm including my after_sign_up_path_for def so you can see what I'm trying with no success. The resource in my Devise implementation is User and a user has a Site.
RegistrationsController:
def after_sign_up_path_for(resource)
# Site.current_id = resource.site.id
sign_in resource, bypass: true
"http://#{resource.site.host}.#{ENV['BASE_HOST']}/sites/#{resource.site.id}/edit"
# edit_site_url
end
Any help is appreciated.

I had the same issue and solved it the following way:
Create a new model (I called it LoginKey) that contains the user_id and a random SHA1 key.
When the user is authenticated at the parent domain (for example: mydomain.com/users/sign_in), a new LoginKey is created and the user is redirected to the corresponding subdomain to an action that I called login_with_key (for example: user_subdomain.mydomain.com/users/login_with_key?key=f6bb001ca50709efb22ba9b897d928086cb5d755322a3278f69be4d4daf54bbb)
Automatically log the user in with the key provided:
key = LoginKey.find_by_login_key(params[:key])
sign_in(key.user) unless key.nil?
Destroy the key:
key.destroy
I didn't like this solution 100%, I tried out a lot of different approaches that do not require a db record to be created, but always faced security concerns, and I think this one is safe.

Related

Is it possible to do Devise sign in, out, etc without any redirection whatsover?

I'm using Devise, but not using the Devise controllers directly because I'm performing all of the actions through a custom built GraphQL API. One issue I have, for example, is that after enabling confirmable, if a user tries to sign in and I call Devise::Controllers::Helpers#sign_in the user gets redirected to /api/v1/users/sign_in, which doesn't exist and it would be wrong even if it exist. Instead, I need the failure to sign in to be returned back to my code (return value, exception, whatever), so that my API can encode that response to the frontend.
How can I do that?
For example, this is my log in function:
def resolve(email:, password:)
user = User.find_for_authentication(email: email)
if user&.valid_password?(password)
context[:sign_in].call(user)
{ current_user: user }
else
{ errors: [{ message: 'Email or password incorrect.' }] }
end
end
context[:sign_in] is set up in the GraphqlController by including Devise::Controllers::Helpers and then simply:
context = {
current_user: current_user,
sign_in: method(:sign_in),
sign_out: method(:sign_out)
}
Note: I am not using GraphqlDevise because I don't want to use Devise Token Auth, I'm using cookies.
I believe passing devise's sign_in/sign_out methods via context is probably a deadend.
The suggestion in the comment to your question from #Int'l Man Of Coding Mystery is good ie you could use: https://github.com/graphql-devise/graphql_devise.
If you're not keen in introducing another dependency and figuring out how to wire everything you can perhaps go with overriding devise's SessionController.
See for some examples here: Rails - How to override devise SessionsController to perform specific tasks when user signs in?
(but also don't hesitate to look at the source code for the matching Devise release: https://github.com/heartcombo/devise/blob/master/app/controllers/devise/sessions_controller.rb)
Depending on your use case you might be even able to do what you need by using some of the config options - e.g. you can perhaps try to override after_sign_in_path etc.

Rails 4 multitencancy with subdomain login management

Scenario: Multi-tenant rails app that uses subdomains and devise
Problem: I want the user to be able to log into mydomain.com then be forwarded to their own subdomain1.mydomain.com address as a logged-in user. Right now they can only log directly into their own subdomain.
I'm a relative Rails newbie and I can't find a simple solution (although it seems like there must be one). Ideally I would like to have mydomain.com and subdomain1.mydomain.com share one cookie, but my skills aren't there for writing custom middleware. Obviously since it's multitenant I can't share one session across all subdomains. Stuck on this for a few days and curious if there is a simple solution (such as a config.session_store domain setting) that I'm missing before I start looking at OAuth or other more cumbersome solutions. Any help will be appreciated!
Edit: Of course I only found this after posting. Log a user into their subdomain after registration with Rails and Devise . Will try the config.session_store domain: :all with a before filter recommendation and post any details if it doesn't work, seems like a good idea at least.
Edit: SOLUTION that worked for my particular Devise with subdomains setup:
class ApplicationController < ActionController::Base
before_action :check_subdomain
def check_subdomain
unless request.subdomain == "" or request.subdomain == session[:subdomain]
redirect_to request.protocol+request.domain
end
end
end
session_store.rb
My::Application.config.session_store :cookie_store, key: '_my_session' , :domain => :all, :tld_length => 2
Basically I set the subdomain in the session with session[:subdomain] at login and use that to scope the session to the current user. Otherwise when the domain is set to :all in session_store it breaks the scope. If the user is not authorized it redirects them to the public home page via the request.protocol (http:// or https://) +request.domain redirect. Simple! Now users can move between the base domain and their subdomain within the same session.
Cookie
From what you've posted, I'd estimate you have a problem with the tracking of your session cookie. We had a similar problem with our subdomain-powered application, which lead to the cookie being dropped each time you switched between the two
We found the remedy here:
Share session (cookies) between subdomains in Rails?
#config/initializers/session_store.rb
Your_App::Application.config.session_store :cookie_store, key: '_your_app_session', domain: :all, tld_length: 2
The trick is the tld_length argument - this allows you to define how many "levels" of the domain can be accommodated; IE if you're using a sub domain, you'll need to set the tld_length to reflect it
Forwarding
I'm not sure whether you have a problem with your forwarding or not; I'll give you some ideas anyway.
When you log into a "subdomain", unless you've got a true multi-tenancy implementation of Rails (where each user is stored in a different database), you should be able to allow the users to login on the main form, and then redirect them to the subdomain without an issue
Something you need to consider is the subdomain constraint will only be populated if you use _url path helpers:
<%= link_to "Your Name", path_url(subdomain: "subdomain_1") %>
The reason for this is the _path helper is relative to the base URL, and consequently cannot populate the subdomain option. Alternatively, the _url path helper points to the URL in its entirety -- allowing you to define the sub domain as required
--
If you send the request & continue to want the user to remain signed-in, you'll need to ensure you're able to persist the authentication across the sub-domains. IE if you have a single-sign in form on the "main" page, you'll want to ensure you can continue the authentication into the subdomains

Setting Pundit role for user from Devise Registrations New View / Controller

I have both Pundit and Devise setup and working correctly in my rails app. However I am unsure about how to let the user decide their role when signing up.
At the moment:
I have a URL param which is passed to the Devise new view.
In the form_for I set a hidden field called role to the value of the param.
This works.
But I am concerned that a malicious user could change this param to say "Admin" and now they are an admin.
How should I handle this? I don't want to put a restriction in the model as that will cause issues when I want to create an admin. Should I override the devise registrations controller to put a check in there?
You don't need to override Devise's RegistrationsController for what you're trying to do.
If you want admins to be able to create users that have an arbitrary role set, you could simply use your own controller. Devise still makes it easy to create a user yourself, so you'll just have to make a controller handling this. Of course, don't forget to protect it using Pundit so only admins can use this functionality.
This approach still works if you use the Confirmable module. As no confirmation e-mail will be sent on user creation, though, you'll either have to call user.confirm! after saving the model to immediately unlock the account, or manually send the confirmation e-mail using user.send_confirmation_instructions.
Edit:
This Pundit policy may or may not work for what you're trying to do. You will have to override the create action of Devise's RegistrationsController here in order to use Pundit's authorize method. For dryness' sake, you should also move the roles list elsewhere, perhaps into the model.
class UserPolicy < Struct.new(:current_user, :target_user)
def create?
registration_roles.include?(target_user.role) if current_user.nil?
end
private
def registration_roles
%w(RED BLU Spectator)
end
end
After a fair amount of googling I have an answer. First stick some validation in your model for the roles Active Record Validations Guide: See 2.6 inclusion: validator option
After this your roles are validated to ensure they are correct, you could of course have a lookup table as well. Then you have two options:
Use a conditional before_save Callback for new records. Then check if the new record has the role your protecting and if so raise an error. To catch later (in an overridden devise controller (see second option).
Override the Devise registrations controller See this SO question. And put some checks in a completely overridden create action. Use the session to store the url param passed to the new action (also needs to be completely overridden). Then if the create action fails and redirects to new you still have access to the role in the session (as the param will be cleared from the URL unless you manipulate it).
So either way you need to override the registrations controller, its just a case of how much.
I suspect there is a way to do this with just Pundit. But I have yet to be able to get it to work.

Have different view based on type of user

I have a user model that has a few different types, i.e. there is a user attribute that is set as either a usertype1, usertype2, or usertype3. How can I route the user to a different page based on the user type? I am using devise for authentication.
If you want to redirect the user after logging in then check this devise wiki. The specific code is
# app/controllers/application_controller.rb
def after_sign_in_path_for(resource)
if usertype1?
some_url
elsif usertype2?
another_url
elsif usertype3?
some_other_url
end
end
But if what you're looking for is more generic then that you probably want to define roles and give users access to certain parts of the site based on their roles - also known as authorization. For that I'd recommend using cancan. You can also watch the railscast episode if you want more info.
Edit:
See also this so question

rails devise hook into on_login

I'm looking to hook into devise after login / after session create. How would I go about doing this?
Basically I want to set a users location every time they login, and to do that I need an after login hook of sorts.
Devise is built on Warden, so you can use Warden's after_authentication hook.
Put this in an initializer:
Warden::Manager.after_authentication do |user,auth,opts|
# do something with user
end
The remote IP address and other request info is stored in auth.request (i.e. auth.request.remote_ip).
See https://github.com/hassox/warden/wiki/callbacks
Devise updates the value of the user.current_sign_in_at timestamp on successful login. So, you could simply add a before_save filter to your User model. In that filter, check to see if the value of this field has changed, and if it has, set the users location.
BTW - I'm not sure what you mean by "location" - if you mean IP address, Devise already stores that for you.
Here's a page from the devise wiki: How To: Redirect to a specific page on successful sign in.
In summary, the recommendation is to add the following method to the application controller:
app/controllers/application_controller.rb
def after_sign_in_path_for(resource)
custom_location_for(resource) || welcome_path
end
In the above code, resource means the object (user, account, etc) that you've implemented devise authentication for. (The object that has the devise_for in your routes.)

Resources