Handling Rails 4 Sessions With Go

Alright guys, as you all already know, Rails is dead or at least dying. It’s slow, it’s vulnerable and it does not scale, which is a huge disadvantage for your next gen social dating service with geolocation and badges. Today we will play rats abandoning a sinking ship of Ruby on Rails and learn how to handle a session created by a Rails application. I’m going to talk about web apps that use Rails 4.0.0 or greater, because even my grandmother can fake an authorization stored in a Base64 encoded cookie (and remember, if you don’t keep your app up-to-date, you will be eaten) very soon.

TL;DR

With gorails/session and gorails/marshal you can access session variables set within the Rails 4 app. The access is read-only for now, but this is enough to e.g. handle the authorization made by the devise gem. See README for details.

Rails 4.0 Session Cookie

Doing something just for the sake of being educated is boring. Better let’s take a real world problem and learn something while solving it. A good reason to want to handle a Rails session is to handle an authentication made by an industry standard devise gem. Actually, the same method can be applied to any authentication gem that uses session cookie as a storage.

As I mentioned before, prior to Rails 4 session data was stored as a Base64 encoded Ruby object. Starting from version 4.0.0 Rails encrypts and signs this object before saving. Both sign and encrypt secrets are generated from secret_key_base string which could be found in config/initializers/secret_token.rb of your Rails application.

Decoding a session cookie with Ruby is pretty straightforward:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def decrypt_session_cookie(cookie)
  cookie = CGI.unescape(cookie)
  config = Rails.application.config

  encrypted_cookie_salt = config.action_dispatch.encrypted_cookie_salt               # "encrypted cookie" by default
  encrypted_signed_cookie_salt = config.action_dispatch.encrypted_signed_cookie_salt # "signed encrypted cookie" by default

  key_generator = ActiveSupport::KeyGenerator.new(config.secret_key_base, iterations: 1000)
  secret = key_generator.generate_key(encrypted_cookie_salt)
  sign_secret = key_generator.generate_key(encrypted_signed_cookie_salt)

  encryptor = ActiveSupport::MessageEncryptor.new(secret, sign_secret)
  encryptor.decrypt_and_verify(cookie)
end

Here is a structure of signed encrypted Rails session cookie:

1
2
3
4
5
6
7
+--------------------------- URI Encode --------------------------+
| +--------------------------- base64 -------+------+-----------+ |
| | +---------------- base64 --------------+ |      |           | |
| | | encrypted data | "--" | init. vector | | "--" | signature | |
| | +----------------+------+--------------+ |      |           | |
| +------------------------------------------+------+-----------+ |
+-----------------------------------------------------------------+

So to decode a Rails session we need to defeat two beasts:

  1. Encryption
  2. Marshalling

This is the most tricky part, because Rails uses Ruby Marshal format to serialize a session object. As a paranoid developer, you might also like to verify the signature, but I will leave this as a homework.

Decrypt

We’re about to implement the part of ActiveSupport that encrypts serialized session object in Go.

By default Rails uses a cipher-block chaining mode of AES-256 encryption algorithm. Luckily Google did most of the dirty job for us. The package crypto/aes is available out of the box and provides various implementations of the AES algorithm.

Rails uses standard PBKDF2 with SHA-1 as a hash function to generate the secret. Again, Google already took care of it. All you need to do is

1
go get code.google.com/p/go.crypto/pbkdf2

As you might have noticed from the code above, the number of iterations is a constant in Rails 4 and it is set to 1000. The key size is also a constant for now and should be set to 64.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import (
  "code.google.com/p/go.crypto/pbkdf2"
  "crypto/sha1"
)

const (
  keyIterNum = 1000
  keySize    = 64
)

func GenerateSecret(secretKeyBase, salt string) []byte {
  return pbkdf2.Key(
    []byte(secretKeyBase),
    []byte(salt),
    keyIterNum,
    keySize,
    sha1.New,
  )
}

