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…
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.
Hybrid Cookie Store / DB approach
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.
Written by
Related posts
Apply now for GitHub Universe 2023 micro-mentoring
As part of our ongoing commitment to accelerate human progress through Social Impact initiatives, we’re offering students 30-minute, 1:1 micro-mentoring sessions with GitHub employees ahead of Universe.
The 2023 Open Source Program Office (OSPO) Survey is live!
Help quantify the state of enterprise open source by taking the 2023 OSPO survey.
Godot 4.0 Release Party 🎉
We are delighted to host the Godot 4.0 Release Party at GitHub HQ on Wednesday, March 22 from 6:30 pm to 9:30 pm. And you’re invited!