Devise OmniAuth with a Multi-tenant Rails 5 App - ruby-on-rails

Here is the situation. I have a multi-tenant rails app using the apartment gem where I need to implement a LinkedIn OmniAuth Strategy.
As you can see by my routes, Devise users, and the associated routes, are only persisted on the individual schemas of the subdomains.
Example Route:
Good: https://frank.example.io/users/sign_in
Bad: https://example.io/users/sign_in
Routes
class SubdomainPresent
def self.matches?(request)
request.subdomain.present?
end
end
class SubdomainBlank
def self.matches?(request)
request.subdomain.blank?
end
end
Rails.application.routes.draw do
constraints(SubdomainPresent) do
...
devise_for :users, controllers: {
omniauth_callbacks: 'omniauth_callbacks'
}
devise_scope :user do
get '/users/:id', to: 'users/registrations#show', as: "show_user"
end
...
end
end
My specific problem is that LinkedIn does not support wildcards with their callback URLs so I am lost on how I might be able to direct users to the right domain after OAuth authentication.

So it turns out the answer was to pass parameters in the authorization link which would eventually get passed to the callback action throught request.env["omniauth.params"]
Authorization Link format:
Here I was having trouble adding the parameters to the Devise URL builder so I just manually added the parameters. This can probably be moved to a url helper
<%= link_to "Connect your Linkedin", "#{omniauth_authorize_path(:user, :linkedin)}?subdomain=#{request.subdomain}" %>
Routes:
Then I defined a route constrained by a blank subdomain pointing to the callback action.
class SubdomainPresent
def self.matches?(request)
request.subdomain.present?
end
end
class SubdomainBlank
def self.matches?(request)
request.subdomain.blank?
end
end
Rails.application.routes.draw do
constraints(SubdomainPresent) do
...
devise_for :users, controllers: {
omniauth_callbacks: 'omniauth_callbacks'
}
resources :users
...
end
constraints(SubdomainBlank) do
root 'welcome#index'
...
devise_scope :user do
get 'linkedin/auth/callback', to: 'omniauth_callbacks#linkedin'
end
...
end
end
Controller:
I used this tutorial to set up my callback controllers: Rails 4 OmniAuth using Devise with Twitter, Facebook and Linkedin. My main objective with the callback controller was to have it reside in in the blank subdomain so I only had to give one call back URL to my LinkedIn Dev App. With this controller I search the omniauth params for the subdomain parameter and use that to switch to the proper schema.
def self.provides_callback_for(provider)
class_eval %Q{
def #{provider}
raise ArgumentError, "you need a subdomain parameter with this route" if request.env["omniauth.params"].empty?
subdomain = request.env["omniauth.params"]["subdomain"]
Apartment::Tenant.switch!(subdomain)
...
end
}
end

could you register each domain as a callback with linked (I guess if you have a lot that becomes unmanageable quickly).. You could cookie the user before sending them to linkedin so when they return you know which subdomain they belong to.

Related

Omniauth && devise no route

I trying to use omniauth to do login with social networks.
I using this wiki but i have no success
There is my code:
Routes.rb
get '/auth/:provider/callback', to: 'auth#create'
my auth controller:
class AuthController < ApplicationController
def create
p request.env['omniauth.auth']
end
def vkontakte
end
def facebook
end
end
Why i have route error?
No route matches [GET] "/auth/vkontakte"
Thnks
First, I think you have to do some config in your config/initializers/devise.rb.
config.omniauth :vkontakte, APP_ID, APP_SECRET
The you need to add something like
devise_for :users, controllers: { omniauth_callbacks: "omniauth_callbacks" }
with that you can implement the method in the
class OmniauthCallbacksController < Devise::OmniauthCallbacksController
def vkontakte
# you can do anything you want to do here. Note that you have access to the `request.env["omniauth.auth"]` which holds all the user information you'd require.
end
end
One more thing: if you check the devise documentation, you'd find different configuration options, but always be careful not to overwrite/create AuthController because by default that's what Omniauth uses, I think and I believe in order to work with devise, you should be inheriting some stuff from Devise not ApplicationController.

