What is the proper way to send a subdomain.herokuapp.com to the apex domain of the application? This is to avoid multiple domain names with the same content.
https://github.com/tylerhunt/rack-canonical-host seems to be the perfect choice for this. Leaving it here for anyone else who has the same question.
Quoting from https://devcenter.heroku.com/articles/custom-domains
The domain myapp.herokuapp.com will always remain active, even if
you’ve set up a custom domain. If you want users to use the custom
domain exclusively, you should send HTTP status 301 Moved Permanently
to tell web browsers to use the custom domain. The Host HTTP request
header field will show which domain the user is trying to access; send
a redirect if that field is myapp.herokuapp.com.
You can redirect requests to the "subdomain.herokuapp.com" using a before filter in ApplicationController or using a constraint in rails routing.
For a comprehensive answer with some bit of extensibility, in totality it looks something like this;
class ApplicationController < ActionController::Base
before_filter :redirect_to_example if Rails.env.production?
# Prevent CSRF attacks by raising an exception.
# For APIs, you may want to use :null_session instead.
protect_from_forgery with: :exception
private
# Redirect to the appropriate domain i.e. example.com
def redirect_to_example
domain_to_redirect_to = 'example.com'
domain_exceptions = ['example.com', 'www.example.com']
should_redirect = !(domain_exceptions.include? request.host)
new_url = "#{request.protocol}#{domain_to_redirect_to}#{request.fullpath}"
redirect_to new_url, status: :moved_permanently if should_redirect
end
end
This will redirect everything to domain_to_redirect_to except what's in domain_exceptions.
Related
My rails controller has two types of handlers, one type is conventional response with a web page, another is designed to respond to $http get requests from Angular, and returns json to be processed by the pages javascript code.
I use devise, and I this code at the top of my application controller
protect_from_forgery
before_action :authenticate_user!
after_action :set_csrf_cookie
The problem is that when for example, the login goes stale, I think authenticate_user is returning my "unauthorized" web page to the caller, rather than the json that would inform the caller that the current user is no longer authorized, and then I could handle the condition on the client side properly.
Any thoughts on efficient way to do this, withoug having to take out authenticate_user! from the application controller.
Most of my controllers have handlers for about 15 routes, about 50/50 which of them are designed to return json to ajax calls, and the others return web pages. I like the security that authenticate_user! in the application controller provides, and am hesitant to remove it and instead have to have different code to handle security in each of my methods.
Thanks.
To understand how this works you really got to get into Warden (which devise is built on top of) and Rack. What before_action :authenticate_user! does is call Warden.authenticate! and asks it to identify a user. Warden identifies users by using strategies. A strategy can be just using session[:user_id] to find a user from the database (which what happens 99% of the time in Devise) or something more novel like HTTP Basic Auth.
If all the available strategies fail then the failure app is called. This is a Rack application. In Devise this is just a basic Rails controller (Rails controllers are Rack compliant applications) that usually returns a redirect. If you are running Rails in the development environment you may get a HTML response though as the error handler that shows you those friendly little exception pages kicks in.
You can customize the response by providing your own failure application:
class CustomAuthFailure < Devise::FailureApp
def respond
self.status = 401
self.content_type = 'json'
self.response_body = {"errors" => ["Invalid login credentials"]}.to_json
end
end
# config/initializers/devise.rb
config.warden do |manager|
manager.failure_app = CustomAuthFailure
end
I am new to both web and Rails, and I found that it is a common usage to put something like this in the beginning of a controller:
before_action :authenticate_admin, only: [:only_admin]
where
def authenticate_admin
unless current_user.admin?
flash[:alert] = "You are not admin!"
redirect_to '/'
end
end
Considering if it's possible to somehow ignore this redirection from the client side, I feel really unsafe. How is redirect_to implemented? Why can this usage assure safety?
redirect_to will - well terminate your current HTTP request and require the client to initiate a new call to /, which is supposed to be a public page. You could redirect wherever you want.
In other words, it's a HTTP redirect.
The code first checks if the user is an admin. If not, it "alerts" the message and redirects the client to /.
If you are asking if you can ignore this redirect on the server by removing this call, you shouldn't ignore the redirect, otherwise you just check if the user is admin but are not taking any action. Effectively, the code is obligating the client to go to /. Assuming that that URL is safe to use for non-admins, this is safe.
If you are asking if the client (the browser) is able to ignore the redirect - it's not possible for the client to ignore this redirect; rails will terminate the connection and issue a HTTP redirect, which the browser can't ignore, because it's on HTTP level.
Consider reframing your question to be more precise
Scenario: Multi-tenant rails app that uses subdomains and devise
Problem: I want the user to be able to log into mydomain.com then be forwarded to their own subdomain1.mydomain.com address as a logged-in user. Right now they can only log directly into their own subdomain.
I'm a relative Rails newbie and I can't find a simple solution (although it seems like there must be one). Ideally I would like to have mydomain.com and subdomain1.mydomain.com share one cookie, but my skills aren't there for writing custom middleware. Obviously since it's multitenant I can't share one session across all subdomains. Stuck on this for a few days and curious if there is a simple solution (such as a config.session_store domain setting) that I'm missing before I start looking at OAuth or other more cumbersome solutions. Any help will be appreciated!
Edit: Of course I only found this after posting. Log a user into their subdomain after registration with Rails and Devise . Will try the config.session_store domain: :all with a before filter recommendation and post any details if it doesn't work, seems like a good idea at least.
Edit: SOLUTION that worked for my particular Devise with subdomains setup:
class ApplicationController < ActionController::Base
before_action :check_subdomain
def check_subdomain
unless request.subdomain == "" or request.subdomain == session[:subdomain]
redirect_to request.protocol+request.domain
end
end
end
session_store.rb
My::Application.config.session_store :cookie_store, key: '_my_session' , :domain => :all, :tld_length => 2
Basically I set the subdomain in the session with session[:subdomain] at login and use that to scope the session to the current user. Otherwise when the domain is set to :all in session_store it breaks the scope. If the user is not authorized it redirects them to the public home page via the request.protocol (http:// or https://) +request.domain redirect. Simple! Now users can move between the base domain and their subdomain within the same session.
Cookie
From what you've posted, I'd estimate you have a problem with the tracking of your session cookie. We had a similar problem with our subdomain-powered application, which lead to the cookie being dropped each time you switched between the two
We found the remedy here:
Share session (cookies) between subdomains in Rails?
#config/initializers/session_store.rb
Your_App::Application.config.session_store :cookie_store, key: '_your_app_session', domain: :all, tld_length: 2
The trick is the tld_length argument - this allows you to define how many "levels" of the domain can be accommodated; IE if you're using a sub domain, you'll need to set the tld_length to reflect it
Forwarding
I'm not sure whether you have a problem with your forwarding or not; I'll give you some ideas anyway.
When you log into a "subdomain", unless you've got a true multi-tenancy implementation of Rails (where each user is stored in a different database), you should be able to allow the users to login on the main form, and then redirect them to the subdomain without an issue
Something you need to consider is the subdomain constraint will only be populated if you use _url path helpers:
<%= link_to "Your Name", path_url(subdomain: "subdomain_1") %>
The reason for this is the _path helper is relative to the base URL, and consequently cannot populate the subdomain option. Alternatively, the _url path helper points to the URL in its entirety -- allowing you to define the sub domain as required
--
If you send the request & continue to want the user to remain signed-in, you'll need to ensure you're able to persist the authentication across the sub-domains. IE if you have a single-sign in form on the "main" page, you'll want to ensure you can continue the authentication into the subdomains
I'm developing an AngularJS application with a RoR backend and ran into an issue when using multiple layouts. The application uses one layout when rendering pages to an unauthenticated user, and changes to another layout once the user is authenticated.
The layout is server on the initial pageload, and is managed by Rails. Some sample code illustrating how we're loading the different layouts based on the route:
class SampleController < ApplicationController
layout :current_layout
def current_layout
"layout" unless request.xhr?
end
end
Sample controller for different section:
class SampleController2 < ApplicationController
layout :current_layout
def current_layout
"anotherLayout" unless request.xhr?
end
end
This is defined separately for the controllers managing the authenticated/unauthenticated users, and basically serves up the proper layout. We're using a XHR check to prevent routing loops when Angular comes into picture.
So this works fine in most browser, but breaks when using IE9. Angular falls back to using #! URLs in IE9, so Rails has no idea which controller to load since the hash doesn't get sent to the backend. In this case, Rails loads the root and it's associated layout. If the authenticated section is set as the default, then it loads this layout even for unauthenticated users, and vice-versa.
So basically, I need to find a way to make this multiple layout application work properly even in browsers which don't support HTML5 pushState. I've checked all over the place for a proper solution for this and couldn't come up with anything yet.
Alright, so after testing lots of tweaks on both the client and server side, I ended up using the following solution. Basically, I just stopped exposing Angular to multiple layouts and dealt with that in Rails. Of course, this does put a limitation on users.
The app is divided into two main sections: dashboard and authentication. So in the backend, we restrict the users from accessing any authentication pages if they're logged in, and obviously dashboard pages can only be accessed if the user is authenticated.
The root of the problem was that Rails had no way of knowing which layout to serve, if a layout should be served at all. We used the cookies to differentiate the authenticated and unauthenticated users. If the cookie is set, and it passes the authenticity testing, then the dashboard layout should be loaded. Otherwise, any user trying to access the dashboard will be redirected to the authentication section:
class DashboardController < ApplicationController
layout :current_layout
before_filter :authenticate_user
def authenticate_user
if request.xhr? && !cookies[:access_token]
redirect_to "/login"
end
end
def current_layout
if cookies[:access_token]
"dashboard" unless request.xhr?
else
"application" unless request.xhr?
end
end
end
Similarly for the authentication section:
class AuthenticationController < ApplicationController
layout :current_layout
before_filter :redirect_if_authenticated
def redirect_if_authenticated
if request.xhr? && cookies[:access_token]
redirect_to "/dashboard"
end
end
def current_layout
if cookies[:access_token]
"dashboard" unless request.xhr?
else
"application" unless request.xhr?
end
end
end
So main points to note here:
If the request is an XHR, don't serve the layout again. Serving the layout again will cause infinite loading loops in IE9, possibly in other IE versions as well.
Since we're not getting the URL Fragment at the server side, we have no way of knowing which layout should be loaded. So we're using the cookies as the source of truth, and access is controlled solely based on this.
This introduces some asymmetry on the clientside: In some cases, the url and layout will remain if the user types in the URL for another section. Since we can't do anything based on the URL from the server, and since cookies remain unchanged in this case, this has to be handled by Angular.
For the last point, the authentication section in my case only had a few possible urls, so I just redirected if I got a regex match as follows:
if(Modernizr.history) {
if(!/(login|sign|pass)/.test(location.pathname)) {
$location.path('/');
}
} else {
if(location.hash.length > 3 && !/(login|sign|pass)/.test(location.hash)) {
$window.location.href = '/';
}
}
Checking for the history API here, because in older IE browsers(and I guess in other older ones as well?), $location.path('/') wasn't reloading the entire page consistently. When using this, the $routeProvider configuration should also conditionally load the layouts, for cases where we redirect to root /:
$routeProvider
// Code for routes
.otherwise({
// Of course, add something to check access_token authenticity here as well :P
redirectTo: $.cookie('access_token') ? "/dashboard" : "/login"
});
So, with these couple tweaks, the app is working properly in all browsers and functioning with multiple layouts. Of course, I know nothing about RoR, so I'm hoping someone is going to have a few improvements to suggest, or a better answer. ;)
I am building a product that has static pages and dynamic pages(product related). Both category of pages have different release life cycle. The marketing team working with the designer, release the static pages and the product pages are released by the engineering team.
The static pages reside in public/home and they are self contained. They don't need access to the Rails infrastructure other than providing links.
In this setup, I am trying to implement the following behavior:
When an un-authenticated visitor launches http://www.xyz.com, the user should be taken to the static landing page.
When an authenticated visitor launches http://www.xyz.com, the user should be taken to the product landing page (LandingsController, index action).
In my current implementation, I check if the user is authenticated in the Rails world and render the static page OR the product page.
I want to know the following:
1) How do you handle such scenarios?
2) Is there a way to avoid entering the Rails stack for static home page.
3) Is there a customization for the root_path method to return different root based on the context
1) How do you handle such scenarios?
The common answer would look like this:
class LandingsController < ApplicationController
before_filter :login_required
def index
...
end
...
private
def login_required
if not_logged_in? # This methods depends on your authentication strategy
send_file "/your/static/path/#{params[:action]}", :type => "application/html charset=utf8;"
return false # Halt chain
end
end
send_file documentation
And, depending on the correspondence between each of your actions and your templates, you can further abstract the login_required method into the ApplicationController, and validate if the file exists.
2) Is there a way to avoid entering the Rails stack for static pages
Yes. You have to take my word for it, because I haven't done it myself, but you can use a Rack middleware to do that. Here is an example of how to do something similar, with the exception that instead of a redirect, you would serve the file statically (just set the headers and the results of File.read as content) This depends on the authentication library you're working with, though.
3) Is there a customization for the root_path method to return different
root based on the context
You cannot define a conditional route (that is, defining multiple routes in the routes.rb file), but you can override the root_url method in ApplicationController, assuming you are using a named path root in your route definitions. Something like
class ApplicationController
def root_url(*options)
if logged_in?
"/return/something/custom"
else
super(*options)
end
end
end
This, however, sound really a bad idea, since 1) You should point to the same url, and let the controller handle the request (your links should be blind of where to take you), and 2) It may potentially break other stuff that rely on the root_url and root_path methods.
Unfortunately, Rails' routing can only route requests to different controllers based on something in the request, making the per-request session data just out of reach. Your current implementation is certainly the most common strategy. I am guessing something like this:
def index
if logged_in?
# any logged in logic you need.
else
render :file => 'public/home', :layout => false
end
end
The only way to refactor this to make it feel less "icky" is to move that render call to a before_filter. Since the filter will have rendered?, your action won't get invoked at all. Of course, you could also choose to redirect_to another location for authenticated (or non-authenticated) requests in a before filter, which would solve the problem entirely.
The only thing you could do would be based on the non-existence of the session cookie.
Write a middleware component or Rack application (etc.) that explicitly handles the request if no session cookie is present. Similarly, you could use middleware to re-write the request, and then pass it onto the application layer.
Use a similar strategy as #1, but do it via web server configuration (Apache or nginx), avoiding the Rails app entirely.
But, it's definitely possible for someone to have a session and yet not be logged in (e.g. if they went to another page which you didn't handle this way), or even have invalid session data, so you wouldn't be able to actually eliminate the code you have now. These changes would only serve to increase the performance of the session-less requests, but unless those pages are causing a significant problem (which I doubt), so I would not recommend doing so.