Skip to content

Commit bc773b8

Browse files
authored
Merge pull request #571 from Hockenba/fix/propagate-session-destroy-errors-on-logout
Propagate Lua-Resty-Session Revocation Store Failures
2 parents fbfcabf + 0445d70 commit bc773b8

7 files changed

Lines changed: 288 additions & 14 deletions

File tree

ChangeLog

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
07/01/2026
2+
- propagate session destroy and closed-mode revocation store failures from
3+
logout and session start instead of continuing as if logout succeeded
24
- do not use the shared JWT verification cache for bearer JWT validation
35
calls that pass additional claim validators, preventing a token accepted
46
by one validator set from bypassing a stricter validator set on cache hit.

lib/resty/openidc.lua

Lines changed: 48 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,37 @@ local function is_session_present(session)
292292
return session ~= nil and next(session:get_data()) ~= nil
293293
end
294294

295+
-- start a session; propagate closed-mode revocation store failures
296+
local function openidc_start_session(session_opts)
297+
local session, err = r_session.start(session_opts)
298+
if not session then
299+
log(ERROR, "Error starting session: " .. (err or "unknown error"))
300+
return nil, err
301+
end
302+
303+
-- session uses either message when revocation_fail_mode is closed and the
304+
-- revocation store is unavailable (on open or destroy)
305+
if err and session.revocation_fail_mode == "closed"
306+
and (err:find("unable to check session revocation", 1, true)
307+
or err:find("unable to mark session revoked", 1, true)) then
308+
log(ERROR, "Error starting session: " .. err)
309+
return session, err
310+
end
311+
312+
return session, nil
313+
end
314+
315+
-- destroy an open session; propagate destroy() failures
316+
local function openidc_destroy_session(session)
317+
if session and session.state == "open" and is_session_present(session) then
318+
local ok, err = session:destroy()
319+
if not ok then
320+
log(ERROR, "failed to destroy session: " .. err)
321+
return err
322+
end
323+
end
324+
end
325+
295326
-- set value in server-wide cache if available
296327
local function openidc_cache_set(type, key, value, exp)
297328
local dict = ngx.shared[type]
@@ -1870,8 +1901,9 @@ local function openidc_logout(opts, session)
18701901
end
18711902
end
18721903

1873-
if is_session_present(session) then
1874-
session:destroy()
1904+
err = openidc_destroy_session(session)
1905+
if err then
1906+
return err
18751907
end
18761908

18771909
if opts.revoke_tokens_on_logout then
@@ -2047,11 +2079,9 @@ function openidc.authenticate(opts, target_url, unauth_action, session_or_opts)
20472079
if is_session(session_or_opts) then
20482080
session = session_or_opts
20492081
else
2050-
local session_error
2051-
session, session_error = r_session.start(session_or_opts)
2052-
if session == nil then
2053-
log(ERROR, "Error starting session: " .. session_error)
2054-
return nil, session_error, target_url, session
2082+
session, err = openidc_start_session(session_or_opts)
2083+
if err then
2084+
return nil, err, target_url, session
20552085
end
20562086
end
20572087

@@ -2084,7 +2114,10 @@ function openidc.authenticate(opts, target_url, unauth_action, session_or_opts)
20842114
return nil, err, session:get("original_url"), session
20852115
end
20862116

2087-
openidc_logout(opts, session)
2117+
err = openidc_logout(opts, session)
2118+
if err then
2119+
return nil, err, target_url, session
2120+
end
20882121
return nil, nil, target_url, session
20892122
end
20902123

@@ -2185,7 +2218,13 @@ end
21852218
-- get a valid access_token (eventually refreshing the token), or nil if there's no valid access_token
21862219
function openidc.access_token(opts, session_opts)
21872220

2188-
local session = r_session.start(session_opts)
2221+
local session, err = openidc_start_session(session_opts)
2222+
if err then
2223+
if session then
2224+
session:close()
2225+
end
2226+
return nil, err
2227+
end
21892228
local token, err = openidc_access_token(opts, session, true)
21902229
session:close()
21912230
return token, err

