how to setup rails Authenticity Token to work with multiple domains? - ruby-on-rails

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

Related

How do I set the port correctly in email links during a system test?

I'm writing a System test to confirm the entire sign up flow is working in a Rails 7 app (with the Clearance gem and an email confirmation SignInGuard).
The test is working fine right up until I "click" the confirm link in the email (after parsing it with Nokogiri). For some reason the URL in the email points to my dev server (port 3000) instead of pointing to the test server (port 49736, 49757, 49991, whatever).
I could look up the current port the test server is using (it changes every run) and replace the port portion of the URL but that seems quite hacky. Am I missing something obvious or doing something wrong?
URL in mailer: confirm_email_url(#user.email_confirmation_token)
Route from rails routes:
Prefix Verb URI Pattern Controller#Action
confirm_email GET /confirm_email/:token(.:format) email_confirmations#update
The system test so far:
require "application_system_test_case"
require "test_helper"
require "action_mailer/test_helper"
class UserSignUpFlowTest < ApplicationSystemTestCase
include ActionMailer::TestHelper
test "Sign up for a user account" do
time = Time.now
email = "test_user_#{time.to_s(:number)}#example.com"
password = "Password#{time.to_s(:number)}!"
# Sign up (sends confirmation email)
visit sign_up_url
fill_in "Email", with: email
fill_in "Password", with: password
assert_emails 1 do
click_on "Sign up"
sleep 1 # Not sure why this is required... Hotwire/Turbo glitch?
end
assert_selector "span", text: I18n.t("flashes.confirmation_pending")
# Confirm
last_email = ActionMailer::Base.deliveries.last
parsed_email = Nokogiri::HTML(last_email.body.decoded)
target_link = parsed_email.at("a:contains('#{I18n.t("clearance_mailer.confirm_email.link_text")}')")
visit target_link["href"]
# ^^^^^^^^^^^^^^^^^^^^^^^^^
# This is the bit that fails... The link in the email points to my dev server (port 3000) rather than
# the test server (port 49736, 49757, 49991, etc). I only figured this out when my dev server crashed
# and Selenium started choking on a "net::ERR_CONNECTION_REFUSED" error
assert_selector "span", text: I18n.t("flashes.email_confirmed")
end
end
Edit
For the time being I've worked around it by replacing visit target_link["href"] with visit hacky_way_to_fix_incorrect_port(target_link["href"]):
private
def hacky_way_to_fix_incorrect_port(url)
uri = URI(url)
return "#{root_url}#{uri.path}"
end
The URL used in mailers is specified by:
Rails.application.configure.action_mailer.default_url_options
In config/environments/test.rb I had set mine to port 3000 when I first installed Clearance:
config.action_mailer.default_url_options = {host: "localhost:3000"}
To fix it, I first tried specifying the port dynamically but the suggested method didn't actually work and it seems it isn't necessary. Removing the port number was enough to get my system test passing:
config.action_mailer.default_url_options = {host: "localhost"}
As mentioned by Thomas Walpole, the reason this works is that Capybara.always_include_port is set to true. With this setting, attempts to access http://localhost/confirm_email/<token> (with no port specified) are automatically rerouted to http://localhost:<port>/confirm_email/<token>.
The always_include_port setting defaults to false in the Capybara gem but it turns out Rails sets it to true when it starts up the System Test server.

Feature tests not passing REQUEST_URI to a middleware with RSpec and Capybara

I am trying to setup feature testing on an application.
I decided to install Capybara, and thus added it to my project's Gemfile:
group :test do
gem "capybara"
end
I declare my tests on the spec/feature folder, and the test manages to execute:
require "rails_helper"
feature 'My Feature' do
scenario 'User visits feature page' do
visit '/my-feature'
expect(page).to have_text('Stuff')
end
end
Issue: I have an URL middleware that does not detect the env['REQUEST_URI] flag and thus my test fails:
class UrlNormalizationMiddleware
def initialize(app)
#app = app
end
def call(env)
uri_items = env['REQUEST_URI'].split('?')
...
#app.call(env)
end
end
The actual application loads, and passes values on env['REQUEST_URI'], but doesn't on the test environment.
Anything else that I need to setup?
Thanks!
REQUEST_URI is not part of the rack spec, which means it's not guaranteed to be set, and you shouldn't be using it in your middleware. Instead you should be using things like PATH_INFO, QUERY_STRING, etc. which are specified in the rack spec and should therefore be available - https://github.com/rack/rack/blob/master/SPEC

Rails - conditionally log for a specific hostname

I'm looking for a way to configure a Rails server log only if the client has contacted a specific hostname. e.g. I could make it so that http://public.example.com doesn't get logged, but http://debug.example.com (same underlying Rails app server) does get logged (or ideally gets logged in more detail than the regular host). It would help with production debugging.
You can use gem Lograge to customize your log. This gem will give you much more custom to your log. For example, in your case, I will do this
After install the gem. Create a file at config/initializers/lograge.rb
# config/initializers/lograge.rb
Rails.application.configure do
config.lograge.enabled = true
config.lograge.custom_options = lambda do |event|
# custom log on specific domain
if event.payload[:host] == "debug.example.com"
{:host => event.payload[:host]}
else
{}
end
end
end
And in your Application Controller
# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
# This will add request's host to lograge so you can use it to filter log later
def append_info_to_payload(payload)
super
payload[:host] = request.host
end
end
Now you can customize your log base on domain, on how to customize it please read at: https://github.com/roidrage/lograge

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

Rails - Ensuring www is in the URL

I have my app hosted on Heroku, and have a cert for www.mysite.com
I'm trying to solve for
Ensuring www is in the URL, and that the URL is HTTPS
Here's what I have so far:
class ApplicationController < ActionController::Base
before_filter :check_uri
def check_uri
redirect_to request.protocol + "www." + request.host_with_port + request.request_uri if !/^www/.match(request.host) if Rails.env == 'production'
end
But this doesn't seem to being working. Any suggestions or maybe different approaches to solve for ensuring HTTPs and www. is in the URL?
Thanks
For the SSL, use rack-ssl.
# config/environments/production.rb
MyApp::Application.configure do
require 'rack/ssl'
config.middleware.use Rack::SSL
# the rest of the production config....
end
For the WWW, create a Rack middleware of your own.
# lib/rack/www.rb
class Rack::Www
def initialize(app)
#app = app
end
def call(env)
if env['SERVER_NAME'] =~ /^www\./
#app.call(env)
else
[ 307, { 'Location' => 'https://www.my-domain-name.com/' }, '' ]
end
end
end
# config/environments/production.rb
MyApp::Application.configure do
config.middleware.use Rack::Www
# the rest of the production config....
end
To test this in the browser, you can edit your /etc/hosts file on your local development computer
# /etc/hosts
# ...
127.0.0.1 my-domain-name.com
127.0.0.1 www.my-domain-name.com
run the application in production mode on your local development computer
$ RAILS_ENV=production rails s -p 80
and browse to http://my-domain-name.com/ and see what happens.
For the duration of the test, you may want to comment out the line redirecting you to the HTTPS site.
There may also be ways to test this with the standard unit-testing and integration-testing tools that many Rails projects use, such as Test::Unit and RSpec.
Pivotal Labs has some middleware called Refraction that is a mod_rewrite replacement, except it lives in your source code instead of your Apache config.
It may be a little overkill for what you need, but it handles this stuff pretty easily.
In Rails 3
#config/routes.rb
Example::Application.routes.draw do
redirect_proc = Proc.new { redirect { |params, request|
URI.parse(request.url).tap { |x| x.host = "www.example.net"; x.scheme = "https" }.to_s
} }
constraints(:host => "example.net") do
match "(*x)" => redirect_proc.call
end
constraints(:scheme => "http") do
match "(*x)" => redirect_proc.call
end
# ....
# .. more routes ..
# ....
end
I think the issue is you are running on Heroku. Check the Heroku documentation regarding Wildcard domains:
"If you'd like your app to respond to any subdomain under your custom domain name (as in *.yourdomain.com), you’ll need to use the wildcard domains add-on. ..."
$ heroku addons:add wildcard_domains
Also look at Redirecting Traffic to Specific Domain:
"If you have multiple domains, or your app has users that access it via its Heroku subdomain but you later switched to your own custom domain, you will probably want to get all users onto the same domain with a redirect in a before filter. Something like this will do the job:"
class ApplicationController
before_filter :ensure_domain
TheDomain = 'myapp.mydomain.com'
def ensure_domain
if request.env['HTTP_HOST'] != TheDomain
redirect_to TheDomain
end
end
end
Try this
def check_uri
if Rails.env == 'production' && request && (request.subdomains.first != "www" || request.protocol != 'https://')
redirect_to "https://www.mysite.com" + request.path, :status => 301 and return
end
end
Your best bet would be to set up redirect with your DNS provider, so it happens long before any request reaches your server. From the Heroku Dev Center:
Subdomain redirection results in a 301 permanent redirect to the specified subdomain for all requests to the naked domain so all current and future requests are properly routed and the full www hostname is displayed in the user’s location field.
DNSimple provides a convenient URL redirect seen here redirecting from
the heroku-sslendpoint.com naked domain to the
www.heroku-sslendpoint.com subdomain.
For proper configuration on Heroku the www subdomain should then be a
CNAME record reference to yourappname.herokuapp.com.
It's not just DNSimple that does this. My DNS provider is 123 Reg and they support it but call it web forwarding.

Resources