Skip to content
86 changes: 86 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,8 @@ http {
* [session.start](#sessionstart)
* [session.logout](#sessionlogout)
* [session.destroy](#sessiondestroy)
* [session.revoke_subject](#sessionrevoke_subject)
* [session.revoke_sid](#sessionrevoke_sid)
* [Instance Methods](#instance-methods)
* [session:open](#sessionopen-1)
* [session:save](#sessionsave)
Expand All @@ -216,6 +218,8 @@ http {
* [session:get_audience](#sessionget_audience)
* [session:set_subject](#sessionset_subject)
* [session:get_subject](#sessionget_subject)
* [session:set_sid](#sessionset_sid)
* [session:get_sid](#sessionget_sid)
* [session:get_property](#sessionget_property)
* [session:set_remember](#sessionset_remember)
* [session:get_remember](#sessionget_remember)
Expand Down Expand Up @@ -367,6 +371,27 @@ 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 can also be revoked by their subject (`session:set_subject`) or by an
identity provider session id (`session:set_sid`, e.g. the OpenID Connect `sid`
claim). `session.revoke_subject(subject, ttl, configuration)` and
`session.revoke_sid(sid, ttl, configuration)` write a mark without an open
session; sessions carrying the value that were created at or before the mark
are rejected on `session:open`, later ones are not. `ttl` must cover the
sessions' absolute timeout (`remember_absolute_timeout` when remember cookies
are used). Marks are stored under a `subject:` or `sid:` prefix and go through
`hash_subject` and `hash_storage_key`, enable them when the values may contain
personal data. Both functions always return write errors; `revocation_fail_mode`
applies to `session:open` and `session:destroy` only.

```lua
-- On login
session:set_subject(id_token.sub)
session:set_sid(id_token.sid)

-- In a back-channel logout handler, without an open session
require("resty.session").revoke_sid(logout_token.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.
Expand Down Expand Up @@ -884,6 +909,40 @@ local ok, err, exists, destroyed = require "resty.session".destroy({
See [configuration](#configuration) for possible configuration settings.


### session.revoke_subject

**syntax:** *ok, err = session.revoke_subject(subject, ttl, configuration)*

It marks a subject (see `session:set_subject`) as revoked without an open
session; sessions carrying the subject that were created at or before the mark
are rejected on `session:open`. `ttl` (in seconds) must cover the sessions'
absolute timeout (`remember_absolute_timeout` when remember cookies are used).
Write errors are always returned.

```lua
local ok, err = require "resty.session".revoke_subject("john@doe.com", 86400)
```

See [configuration](#configuration) for possible configuration settings.


### session.revoke_sid

**syntax:** *ok, err = session.revoke_sid(sid, ttl, configuration)*

It marks an identity provider session id (see `session:set_sid`) as revoked
without an open session; sessions carrying the sid that were created at or
before the mark are rejected on `session:open`. `ttl` (in seconds) must cover
the sessions' absolute timeout (`remember_absolute_timeout` when remember
cookies are used). Write errors are always returned.

```lua
local ok, err = require "resty.session".revoke_sid(logout_token.sid, 86400)
```

See [configuration](#configuration) for possible configuration settings.


## Instance Methods

### session:open
Expand Down Expand Up @@ -1117,6 +1176,33 @@ end
```


### session:set_sid

**syntax:** *session:set_sid(sid)*

Set an identity provider session id (e.g. the OpenID Connect `sid` claim) so
that `session.revoke_sid` can revoke the session by it.

```lua
local session = require "resty.session".new()
session:set_sid(id_token.sid)
```


### session:get_sid

**syntax:** *sid = session:get_sid()*

Get the identity provider session id.

```lua
local session, err, exists = require "resty.session".open()
if exists then
local sid = session:get_sid()
end
```


### session:get_property

**syntax:** *value = session:get_property(name)*
Expand Down
185 changes: 171 additions & 14 deletions lib/resty/session.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -412,10 +407,37 @@ 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

local revoked_at = tonumber(mark)
return revoked_at ~= nil and revoked_at >= creation_time, nil
end


local function subject_revocation_key(self, subject)
return "subject:" .. self.hash_subject(subject)
end


local function sid_revocation_key(sid)
return "sid:" .. sid
end


local function is_revoked_by(self, subject, sid, current_time, creation_time)
if subject then
local revoked, err = is_revoked(self, subject_revocation_key(self, subject), self.cookie_name, current_time, creation_time)
if revoked or err then
return revoked, err
end
end

if sid then
return is_revoked(self, sid_revocation_key(sid), self.cookie_name, current_time, creation_time)
end

return false, nil
end

Expand Down Expand Up @@ -795,7 +817,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
Expand Down Expand Up @@ -945,9 +967,35 @@ 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 or sid as userdata
if type(data[i][3]) == "userdata" then
data[i][3] = nil
end
if type(data[i][4]) == "userdata" then
data[i][4] = nil
end

if not audience_index and data[i][2] == audience then
audience_index = i
break
end
end

if self.revocation then
for i = 1, count do
if remember or i == audience_index then
local revoked, err = is_revoked_by(self, data[i][3], data[i][4], current_time, creation_time)
if err then
return nil, err
end
if revoked then
if remember then
self.remember_meta = {}
else
self.meta = DUMMY_META
end
return nil, "session revoked"
end
end
end
end

Expand Down Expand Up @@ -1942,6 +1990,42 @@ function metatable:get_subject()
end


---
-- Set session sid.
--
-- An identity provider's session id (e.g. the OpenID Connect `sid` claim)
-- carried in the payload; `session.revoke_sid` revokes sessions by it.
--
-- @function instance:set_sid
-- @tparam string|nil sid identity provider session id (`nil` clears it)
--
-- @usage
-- local session = require("resty.session").new()
-- session:set_sid(id_token.sid)
function metatable:set_sid(sid)
assert(self.state ~= STATE_CLOSED, "unable to set sid on closed session")
assert(sid == nil or type(sid) == "string", "invalid sid")
self.data[self.data_index][4] = sid
end


---
-- Get session sid.
--
-- @function instance:get_sid
-- @treturn string|nil identity provider session id
--
-- @usage
-- local session, err, exists = require("resty.session").open()
-- if exists then
-- local sid = session:get_sid()
-- end
function metatable:get_sid()
assert(self.state ~= STATE_CLOSED, "unable to get sid on closed session")
return self.data[self.data_index][4]
end


---
-- Get session property.
--
Expand Down Expand Up @@ -2925,6 +3009,79 @@ function session.destroy(configuration)
end


local function revoke(self, key, ttl)
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)

@bungle bungle Sep 10, 2026 •

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should the storage keys be prefixed with something, like: revoked: or r:, just to be sure that this mechanism cannot be used to manipulate existing sessions, or am I just thinking something that is a non-issue?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we already added prefixes (subject:/ sid:) i think this should be a non-issue (?)

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


---
-- Revoke sessions by subject.
--
-- Writes a mark for a subject (see `session:set_subject`); sessions carrying
-- it that were created at or before now are rejected on open. `ttl` must
-- cover the sessions' absolute timeout (`remember_absolute_timeout` with
-- remember cookies). Write failures are always returned.
--
-- @function module.revoke_subject
-- @tparam string subject subject
-- @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_subject("john@doe.com", 86400)
function session.revoke_subject(subject, ttl, configuration)
assert(type(subject) == "string" and subject ~= "", "invalid subject")
assert(type(ttl) == "number" and ttl > 0, "invalid revocation ttl")

local self = session.new(configuration)
return revoke(self, subject_revocation_key(self, subject), ttl)
end


---
-- Revoke sessions by sid.
--
-- Writes a mark for an identity provider session id (see `session:set_sid`);
-- sessions carrying it that were created at or before now are rejected on
-- open. `ttl` must cover the sessions' absolute timeout
-- (`remember_absolute_timeout` with remember cookies). Write failures are
-- always returned.
--
-- @function module.revoke_sid
-- @tparam string sid identity provider session id
-- @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_sid(logout_token.sid, 86400)
function session.revoke_sid(sid, ttl, configuration)
assert(type(sid) == "string" and sid ~= "", "invalid sid")
assert(type(ttl) == "number" and ttl > 0, "invalid revocation ttl")

return revoke(session.new(configuration), sid_revocation_key(sid), ttl)
end


function session.__set_ngx_log(ngx_log)
log = ngx_log
end
Expand Down
Loading
Loading