As lately is happening to me a lot, Ruby ecosystem has lots of tutorials and guides that range from beginner to intermediate, but lacks more advanced topics. Recently I had to implement a security feature that surprisingly wasn't present at Rails: Session invalidation when you change your password.
Many sites, CartoDB included, use Rails CookieStore, which is just cookie based session handling: You securely serialize and deserialize session data (usually the user identifier) and avoid storing sessions serverside. Really cool in theory but has a flaw: If there is no serverside session management, how do I signal a password change so the other cookies with my session for example at other browsers become invalid?
Reading the official Ruby on Rails Security Guide I hoped to find the answer, but no, instead it lists lots of security hardening points, but just recommends to make your session expire, use a general secret_key (but changing it would invalidate all sessions, not just a given user ones) and in the end to go for database-based session handling for proper security. Well, I agree it is better, but sometimes you cannot adopt some changes as easy as they seem, so... what about improving CookieStore?
First I went deep, checking CookieStore and its "mixin parent" AbstractStore source codes. They just wrap actual session handling on storing at a cookie, but the parent had an interesting method, generate_sid (session Id). Maybe if I could change the generation of the session would be enough... so I also checked Rack::Session::Abstract::ID, the parent of all stores. I did some tests inheriting from CookieStore (as I don't fancy monkey patching even if Rack's code suggests it) but quickly I found that when you are generating a sid, really you don't have context of "users".. and you shouldn't, because this is really inside. This is for people desiring to modify the session id generation algorithm, or the actual storage of session data.
So, I went up, because over Rails we use Warden to ease all authentication (we have user/pass, API key, OAuth...). Digging into its wiki I found that you can have more session data than just the user id that you deserialize into a full User object upon retrieving an existing session. But that example wasn't enough, as it only worked playing with default session scopes. We use scope-based sessions because our usernames are unique and cannot be repeated, so for example I can have a session cookie with the scope "kartones" and another with the scope "test" (or different roles, or other ideas you might have).
Cheking more about Warden, I found some interesting callbacks, but again the examples were silly and not too useful, so as usually happens with Ruby, it is better to again check the source code to see the internals. And inside hooks.rb I found the answer, in the documentation block of after_set_user. There, I could filter to handling authentications and store additional session data at Warden initializer file... something that if your password changes changes too, e.g.:
Warden::Manager.after_set_user except: :fetch do |user, auth, opts|
auth.session(opts[:scope])[:sec_token] = Digest::SHA1.hexdigest(user.crypted_password)
Now, editing the traditional Rails base ApplicationController I can add some methods to handle this additiona data:
warden.session(user.username)[:sec_token] = Digest::SHA1.hexdigest(user.crypted_password)
warden.session(user.username)[:sec_token] == Digest::SHA1.hexdigest(user.crypted_password)
def validate_session(user = current_user, reset_session_on_error = true)
reset_session if reset_session_on_error
And then just add the new logic to the authentication endpoints, for example:
is_auth = authenticated?(CartoDB.extract_subdomain(request))
is_auth ? validate_session(current_user) : not_authorized
Now it would only remain to call update_session_security_token upon a password change, and all other cookie sessions will become invalid.
Why this is not an option either at Rails or Warden, I don't know, but I couldn't find a single tutorial, post or message detailing all this info, so let's hope this post helps fix that.