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.
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
Here is a structure of signed encrypted Rails session cookie:
1 2 3 4 5 6 7
So to decode a Rails session we need to defeat two beasts:
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.
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
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
As you might have noticed from the code above, the number of iterations is a constant in Rails 4 and it is set to
The key size is also a constant for now and should be set to
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
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
Here is where the real beast lives. It took me about 2 weeks to implement unmarshalling of Ruby objects serialized with
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
The code above will exit with an error
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
"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
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
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!