Open-source project
tuwukee/jwt_sessions avatar
tuwukee/jwt_sessions

jwt_sessions: stateful JWT sessions for single page applications

XSS/CSRF safe JWT auth designed for SPA

594 stars53 forksRubyMIT

At a glance

What is it?
The jwt_sessions gem pairs an access token and a refresh token and keeps the session on the server, in Redis by default, which is what makes logout, password resets and refresh token hijack protection possible at all. It is framework agnostic, with Rails integration available out of the box.
Who is it for?
Adopt jwt_sessions if you run a Ruby API behind a single page application and you want the operational benefits of a server side session, real logout, revocation and refresh token hijack protection, without giving up JWTs for your API calls. Do not adopt it if you want a stateless setup with no shared store, since the whole model depends on one, and note that the gem must share its signing key with anything else that verifies the tokens.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 9 days ago.
What is it written in?
Mainly Ruby, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.

Editorial analysis

Stateful sessions, which is the whole point

The primary goal of the gem is stated as configurable, manageable and safe stateful sessions based on JSON Web Tokens, and the word stateful is the one to hold on to. The gem stores the JWT based sessions on the backend, with Redis and memory stores supported, and the README says this makes it possible to manage sessions, reset passwords and log out users in a reliable and secure way. Those three capabilities are precisely what a purely stateless JWT setup gives up, so the design is a deliberate trade of server state for control. It is described as framework agnostic yet easily integrable, with Rails integration available out of the box. The intended audience is a single page application talking to a Ruby API, and the topic tags name access token, refresh token and CSRF token, which is the vocabulary the rest of the README uses.

Access and refresh tokens, and a store that polices them

The core concept is that each session is represented by a pair of tokens, access and refresh. The access token is used to retrieve secure resources, and the refresh token is used to renew the access token once it has expired, with the general expectation that the refresh token lives longer than the access token. What makes this more than a token pair is the session store, and the README assigns it two jobs, handling CSRF checks and preventing refresh token hijacking. The hijack protection follows from the stateful model. Because the server knows which refresh token belongs to which session, a stolen refresh token that does not match what the server has on record can be rejected, and the session can be invalidated the moment the legitimate client comes back. Token encoding and decoding are handled by the ruby-jwt gem, whose reserved claim names are supported, and jwt_sessions itself uses the ext claim and HS256 signing by default.

Install and set the signing key

Installation is a Gemfile line and a bundle:

ruby
gem "jwt_sessions"
bash
bundle install

The first thing to configure is the algorithm and the signing key, and the default algorithm is HS256:

ruby
JWTSessions.signing_key = "secret"

That single line is where the security of your deployment begins, since with HS256 the same secret signs and verifies. A shared signing key across several services is a common setup, and it is also a common source of trouble when one service rotates it and the others do not, because tokens signed with the old key stop verifying. The README notes that the gem relies on ruby-jwt for the cryptography, which is why the list of supported signing algorithms and claim checks is the one that library provides.

The Authorization mixin decides where the token comes from

The Authorization mixin provides the helper methods that retrieve the access and refresh tokens from incoming requests and verify the CSRF token if needed. Its assumption is that a token can be found either in a cookie or in a header, with both names configurable, and the order matters, because it tries to retrieve the token from headers first and then from cookies, with the CSRF check included, if the header check fails. For a single page application that distinction is the security model in one sentence. A token sent in the Authorization header is not attached by the browser automatically, so a cross site request cannot make the browser attach it, and the CSRF check is not needed for that path. A token in a cookie is attached automatically, so there the CSRF token has to do the work, and that is where the CSRF token from the login response comes into play.

Creating a session, and what login returns

Each token carries a payload with the session info, and the payload is a regular Ruby hash that usually holds a user id or other identifying data, though it can be an empty hash. The session is then created with that payload, and by default the same payload is sewn into both the access and the refresh token:

ruby
> session = JWTSessions::Session.new(payload: payload)

Sometimes it makes sense to keep different data in the two payloads. The README gives the reason, that the access token may contain rich data including user settings, while the refresh token should include only the bare minimum needed to reconstruct a payload for the new access token during a refresh, and that is done with the refresh_payload option. Calling login on the session returns the token set:

