Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 76 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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` | Storage used for cookie session revocation records. Use `nil` or `false` to disable, a storage name such as `"shm"`, `"redis"`, `"mysql"`, or `"postgres"`, a custom storage module name, or a storage `table` with `set`/`get` methods. |
| `revocation_fail_mode` | `"open"` | Behavior when the revocation store is unreachable, use `"open"` (treat as not revoked) or `"closed"` (reject the session). |
Comment thread
bungle marked this conversation as resolved.
| `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) |
Expand All @@ -343,6 +346,79 @@ 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 storage-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"`). Select the backend explicitly with
`revocation = "dshm"`, `"file"`, `"memcached"`, `"mysql"`, `"postgres"`,
`"redis"`, or `"shm"`. The backend uses its normal configuration section and
the same storage `set`/`get` contract used for session data. Custom storage
module names and pre-built storage tables are also supported. Setting
`revocation = false` or leaving it unset disables revocation.

On every `session:open`, the library checks whether the session identifier is
revoked. On `session:destroy`, the identifier is written to the selected
Comment thread
bungle marked this conversation as resolved.
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.

Use `revocation_fail_mode` to control behavior when the storage is unavailable:

- `"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.

Examples:

```lua
-- Redis denylist
require("resty.session").init({
storage = "cookie",
revocation = "redis",
redis = {
host = "127.0.0.1",
password = "secret",
prefix = "sessions",
},
})

-- Shared memory denylist
require("resty.session").init({
storage = "cookie",
revocation = "shm",
shm = {
zone = "sessions",
prefix = "revocations",
},
})

-- MySQL denylist
require("resty.session").init({
storage = "cookie",
revocation = "mysql",
mysql = {
host = "127.0.0.1",
database = "sessions",
username = "session",
password = "secret",
},
})
```

The same pattern works for `"dshm"`, `"file"`, `"memcached"`, and `"postgres"`.


## DSHM Storage Configuration

With DHSM storage you can use the following settings (set the `storage` to `"dshm"`):
Expand Down
127 changes: 127 additions & 0 deletions lib/resty/session.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {}
Expand Down Expand Up @@ -372,6 +374,83 @@ local function get_store_ttl(self, remember, current_time, creation_time, rollin
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
Expand Down Expand Up @@ -716,6 +795,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
Expand Down Expand Up @@ -1253,6 +1340,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
Expand Down Expand Up @@ -2341,6 +2433,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 Storage used for cookie session revocation records, use `nil` or `false` to disable, `"dshm"`, `"file"`, `"memcached"`, `"mysql"`, `"postgres"`, `"redis"`, or `"shm"`, a custom storage module name, or a storage `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" }`
-- @field memcached Configuration for memcached storage, e.g. `{ prefix = "sessions" }`
Expand Down Expand Up @@ -2401,6 +2495,7 @@ local function opt(configuration, name, default)
end
end
end

end

else
Expand Down Expand Up @@ -2451,6 +2546,28 @@ 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
local t = type(value)
if t == "string" then
value = assert(load_storage(value, configuration), "unable to load session revocation storage")

elseif t == "table" then
if type(value.set) ~= "function" or type(value.get) ~= "function" then
error("invalid session revocation")
end

else
error("invalid session revocation")
end
end

elseif name == "revocation_fail_mode" then
assert(value == "open" or value == "closed", "invalid revocation fail mode")
end
end

Expand Down Expand Up @@ -2497,6 +2614,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

---
Expand Down Expand Up @@ -2553,6 +2672,12 @@ 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 storage then
revocation = nil
end

if cookie_prefix == "__Host-" then
cookie_name = cookie_prefix .. cookie_name
Expand Down Expand Up @@ -2625,6 +2750,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,
Expand Down
4 changes: 2 additions & 2 deletions lib/resty/session/file/thread.lua
Original file line number Diff line number Diff line change
Expand Up @@ -128,8 +128,8 @@ local function get(path, prefix, suffix, name, key, current_time)
-- TODO: do we want to check expiry here?
-- The cookie header already has the info and has a MAC too.
local exp = get_modification(file_path)
if exp and exp < current_time then
return nil, "expired"
if not exp or exp < current_time then
return nil
end

return file_read(file_path)
Expand Down
6 changes: 3 additions & 3 deletions lib/resty/session/mysql.lua
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ local DEFAULT_TABLE = "sessions"
local DEFAULT_CHARSET = "ascii"


local SET = "INSERT INTO %s (sid, name, data, exp) VALUES ('%s', '%s', '%s', FROM_UNIXTIME(%d)) AS new ON DUPLICATE KEY UPDATE data = new.data"
local SET = "INSERT INTO %s (sid, name, data, exp) VALUES ('%s', '%s', '%s', FROM_UNIXTIME(%d)) AS new ON DUPLICATE KEY UPDATE data = new.data, exp = new.exp"
local SET_META_PREFIX = "INSERT INTO %s (aud, sub, sid) VALUES "
local SET_META_VALUES = "('%s', '%s', '%s')"
local SET_META_SUFFIX = " ON DUPLICATE KEY UPDATE sid = sid"
Expand Down Expand Up @@ -193,12 +193,12 @@ function metatable:get(name, key, current_time) -- luacheck: ignore

local row = res[1]
if not row then
return nil, "session not found"
return nil
end

local data = row.data
if not row.data then
return nil, "session not found"
return nil
end

return data
Expand Down
Loading
Loading