3

I have a multi tenant rails app that let accounts use their own custom domain. The app is hosted on Heroku and the parent domain has a ssl certificate. I want my custom domain users to be able to sign in using the parent domain (www.foo.com) and be redirected to their custom domain (www.bar.com) How can I persist the session on the custom domain when the user signs in?

This is a functionality that's very similar to the way Shopify works.

user177468
  • 225
  • 5
  • 18
  • please elaborate a bit, is the custom domain a sub-domain? are the tenants all using the same rails application instance? HTTPS? – sled Mar 25 '16 at 12:39
  • Hi. All accounts are using the same rails instance. The parent domain has ssl, custom ones does not. That's why I want to sign users through https://www.foo.com (domain with ssl) and redirect them to http://www.bar.com. – user177468 Mar 25 '16 at 13:56
  • okay - so requiring SSL for your tenants through e.g CloudFlare is not an option? The point here is, that even if you protect the login credentials through SSL, the session cookie can be intercepted by a man-in-the-middle after the login. – sled Mar 25 '16 at 17:21
  • Yes, there's a chance that will happen. Is there any other way? How does Shopify does it? They seem to use the same approach I'm trying here. How can I use Cloudfare, can you explain more? – user177468 Mar 25 '16 at 21:47
  • I had a look at Shopify, they seem to use sub-domains for the admin panel (e.g `yourstore.myshopify.com`), the shop is run under a custom domain but it's just delivering content with minimal logic. Also the check-outs are run through `checkout.shopify.com` for example. – sled Mar 26 '16 at 12:06
  • Yep. And the login and signup urls too. Check the form action url, it's a subdomain on the shopify domain. So how do they create the session and redirect the users back to the custom domain? – user177468 Mar 27 '16 at 08:33

4 Answers4

4

Let's take a look at shopify and how they do it.

Shopify differentiates between two cases:

  • The tenant's domain has SSL
  • The tenant's domain has no SSL

Case #1 - The tenant's domain has SSL

Tenant's fictive domain: https://www.secure.shop

The sign-up form points to https://www.secure.shop/signup and after a successful sign-up I get a 302 Found which redirects me to https://www.secure.shop and sets a session cookie.

-> POST https://www.secure.shop/signup, (signup data)
<- 302 Found 
   Location: https://www.secure.shop
   Set-Cookie: _session_id=eba010959d42ec1b734c7bc335ca13cb; path=/;secure; HttpOnly
-> GET https://www.secure.shop
<- 200 OK

The sign-in form points to https://www.secure.shop/login and after a successful sign-in I get a 302 Found which redirects me to https://www.secure.shop and sets a session cookie.

-> POST https://www.secure.shop/login, (credentials)
<- 302 Found 
   Location: https://www.secure.shop
   Set-Cookie: _session_id=238aba8be83ceb3ba4a8ae4d94b1b026; path=/;secure; HttpOnly
-> GET https://www.secure.shop
<- 200 OK

The check-out happens through https://www.secure.shop/checkout.

The log-out points to https://www.secure.shop/logout and what happens is:

-> GET https://www.secure.shop/logout
<- 302 Found
   Location: https://www.secure.shop
   Set-Cookie: _session_id=3b778bb251e170a9e3b1cd8794862203; path=/;  secure; HttpOnly
-> GET https://www.secure.shop
<- 200 OK

Conclusion: Everything runs under the tenant's domain. One session cookie, no magic involved.

Case #2: The tenant's domain has no SSL

Tenant's fictive domain: http://www.insecure.shop

The sign-up form points to https://insecureshop.myshopify.com/account and when I create a new account the following happens:

-> POST https://insecureshop.myshopify.com/account, (signup data)
<- 302 Found 
   Location: http://www.insecureshop.com/account?sid=514d3e699fd55ddb7c12398405e65abf
   Set-Cookie: _secure_session_id=c31d544b27ee8a49b5a6cf9e303e6829; path=/; secure; HttpOnly
-> GET http://www.insecureshop.com/account?sid=514d3e699fd55ddb7c12398405e65abf
<- 200 OK 
   Set-Cookie: _session_id=18d5f07e1e61d8707e111879860abad6; path=/; HttpOnly

The sign-in form points to https://insecureshop.myshopify.com/account/login and what happens is:

-> POST https://insecureshop.myshopify.com/account/login, (credentials)
<- 302 Found
   Location: http://www.insecure.shop/account?sid=a232e58b8cb9fb4936ddf889ab7e73e4
   Set-Cookie: _secure_session_id=d54a653cf4fcb66b831968e9e669b005; path=/; secure; HttpOnly
-> GET http://www.insecure.shop/account?sid=a232e58b8cb9fb4936ddf889ab7e73e4
<- 200 OK
   Set-Cookie: _session_id=44e041cdbcb64d2a2281bb64db52ada0; path=/; HttpOnly

The check-out happens through https://checkout.shopify.com

The log-out points to http://www.insecure.shop/account/logout and what happens is:

-> GET http://www.insecure.shop/account/logout
<- 302 Found
   Location: https://insecureshop.myshopify.com/account/logout?sid=d6c774d39307def7f772de31031c665c
   Set-Cookie: _session_id=8dfb0a130d6f479d1af3a52c40ad3be6; path=/; HttpOnly
-> GET https://insecureshop.myshopify.com/account/logout?sid=d6c774d39307def7f772de31031c665c
<- 302 Found
   Location: http://www.insecure.shop
   Set-Cookie: _secure_session_id=18fcde259616586f89831399cc9c2425; path=/; secure; HttpOnly
-> GET http://www.insecure.shop
<- 200 OK