All you need to do is create a crypto.NewCBCDecrypter using first 32 bytes (guess why? hint: the 256 in AES-256 is the key size in bits) of generated secret, initialize it with the given initialization vector and decode the session data. Easy!

Not really? Then you might like to use our gorails/session package, which does all this stuff for you:

1
2
3
4
5
6
7
8
9
10
11
import "github.com/adeven/gorails/session"

const (
  secretKeyBase = "..."              // can be found in config/initializers/secret_token.rb
  salt          = "encrypted cookie" // default value for Rails 4 app
)

// sessionCookie - raw _<your app name>_session cookie, i.e. for MyRailsApp that will be _my_rails_app_session
func GetRailsSessionData(sessionCookie string) (decryptedCookieData []byte, err error) {
  return session.DecryptSignedCookie(sessionCookie, secretKeyBase, salt)
}

Unmarshal

Here is where the real beast lives. It took me about 2 weeks to implement unmarshalling of Ruby objects serialized with the Marshal module. Thanks Jake Goulding for detailed description of Marshal format.

Omitting details, Ruby’s Marshal package represents an object as a byte sequence where a value is prefixed with a 1 byte header that describes its type, e.g. 'i' denotes that the following sequence should be treated as an integer. The size of strings, arrays and maps goes after the type header and a string has a suffix describing its encoding. Serialization is performed recursively, i.e. to serialize an array you need to serialize each of its elements and then prepend the result with a header that describes the type and size. Maps (or Hashes in the Ruby world) are being serialized as arrays of key-value pairs.

Our implementation of marshal.MarshalledObject uses the Maybe monad to perform the type assertion:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import (
  "github.com/adeven/gorails/marshal"
  "log"
)

const (
  serializedStringObject = "..." // Serialized string
)

func main() {
  intValue, err := marshal.CreateMarshalledObject(serializedStringObject).GetAsInteger()
  if err != nil {
    log.Panic(err)
  }

  log.Printf("unmarshalled int value: %d", intValue)
}

The code above will exit with an error

1
"gorails/marshal: an attempt to implicitly typecast a marshalled object"

since we tried to treat a string as an integer.

Remember why do we started all this? Right, given a session cookie we wanted to figure out if the user has been authorized and authenticate him/her in our Go app. To do this we need to know how devise stores the authorized user. The easies way is to take a cookie for one of your Rails apps that uses devise and decrypt it as shown above. Most likely you will get something like this:

1
2
3
4
5
{
  "session_id"           => "974561a18fc4aa7e44a19240647abaf4e",
  "warden.user.user.key" => [[1], "$2a$10$QOxmmQkm8i8WJ85TE9.cZO"],
  "_csrf_token"          => "jtbd2pGvI1AMIM3KqesbjU1K4wv3blFLp2tert3D8sc="
}

Here "warden.user.user.key" (this key may vary depending on the name of your User model) contains an array, which has an id of authorized user as a first element. Here is how it might look like written in Go using gorails/marshal:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import (
  "errors"
  "github.com/adeven/gorails/marshal"
)

func GetAuthUserId(decryptedSessionData []byte) (userId int64, err error) {
  unauthorizedUser := errors.New("Unauthorized user")
  invalidAuthData := errors.New("Invalid auth data")

  sessionData, err := marshal.CreateMarshalledObject(decryptedSessionData).GetAsMap()
  if err != nil {
    return
  }

  wardenData, ok := sessionData["warden.user.user.key"]
  if !ok {
    return 0, unauthorizedUser
  }

  wardenUserKey, err := wardenData.GetAsArray()
  if err != nil {
    return
  }
  if len(wardenUserKey) < 1 {
    return 0, invalidAuthData
  }

  userData, err := wardenUserKey[0].GetAsArray()
  if err != nil {
    return
  }
  if len(userData) < 1 {
    return 0, invalidAuthData
  }

  userId, err = userData[0].GetAsInteger()
  return
}

This function takes the decrypted session data and returns an id or an error when there is no user authorized, which means that nothing can stop you from rewriting your JSON API in Go :)

Have fun and take care!

Comments