Ruby on Rails: redirect unauthenticated user to root instead of the sign in page

I am using Rails 4 and Devise 3.
I am using the following in the routes file to prevent access to a page from non authenticated users (not signed in):
authenticate :user do
#page to protect
end
This redirects me to the user/sign_in page, but I want the user to be redirected to the root. So, I added the following as well to the routes page:
get 'user/sign_in' => redirect('/')
But this will mess up what I did in the sessions_controllers:
def new
return render :json => {:success => false, :type => "signinn", :errors => ["You have to confirm your email address before continuing."]}
end
This will stop showing. So, I would like another solution that redirects users to the root directly, instead of having to use authenticate :user and then get 'user/sign_in' => redirect('/').
The following may not have anything to do with redirecting the user to the root, but I would like to explain more about why I am overwriting the new method in the sessions_controller. I moved the sign_in and sign_up views to the root page (home page). In this case, I also needed to hack the error messages so that they appear in the home page, instead of redirecting the user to user/sign_in to show the errors. I used ajax for that.
Update
What I am looking for is something like this:
if user_authenticated?
#show the protected page
else
# redirect the user to the ROOT
end
You can set your devise scope to look something like this.
devise_scope :user do
authenticated :user do
root :to => 'pages#dashboard', as: :authenticated_root
end
unauthenticated :user do
root :to => 'session#new', as: :unauthenticated_root
end
end
EDIT (Based on the new info):
If you have something like this in your routes.rb
root to: 'visitors#index`
then you can have something like this in your visitors_controller.rb
class VisitorsController < ApplicationController
def index
if current_user
redirect_to some_authenticated_path
else
# business logic here
end
end
end
You will still want to handle the propper authorization requirements and authentication requirements in the some_authenticated_path controller and action.
I'm sorry I didn't understand your question, anyway you can do something like this:
class CustomFailure < Devise::FailureApp
def route(scope)
#return super unless [:worker, :employer, :user].include?(scope) #make it specific to a scope
new_user_session_url(:subdomain => 'secure')
end
# You need to override respond to eliminate recall
def respond
if http_auth?
http_auth
else
redirect
end
end
end
And in config/initializers/devise.rb:
config.warden do |manager|
manager.failure_app = CustomFailure
end
This was taken from the wiki of devise:
https://github.com/plataformatec/devise/wiki/How-To%3a-Redirect-to-a-specific-page-when-the-user-can-not-be-authenticated

Redirect to log in page if user is not authenticated with Devise

I'm using Devise with Ruby on Rails.
What is the recommended way to redirect unauthenticated users to the sessions#new page if they attempt to access a page that requires authentication?
Right now I get an error that says no route matches the one they attempt to access (leading to a 404 error in production).
Just simple add this method to application_controller.rb
protected
def authenticate_user!
if user_signed_in?
super
else
redirect_to login_path, :notice => 'if you want to add a notice'
## if you want render 404 page
## render :file => File.join(Rails.root, 'public/404'), :formats => [:html], :status => 404, :layout => false
end
end
And you can call this method on before_filter another controllers you want.
e.g :
class HomesController < ApplicationController
before_filter :authenticate_user!
## if you want spesific action for require authentication
## before_filter :authenticate_user!, :only => [:action1, :action2]
end
Don't forget add login_path into routes.rb
devise_scope :user do
match '/sign-in' => "devise/sessions#new", :as => :login
end
note : I always use this way when play with devise for my apps authentication.. (rails 3.2 and rails 4.0.1)
You can do just like GeekTol wrote, or just put
before_action :authenticate_user!
in your controller.
In this case, devise uses the default authenticate_user! method, that will redirect to the "user_session_path" and use the default flash message.
It's not necessary to rewrite authenticate_user! method, unless you want to customize it.
I thought you could just add:
before_action :authenticate_user!
to each controller that required the user to be logged in.
I'm a Rails beginner but I found this in my own searches and it works well in my application.
You should refer to Devise's own How To: How To: Redirect to a specific page when the user can not be authenticated.
Another alternative I can think of is creating a routing Constraint wrapping your protected routes. You'd better stick to Devise's way, but here is an example:
#On your routes.rb
constraints(Constraints::LoginRequired) do
get '/example'
end
#Somewhere like lib/constraints/login_required.rb
module Constraints
class LoginRequired
def self.matches?(request)
#some devise code that checks if the user is logged in
end
end
end
Add this code in your config/routes.rb devise_for :users and resources :users and you can generate devise in views.

