Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added revocable session strategy to support OP initiated logout scenarios #330

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
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
135 changes: 135 additions & 0 deletions lib/resty/session/strategies/revocable.lua
Original file line number Diff line number Diff line change
@@ -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