lua-resty-openidc-1.9.0-1.rockspec

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ description = {
2424
dependencies = {
2525
"lua >= 5.1",
2626
"lua-resty-http >= 0.08",
27-
"lua-resty-session >= 4.0.3",
27+
"lua-resty-session == 4.2.0-1",
2828
"lua-resty-jwt >= 0.2.0",
2929
"lua-resty-openssl >= 1.8.0"
3030
}

tests/Dockerfile

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
FROM openresty/openresty:focal
22

33
# install dependencies
4-
RUN ["luarocks", "install", "lua-resty-session", "4.0.3"]
4+
RUN ["luarocks", "install", "lua-resty-session", "4.2.0-1"]
55
RUN ["luarocks", "install", "lua-resty-http"]
66
RUN ["luarocks", "install", "lua-resty-jwt"]
77

88
# install test dependencies
99
RUN apt-get update \
10-
&& apt-get install -y --no-install-recommends git \
10+
&& apt-get install -y --no-install-recommends ca-certificates git \
1111
&& rm -rf /var/lib/apt/lists/*
1212
RUN ["luarocks", "install", "busted"]
1313
RUN ["luarocks", "install", "LuaSocket"]

tests/spec/access_token_access_spec.lua

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,59 @@ describe("if there is an active non-expired login", function()
3939
end)
4040
end)
4141

42+
describe("if revocation_fail_mode is closed and the revocation store read fails", function()
43+
test_support.start_server({
44+
revocation_test = {
45+
fail_mode = "closed",
46+
get_fails_after = 1,
47+
},
48+
})
49+
teardown(test_support.stop_server)
50+
51+
local _, _, cookies = test_support.login()
52+
local content_table = {}
53+
local _, status = http.request({
54+
url = "http://localhost/access_token",
55+
redirect = false,
56+
headers = { cookie = cookies },
57+
sink = ltn12.sink.table(content_table)
58+
})
59+
local body = table.concat(content_table)
60+
61+
it("returns the session start failure", function()
62+
assert.are.equals(401, status)
63+
assert.truthy(string.find(body, "unable to check session revocation", 1, true))
64+
end)
65+
66+
it("does not return the access token", function()
67+
assert.is_nil(string.find(body, "a_token", 1, true))
68+
end)
69+
end)
70+
71+
describe("if revocation_fail_mode is open and the revocation store read fails", function()
72+
test_support.start_server({
73+
revocation_test = {
74+
fail_mode = "open",
75+
get_fails_after = 1,
76+
},
77+
})
78+
teardown(test_support.stop_server)
79+
80+
local _, _, cookies = test_support.login()
81+
local content_table = {}
82+
local _, status = http.request({
83+
url = "http://localhost/access_token",
84+
redirect = false,
85+
headers = { cookie = cookies },
86+
sink = ltn12.sink.table(content_table)
87+
})
88+
89+
it("returns the access token", function()
90+
assert.are.equals(200, status)
91+
assert.are.equals("a_token\n", table.concat(content_table))
92+
end)
93+
end)
94+
4295
describe("if there is an active non-expired login but access token is not stored in session", function()
4396
test_support.start_server({
4497
access_token_opts = {
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
local http = require("socket.http")
2+
local ltn12 = require("ltn12")
3+
local test_support = require("test_support")
4+
require 'busted.runner'()
5+
6+
describe("when session startup returns no session", function()
7+
test_support.start_server({
8+
session_start_fails = true,
9+
})
10+
teardown(test_support.stop_server)
11+
12+
local response_body = {}
13+
local _, status = http.request({
14+
url = "http://127.0.0.1/default/t",
15+
redirect = false,
16+
sink = ltn12.sink.table(response_body),
17+
})
18+
19+
it("returns the startup error without authenticating the request", function()
20+
assert.are.equals(401, status)
21+
assert.truthy(string.find(table.concat(response_body), "session start failed", 1, true))
22+
end)
23+
end)
24+
25+
describe("when revocation_fail_mode is closed and the revocation store read fails", function()
26+
test_support.start_server({
27+
revocation_test = {
28+
fail_mode = "closed",
29+
get_fails_after = 1,
30+
},
31+
})
32+
teardown(test_support.stop_server)
33+
34+
local _, _, cookie = test_support.login()
35+
36+
local response_body = {}
37+
local _, status = http.request({
38+
url = "http://127.0.0.1/default/t",
39+
headers = { cookie = cookie },
40+
redirect = false,
41+
sink = ltn12.sink.table(response_body),
42+
})
43+
local body = table.concat(response_body)
44+
45+
it("propagates the session start failure", function()
46+
assert.are.equals(401, status)
47+
assert.truthy(string.find(body, "unable to check session revocation", 1, true))
48+
end)
49+
50+
it("does not authenticate the request", function()
51+
assert.is_nil(string.find(body, "hello, world!", 1, true))
52+
end)
53+
end)
54+
55+
describe("when revocation_fail_mode is open and the revocation store read fails", function()
56+
test_support.start_server({
57+
revocation_test = {
58+
fail_mode = "open",
59+
get_fails_after = 1,
60+
},
61+
})
62+
teardown(test_support.stop_server)
63+
64+
local _, _, cookie = test_support.login()
65+
66+
local response_body = {}
67+
local _, status = http.request({
68+
url = "http://127.0.0.1/default/t",
69+
headers = { cookie = cookie },
70+
redirect = false,
71+
sink = ltn12.sink.table(response_body),
72+
})
73+
74+
it("authenticates the request", function()
75+
assert.are.equals(200, status)
76+
assert.truthy(string.find(table.concat(response_body), "hello, world!", 1, true))
77+
end)
78+
end)
79+
80+
describe("when revocation_fail_mode is closed and the revocation store is unreachable during logout", function()
81+
test_support.start_server({
82+
revocation_test = {
83+
fail_mode = "closed",
84+
set_fails = true,
85+
},
86+
})
87+
teardown(test_support.stop_server)
88+
89+
local _, _, cookie = test_support.login()
90+
91+
local response_body = {}
92+
local _, status = http.request({
93+
url = "http://127.0.0.1/default/logout",
94+
headers = { cookie = cookie },
95+
redirect = false,
96+
sink = ltn12.sink.table(response_body),
97+
})
98+
local body = table.concat(response_body)
99+
100+
it("propagates destroy failure", function()
101+
assert.are.equals(401, status)
102+
assert.truthy(string.find(body, "unable to mark session revoked", 1, true))
103+
end)
104+
105+
it("leaves the session valid for replay", function()
106+
local _, replay_status = http.request({
107+
url = "http://127.0.0.1/default/t",
108+
headers = { cookie = cookie },
109+
redirect = false,
110+
})
111+
assert.are.equals(200, replay_status)
112+
end)
113+
end)
114+
115+
describe("when revocation_fail_mode is open and the revocation store is unreachable during logout", function()
116+
test_support.start_server({
117+
revocation_test = {
118+
fail_mode = "open",
119+
set_fails = true,
120+
},
121+
})
122+
teardown(test_support.stop_server)
123+
124+
local _, _, cookie = test_support.login()
125+
126+
local _, status = http.request({
127+
url = "http://127.0.0.1/default/logout",
128+
headers = { cookie = cookie },
129+
redirect = false,
130+
})
131+
132+
it("logout still completes", function()
133+
assert.are.equals(200, status)
134+
end)
135+
end)

0 commit comments

Comments
 (0)