---
title: "Cookies on Rails"
description:
  "A deep dive into how Rails handles cookies. How Rails keeps cookies encrypted
  and how to protect your data."
canonical_url: "https://www.bigbinary.com/blog/cookies-on-rails"
markdown_url: "https://www.bigbinary.com/blog/cookies-on-rails.md"
---

# Cookies on Rails

A deep dive into how Rails handles cookies. How Rails keeps cookies encrypted
and how to protect your data.

- Author: Neeraj Singh
- Published: March 19, 2013
- Categories: Rails

Let's see how session data is handled in Rails 3.2 .

If you generate a Rails application in 3.2 then ,by default, you will see a file
at `config/initializers/session_store.rb`. The contents of this file is
something like this.

```ruby
Demo::Application.config.session_store :cookie_store, key: '_demo_session'
```

As we can see `_demo_session` is used as the key to store cookie data.

A single site can have cookies under different key. For example airbnb is using
14 different keys to store cookie data.

![airbnb cookies](https://www.bigbinary.com/blog/images/images_used_in_blog/2013/cookies-on-rails/airbnb.png)

### Session information

Now let's see how Rails 3.2.13 stores session information.

In `3.2.13` version of Rails application I added following line to create
session data.

```ruby
session[:github_username] = 'neerajsingh0101'
```

Then I visit the action that executes above code. Now if I go and look for
cookies for `localhost:3000` then this is what I see .

![demo session](https://www.bigbinary.com/blog/images/images_used_in_blog/2013/cookies-on-rails/Screenshot_3_19_13_11_33_PM.png)

As you can see I have only one cookie with key `_demo_session` .

## Deciphering content of the cookie

The cookie has following data.

```plaintext
BAh7CEkiD3Nlc3Npb25faWQGOgZFRkkiJTgwZGFiNzhiYWZmYTc3NjU1ZmVmMGUxM2EzYmEyMDhhBjsAVEkiFGdpdGh1Yl91c2V
ybmFtZQY7AEZJIhJuZWVyYWpkb3RuYW1lBjsARkkiEF9jc3JmX3Rva2VuBjsARkkiMU1KTCs2dXVnRFo2R2NTdG5Kb3E2dm5Bcl
ZYRGJGbjJ1TXZEU0swamxyWU09BjsARg%3D%3D--b5bcce534ceab56616d4a215246e9eb1fc9984a4
```

Let's open `rails console` and try to decipher this information.

```ruby
content = 'BAh7CEkiD3Nlc3Npb25faWQGOgZFRkkiJTgwZGFiNzhiYWZmYTc3NjU1ZmVmMGUxM2EzYmEyMDhhBjsAVEkiFGdpdGh1Yl91c2V
ybmFtZQY7AEZJIhJuZWVyYWpkb3RuYW1lBjsARkkiEF9jc3JmX3Rva2VuBjsARkkiMU1KTCs2dXVnRFo2R2NTdG5Kb3E2dm5BclZYRGJGbjJ1T
XZEU0swamxyWU09BjsARg%3D%3D--b5bcce534ceab56616d4a215246e9eb1fc9984a4'
```

When the content is written to cookie then it is escaped. So first we need to
`unescape` it.

```ruby
> unescaped_content = URI.unescape(content)
=> "BAh7CEkiD3Nlc3Npb25faWQGOgZFRkkiJTgwZGFiNzhiYWZmYTc3NjU1ZmVmMGUxM2EzYmEyMDhhBjsAVEkiFGdpdGh1Yl91c2V
ybmFtZQY7AEZJIhJuZWVyYWpkb3RuYW1lBjsARkkiEF9jc3JmX3Rva2VuBjsARkkiMU1KTCs2dXVnRFo2R2NTdG5Kb3E2dm5BclZYRG
JGbjJ1TXZEU0swamxyWU09BjsARg==--b5bcce534ceab56616d4a215246e9eb1fc9984a4"
```

Notice that towards the end `unescaped_content` has `--` . That is a separation
marker. The value before `--` is the real payload. The value after `--` is
digest of data.

```ruby
> data, digest = unescaped_content.split('--')
=> ["BAh7CEkiD3Nlc3Npb25faWQGOgZFRkkiJTgwZGFiNzhiYWZmYTc3NjU1ZmVmMGUxM2EzYmEyMDhhBjsAVEkiFGdpdGh1Yl91c2V
ybmFtZQY7AEZJIhJuZWVyYWpkb3RuYW1lBjsARkkiEF9jc3JmX3Rva2VuBjsARkkiMU1KTCs2dXVnRFo2R2NTdG5Kb3E2dm5BclZYRGJ
GbjJ1TXZEU0swamxyWU09BjsARg==", "b5bcce534ceab56616d4a215246e9eb1fc9984a4"]
```

The data is `Base64` encoded. So let's unecode it.

```ruby
> Marshal.load(::Base64.decode64(data))
=> {"session_id"=>"80dab78baffa77655fef0e13a3ba208a",
    "github_username"=>"neerajsingh0101",
    "_csrf_token"=>"MJL+6uugDZ6GcStnJoq6vnArVXDbFn2uMvDSK0jlrYM="}
```

So we are able to get the data that is stored in cookie. However we can't tamper
with the cookie because if we change the cookie data then the digest will not
match.

Now let's see how Rails matches the digest.

In order to create the digest Rails makes of use of
`config/initializer/secret_token.rb` . In my case the file has following
content.

```ruby
Demo::Application.config.secret_token = '111111111111111111111111111111'
```

This secret token is used to create the digest.

```ruby
> secret_token =  '111111111111111111111111111111'
> OpenSSL::HMAC.hexdigest(OpenSSL::Digest.const_get('SHA1').new, secret_token, data)
=> "b5bcce534ceab56616d4a215246e9eb1fc9984a4"
```

Notice that the result of above produces a value that is same as `digest` in
earlier step. So if cookie data is tampered with then the digest match will
fail. This is why it is absolute necessary that attacker should not be able to
get access to `secret_token` value.

Did you notice that we can access the cookie data without needing
`secret_token`. It means the data stored in cookie is not encrypted and anyone
can see it. That is why it is recommended that application should not store any
sensitive information in cookie .

## Using cookie gives you more control

In the previous example we used `session` to store and retrieve data from
cookie. We can directly use `cookie` and that gives us a little bit more
control.

## Using unsigned cookie

```ruby
cookies[:github_username] = 'neerajsingh0101'
```

Now if we look at cookie stored in browser then this is what we see.

![update cookie](https://www.bigbinary.com/blog/images/images_used_in_blog/2013/cookies-on-rails/Screenshot_3_20_13_9_49_PM.png)

As you can see now we have two keys in our cookie. One created by `session` and
the other one created by code written above.

Another thing to note is that the data stored for key `github_username` is not
`Base64encoded` and it also does not have `--` to separate the data from the
digest. It means this type of cookie data can be tampered with by the user and
the Rails application will not be able to detect that the data has been tampered
with.

Now let's try to sign the cookie data to make it tamper proof.

```ruby
cookies.signed[:twitter_username] = 'neerajsingh0101'
```

Now let's look at cookies in browser.

![update cookies](https://www.bigbinary.com/blog/images/images_used_in_blog/2013/cookies-on-rails/Screenshot_3_20_13_9_54_PM.png)

This time we got data with another key `twitter_username` . Another thing to
notice is that cookie data is signed and is tamper proof.

When we use `session` then behind the scene it uses `cookies.signed`. That's why
we end up seeing signed data for key `_demo_session` .

## Tampering signed cookie

What happens when user tampers with signed cookie data.

Rails does not raise any exception. However when you try to access cookie data
then nil is returned because the data has been tampered with.

## Security should be on by default

session , by default, uses signed cookies which prevents any kind of tampering
of data but the data is still visible to users. It means we can't store
sensitive information in session.

It would be nice if the session data is stored in encrypted format. And that's
the topic of our next discussion.

## Rails 4 stores session data in encrypted format

If you generate a Rails application in Rails 4 then ,by default, you will see a
file at `config/initializers/session_store.rb` . The contents of this file is
something like

```ruby
Demo::Application.config.session_store :cookie_store, key: '_demo_session'
```

Also you will notice that file at `config/initializers/secret_token.rb` looks
like this .

```ruby
Demo::Application.config.secret_key_base = 'b14e9b5b720f84fe02307ed16bc1a32ce6f089e10f7948422ccf3349d8ab586869c11958c70f46ab4cfd51f0d41043b7b249a74df7d53c7375d50f187750a0f5'
```

Notice that in Rails 3.2.x the key was `secret_token`. Now the key is
`secret_key_base` .

```ruby
session[:github_username] = 'neerajsingh0101'
```

![cookies and site data](https://www.bigbinary.com/blog/images/images_used_in_blog/2013/cookies-on-rails/Screenshot_3_22_13_4_13_PM.png)

Cookie has following data.

```ruby
RkxNUWo4NlBKakoyU1VqZWJIKzNaV0lQVVJwQjZhdUVTRnowVHppSVJ3Mk84TStoS1hndFZFNHlNaGw2RHBCc0ZiaEpsM0NtYTg4d
nptcjFaQWVJbUdOaFh5MVlCdWVmSHBMNWpKbkRKR0JrSU5KZFYwVjVyWTZ3aUNqSWxJM1RTMkQybEtPUFE5VDFsZVJyakx0dFh3PT
0tLTZ5NGIreU00Z0MyNnErS29SSGEyZkE9PQ%3D%3D--3f2fd67e4e7785933485a583720d29ba88bca15f
```

Let's open `rails console` and try to decipher this information.

```ruby
content = 'RkxNUWo4NlBKakoyU1VqZWJIKzNaV0lQVVJwQjZhdUVTRnowVHppSVJ3Mk84TStoS1hndFZFNHlNaGw2RHBCc0ZiaEpsM0NtYTg4d
nptcjFaQWVJbUdOaFh5MVlCdWVmSHBMNWpKbkRKR0JrSU5KZFYwVjVyWTZ3aUNqSWxJM1RTMkQybEtPUFE5VDFsZVJyakx0dFh3PT
0tLTZ5NGIreU00Z0MyNnErS29SSGEyZkE9PQ%3D%3D--3f2fd67e4e7785933485a583720d29ba88bca15f'
```

When the content is written to cookie then it is escaped. So first we need to
`unescape` it.

```ruby
unescaped_content = URI.unescape(content)
=> "RkxNUWo4NlBKakoyU1VqZWJIKzNaV0lQVVJwQjZhdUVTRnowVHppSVJ3Mk84TStoS1hndFZFNHlNaGw2RHBCc0ZiaEpsM0NtYTg4d
nptcjFaQWVJbUdOaFh5MVlCdWVmSHBMNWpKbkRKR0JrSU5KZFYwVjVyWTZ3aUNqSWxJM1RTMkQybEtPUFE5VDFsZVJyakx0dFh3PT 0tLTZ
5NGIreU00Z0MyNnErS29SSGEyZkE9PQ==--3f2fd67e4e7785933485a583720d29ba88bca15f"
```

Now we need `secret_key_base` value. And using that let's build `key_generator`
.

```ruby
secret_key_base = 'b14e9b5b720f84fe02307ed16bc1a32ce6f089e10f7948422ccf3349d8ab586869c11958c70f46ab4cfd51f0d41043b7b249a74df7d53c7375d50f187750a0f5'
key_generator = ActiveSupport::KeyGenerator.new(secret_key_base, iterations: 1000)
key_generator = ActiveSupport::CachingKeyGenerator.new(key_generator)
```

Our `MessageEncryptior` needs two long random strings for encryption. So let's
generate two keys for encryptor.

```ruby
secret = key_generator.generate_key('encrypted cookie')
sign_secret = key_generator.generate_key('signed encrypted cookie')
encryptor = ActiveSupport::MessageEncryptor.new(secret, sign_secret)
```

Now we can finally decipher the data.

```ruby
data =  encryptor.decrypt_and_verify(unescaped_content)
puts data
=> neerajsingh0101
```

As you can see we need the `secret_key_base` to make sense out of cookie data.
So in Rails 4 the session data will be encrypted ,by default.

## How to migrate from Rails 3.x to Rails 4 without loosing cookie

Rails4 will transparently will upgrade cookies from unencrypted to encrypted
cookies. This is a brilliant example of trivial choices removed by Rails.

## Links

- [Human page](https://www.bigbinary.com/blog/cookies-on-rails)
