Rails relies on signed sessions to keep track of logged-in users. Since Rails 5.2, those sessions use AES GCM for authenticated encryption. They are secure, but there is a limitation: you cannot invalidate them early. Once issued, a session remains valid until it expires. Some workarounds exist, like caching sessions you want to revoke, but there is no simple universal solution built into Rails.
Devise is the standard authentication library for Rails. It handles registration, login, and password changes. Devise needs a way to invalidate sessions when a user changes their password, but Rails does not make that easy. The trick is to combine existing data with the session so invalidation happens automatically.
When a user logs in, Devise stores in the session:
authenticatable_salt
, derived from the BCrypt hash)The code below illustrates how authenticatable_salt
is computed:
# A reliable way to expose the salt regardless of the implementation.
def authenticatable_salt
encrypted_password[0,29] if encrypted_password
end
Where encrypted_password
is the BCrypt hash of the password.
And we can see how the two values are added to the session in the following snippet:
def serialize_into_session(record)
[record.to_key, record.authenticatable_salt]
end
On each request, Devise verifies that the user is still authenticated by checking two things:
authenticatable_salt
matches the value stored in the database for that user.If the values match, the session is valid. If they do not match, the session is rejected.
def serialize_from_session(key, salt)
record = to_adapter.get(key)
record if record && record.authenticatable_salt == salt
end
When a user changes their password, the BCrypt hash changes, and so does authenticatable_salt
. Devise updates the salt in the current browser’s session. All other sessions for the same user, on other devices or browsers, still carry the old salt. Those sessions will fail the next check and are effectively invalidated.
authenticatable_salt
changes with the hash.This small design choice has some big consequences for both security and usability:
authenticatable_salt
, forging a valid session is not feasible.This design does not change how Rails sessions work. It accepts their constraints and then adds a small piece of data that ties session validity to password state. It is minimal, universal, and effective.
It is worth contrasting this with other approaches teams often take, and why they fall short:
Good security engineering often comes from using existing data in a smart way. Devise’s use of authenticatable_salt
turns a hard problem into a simple check that works everywhere.