Rails.application.routes.recognize_path with devise authenticated route

I have a devise enabled route as:
#config/routes.rb
authenticated :user {
root :to => 'home#signed_in'
}
root :to => 'home#index
and controller:
#app/controllers/home_controller.rb
class HomeController < ApplicationController
def signed_in
Rails.application.routes.recognize_path '/'
end
end
which raises:
NoMethodError: undefined method `authenticate?' for nil:NilClass
...lib/devise/rails/routes.rb:286:in `block in authenticated'
I need such thing to render different templates in destroy action based on request.referer controller name. How can get 'authenticated' controller/action name for such URL?
It seems using Rails.application.routes.recognize_path does not work well with custom routes constraints like devise #authenticated method.
See https://github.com/plataformatec/devise/issues/3747
recognize_path won't work as is with constraints that require request related objects (like the warden instance that Devise expects to be in the request env Hash). If you are doing this inside a controller code you might be able to pass the warden object from request.env to the recognize_path call, or try to pass down the object down the the class that is recognizing the path.
In the other hand, as recognize_path isn't a documented public API, I strongly suggest you to not use it and save pieces of the params Hash instead of the raw URL on your application.
One solution to your problem could be to save the controller/action names in session and access them when you need.
authenticated :user do
root :to => 'home#signed_in'
end
unauthenticated :user do
root :to => 'home#index'
end
You can use this: https://gist.github.com/NullVoxPopuli/8c8af217b7404336c72a
class RouteRecognizer
include Singleton
ROUTE_LIST = Rails.application.routes.routes.collect{|r| r.path.spec.to_s}
REGEX_ROUT_LIST = ROUTE_LIST.map{|r|
Regexp.new(r.gsub(/\:(.*)id/, "(\d+)").gsub("(.:format)", ""))
}
def self.is_route?(path)
REGEX_ROUT_LIST.each do |regex|
return true if !!(path =~ regex)
end
false
end
end
and then call with
RouteRecognizer.is_route?('http://whatever')

How i can redirect to different signout paths using Rails and Devise

I am using rails3 and gem devise and i have two roles admin and customer and i want after user
sign out admin should redirect to different path and customer should redirect to different path
when sign out..
You can get your desired functionality by using devise method after sign_out path.
but before you define these methods in application helper.
def is_admin?(user)
admin_role = Role.find(:first, :conditions => ["name = ?", "admin"])
return user.roles.include?(admin_role)
end
def is_customer?(user)
admin_role = Role.find(:first, :conditions => ["name = ?", "customer"])
return user.roles.include?(admin_role)
end
After that include application helper in application controller and define this method
def after_sign_out_path_for(resource_or_scope)
if is_admin?(current_user)
home_path = "/admin/users/sign_in"
elsif is_customer?(current_user)
home_path = "/customer"
end
respond_to?(home_path, true) ? send(root_path) : home_path
end
Hope it will work fine !!!
You can override the after_sign_out_path_for(resource) method that devise uses. Just mention the logic or just the desired redirection path after checking the roles of your user in the method. The devise session's destroy action calls the redirection path through this method.
def after_sign_out_path_for(resource)
#logic
end
Hope this is helpful..
The Devise README covers how to set up custom routes and controllers. In short you'll want to customize your routes for each model, e.g.:
devise_for :admins, :controllers => { :sessions => "admins/sessions" }
devise_for :customers, :controllers => { :sessions => "customers/sessions" }
Then create the corresponding controllers and override Devise::SessionsController#destroy, e.g.:
class Admins::SessionsController < Devise::SessionsController
def destroy
super
redirect_to ...
end
end

Resources