Conclusion: In the case of an insecure shop, everything is done twice, two separate (and different!) sessions are created once on the tenant's insecure domain and once on myshopify subdomain of the tenant. Both sessions point to the same user record in the back-end.

The credentials are sent encrypted and a single-use token is passed on the redirect to the insecure domain which then creates an authenticated session on the insecure domain. This token is transmitted in plain text.

First thing that pops in your mind is probably:

What if a man-in-the-middle intercepts that token and hijacks the session?

Well, the attacker will be authenticated as you on http://www.insecure.shop, but not on https://insecureshop.myshopify.com.

What if we try to be smart and use an AJAX request with CORS and set the session cookie manually with JavaScript, so no redirect with magic tokens occur

This doesn't help either since the session cookie itself is transmitted in plain text all the time, so the session can be hijacked anyway. Even worse, we would have only one session.

Why are they using two different sessions / sessions ids for the same user?

Here lies the magic, your session on http://www.insecure.shop can be compromised easily, however your session on https://insecureshop.myshopify.com is not compromised since the attacker doesn't know the session ID because the cookie was transmitted by SSL and the session ID is different from the "insecure" one.

But still the attacker can abuse my account on http://www.insecure.shop

True, but what is he able to do? He can add products to your cart, read your profile and so on, but he can't do a check-out and charge your credit card. Why? Because the check-out goes through the secure part at https://insecureshop.myshopify.com, for which he doesn't have the session cookie and thus is unauthenticated.

But if the attacker is smart, he could just change the password and re-login on both the insecure and secure part

Not if you add appropriate measures, i.e require the user to enter his password to do any profile changes. The attacker doesn't know the credentials since they were transmitted by SSL.

Still, isn't there a better solution

There is - use HTTPS everywhere, CloudFlare for examples makes it dead easy to put SSL in front of your customer's domains. This gives both you less overhead to implement and an added value for your customers and your customer's customers. Win win situation.

You don't have to use a third party solution like CloudFlare for this, since you are in charge - all traffic goes through your server / front-end proxy (e.g nginx). You could manage the SSL certificates for your customers and charge them, however this becomes quite cumbersome both for accounting and configuration since each domain needs its own certificate.

UPDATE (important)

Please note the subtle difference in the insecure case, the two cookies have the names _session_id and _secure_session_id. This is for good reason as both sessions exist on the same rails instance and they could be used interchangeably, which is a bad thing. What I think they're doing is to set a flag on the session whether the session was created through a secure channel and add an appropriate before action like

before_action :require_secure_session, only: :checkout

def require_secure_session
  head :unauthorized unless session[:is_secure_flag]
end

Sources:

Shop examples were taken from http://wemakewebsites.com/blog/80-best-shopify-stores-for-ecommerce-inspiration, https one was #79, and http one was #23. Tools used: Chrome developer tools (network tab), cURL.

sled
  • 14,525
  • 3
  • 42
  • 70
  • I'm using the rack-cors gem for the cross-domain requests, but I keep getting a Can't verify CSRF token authenticity on the logs. If I do a skip_before_action :verify_authenticity_token on the controller everything works. Should this be the way to do it? Let the thing handle requests from everywhere? I can write the origin requests permitted in the rack-cors configuration. I guess that should do it, but it should be dynamic. – user177468 Mar 29 '16 at 07:07
0

You may find this answer helpful. In the redirect URL or HTTP request to the custom domain you could pass in a token that gets caught by the rails app of the custom domain to log the user in.

Community
  • 1
  • 1
Kelsey Hannan
  • 2,857
  • 2
  • 30
  • 46
  • So the basic idea is to send a single access token in the url and use it to login the user. The problem I see with this approach is that the token will be sent unencrypted and could be sniffed out. Am I right? – user177468 Mar 23 '16 at 07:07
  • In theory yes, though you could set up the token to instantly expire to make the attack difficult to execute on. Another approach is to use session cookies to change how your site responds to domains of other origins. This answer may be of help: http://stackoverflow.com/questions/5123325/correct-way-to-share-login-sessions-across-subdomains-in-rails-3 – Kelsey Hannan Mar 23 '16 at 07:37
  • That article talks mostly about subdomains (which is easy). Do you want me to read something specific there? I wonder how does shopify does it. They are the only ones I'm aware of that handles pretty much the same feature. – user177468 Mar 23 '16 at 07:56
0

I created an app that had a similar functionality. The way I handled this was to have a simple redirection. You can save the subdomain string to the user account when they create the account, then when the user successfully signs in you can redirect them like so

    if current_user.subdomain?
      redirect_to root_url(subdomain: current_user.subdomain)
    else
      redirect_to root_url, notice: "Logged in!"
    end
j_rhoades
  • 76
  • 3
0

I think there are 2 ways to do this :

1. By sending user session information through request headers to custom domain when returning from parent domain.

Sample code how it should be like we store session to redis we can send it through header to other server :

def after_sign_in_path_for(resource_or_scope)
  #store session to redis
  if current_user
    # an unique MD5 key
    cookies["_validation_token_key"] = Digest::MD5.hexdigest("#{session[:session_id]}:#{current_user.id}")
    # store session data or any authentication data you want here, generate to JSON data
    stored_session = JSON.generate({"user_id"=> current_user.id, "username"=>current_user.screen_name, ... ...})
    $redis.hset(
      "mySessionStore",
      cookies["_validation_token_key"],
      stored_session,
     )
   end
end

2. Have user forced login through code through a session service to start a session on custom domain identical to parent domain for same user.

Muhammad Ali
  • 2,173
  • 15
  • 20