Rails/Devise/Pundit : Redirect after login if next action not authorized - ruby

I am using Rails 7, Devise and Pundit.
I've got Users and Projects.
Only Users classified as "admin" or "moderator" can perform actions (New,
Edit, Update, Destroy, ...).
Unlogged Users and Users classified as "user" can see Index and Show pages.
When I'm on a show page ('http://localhost:3000/projects/[id]') as an unlogged User and try to edit it (via 'http://localhost:3000/projects/[id]/edit') it sends me to a Devise login page which is normal. Once logged in correctly with an unauthorized profile (User classified as "user") Pundit authorization kicks in and rescues the request.
=> The problem is here :
First Firefox tells me that the page isn't redirected properly ... Probably because I'm sent back to 'http://localhost:3000/users/sign_in' while being signed in.
When I reload my page it tells me via an alert "You are already signed in." on my root_path page.
Application_controller :
class ApplicationController < ActionController::Base
before_action :store_user_location!, if: :storable_location?
before_action :authenticate_user!, except: [:index, :show]
before_action :configure_permitted_parameters, if: :devise_controller?
include Pundit
protect_from_forgery with: :exception
rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized
after_action :verify_authorized, except: :index, unless: :skip_pundit?
after_action :verify_policy_scoped, only: :index, unless: :skip_pundit?
protected
def configure_permitted_parameters
devise_parameter_sanitizer.permit(:sign_up, keys: [:username])
devise_parameter_sanitizer.permit(:sign_in, keys: [:username])
devise_parameter_sanitizer.permit(:account_update, keys: [:username])
end
private
def skip_pundit?
devise_controller? || params[:controller] =~ /(^(rails_)?admin)|(^pages$)/
end
def user_not_authorized
flash[:alert] = "You are not authorized to perform this action."
redirect_back(fallback_location: root_path)
end
def storable_location?
request.get? && is_navigational_format? && !devise_controller? && !request.xhr?
end
def store_user_location!
# :user is the scope we are authenticating
store_location_for(:user, request.fullpath)
end
def after_sign_in_path_for(resource_or_scope)
stored_location_for(resource_or_scope) || super
end
end
Project_policy :
class ProjectPolicy < ApplicationPolicy
class Scope < Scope
# NOTE: Be explicit about which records you allow access to!
# def resolve
# scope.all
# end
def resolve
scope.all
end
private
attr_reader :user, :scope
end
def index?
true
end
def show?
true
end
def create?
user.admin? || user.moderator?
end
def edit?
user.admin? || user.moderator?
end
def update?
user.admin? || user.moderator?
end
def destroy?
user.admin? || user.moderator?
end
end
I don't think more is needed but if some code samples are missing don't hesitate to tell me ! I'd like to find a way to handle this properly. Thanks !

I know it is in the Pundit documentation but have you tried without the protect_from_forgery line? I can tell you from first hand experience Pundit works without it...
EDIT: Try to move the protect_from_forgery before the before_action :authenticate_user!

I found a solution but it's probably not a clean one and I don't know if it is safe or if it's durable.
I removed from my application_controller the following :
the method : storable_location?
the method : store_user_location!
before_action :store_user_location!, if: :storable_location?
This is what I added/modified under "private".
# Redirect after login via Devise
def after_sign_in_path_for(resource)
session["user_return_to"] || root_path
end
# Redirect if unauthorized by Pundit
def user_not_authorized
session["user_return_to"] = redirection_reroll
flash[:alert] = "You are not authorized to perform this action."
redirect_to(session["user_return_to"] || root_path)
end
# Reroll redirection path when unauthorized
def redirection_reroll
checker = ["new", "edit"]
path = session["user_return_to"].split("/")
if checker.include? path[-1]
path.pop()
end
session["user_return_to"] = path.join("/")
end

Related

Pundit with second devise model

