From ae1dead82d2a708ff566120aec5af4e504f66513 Mon Sep 17 00:00:00 2001 From: janiussyafiq Date: Wed, 9 Sep 2026 09:29:12 +0800 Subject: [PATCH 1/6] feat(*): application supplied revocation keys for cookie sessions Sessions can carry revocation keys in the encrypted payload via session:set_revocation_keys. On open, a key whose revocation mark is at or after the session creation time rejects the session with "session revoked". Marks are unix timestamps; the 4.2.0 mark "1" still means revoked regardless of time. --- lib/resty/session.lua | 82 ++++++++++++++--- spec/06-revocation-1_spec.lua | 166 +++++++++++++++++++++++++++++++++- 2 files changed, 231 insertions(+), 17 deletions(-) diff --git a/lib/resty/session.lua b/lib/resty/session.lua index 2ad91093..c96e3a95 100644 --- a/lib/resty/session.lua +++ b/lib/resty/session.lua @@ -387,23 +387,18 @@ local function handle_revocation_error(self, err, msg) end -local function is_session_revoked(self, sid, cookie_name) - if self.storage or not sid then - return false, nil - end - +local function is_revoked(self, key, cookie_name, current_time, creation_time) 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 + local storage_key, err = self.hash_storage_key(key) + if not storage_key then + return nil, err end - local current_time = time() - local data, err = revocation:get(cookie_name, key, current_time) + local mark, err = revocation:get(cookie_name, storage_key, current_time) if err then local ok, rerr = handle_revocation_error(self, err, "unable to check session revocation") if not ok then @@ -412,11 +407,12 @@ local function is_session_revoked(self, sid, cookie_name) return false, nil end - if data == REVOCATION_MARK then + if mark == REVOCATION_MARK then return true, nil end - return false, nil + local revoked_at = tonumber(mark) + return revoked_at ~= nil and revoked_at >= creation_time, nil end @@ -795,7 +791,7 @@ local function open(self, remember, meta_only) end end - local revoked, err = is_session_revoked(self, sid, cookie_name) + local revoked, err = is_revoked(self, sid, cookie_name, current_time, creation_time) if err then return nil, err end @@ -945,9 +941,13 @@ local function open(self, remember, meta_only) local audience_index local count = #data for i = 1, count do - if data[i][2] == audience then + -- cjson decodes the JSON null of a missing subject as userdata + if type(data[i][3]) == "userdata" then + data[i][3] = nil + end + + if not audience_index and data[i][2] == audience then audience_index = i - break end end @@ -959,6 +959,19 @@ local function open(self, remember, meta_only) return nil, "missing session audience", true end + local keys = data[audience_index][4] + if keys then + for i = 1, #keys do + local revoked, err = is_revoked(self, keys[i], self.cookie_name, current_time, creation_time) + if err then + return nil, err + end + if revoked then + return nil, "session revoked" + end + end + end + self.state = STATE_OPEN self.data = data self.data_index = audience_index @@ -1942,6 +1955,45 @@ function metatable:get_subject() end +--- +-- Set session revocation keys. +-- +-- Revocation keys are application supplied identifiers carried in the +-- session payload for the current audience, for example an identity +-- provider's session or subject identifier. When a `revocation` storage +-- is configured, `session:open` rejects a session that carries a key +-- revoked with `session.revoke` at or after the session was created. +-- +-- @function instance:set_revocation_keys +-- @tparam table|nil keys array of revocation keys (`nil` clears them) +-- +-- @usage +-- local session = require("resty.session").new() +-- session:set_revocation_keys({ "sid:" .. sid, "sub:" .. sub }) +function metatable:set_revocation_keys(keys) + assert(self.state ~= STATE_CLOSED, "unable to set revocation keys on closed session") + assert(keys == nil or type(keys) == "table", "invalid revocation keys") + self.data[self.data_index][4] = keys +end + + +--- +-- Get session revocation keys. +-- +-- @function instance:get_revocation_keys +-- @treturn table|nil array of revocation keys +-- +-- @usage +-- local session, err, exists = require("resty.session").open() +-- if exists then +-- local keys = session:get_revocation_keys() +-- end +function metatable:get_revocation_keys() + assert(self.state ~= STATE_CLOSED, "unable to get revocation keys on closed session") + return self.data[self.data_index][4] +end + + --- -- Get session property. -- diff --git a/spec/06-revocation-1_spec.lua b/spec/06-revocation-1_spec.lua index 4fdcbafb..f66274c4 100644 --- a/spec/06-revocation-1_spec.lua +++ b/spec/06-revocation-1_spec.lua @@ -149,8 +149,8 @@ for _, st in ipairs({ return extract_cookie(cookie_name, cookies["Set-Cookie"]) end - local function open_session(session_cookie) - local s = session.new() + local function open_session(session_cookie, configuration) + local s = session.new(configuration) session.__set_ngx_var({ ["cookie_" .. cookie_name] = session_cookie, }) @@ -163,6 +163,12 @@ for _, st in ipairs({ return s end + local function mark_key(key) + return session.new().hash_storage_key(key) + end + + local revocation_keys = { "sub:test", "sub:legacy", "sub:other", "sub:aud-a" } + before_each(function() local conf = { cookie_name = cookie_name, @@ -173,6 +179,12 @@ for _, st in ipairs({ session.init(conf) end) + after_each(function() + for _, k in ipairs(revocation_keys) do + store:delete(cookie_name, mark_key(k), time()) + end + end) + it("open succeeds for a valid session with revocation enabled", function() local cookies = {} local s = session.new() @@ -235,6 +247,156 @@ for _, st in ipairs({ assert.equals(value, s3:get(test_key)) s3:close() end) + + it("revocation keys: round-trip through the cookie without a subject", function() + local cookies = {} + local s = session.new() + s:set_revocation_keys({ "sub:test", "sid:test" }) + 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.same({ "sub:test", "sid:test" }, s2:get_revocation_keys()) + assert.is_nil(s2:get_subject()) + s2:close() + end) + + it("revocation keys: mark at or after creation revokes", function() + local cookies = {} + local s = session.new() + s:set_revocation_keys({ "sub:test" }) + local session_cookie = save_session(s, cookies) + s:close() + + local ok = store:set(cookie_name, mark_key("sub:test"), tostring(time()), long_ttl, time()) + assert.is_not_nil(ok) + + local s2, err = open_session(session_cookie) + assert.is_nil(s2) + assert.equals("session revoked", err) + end) + + it("revocation keys: mark before creation does not revoke", function() + local ok = store:set(cookie_name, mark_key("sub:test"), tostring(time() - 1), long_ttl, time()) + assert.is_not_nil(ok) + + local cookies = {} + local s = session.new() + s:set_revocation_keys({ "sub:test" }) + 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("revocation keys: legacy mark revokes regardless of time", function() + local ok = store:set(cookie_name, mark_key("sub:legacy"), "1", long_ttl, time()) + assert.is_not_nil(ok) + + local cookies = {} + local s = session.new() + s:set_revocation_keys({ "sub:legacy" }) + local session_cookie = save_session(s, cookies) + s:close() + + local s2, err = open_session(session_cookie) + assert.is_nil(s2) + assert.equals("session revoked", err) + end) + + it("revocation keys: mark for another key does not revoke", function() + local cookies = {} + local s = session.new() + s:set_revocation_keys({ "sub:test" }) + local session_cookie = save_session(s, cookies) + s:close() + + local ok = store:set(cookie_name, mark_key("sub:other"), tostring(time()), long_ttl, time()) + assert.is_not_nil(ok) + + 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("revocation keys: are scoped to the audience", function() + local cookies = {} + local s = session.new({ audience = "a" }) + s:set_revocation_keys({ "sub:aud-a" }) + local session_cookie = save_session(s, cookies) + s:close() + + session.__set_ngx_var({ + ["cookie_" .. cookie_name] = session_cookie, + }) + local s2, err, exists = session.open({ audience = "b" }) + assert.is_false(exists) + assert.equals("missing session audience", err) + + local both_audiences_cookie = save_session(s2, cookies) + s2:close() + + local ok = store:set(cookie_name, mark_key("sub:aud-a"), tostring(time()), long_ttl, time()) + assert.is_not_nil(ok) + + local sb, err_b = open_session(both_audiences_cookie, { audience = "b" }) + assert.is_not_nil(sb) + assert.is_nil(err_b) + sb:close() + + local sa, err_a = open_session(both_audiences_cookie, { audience = "a" }) + assert.is_nil(sa) + assert.equals("session revoked", err_a) + end) + + it("revocation keys: revoke a remembered session", function() + local conf = { + cookie_name = cookie_name, + storage = "cookie", + revocation = st, + remember = true, + } + conf[st] = storage_configs[st] + session.init(conf) + + local cookies = {} + local s = session.new() + s:set_remember(true) + s:set_revocation_keys({ "sub:test" }) + save_session(s, cookies) + local remember_cookie = extract_cookie("remember", cookies["Set-Cookie"]) + assert.is_not_equal("", remember_cookie) + s:close() + + session.__set_ngx_header(cookies) + session.__set_ngx_var({ + ["cookie_remember"] = remember_cookie, + }) + local s2 = session.new() + local ok, err = s2:open() + assert.is_true(ok) + assert.is_nil(err) + s2:close() + + local mark_ok = store:set(cookie_name, mark_key("sub:test"), tostring(time()), long_ttl, time()) + assert.is_not_nil(mark_ok) + + session.__set_ngx_header(cookies) + session.__set_ngx_var({ + ["cookie_remember"] = remember_cookie, + }) + local s3 = session.new() + local ok3 = s3:open() + assert.is_nil(ok3) + end) end) end) end From 175e34308bf4c95be732d0c87fdbcd9eabac0962 Mon Sep 17 00:00:00 2001 From: janiussyafiq Date: Wed, 9 Sep 2026 09:46:39 +0800 Subject: [PATCH 2/6] feat(*): session.revoke to mark a revocation key without a session Writes a unix timestamp mark for an application supplied key through the configured revocation storage, so sessions carrying that key and created at or before the mark are rejected on open. Intended for identity provider initiated logout, where no cookie is available. --- lib/resty/session.lua | 50 +++++++++++++++++++++++++++++++---- spec/06-revocation-1_spec.lua | 30 +++++++++++++++++++++ spec/07-revocation-2_spec.lua | 28 ++++++++++++++++++++ 3 files changed, 103 insertions(+), 5 deletions(-) diff --git a/lib/resty/session.lua b/lib/resty/session.lua index c96e3a95..55aef6e4 100644 --- a/lib/resty/session.lua +++ b/lib/resty/session.lua @@ -1958,11 +1958,9 @@ end --- -- Set session revocation keys. -- --- Revocation keys are application supplied identifiers carried in the --- session payload for the current audience, for example an identity --- provider's session or subject identifier. When a `revocation` storage --- is configured, `session:open` rejects a session that carries a key --- revoked with `session.revoke` at or after the session was created. +-- Application supplied identifiers (e.g. an identity provider's session +-- or subject id) carried in the payload; `session.revoke` revokes sessions +-- by them. -- -- @function instance:set_revocation_keys -- @tparam table|nil keys array of revocation keys (`nil` clears them) @@ -2977,6 +2975,48 @@ function session.destroy(configuration) end +--- +-- Revoke sessions by a revocation key. +-- +-- Writes a mark for an application supplied key (see +-- `session:set_revocation_keys`); sessions carrying it that were created +-- at or before now are rejected on open. `ttl` must cover the sessions' +-- absolute timeout. Write failures are always returned. +-- +-- @function module.revoke +-- @tparam string key revocation key +-- @tparam number ttl mark time-to-live in seconds +-- @tparam[opt] table configuration session @{configuration} overrides +-- @treturn boolean `true` when the mark was written, otherwise `nil` +-- @treturn string error message +-- +-- @usage +-- local ok, err = require("resty.session").revoke("sub:" .. sub, 86400) +function session.revoke(key, ttl, configuration) + assert(type(key) == "string" and key ~= "", "invalid revocation key") + assert(type(ttl) == "number" and ttl > 0, "invalid revocation ttl") + + local self = session.new(configuration) + local revocation = self.revocation + if not revocation then + return nil, "session revocation is not enabled" + end + + local storage_key, err = self.hash_storage_key(key) + if not storage_key then + return nil, err + end + + local current_time = time() + local ok, err = revocation:set(self.cookie_name, storage_key, tostring(current_time), ttl, current_time) + if not ok then + return nil, errmsg(err, "unable to revoke session key") + end + + return true +end + + function session.__set_ngx_log(ngx_log) log = ngx_log end diff --git a/spec/06-revocation-1_spec.lua b/spec/06-revocation-1_spec.lua index f66274c4..b51db1da 100644 --- a/spec/06-revocation-1_spec.lua +++ b/spec/06-revocation-1_spec.lua @@ -198,6 +198,36 @@ for _, st in ipairs({ s2:close() end) + it("revoke: session carrying a revoked key cannot be reopened", function() + local cookies = {} + local s = session.new() + s:set_revocation_keys({ "sub:test" }) + local session_cookie = save_session(s, cookies) + s:close() + + assert.is_true(session.revoke("sub:test", long_ttl)) + + local s2, err = open_session(session_cookie) + assert.is_nil(s2) + assert.equals("session revoked", err) + end) + + it("revoke: session created after the revocation opens", function() + assert.is_true(session.revoke("sub:test", long_ttl)) + sleep(1) + + local cookies = {} + local s = session.new() + s:set_revocation_keys({ "sub:test" }) + 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:close() + end) + it("destroy: rejected cookie cannot be reopened", function() local cookies = {} local s = session.new() diff --git a/spec/07-revocation-2_spec.lua b/spec/07-revocation-2_spec.lua index 87e5bd8d..27e5251e 100644 --- a/spec/07-revocation-2_spec.lua +++ b/spec/07-revocation-2_spec.lua @@ -370,6 +370,26 @@ describe("Revocation tests 2 session: revocation_fail_mode", function() assert.matches("unable to mark session revoked", err) assert.equals("open", s.state) end) + + it("revoke: reports a failed write even in open fail mode", function() + session.init({ + cookie_name = cookie_name, + storage = "cookie", + revocation = { + set = function() + return nil, "connection refused" + end, + get = function() + return nil + end, + }, + revocation_fail_mode = "open", + }) + + local ok, err = session.revoke("sub:test", 60) + assert.is_nil(ok) + assert.matches("unable to revoke session key", err) + end) end) @@ -419,4 +439,12 @@ describe("Revocation tests 2 session: Fields validation", function() assert.is_false(ok) assert.matches("invalid revocation fail mode", err) end) + + it("revoke fails when revocation is not enabled", function() + session.init({ storage = "cookie" }) + + local ok, err = session.revoke("sub:test", 60) + assert.is_nil(ok) + assert.equals("session revocation is not enabled", err) + end) end) From 7b222c7340e6f931d38333f3e3b2b13c5431a6d5 Mon Sep 17 00:00:00 2001 From: janiussyafiq Date: Wed, 9 Sep 2026 09:53:41 +0800 Subject: [PATCH 3/6] docs(*): document revocation keys and session.revoke --- README.md | 63 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/README.md b/README.md index bac2f9fb..fa969e0f 100644 --- a/README.md +++ b/README.md @@ -200,6 +200,7 @@ http { * [session.start](#sessionstart) * [session.logout](#sessionlogout) * [session.destroy](#sessiondestroy) + * [session.revoke](#sessionrevoke) * [Instance Methods](#instance-methods) * [session:open](#sessionopen-1) * [session:save](#sessionsave) @@ -216,6 +217,8 @@ http { * [session:get_audience](#sessionget_audience) * [session:set_subject](#sessionset_subject) * [session:get_subject](#sessionget_subject) + * [session:set_revocation_keys](#sessionset_revocation_keys) + * [session:get_revocation_keys](#sessionget_revocation_keys) * [session:get_property](#sessionget_property) * [session:set_remember](#sessionset_remember) * [session:get_remember](#sessionget_remember) @@ -367,6 +370,23 @@ storage 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. +Sessions may also carry application supplied revocation keys (for example an +identity provider's `sid` and `sub`), set with `session:set_revocation_keys`. +`session.revoke(key, ttl, configuration)` marks a key without an open session; +sessions carrying it that were created at or before the mark are rejected on +`session:open`, later ones are not. `ttl` must cover the sessions' absolute +timeout. Keys go through `hash_storage_key`, enable it when they may contain +personal data. `session.revoke` always returns write errors; +`revocation_fail_mode` applies to `session:open` and `session:destroy` only. + +```lua +-- On login +session:set_revocation_keys({ "sid:" .. sid, "sub:" .. sub }) + +-- In a back-channel logout handler, without an open session +require("resty.session").revoke("sid:" .. sid, 86400) +``` + Use `revocation_fail_mode` to control behavior when the storage is unavailable: - `"open"` (default): log a warning and treat the session as not revoked. @@ -884,6 +904,22 @@ local ok, err, exists, destroyed = require "resty.session".destroy({ See [configuration](#configuration) for possible configuration settings. +### session.revoke + +**syntax:** *ok, err = session.revoke(key, ttl, configuration)* + +It marks a revocation key (see `session:set_revocation_keys`) without an open +session; sessions carrying the key that were created at or before the mark +are rejected on `session:open`. `ttl` (in seconds) must cover the sessions' +absolute timeout. Write errors are always returned. + +```lua +local ok, err = require "resty.session".revoke("sid:" .. sid, 86400) +``` + +See [configuration](#configuration) for possible configuration settings. + + ## Instance Methods ### session:open @@ -1117,6 +1153,33 @@ end ``` +### session:set_revocation_keys + +**syntax:** *session:set_revocation_keys(keys)* + +Set application supplied revocation keys, e.g. an identity provider's `sid` +and `sub`, so that `session.revoke` can revoke the session by them. + +```lua +local session = require "resty.session".new() +session:set_revocation_keys({ "sid:" .. sid, "sub:" .. sub }) +``` + + +### session:get_revocation_keys + +**syntax:** *keys = session:get_revocation_keys()* + +Get session revocation keys. + +```lua +local session, err, exists = require "resty.session".open() +if exists then + local keys = session:get_revocation_keys() +end +``` + + ### session:get_property **syntax:** *value = session:get_property(name)* From 0c5a5758753c2070fa527aaac27557b0af30fd50 Mon Sep 17 00:00:00 2001 From: janiussyafiq Date: Wed, 9 Sep 2026 10:20:04 +0800 Subject: [PATCH 4/6] docs(*): note remember_absolute_timeout for revocation key ttl --- README.md | 10 ++++++---- lib/resty/session.lua | 3 ++- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index fa969e0f..a069dad9 100644 --- a/README.md +++ b/README.md @@ -375,9 +375,10 @@ identity provider's `sid` and `sub`), set with `session:set_revocation_keys`. `session.revoke(key, ttl, configuration)` marks a key without an open session; sessions carrying it that were created at or before the mark are rejected on `session:open`, later ones are not. `ttl` must cover the sessions' absolute -timeout. Keys go through `hash_storage_key`, enable it when they may contain -personal data. `session.revoke` always returns write errors; -`revocation_fail_mode` applies to `session:open` and `session:destroy` only. +timeout (`remember_absolute_timeout` when remember cookies are used). Keys go +through `hash_storage_key`, enable it when they may contain personal data. +`session.revoke` always returns write errors; `revocation_fail_mode` applies +to `session:open` and `session:destroy` only. ```lua -- On login @@ -911,7 +912,8 @@ See [configuration](#configuration) for possible configuration settings. It marks a revocation key (see `session:set_revocation_keys`) without an open session; sessions carrying the key that were created at or before the mark are rejected on `session:open`. `ttl` (in seconds) must cover the sessions' -absolute timeout. Write errors are always returned. +absolute timeout (`remember_absolute_timeout` when remember cookies are used). +Write errors are always returned. ```lua local ok, err = require "resty.session".revoke("sid:" .. sid, 86400) diff --git a/lib/resty/session.lua b/lib/resty/session.lua index 55aef6e4..ec356e69 100644 --- a/lib/resty/session.lua +++ b/lib/resty/session.lua @@ -2981,7 +2981,8 @@ end -- Writes a mark for an application supplied key (see -- `session:set_revocation_keys`); sessions carrying it that were created -- at or before now are rejected on open. `ttl` must cover the sessions' --- absolute timeout. Write failures are always returned. +-- absolute timeout (`remember_absolute_timeout` with remember cookies). +-- Write failures are always returned. -- -- @function module.revoke -- @tparam string key revocation key From 660fe381a4d562a302ec45a08af44080d64729ed Mon Sep 17 00:00:00 2001 From: janiussyafiq Date: Wed, 9 Sep 2026 11:02:58 +0800 Subject: [PATCH 5/6] fix(*): revoked keys survive re-login and remember restoration Check every audience's revocation keys when opening a remember cookie, since restoration mints a fresh session cookie carrying all of them. Reset the rejected cookie's meta on revocation so a re-login does not inherit the revoked creation time. --- lib/resty/session.lua | 25 ++++++++----- spec/06-revocation-1_spec.lua | 70 +++++++++++++++++++++++++++++++++++ 2 files changed, 86 insertions(+), 9 deletions(-) diff --git a/lib/resty/session.lua b/lib/resty/session.lua index ec356e69..2bb5fb58 100644 --- a/lib/resty/session.lua +++ b/lib/resty/session.lua @@ -959,15 +959,22 @@ local function open(self, remember, meta_only) return nil, "missing session audience", true end - local keys = data[audience_index][4] - if keys then - for i = 1, #keys do - local revoked, err = is_revoked(self, keys[i], self.cookie_name, current_time, creation_time) - if err then - return nil, err - end - if revoked then - return nil, "session revoked" + for i = 1, count do + local keys = data[i][4] + if keys and (remember or i == audience_index) then + for j = 1, #keys do + local revoked, err = is_revoked(self, keys[j], self.cookie_name, current_time, creation_time) + if err then + return nil, err + end + if revoked then + if remember then + self.remember_meta = DUMMY_REMEMBER_META + else + self.meta = DUMMY_META + end + return nil, "session revoked" + end end end end diff --git a/spec/06-revocation-1_spec.lua b/spec/06-revocation-1_spec.lua index b51db1da..3ab11bef 100644 --- a/spec/06-revocation-1_spec.lua +++ b/spec/06-revocation-1_spec.lua @@ -228,6 +228,76 @@ for _, st in ipairs({ s2:close() end) + it("revoke: re-login after a revoked open gets a fresh session", function() + local cookies = {} + local s = session.new() + s:set_revocation_keys({ "sub:test" }) + local session_cookie = save_session(s, cookies) + s:close() + + assert.is_true(session.revoke("sub:test", long_ttl)) + + session.__set_ngx_var({ + ["cookie_" .. cookie_name] = session_cookie, + }) + local s2, err, exists = session.open() + assert.equals("session revoked", err) + assert.is_false(exists) + + sleep(1) + s2:set_revocation_keys({ "sub:test" }) + session_cookie = save_session(s2, cookies) + s2:close() + + local s3 + s3, err = open_session(session_cookie) + assert.is_not_nil(s3) + assert.is_nil(err) + s3:close() + end) + + it("revoke: remember cookie carrying a revoked audience is not restored", function() + local conf = { + cookie_name = cookie_name, + storage = "cookie", + revocation = st, + remember = true, + } + conf[st] = storage_configs[st] + session.init(conf) + + local cookies = {} + local a = session.new({ audience = "a" }) + a:set_remember(true) + a:set_revocation_keys({ "sub:aud-a" }) + local session_cookie = save_session(a, cookies) + local remember_cookie = extract_cookie("remember", cookies["Set-Cookie"]) + a:close() + + session.__set_ngx_var({ + ["cookie_" .. cookie_name] = session_cookie, + ["cookie_remember"] = remember_cookie, + }) + local b, err = session.open({ audience = "b" }) + assert.equals("missing session audience", err) + b:set_remember(true) + cookies = {} + save_session(b, cookies) + remember_cookie = extract_cookie("remember", cookies["Set-Cookie"]) + assert.is_not_equal("", remember_cookie) + b:close() + + assert.is_true(session.revoke("sub:aud-a", long_ttl)) + + session.__set_ngx_header(cookies) + session.__set_ngx_var({ + ["cookie_remember"] = remember_cookie, + }) + local b2 = session.new({ audience = "b" }) + local ok = b2:open() + assert.is_nil(ok) + end) + it("destroy: rejected cookie cannot be reopened", function() local cookies = {} local s = session.new() From 226caf3fb860c3c5122e096d0cdb67f5a4b5daf0 Mon Sep 17 00:00:00 2001 From: janiussyafiq Date: Wed, 9 Sep 2026 11:36:43 +0800 Subject: [PATCH 6/6] fix(*): check revocation keys before the missing-audience path Run the key check before a remember cookie's data is kept for an absent audience, and leave a rejected remember cookie with empty metadata so save, logout and destroy do not reload its creation time. --- lib/resty/session.lua | 18 ++++---- spec/06-revocation-1_spec.lua | 82 +++++++++++++++++++++++++++++++++++ 2 files changed, 91 insertions(+), 9 deletions(-) diff --git a/lib/resty/session.lua b/lib/resty/session.lua index 2bb5fb58..b6bac2be 100644 --- a/lib/resty/session.lua +++ b/lib/resty/session.lua @@ -951,14 +951,6 @@ local function open(self, remember, meta_only) end end - if not audience_index then - data[count + 1] = self.data[data_index] - self.state = STATE_NEW - self.data = data - self.data_index = count + 1 - return nil, "missing session audience", true - end - for i = 1, count do local keys = data[i][4] if keys and (remember or i == audience_index) then @@ -969,7 +961,7 @@ local function open(self, remember, meta_only) end if revoked then if remember then - self.remember_meta = DUMMY_REMEMBER_META + self.remember_meta = {} else self.meta = DUMMY_META end @@ -979,6 +971,14 @@ local function open(self, remember, meta_only) end end + if not audience_index then + data[count + 1] = self.data[data_index] + self.state = STATE_NEW + self.data = data + self.data_index = count + 1 + return nil, "missing session audience", true + end + self.state = STATE_OPEN self.data = data self.data_index = audience_index diff --git a/spec/06-revocation-1_spec.lua b/spec/06-revocation-1_spec.lua index 3ab11bef..1126b190 100644 --- a/spec/06-revocation-1_spec.lua +++ b/spec/06-revocation-1_spec.lua @@ -298,6 +298,88 @@ for _, st in ipairs({ assert.is_nil(ok) end) + it("revoke: remember cookie with a revoked audience is not restored for a new audience", function() + local conf = { + cookie_name = cookie_name, + storage = "cookie", + revocation = st, + remember = true, + } + conf[st] = storage_configs[st] + session.init(conf) + + local cookies = {} + local a = session.new({ audience = "a" }) + a:set_remember(true) + a:set_revocation_keys({ "sub:aud-a" }) + save_session(a, cookies) + local remember_cookie = extract_cookie("remember", cookies["Set-Cookie"]) + a:close() + + assert.is_true(session.revoke("sub:aud-a", long_ttl)) + + session.__set_ngx_var({ + ["cookie_remember"] = remember_cookie, + }) + local b, err = session.open({ audience = "b" }) + assert.is_not_nil(err) + cookies = {} + local session_cookie = save_session(b, cookies) + b:close() + + local a2 + a2, err = open_session(session_cookie, { audience = "a" }) + assert.is_nil(a2) + assert.equals("missing session audience", err) + end) + + it("revoke: re-login after revocation issues a fresh remember cookie", function() + local conf = { + cookie_name = cookie_name, + storage = "cookie", + revocation = st, + remember = true, + } + conf[st] = storage_configs[st] + session.init(conf) + + local cookies = {} + local s = session.new() + s:set_remember(true) + s:set_revocation_keys({ "sub:test" }) + local session_cookie = save_session(s, cookies) + local remember_cookie = extract_cookie("remember", cookies["Set-Cookie"]) + s:close() + + assert.is_true(session.revoke("sub:test", long_ttl)) + sleep(1) + + session.__set_ngx_var({ + ["cookie_" .. cookie_name] = session_cookie, + ["cookie_remember"] = remember_cookie, + }) + local s2, err = session.open() + assert.equals("session revoked", err) + + s2:set_remember(true) + s2:set_revocation_keys({ "sub:test" }) + cookies = {} + save_session(s2, cookies) + remember_cookie = extract_cookie("remember", cookies["Set-Cookie"]) + assert.is_not_equal("", remember_cookie) + s2:close() + + session.__set_ngx_header(cookies) + session.__set_ngx_var({ + ["cookie_remember"] = remember_cookie, + }) + local s3 = session.new() + local ok, err = s3:open() + assert.is_true(ok) + assert.is_nil(err) + s3:close() + end) + it("destroy: rejected cookie cannot be reopened", function() local cookies = {} local s = session.new()