jwt_sessions Ruby Gem

repository·main·Indexed 20 days ago

https://github.com/tuwukee/jwt_sessions

A Ruby gem providing secure, stateful JWT-based authentication for Single Page Applications (SPAs). It implements a dual-token (access and refresh) pattern with backend storage supporting Redis and memory to mitigate CSRF and XSS risks. The library is framework-agnostic with built-in Rails integration, offering features such as configurable signing algorithms (HS256, RS256), namespace-based session management, and CSRF protection.

Tokens
14.1K
Snippets
50
Records
57
Agent score
68%

What's inside jwt_sessions

  1. Overview of jwt_sessions capabilities

    main

    jwt_sessions provides configurable, manageable, and safe stateful sessions based on JSON Web Tokens (JWT).

    Key features include:

    • Backend Session Storage: Stores session state in Redis or memory to enable reliable logout and session management.
    • Framework Agnostic: Can be used in any Ruby application, with built-in integration for Rails.
    • JWT Standard Compliance: Uses the ruby-jwt gem for encoding/decoding, supporting standard claims and various cryptographic signing algorithms. By default, it uses the ext claim and HS256 signing.
  2. Handle CSRF Protection

    main

    When using cookies for token transport, the gem provides CSRF protection. The login and refresh methods of JWTSessions::Session return a :csrf token.

    Usage:

    1. The client must send this CSRF token in a header (defaulting to X-CSRF-Token) for all non-GET/HEAD requests.
    2. The Authorization mixin automatically verifies this token against the one stored in the session.
    3. If a mismatch occurs, JWTSessions::Errors::Unauthorized is raised.

    You can manually generate a masked token using session.masked_csrf(access_token) to mitigate BREACH attacks.

    # Example: Rendering CSRF in a Rails controller
    render json: { csrf: tokens[:csrf] }
  3. How jwt_sessions works: Access and Refresh tokens

    main

    The core concept of jwt_sessions is a dual-token system where each session is represented by a pair of tokens:

    1. Access Token: Used to retrieve secure resources. It typically has a shorter lifespan.
    2. Refresh Token: Used to renew the access token once it has expired. It typically has a longer lifespan.

    To ensure security (XSS/CSRF protection) and allow for reliable session management (like logging out users or resetting passwords), the gem uses a backend session store (supporting Redis and memory) to handle CSRF checks and prevent refresh token hijacking.

  4. Use Authorization mixin in Non-Rails applications

    main

    For non-Rails frameworks (like Sinatra), include JWTSessions::Authorization in your class and implement three required methods to allow the gem to extract request data:

    1. request_headers: Must return a hash-like object of request headers.
    2. request_cookies: Must return a hash-like object of request cookies.
    3. request_method: Must return the current request verb as an uppercase string (e.g., 'GET', 'POST').
    class SimpleApp < Sinatra::Base
      include JWTSessions::Authorization
    
      def request_headers
        env.inject({}) { |acc, (k,v)| acc[$1.downcase] = v if k =~ /^http_(.*)/i; acc }
      end
    
      def request_cookies
        request.cookies
      end
    
      def request_method
        request.request_method
      end
    end
  5. Configure JWTSessions signing keys and algorithms

    main

    Before using the gem, you must configure a signing algorithm and a key. By default, it uses HS256. For HMAC algorithms, you can use the signing_key shortcut. For asymmetric algorithms like RS256, you must specify both private_key and public_key.

    # For HMAC (e.g., HS256)
    JWTSessions.algorithm = "HS256"
    JWTSessions.signing_key = "secret"
    
    # For RSA (e.g., RS256)
    JWTSessions.algorithm   = "RS256"
    JWTSessions.private_key = OpenSSL::PKey::RSA.generate(2048)
    JWTSessions.public_key  = JWTSessions.private_key.public_key
  6. Use a mixed approach for Access Tokens and Refresh Tokens

    main

    You can implement a security strategy where the access token is stored in localStorage on the client side, while the refresh token is stored in an HTTP-only secure cookie. This prevents the refresh token from being accessible via JavaScript, mitigating XSS risks.

    In a Rails controller, you achieve this by setting the cookie using JWTSessions.refresh_cookie and rendering the access token and CSRF token in the JSON response body.

    class LoginController < ApplicationController
      def create
        user = User.find_by(email: params[:email])
        if user&.authenticate(params[:password])
    
          payload = { user_id: user.id, role: user.role, permissions: user.permissions }
          refresh_payload = { user_id: user.id }
          session = JWTSessions::Session.new(payload: payload, refresh_payload: refresh_payload)
          tokens = session.login
    
          # Set the refresh token in an HTTP-only cookie
          response.set_cookie(JWTSessions.refresh_cookie,
                              value: tokens[:refresh],
                              httponly: true,
                              secure: Rails.env.production?)
    
          # Return access and csrf tokens in the JSON body
          render json: { access: tokens[:access], csrf: tokens[:csrf] }
        else
          render json: "Cannot login", status: :unauthorized
        end
      end
    end
  7. Refresh Sessions using Access Tokens

    main

    To avoid storing refresh tokens on web/JS clients, you can enable refresh_by_access_allowed: true when creating a session. This links the access token to its corresponding refresh token.

    Workflow:

    1. Login: Create a session with refresh_by_access_allowed: true.
    2. Refresh: Use session.refresh_by_access_payload to get new tokens using only the access token.
    3. Security: Use the before_action :authorize_refresh_by_access_request! to protect the refresh endpoint.
    4. Expiration: When the access token is expired, use claimless_payload to bypass expiration validation during the refresh process.

    Prohibiting premature refresh: You can pass a block to refresh_by_access_payload. The block is executed if the refresh is attempted before the access token has expired. You can use this to raise an error and prevent users from refreshing while the access token is still valid.

    # Refreshing with an expired access token
    class RefreshController < ApplicationController
      before_action :authorize_refresh_by_access_request!
    
      def create
        # Use claimless_payload to skip expiration validation
        session = JWTSessions::Session.new(payload: claimless_payload, refresh_by_access_allowed: true)
        tokens  = session.refresh_by_access_payload
        
        response.set_cookie(JWTSessions.access_cookie, value: tokens[:access], httponly: true)
        render json: { csrf: tokens[:csrf] }
      end
    end
    
    # Prohibiting refresh before expiration
    tokens = session.refresh_by_access_payload do
      raise JWTSessions::Errors::Unauthorized, "Refresh action is performed before the expiration of the access token."
    end
  8. Integrate JWTSessions with Rails

    main

    To use the gem in a Rails API:

    1. Include JWTSessions::RailsAuthorization in your controllers.
    2. Handle JWTSessions::Errors::Unauthorized exceptions (e.g., via rescue_from).
    3. Use before_action :authorize_access_request! to protect routes requiring an access token.
    4. Use before_action :authorize_refresh_request! to protect routes requiring a refresh token.

    Access tokens are expected in the Authorization: Bearer <token> header. Refresh tokens are expected in a configurable header (defaulting to X-Refresh-Token).

    class ApplicationController < ActionController::API
      include JWTSessions::RailsAuthorization
      rescue_from JWTSessions::Errors::Unauthorized, with: :not_authorized
    
      private
    
      def not_authorized
        render json: { error: "Not authorized" }, status: :unauthorized
      end
    end
    
    class UsersController < ApplicationController
      before_action :authorize_access_request!
    
      def index
        # Access the decoded payload via the `payload` method
        user_id = payload["user_id"]
        # ...
      end
    end
  9. Install libsodium for development/testing

    main

    The library requires RbNaCl and the sodium cryptographic library for cryptographic operations. If you are developing on MacOS and need to install the required system dependency, you can use Homebrew.

    brew install libsodium
  10. Configure JWT Claims and Verification

    main

    By default, the gem only verifies the exp (expiration) claim. You can enable verification for other claims globally via JWTSessions.jwt_options.

    Global Options:

    • verify_iss: Verify Issuer.
    • verify_sub: Verify Subject.
    • verify_iat: Verify Issued At.
    • verify_aud: Verify Audience.
    • leeway: Global clock skew allowance in seconds.

    Controller-level Overrides: To pass specific claims (like sub, aud, iss) or custom leeways for a specific request, implement a token_claims method in your controller.

    # Global configuration
    JWTSessions.jwt_options[:verify_iss] = true
    JWTSessions.jwt_options[:leeway]     = 30
    
    # Controller-level override
    class UsersController < ApplicationController
      def token_claims
        {
          aud: ["admin", "staff"],
          verify_aud: true,
          exp_leeway: 15
        }
      end
    end
  11. Configure JWTSessions::Session options

    main

    When initializing JWTSessions::Session.new, the following options are available:

    • payload: Hash of session data for the access token. (Default: {})
    • refresh_payload: Hash of session data for the refresh token. (Default: same as payload)
    • access_claims: Hash of JWT claims to validate in the access token.
    • refresh_claims: Hash of JWT claims to validate in the refresh token.
    • namespace: String used to group sessions (e.g., by user ID) to enable features like global logout.
    • refresh_by_access_allowed: Boolean. If true, allows refreshing a session using the last expired access token.
    • access_exp: Integer. Access token expiration in seconds (overrides global settings).
    • refresh_exp: Integer. Refresh token expiration in seconds (overrides global settings).