Modeling your App's User Session

If you’ve been keeping an eye on your cookies, you may have noticed some recent changes GitHub has made to how we track your session. You shouldn’t notice any difference in session behavior (beyond the new ability to revoke sessions), but we’d like to explain what prompted the change.

Replay attacks on stateless session stores have been known and documented for quite some time in Rails and Django. Using signed cookies for sessions is still incredibly easy to use and scales to high traffic web apps. You just need to understand its limitations. When implementing authentication, simply storing a user ID in the session cookie leaves you open to replay attacks and provides no means for revocation.

The other option is to switch to persisted storage for sessions. Either using a database, memcache or redis. On a high traffic site this may be a performance concern since a session may be allocated even for anonymous browsing traffic. Another downside, there is no clear insight into these sessions. They are stored as serialized objects. So there’s no way to query the store to see if a user has any sessions. It’s all abstracted away by Rails.

After ruling out Rails’ built in method for DB backed sessions, we decided that the concept of user sessions ought to be treated as a first class domain concern. Something with a real application API we can query, test and extend with other app concerns.

The UserSession class is just a normal ActiveRecord class like any other. There’s no excess Rails abstraction layer between it. We’ve extended it with other concerns such as manual revocation, sudo mode tracking and data like IP and user agent to help users identify sessions on the active sessions page.

class UserSession < ActiveRecord::Base
  belongs_to :user

  before_validation :set_unique_key

  scope :active, lambda {
    { :conditions => ["accessed_at >= ? AND revoked_at == NULL", 2.weeks.ago] }
  }

  def self.authenticate(key)
    self.active.find_by_key(key)
  end

  def revoke!
    self.revoked_at = Time.now
    save!
  end

  def sudo?
    sudo_enabled_at > 1.hour.ago
  end

  def sudo!
    self.sudo_enabled_at = Time.now
    save!
  end

  def access(request)
    self.accessed_at = Time.now
    self.ip          = request.ip
    self.user_agent  = request.user_agent
    save
  end

  private
    def set_unique_key
      self.key = SecureRandom.urlsafe_base64(32)
    end
end

Staying true to the restful authentication spirit, SessionsController#create creates a new UserSession and SessionsController#destroy deletes it.

A separate cookie called user_session is set referencing the record unique random key. Only signed in users allocate this record. Anonymous traffic to GitHub never creates junk data in our sessions table.

We still have our signed cookie store around as session in our controllers. This handles non-sensitive data like flash notices and multi-step form state. Then we have a separate user_session helper that references the current user’s session record.

This infrastructure change took a few months. For a month, we ran both the old session code path on this new user session path at once. This allowed users to transition over to the new cookie without noticing.

Overall, we are pretty happy with the change. It has made our authentication logic much more clear and explicit. This opens up some new potential now that we have the data on the server.

Have feedback on this post? Let @github know on Twitter.
Need help or found a bug? Contact us.

Changelog

Subscribe

Discover new ways to build better

Try Marketplace apps free for 14 days

Learn more