{"id":13512317,"url":"https://github.com/tuwukee/jwt_sessions","last_synced_at":"2025-05-15T04:02:40.358Z","repository":{"id":31896367,"uuid":"124354168","full_name":"tuwukee/jwt_sessions","owner":"tuwukee","description":"XSS/CSRF safe JWT auth designed for SPA","archived":false,"fork":false,"pushed_at":"2024-09-21T09:27:56.000Z","size":311,"stargazers_count":596,"open_issues_count":6,"forks_count":55,"subscribers_count":11,"default_branch":"main","last_synced_at":"2025-05-13T06:11:11.917Z","etag":null,"topics":["access-token","api","auth","csrf-token","jwt","refresh-token","ruby"],"latest_commit_sha":null,"homepage":null,"language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/tuwukee.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2018-03-08T07:31:08.000Z","updated_at":"2025-05-11T19:26:45.000Z","dependencies_parsed_at":"2024-06-18T20:10:28.656Z","dependency_job_id":"7c283be3-655a-4781-92e4-1b4a4c6d66b0","html_url":"https://github.com/tuwukee/jwt_sessions","commit_stats":{"total_commits":197,"total_committers":17,"mean_commits":"11.588235294117647","dds":"0.12182741116751272","last_synced_commit":"ea829dc64e5e1776e10fca32438cee389feb8ebb"},"previous_names":[],"tags_count":9,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tuwukee%2Fjwt_sessions","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tuwukee%2Fjwt_sessions/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tuwukee%2Fjwt_sessions/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tuwukee%2Fjwt_sessions/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/tuwukee","download_url":"https://codeload.github.com/tuwukee/jwt_sessions/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":254270640,"owners_count":22042858,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["access-token","api","auth","csrf-token","jwt","refresh-token","ruby"],"created_at":"2024-08-01T03:01:43.497Z","updated_at":"2025-05-15T04:02:40.260Z","avatar_url":"https://github.com/tuwukee.png","language":"Ruby","readme":"# jwt_sessions\n[![Gem Version](https://badge.fury.io/rb/jwt_sessions.svg)](https://badge.fury.io/rb/jwt_sessions)\n[![Maintainability](https://api.codeclimate.com/v1/badges/53de11b8334933b1c0ef/maintainability)](https://codeclimate.com/github/tuwukee/jwt_sessions/maintainability)\n[![Codacy Badge](https://app.codacy.com/project/badge/Grade/6ba02043e42144a9af96c9675207a5c4)](https://www.codacy.com/gh/tuwukee/jwt_sessions/dashboard?utm_source=github.com\u0026amp;utm_medium=referral\u0026amp;utm_content=tuwukee/jwt_sessions\u0026amp;utm_campaign=Badge_Grade)\n[![Build Status](https://travis-ci.org/tuwukee/jwt_sessions.svg?branch=master)](https://travis-ci.org/tuwukee/jwt_sessions)\n\nXSS/CSRF safe JWT auth designed for SPA\n\n## Table of Contents\n\n  - [Synopsis](#synopsis)\n  - [Installation](#installation)\n  - [Getting Started](#getting-started)\n    - [Creating a session](#creating-a-session)\n    - [Rails integration](#rails-integration)\n    - [Non-Rails usage](#non-rails-usage)\n  - [Configuration](#configuration)\n      - [Token store](#token-store)\n      - [JWT signature](#jwt-signature)\n      - [Request headers and cookies names](#request-headers-and-cookies-names)\n      - [Expiration time](#expiration-time)\n      - [Exceptions](#exceptions)\n      - [CSRF and cookies](#csrf-and-cookies)\n        - [Refresh with access token](#refresh-with-access-token)\n      - [Refresh token hijack protection](#refresh-token-hijack-protection)\n  - [Flush Sessions](#flush-sessions)\n        - [Sessions namespace](#sessions-namespace)\n        - [Logout](#logout)\n  - [Examples](#examples)\n  - [Contributing](#contributing)\n  - [License](#license)\n\n## Synopsis\n\nThe primary goal of this gem is to provide configurable, manageable, and safe stateful sessions based on JSON Web Tokens.\n\nThe gem stores JWT based sessions on the backend (currently, Redis and memory stores are supported), making it possible to manage sessions, reset passwords and logout users in a reliable and secure way.\n\nIt is designed to be framework agnostic, yet easily integrable, and Rails integration is available out of the box.\n\nThe core concept behind `jwt_sessions` is that each session is represented by a pair of tokens: `access` and `refresh`. The session store is used to handle CSRF checks and prevent refresh token hijacking. Both tokens have configurable expiration times but in general the refresh token is supposed to have a longer lifespan than the access token. The access token is used to retrieve secure resources and the refresh token is used to renew the access token once it has expired. The default token store uses Redis.\n\nAll tokens are encoded and decoded by [ruby-jwt](https://github.com/jwt/ruby-jwt) gem. Its reserved claim names are supported and it can configure claim checks and cryptographic signing algorithms supported by it.\n`jwt_sessions` itself uses `ext` claim and `HS256` signing by default.\n\n\n## Installation\n\nPut this line in your Gemfile:\n\n```ruby\ngem \"jwt_sessions\"\n```\n\nThen run:\n\n```\nbundle install\n```\n\n## Getting Started\n\nYou should configure an algorithm and specify the signing key. By default the gem uses the `HS256` signing algorithm.\n\n```ruby\nJWTSessions.signing_key = \"secret\"\n```\n\n`Authorization` mixin provides helper methods which are used to retrieve the access and refresh tokens from incoming requests and verify the CSRF token if needed. It assumes that a token can be found either in a cookie or in a header (cookie and header names are configurable). It tries to retrieve the token from headers first and then from cookies (CSRF check included) if the header check fails.\n\n### Creating a session\n\nEach token contains a payload with custom session info. The payload is a regular Ruby hash. \\\nUsually, it contains a user ID or other data which help identify the current user but the payload can be an empty hash as well.\n\n```ruby\n\u003e payload = { user_id: user.id }\n=\u003e {:user_id=\u003e1}\n```\n\nGenerate the session with a custom payload. By default the same payload is sewn into the session's access and refresh tokens.\n\n```ruby\n\u003e session = JWTSessions::Session.new(payload: payload)\n=\u003e #\u003cJWTSessions::Session:0x00007fbe2cce9ea0...\u003e\n```\n\nSometimes it makes sense to keep different data within the payloads of the access and refresh tokens. \\\nThe access token may contain rich data including user settings, etc., while the appropriate refresh token will include only the bare minimum which will be required to reconstruct a payload for the new access token during refresh.\n\n```ruby\nsession = JWTSessions::Session.new(payload: payload, refresh_payload: refresh_payload)\n```\n\nNow we can call `login` method on the session to retrieve a set of tokens.\n\n```ruby\n\u003e session.login\n=\u003e {:csrf=\u003e\"BmhxDRW5NAEIx...\",\n    :access=\u003e\"eyJhbGciOiJIUzI1NiJ9...\",\n    :access_expires_at=\u003e\"...\"\n    :refresh=\u003e\"eyJhbGciOiJIUzI1NiJ9...\",\n    :refresh_expires_at=\u003e\"...\"}\n```\n\nAccess/refresh tokens automatically contain expiration time in their payload. Yet expiration times are also added to the output just in case. \\\nThe token's payload will be available in the controllers once the access (or refresh) token is authorized.\n\nTo perform the refresh do:\n\n```ruby\n\u003e session.refresh(refresh_token)\n=\u003e {:csrf=\u003e\"+pk2SQrXHRo1iV1x4O...\",\n    :access=\u003e\"eyJhbGciOiJIUzI1...\",\n    :access_expires_at=\u003e\"...\"}\n```\n\nAvailable `JWTSessions::Session.new` options:\n\n- **payload**: a hash object with session data which will be included into an access token payload. Default is an empty hash.\n- **refresh_payload**: a hash object with session data which will be included into a refresh token payload. Default is the value of the access payload.\n- **access_claims**: a hash object with [JWT claims](https://github.com/jwt/ruby-jwt#support-for-reserved-claim-names) which will be validated within the access token payload. For example, `JWTSessions::Session.new(payload: { user_id: 1, aud: ['admin'], verify_aud: true })` means that the token can be used only by \"admin\" audience. Also, the endpoint can automatically validate claims instead. See `token_claims` method.\n- **refresh_claims**: a hash object with [JWT claims](https://github.com/jwt/ruby-jwt#support-for-reserved-claim-names) which will be validated within the refresh token payload.\n- **namespace**: a string object which helps to group sessions by a custom criteria. For example, sessions can be grouped by user ID, making it possible to logout the user from all devices. More info [Sessions Namespace](#sessions-namespace).\n- **refresh_by_access_allowed**: a boolean value. Default is false. It links access and refresh tokens (adds refresh token ID to access payload), making it possible to perform a session refresh by the last expired access token. See [Refresh with access token](#refresh-with-access-token).\n- **access_exp**: an integer value. Contains an access token expiration time in seconds. The value overrides global settings. See [Expiration time](#expiration-time).\n- **refresh_exp**: an integer value. Contains a refresh token expiration time in seconds. The value overrides global settings. See [Expiration time](#expiration-time).\n\nHelper methods within `Authorization` mixin:\n\n- **authorize_access_request!**: validates access token within the request.\n- **authorize_refresh_request!**: validates refresh token within the request.\n- **found_token**: a raw token found within the request.\n- **payload**: a decoded token's payload. Returns an empty hash in case the token is absent in the request headers/cookies.\n- **claimless_payload**: a decoded token's payload without claims validation (can be used for checking data of an expired token).\n- **token_claims**: the method should be defined by a developer and is expected to return a hash-like object with claims to be validated within a token's payload.\n\n### Rails integration\n\nInclude `JWTSessions::RailsAuthorization` in your controllers and add `JWTSessions::Errors::Unauthorized` exception handling if needed.\n\n```ruby\nclass ApplicationController \u003c ActionController::API\n  include JWTSessions::RailsAuthorization\n  rescue_from JWTSessions::Errors::Unauthorized, with: :not_authorized\n\n  private\n\n  def not_authorized\n    render json: { error: \"Not authorized\" }, status: :unauthorized\n  end\nend\n```\n\nSpecify a signing key for JSON Web Tokens in `config/initializers/jwt_session.rb` \\\nIt is advisable to store the key itself in a secure way, f.e. within app credentials.\n\n```ruby\nJWTSessions.algorithm = \"HS256\"\nJWTSessions.signing_key = Rails.application.credentials.secret_jwt_signing_key\n```\n\nMost of the algorithms require private and public keys to sign a token. However, HMAC requires only a single key and you can use the `signing_key` shortcut to sign the token. For other algorithms you must specify private and public keys separately.\n\n```ruby\nJWTSessions.algorithm   = \"RS256\"\nJWTSessions.private_key = OpenSSL::PKey::RSA.generate(2048)\nJWTSessions.public_key  = JWTSessions.private_key.public_key\n```\n\nYou can build a login controller to receive access, refresh and CSRF tokens in exchange for the user's login/password. \\\nRefresh controller allows you to get a new access token using the refresh token after access is expired. \\\n\nHere is an example of a simple login controller, which returns a set of tokens as a plain JSON response. \\\nIt is also possible to set tokens as cookies in the response instead.\n\n```ruby\nclass LoginController \u003c ApplicationController\n  def create\n    user = User.find_by!(email: params[:email])\n    if user.authenticate(params[:password])\n      payload = { user_id: user.id }\n      session = JWTSessions::Session.new(payload: payload)\n      render json: session.login\n    else\n      render json: \"Invalid user\", status: :unauthorized\n    end\n  end\nend\n```\n\nNow you can build a refresh endpoint. To protect the endpoint use the before_action `authorize_refresh_request!`. \\\nThe endpoint itself should return a renewed access token.\n\n```ruby\nclass RefreshController \u003c ApplicationController\n  before_action :authorize_refresh_request!\n\n  def create\n    session = JWTSessions::Session.new(payload: access_payload)\n    render json: session.refresh(found_token)\n  end\n\n  def access_payload\n    # payload here stands for refresh token payload\n    build_access_payload_based_on_refresh(payload)\n  end\nend\n```\n\nIn the above example, `found_token` is a token fetched from request headers or cookies. In the context of `RefreshController` it is a refresh token. \\\nThe refresh request with headers must include `X-Refresh-Token` (header name is configurable) with the refresh token.\n\n```\nX-Refresh-Token: eyJhbGciOiJIUzI1NiJ9...\nPOST /refresh\n```\n\nWhen there are login and refresh endpoints, you can protect the rest of your secured controllers with `before_action :authorize_access_request!`.\n\n```ruby\nclass UsersController \u003c ApplicationController\n  before_action :authorize_access_request!\n\n  def index\n    ...\n  end\n\n  def show\n    ...\n  end\nend\n```\n\nHeaders must include `Authorization: Bearer` with access token.\n\n```\nAuthorization: Bearer eyJhbGciOiJIUzI1NiJ9...\nGET /users\n```\n\nThe `payload` method is available to fetch encoded data from the token.\n\n```ruby\ndef current_user\n  @current_user ||= User.find(payload[\"user_id\"])\nend\n```\n\nMethods `authorize_refresh_request!` and `authorize_access_request!` will always try to fetch the tokens from the headers first and then from the cookies.\nFor the cases when an endpoint must support only one specific token transport the following authorization methods can be used instead:\n\n```ruby\nauthorize_by_access_cookie!\nauthorize_by_access_header!\nauthorize_by_refresh_cookie!\nauthorize_by_refresh_header!\n```\n\n### Non-Rails usage\n\nYou must include `JWTSessions::Authorization` module to your auth class and within it implement the following methods:\n\n1. request_headers\n\n```ruby\ndef request_headers\n  # must return hash-like object with request headers\nend\n```\n\n2. request_cookies\n\n```ruby\ndef request_cookies\n  # must return hash-like object with request cookies\nend\n```\n\n3. request_method\n\n```ruby\ndef request_method\n  # must return current request verb as a string in upcase, f.e. 'GET', 'HEAD', 'POST', 'PATCH', etc\nend\n```\n\nExample Sinatra app. \\\nNOTE: Rack updates HTTP headers by using the `HTTP_` prefix, upcasing and underscores for the sake of simplicity. JWTSessions token header names are converted to the rack-style in this example.\n\n```ruby\nrequire \"sinatra/base\"\n\nJWTSessions.access_header = \"authorization\"\nJWTSessions.refresh_header = \"x_refresh_token\"\nJWTSessions.csrf_header = \"x_csrf_token\"\nJWTSessions.signing_key = \"secret key\"\n\nclass SimpleApp \u003c Sinatra::Base\n  include JWTSessions::Authorization\n\n  def request_headers\n    env.inject({}) { |acc, (k,v)| acc[$1.downcase] = v if k =~ /^http_(.*)/i; acc }\n  end\n\n  def request_cookies\n    request.cookies\n  end\n\n  def request_method\n    request.request_method\n  end\n\n  before do\n    content_type \"application/json\"\n  end\n\n  post \"/login\" do\n    access_payload = { key: \"access value\" }\n    refresh_payload = { key: \"refresh value\" }\n    session = JWTSessions::Session.new(payload: access_payload, refresh_payload: refresh_payload)\n    session.login.to_json\n  end\n\n  # POST /refresh\n  # x_refresh_token: ...\n  post \"/refresh\" do\n    authorize_refresh_request!\n    access_payload = { key: \"reloaded access value\" }\n    session = JWTSessions::Session.new(payload: access_payload, refresh_payload: payload)\n    session.refresh(found_token).to_json\n  end\n\n  # GET /payload\n  # authorization: Bearer ...\n  get \"/payload\" do\n    authorize_access_request!\n    payload.to_json\n  end\n\n  # ...\nend\n```\n\n## Configuration\n\nList of configurable settings with their default values.\n\n##### Token store\n\nIn order to configure a token store you should set up a store adapter in a following way: `JWTSessions.token_store = :redis, { redis_url: 'redis://127.0.0.1:6379/0' }` (options can be omitted). Currently supported stores are `:redis` and `:memory`. Please note, that if you want to use Redis as a store then you should have `redis-client` gem listed in your Gemfile. If you do not configure the adapter explicitly, this gem will try to load `redis-client` and use it. Otherwise it will fall back to a `memory` adapter.\n\nMemory store only accepts a `prefix` (used for Redis db keys). Here is a default configuration for Redis:\n\n```ruby\nJWTSessions.token_store = :redis, {\n  redis_host: \"127.0.0.1\",\n  redis_port: \"6379\",\n  redis_db_name: \"0\",\n  token_prefix: \"jwt_\",\n  pool_size: Integer(ENV.fetch(\"RAILS_MAX_THREADS\", 5))\n}\n```\nOn default `pool_size` is set to 5. Override it with the value of max number of parallel redis connections in your app.\n\nYou can also provide a Redis URL instead:\n\n```ruby\nJWTSessions.token_store = :redis, { redis_url: \"redis://localhost:6397\" }\n```\n\n**NOTE:** if `REDIS_URL` environment variable is set it is used automatically.\n\nSSL, timeout, reconnect, etc. redis settings are supported:\n```ruby\nJWTSessions.token_store = :redis, {\n  read_timeout: 1.5,\n  reconnect_attempts: 10,\n  ssl_params: { verify_mode: OpenSSL::SSL::VERIFY_NONE }\n}\n```\n\nIf you already have a configured Redis client, you can pass it among the options to reduce opened connections to a Redis server:\n\n```ruby\nJWTSessions.token_store = :redis, {redis_client: redis_pool}\n```\n\n##### JWT signature\n\n```ruby\nJWTSessions.algorithm = \"HS256\"\n```\n\nYou need to specify a secret to use for HMAC as this setting does not have a default value.\n\n```ruby\nJWTSessions.signing_key = \"secret\"\n```\n\nIf you are using another algorithm like RSA/ECDSA/EDDSA you should specify private and public keys.\n\n```ruby\nJWTSessions.private_key = \"abcd\"\nJWTSessions.public_key  = \"efjh\"\n```\n\nNOTE: ED25519 and HS512256 require `rbnacl` installation in order to make it work.\n\njwt_sessions only uses `exp` claim by default when it decodes tokens and you can specify which additional claims to use by\nsetting `jwt_options`. You can also specify leeway to account for clock skew.\n\n```ruby\nJWTSessions.jwt_options[:verify_iss] = true\nJWTSessions.jwt_options[:verify_sub] = true\nJWTSessions.jwt_options[:verify_iat] = true\nJWTSessions.jwt_options[:verify_aud] = true\nJWTSessions.jwt_options[:leeway]     = 30 # seconds\n```\n\nTo pass options like `sub`, `aud`, `iss`, or leeways you should specify a method called `token_claims` in your controller.\n\n```ruby\nclass UsersController \u003c ApplicationController\n  before_action :authorize_access_request!\n\n  def token_claims\n    {\n      aud: [\"admin\", \"staff\"],\n      verify_aud: true, # can be used locally instead of a global setting\n      exp_leeway: 15 # will be used instead of default leeway only for exp claim\n    }\n  end\nend\n```\n\nClaims are also supported by `JWTSessions::Session` and you can pass `access_claims` and `refresh_claims` options in the initializer.\n\n##### Request headers and cookies names\n\nDefault request headers/cookies names can be reconfigured.\n\n```ruby\nJWTSessions.access_header  = \"Authorization\"\nJWTSessions.access_cookie  = \"jwt_access\"\nJWTSessions.refresh_header = \"X-Refresh-Token\"\nJWTSessions.refresh_cookie = \"jwt_refresh\"\nJWTSessions.csrf_header    = \"X-CSRF-Token\"\n```\n\n##### Expiration time\n\nAccess token must have a short life span, while refresh tokens can be stored for a longer time period.\n\n```ruby\nJWTSessions.access_exp_time = 3600 # 1 hour in seconds\nJWTSessions.refresh_exp_time = 604800 # 1 week in seconds\n```\n\nIt is defined globally, but can be overridden on a session level. See `JWTSessions::Session.new` options for more info.\n\n##### Exceptions\n\n`JWTSessions::Errors::Error` - base class, all possible exceptions are inhereted from it. \\\n`JWTSessions::Errors::Malconfigured` - some required gem settings are empty, or methods are not implemented. \\\n`JWTSessions::Errors::InvalidPayload` - token's payload doesn't contain required keys or they are invalid. \\\n`JWTSessions::Errors::Unauthorized` - token can't be decoded or JWT claims are invalid. \\\n`JWTSessions::Errors::ClaimsVerification` - JWT claims are invalid (inherited from `JWTSessions::Errors::Unauthorized`). \\\n`JWTSessions::Errors::Expired` - token is expired (inherited from `JWTSessions::Errors::ClaimsVerification`).\n\n#### CSRF and cookies\n\nWhen you use cookies as your tokens transport it becomes vulnerable to CSRF. That is why both the login and refresh methods of the `Session` class produce CSRF tokens for you. `Authorization` mixin expects that this token is sent with all requests except GET and HEAD in a header specified among this gem's settings (`X-CSRF-Token` by default). Verification will be done automatically and the `Authorization` exception will be raised in case of a mismatch between the token from the header and the one stored in the session. \\\nAlthough you do not need to mitigate BREACH attacks it is still possible to generate a new masked token with the access token.\n\n```ruby\nsession = JWTSessions::Session.new\nsession.masked_csrf(access_token)\n```\n\n##### Refresh with access token\n\nSometimes it is not secure enough to store the refresh tokens in web / JS clients. \\\nThis is why you have the option to only use an access token and to not pass the refresh token to the client at all. \\\nSession accepts `refresh_by_access_allowed: true` setting, which links the access token to the corresponding refresh token.\n\nExample Rails login controller, which passes an access token token via cookies and renders CSRF:\n\n```ruby\nclass LoginController \u003c ApplicationController\n  def create\n    user = User.find_by!(email: params[:email])\n    if user.authenticate(params[:password])\n\n      payload = { user_id: user.id }\n      session = JWTSessions::Session.new(payload: payload, refresh_by_access_allowed: true)\n      tokens = session.login\n      response.set_cookie(JWTSessions.access_cookie,\n                          value: tokens[:access],\n                          httponly: true,\n                          secure: Rails.env.production?)\n\n      render json: { csrf: tokens[:csrf] }\n    else\n      render json: \"Invalid email or password\", status: :unauthorized\n    end\n  end\nend\n```\n\nThe gem provides the ability to refresh the session by access token.\n\n```ruby\nsession = JWTSessions::Session.new(payload: payload, refresh_by_access_allowed: true)\ntokens  = session.refresh_by_access_payload\n```\n\nIn case of token forgery and successful refresh performed by an attacker the original user will have to logout. \\\nTo protect the endpoint use the before_action `authorize_refresh_by_access_request!`. \\\nRefresh should be performed once the access token is already expired and we need to use the `claimless_payload` method in order to skip JWT expiration validation (and other claims) in order to proceed.\n\nOptionally `refresh_by_access_payload` accepts a block argument (the same way `refresh` method does).\nThe block will be called if the refresh action is performed before the access token is expired.\nThereby it's possible to prohibit users from making refresh calls while their access token is still active.\n\n```ruby\ntokens = session.refresh_by_access_payload do\n  # here goes malicious activity alert\n  raise JWTSessions::Errors::Unauthorized, \"Refresh action is performed before the expiration of the access token.\"\nend\n```\n\nExample Rails refresh by access controller with cookies as token transport:\n\n```ruby\nclass RefreshController \u003c ApplicationController\n  before_action :authorize_refresh_by_access_request!\n\n  def create\n    session = JWTSessions::Session.new(payload: claimless_payload, refresh_by_access_allowed: true)\n    tokens  = session.refresh_by_access_payload\n    response.set_cookie(JWTSessions.access_cookie,\n                        value: tokens[:access],\n                        httponly: true,\n                        secure: Rails.env.production?)\n\n    render json: { csrf: tokens[:csrf] }\n  end\nend\n\n```\n\nFor the cases when an endpoint must support only one specific token transport the following auth methods can be used instead:\n\n```ruby\nauthorize_refresh_by_access_cookie!\nauthorize_refresh_by_access_header!\n```\n\n#### Refresh token hijack protection\n\nThere is a security recommendation regarding the usage of refresh tokens: only perform refresh when an access token expires. \\\nSessions are always defined by a pair of tokens and there cannot be multiple access tokens for a single refresh token. Simultaneous usage of the refresh token by multiple users can be easily noticed as refresh will be performed before the expiration of the access token by one of the users. As a result, `refresh` method of the `Session` class supports an optional block as one of its arguments which will be executed only in case of refresh being performed before the expiration of the access token.\n\n```ruby\nsession = JwtSessions::Session.new(payload: payload)\nsession.refresh(refresh_token) { |refresh_token_uid, access_token_expiration| ... }\n```\n\n## Flush Sessions\n\nFlush a session by its refresh token. The method returns number of flushed sessions:\n\n```ruby\nsession = JWTSessions::Session.new\ntokens = session.login\nsession.flush_by_token(tokens[:refresh]) # =\u003e 1\n```\n\nFlush a session by its access token:\n\n```ruby\nsession = JWTSessions::Session.new(refresh_by_access_allowed: true)\ntokens = session.login\nsession.flush_by_access_payload\n# or\nsession = JWTSessions::Session.new(refresh_by_access_allowed: true, payload: payload)\nsession.flush_by_access_payload\n```\n\nOr by refresh token UID:\n\n```ruby\nsession.flush_by_uid(uid) # =\u003e 1\n```\n\n##### Sessions namespace\n\nIt's possible to group sessions by custom namespaces:\n\n```ruby\nsession = JWTSessions::Session.new(namespace: \"account-1\")\n```\n\nSelectively flush sessions by namespace:\n\n```ruby\nsession = JWTSessions::Session.new(namespace: \"ie-sessions\")\nsession.flush_namespaced # will flush all sessions which belong to the same namespace\n```\n\nSelectively flush one single session inside a namespace by its access token:\n\n```ruby\nsession = JWTSessions::Session.new(namespace: \"ie-sessions\", payload: payload)\nsession.flush_by_access_payload # will flush a specific session which belongs to an existing namespace\n```\n\nFlush access tokens only:\n\n```ruby\nsession = JWTSessions::Session.new(namespace: \"ie-sessions\")\nsession.flush_namespaced_access_tokens # will flush all access tokens which belong to the same namespace, but will keep refresh tokens\n```\n\nForce flush of all app sessions:\n\n```ruby\nJWTSessions::Session.flush_all\n```\n\n##### Logout\n\nTo logout you need to remove both access and refresh tokens from the store. \\\nFlush sessions methods can be used to perform logout. \\\nRefresh token or refresh token UID is required to flush a session. \\\nTo logout with an access token, `refresh_by_access_allowed` should be set to true on access token creation. If logout by access token is allowed it is recommended to ignore the expiration claim and to allow to logout with the expired access token.\n\n## Examples\n\n[Rails API](test/support/dummy_api) \\\n[Sinatra API](test/support/dummy_sinatra_api)\n\nYou can use a mixed approach for the cases when you would like to store an access token in localStorage and refresh token in HTTP-only secure cookies. \\\nRails controllers setup example:\n\n```ruby\nclass LoginController \u003c ApplicationController\n  def create\n    user = User.find_by(email: params[:email])\n    if user\u0026.authenticate(params[:password])\n\n      payload = { user_id: user.id, role: user.role, permissions: user.permissions }\n      refresh_payload = { user_id: user.id }\n      session = JWTSessions::Session.new(payload: payload, refresh_payload: refresh_payload)\n      tokens = session.login\n      response.set_cookie(JWTSessions.refresh_cookie,\n                          value: tokens[:refresh],\n                          httponly: true,\n                          secure: Rails.env.production?)\n\n      render json: { access: tokens[:access], csrf: tokens[:csrf] }\n    else\n      render json: \"Cannot login\", status: :unauthorized\n    end\n  end\nend\n\nclass RefreshController \u003c ApplicationController\n  before_action :authorize_refresh_request!\n\n  def create\n    tokens = JWTSessions::Session.new(payload: access_payload).refresh(found_token)\n    render json: { access: tokens[:access], csrf: tokens[:csrf] }\n  end\n\n  def access_payload\n    user = User.find_by!(email: payload[\"user_id\"])\n    { user_id: user.id, role: user.role, permissions: user.permissions }\n  end\nend\n\nclass ResourcesController \u003c ApplicationController\n  before_action :authorize_access_request!\n  before_action :validate_role_and_permissions_from_payload\n\n  # ...\nend\n```\n\n## Contributing\n\nFork \u0026 Pull Request. \\\nRbNaCl and sodium cryptographic library are required for tests.\n\nFor MacOS see [these instructions](http://macappstore.org/libsodium/). \\\nFor example, with Homebrew:\n\n```\nbrew install libsodium\n```\n\n## License\n\nMIT\n","funding_links":[],"categories":["Ruby"],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftuwukee%2Fjwt_sessions","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ftuwukee%2Fjwt_sessions","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftuwukee%2Fjwt_sessions/lists"}