Cross domain session sharing in Rails - Part 1

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.

If you have carefully seen how Google authenticates you, you would have noticed that just signing into GMail, Google also signs you in YouTube, Google maps and all the other Google applications. You might not be aware of it but that's what Google does.

We wanted to achieve something similar where logging in one application means you don't have to login again in the second or third application. All our applications are powered by Rails. So we decided to look into how to share the user session among multiple apps using Rails.

How session works in a single Rails application

Before trying to share the session with other Rails apps, we need to understand how session works in single Rails app.

You can configure the session in config/initializers/session_store.rb. The default session store in Rails is cookie store.

One can access the session using the session method in controllers and add data to it.

class PostsController < ApplicationController
  def create
    session[:creator_id] = current_user.id
  end
end

We use devise for the authentication. Devise stores the logged in user's information in the session. This information is used to authenticate the user for all the requests after the user is logged in using cookies. Though cookie store is the default session store provided by Rails, we use Redis as the session store. But browsers don't understand anything about Redis. They understand cookies very well.

Cookies

Rails provides cookies method to set and retrieve the cookies. We can set any cookie as following in any of the controller.

class PostsController < ApplicationController
  def create
    cookies[:total_count] = params[:post][:content].length
  end
end

Nothing actually happens on the browser until this Rails handles this request and sends response to the browser. The cookies method just maintains a hash of cookies on server side. Rails sends Set-Cookie header for all the cookies present on server side to the browser in the response. Once the browser receives the response with Set-Cookie header from Rails, it sets the cookie. For all the subsequent requests, the cookie is sent back to the Rails application until it is expired.

Cookies have some rules.

  • Just setting them on server side doesn't set them on browser. The response should hit the browser which will set the cookie that you want.
  • You should set expiry time for the cookie. Rails allows that using expires option.
  • You can set cookies only for YOUR domain. I can't set cookie for Google.com if my application does not serve Google.com. Browsers only send cookies that are set for your domain back to the server in every request.
cookies[:version] = {
  value: version,
  domain: URI(ENV['HOST']).host,
  expires: 1.day

This will the set the cookie version for a day on the domain specified by ENV['HOST'].

Session store

Though we just found out that cookies is all that matters for the browsers, that's just one side of the story. Client side is ruled by the cookies whereas server side is controlled by the session. Session store rules are driven in Rails by config/initializers/session_store.rb

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

This is typical configuration for a session store. This code has two parts.

The configuration outside of the redis hash is used by Rails whereas the redis hash is used by the Redis.

  • The key _kittens_session is used by Rails to set a cookie in the browser. The value of this cookie is generated randomly. This cookie is important as it plays vital role in the authentication process as we will see later.
  • The JSON serializer will tell Rails to store the session in JSON format.
  • The options in the redis hash are passed to Redis so that Redis will store the values only for a day and will use specific prefix for all the session data. We can also set the URL for the Redis so that we can use different Redis databses for sessions and other parts such as Sidekiq. In Redis every session key will be prefixed by kittens:session.

How authentication uses session and cookies?

Even before a user is authenticated Rails sets a cookie for the session in the browser. You can verify this by visiting Codetriage.com - a Rails application.

If you refresh the page, you will notice that the cookie changes its value.

Once the user is logged in, Devise stores the user information in the session using the session store key. As we are using Redis session store, the user information is stored in Redis. Let's say the value of the cookie _kittens_session is 0121304abb1164bf35b6b3878aa07e87. Then we can see what Devise has stored against this session in Redis.

127.0.0.1:6379[2]> GET "memory:session:0121304abb1164bf35b6b3878aa07e87"
"{\"_csrf_token\":\"9FzOkU2Sek4om72OkB15dftDVjZLQQrydvzTsslC458=\",\"warden.user.user.key\":[[1],\"$2a$11$Eat/.BYgD9PxR9IXmh616.\"]}

When user visits any page of the app, the browser sends _kittens_session cookie with value 0121304abb1164bf35b6b3878aa07e8 to the server. Devise uses this value and checks whether there is a user in session or not. If yes, it fetches the user from the database using the encrypted data stored in the session and logs in the user.

When the user logs out, Devise makes sure to destroy the session from the session store. So next time the session cookie is sent by the browser, there is no user against it in the session. So user is forced to login again. This cycle continues till the end.

Session key is important and is used to verify if the logged in user's session is present in the session store or not. If you change the session key, then all the existing logged in users are effectively logged out.

In this post, we saw how Rails and Devise use session and cookies to authenticate a user. In part two of this post, we will use this information to share the session data across multiple Rails apps with the help of cookies.


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.