Home > Software engineering >  Rails/Devise/Pundit : Redirect after login if next action not authorized
Rails/Devise/Pundit : Redirect after login if next action not authorized

Time:04-30

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 !

CodePudding user response:

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!

CodePudding user response:

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