Friday, October 16, 2009

per request rails authenticity_tokens

An example of how to present a different rails authentication_token per request. This doesn't conveniently cache keys, or tackle the problem of storage and expiration of the tokens yet but shows how you can embed information that might allow you to do that. Used a public key encryption scheme, but probably should really be symmetric to ease the load on the server.

Copy & paste to see me!

module ActionController #:nodoc:
  module RequestForgeryProtection
    # This module overrides the default rails authenticity_token behavior by using 
    # the normal rails token as a secret that only lives in the current user's 
    # session. The new token is an encrypted hash containing that secret and a
    # timestamp that could be used to timeout the code.
    class << self
      attr_accessor :key_secret
    end

    protected

      # Returns true or false if a request is verified.  Checks:
      #
      # * is the format restricted?  By default, only HTML requests are checked.
      # * is it a GET request?  Gets should be safe and idempotent
      # * Does the form_authenticity_token match the given token value from the params?
      def verified_request?
        !protect_against_forgery?     ||
          request.method == :get      ||
          request.xhr?                ||
          !verifiable_request_format? ||
          check_private_data(params[request_forgery_protection_token])
#          form_authenticity_token == params[request_forgery_protection_token]
      end

      def check_private_data(input)
        ret = true
        logger.debug("sec-: " + ActionController::RequestForgeryProtection.key_secret)
        # set key in environment.rb with ActionController::RequestForgeryProtection.key_secret = 'key'
        p_key = OpenSSL::PKey::RSA.new(File.read(RAILS_ROOT + '/config/webprivate.pem'), ActionController::RequestForgeryProtection.key_secret)
        yml = p_key.private_decrypt(Base64.decode64(input))
        logger.debug("YAML TOKEN: " + yml)
        hsh = YAML::load(yml)
        if not hsh[:rnd] == session[:_csrf_token]
          ret = false
        else
          #check some other things like token store and time for expirations
        end
        ret
      end

      # Sets the token value for the current session.  Pass a :secret option
      # in +protect_from_forgery+ to add a custom salt to the hash.
      def form_authenticity_token
        session[:_csrf_token] ||= ActiveSupport::SecureRandom.base64(32)
        hsh = {}
        hsh[:time] = Time.now
        hsh[:rnd] = session[:_csrf_token]
        p_key = OpenSSL::PKey::RSA.new(File.read(RAILS_ROOT + '/config/webpublic.pem'))
        yml = hsh.to_yaml
        logger.debug("YAML Token: " + yml)
        crypted = p_key.public_encrypt(yml)
        token = Base64.encode64(crypted)
        #puts token
        token
      end

  end
end