Cross domain session sharing in Rails - Part 2

We have multiple micro services at Memory.ai which talk to each other and share data. As we are extracting more and more micro services, we were facing the problem of adding authentication layer in every service. In an ideal world, only one service would perform the authentication and other services will just delegate the authentication to that service.

In the first part of this blog series, we saw how Rails and Devise use cookies and session to authenticate the user. In this post, we will see how to share the cookies and session between multiple Rails apps.

Earlier our authentication logic was part of app.kittens.io. But when we started working on a new app - dev.kittens.io, we didn't want to duplicate the effort for authentication again. More-so, we didn't want the users to sign up again on the new app that we were building. So we decided to extract the authentication into a separate app: auth.kittens.io.

What we want

We have two apps: app.kittens.io and dev.kittens.io. We want to make sure that once the user is authenticated for app.kittens.io they are also authenticated for dev.kittens.io and vice-versa.

We use Redis as our session store in the app.kittens.io. To solve this problem of sharing the authentication logic across multiple Rails app, we decided to use same Redis as session store for all the new Rails applications. So once the session for a user is written on auth.kittens.io, we just had to make sure that same Redis session is found out when the user navigates to dev.kittens.io or app.kittens.io.

In a trivial workflow, the user session is stored in the session store with a unique identifier. This identifier is actually the value of the session cookie which is shared between the browser and the server. The browser sends this session cookie for every request to the server and the server looks for the session for this key in the session store. You can see an example of such session cookie below.

When the user is authenticated using Devise, the session is updated with the user details. For eg. in a normal case, following details will be stored in the session:

    {
     "_csrf_token" => 'bk+V3AGLrt0JSQpdIa7ksLgDJsGRmWNZ3y8LJ1j0kuM=',
     "warden.user.user.key" => [1, '$2a$11$YRhQFL.rLh5SRn.dpKqCk.']
    }

When next time a request comes to the server from the browser, Devise finds out this same session from the session store and declares that the user is already logged in.

So now our problem is reduced to just sharing the cookie between two Rails apps app.kittens.io and dev.kittens.io.

Session Cookies in Rails

The session cookies are set against the request host. So in our case, the session cookie set by auth.kittens.io is set against auth.kittens.io domain. So it is not available on app.kittens.io or dev.kittens.io. Instead we want to set the cookie on .kittens.io domain so that it is available on all of the subdomains of kittens.io

We need to change the session store configuration to below to make this happen.

Rails.application.config.session_store :redis_session_store,
  key: '_kittens_session',
  serializer: :json,
  domain: :all,
  redis: {
    expire_after: 1.week,
    key_prefix: 'kittens:session:',
    url: ENV['REDIS_SESSIONS_URL']
}

Here we have modified the session store configuration and added following options:

domain: :all

When domain: :all option is specified, Rails derives the actual domain by removing the subdomains from the request host and then sets the cookie against it. Otherwise it sets the cookie against request.host. Additionally, Rails also adds . prefix to the root domain before setting the cookie if domain: :all option is specified. This makes the cookies set against .kittens.io in our case.

Additionally Rails also allows option tld_length which can restrict deriving the subdomains to a number. If you have only two level subdomains like auth.kittens.io or dev.kittens.io you can pass tld_length: 2 in the session store configuration. This option is only used by Rails when you use domain: :all. In case, we don't pass it, subdomains such as www.auth.kittens.io are also mapped to kittens.io.

Such cookie is now accessible to the all the subdomains of kittens.io so the Rails apps hosting app.kittens.io and dev.kittens.io can access the cookie and can look up the Redis session store to see if the session for the user to whom this cookie belongs, exists or not. The lookup works automatically when we are using Devise in all the Rails applications. So the user login works automagically on other Rails apps.

This change allowed us not repeat the authentication in all the new apps that we are building. It also allowed to scale the users and authentication service independently.


Subscribe to my newsletter to know more about Rails internals and get to know real world experience reports of using Rails out in the field.