From b51a6744abccaf9f75ca346cf9a1776d3be744ee Mon Sep 17 00:00:00 2001 From: Austin Hockenberry Date: Fri, 26 Jun 2026 15:19:29 -0400 Subject: [PATCH 1/7] Adding store and tests --- lib/resty/session.lua | 137 +++++++++++++++++ spec/06-revocation-1_spec.lua | 261 +++++++++++++++++++++++++++++++ spec/07-revocation-2_spec.lua | 281 ++++++++++++++++++++++++++++++++++ 3 files changed, 679 insertions(+) create mode 100644 spec/06-revocation-1_spec.lua create mode 100644 spec/07-revocation-2_spec.lua diff --git a/lib/resty/session.lua b/lib/resty/session.lua index ce766f32..15f92867 100644 --- a/lib/resty/session.lua +++ b/lib/resty/session.lua @@ -148,6 +148,8 @@ local DEFAULT_FLAGS local DEFAULT_REQUEST_HEADERS local DEFAULT_RESPONSE_HEADERS local DEFAULT_STORAGE +local DEFAULT_REVOCATION +local DEFAULT_REVOCATION_FAIL_MODE local DUMMY_META = {} @@ -372,6 +374,110 @@ local function get_store_ttl(self, remember, current_time, creation_time, rollin end +local load_revocation do + local REDIS + + load_revocation = function(configuration) + if not configuration then + return nil + end + + if configuration.storage then + return nil + end + + local redis_cfg = configuration.redis or configuration + if not redis_cfg or not redis_cfg.host then + return nil + end + + if not REDIS then + REDIS = require("resty.session.redis") + end + + return REDIS.new(redis_cfg) + end +end + + +local REVOCATION_MARK = "1" + + +local function handle_revocation_error(self, err, msg) + if self.revocation_fail_mode == "open" then + log(WARN, "[session] ", msg, ": ", err) + return true + end + + return nil, errmsg(err, msg) +end + + +local function is_session_revoked(self, sid, cookie_name) + if self.storage or not sid then + return false, nil + end + + local revocation = self.revocation + if not revocation then + return false, nil + end + + local key, herr = self.hash_storage_key(sid) + if not key then + return nil, herr + end + + local current_time = time() + local data, err = revocation:get(cookie_name, key, current_time) + if err then + local ok, rerr = handle_revocation_error(self, err, "unable to check session revocation") + if not ok then + return nil, rerr + end + return false, nil + end + + if data == REVOCATION_MARK then + return true, nil + end + + return false, nil +end + + +local function mark_session_revoked(self, remember, meta) + if self.storage then + return true + end + + local revocation = self.revocation + if not revocation then + return true + end + + local sid = meta and meta.sid + if not sid then + return true + end + + local cookie_name = remember and self.remember_cookie_name or self.cookie_name + local key, herr = self.hash_storage_key(sid) + if not key then + return nil, herr + end + + local current_time = time() + local ttl = get_store_ttl(self, remember, current_time, meta.creation_time, meta.rolling_offset) + local ok, err = revocation:set(cookie_name, key, REVOCATION_MARK, ttl, current_time) + if not ok then + return handle_revocation_error(self, err, "unable to mark session revoked") + end + + return true +end + + local function get_store_metadata(self) if not self.store_metadata then @@ -716,6 +822,14 @@ local function open(self, remember, meta_only) end end + local revoked, err = is_session_revoked(self, sid, cookie_name) + if err then + return nil, err + end + if revoked then + return nil, "session revoked" + end + local data_index = self.data_index local audience = self.data[data_index][2] local initial_chunk, ciphertext, ciphertext_encoded, info_data do @@ -1253,6 +1367,11 @@ local function destroy(self, remember) local cookie_name_size = #cookie_name local storage = self.storage + local ok, err = mark_session_revoked(self, remember, meta) + if not ok then + return nil, err + end + local cookie_chunks = 1 local data_size = meta.data_size if not storage and data_size then @@ -2401,6 +2520,9 @@ local function opt(configuration, name, default) end end end + + elseif name == "revocation" then + value = load_revocation(configuration) end else @@ -2451,6 +2573,15 @@ local function opt(configuration, name, default) assert(t == "table", "invalid session storage") end end + + elseif name == "revocation" then + if value == false then + value = nil + + else + assert(type(value) == "table", "invalid session revocation") + value = load_revocation(value) + end end end @@ -2497,6 +2628,8 @@ function session.init(configuration) DEFAULT_REQUEST_HEADERS = opt(configuration, "request_headers") DEFAULT_RESPONSE_HEADERS = opt(configuration, "response_headers") DEFAULT_STORAGE = opt(configuration, "storage") + DEFAULT_REVOCATION = opt(configuration, "revocation") + DEFAULT_REVOCATION_FAIL_MODE = opt(configuration, "revocation_fail_mode", "open") end --- @@ -2553,6 +2686,8 @@ function session.new(configuration) local request_headers = opt(configuration, "request_headers", DEFAULT_REQUEST_HEADERS) local response_headers = opt(configuration, "response_headers", DEFAULT_RESPONSE_HEADERS) local storage = opt(configuration, "storage", DEFAULT_STORAGE) + local revocation = opt(configuration, "revocation", DEFAULT_REVOCATION) + local revocation_fail_mode = opt(configuration, "revocation_fail_mode", DEFAULT_REVOCATION_FAIL_MODE) if cookie_prefix == "__Host-" then cookie_name = cookie_prefix .. cookie_name @@ -2625,6 +2760,8 @@ function session.new(configuration) remember = remember, flags = flags, storage = storage, + revocation = revocation, + revocation_fail_mode = revocation_fail_mode, ikm = ikm, ikm_fallbacks = ikm_fallbacks, request_headers = request_headers, diff --git a/spec/06-revocation-1_spec.lua b/spec/06-revocation-1_spec.lua new file mode 100644 index 00000000..4407b370 --- /dev/null +++ b/spec/06-revocation-1_spec.lua @@ -0,0 +1,261 @@ +local session = require "resty.session" +local redis_storage = require "resty.session.redis" +local decode_base64url = require("resty.session.utils").decode_base64url +local encode_base64url = require("resty.session.utils").encode_base64url + + +local before_each = before_each +local lazy_setup = lazy_setup +local describe = describe +local assert = assert +local ipairs = ipairs +local sleep = ngx.sleep +local it = it + + +local redis_config = { + host = "127.0.0.1", + password = "password", +} + + +local function extract_cookie(cookie_name, cookies) + local session_cookie + if type(cookies) == "table" then + for _, v in ipairs(cookies) do + session_cookie = ngx.re.match(v, cookie_name .. "=([\\w-]+);") + if session_cookie then + return session_cookie[1] + end + end + return "" + end + session_cookie = ngx.re.match(cookies, cookie_name .. "=([\\w-]+);") + return session_cookie and session_cookie[1] or "" +end + + +describe("Revocation tests 1", function() + local store + local long_ttl = 60 + local short_ttl = 2 + local id = "test_id_1iiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiii" + local id1 = "test_id_2iiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiii" + local id2 = "test_id_3iiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiii" + local cookie = "session_cookie" + + lazy_setup(function() + store = redis_storage.new(redis_config) + assert.is_not_nil(store) + end) + + describe("[#redis] revocation: GET (not revoked)", function() + it("GET: missing revocation key returns not revoked marker", function() + local data, err = store:get(cookie, encode_base64url(id), ngx.time()) + assert.is_nil(err) + assert.is_not_equal("1", data) + end) + end) + + describe("[#redis] revocation: SET + GET", function() + it("SET: stores revocation mark and GET observes it", function() + local ok, err = store:set(cookie, encode_base64url(id1), "1", long_ttl, ngx.time()) + assert.is_not_nil(ok) + assert.is_nil(err) + + local data + data, err = store:get(cookie, encode_base64url(id1), ngx.time()) + assert.is_nil(err) + assert.equals("1", data) + end) + + it("SET: ttl expires revocation entry", function() + local ok, err = store:set(cookie, encode_base64url(id2), "1", short_ttl, ngx.time()) + assert.is_not_nil(ok) + assert.is_nil(err) + + local data + data, err = store:get(cookie, encode_base64url(id2), ngx.time()) + assert.is_nil(err) + assert.equals("1", data) + + sleep(short_ttl + 1) + + data, err = store:get(cookie, encode_base64url(id2), ngx.time()) + assert.is_nil(err) + assert.is_not_equal("1", data) + end) + end) + + describe("session: configuration", function() + local configuration = {} + local cookie_name = "session_cookie" + + before_each(function() + configuration = { + cookie_name = cookie_name, + redis = redis_config, + } + session.init(configuration) + end) + + it("auto-loads revocation from redis configuration", function() + local s = session.new() + assert.is_not_nil(s.revocation) + assert.is_function(s.revocation.set) + assert.is_function(s.revocation.get) + end) + + it("does not load revocation without a redis host", function() + session.init({ + cookie_name = cookie_name, + redis = { password = "password" }, + }) + + local s = session.new() + assert.is_nil(s.revocation) + end) + + it("skips revocation when storage backend is configured", function() + session.init({ + cookie_name = cookie_name, + storage = "redis", + redis = { + prefix = "sessions", + password = "password", + }, + }) + + local s = session.new() + assert.is_nil(s.revocation) + end) + end) + + describe("session: destroy", function() + local configuration = {} + local cookie_name = "session_cookie" + local test_key = "test_key" + local value = "test_data" + + local function save_session(s, cookies) + session.__set_ngx_header(cookies) + s:set(test_key, value) + local ok, err = s:save() + assert.is_true(ok) + assert.is_nil(err) + return extract_cookie(cookie_name, cookies["Set-Cookie"]) + end + + local function open_session(session_cookie) + local s = session.new() + session.__set_ngx_var({ + ["cookie_" .. cookie_name] = session_cookie, + }) + + local ok, err = s:open() + if not ok then + return nil, err + end + + return s + end + + before_each(function() + configuration = { + cookie_name = cookie_name, + redis = redis_config, + } + session.init(configuration) + end) + + it("destroy: rejected cookie cannot be reopened", function() + local cookies = {} + local s = session.new() + local session_cookie = save_session(s, cookies) + assert.is_not_equal("", session_cookie) + + s:close() + + local s2, err = open_session(session_cookie) + assert.is_not_nil(s2) + assert.is_nil(err) + assert.equals(value, s2:get(test_key)) + + session.__set_ngx_header(cookies) + local ok + ok, err = s2:destroy() + assert.is_true(ok) + assert.is_nil(err) + + local s3 + s3, err = open_session(session_cookie) + assert.is_nil(s3) + assert.equals("session revoked", err) + end) + end) + + describe("session: open", function() + local configuration = {} + local cookie_name = "session_cookie" + local test_key = "test_key" + local value = "test_data" + + local function save_session(s, cookies) + session.__set_ngx_header(cookies) + s:set(test_key, value) + local ok, err = s:save() + assert.is_true(ok) + assert.is_nil(err) + return extract_cookie(cookie_name, cookies["Set-Cookie"]) + end + + local function open_session(session_cookie) + local s = session.new() + session.__set_ngx_var({ + ["cookie_" .. cookie_name] = session_cookie, + }) + + local ok, err = s:open() + if not ok then + return nil, err + end + + return s + end + + before_each(function() + configuration = { + cookie_name = cookie_name, + redis = redis_config, + } + session.init(configuration) + end) + + it("open: revoked identifier is rejected in closed fail mode", function() + local cookies = {} + local s = session.new() + local session_cookie = save_session(s, cookies) + s:close() + + local s2, err = open_session(session_cookie) + assert.is_not_nil(s2) + assert.is_nil(err) + + local identifier = decode_base64url(s2:get_property("id")) + local storage_key, skerr = s2.hash_storage_key(identifier) + assert.is_not_nil(storage_key) + assert.is_nil(skerr) + local ok + ok, err = s2.revocation:set(s2.cookie_name, storage_key, "1", long_ttl, ngx.time()) + assert.is_not_nil(ok) + assert.is_nil(err) + + s2:close() + + local s3 + s3, err = open_session(session_cookie) + assert.is_nil(s3) + assert.equals("session revoked", err) + end) + end) +end) diff --git a/spec/07-revocation-2_spec.lua b/spec/07-revocation-2_spec.lua new file mode 100644 index 00000000..12c6c880 --- /dev/null +++ b/spec/07-revocation-2_spec.lua @@ -0,0 +1,281 @@ +local session = require "resty.session" +local redis_storage = require "resty.session.redis" +local encode_base64url = require("resty.session.utils").encode_base64url + + +local before_each = before_each +local describe = describe +local assert = assert +local pcall = pcall +local ipairs = ipairs +local it = it + + +local redis_config = { + host = "127.0.0.1", + password = "password", +} + + +local bad_redis_config = { + host = "127.0.0.1", + port = 1, + password = "password", + connect_timeout = 100, + send_timeout = 100, + read_timeout = 100, +} + + +local function extract_cookie(cookie_name, cookies) + local session_cookie + if type(cookies) == "table" then + for _, v in ipairs(cookies) do + session_cookie = ngx.re.match(v, cookie_name .. "=([\\w-]+);") + if session_cookie then + return session_cookie[1] + end + end + return "" + end + session_cookie = ngx.re.match(cookies, cookie_name .. "=([\\w-]+);") + return session_cookie and session_cookie[1] or "" +end + + +describe("Revocation tests 2", function() + local long_ttl = 60 + local id = "test_id_1iiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiii" + local id1 = "test_id_2iiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiii" + local cookie = "session_cookie" + + describe("[#redis] revocation: failures", function() + it("SET: connection failure returns error", function() + local bad_store = redis_storage.new(bad_redis_config) + + local ok, err = bad_store:set(cookie, encode_base64url(id), "1", long_ttl, ngx.time()) + assert.is_nil(ok) + assert.is_not_nil(err) + end) + + it("GET: connection failure returns error", function() + local bad_store = redis_storage.new(bad_redis_config) + + local data, err = bad_store:get(cookie, encode_base64url(id1), ngx.time()) + assert.is_nil(data) + assert.is_not_nil(err) + end) + end) + + describe("session: Fields validation", function() + local configuration = {} + local cookie_name = "session_cookie" + + before_each(function() + configuration = { + cookie_name = cookie_name, + redis = redis_config, + } + session.init(configuration) + end) + + it("new validates revocation configuration", function() + local ok, err = pcall(session.new, { + revocation = "redis", + }) + assert.is_false(ok) + assert.matches("invalid session revocation", err) + end) + end) + + describe("session: open", function() + local configuration = {} + local cookie_name = "session_cookie" + local test_key = "test_key" + local value = "test_data" + + local function save_session(s, cookies) + session.__set_ngx_header(cookies) + s:set(test_key, value) + local ok, err = s:save() + assert.is_true(ok) + assert.is_nil(err) + return extract_cookie(cookie_name, cookies["Set-Cookie"]) + end + + local function open_session(session_cookie) + local s = session.new() + session.__set_ngx_var({ + ["cookie_" .. cookie_name] = session_cookie, + }) + + local ok, err = s:open() + if not ok then + return nil, err + end + + return s + end + + before_each(function() + configuration = { + cookie_name = cookie_name, + redis = redis_config, + } + session.init(configuration) + end) + + it("open: closed fail mode rejects when redis is unreachable", function() + local cookies = {} + local s = session.new() + local session_cookie = save_session(s, cookies) + s:close() + + session.init({ + cookie_name = cookie_name, + redis = bad_redis_config, + revocation_fail_mode = "closed", + }) + + local opened, err = open_session(session_cookie) + assert.is_nil(opened) + assert.matches("unable to check session revocation", err) + end) + end) + + describe("session: revocation_fail_mode", function() + local configuration = {} + local cookie_name = "session_cookie" + local test_key = "test_key" + local value = "test_data" + local session_cookie + local cookies + + local function save_session(s, cookies) + session.__set_ngx_header(cookies) + s:set(test_key, value) + local ok, err = s:save() + assert.is_true(ok) + assert.is_nil(err) + return extract_cookie(cookie_name, cookies["Set-Cookie"]) + end + + local function open_session(session_cookie) + local s = session.new() + session.__set_ngx_var({ + ["cookie_" .. cookie_name] = session_cookie, + }) + + local ok, err = s:open() + if not ok then + return nil, err + end + + return s + end + + before_each(function() + configuration = { + cookie_name = cookie_name, + redis = redis_config, + } + session.init(configuration) + + cookies = {} + local s = session.new() + session_cookie = save_session(s, cookies) + s:close() + end) + + it("destroy: closed fail mode fails when marking revoked fails", function() + local s, err = open_session(session_cookie) + assert.is_not_nil(s) + assert.is_nil(err) + + s.revocation = { + set = function() + return nil, "connection refused" + end, + get = function() + return nil + end, + } + s.revocation_fail_mode = "closed" + + session.__set_ngx_header(cookies) + local ok + ok, err = s:destroy() + assert.is_nil(ok) + assert.matches("unable to mark session revoked", err) + assert.equals("open", s.state) + end) + + it("destroy: open fail mode succeeds when marking revoked fails", function() + local s, err = open_session(session_cookie) + assert.is_not_nil(s) + assert.is_nil(err) + + s.revocation = { + set = function() + return nil, "connection refused" + end, + get = function() + return nil + end, + } + s.revocation_fail_mode = "open" + + session.__set_ngx_header(cookies) + local ok + ok, err = s:destroy() + assert.is_true(ok) + assert.is_nil(err) + assert.equals("closed", s.state) + end) + + it("open: closed fail mode fails when checking revocation fails", function() + local s = session.new({ + revocation = { + set = function() + return true + end, + get = function() + return nil, "connection refused" + end, + }, + revocation_fail_mode = "closed", + }) + + session.__set_ngx_var({ + ["cookie_" .. cookie_name] = session_cookie, + }) + + local opened, err = s:open() + assert.is_nil(opened) + assert.matches("unable to check session revocation", err) + end) + + it("open: open fail mode succeeds when checking revocation fails", function() + local s = session.new({ + revocation = { + set = function() + return true + end, + get = function() + return nil, "connection refused" + end, + }, + revocation_fail_mode = "open", + }) + + session.__set_ngx_var({ + ["cookie_" .. cookie_name] = session_cookie, + }) + + local opened, err = s:open() + assert.is_true(opened) + assert.is_nil(err) + assert.equals(value, s:get(test_key)) + end) + end) +end) From 39df6f79b12335e013cebae9e5576b08c5a457f9 Mon Sep 17 00:00:00 2001 From: Austin Hockenberry Date: Tue, 30 Jun 2026 14:21:31 -0400 Subject: [PATCH 2/7] Addressing comments --- README.md | 54 ++++++++++++++++++++++++++++++ lib/resty/session.lua | 62 +++++++++++++++++++++-------------- lib/resty/session/utils.lua | 23 ++++++++++--- spec/06-revocation-1_spec.lua | 32 ++++++++++++++++++ 4 files changed, 141 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index 8f3f2e45..3a5ddb18 100644 --- a/README.md +++ b/README.md @@ -179,6 +179,7 @@ http { * [Configuration](#configuration) * [Session Configuration](#session-configuration) * [Cookie Storage Configuration](#cookie-storage-configuration) + * [Session Revocation Configuration](#session-revocation-configuration) * [DSHM Storage Configuration](#dshm-storage-configuration) * [File Storage Configuration](#file-storage-configuration) * [Memcached Storage Configuration](#memcached-storage-configuration) @@ -327,6 +328,8 @@ Here are the possible session configuration options: | `request_headers` | `nil` | Set of headers to send to upstream, use `id`, `audience`, `subject`, `timeout`, `idling-timeout`, `rolling-timeout`, `absolute-timeout`. E.g. `{ "id", "timeout" }` will set `Session-Id` and `Session-Timeout` request headers when `set_headers` is called. | | `response_headers` | `nil` | Set of headers to send to downstream, use `id`, `audience`, `subject`, `timeout`, `idling-timeout`, `rolling-timeout`, `absolute-timeout`. E.g. `{ "id", "timeout" }` will set `Session-Id` and `Session-Timeout` response headers when `set_headers` is called. | | `storage` | `nil` | Storage is responsible of storing session data, use `nil` or `"cookie"` (data is stored in cookie), `"dshm"`, `"file"`, `"memcached"`, `"mysql"`, `"postgres"`, `"redis"`, or `"shm"`, or give a name of custom module (`"custom-storage"`), or a `table` that implements session storage interface. | +| `revocation` | `nil` | Enable Redis-backed session revocation for cookie (stateless) sessions, use `nil`, `true`, `false`, a Redis configuration `table`, or a `table` that implements the revocation store interface (see below). | +| `revocation_fail_mode` | `"open"` | Behavior when the revocation store is unreachable, use `"open"` (treat as not revoked) or `"closed"` (reject the session). | | `dshm` | `nil` | Configuration for dshm storage, e.g. `{ prefix = "sessions" }` (see below) | | `file` | `nil` | Configuration for file storage, e.g. `{ path = "/tmp", suffix = "session" }` (see below) | | `memcached` | `nil` | Configuration for memcached storage, e.g. `{ prefix = "sessions" }` (see below) | @@ -343,6 +346,56 @@ When storing data to cookie, there is no additional configuration required, just set the `storage` to `nil` or `"cookie"`. +## Session Revocation Configuration + +Cookie (stateless) sessions are self-contained: once issued, a cookie remains +valid until it expires according to the configured timeouts. Revocation adds +an optional Redis-backed denylist so that destroyed sessions are rejected +immediately, without waiting for the cookie to expire. + +Revocation is only available when session data is stored in the cookie +(`storage` is `nil` or `"cookie"`). It must be enabled explicitly with +`revocation = true` (using the `redis` configuration) or +`revocation = { ... }` (inline Redis or custom store settings). Setting +`revocation = false` disables it. + +On every `session:open`, the library checks whether the session identifier is +revoked. On `session:destroy`, the identifier is written to Redis with a TTL +equal to the remaining session lifetime (rolling and absolute timeouts). The +revocation mark is a lightweight sentinel; no session payload is stored in +Redis. + +Use `revocation_fail_mode` to control behavior when Redis is unreachable: + +- `"open"` (default): log a warning and treat the session as not revoked. + Destroy still clears the cookie even if the revocation write fails. +- `"closed"`: reject the session open or destroy operation. + +Revocation applies to `session:destroy` (and `session:logout` when it destroys +the last audience). It does not revoke the previous session identifier on +`session:save` (session rotation) or partial `session:logout` (multiple +audiences). After rotation or partial logout, the previous cookie remains +usable until its `stale_ttl` or timeout elapses. + +Example: + +```lua +require("resty.session").init({ + storage = "cookie", + revocation = true, + redis = { + host = "127.0.0.1", + password = "secret", + prefix = "sessions", + }, +}) +``` + +The `redis.mode` setting selects whether a Redis connection is used for +session data (`"storage"`) or for revocation (`"revocation"`). When unset, +it defaults to `"revocation"` for cookie storage and `"storage"` otherwise. + + ## DSHM Storage Configuration With DHSM storage you can use the following settings (set the `storage` to `"dshm"`): @@ -529,6 +582,7 @@ connections. Common configuration settings among them all: | Option | Default | Description | |---------------------|:-------:|----------------------------------------------------------------------------------------------| +| `mode` | `nil` | Role of this Redis connection: `"storage"` for session data or `"revocation"` for the session denylist. Defaults to `"revocation"` when `storage` is `nil` or `"cookie"`, otherwise `"storage"`. | | `prefix` | `nil` | Prefix for the keys stored in Redis. | | `suffix` | `nil` | Suffix for the keys stored in Redis. | | `username` | `nil` | The database username to authenticate. | diff --git a/lib/resty/session.lua b/lib/resty/session.lua index 15f92867..4470c5a2 100644 --- a/lib/resty/session.lua +++ b/lib/resty/session.lua @@ -44,6 +44,7 @@ local encode_base64url = utils.encode_base64url local decode_base64url = utils.decode_base64url local table_is_empty = utils.is_empty_table local load_storage = utils.load_storage +local load_redis = utils.load_redis local encode_json = utils.encode_json local decode_json = utils.decode_json local base64_size = utils.base64_size @@ -374,29 +375,26 @@ local function get_store_ttl(self, remember, current_time, creation_time, rollin end -local load_revocation do - local REDIS - - load_revocation = function(configuration) - if not configuration then - return nil - end - - if configuration.storage then - return nil - end +local function load_revocation(configuration) + if not configuration then + return nil + end - local redis_cfg = configuration.redis or configuration - if not redis_cfg or not redis_cfg.host then - return nil - end + local session_storage = configuration.storage + if session_storage and session_storage ~= "cookie" then + return nil + end - if not REDIS then - REDIS = require("resty.session.redis") - end + local redis_cfg = configuration.redis + if not redis_cfg or not redis_cfg.host then + return nil + end - return REDIS.new(redis_cfg) + if redis_cfg.mode == "storage" then + return nil end + + return load_redis(redis_cfg) end @@ -478,7 +476,6 @@ local function mark_session_revoked(self, remember, meta) end - local function get_store_metadata(self) if not self.store_metadata then return @@ -2460,6 +2457,8 @@ local session = { -- @field request_headers Set of headers to send to upstream, use `id`, `audience`, `subject`, `timeout`, `idling-timeout`, `rolling-timeout`, `absolute-timeout`. E.g. `{ "id", "timeout" }` will set `Session-Id` and `Session-Timeout` request headers when `set_headers` is called. -- @field response_headers Set of headers to send to downstream, use `id`, `audience`, `subject`, `timeout`, `idling-timeout`, `rolling-timeout`, `absolute-timeout`. E.g. `{ "id", "timeout" }` will set `Session-Id` and `Session-Timeout` response headers when `set_headers` is called. -- @field storage Storage is responsible of storing session data, use `nil` or `"cookie"` (data is stored in cookie), `"dshm"`, `"file"`, `"memcached"`, `"mysql"`, `"postgres"`, `"redis"`, or `"shm"`, or give a name of custom module (`"custom-storage"`), or a `table` that implements session storage interface (defaults to `nil`) +-- @field revocation Enable Redis-backed session revocation for cookie (stateless) sessions, use `nil`, `true`, `false`, a Redis configuration `table`, or a `table` that implements the revocation store interface (defaults to `nil`) +-- @field revocation_fail_mode Behavior when the revocation store is unreachable, use `"open"` (treat as not revoked) or `"closed"` (reject the session) (defaults to `"open"`) -- @field dshm Configuration for dshm storage, e.g. `{ prefix = "sessions" }` -- @field file Configuration for file storage, e.g. `{ path = "/tmp", suffix = "session" }` -- @field memcached Configuration for memcached storage, e.g. `{ prefix = "sessions" }` @@ -2521,8 +2520,6 @@ local function opt(configuration, name, default) end end - elseif name == "revocation" then - value = load_revocation(configuration) end else @@ -2575,13 +2572,28 @@ local function opt(configuration, name, default) end elseif name == "revocation" then - if value == false then + if value == true then + value = assert(load_revocation(configuration), "unable to load session revocation") + + elseif value == false then value = nil + elseif type(value) == "table" then + if type(value.set) == "function" and type(value.get) == "function" then + -- custom revocation store + else + value = assert(load_revocation({ + storage = configuration and configuration.storage, + redis = value, + }), "unable to load session revocation") + end + else - assert(type(value) == "table", "invalid session revocation") - value = load_revocation(value) + error("invalid session revocation") end + + elseif name == "revocation_fail_mode" then + assert(value == "open" or value == "closed", "invalid revocation fail mode") end end diff --git a/lib/resty/session/utils.lua b/lib/resty/session/utils.lua index b5b88749..b8c6dddd 100644 --- a/lib/resty/session/utils.lua +++ b/lib/resty/session/utils.lua @@ -859,13 +859,26 @@ end +local load_redis do + local REDIS + + load_redis = function(cfg) + if not REDIS then + REDIS = require("resty.session.redis") + end + + return REDIS.new(cfg) + end +end + + + local load_storage do local DSHM local FILE local MEMCACHED local MYSQL local POSTGRES - local REDIS local REDIS_SENTINEL local REDIS_CLUSTER local SHM @@ -923,6 +936,8 @@ local load_storage do elseif storage == "redis" then local cfg = configuration and configuration.redis if cfg then + assert(cfg.mode ~= "revocation", "invalid redis mode for session storage") + if cfg.nodes then if not REDIS_CLUSTER then REDIS_CLUSTER = require("resty.session.redis.cluster") @@ -937,10 +952,7 @@ local load_storage do end end - if not REDIS then - REDIS = require("resty.session.redis") - end - return REDIS.new(cfg) + return load_redis(cfg) elseif storage == "shm" then if not SHM then @@ -1195,6 +1207,7 @@ return { decrypt_aes_256_gcm = decrypt_aes_256_gcm, hmac_sha256 = hmac_sha256, load_storage = load_storage, + load_redis = load_redis, errmsg = errmsg, get_name = get_name, set_flag = set_flag, diff --git a/spec/06-revocation-1_spec.lua b/spec/06-revocation-1_spec.lua index 4407b370..7c27f0af 100644 --- a/spec/06-revocation-1_spec.lua +++ b/spec/06-revocation-1_spec.lua @@ -129,6 +129,38 @@ describe("Revocation tests 1", function() local s = session.new() assert.is_nil(s.revocation) end) + + it("loads revocation when storage is cookie and redis mode is revocation", function() + session.init({ + cookie_name = cookie_name, + storage = "cookie", + redis = { + host = redis_config.host, + password = redis_config.password, + mode = "revocation", + }, + }) + + local s = session.new() + assert.is_not_nil(s.revocation) + assert.is_function(s.revocation.set) + assert.is_function(s.revocation.get) + end) + + it("skips revocation when storage is cookie and redis mode is storage", function() + session.init({ + cookie_name = cookie_name, + storage = "cookie", + redis = { + host = redis_config.host, + password = redis_config.password, + mode = "storage", + }, + }) + + local s = session.new() + assert.is_nil(s.revocation) + end) end) describe("session: destroy", function() From b14a41ab81d82338448ca567c75d637b6349505a Mon Sep 17 00:00:00 2001 From: Austin Hockenberry Date: Tue, 30 Jun 2026 15:14:01 -0400 Subject: [PATCH 3/7] Make revokation load generic --- lib/resty/session.lua | 59 +++++++++--------------- lib/resty/session/utils.lua | 85 +++++++++++++++++++++++++++++++++++ spec/07-revocation-2_spec.lua | 12 ++++- 3 files changed, 116 insertions(+), 40 deletions(-) diff --git a/lib/resty/session.lua b/lib/resty/session.lua index 4470c5a2..2e565afc 100644 --- a/lib/resty/session.lua +++ b/lib/resty/session.lua @@ -44,7 +44,7 @@ local encode_base64url = utils.encode_base64url local decode_base64url = utils.decode_base64url local table_is_empty = utils.is_empty_table local load_storage = utils.load_storage -local load_redis = utils.load_redis +local load_revocation = utils.load_revocation local encode_json = utils.encode_json local decode_json = utils.decode_json local base64_size = utils.base64_size @@ -375,29 +375,6 @@ local function get_store_ttl(self, remember, current_time, creation_time, rollin end -local function load_revocation(configuration) - if not configuration then - return nil - end - - local session_storage = configuration.storage - if session_storage and session_storage ~= "cookie" then - return nil - end - - local redis_cfg = configuration.redis - if not redis_cfg or not redis_cfg.host then - return nil - end - - if redis_cfg.mode == "storage" then - return nil - end - - return load_redis(redis_cfg) -end - - local REVOCATION_MARK = "1" @@ -2457,7 +2434,7 @@ local session = { -- @field request_headers Set of headers to send to upstream, use `id`, `audience`, `subject`, `timeout`, `idling-timeout`, `rolling-timeout`, `absolute-timeout`. E.g. `{ "id", "timeout" }` will set `Session-Id` and `Session-Timeout` request headers when `set_headers` is called. -- @field response_headers Set of headers to send to downstream, use `id`, `audience`, `subject`, `timeout`, `idling-timeout`, `rolling-timeout`, `absolute-timeout`. E.g. `{ "id", "timeout" }` will set `Session-Id` and `Session-Timeout` response headers when `set_headers` is called. -- @field storage Storage is responsible of storing session data, use `nil` or `"cookie"` (data is stored in cookie), `"dshm"`, `"file"`, `"memcached"`, `"mysql"`, `"postgres"`, `"redis"`, or `"shm"`, or give a name of custom module (`"custom-storage"`), or a `table` that implements session storage interface (defaults to `nil`) --- @field revocation Enable Redis-backed session revocation for cookie (stateless) sessions, use `nil`, `true`, `false`, a Redis configuration `table`, or a `table` that implements the revocation store interface (defaults to `nil`) +-- @field revocation Session revocation backend for cookie (stateless) sessions, use `nil` (auto-load from `redis` when configured), `false` to disable, `"redis"`, `true` (alias for `"redis"`), or a pre-built store `table` with `set`/`get` methods (defaults to `nil`) -- @field revocation_fail_mode Behavior when the revocation store is unreachable, use `"open"` (treat as not revoked) or `"closed"` (reject the session) (defaults to `"open"`) -- @field dshm Configuration for dshm storage, e.g. `{ prefix = "sessions" }` -- @field file Configuration for file storage, e.g. `{ path = "/tmp", suffix = "session" }` @@ -2520,6 +2497,9 @@ local function opt(configuration, name, default) end end + elseif name == "revocation" then + value = load_revocation(nil, configuration) + end else @@ -2572,24 +2552,25 @@ local function opt(configuration, name, default) end elseif name == "revocation" then - if value == true then - value = assert(load_revocation(configuration), "unable to load session revocation") - - elseif value == false then + if value == false then value = nil - elseif type(value) == "table" then - if type(value.set) == "function" and type(value.get) == "function" then - -- custom revocation store + else + local t = type(value) + if t == "string" then + value = assert(load_revocation(value, configuration), "unable to load session revocation") + + elseif value == true then + value = assert(load_revocation("redis", configuration), "unable to load session revocation") + + elseif t == "table" then + if type(value.set) ~= "function" or type(value.get) ~= "function" then + error("invalid session revocation") + end + else - value = assert(load_revocation({ - storage = configuration and configuration.storage, - redis = value, - }), "unable to load session revocation") + error("invalid session revocation") end - - else - error("invalid session revocation") end elseif name == "revocation_fail_mode" then diff --git a/lib/resty/session/utils.lua b/lib/resty/session/utils.lua index b8c6dddd..b40822f4 100644 --- a/lib/resty/session/utils.lua +++ b/lib/resty/session/utils.lua @@ -971,6 +971,90 @@ local load_storage do end + +local load_revocation do + local CUSTOM = {} + + --- + -- Loads session revocation store and creates a new instance using session configuration. + -- + -- @function utils.load_revocation + -- @tparam nil|boolean|string revocation revocation store name, `nil` to auto-load from + -- `redis` when configured for revocation, `true` for `"redis"`, or `false` to disable + -- @tparam[opt] table configuration session configuration + -- @treturn table|nil instance of session revocation store + -- @treturn string|nil error message + -- + -- @usage + -- local redis = require("resty.session.utils").load_revocation("redis", { + -- redis = { + -- host = "127.0.0.1", + -- } + -- }) + load_revocation = function(revocation, configuration) + if revocation == false then + return nil + end + + if not revocation then + if not configuration then + return nil + end + + local redis_cfg = configuration.redis + if redis_cfg and redis_cfg.host and redis_cfg.mode ~= "storage" then + local session_storage = configuration.storage + if not session_storage or session_storage == "cookie" then + revocation = "redis" + end + end + + if not revocation then + return nil + end + end + + if revocation == true then + revocation = "redis" + end + + if type(revocation) ~= "string" then + error("invalid session revocation") + end + + if revocation == "cookie" then + return nil + end + + local session_storage = configuration and configuration.storage + if session_storage and session_storage ~= "cookie" then + return nil + end + + if revocation == "redis" then + local cfg = configuration and configuration.redis + if not cfg or not cfg.host then + return nil + end + + if cfg.mode == "storage" then + return nil + end + + return load_redis(cfg) + + else + if not CUSTOM[revocation] then + CUSTOM[revocation] = require(revocation) + end + + return CUSTOM[revocation].new(configuration and configuration[revocation]) + end + end +end + + + --- -- Helper to format error messages. -- @@ -1208,6 +1292,7 @@ return { hmac_sha256 = hmac_sha256, load_storage = load_storage, load_redis = load_redis, + load_revocation = load_revocation, errmsg = errmsg, get_name = get_name, set_flag = set_flag, diff --git a/spec/07-revocation-2_spec.lua b/spec/07-revocation-2_spec.lua index 12c6c880..04bf1288 100644 --- a/spec/07-revocation-2_spec.lua +++ b/spec/07-revocation-2_spec.lua @@ -79,9 +79,19 @@ describe("Revocation tests 2", function() session.init(configuration) end) + it("new loads redis revocation from string configuration", function() + local s = session.new({ + revocation = "redis", + redis = redis_config, + }) + assert.is_not_nil(s.revocation) + assert.is_function(s.revocation.set) + assert.is_function(s.revocation.get) + end) + it("new validates revocation configuration", function() local ok, err = pcall(session.new, { - revocation = "redis", + revocation = 123, }) assert.is_false(ok) assert.matches("invalid session revocation", err) From 620157a14122c7a64e5e2a8d2efea8435671647e Mon Sep 17 00:00:00 2001 From: Austin Hockenberry Date: Wed, 1 Jul 2026 09:12:39 -0400 Subject: [PATCH 4/7] load_redis not needed --- lib/resty/session/utils.lua | 27 ++++++++++----------------- 1 file changed, 10 insertions(+), 17 deletions(-) diff --git a/lib/resty/session/utils.lua b/lib/resty/session/utils.lua index b40822f4..40d0b44a 100644 --- a/lib/resty/session/utils.lua +++ b/lib/resty/session/utils.lua @@ -859,26 +859,13 @@ end -local load_redis do - local REDIS - - load_redis = function(cfg) - if not REDIS then - REDIS = require("resty.session.redis") - end - - return REDIS.new(cfg) - end -end - - - local load_storage do local DSHM local FILE local MEMCACHED local MYSQL local POSTGRES + local REDIS local REDIS_SENTINEL local REDIS_CLUSTER local SHM @@ -952,7 +939,10 @@ local load_storage do end end - return load_redis(cfg) + if not REDIS then + REDIS = require("resty.session.redis") + end + return REDIS.new(cfg) elseif storage == "shm" then if not SHM then @@ -973,6 +963,7 @@ end local load_revocation do + local REDIS local CUSTOM = {} --- @@ -1041,7 +1032,10 @@ local load_revocation do return nil end - return load_redis(cfg) + if not REDIS then + REDIS = require("resty.session.redis") + end + return REDIS.new(cfg) else if not CUSTOM[revocation] then @@ -1291,7 +1285,6 @@ return { decrypt_aes_256_gcm = decrypt_aes_256_gcm, hmac_sha256 = hmac_sha256, load_storage = load_storage, - load_redis = load_redis, load_revocation = load_revocation, errmsg = errmsg, get_name = get_name, From ad5d068a23977a660746a39a78cccde0ca2401d8 Mon Sep 17 00:00:00 2001 From: Austin Hockenberry Date: Wed, 1 Jul 2026 09:16:50 -0400 Subject: [PATCH 5/7] refactor load_revokation --- lib/resty/session/utils.lua | 42 +++++++++++-------------------------- 1 file changed, 12 insertions(+), 30 deletions(-) diff --git a/lib/resty/session/utils.lua b/lib/resty/session/utils.lua index 40d0b44a..f401ae57 100644 --- a/lib/resty/session/utils.lua +++ b/lib/resty/session/utils.lua @@ -983,29 +983,25 @@ local load_revocation do -- } -- }) load_revocation = function(revocation, configuration) - if revocation == false then + if revocation == false or revocation == "cookie" then return nil end - if not revocation then - if not configuration then - return nil - end + if revocation == true then + revocation = "redis" + end - local redis_cfg = configuration.redis - if redis_cfg and redis_cfg.host and redis_cfg.mode ~= "storage" then - local session_storage = configuration.storage - if not session_storage or session_storage == "cookie" then - revocation = "redis" - end - end + local session_storage = configuration and configuration.storage + if session_storage and session_storage ~= "cookie" then + return nil + end - if not revocation then + if not revocation then + local redis_cfg = configuration and configuration.redis + if not redis_cfg or not redis_cfg.host or redis_cfg.mode == "storage" then return nil end - end - if revocation == true then revocation = "redis" end @@ -1013,22 +1009,9 @@ local load_revocation do error("invalid session revocation") end - if revocation == "cookie" then - return nil - end - - local session_storage = configuration and configuration.storage - if session_storage and session_storage ~= "cookie" then - return nil - end - if revocation == "redis" then local cfg = configuration and configuration.redis - if not cfg or not cfg.host then - return nil - end - - if cfg.mode == "storage" then + if not cfg or not cfg.host or cfg.mode == "storage" then return nil end @@ -1041,7 +1024,6 @@ local load_revocation do if not CUSTOM[revocation] then CUSTOM[revocation] = require(revocation) end - return CUSTOM[revocation].new(configuration and configuration[revocation]) end end From 199727357c4c9d33e475f7aeeeee33ed043d1d72 Mon Sep 17 00:00:00 2001 From: Austin Hockenberry Date: Thu, 2 Jul 2026 21:42:35 -0400 Subject: [PATCH 6/7] test(revocation): align specs with redis.mode configuration Drop redundant open tests and post-open revocation mutation; exercise revocation via redis.mode and real Redis integration where possible. Co-authored-by: Cursor --- spec/06-revocation-1_spec.lua | 68 +------------------ spec/07-revocation-2_spec.lua | 122 +++++++++++++++------------------- 2 files changed, 55 insertions(+), 135 deletions(-) diff --git a/spec/06-revocation-1_spec.lua b/spec/06-revocation-1_spec.lua index 7c27f0af..650b2de8 100644 --- a/spec/06-revocation-1_spec.lua +++ b/spec/06-revocation-1_spec.lua @@ -1,6 +1,5 @@ local session = require "resty.session" local redis_storage = require "resty.session.redis" -local decode_base64url = require("resty.session.utils").decode_base64url local encode_base64url = require("resty.session.utils").encode_base64url @@ -99,7 +98,7 @@ describe("Revocation tests 1", function() session.init(configuration) end) - it("auto-loads revocation from redis configuration", function() + it("loads revocation when cookie storage has redis without storage mode", function() local s = session.new() assert.is_not_nil(s.revocation) assert.is_function(s.revocation.set) @@ -225,69 +224,4 @@ describe("Revocation tests 1", function() assert.equals("session revoked", err) end) end) - - describe("session: open", function() - local configuration = {} - local cookie_name = "session_cookie" - local test_key = "test_key" - local value = "test_data" - - local function save_session(s, cookies) - session.__set_ngx_header(cookies) - s:set(test_key, value) - local ok, err = s:save() - assert.is_true(ok) - assert.is_nil(err) - return extract_cookie(cookie_name, cookies["Set-Cookie"]) - end - - local function open_session(session_cookie) - local s = session.new() - session.__set_ngx_var({ - ["cookie_" .. cookie_name] = session_cookie, - }) - - local ok, err = s:open() - if not ok then - return nil, err - end - - return s - end - - before_each(function() - configuration = { - cookie_name = cookie_name, - redis = redis_config, - } - session.init(configuration) - end) - - it("open: revoked identifier is rejected in closed fail mode", function() - local cookies = {} - local s = session.new() - local session_cookie = save_session(s, cookies) - s:close() - - local s2, err = open_session(session_cookie) - assert.is_not_nil(s2) - assert.is_nil(err) - - local identifier = decode_base64url(s2:get_property("id")) - local storage_key, skerr = s2.hash_storage_key(identifier) - assert.is_not_nil(storage_key) - assert.is_nil(skerr) - local ok - ok, err = s2.revocation:set(s2.cookie_name, storage_key, "1", long_ttl, ngx.time()) - assert.is_not_nil(ok) - assert.is_nil(err) - - s2:close() - - local s3 - s3, err = open_session(session_cookie) - assert.is_nil(s3) - assert.equals("session revoked", err) - end) - end) end) diff --git a/spec/07-revocation-2_spec.lua b/spec/07-revocation-2_spec.lua index 04bf1288..089d524d 100644 --- a/spec/07-revocation-2_spec.lua +++ b/spec/07-revocation-2_spec.lua @@ -27,6 +27,16 @@ local bad_redis_config = { } +local function revocation_redis(config) + local cfg = {} + for k, v in pairs(config) do + cfg[k] = v + end + cfg.mode = "revocation" + return cfg +end + + local function extract_cookie(cookie_name, cookies) local session_cookie if type(cookies) == "table" then @@ -79,10 +89,14 @@ describe("Revocation tests 2", function() session.init(configuration) end) - it("new loads redis revocation from string configuration", function() + it("new loads redis revocation when redis mode is revocation", function() local s = session.new({ - revocation = "redis", - redis = redis_config, + storage = "cookie", + redis = { + host = redis_config.host, + password = redis_config.password, + mode = "revocation", + }, }) assert.is_not_nil(s.revocation) assert.is_function(s.revocation.set) @@ -143,7 +157,7 @@ describe("Revocation tests 2", function() session.init({ cookie_name = cookie_name, - redis = bad_redis_config, + redis = revocation_redis(bad_redis_config), revocation_fail_mode = "closed", }) @@ -198,94 +212,66 @@ describe("Revocation tests 2", function() end) it("destroy: closed fail mode fails when marking revoked fails", function() - local s, err = open_session(session_cookie) - assert.is_not_nil(s) - assert.is_nil(err) - - s.revocation = { - set = function() - return nil, "connection refused" - end, - get = function() - return nil - end, - } - s.revocation_fail_mode = "closed" - - session.__set_ngx_header(cookies) - local ok - ok, err = s:destroy() - assert.is_nil(ok) - assert.matches("unable to mark session revoked", err) - assert.equals("open", s.state) - end) - - it("destroy: open fail mode succeeds when marking revoked fails", function() - local s, err = open_session(session_cookie) - assert.is_not_nil(s) - assert.is_nil(err) - - s.revocation = { - set = function() - return nil, "connection refused" - end, - get = function() - return nil - end, - } - s.revocation_fail_mode = "open" - - session.__set_ngx_header(cookies) - local ok - ok, err = s:destroy() - assert.is_true(ok) - assert.is_nil(err) - assert.equals("closed", s.state) - end) - - it("open: closed fail mode fails when checking revocation fails", function() local s = session.new({ revocation = { set = function() - return true + return nil, "connection refused" end, get = function() - return nil, "connection refused" + return nil end, }, revocation_fail_mode = "closed", }) - session.__set_ngx_var({ ["cookie_" .. cookie_name] = session_cookie, }) - local opened, err = s:open() - assert.is_nil(opened) - assert.matches("unable to check session revocation", err) + local ok, err = s:open() + assert.is_true(ok) + assert.is_nil(err) + + session.__set_ngx_header(cookies) + ok, err = s:destroy() + assert.is_nil(ok) + assert.matches("unable to mark session revoked", err) + assert.equals("open", s.state) end) - it("open: open fail mode succeeds when checking revocation fails", function() - local s = session.new({ - revocation = { - set = function() - return true - end, - get = function() - return nil, "connection refused" - end, - }, + it("destroy: open fail mode succeeds when marking revoked fails", function() + session.init({ + cookie_name = cookie_name, + redis = revocation_redis(bad_redis_config), revocation_fail_mode = "open", }) + local s = session.new() session.__set_ngx_var({ ["cookie_" .. cookie_name] = session_cookie, }) - local opened, err = s:open() + local ok, err = s:open() + assert.is_true(ok) + assert.is_nil(err) + + session.__set_ngx_header(cookies) + ok, err = s:destroy() + assert.is_true(ok) + assert.is_nil(err) + assert.equals("closed", s.state) + end) + + it("open: open fail mode succeeds when redis is unreachable", function() + session.init({ + cookie_name = cookie_name, + redis = revocation_redis(bad_redis_config), + revocation_fail_mode = "open", + }) + + local opened, err = open_session(session_cookie) assert.is_true(opened) assert.is_nil(err) - assert.equals(value, s:get(test_key)) + assert.equals(value, opened:get(test_key)) end) end) end) From bc0d909ed91e26ed259714b487a975206cf950a6 Mon Sep 17 00:00:00 2001 From: Austin Hockenberry Date: Thu, 2 Jul 2026 21:53:27 -0400 Subject: [PATCH 7/7] test(revocation): consolidate specs for normal and edge cases Reorder happy-path tests first, dedupe overlap, and group fail-mode coverage. --- spec/06-revocation-1_spec.lua | 240 ++++++++++++++++++++++------------ spec/07-revocation-2_spec.lua | 210 ++++++++++++----------------- 2 files changed, 242 insertions(+), 208 deletions(-) diff --git a/spec/06-revocation-1_spec.lua b/spec/06-revocation-1_spec.lua index 650b2de8..a6d0af05 100644 --- a/spec/06-revocation-1_spec.lua +++ b/spec/06-revocation-1_spec.lua @@ -48,11 +48,135 @@ describe("Revocation tests 1", function() assert.is_not_nil(store) end) - describe("[#redis] revocation: GET (not revoked)", function() - it("GET: missing revocation key returns not revoked marker", function() - local data, err = store:get(cookie, encode_base64url(id), ngx.time()) + describe("session: normal use", function() + local cookie_name = "session_cookie" + local test_key = "test_key" + local value = "test_data" + + local function save_session(s, cookies) + session.__set_ngx_header(cookies) + s:set(test_key, value) + local ok, err = s:save() + assert.is_true(ok) assert.is_nil(err) - assert.is_not_equal("1", data) + return extract_cookie(cookie_name, cookies["Set-Cookie"]) + end + + local function open_session(session_cookie) + local s = session.new() + session.__set_ngx_var({ + ["cookie_" .. cookie_name] = session_cookie, + }) + + local ok, err = s:open() + if not ok then + return nil, err + end + + return s + end + + before_each(function() + session.init({ + cookie_name = cookie_name, + storage = "cookie", + redis = { + host = redis_config.host, + password = redis_config.password, + mode = "revocation", + }, + }) + end) + + it("open succeeds for a valid session with revocation enabled", function() + local cookies = {} + local s = session.new() + local session_cookie = save_session(s, cookies) + s:close() + + local s2, err = open_session(session_cookie) + assert.is_not_nil(s2) + assert.is_nil(err) + assert.equals(value, s2:get(test_key)) + s2:close() + end) + + it("destroy: rejected cookie cannot be reopened", function() + local cookies = {} + local s = session.new() + local session_cookie = save_session(s, cookies) + assert.is_not_equal("", session_cookie) + + s:close() + + local s2, err = open_session(session_cookie) + assert.is_not_nil(s2) + assert.is_nil(err) + assert.equals(value, s2:get(test_key)) + + session.__set_ngx_header(cookies) + local ok + ok, err = s2:destroy() + assert.is_true(ok) + assert.is_nil(err) + + local s3 + s3, err = open_session(session_cookie) + assert.is_nil(s3) + assert.equals("session revoked", err) + end) + + it("save rotation does not revoke the previous cookie", function() + local cookies = {} + local s = session.new() + local session_cookie = save_session(s, cookies) + s:close() + + local s2, err = open_session(session_cookie) + assert.is_not_nil(s2) + assert.is_nil(err) + + s2:set(test_key, "rotated") + session.__set_ngx_header(cookies) + local ok + ok, err = s2:save() + assert.is_true(ok) + assert.is_nil(err) + s2:close() + + local s3 + s3, err = open_session(session_cookie) + assert.is_not_nil(s3) + assert.is_nil(err) + assert.equals(value, s3:get(test_key)) + s3:close() + end) + + it("cookie session without revocation clears cookie on destroy", function() + session.init({ + cookie_name = cookie_name, + storage = "cookie", + }) + + local cookies = {} + local s = session.new() + local session_cookie = save_session(s, cookies) + s:close() + + local s2, err = open_session(session_cookie) + assert.is_not_nil(s2) + assert.is_nil(err) + + session.__set_ngx_header(cookies) + local ok + ok, err = s2:destroy() + assert.is_true(ok) + assert.is_nil(err) + + local s3 + s3, err = open_session(session_cookie) + assert.is_nil(s3) + assert.is_not_equal("session revoked", err) end) end) @@ -68,6 +192,12 @@ describe("Revocation tests 1", function() assert.equals("1", data) end) + it("GET: missing revocation key returns not revoked marker", function() + local data, err = store:get(cookie, encode_base64url(id), ngx.time()) + assert.is_nil(err) + assert.is_not_equal("1", data) + end) + it("SET: ttl expires revocation entry", function() local ok, err = store:set(cookie, encode_base64url(id2), "1", short_ttl, ngx.time()) assert.is_not_nil(ok) @@ -98,21 +228,28 @@ describe("Revocation tests 1", function() session.init(configuration) end) - it("loads revocation when cookie storage has redis without storage mode", function() + it("loads revocation when storage is cookie and redis mode is revocation", function() + session.init({ + cookie_name = cookie_name, + storage = "cookie", + redis = { + host = redis_config.host, + password = redis_config.password, + mode = "revocation", + }, + }) + local s = session.new() assert.is_not_nil(s.revocation) assert.is_function(s.revocation.set) assert.is_function(s.revocation.get) end) - it("does not load revocation without a redis host", function() - session.init({ - cookie_name = cookie_name, - redis = { password = "password" }, - }) - + it("loads revocation when cookie storage has redis without storage mode", function() local s = session.new() - assert.is_nil(s.revocation) + assert.is_not_nil(s.revocation) + assert.is_function(s.revocation.set) + assert.is_function(s.revocation.get) end) it("skips revocation when storage backend is configured", function() @@ -129,99 +266,40 @@ describe("Revocation tests 1", function() assert.is_nil(s.revocation) end) - it("loads revocation when storage is cookie and redis mode is revocation", function() + it("skips revocation when storage is cookie and redis mode is storage", function() session.init({ cookie_name = cookie_name, storage = "cookie", redis = { host = redis_config.host, password = redis_config.password, - mode = "revocation", + mode = "storage", }, }) local s = session.new() - assert.is_not_nil(s.revocation) - assert.is_function(s.revocation.set) - assert.is_function(s.revocation.get) + assert.is_nil(s.revocation) end) - it("skips revocation when storage is cookie and redis mode is storage", function() + it("does not load revocation without a redis host", function() session.init({ cookie_name = cookie_name, - storage = "cookie", - redis = { - host = redis_config.host, - password = redis_config.password, - mode = "storage", - }, + redis = { password = "password" }, }) local s = session.new() assert.is_nil(s.revocation) end) - end) - describe("session: destroy", function() - local configuration = {} - local cookie_name = "session_cookie" - local test_key = "test_key" - local value = "test_data" - - local function save_session(s, cookies) - session.__set_ngx_header(cookies) - s:set(test_key, value) - local ok, err = s:save() - assert.is_true(ok) - assert.is_nil(err) - return extract_cookie(cookie_name, cookies["Set-Cookie"]) - end - - local function open_session(session_cookie) - local s = session.new() - session.__set_ngx_var({ - ["cookie_" .. cookie_name] = session_cookie, - }) - - local ok, err = s:open() - if not ok then - return nil, err - end - - return s - end - - before_each(function() - configuration = { + it("skips revocation when revocation is explicitly false", function() + session.init({ cookie_name = cookie_name, redis = redis_config, - } - session.init(configuration) - end) + revocation = false, + }) - it("destroy: rejected cookie cannot be reopened", function() - local cookies = {} local s = session.new() - local session_cookie = save_session(s, cookies) - assert.is_not_equal("", session_cookie) - - s:close() - - local s2, err = open_session(session_cookie) - assert.is_not_nil(s2) - assert.is_nil(err) - assert.equals(value, s2:get(test_key)) - - session.__set_ngx_header(cookies) - local ok - ok, err = s2:destroy() - assert.is_true(ok) - assert.is_nil(err) - - local s3 - s3, err = open_session(session_cookie) - assert.is_nil(s3) - assert.equals("session revoked", err) + assert.is_nil(s.revocation) end) end) end) diff --git a/spec/07-revocation-2_spec.lua b/spec/07-revocation-2_spec.lua index 089d524d..ae0a55f9 100644 --- a/spec/07-revocation-2_spec.lua +++ b/spec/07-revocation-2_spec.lua @@ -27,16 +27,6 @@ local bad_redis_config = { } -local function revocation_redis(config) - local cfg = {} - for k, v in pairs(config) do - cfg[k] = v - end - cfg.mode = "revocation" - return cfg -end - - local function extract_cookie(cookie_name, cookies) local session_cookie if type(cookies) == "table" then @@ -59,64 +49,13 @@ describe("Revocation tests 2", function() local id1 = "test_id_2iiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiii" local cookie = "session_cookie" - describe("[#redis] revocation: failures", function() - it("SET: connection failure returns error", function() - local bad_store = redis_storage.new(bad_redis_config) - - local ok, err = bad_store:set(cookie, encode_base64url(id), "1", long_ttl, ngx.time()) - assert.is_nil(ok) - assert.is_not_nil(err) - end) - - it("GET: connection failure returns error", function() - local bad_store = redis_storage.new(bad_redis_config) - - local data, err = bad_store:get(cookie, encode_base64url(id1), ngx.time()) - assert.is_nil(data) - assert.is_not_nil(err) - end) - end) - - describe("session: Fields validation", function() - local configuration = {} - local cookie_name = "session_cookie" - - before_each(function() - configuration = { - cookie_name = cookie_name, - redis = redis_config, - } - session.init(configuration) - end) - - it("new loads redis revocation when redis mode is revocation", function() - local s = session.new({ - storage = "cookie", - redis = { - host = redis_config.host, - password = redis_config.password, - mode = "revocation", - }, - }) - assert.is_not_nil(s.revocation) - assert.is_function(s.revocation.set) - assert.is_function(s.revocation.get) - end) - - it("new validates revocation configuration", function() - local ok, err = pcall(session.new, { - revocation = 123, - }) - assert.is_false(ok) - assert.matches("invalid session revocation", err) - end) - end) - - describe("session: open", function() + describe("session: revocation_fail_mode", function() local configuration = {} local cookie_name = "session_cookie" local test_key = "test_key" local value = "test_data" + local session_cookie + local cookies local function save_session(s, cookies) session.__set_ngx_header(cookies) @@ -147,68 +86,59 @@ describe("Revocation tests 2", function() redis = redis_config, } session.init(configuration) - end) - it("open: closed fail mode rejects when redis is unreachable", function() - local cookies = {} + cookies = {} local s = session.new() - local session_cookie = save_session(s, cookies) + session_cookie = save_session(s, cookies) s:close() + end) + it("open: default fail mode allows open when redis is unreachable", function() session.init({ cookie_name = cookie_name, - redis = revocation_redis(bad_redis_config), - revocation_fail_mode = "closed", + redis = bad_redis_config, }) local opened, err = open_session(session_cookie) - assert.is_nil(opened) - assert.matches("unable to check session revocation", err) + assert.is_true(opened) + assert.is_nil(err) + assert.equals("open", opened.revocation_fail_mode) + assert.equals(value, opened:get(test_key)) end) - end) - describe("session: revocation_fail_mode", function() - local configuration = {} - local cookie_name = "session_cookie" - local test_key = "test_key" - local value = "test_data" - local session_cookie - local cookies - - local function save_session(s, cookies) - session.__set_ngx_header(cookies) - s:set(test_key, value) - local ok, err = s:save() - assert.is_true(ok) - assert.is_nil(err) - return extract_cookie(cookie_name, cookies["Set-Cookie"]) - end + it("destroy: open fail mode succeeds when marking revoked fails", function() + session.init({ + cookie_name = cookie_name, + redis = bad_redis_config, + revocation_fail_mode = "open", + }) - local function open_session(session_cookie) local s = session.new() session.__set_ngx_var({ ["cookie_" .. cookie_name] = session_cookie, }) local ok, err = s:open() - if not ok then - return nil, err - end + assert.is_true(ok) + assert.is_nil(err) - return s - end + session.__set_ngx_header(cookies) + ok, err = s:destroy() + assert.is_true(ok) + assert.is_nil(err) + assert.equals("closed", s.state) + end) - before_each(function() - configuration = { + it("open: closed fail mode rejects when redis is unreachable", function() + session.init({ cookie_name = cookie_name, - redis = redis_config, - } - session.init(configuration) + redis = bad_redis_config, + revocation_fail_mode = "closed", + }) - cookies = {} - local s = session.new() - session_cookie = save_session(s, cookies) - s:close() + local opened, err = open_session(session_cookie) + assert.is_nil(opened) + assert.matches("unable to check session revocation", err) end) it("destroy: closed fail mode fails when marking revoked fails", function() @@ -237,41 +167,67 @@ describe("Revocation tests 2", function() assert.matches("unable to mark session revoked", err) assert.equals("open", s.state) end) + end) - it("destroy: open fail mode succeeds when marking revoked fails", function() + describe("session: Fields validation", function() + local configuration = {} + local cookie_name = "session_cookie" + + before_each(function() + configuration = { + cookie_name = cookie_name, + redis = redis_config, + } + session.init(configuration) + end) + + it("new defaults revocation_fail_mode to open", function() session.init({ cookie_name = cookie_name, - redis = revocation_redis(bad_redis_config), - revocation_fail_mode = "open", + redis = redis_config, }) local s = session.new() - session.__set_ngx_var({ - ["cookie_" .. cookie_name] = session_cookie, + assert.equals("open", s.revocation_fail_mode) + end) + + it("new rejects redis storage with revocation mode", function() + local ok, err = pcall(session.new, { + storage = "redis", + redis = { + host = redis_config.host, + password = redis_config.password, + mode = "revocation", + }, }) + assert.is_false(ok) + assert.matches("invalid redis mode for session storage", err) + end) - local ok, err = s:open() - assert.is_true(ok) - assert.is_nil(err) + it("new validates revocation configuration", function() + local ok, err = pcall(session.new, { + revocation = 123, + }) + assert.is_false(ok) + assert.matches("invalid session revocation", err) + end) + end) - session.__set_ngx_header(cookies) - ok, err = s:destroy() - assert.is_true(ok) - assert.is_nil(err) - assert.equals("closed", s.state) + describe("[#redis] revocation: failures", function() + it("SET: connection failure returns error", function() + local bad_store = redis_storage.new(bad_redis_config) + + local ok, err = bad_store:set(cookie, encode_base64url(id), "1", long_ttl, ngx.time()) + assert.is_nil(ok) + assert.is_not_nil(err) end) - it("open: open fail mode succeeds when redis is unreachable", function() - session.init({ - cookie_name = cookie_name, - redis = revocation_redis(bad_redis_config), - revocation_fail_mode = "open", - }) + it("GET: connection failure returns error", function() + local bad_store = redis_storage.new(bad_redis_config) - local opened, err = open_session(session_cookie) - assert.is_true(opened) - assert.is_nil(err) - assert.equals(value, opened:get(test_key)) + local data, err = bad_store:get(cookie, encode_base64url(id1), ngx.time()) + assert.is_nil(data) + assert.is_not_nil(err) end) end) end)