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.
Let's take a look at shopify and how they do it.
Shopify differentiates between two cases:
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.