How Devise Solves Session Invalidation in Rails

Published: 03 Sep 2025

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’s Clever Solution

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:

  • User ID
  • Part of the user’s hashed password (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
How the Check Works on Every Request

On each request, Devise verifies that the user is still authenticated by checking two things:

  1. The user ID from the session identifies a valid user.
  2. The session’s 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
What Happens When the Password Changes

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.

  • Password changes → BCrypt hash changes.
  • authenticatable_salt changes with the hash.
  • Current session is updated; all other sessions become invalid on their next request.
Security Implications

This small design choice has some big consequences for both security and usability:

  • Automatic revocation across devices: No central session blocklist is required. The mismatch on the next request logs out stale sessions.
  • Forgery resistance: Even if an attacker knows the secret used to protect sessions, they still do not know the user’s hashed password. Without the correct authenticatable_salt, forging a valid session is not feasible.
Why This Is Clever

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.

Common Alternatives and Pitfalls

It is worth contrasting this with other approaches teams often take, and why they fall short:

  • Per-session blocklists: Possible, but adds complexity and storage overhead. You have to track, expire, and check a denylist on each request.
  • Short session lifetimes: Reduces risk but harms user experience and does not offer immediate invalidation.
Takeaway

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.

Photo of Louis Nyffenegger
Written by Louis Nyffenegger
Founder and CEO @PentesterLab