1

I have certain actions that require a user to be authenticated. If the user attempts these actions, I want my website to automatically be able to redirect them to log in and then automatically "replay" whatever the original action was. I am using Devise for authentication and CanCan for role/abilities. To this end, I have the following code:

class ApplicationController < ActionController::Base

    # CanCan detected an role violation. Redirect to login page.
    rescue_from CanCan::AccessDenied do |exception|
        session[:saved_params] = params
        user_signed_in? ? render_forbidden : render_unauthorized
    end
end

class Users::SessionsController < Devise::SessionsController
    def after_sign_in_path_for(resource)
        if session[:saved_params]
            controller = session[:saved_params][:controller].capitalize+"Controller"
            replay_with_params(controller, session[:saved_params][:action], session[:saved_params])
        end
    end
end

Desired Behavior

The user flow would be like this:

  1. User arrives at Event#newform, fills out form, and submits it.
  2. In ApplicationController, CanCan detects anonymous user attempting to do something that they are not allowed to do. App Controller saves the params of the current request and redirects to login page.
  3. User logs in at Devise login page. The SessionControllerdetects session[:saved_params] and attempts to "replay" the original request from Step 1.

Attempt #1: Directly invoking the controller's action

class ApplicationController < ActionController::Base

    # Replay's another controller's action
    def replay_with_params(controller,action,params)
        if controller.instance_of? String
            klass = Object.const_get(controller)
            c = klass.new
        elsif controller.instance_of? Class
            c = controller.new
        else
            throw "Unrecognized controller type: #{controller}"
        end

        c.params = params
        c.dispatch(action, request)
        c.response.body
    end

end

The replay_with_params function is something I tweaked from https://stackoverflow.com/a/7085969/86421.

The Problem

The problem with this is that I cannot reuse the authenticity token from another request. I either need to generate a new one, or skip_before_action :verify_authenticity_token on the target controller. I dont think generating a new token makes sense since there is no front-end to the replayed request. Skipping verification of the token is not appealing for obvious reasons.

Attempt #2: Redirection

Redirection would actually make more sense, I think, but it would only be feasible for GET requests. In my scenario, I need to be able to work with all verbs.

Fundamentally Different Solutions

I could intercept with a modal popup and make the user login there while keeping the original html form intact. This may be the best solution, but I prefer the flow of my original solution better. One of the reasons why I dont like this is because now I have to build my CanCan role/ability logic into my views, or else manually add the modal code into each view. The former seems like a violation of MVC and the latter is tedious and error prone.

Is what I am trying to do possible? I am running into roadblocks on every angle. How can I intercept a request, force someone to login and then replay the original request?

Community
  • 1
  • 1
Jeff
  • 13,943
  • 11
  • 55
  • 103
  • You can save the record temporarily in the database, with some flag that indicates it's not a published event and then after user login change that flag to published. – Loqman Sep 03 '16 at 16:24

1 Answers1

1

I would propose a very different solution - if your users should not be able to create a resource unless they are authorized you should deny access before they can actually fill in the form.

This avoids complicating your authorization/authentication logic and opening your users to session hijacking attacks or other exploits. It also avoids violating the stateless nature of REST more than necessary.

In CanCanCan you can define an action alias as so to cover the :new action:

alias_action :new, :create, to: :make

Or

alias_action :new, :create, :edit, :update, :destroy, to: :crud

You can then use this in your ability definition as so:

can :make, Event

There are quite a few guides and SO question that detail how you can save the original request URL in the session and since it is a simple GET request you can simply redirect the user back after authenticating.

max
  • 96,212
  • 14
  • 104
  • 165
  • If you REALLY have to have a form which the user can complete before authentication the approach mentioned by @Loqman where you save the record with a flag in the database is the way to go. – max Sep 03 '16 at 16:57
  • This scenario is precisely what I was trying to avoid. However, I can't argue that it is more simple and clean than my scenario, and perhaps for that reason it should be preferred to my scenario. One thing I think you may have missed though is that I would want my scenario to work with all HTTP verbs - not just GET. – Jeff Sep 03 '16 at 17:17
  • The thing is that in REST that POST, PATCH, DELETE etc should not be replayed. Thats one of the core reasons you are running into roadblocks. Rails is deliberately designed to not allow internal redirects for example as this leads to horrible coding practices. – max Sep 03 '16 at 17:31
  • But there is nothing really stopping you from shoving a bunch of data into the session and refilling the form with it. Just make sure you are properly aware of the danger of session hijacking and try to mitigate it. – max Sep 03 '16 at 17:33
  • I actually never even thought of going back to the form and repopulating it. I guess thats just one more proof that I am overthinking/overengineering. You're making too much sense, max! ;) – Jeff Sep 03 '16 at 17:41