I manage the authorization of users in my app with the pundit gem. Everything works fine for the user. Now I created a second devise model: Employers. I want to show a specific page to both logged in user as well as logged in employers. How do I do that?
Here is my policy for the model:
class CurriculumPolicy < ApplicationPolicy
class Scope < Scope
def resolve
scope.all
end
end
def create?
return true
end
def show?
record.user == user || user.admin
end
def update?
record.user == user || user.admin
end
def destroy?
record.user == user || user.admin
end
end
And here is my controller for the index page which I want to make accessible:
class CurriculumsController < ApplicationController
skip_before_action :authenticate_user!, only: [:new, :create, :index]
before_action :set_curriculum, only: [:show, :edit, :update, :destroy]
def index
# #curriculums = policy_scope(Curriculum).order(created_at: :desc)
if params[:query]
#curriculums = policy_scope(Curriculum).joins(:user)
.where('users.job_category ILIKE ?', "%#{params[:query]}%")
.where(
'job_category ILIKE :query', query: "%#{params[:query]}%"
)
else
#curriculums = policy_scope(Curriculum).order(created_at: :desc)
end
end
private
def set_curriculum
#curriculum = Curriculum.find(params[:id])
end
def curriculum_params
params.require(:curriculum).permit(:doc)
end
end
You can have workaround here like below for each actions
def show?
true if #user.class.table_name == "employees"
end

Can't create sessions for some of my STI User types using Blizzard's omniauth-bnet gem

I'm working on a Rails 5 app using the omniauth-bnet gem, not devise, have a Single Sign On through that gem, and have a few User types, using Single Table Inheritance. For whatever reason, the admin type can login fine, but the average User cannot create a session. Here's some of the relevant code.
items_controller.rb:
before_action :check_authorization, except: [:show]
before_action :check_for_email, except: [:show]
...
private
def check_authorization
unless current_user
redirect_to root_path
end
end
def check_for_email
unless current_user.email
redirect_to signup_add_email_url
end
end
sessions_controller.rb:
class SessionsController < ApplicationController
def create
begin
#user = User.from_omniauth(request.env['omniauth.auth'])
session[:user_id] = #user.id
flash[:success] = "Well met, #{#user.name}!"
rescue
flash[:warning] = "There was an error while trying to create your
account..."
end
redirect_to items_path
end
...
admin_user.rb:
class AdminUser < User
end
normal_user.rb:
class NormalUser < User
end
user.rb:
class User < ApplicationRecord
...
class << self
def from_omniauth(auth_hash)
user = find_or_create_by(name: auth_hash['info']['battletag'], uid:
auth_hash['uid'], provider: auth_hash['provider'])
user.name = auth_hash['info']['battletag']
user.uid = auth_hash['uid']
user.token = auth_hash['credentials']['token']
user.save!
user
end
end
routes.rb:
...
# Auth
get '/auth/:provider/callback', to: 'sessions#create'
...
The logs show that my NormalUser type session never gets created. Yet the AdminUser type doesn't have any problem logging in...
Any ideas? I've tried everything I can google or think of.

Devise not storing sessions and losing credentials after redirect

