Adding authentication info in Rails proxy middleware - ruby-on-rails

I am using the rack-proxy gem in Rails to proxy requests to an external server. Thing is, the external endpoint requires authentication. How do I provide that information from the middleware?
Here's what I have so far:
require 'rack/proxy'
class MyProxy < Rack::Proxy
MY_REQUEST = %r{^/path/(.*)}
def initialize(app)
#app = app
end
def call(env)
if m = MY_REQUEST.match(env['PATH_INFO'])
env['PATH_INFO'] = "https://otherserver.org/#{m[1]}"
env['HTTP_HOST'] = "otherserver.org"
#the otherserver.org endpoint requires authentication
super env
else
#app.call(env)
end
end
end

Depends on what kind of authentication the other server is using. If its just plain HTTP Authentication you can do something like:
env['Authentication'] = 'Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ=='
Where the value part follows the spec at: http://en.wikipedia.org/wiki/Basic_access_authentication#cite_ref-8

Following the Rack spec here gave me good pointers - http://rubydoc.info/github/rack/rack/master/file/SPEC
Had to do:
env['HTTP_AUTHORIZATION'] = 'Basic <base64 username:password>'

Related

How to make Rack::Attack work behind a load balancer?

I used the example throttle code for Rack::Attack.
throttle('req/ip', limit: 100, period: 5.minutes) do |req|
req.ip unless req.path.starts_with?('/assets')
end
This worked great on our staging server but immediately ran into the limit on production because req.ip returns the IP address of our load balancer and not the remote_ip of the client.
Note that remote_ip is a method in ActionDispatch::Request but not Rack::Attack::Request.
We are using Rails 3.2.2 on Ruby 2.2.
I was able to get it working by adding a method to Rack::Attack::Request
class Rack::Attack
class Request < ::Rack::Request
def remote_ip
#remote_ip ||= (env['action_dispatch.remote_ip'] || ip).to_s
end
end
end
then using
req.remote_ip unless req.path.starts_with?('/assets')

Uninitialized constant error after uploading on Heroku

There is the following problem: I'm developing some Rails application on my local machine, and all is good, app works, but after uploading on Heroku there would be the following error (I saw it using 'heroku logs'):
NameError (uninitialized constant Api::V1::ApiV1Controller::UndefinedTokenTypeError)
My code:
def require_token
begin
Some code which generates UndefinedTokenTypeError
rescue UndefinedTokenTypeError => e
render json: e.to_json
end
end
UndefinedTokenTypeError is in lib/errors.rb file:
class EmptyCookieParamsError < StandardError
def to_json
{ result_code: 1 }
end
end
class UndefinedTokenTypeError < StandardError
def to_json
{ result_code: 2 }
end
end
I've got the same version for Rails/Ruby on my local machine (2.0). How can I fix it? Thanks.
From what I can see, you may be experiencing either a CORS-related issue or you're not authenticating properly
Cross Origin Resource Sharing
CORS is a standard HTML protocol, which basically governs which websites can "ping" your site. Facebook & Twitter's third-party widgets only work because they allow any site to send them data
For Rails to work with CORS, it's recommended to install the Rack-CORS gem. This will allow you to put this code in your config/application.rb file:
#CORS
config.middleware.use Rack::Cors do
allow do
origins '*'
resource '/data*', :headers => :any, :methods => :post
end
end
Because you're experiencing these issues on Heroku, it could be the problem you're experiencing. Even if it isn't, it's definitely useful to appreciate how CORS works
Authentication
Unless your API is public, you'll likely be authenticating the requests
The way we do this is with the authenticate_or_request_with_http_token function, which can be seen here:
#Check Token
def restrict_access
authenticate_or_request_with_http_token do |token, options|
user = User.exists?(public_key: token)
#token = token if user
end
end
We learnt how to do this with this Railscast, which discusses how to protect an API. The reason I asked about your code was because the above works for us on Heroku, and you could gain something from it!
Running on Heroku will be using the production environment. Check to see what is different between environments/development.rb and environments/production.rb
You can try running your app in production mode on your local machine, rails server -e production
I am guessing your config.autoload_paths isn't set correctly. Should be in config/application.rb

How to add/remove rack middleware while server is running?

In capybara specs I want to test absence of XSS vulnerability. We use selenium-webdriver with chromium to run browser specs, but chrome by default has XSS protection, which may be disabled by setting X-XSS-Protection header to 0. I wrote a middleware to set this header, and it works if enabled in config/environments/test.rb. As this header is required only in this spec, I don't want to have it enabled for all specs.
I tried following:
describe 'without xss protection' do
before :all do
Rails.configuration.middleware.use Rack::DisableXssProtection
end
after :all do
Rails.configuration.middleware.delete Rack::DisableXssProtection
end
it 'should not have xss', :needs_browser do
visit new_order_path
page.driver.execute_script <<-EOF
$("<input/>", {
id: "new_input",
name: "bad_field",
type: "radio",
value: "<script>alert('fail');</script>"
}).appendTo("#some_form");
EOF
find('#new_input').click
click_on 'submit'
end
end
If I stop anywhere inside this spec, I can see it in Rails.configuration.middleware, but it is not called (header is not set and if I put raise in this middleware it is ignored).
So, how can I add/remove middleware while server is running?
EDIT: middleware is just the following:
module Rack
class DisableXssProtection
def initialize(app)
#app = app
end
def call(env)
status, headers, body = #app.call(env)
headers['X-XSS-Protection'] = '0'
[status, headers, body]
end
end
end
As you're testing Rack::DisableXssProtection itself, it would make sense to extract it as a gem, and test it in isolation with a dummy Rails application.

