March 18, 2015

Sorcery + Grape on Rails

最近手头有一个需要快速开发的新Web项目, 于是想了想还是不用sinatra, 直接用RoR来写. 因为需要为手机提供API, 感觉在Rails上面架设sinatra有点自欺欺人的感觉, 于是第一次尝试在Rails上面加一层Grape... 于是问题就来了.

Sorcery在Session/Cookies方面的操作完全依赖Rails的API, 以前用sinatra的时候, 基本上没有感觉到什么不适, 这次在Grape上使用Sorcery就爆炸了OTL. Grape::Endpoint.send(:include, Sorcery::Controller)之后就开始各种undefined. 看了看大概都集中在ActionDispatch和ActionController里. 下午大概的做了个hotfix, 可惜因为Grape自用的Cookies和ActionDispatch::Cookies区别实在太大, Grape的remember_me功能在grape上暂时用不了.

module API
  module SorceryAdapter

    AUTHENTICITY_TOKEN_LENGTH = 32

    def self.included(mod)
      mod.instance_eval {
        helpers do
          ### Adapt for Sorcery (some directly taken from rails codes)

          # Get session
          def session
            env[Rack::Session::Abstract::ENV_SESSION_KEY]
          end

          # Disable remember_me because of the cookies type conflict
          def current_user
            unless defined?(@current_user)
              @current_user = login_from_session || nil
            end
            @current_user
          end

          ## ActionDispatch::Request
          def reset_session
            if session && session.respond_to?(:destroy)
              session.destroy
            else
              self.session = {}
            end
            @env['action_dispatch.request.flash_hash'] = nil
          end

          ## ActionController::RequestForgeryProtection::ProtectionMethods::NullSession
          # Sets the token value for the current session.
          def form_authenticity_token
            masked_authenticity_token(session)
          end

          def real_csrf_token(session)
            session[:_csrf_token] ||= SecureRandom.base64(AUTHENTICITY_TOKEN_LENGTH)
            Base64.strict_decode64(session[:_csrf_token])
          end

          # Creates a masked version of the authenticity token that varies
          # on each request. The masking is used to mitigate SSL attacks
          # like BREACH.
          def masked_authenticity_token(session)
            one_time_pad = SecureRandom.random_bytes(AUTHENTICITY_TOKEN_LENGTH)
            encrypted_csrf_token = xor_byte_strings(one_time_pad, real_csrf_token(session))
            masked_token = one_time_pad + encrypted_csrf_token
            Base64.strict_encode64(masked_token)
          end

          def xor_byte_strings(s1, s2)
            s1.bytes.zip(s2.bytes).map { |(c1,c2)| c1 ^ c2 }.pack('c*')
          end
        end
      }
    end
  end
end