ruby
> session.login
=> {:csrf=>"BmhxDRW5NAEIx...",
    :access=>"eyJhbGciOiJIUzI1NiJ9...",
    :access_expires_at=>"..."
    :refresh=>"eyJhbGciOiJIUzI1NiJ9...",
    :refresh_expires_at=>"..."}

The expiration times are inside the token payloads automatically, and they are also returned in the hash in case you need them. Refreshing is a single call that takes the refresh token and returns a new CSRF token, a new access token and its expiry:

ruby
> session.refresh(refresh_token)

The session options are where the policy lives

The list of options on JWTSessions::Session.new is long, and it is effectively the configuration surface for your auth system. access_claims and refresh_claims take hashes of JWT claims that are validated within the respective payloads, and the README gives the example of a payload with an audience of admin and verify_aud set to true, meaning the token can only be used by that audience, with the endpoint able to validate the claims automatically through the token_claims method. The namespace option is a string that groups sessions by a custom criterion, and the README suggests grouping by user id, which makes it possible to log a user out of all devices. refresh_by_access_allowed is a boolean, false by default, that links the access and refresh tokens by adding the refresh token id to the access payload, so a refresh can be performed with the last expired access token. access_exp and refresh_exp override the global expiration settings per session, in seconds.

CSRF, sessions namespace and flushing sessions

The README table of contents points to a configuration section with named subsections for the token store, the JWT signature, the request headers and cookies names, the expiration time, the exceptions, CSRF and cookies, refresh with access token, and refresh token hijack protection, followed by a section on flushing sessions with a sessions namespace subsection and a logout subsection. That structure tells you the design intent, because CSRF is treated as a property of the cookie transport rather than an add-on, and the namespace and flushing sections exist so that sessions can be enumerated and revoked server side. The sessions namespace is what makes a global logout possible, since a namespace groups the sessions of one user and the store can then drop them all. The last push was on 2026-10-13, the project is on main, there are no GitHub releases, and the licence is MIT, with the source under lib/ and the tests under test/ driven by a Rakefile.

Editorial conclusion

Adopt jwt_sessions if you run a Ruby API behind a single page application and you want the operational benefits of a server side session, real logout, revocation and refresh token hijack protection, without giving up JWTs for your API calls. Do not adopt it if you want a stateless setup with no shared store, since the whole model depends on one, and note that the gem must share its signing key with anything else that verifies the tokens. Verify first that your Redis instance is reachable and that you pick an access expiry and a refresh expiry deliberately, remember that the Authorization mixin reads headers before cookies so an XSS exposed token in local storage still bypasses the CSRF check, and set the session namespace if you need to log a user out of all devices at once.

Frequently asked questions

What does the jwt_sessions gem do?

It provides configurable, manageable and safe stateful sessions based on JSON Web Tokens, storing the sessions on the backend with Redis and memory stores supported. The README says this makes it possible to manage sessions, reset passwords and log out users reliably.

How are the access and refresh tokens used?

Each session is a pair of tokens. The access token retrieves secure resources, the refresh token renews the access token after it expires, and the session store handles the CSRF check and refresh token hijack protection. Both have configurable expiration times, with the refresh token normally the longer lived.

How do I install and configure jwt_sessions?

Add gem "jwt_sessions" to the Gemfile and run bundle install, then set the signing key, for example JWTSessions.signing_key = "secret". The default signing algorithm is HS256, and encoding and decoding are handled by the ruby-jwt gem.

Where does jwt_sessions look for the token in a request?

The Authorization mixin tries the request headers first and then the cookies, with the CSRF check included, and both the header and cookie names are configurable. That is why a token sent in the Authorization header needs no CSRF token while a cookie based one does.

Can I log a user out of all their devices with jwt_sessions?

Yes, by using the session namespace option, which groups sessions by a criterion such as the user id, so a namespace can be flushed. The README also has a session options list covering access_claims, refresh_claims, namespace, refresh_by_access_allowed, access_exp and refresh_exp.

Official sources

  1. Issues
  2. License: MIT
  3. README
  4. tuwukee/jwt_sessions on GitHub
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/tuwukee-jwt-sessions.svg)](https://hysenlabs.com/projects/tuwukee-jwt-sessions)

Was this useful?