Skip to content
Open
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
65 changes: 65 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
Expand Down Expand Up @@ -367,6 +370,24 @@ 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 (`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
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.
Expand Down Expand Up @@ -884,6 +905,23 @@ 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 (`remember_absolute_timeout` when remember cookies are used).
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
Expand Down Expand Up @@ -1117,6 +1155,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)*
Expand Down
130 changes: 115 additions & 15 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,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


Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -945,9 +941,33 @@ 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

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 = {}
else
self.meta = DUMMY_META
end
return nil, "session revoked"
end
end
end
end

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


---
-- Set session revocation keys.
--
-- 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)
--
-- @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.
--
Expand Down Expand Up @@ -2925,6 +2982,49 @@ 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 (`remember_absolute_timeout` with remember cookies).
-- 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
Expand Down
Loading