It is a VERY strange bug and I am leading with it for 24 hours. It was working well and suddenly it started to fail.
The problem:
When I want to login with Facebook, the app redirec to Facebook permissions request, go back, save the update in the account model (access_token, and updated_at), but I am redirected to the home without permissions to access to signed_in sections.
My stack is:
Rails4, Devise 3.0.0.rc, Omniauth, Omniauth-facebook 1.4.0.
The app only accept login with Facebook.
Take a look:
Omniauth controller: account_signed_in? = true
class Accounts::OmniauthCallbacksController < Devise::OmniauthCallbacksController
def facebook
# You need to implement the method below in your model (e.g. app/models/user.rb)
#account = Account.find_for_facebook_oauth(request.env["omniauth.auth"], current_account)
if #account.persisted?
sign_in_and_redirect #account, :event => :authentication #this will throw if #user is not activated
puts account_signed_in? # <-- true
set_flash_message(:notice, :success, :kind => "Facebook") if is_navigational_format?
else
session["devise.facebook_data"] = request.env["omniauth.auth"]
redirect_to new_account_registration_url
end
end
ApplicationController: account_signed_in? = true
class ApplicationController < ActionController::Base
# Prevent CSRF attacks by raising an exception.
# For APIs, you may want to use :null_session instead.
protect_from_forgery with: :exception
private
def stored_location_for(resource_or_scope)
nil
end
def after_sign_in_path_for(resource_or_scope)
puts account_signed_in? # <-- true
current_account.pages.empty? ? new_page_path : pages_path
end
StaticController (home) account_signed_in? = false
class StaticController < ApplicationController
def home
puts account_signed_in? # <- false
render layout: 'home'
end
I don't know if can there be something that disturb the normal flow of sessions between Devise and Rails.
Found that!
The sessions weren't saved because of the domain parameter in session_store.rb:
BrainedPage::Application.config.session_store :cookie_store,
key: '_my_session', :domain => Rails.configuration.domain
Seems I had changed the domain configuration in development environment (added port, because I was using this var for other propose too), and I didn't realize the impact it could make.

Use CanCan Authorization along with Custom Authentication in Rails 3

I am new to Rails and have been developing an app in rails 3 after following a Lynda.com tutorial where Kevin Skoglund showed us a way to authenticate a user using SHA1 Digest. I used that in my app and there is a need now to put in some Authorization. When I searched around, I found CanCan to be one of the better ones for authorization in rails. However, CanCan seems to be mostly implemented using Devise or Authlogic authentication and not custom authentication.
I wanted to know if it is at all possible to use CanCan if we use custom authentication, like I did. Is so, how to go about getting CanCan to work ?
It looks like CanCan needs some 'create_user' to be present but I am not sure how/where to create it.
Another alternative that I thought would be to put in my custom check on every page to check the user role and redirect them to an error page if they are unauthorized but that seems like a bad way to approach this problem...Your views on this please.
Please let me know if you need any additional information. I am using Ruby 1.9.3 and Rails 3.2.1.
Below is the way I have my current authentication set up. Any help would be greatly appreciated.
access_controller.rb
class AccessController < ApplicationController
before_filter :confirm_logged_in, :except => [:login, :attempt_login, :logout]
def attempt_login
authorized_user = User.authenticate(params[:username], params[:password])
if authorized_user
session[:user_id] = authorized_user.id
flash[:notice] = "You are logged in"
redirect_to(:controller => 'orders', :action => 'list')
else
flash[:notice] = "Invalid Username/password combination"
redirect_to(:action => 'login')
end
end
def logout
session[:user_id] = nil
flash[:notice] = "You have been logged out"
redirect_to(:action => 'login')
end
end
user.rb (User Model)
require 'digest/sha1'
class User < ActiveRecord::Base
has_one :profile
has_many :user_roles
has_many :roles, :through => :user_roles
attr_accessor :password
attr_protected :hashed_password, :salt
def self.authenticate(username="", password="")
user = User.find_by_username(username)
if user && user.password_match(password)
return user
else
return false
end
end
def password_match(password="")
hashed_password == User.hash_with_salt(password, salt)
end
validates_length_of :password, :within => 4..25, :on => :create
before_save :create_hashed_password
after_save :clear_password
def self.make_salt(username="")
Digest::SHA1.hexdigest("Use #{username} with #{Time.now} to make salt")
end
def self.hash_with_salt(password="", salt="")
Digest::SHA1.hexdigest("Put #{salt} on the #{password}" )
end
private
def create_hashed_password
unless password.blank?
self.salt = User.make_salt(username) if salt.blank?
self.hashed_password = User.hash_with_salt(password, salt)
end
end
def clear_password
self.password = nil
end
end
ApplicationController.rb
class ApplicationController < ActionController::Base
protect_from_forgery
private
def confirm_logged_in
unless session[:user_id]
flash[:notice] = "Please Log In"
redirect_to(:controller => 'access', :action => 'login')
return false
else
return true
end
end
end
I recommend first reading or watching the Railscast about CanCan. It is produced by the author of this gem and therefore very informative:
http://railscasts.com/episodes/192-authorization-with-cancan
You can also get help on the Github page:
https://github.com/ryanb/cancan
Somehow, you need to fetch the currently logged in user. This is what the current_user method does, and it needs to be defined on the users controller. Try something like this:
class UsersController < ApplicationController
# your other actions here
def current_user
User.find(session[:user_id])
end
end
Then, you should be able to use CanCan as described in the resources above.

