Restoring Rails session data when cookies aren't available

C is for Cookie
If you've ever needed to implement user-friendly upload, you know intimately what a pain it is to get right. The web just isn't built for uploading files from a browser. I mean, it kinda works, but even then only with a dozen or so limitations. Even the major photo and video sites have tried various solutions to make this easier for users. So when I built Dibs.net, I decided rather quickly to abandon all hope of getting it working flawlessly with plain ol' Javascript and HTML, and instead looked into using a fairly nonintrusive Flash uploader component. (Without Flash installed, it just falls back to a simple HTML-based file-upload form.)

That's not to say it was perfectly simple to get working with Rails. Because Dibs.net accepts uploads only from logged-in users, I ran into two limitations that would not allow me to use this solution:

  • Flash doesn't send the cookies from the browser (at least it doesn't in Firefox; it might in IE)
  • Rails doesn't support non-cookie sessions

Because Flash doesn't send the session cookie, Rails thinks the request is coming from a new, logged-out user and creates a new session for it. Adding a cookies feature to Flash was well out of my hands since I don't work for Adobe, so I looked into a way to restore the session from a session key passed as a URL parameter. After some experimentation, I found a solution that works great.

Assumptions

I use a modified version of the acts_as_authenticated plugin. Upon authentication, the plugin sets the :user session key to the authenticated user's id. You'll need to adapt for your own configuration.

Example Rails Code

In RAILS_ROOT/app/controllers/show_my_ip_controller.rb:

class ImagesController < ApplicationController
	session :off, :only => :create
	prepend_before_filter :restore_session_user_from_param, :only => :create
	requires_login :except => :index

	def create
	  # Handle the file upload here
	end

	private
	def restore_session_user_from_param
	    data = ActionController::CgiRequest::DEFAULT_SESSION_OPTIONS[:database_manager].session_class.find_session( params[:_session_id] ).data
	    sess_obj = Marshal.load( Base64.decode64( data ) )
	    @current_user = User.find( sess_obj[:user] )
  	rescue
    	authorization_required
  	end
end

Then we include the session id as a parameter in the form's action URL in the view:

<form action="<%= images_path(:_session_id => session.session_id) %>" method="post" id="photoupload" enctype="multipart/form-data">

How it works

Under normal circumstances the acts_as_authenticated plugin sets the @current_user instance variable to the current logged-in user at the start of each request. Since we have no session data when a Flash app hits the controller, there's effectively no current_user. Our goal is to get current_user working, so we:

  • turn sessions off for the relevant action; otherwise Rails will create useless sessions any time Flash hits that action
  • prepend a before filter to set the @current_user instance variable
  • require login for most of the actions, including create

In the before_filter, we grab the session data from whatever session store we're using, decode and unmarshall it, and set the @current_user instance variable to the User we find with the id we get from the session hash.

Simple? Not really. But it works!

Further Reading

I couldn't find much documentation on any of this beyond stomping through the Rails code & Ruby's CGI Standard Library docs.

Update: A Word of Caution

I forgot to mention when I published this earlier that there's a reason parameterized sessions is discouraged: browsers will send the entire current link, including the session id, in referer headers to offsite hosts. This doesn't affect Dibs.net's Flash upload, but in other scenarios use the above with caution, or with SSL.

Feedback and Article Ideas

Want to see a topic explored here? Send Me a Message.