From dc452d72d111e220a2e6c4288be994b607a74a6f Mon Sep 17 00:00:00 2001 From: Kishan Verma Date: Wed, 2 Feb 2022 20:27:20 +0530 Subject: [PATCH 1/4] feature: replaced uid with uuid --- lib/jwt_sessions/access_token.rb | 34 ++++---- lib/jwt_sessions/authorization.rb | 17 ++-- lib/jwt_sessions/rails_authorization.rb | 2 +- lib/jwt_sessions/refresh_token.rb | 48 +++++------ lib/jwt_sessions/session.rb | 84 +++++++++---------- .../store_adapters/abstract_store_adapter.rb | 14 ++-- .../store_adapters/memory_store_adapter.rb | 40 ++++----- .../store_adapters/redis_store_adapter.rb | 58 ++++++------- .../test_memory_store_adapter.rb | 22 ++--- test/units/jwt_sessions/test_access_token.rb | 2 +- test/units/jwt_sessions/test_refresh_token.rb | 2 +- test/units/jwt_sessions/test_session.rb | 16 ++-- 12 files changed, 170 insertions(+), 169 deletions(-) diff --git a/lib/jwt_sessions/access_token.rb b/lib/jwt_sessions/access_token.rb index 05b0ac7..aba7edb 100644 --- a/lib/jwt_sessions/access_token.rb +++ b/lib/jwt_sessions/access_token.rb @@ -2,26 +2,26 @@ module JWTSessions class AccessToken - attr_reader :payload, :uid, :expiration, :csrf, :store + attr_reader :payload, :uuid, :expiration, :csrf, :store - def initialize(csrf, payload, store, uid = SecureRandom.uuid, expiration = JWTSessions.access_expiration) + def initialize(csrf, payload, store, uuid = SecureRandom.uuid, expiration = JWTSessions.access_expiration) @csrf = csrf - @uid = uid + @uuid = uuid @expiration = expiration - @payload = payload.merge("uid" => uid, "exp" => expiration.to_i) + @payload = payload.merge("uuid" => uuid, "exp" => expiration.to_i) @store = store end def destroy - store.destroy_access(uid) + store.destroy_access(uuid) end - def refresh_uid=(uid) - self.payload["ruid"] = uid + def refresh_uuid=(uuid) + self.payload["ruuid"] = uuid end - def refresh_uid - payload["ruid"] + def refresh_uuid + payload["ruuid"] end def token @@ -31,27 +31,27 @@ def token class << self def create(csrf, payload, store, expiration = JWTSessions.access_expiration) new(csrf, payload, store, SecureRandom.uuid, expiration).tap do |inst| - store.persist_access(inst.uid, inst.csrf, inst.expiration) + store.persist_access(inst.uuid, inst.csrf, inst.expiration) end end - def destroy(uid, store) - store.destroy_access(uid) + def destroy(uuid, store) + store.destroy_access(uuid) end # AccessToken's find method cannot be used to retrieve token's payload # or any other information but is intended to identify if the token is present # and to retrieve session's CSRF token - def find(uid, store) - token_attrs = store.fetch_access(uid) + def find(uuid, store) + token_attrs = store.fetch_access(uuid) raise Errors::Unauthorized, "Access token not found" if token_attrs.empty? - build_with_token_attrs(store, uid, token_attrs) + build_with_token_attrs(store, uuid, token_attrs) end private - def build_with_token_attrs(store, uid, token_attrs) - new(token_attrs[:csrf], {}, store, uid) + def build_with_token_attrs(store, uuid, token_attrs) + new(token_attrs[:csrf], {}, store, uuid) end end end diff --git a/lib/jwt_sessions/authorization.rb b/lib/jwt_sessions/authorization.rb index 6a879b2..8eb741a 100644 --- a/lib/jwt_sessions/authorization.rb +++ b/lib/jwt_sessions/authorization.rb @@ -67,15 +67,15 @@ def should_check_csrf? end def request_headers - raise Errors::Malconfigured, "request_headers is not implemented" + raise Errors::Malconfigured, 'request_headers is not implemented' end def request_cookies - raise Errors::Malconfigured, "request_cookies is not implemented" + raise Errors::Malconfigured, 'request_cookies is not implemented' end def request_method - raise Errors::Malconfigured, "request_method is not implemented" + raise Errors::Malconfigured, 'request_method is not implemented' end def valid_csrf_token?(csrf_token, token_type) @@ -98,20 +98,21 @@ def cookie_based_auth(token_type) def retrieve_csrf token = request_headers[JWTSessions.csrf_header] - raise Errors::Unauthorized, "CSRF token is not found" unless token + raise Errors::Unauthorized, 'CSRF token is not found' unless token token end def token_from_headers(token_type) - raw_token = request_headers[JWTSessions.header_by(token_type)] || "" - token = raw_token.split(" ")[-1] - raise Errors::Unauthorized, "Token is not found" unless token + raw_token = request_headers[JWTSessions.header_by(token_type)] || '' + raw_token = request_headers.env[JWTSessions.header_by(token_type)] || '' unless raw_token.present? + token = raw_token.split(' ')[-1] + raise Errors::Unauthorized, 'Token is not found' unless token token end def token_from_cookies(token_type) token = request_cookies[JWTSessions.cookie_by(token_type)] - raise Errors::Unauthorized, "Token is not found" unless token + raise Errors::Unauthorized, 'Token is not found' unless token token end diff --git a/lib/jwt_sessions/rails_authorization.rb b/lib/jwt_sessions/rails_authorization.rb index bed992d..3d54a73 100644 --- a/lib/jwt_sessions/rails_authorization.rb +++ b/lib/jwt_sessions/rails_authorization.rb @@ -17,7 +17,7 @@ def request_cookies end def request_method - request.method + request.request_method end end end diff --git a/lib/jwt_sessions/refresh_token.rb b/lib/jwt_sessions/refresh_token.rb index 0ab1e59..9e0ed19 100644 --- a/lib/jwt_sessions/refresh_token.rb +++ b/lib/jwt_sessions/refresh_token.rb @@ -2,28 +2,28 @@ module JWTSessions class RefreshToken - attr_reader :expiration, :uid, :token, :csrf, :access_uid, :access_expiration, :store, :namespace + attr_reader :expiration, :uuid, :token, :csrf, :access_uuid, :access_expiration, :store, :namespace def initialize(csrf, - access_uid, + access_uuid, access_expiration, store, options = {}) @csrf = csrf - @access_uid = access_uid + @access_uuid = access_uuid @access_expiration = access_expiration @store = store - @uid = options.fetch(:uid, nil) || SecureRandom.uuid + @uuid = options.fetch(:uuid, nil) || SecureRandom.uuid @expiration = options.fetch(:expiration, nil) || JWTSessions.refresh_expiration @namespace = options.fetch(:namespace, nil) - @token = Token.encode(options.fetch(:payload, {}).merge("uid" => uid, "exp" => expiration.to_i)) + @token = Token.encode(options.fetch(:payload, {}).merge("uuid" => uuid, "exp" => expiration.to_i)) end class << self - def create(csrf, access_uid, access_expiration, store, payload, namespace, expiration = JWTSessions.refresh_expiration) + def create(csrf, access_uuid, access_expiration, store, payload, namespace, expiration = JWTSessions.refresh_expiration) inst = new( csrf, - access_uid, + access_uuid, access_expiration, store, payload: payload, @@ -36,63 +36,63 @@ def create(csrf, access_uid, access_expiration, store, payload, namespace, expir def all(namespace, store) tokens = store.all_refresh_tokens(namespace) - tokens.map do |uid, token_attrs| - build_with_token_attrs(store, uid, token_attrs, namespace) + tokens.map do |uuid, token_attrs| + build_with_token_attrs(store, uuid, token_attrs, namespace) end end # first_match should be set to true when # we need to search through the all namespaces - def find(uid, store, namespace = nil, first_match: false) - token_attrs = store.fetch_refresh(uid, namespace, first_match) + def find(uuid, store, namespace = nil, first_match: false) + token_attrs = store.fetch_refresh(uuid, namespace, first_match) raise Errors::Unauthorized, "Refresh token not found" if token_attrs.empty? - build_with_token_attrs(store, uid, token_attrs, namespace) + build_with_token_attrs(store, uuid, token_attrs, namespace) end - def destroy(uid, store, namespace) - store.destroy_refresh(uid, namespace) + def destroy(uuid, store, namespace) + store.destroy_refresh(uuid, namespace) end private - def build_with_token_attrs(store, uid, token_attrs, namespace) + def build_with_token_attrs(store, uuid, token_attrs, namespace) new( token_attrs[:csrf], - token_attrs[:access_uid], + token_attrs[:access_uuid], token_attrs[:access_expiration], store, namespace: namespace, payload: {}, - uid: uid, + uuid: uuid, expiration: token_attrs[:expiration] ) end end - def update(access_uid, access_expiration, csrf) + def update(access_uuid, access_expiration, csrf) @csrf = csrf - @access_uid = access_uid + @access_uuid = access_uuid @access_expiration = access_expiration store.update_refresh( - uid: uid, + uuid: uuid, access_expiration: access_expiration, - access_uid: access_uid, + access_uuid: access_uuid, csrf: csrf, namespace: namespace ) end def destroy - store.destroy_refresh(uid, namespace) + store.destroy_refresh(uuid, namespace) end private def persist_in_store store.persist_refresh( - uid: uid, + uuid: uuid, access_expiration: access_expiration, - access_uid: access_uid, + access_uuid: access_uuid, csrf: csrf, expiration: expiration, namespace: namespace diff --git a/lib/jwt_sessions/session.rb b/lib/jwt_sessions/session.rb index 4d2fb6c..e246f83 100644 --- a/lib/jwt_sessions/session.rb +++ b/lib/jwt_sessions/session.rb @@ -49,36 +49,36 @@ def masked_csrf(access_token) def refresh(refresh_token, &block) refresh_token_data(refresh_token) - refresh_by_uid(&block) + refresh_by_uuid(&block) end def refresh_by_access_payload(&block) raise Errors::InvalidPayload if payload.nil? - ruid = retrieve_val_from(payload, :access, "ruid", "refresh uid") - retrieve_refresh_token(ruid) + ruuid = retrieve_val_from(payload, :access, "ruuid", "refresh uuid") + retrieve_refresh_token(ruuid) - check_access_uid_within_refresh_token(&block) if block_given? + check_access_uuid_within_refresh_token(&block) if block_given? - refresh_by_uid(&block) + refresh_by_uuid(&block) end def flush_by_access_payload raise Errors::InvalidPayload if payload.nil? - ruid = retrieve_val_from(payload, :access, "ruid", "refresh uid") - flush_by_uid(ruid) + ruuid = retrieve_val_from(payload, :access, "ruuid", "refresh uuid") + flush_by_uuid(ruuid) end # flush the session by refresh token def flush_by_token(token) - uid = token_uid(token, :refresh, @refresh_claims) - flush_by_uid(uid) + uuid = token_uuid(token, :refresh, @refresh_claims) + flush_by_uuid(uuid) end - # flush the session by refresh token uid - def flush_by_uid(uid) - token = retrieve_refresh_token(uid) + # flush the session by refresh token uuid + def flush_by_uuid(uuid) + token = retrieve_refresh_token(uuid) - AccessToken.destroy(token.access_uid, store) + AccessToken.destroy(token.access_uuid, store) token.destroy end @@ -87,7 +87,7 @@ def flush_namespaced_access_tokens return 0 unless namespace tokens = RefreshToken.all(namespace, store) tokens.each do |token| - AccessToken.destroy(token.access_uid, store) + AccessToken.destroy(token.access_uuid, store) # unlink refresh token from the current access token token.update(nil, nil, token.csrf) end.count @@ -97,7 +97,7 @@ def flush_namespaced return 0 unless namespace tokens = RefreshToken.all(namespace, store) tokens.each do |token| - AccessToken.destroy(token.access_uid, store) + AccessToken.destroy(token.access_uuid, store) token.destroy end.count end @@ -105,17 +105,17 @@ def flush_namespaced def self.flush_all(store = JWTSessions.token_store) tokens = RefreshToken.all(nil, store) tokens.each do |token| - AccessToken.destroy(token.access_uid, store) + AccessToken.destroy(token.access_uuid, store) token.destroy end.count end def valid_access_request?(external_csrf_token, external_payload) - ruid = retrieve_val_from(external_payload, :access, "ruid", "refresh uid") - uid = retrieve_val_from(external_payload, :access, "uid", "access uid") + ruuid = retrieve_val_from(external_payload, :access, "ruuid", "refresh uuid") + uuid = retrieve_val_from(external_payload, :access, "uuid", "access uuid") - refresh_token = RefreshToken.find(ruid, JWTSessions.token_store, first_match: true) - return false unless uid == refresh_token.access_uid + refresh_token = RefreshToken.find(ruuid, JWTSessions.token_store, first_match: true) + return false unless uuid == refresh_token.access_uuid CSRFToken.new(refresh_token.csrf).valid_authenticity_token?(external_csrf_token) end @@ -130,9 +130,9 @@ def valid_refresh_csrf?(refresh_token, csrf_token) refresh_csrf(refresh_token).valid_authenticity_token?(csrf_token) end - def refresh_by_uid(&block) + def refresh_by_uuid(&block) check_refresh_on_time(&block) if block_given? - AccessToken.destroy(@_refresh.access_uid, store) + AccessToken.destroy(@_refresh.access_uuid, store) issue_tokens_after_refresh end @@ -147,25 +147,25 @@ def refresh_csrf(refresh_token) end def access_token_data(token, _first_match = false) - uid = token_uid(token, :access, @access_claims) - data = store.fetch_access(uid) + uuid = token_uuid(token, :access, @access_claims) + data = store.fetch_access(uuid) raise Errors::Unauthorized, "Access token not found" if data.empty? data end def refresh_token_data(token, first_match = false) - uid = token_uid(token, :refresh, @refresh_claims) - retrieve_refresh_token(uid, first_match: first_match) + uuid = token_uuid(token, :refresh, @refresh_claims) + retrieve_refresh_token(uuid, first_match: first_match) end - def token_uid(token, type, claims) + def token_uuid(token, type, claims) token_payload = JWTSessions::Token.decode(token, claims).first - uid = token_payload.fetch("uid", nil) - if uid.nil? - message = "#{type.to_s.capitalize} token payload does not contain token uid" + uuid = token_payload.fetch("uuid", nil) + if uuid.nil? + message = "#{type.to_s.capitalize} token payload does not contain token uuid" raise Errors::InvalidPayload, message end - uid + uuid end def retrieve_val_from(token_payload, type, val_key, val_name) @@ -177,8 +177,8 @@ def retrieve_val_from(token_payload, type, val_key, val_name) val end - def retrieve_refresh_token(uid, first_match: false) - @_refresh = RefreshToken.find(uid, store, namespace, first_match: first_match) + def retrieve_refresh_token(uuid, first_match: false) + @_refresh = RefreshToken.find(uuid, store, namespace, first_match: first_match) end def tokens_hash @@ -202,14 +202,14 @@ def refresh_tokens_hash def check_refresh_on_time expiration = @_refresh.access_expiration return if expiration.size.zero? - yield @_refresh.uid, expiration if expiration.to_i > Time.now.to_i + yield @_refresh.uuid, expiration if expiration.to_i > Time.now.to_i end - def check_access_uid_within_refresh_token - uid = retrieve_val_from(payload, :access, "uid", "access uid") - access_uid = @_refresh.access_uid - return if access_uid.size.zero? - yield @_refresh.uid, @_refresh.access_expiration if access_uid != uid + def check_access_uuid_within_refresh_token + uuid = retrieve_val_from(payload, :access, "uuid", "access uuid") + access_uuid = @_refresh.access_uuid + return if access_uuid.size.zero? + yield @_refresh.uuid, @_refresh.access_expiration if access_uuid != uuid end def issue_tokens_after_refresh @@ -221,14 +221,14 @@ def issue_tokens_after_refresh end def update_refresh_token - @_refresh.update(@_access.uid, @_access.expiration, @_csrf.encoded) + @_refresh.update(@_access.uuid, @_access.expiration, @_csrf.encoded) @refresh_token = @_refresh.token link_access_to_refresh end def link_access_to_refresh return unless refresh_by_access_allowed - @_access.refresh_uid = @_refresh.uid + @_access.refresh_uuid = @_refresh.uuid @access_token = @_access.token @payload = @_access.payload end @@ -241,7 +241,7 @@ def create_csrf_token def create_refresh_token @_refresh = RefreshToken.create( @_csrf.encoded, - @_access.uid, + @_access.uuid, @_access.expiration, store, refresh_payload, diff --git a/lib/jwt_sessions/store_adapters/abstract_store_adapter.rb b/lib/jwt_sessions/store_adapters/abstract_store_adapter.rb index 190cb1b..2185498 100644 --- a/lib/jwt_sessions/store_adapters/abstract_store_adapter.rb +++ b/lib/jwt_sessions/store_adapters/abstract_store_adapter.rb @@ -3,24 +3,24 @@ module JWTSessions module StoreAdapters class AbstractStoreAdapter - def fetch_access(_uid) + def fetch_access(_uuid) raise NotImplementedError end - def persist_access(_uid, _csrf, _expiration) + def persist_access(_uuid, _csrf, _expiration) raise NotImplementedError end # Set first_match to true to look up through all namespaces - def fetch_refresh(_uid, _namespace, _first_match) + def fetch_refresh(_uuid, _namespace, _first_match) raise NotImplementedError end - def persist_refresh(_uid:, _access_expiration:, _access_uid:, _csrf:, _expiration:, _namespace:) + def persist_refresh(_uuid:, _access_expiration:, _access_uuid:, _csrf:, _expiration:, _namespace:) raise NotImplementedError end - def update_refresh(_uid:, _access_expiration:, _access_uid:, _csrf:, _namespace:) + def update_refresh(_uuid:, _access_expiration:, _access_uuid:, _csrf:, _namespace:) raise NotImplementedError end @@ -28,11 +28,11 @@ def all_refresh_tokens(_namespace) raise NotImplementedError end - def destroy_refresh(_uid, _namespace) + def destroy_refresh(_uuid, _namespace) raise NotImplementedError end - def destroy_access(_uid) + def destroy_access(_uuid) raise NotImplementedError end end diff --git a/lib/jwt_sessions/store_adapters/memory_store_adapter.rb b/lib/jwt_sessions/store_adapters/memory_store_adapter.rb index 6f68512..12b0128 100644 --- a/lib/jwt_sessions/store_adapters/memory_store_adapter.rb +++ b/lib/jwt_sessions/store_adapters/memory_store_adapter.rb @@ -12,46 +12,46 @@ def initialize(**options) end end - def fetch_access(uid) - access_token = value_if_not_expired(uid, "access", "") + def fetch_access(uuid) + access_token = value_if_not_expired(uuid, "access", "") access_token.empty? ? {} : { csrf: access_token[:csrf] } end - def persist_access(uid, csrf, expiration) + def persist_access(uuid, csrf, expiration) access_token = { csrf: csrf, expiration: expiration } - storage[""]["access"].store(uid, access_token) + storage[""]["access"].store(uuid, access_token) end - def fetch_refresh(uid, namespace, first_match = false) + def fetch_refresh(uuid, namespace, first_match = false) if first_match storage.keys.each do |namespace_key| - val = value_if_not_expired(uid, "refresh", namespace_key) + val = value_if_not_expired(uuid, "refresh", namespace_key) return val unless val.empty? end {} else - value_if_not_expired(uid, "refresh", namespace.to_s) + value_if_not_expired(uuid, "refresh", namespace.to_s) end end - def persist_refresh(uid:, access_expiration:, access_uid:, csrf:, expiration:, namespace: "") + def persist_refresh(uuid:, access_expiration:, access_uuid:, csrf:, expiration:, namespace: "") update_refresh_fields( - uid, + uuid, namespace.to_s, csrf: csrf, access_expiration: access_expiration, - access_uid: access_uid, + access_uuid: access_uuid, expiration: expiration ) end - def update_refresh(uid:, access_expiration:, access_uid:, csrf:, namespace: "") + def update_refresh(uuid:, access_expiration:, access_uuid:, csrf:, namespace: "") update_refresh_fields( - uid, + uuid, namespace.to_s, csrf: csrf, access_expiration: access_expiration, - access_uid: access_uid + access_uuid: access_uuid ) end @@ -63,12 +63,12 @@ def all_refresh_tokens(namespace) end end - def destroy_refresh(uid, namespace) - storage[namespace.to_s]["refresh"].delete(uid) + def destroy_refresh(uuid, namespace) + storage[namespace.to_s]["refresh"].delete(uuid) end - def destroy_access(uid) - storage[""]["access"].delete(uid) + def destroy_access(uuid) + storage[""]["access"].delete(uuid) end private @@ -84,12 +84,12 @@ def update_refresh_fields(key, namespace, fields) end def select_keys(keys_hash, acc) - keys_hash.keys.each do |uid| - value = keys_hash[uid] + keys_hash.keys.each do |uuid| + value = keys_hash[uuid] if value[:expiration] && value[:expiration] < Time.now.to_i keys_hash.delete(key) else - acc[uid] = value + acc[uuid] = value end end diff --git a/lib/jwt_sessions/store_adapters/redis_store_adapter.rb b/lib/jwt_sessions/store_adapters/redis_store_adapter.rb index a9ca6b6..02723c8 100644 --- a/lib/jwt_sessions/store_adapters/redis_store_adapter.rb +++ b/lib/jwt_sessions/store_adapters/redis_store_adapter.rb @@ -5,7 +5,7 @@ module StoreAdapters class RedisStoreAdapter < AbstractStoreAdapter attr_reader :prefix, :storage - REFRESH_KEYS = %i[csrf access_uid access_expiration expiration].freeze + REFRESH_KEYS = %i[csrf access_uuid access_expiration expiration].freeze def initialize(token_prefix: JWTSessions.token_prefix, **options) @prefix = token_prefix @@ -20,31 +20,31 @@ def initialize(token_prefix: JWTSessions.token_prefix, **options) end end - def fetch_access(uid) - csrf = storage.get(access_key(uid)) + def fetch_access(uuid) + csrf = storage.get(access_key(uuid)) csrf.nil? ? {} : { csrf: csrf } end - def persist_access(uid, csrf, expiration) - key = access_key(uid) + def persist_access(uuid, csrf, expiration) + key = access_key(uuid) storage.set(key, csrf) storage.expireat(key, expiration) end - def fetch_refresh(uid, namespace, first_match = false) - key = first_match ? first_refresh_key(uid) : full_refresh_key(uid, namespace) + def fetch_refresh(uuid, namespace, first_match = false) + key = first_match ? first_refresh_key(uuid) : full_refresh_key(uuid, namespace) values = storage.hmget(key, *REFRESH_KEYS).compact return {} if values.length != REFRESH_KEYS.length REFRESH_KEYS.each_with_index.each_with_object({}) { |(key, index), acc| acc[key] = values[index] } end - def persist_refresh(uid:, access_expiration:, access_uid:, csrf:, expiration:, namespace: nil) - key = full_refresh_key(uid, namespace) + def persist_refresh(uuid:, access_expiration:, access_uuid:, csrf:, expiration:, namespace: nil) + key = full_refresh_key(uuid, namespace) update_refresh( - uid: uid, + uuid: uuid, access_expiration: access_expiration, - access_uid: access_uid, + access_uuid: access_uuid, csrf: csrf, namespace: namespace ) @@ -52,30 +52,30 @@ def persist_refresh(uid:, access_expiration:, access_uid:, csrf:, expiration:, n storage.expireat(key, expiration) end - def update_refresh(uid:, access_expiration:, access_uid:, csrf:, namespace: nil) + def update_refresh(uuid:, access_expiration:, access_uuid:, csrf:, namespace: nil) storage.hmset( - full_refresh_key(uid, namespace), + full_refresh_key(uuid, namespace), :csrf, csrf, :access_expiration, access_expiration, - :access_uid, access_uid + :access_uuid, access_uuid ) end def all_refresh_tokens(namespace) keys_in_namespace = storage.keys(refresh_key("*", namespace)) (keys_in_namespace || []).each_with_object({}) do |key, acc| - uid = uid_from_key(key) - acc[uid] = fetch_refresh(uid, namespace) + uuid = uuid_from_key(key) + acc[uuid] = fetch_refresh(uuid, namespace) end end - def destroy_refresh(uid, namespace) - key = full_refresh_key(uid, namespace) + def destroy_refresh(uuid, namespace) + key = full_refresh_key(uuid, namespace) storage.del(key) end - def destroy_access(uid) - storage.del(access_key(uid)) + def destroy_access(uuid) + storage.del(access_key(uuid)) end private @@ -105,25 +105,25 @@ def build_redis_url(redis_host: nil, redis_port: nil, redis_db_name: nil) URI.join(redis_base_url, redis_db_name).to_s end - def full_refresh_key(uid, namespace) - "#{prefix}_#{namespace}_refresh_#{uid}" + def full_refresh_key(uuid, namespace) + "#{prefix}_#{namespace}_refresh_#{uuid}" end - def first_refresh_key(uid) - key = full_refresh_key(uid, "*") + def first_refresh_key(uuid) + key = full_refresh_key(uuid, "*") (storage.keys(key) || []).first end - def refresh_key(uid, namespace) + def refresh_key(uuid, namespace) namespace = "*" if namespace.to_s.empty? - full_refresh_key(uid, namespace) + full_refresh_key(uuid, namespace) end - def access_key(uid) - "#{prefix}_access_#{uid}" + def access_key(uuid) + "#{prefix}_access_#{uuid}" end - def uid_from_key(key) + def uuid_from_key(key) key.split("_").last end end diff --git a/test/units/jwt_sessions/store_adapters/test_memory_store_adapter.rb b/test/units/jwt_sessions/store_adapters/test_memory_store_adapter.rb index 79fdef2..ea01414 100644 --- a/test/units/jwt_sessions/store_adapters/test_memory_store_adapter.rb +++ b/test/units/jwt_sessions/store_adapters/test_memory_store_adapter.rb @@ -17,43 +17,43 @@ def test_error_on_unknown_option end def test_persist_and_fetch_access - store.persist_access("uid", "csrf", Time.now.to_i + 3600) - assert_equal({ csrf: "csrf" }, store.fetch_access("uid")) + store.persist_access("uuid", "csrf", Time.now.to_i + 3600) + assert_equal({ csrf: "csrf" }, store.fetch_access("uuid")) - store.persist_access("uid", "csrf", Time.now.to_i - 3600) - assert_equal({}, store.fetch_access("uid")) + store.persist_access("uuid", "csrf", Time.now.to_i - 3600) + assert_equal({}, store.fetch_access("uuid")) end def test_persist_and_fetch_refresh expiration = Time.now.to_i + 3600 store.persist_refresh( - uid: "uid", + uuid: "uuid", access_expiration: expiration, - access_uid: "access_uid", + access_uuid: "access_uuid", csrf: "csrf", expiration: expiration, namespace: "" ) - refresh = store.fetch_refresh("uid", "") + refresh = store.fetch_refresh("uuid", "") assert_equal "csrf", refresh[:csrf] expiration = Time.now.to_i - 3600 store.persist_refresh( - uid: "uid", + uuid: "uuid", access_expiration: expiration, - access_uid: "access_uid", + access_uuid: "access_uuid", csrf: "csrf", expiration: expiration, namespace: "" ) - refresh = store.fetch_refresh("uid", "") + refresh = store.fetch_refresh("uuid", "") assert_nil refresh[:csrf] end def test_update_refresh expiration = Time.now.to_i + 3600 store.persist_refresh( - uid: "uid", + uuid: "uuid", access_expiration: expiration, access_uid: "access_uid", csrf: "csrf", diff --git a/test/units/jwt_sessions/test_access_token.rb b/test/units/jwt_sessions/test_access_token.rb index 2d04f03..6f236f0 100644 --- a/test/units/jwt_sessions/test_access_token.rb +++ b/test/units/jwt_sessions/test_access_token.rb @@ -4,7 +4,7 @@ require "jwt_sessions" class TestAccessToken < Minitest::Test - attr_reader :access_token, :uid + attr_reader :access_token, :uuid def setup JWTSessions.encryption_key = "secret key" diff --git a/test/units/jwt_sessions/test_refresh_token.rb b/test/units/jwt_sessions/test_refresh_token.rb index 79deaa2..cb481ae 100644 --- a/test/units/jwt_sessions/test_refresh_token.rb +++ b/test/units/jwt_sessions/test_refresh_token.rb @@ -4,7 +4,7 @@ require "jwt_sessions" class TestRefreshToken < Minitest::Test - attr_reader :csrf, :token, :access_uid + attr_reader :csrf, :token, :access_uuid def setup JWTSessions::Session.flush_all diff --git a/test/units/jwt_sessions/test_session.rb b/test/units/jwt_sessions/test_session.rb index 0fc3783..ca2fa95 100644 --- a/test/units/jwt_sessions/test_session.rb +++ b/test/units/jwt_sessions/test_session.rb @@ -91,7 +91,7 @@ def test_refresh_by_access_payload decoded_access = JWTSessions::Token.decode(refreshed_tokens[:access]).first assert_equal REFRESH_KEYS, refreshed_tokens.keys.sort assert_equal payload[:test], decoded_access["test"] - assert_equal session.instance_variable_get("@_refresh").uid, decoded_access["ruid"] + assert_equal session.instance_variable_get("@_refresh").uuid, decoded_access["ruuid"] assert_equal access2.expiration > access1.expiration, true end @@ -104,7 +104,7 @@ def test_refresh_by_access_payload_expired JWTSessions.access_exp_time = 3600 assert_equal REFRESH_KEYS, refreshed_tokens.keys.sort assert_equal payload[:test], decoded_access["test"] - assert_equal session.instance_variable_get("@_refresh").uid, decoded_access["ruid"] + assert_equal session.instance_variable_get("@_refresh").uuid, decoded_access["ruuid"] end def test_refresh_by_access_payload_with_block_expired @@ -118,7 +118,7 @@ def test_refresh_by_access_payload_with_block_expired JWTSessions.access_exp_time = 3600 assert_equal REFRESH_KEYS, refreshed_tokens.keys.sort assert_equal payload[:test], decoded_access["test"] - assert_equal session.instance_variable_get("@_refresh").uid, decoded_access["ruid"] + assert_equal session.instance_variable_get("@_refresh").uuid, decoded_access["ruuid"] end def test_refresh_by_access_payload_with_block_not_expired @@ -131,12 +131,12 @@ def test_refresh_by_access_payload_with_block_not_expired end end - def test_refresh_by_access_payload_invalid_uid + def test_refresh_by_access_payload_invalid_uuid session = JWTSessions::Session.new(payload: payload, refresh_by_access_allowed: true) session.login access1 = session.instance_variable_get("@_access") - # should execute the code block for the cases when access UID within the refresh token - # does not match access UID from the session payload + # should execute the code block for the cases when access uuid within the refresh token + # does not match access uuid from the session payload session2 = JWTSessions::Session.new(payload: access1.payload, refresh_by_access_allowed: true) assert_raises JWTSessions::Errors::Unauthorized do session2.refresh_by_access_payload do @@ -145,7 +145,7 @@ def test_refresh_by_access_payload_invalid_uid end end - def test_refresh_by_access_payload_invalid_uid_with_multiple_refreshes + def test_refresh_by_access_payload_invalid_uuid_with_multiple_refreshes JWTSessions.access_exp_time = 0 session = JWTSessions::Session.new(payload: payload, refresh_by_access_allowed: true) session.login @@ -161,7 +161,7 @@ def test_refresh_by_access_payload_invalid_uid_with_multiple_refreshes end end - def test_refresh_by_access_payload_invalid_uid_outdated_access_token + def test_refresh_by_access_payload_invalid_uuid_outdated_access_token JWTSessions.access_exp_time = 0 session = JWTSessions::Session.new(payload: payload, refresh_by_access_allowed: true) original_tokens = session.login From cf0056f5d69c245e57a2bc7b92bb328db147c243 Mon Sep 17 00:00:00 2001 From: Kishan Verma Date: Thu, 29 Jun 2023 22:13:49 +0530 Subject: [PATCH 2/4] feature: added redis cluster --- .gitignore | 1 + .../store_adapters/redis_store_adapter.rb | 11 +++++++++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 5ec0005..6288439 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ test/dummy_api/tmp/ test/support/.DS_Store Gemfile.lock .DS_Store +.idea diff --git a/lib/jwt_sessions/store_adapters/redis_store_adapter.rb b/lib/jwt_sessions/store_adapters/redis_store_adapter.rb index 02723c8..b5385b8 100644 --- a/lib/jwt_sessions/store_adapters/redis_store_adapter.rb +++ b/lib/jwt_sessions/store_adapters/redis_store_adapter.rb @@ -80,7 +80,7 @@ def destroy_access(uuid) private - def configure_redis_client(redis_url: nil, redis_host: nil, redis_port: nil, redis_db_name: nil) + def configure_redis_client(redis_url: nil, redis_host: nil, redis_port: nil, redis_db_name: nil, redis_password: nil, redis_cluster: false) if redis_url && (redis_host || redis_port || redis_db_name) raise ArgumentError, "redis_url cannot be passed along with redis_host, redis_port or redis_db_name options" end @@ -91,7 +91,14 @@ def configure_redis_client(redis_url: nil, redis_host: nil, redis_port: nil, red redis_db_name: redis_db_name ) - Redis.new(url: redis_url) + config = if redis_cluster + { cluster: redis_url, + password: redis_password } + else + { url: redis_url } + end + + Redis.new(config) end def build_redis_url(redis_host: nil, redis_port: nil, redis_db_name: nil) From 224f2451ba419fe66c55bc55f85faf471c15fc9d Mon Sep 17 00:00:00 2001 From: Abhinav Date: Mon, 6 May 2024 16:17:55 +0530 Subject: [PATCH 3/4] Feature: Added methods to update refresh token expiry --- lib/jwt_sessions/refresh_token.rb | 7 +++++ lib/jwt_sessions/session.rb | 26 +++++++++++++++++++ .../store_adapters/memory_store_adapter.rb | 12 +++++++++ .../store_adapters/redis_store_adapter.rb | 18 +++++++++++++ test/units/jwt_sessions/test_refresh_token.rb | 7 +++++ test/units/jwt_sessions/test_session.rb | 9 +++++++ 6 files changed, 79 insertions(+) diff --git a/lib/jwt_sessions/refresh_token.rb b/lib/jwt_sessions/refresh_token.rb index 9e0ed19..80f35db 100644 --- a/lib/jwt_sessions/refresh_token.rb +++ b/lib/jwt_sessions/refresh_token.rb @@ -82,6 +82,13 @@ def update(access_uuid, access_expiration, csrf) ) end + def update_expiry(uuid, expiration) + store.update_refresh_expiry( + uuid: uuid, + expiration: expiration + ) + end + def destroy store.destroy_refresh(uuid, namespace) end diff --git a/lib/jwt_sessions/session.rb b/lib/jwt_sessions/session.rb index e246f83..04a50da 100644 --- a/lib/jwt_sessions/session.rb +++ b/lib/jwt_sessions/session.rb @@ -32,6 +32,13 @@ def login tokens_hash end + def renew_tokens(jti, expiration) + flush_by_jti(jti) + login + reset_refresh_uuid(jti, @_refresh.uuid, expiration) + tokens_hash + end + def valid_csrf?(token, csrf_token, token_type = :access) send(:"valid_#{token_type}_csrf?", token, csrf_token) end @@ -82,6 +89,12 @@ def flush_by_uuid(uuid) token.destroy end + # flush the session by refresh token jti + def flush_by_jti(jti) + uuid = refresh_uuid(jti) + flush_by_uuid(uuid) if uuid + end + # flush access tokens only and keep refresh def flush_namespaced_access_tokens return 0 unless namespace @@ -120,6 +133,19 @@ def valid_access_request?(external_csrf_token, external_payload) CSRFToken.new(refresh_token.csrf).valid_authenticity_token?(external_csrf_token) end + def update_refresh_expiry(uuid, expiration) + token = retrieve_refresh_token(uuid) + token.update_expiry(uuid, JWTSessions.custom_refresh_expiration(expiration)) + end + + def refresh_uuid(jti) + store.refresh_uuid(jti) + end + + def reset_refresh_uuid(jti, uuid, expiration) + store.set_refresh_uuid(jti, uuid, expiration) + end + private def valid_access_csrf?(access_token, csrf_token) diff --git a/lib/jwt_sessions/store_adapters/memory_store_adapter.rb b/lib/jwt_sessions/store_adapters/memory_store_adapter.rb index 12b0128..b9a46d9 100644 --- a/lib/jwt_sessions/store_adapters/memory_store_adapter.rb +++ b/lib/jwt_sessions/store_adapters/memory_store_adapter.rb @@ -55,6 +55,10 @@ def update_refresh(uuid:, access_expiration:, access_uuid:, csrf:, namespace: "" ) end + def update_refresh_expiry(uuid:, expiration:, namespace: nil) + storage[namespace.to_s]["refresh"][uuid][:expiration] = expiration + end + def all_refresh_tokens(namespace) namespace_keys = namespace.nil? ? storage.keys : [namespace] @@ -71,6 +75,14 @@ def destroy_access(uuid) storage[""]["access"].delete(uuid) end + def refresh_uuid(key, namespace = "") + value_if_not_expired(key, "jti", namespace)[:uuid] + end + + def set_refresh_uuid(key, uuid, expiration, namespace = "") + storage[namespace.to_s]["jti"].store(key, { uuid: uuid, expiration: Time.now.to_i + expiration }) + end + private def value_if_not_expired(key, token_type, namespace) diff --git a/lib/jwt_sessions/store_adapters/redis_store_adapter.rb b/lib/jwt_sessions/store_adapters/redis_store_adapter.rb index b5385b8..a772ea0 100644 --- a/lib/jwt_sessions/store_adapters/redis_store_adapter.rb +++ b/lib/jwt_sessions/store_adapters/redis_store_adapter.rb @@ -61,6 +61,12 @@ def update_refresh(uuid:, access_expiration:, access_uuid:, csrf:, namespace: ni ) end + def update_refresh_expiry(uuid:, expiration:, namespace: nil) + key = full_refresh_key(uuid, namespace) + storage.hset(key, :expiration, expiration) + storage.expireat(key, expiration) + end + def all_refresh_tokens(namespace) keys_in_namespace = storage.keys(refresh_key("*", namespace)) (keys_in_namespace || []).each_with_object({}) do |key, acc| @@ -78,6 +84,14 @@ def destroy_access(uuid) storage.del(access_key(uuid)) end + def refresh_uuid(key) + storage.get(jti_key(key)) + end + + def set_refresh_uuid(key, uuid, expiration) + storage.set(jti_key(key), uuid, ex: expiration) + end + private def configure_redis_client(redis_url: nil, redis_host: nil, redis_port: nil, redis_db_name: nil, redis_password: nil, redis_cluster: false) @@ -133,6 +147,10 @@ def access_key(uuid) def uuid_from_key(key) key.split("_").last end + + def jti_key(key) + "refresh_jti_#{key}" + end end end end diff --git a/test/units/jwt_sessions/test_refresh_token.rb b/test/units/jwt_sessions/test_refresh_token.rb index cb481ae..6e221da 100644 --- a/test/units/jwt_sessions/test_refresh_token.rb +++ b/test/units/jwt_sessions/test_refresh_token.rb @@ -29,6 +29,13 @@ def test_update token.destroy end + def test_update_expiry + token.update_expiry( + uuid: token.uid, + expiration: Time.now.to_i + 3600 + ) + end + def test_find found_token = JWTSessions::RefreshToken.find(token.uid, JWTSessions.token_store, nil) assert_equal found_token.access_uid, token.access_uid diff --git a/test/units/jwt_sessions/test_session.rb b/test/units/jwt_sessions/test_session.rb index ca2fa95..c53b3ed 100644 --- a/test/units/jwt_sessions/test_session.rb +++ b/test/units/jwt_sessions/test_session.rb @@ -334,4 +334,13 @@ def test_flush_all JWTSessions::RefreshToken.find(refresh_token.uid, JWTSessions.token_store, nil).token end end + + def update_refresh_expiry + refresh_token = @session.instance_variable_get(:"@_refresh") + refresh_token.update_expiry(refresh_token.uid, JWTSessions.custom_refresh_expiration(Time.now.to_i + 3600)) + + assert_raises JWTSessions::Errors::Unauthorized do + JWTSessions::RefreshToken.find(refresh_token.uid, JWTSessions.token_store, nil) + end + end end From 5296c20f4844e59d489a329e710c347e26d9d8cb Mon Sep 17 00:00:00 2001 From: Abhinav Date: Thu, 2 Oct 2025 18:50:20 +0530 Subject: [PATCH 4/4] Fix: Changed redis method from keys to scan. --- .../store_adapters/redis_store_adapter.rb | 35 +++++++++++++++---- 1 file changed, 29 insertions(+), 6 deletions(-) diff --git a/lib/jwt_sessions/store_adapters/redis_store_adapter.rb b/lib/jwt_sessions/store_adapters/redis_store_adapter.rb index a772ea0..c6a8472 100644 --- a/lib/jwt_sessions/store_adapters/redis_store_adapter.rb +++ b/lib/jwt_sessions/store_adapters/redis_store_adapter.rb @@ -32,7 +32,7 @@ def persist_access(uuid, csrf, expiration) end def fetch_refresh(uuid, namespace, first_match = false) - key = first_match ? first_refresh_key(uuid) : full_refresh_key(uuid, namespace) + key = full_refresh_key(uuid, namespace) values = storage.hmget(key, *REFRESH_KEYS).compact return {} if values.length != REFRESH_KEYS.length @@ -68,7 +68,7 @@ def update_refresh_expiry(uuid:, expiration:, namespace: nil) end def all_refresh_tokens(namespace) - keys_in_namespace = storage.keys(refresh_key("*", namespace)) + keys_in_namespace = scan_keys(refresh_key("*", namespace)) (keys_in_namespace || []).each_with_object({}) do |key, acc| uuid = uuid_from_key(key) acc[uuid] = fetch_refresh(uuid, namespace) @@ -107,9 +107,11 @@ def configure_redis_client(redis_url: nil, redis_host: nil, redis_port: nil, red config = if redis_cluster { cluster: redis_url, - password: redis_password } + password: redis_password, + driver: :hiredis } else - { url: redis_url } + { url: redis_url, + driver: :hiredis } end Redis.new(config) @@ -131,8 +133,29 @@ def full_refresh_key(uuid, namespace) end def first_refresh_key(uuid) - key = full_refresh_key(uuid, "*") - (storage.keys(key) || []).first + pattern = full_refresh_key(uuid, "*") + scan_keys(pattern, count: 1).first + end + + def scan_keys(pattern, count: 100) + keys = [] + cursor = "0" + + loop do + cursor, result = storage.scan(cursor, match: pattern, count: count) + keys.concat(result) + + # Break early if we found enough keys for first_refresh_key + break if count == 1 && keys.any? + break if cursor == "0" + end + + keys + rescue Redis::CommandError => e + # Fallback for Redis versions that don't support SCAN + # or if there's a cluster issue + Rails.logger.warn("SCAN failed, falling back to direct key lookup: #{e.message}") if defined?(Rails) + [] end def refresh_key(uuid, namespace)