From 3462aac8c86b9195bf98aa1e0a7d30bcb4ddd12d Mon Sep 17 00:00:00 2001 From: eriksunsol Date: Tue, 5 May 2020 16:07:59 +0200 Subject: [PATCH] Added revocable session strategy. Session strategy to support revocation of sessions for front-channel and back-channel logout scenarios. --- lib/resty/session/strategies/revocable.lua | 135 +++++++++++++++++++++ 1 file changed, 135 insertions(+) create mode 100644 lib/resty/session/strategies/revocable.lua diff --git a/lib/resty/session/strategies/revocable.lua b/lib/resty/session/strategies/revocable.lua new file mode 100644 index 0000000..6c76206 --- /dev/null +++ b/lib/resty/session/strategies/revocable.lua @@ -0,0 +1,135 @@ +--[[ +Copyright (C) 2020 Modular Management +@Author: Erik Sundkvist - erik.sundkvist@modularmanagement.com +]]-- + +local ngx = ngx +local concat = table.concat + +-- Find strategy to wrap with revocation capability +local wrapped_strategy_name = (ngx.var.revocable_session_strategy or ngx.var.session_strategy or 'default') +if wrapped_strategy_name == 'revocable' then wrapped_strategy_name = 'default' end +local ok, wrapped = pcall(require, 'resty.session.strategies.' .. wrapped_strategy_name) +if not ok then + ngx.log(ngx.WARN, "Wrapped strategy '" .. wrapped_strategy_name .. "' not found. Falling back to 'default'.") + wrapped = require 'resty.session.strategies.default' +else + ngx.log(ngx.DEBUG, "Wrapped strategy '" .. wrapped_strategy_name .. "' loaded.") +end + +-- Inherited API + +local strategy = { + _VERSION = 0.1, + start = wrapped.start, + destroy = wrapped.destroy, + close = wrapped.close, + touch = wrapped.touch, +} + +-- Local stuff + +-- Set up reference to configured storage for revocation list, if $revocation_storage is set. +local revocation_storage +local revocation_storage_name = ngx.var.revocation_storage +if revocation_storage_name then + if revocation_storage_name == "cookie" then + error("$revocation_storage_name must not be 'cookie'") + end + local ok + ok, revocation_storage = pcall(require, "resty.session.storage." .. revocation_storage_name) + if not ok then + ngx.log(ngx.WARN, "$revocation_storage_name = " .. revocation_storage_name .. " (not found)") + revocation_storage = nil + end +end + +-- Get revocation storage reference either as explicitly set through $revocation_storage, +-- or use same as configured for the session provided. Either way, the session object is +-- passed to the storage constructor to allow settings for the storage to be passed the +-- same way as for the session storage itself. +local function get_revocation_storage(session) + if revocation_storage then + return revocation_storage.new(session) + end + if session.cookie.storage == "cookie" then + error("Must set $revocation_storage when $session_storage is 'cookie'") + end + return session.storage.new(session) +end + +local function prefix_revocation_id(iss, session_state) + return concat({ "r", iss, session_state }, ":") +end + +-- Wrapped API + +-- Extend open() method to regenerate session if it has been revoked +function strategy.open(session, cookie) + local ok, err = wrapped.open(session, cookie) + + if not ok then + return ok, err + end + + -- Check revocation + local data = session.data + local revocation_sid = (data.id_token or {}).sid or data.revocation_sid + local iss = (data.id_token or {}).iss + if revocation_sid and iss then + local revocation_storage = get_revocation_storage(session) + local revocation_id = prefix_revocation_id(iss, revocation_sid) + local revoked + revoked, err = revocation_storage:open(revocation_id) + + if err then + return nil, err + end + + if revoked then + ngx.log(ngx.DEBUG, "Session revoked: " .. revocation_id) + session:regenerate(true) + end + end + + return ok, err +end + +-- Extend save() method to revoke revocation if session is authenticated and there is a sid claim in +-- the id_token. +function strategy.save(session, close) + local sid = (session.data.id_token or {}).sid + if sid and session.data.authenticated then + ngx.log(ngx.DEBUG, "Revoking revocation for apparently authenticated session: " .. sid) + -- Revoking the revocation prevents an endless loop if the revocation was not legitimate. + -- Otherwise the OP would be contacted again to authenticate and would be considered logged + -- in already. The RP gets a new id_token OP with the same sid as before (since the OP says + -- we're still logged in with the previous session) which has been revoked here. Repeat. + local revocation_id = prefix_revocation_id(session.data.id_token.iss, sid) + local revocation_storage = get_revocation_storage(session) + revocation_storage:destroy(revocation_id, true) + end + return wrapped.save(session, close) +end + +-- API additions + +-- Revoke session. Call when logout URI generated by OP is processed. +function strategy.revoke(session, iss, sid) + local ok, err + local revocation_id = prefix_revocation_id(iss, sid) + + ngx.log(ngx.DEBUG, "Revoking sessions: " .. revocation_id) + + local revocation_storage = get_revocation_storage(session) + revocation_storage:start(revocation_id) + ok, err = revocation_storage:save(revocation_id, session.cookie.lifetime, "revoked", true) + + if not ok then + ngx.log(ngx.DEBUG, err) + end + + return ok, err +end + +return strategy