How to initialize variables in a Rack statement?

I am using Ruby on Rails 3 and I am trying to use the Rack. Since I am not expert in this matter, I would like to know some thing about that.
The following code is from here.
require 'rack'
class Rack::ForceDomain
def initialize(app, domain)
#app = app
#domain = domain
end
def call(env)
request = Rack::Request.new(env)
if #domain and request.host != #domain
fake_request = Rack::Request.new(env.merge("HTTP_HOST" => #domain))
Rack::Response.new([], 301, "Location" => fake_request.url).finish
else
#app.call(env)
end
end
end
What is the variable app and from where its values are retrieved?
From where and how to pass the domain variable in the initialize method?
Rack is a middleware to interface a higher level app (like rails) to a webserver (like mongrel). In rails, you can get this code to work by using:
# config.middleware.use "Rack::ForceDomain", "mydomain.com"
App is a reference to the Rails instance. Domain is added by the person you got that code from, it is not standard Rack initialize.
You do not need to go down to the rack level for what you are doing though for this. I personally prefer to do the rewrite through nginx, but you can do it in rails 3.
In your config/routes.rb file:
constraints(:host => /example.com/) do
root :to => redirect("http://www.example.com")
match '/*path', :to => redirect {|params| "http://www.example.com/#{params[:path]}"}
end
This is from http://railsdog.com/blog/2010/10/29/redirect-non-www-requests-the-rails3-way/

how to setup rails Authenticity Token to work with multiple domains?

I'm building an app that uses subdomains as account handles (myaccount.domain.com) and I have my sessions configured to work across the sub-domains like so:
config.action_controller.session = {:domain => '.domain.com'}
In addition to the subdomain a user can input a real domain name when they are creating their account. My Nginx config is setup to watch for *.com *.net etc, and this is working to serve out the pages.
The problem comes when a site visitor submits a comment form on a custom domain that was input by the user. The code is throwing an "Invalid AuthenticityToken" exception. I'm 99% sure this is because the domain the user is on isn't specified as the domain in the config.action_controller.session. Thus the authenticity token isn't getting matched up because Rails can't find their session.
So, the question is: Can you set config.action_controller.session to more than 1 domain, and if so can you add / remove from that value at runtime without restarting the app?
I found the answer to this question here: http://codetunes.com/2009/04/17/dynamic-cookie-domains-with-racks-middleware/
This solution worked for me because my app was running on Rails 2.3.5, which uses Rack. The request comes from web server, goes through middleware layers and enters the application. So this middleware layer detects the host with which the application is accessed and sets cookie domain for the request. Here it is:
# app/middlewares/set_cookie_domain.rb
class SetCookieDomain
def initialize(app, default_domain)
#app = app
#default_domain = default_domain
end
def call(env)
host = env["HTTP_HOST"].split(':').first
env["rack.session.options"][:domain] = custom_domain?(host) ? ".#{host}" : "#{#default_domain}"
#app.call(env)
end
def custom_domain?(host)
domain = #default_domain.sub(/^\./, '')
host !~ Regexp.new("#{domain}$", Regexp::IGNORECASE)
end
end
# turn it on in environment.rb
config.load_paths += %W( #{RAILS_ROOT}/app/middlewares )
# production.rb
config.middleware.use "SetCookieDomain", ".example.org"
.example.org is the default domain that will be used unless the application is accessed via custom domain (like site.com), we give it different values depending on environment (production/staging/development etc).
# tests/integration/set_cookie_domain_test.rb (using Shoulda and Webrat)
require 'test_helper'
class SetCookieDomainTest < ActionController::IntegrationTest
context "when accessing site at example.org" do
setup do
host! 'example.org'
visit '/'
end
should "set cookie_domain to .example.org" do
assert_equal '.example.org', #integration_session.controller.request.session_options[:domain]
end
end
context "when accessing site at site.com" do
setup do
host! 'site.com'
visit '/'
end
should "set cookie_domain to .site.com" do
assert_equal '.site.com', #integration_session.controller.request.session_options[:domain]
end
end
context "when accessing site at site.example.org" do
setup do
host! 'site.example.org'
visit '/'
end
should "set cookie_domain to .example.org" do
assert_equal '.example.org', #integration_session.controller.request.session_options[:domain]
end
end
end

Resources