inherited_resources - best practices for missing parent model

Maybe you have seen/read the Railscast/Asciicast about subdomains in Rails 3. I'd like you to ask about best practices on how to implement an application behavior when the parent (in this article: "blog") is not found. Let me explain.
blog1.example.com/articles # it's normal situation
example.com/articles # abnormal situation.
In the second example no blog to find, but articles's routes are still available. I know, I can use something like this ...
def rescue_action(exception)
case exception
when ActiveRecord::RecordNotFound
return redirect_to blogs_path, :status => :moved_permanently
end
super
end
... but is it the "Rails way"? Any idea/comment on this?
What I did in this case, was to restrict the routing based on subdomain or no subdomain. In that case, you can easily have routes that only work on subdomains, resulting in a routing error (404) if someone tries to access that same route without a subdomain.
So for example:
routes.rb
Backend::Application.routes.draw do
constraints AppDomainRoutes.new do
# signup paths
get "/signup" => "accounts#new", as: "signup"
post "/signup" => "accounts#create", as: "signup"
# root
root to: "accounts#new"
end
constraints AccountDomainRoutes.new do
# password reset paths
get "/reset_password/:password_reset_token" => "reset_passwords#edit", as: "reset_user_password"
put "/reset_password/:password_reset_token" => "reset_passwords#update", as: "reset_user_password"
# websites
resources :websites
# root
root to: "websites#new"
end
# request password reset paths
get "/reset_password" => "reset_passwords#new", as: "reset_password_request"
post "/reset_password" => "reset_passwords#create", as: "reset_password_request"
# login paths
get "/login" => "sessions#new", as: "login"
post "/login" => "sessions#create", as: "login"
# logout paths
get "/logout" => "sessions#destroy", as: "logout"
delete "/logout" => "sessions#destroy", as: "logout"
end
And then in lib/routes:
app_domain_routes.rb
class AppDomainRoutes
def matches?(request)
request.subdomain.blank? || request.subdomain == "www"
end
end
account_domain_routes.rb
class AccountDomainRoutes
def matches?(request)
request.subdomain.present? && request.subdomain != "www"
end
end
Now, /signup is only accessible from the main application domain www.mydomain.com or mydomain.com and /websites/new is only accessible from *.mydomain.com. But /login is still accessible in both situations, for convenience sake.
Obviously this doesn't solve the issue of visiting invalid.mydomain.com when invalid in fact is not an account in the database.
For this you go back to the application_controller.rb and handle redirection there, like this:
application_controller.rb
class ApplicationController < ActionController::Base
protect_from_forgery
before_filter :redirect_unknown_account
private
# returns current subdomain (account.subdomain) or nil
def account_subdomain
#account_subdomain ||= request.subdomain if request.subdomain.present? && request.subdomain != "www"
end
def current_account
#current_account ||= Account.find_by_username(account_subdomain) if account_subdomain
end
def redirect_unknown_account
if account_subdomain && ! current_account
redirect_to signup_url(host: app_domain), alert: "This account does not exist."
end
end
def account_domain
#account_domain ||= "#{current_account.username}.#{app_domain}" if current_account
end
def app_domain
#app_domain ||= "mydomain.com"
end
end

Resources