Skip to content

Commit a5700c8

Browse files
authored
Merge pull request #210 from oleeander/master
Revoke refresh_token and access_token on logout
2 parents e131b08 + 1a7060f commit a5700c8

File tree

6 files changed

+265
-3
lines changed

6 files changed

+265
-3
lines changed

AUTHORS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,4 @@ reporting bugs, providing fixes, suggesting useful features or other:
2929
Donghang Lin <https://github.com/dhlin>
3030
Arcadiy Ivanov <https://github.com/arcivanov>
3131
Dmitriy Blok <https://github.com/dmitriyblok>
32+
Oleander Reis <https://github.com/oleeander>

ChangeLog

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
10/18/2018
2+
- add token revocation support on logout (opts.revoke_tokens_on_logout)
3+
14
10/16/2018
25
- lua-resty-openidc now creates a new session whenever the token(s)
36
are refreshed, trying to soften the impact when multiple requests

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,10 @@ http {
177177
-- Connect provider that ignores the paramter as the
178178
-- id_token will be rejected otherwise.
179179
180+
--revoke_tokens_on_logout = false
181+
-- When revoke_tokens_on_logout is set to true a logout notifies the authorization server that previously obtained refresh and access tokens are no longer needed. This requires that revocation_endpoint is discoverable.
182+
-- If there is no revocation endpoint supplied or if there are errors on revocation the user will not be notified and the logout process continues normally.
183+
180184
-- Optional : use outgoing proxy to the OpenID Connect provider endpoints with the proxy_opts table :
181185
-- this requires lua-resty-http >= 0.12
182186
-- proxy_opts = {

lib/resty/openidc.lua

Lines changed: 52 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -340,7 +340,8 @@ local function openidc_authorize(opts, session, target_url, prompt)
340340
end
341341

342342
-- parse the JSON result from a call to the OP
343-
local function openidc_parse_json_response(response)
343+
local function openidc_parse_json_response(response, ignore_body_on_success)
344+
local ignore_body_on_success = ignore_body_on_success or false
344345

345346
local err
346347
local res
@@ -349,6 +350,10 @@ local function openidc_parse_json_response(response)
349350
if response.status ~= 200 then
350351
err = "response indicates failure, status=" .. response.status .. ", body=" .. response.body
351352
else
353+
if ignore_body_on_success then
354+
return nil, nil
355+
end
356+
352357
-- decode the response and extract the JSON object
353358
res = cjson_s.decode(response.body)
354359

@@ -381,7 +386,8 @@ local function openidc_configure_proxy(httpc, proxy_opts)
381386
end
382387

383388
-- make a call to the token endpoint
384-
function openidc.call_token_endpoint(opts, endpoint, body, auth, endpoint_name)
389+
function openidc.call_token_endpoint(opts, endpoint, body, auth, endpoint_name, ignore_body_on_success)
390+
local ignore_body_on_success = ignore_body_on_success or false
385391

386392
local ep_name = endpoint_name or 'token'
387393
local headers = {
@@ -441,7 +447,7 @@ function openidc.call_token_endpoint(opts, endpoint, body, auth, endpoint_name)
441447

442448
log(DEBUG, ep_name .. " endpoint response: ", res.body)
443449

444-
return openidc_parse_json_response(res)
450+
return openidc_parse_json_response(res, ignore_body_on_success)
445451
end
446452

447453
-- make a call to the userinfo endpoint
@@ -1086,6 +1092,39 @@ local function openidc_authorization_response(opts, session)
10861092
return nil, nil, session.data.original_url, session
10871093
end
10881094

1095+
-- token revocation (RFC 7009)
1096+
local function openidc_revoke_token(opts, token_type_hint, token)
1097+
if not opts.discovery.revocation_endpoint then
1098+
log(DEBUG, "no revocation endpoint supplied. unable to revoke " .. token_type_hint .. ".")
1099+
return nil
1100+
end
1101+
1102+
local token_type_hint = token_type_hint or nil
1103+
local body = {
1104+
token = token
1105+
}
1106+
if token_type_hint then
1107+
body['token_type_hint'] = token_type_hint
1108+
end
1109+
1110+
-- ensure revocation endpoint auth method is properly discovered
1111+
err = ensure_config(opts)
1112+
if err then
1113+
log(ERROR, "revocation of " .. token_type_hint .. " unsuccessful: " .. err)
1114+
return false
1115+
end
1116+
1117+
-- call the revocation endpoint
1118+
_, err = openidc.call_token_endpoint(opts, opts.discovery.revocation_endpoint, body, opts.token_endpoint_auth_method, "revocation", true)
1119+
if err then
1120+
log(ERROR, "revocation of " .. token_type_hint .. " unsuccessful: " .. err)
1121+
return false
1122+
else
1123+
log(DEBUG, "revocation of " .. token_type_hint .. " successful")
1124+
return true
1125+
end
1126+
end
1127+
10891128
local openidc_transparent_pixel = "\137\080\078\071\013\010\026\010\000\000\000\013\073\072\068\082" ..
10901129
"\000\000\000\001\000\000\000\001\008\004\000\000\000\181\028\012" ..
10911130
"\002\000\000\000\011\073\068\065\084\120\156\099\250\207\000\000" ..
@@ -1095,7 +1134,17 @@ local openidc_transparent_pixel = "\137\080\078\071\013\010\026\010\000\000\000\
10951134
-- handle logout
10961135
local function openidc_logout(opts, session)
10971136
local session_token = session.data.enc_id_token
1137+
local access_token = session.data.access_token
1138+
local refresh_token = session.data.refresh_token
10981139
session:destroy()
1140+
1141+
if opts.revoke_tokens_on_logout then
1142+
log(DEBUG, "revoke_tokens_on_logout is enabled. " ..
1143+
"trying to revoke access and refresh tokens...")
1144+
openidc_revoke_token(opts, "refresh_token", refresh_token)
1145+
openidc_revoke_token(opts, "access_token", access_token)
1146+
end
1147+
10991148
local headers = ngx.req.get_headers()
11001149
local header = headers['Accept']
11011150
if header and header:find("image/png") then

tests/spec/logout_spec.lua

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -365,3 +365,190 @@ describe("when logout is invoked and discovery contains ping_end_session_endpoin
365365
end)
366366
end)
367367

368+
describe("when revoke_tokens_on_logout is enabled and a valid revocation endpoint is supplied with auth method client_secret_basic", function()
369+
test_support.start_server({
370+
oidc_opts = {
371+
revoke_tokens_on_logout = true,
372+
discovery = {
373+
revocation_endpoint = "http://127.0.0.1/revocation",
374+
token_endpoint_auth_methods_supported = { "foo", "client_secret_post", "client_secret_basic" }
375+
},
376+
token_endpoint_auth_method = "client_secret_basic"
377+
}
378+
})
379+
teardown(test_support.stop_server)
380+
local _, _, cookie = test_support.login()
381+
local _, status, headers = http.request({
382+
url = "http://127.0.0.1/default/logout",
383+
headers = { cookie = cookie },
384+
redirect = false
385+
})
386+
it("the response contains a default HTML-page", function()
387+
assert.are.equals(200, status)
388+
assert.are.equals("text/html", headers["content-type"])
389+
end)
390+
391+
it("the session cookie has been revoked", function()
392+
assert.truthy(string.match(headers["set-cookie"],
393+
"session=; Expires=Thu, 01 Jan 1970 00:00:01 GMT.*"))
394+
end)
395+
396+
it("authorization credentials have not been passed on as post parameters to the revocation endpoint", function()
397+
assert.is_not.error_log_contains("Received revocation request: .*client_id")
398+
end)
399+
400+
it("authorization header has been passed on to the revocation endpoint", function()
401+
assert.error_log_contains("revocation authorization header: Basic .+")
402+
end)
403+
404+
it("token to be revoked has been passed on as a post parameter to the revocation endpoint", function()
405+
assert.error_log_contains("Received revocation request: .*token=.+")
406+
end)
407+
408+
it("debug messages concerning successful revocation have been logged", function()
409+
assert.error_log_contains("revocation of refresh_token successful")
410+
assert.error_log_contains("revocation of access_token successful")
411+
end)
412+
end)
413+
414+
describe("when revoke_tokens_on_logout is enabled and a valid revocation endpoint is supplied with auth method client_secret_post", function()
415+
test_support.start_server({
416+
oidc_opts = {
417+
revoke_tokens_on_logout = true,
418+
discovery = {
419+
revocation_endpoint = "http://127.0.0.1/revocation",
420+
token_endpoint_auth_methods_supported = { "foo", "client_secret_basic", "client_secret_post" }
421+
},
422+
token_endpoint_auth_method = "client_secret_post"
423+
}
424+
})
425+
teardown(test_support.stop_server)
426+
local _, _, cookie = test_support.login()
427+
local _, status, headers = http.request({
428+
url = "http://127.0.0.1/default/logout",
429+
headers = { cookie = cookie },
430+
redirect = false
431+
})
432+
it("the response contains a default HTML-page", function()
433+
assert.are.equals(200, status)
434+
assert.are.equals("text/html", headers["content-type"])
435+
end)
436+
437+
it("the session cookie has been revoked", function()
438+
assert.truthy(string.match(headers["set-cookie"],
439+
"session=; Expires=Thu, 01 Jan 1970 00:00:01 GMT.*"))
440+
end)
441+
442+
it("authorization header has not been passed on to the revocation endpoint", function()
443+
assert.is_not.error_log_contains("revocation authorization header: Basic")
444+
end)
445+
446+
it("authorization credentials have been passed on as post parameters to the revocation endpoint", function()
447+
assert.error_log_contains("Received revocation request: .*client_id=.+")
448+
end)
449+
450+
it("token to be revoked has been passed on as a post parameter to the revocation endpoint", function()
451+
assert.error_log_contains("Received revocation request: .*token=.+")
452+
end)
453+
454+
it("debug messages concerning successful revocation have been logged", function()
455+
assert.error_log_contains("revocation of refresh_token successful")
456+
assert.error_log_contains("revocation of access_token successful")
457+
end)
458+
end)
459+
460+
describe("when revoke_tokens_on_logout is enabled and an invalid revocation endpoint is supplied", function()
461+
test_support.start_server({
462+
oidc_opts = {
463+
revoke_tokens_on_logout = true,
464+
discovery = {
465+
revocation_endpoint = "http://127.0.0.1/invalid_revocation"
466+
}
467+
}
468+
})
469+
teardown(test_support.stop_server)
470+
local _, _, cookie = test_support.login()
471+
local _, status, headers = http.request({
472+
url = "http://127.0.0.1/default/logout",
473+
headers = { cookie = cookie },
474+
redirect = false
475+
})
476+
it("the response still contains a default HTML-page", function()
477+
assert.are.equals(200, status)
478+
assert.are.equals("text/html", headers["content-type"])
479+
end)
480+
481+
it("the session cookie still has been revoked", function()
482+
assert.truthy(string.match(headers["set-cookie"],
483+
"session=; Expires=Thu, 01 Jan 1970 00:00:01 GMT.*"))
484+
end)
485+
486+
it("error messages concerning unseccussful revocation have been logged", function()
487+
assert.error_log_contains("revocation of refresh_token unsuccessful")
488+
assert.error_log_contains("revocation of access_token unsuccessful")
489+
end)
490+
end)
491+
492+
describe("when revoke_tokens_on_logout is enabled but no revocation endpoint is supplied", function()
493+
test_support.start_server({
494+
oidc_opts = {
495+
revoke_tokens_on_logout = true,
496+
discovery = {
497+
revocation_endpoint = nil
498+
}
499+
}
500+
})
501+
teardown(test_support.stop_server)
502+
local _, _, cookie = test_support.login()
503+
local _, status, headers = http.request({
504+
url = "http://127.0.0.1/default/logout",
505+
headers = { cookie = cookie },
506+
redirect = false
507+
})
508+
it("the response still contains a default HTML-page", function()
509+
assert.are.equals(200, status)
510+
assert.are.equals("text/html", headers["content-type"])
511+
end)
512+
513+
it("the session cookie still has been revoked", function()
514+
assert.truthy(string.match(headers["set-cookie"],
515+
"session=; Expires=Thu, 01 Jan 1970 00:00:01 GMT.*"))
516+
end)
517+
518+
it("debug messages concerning unseccussful revocation have been logged", function()
519+
assert.error_log_contains("no revocation endpoint supplied. unable to revoke refresh_token")
520+
assert.error_log_contains("no revocation endpoint supplied. unable to revoke access_token")
521+
end)
522+
end)
523+
524+
describe("when revoke_tokens_on_logout is not defined and a revocation_endpoint is given", function()
525+
test_support.start_server({
526+
oidc_opts = {
527+
revoke_tokens_on_logout = nil,
528+
discovery = {
529+
revocation_endpoint = "http://127.0.0.1/revocation"
530+
}
531+
}
532+
})
533+
teardown(test_support.stop_server)
534+
local _, _, cookie = test_support.login()
535+
local _, status, headers = http.request({
536+
url = "http://127.0.0.1/default/logout",
537+
headers = { cookie = cookie },
538+
redirect = false
539+
})
540+
it("the response still contains a default HTML-page", function()
541+
assert.are.equals(200, status)
542+
assert.are.equals("text/html", headers["content-type"])
543+
end)
544+
545+
it("the session cookie still has been revoked", function()
546+
assert.truthy(string.match(headers["set-cookie"],
547+
"session=; Expires=Thu, 01 Jan 1970 00:00:01 GMT.*"))
548+
end)
549+
550+
it("no messages concerning revocation have been logged", function()
551+
assert.is_not.error_log_contains("revocation")
552+
assert.is_not.error_log_contains("revoke")
553+
end)
554+
end)

tests/spec/test_support.lua

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -370,6 +370,23 @@ JWT_SIGN_SECRET]=]
370370
end
371371
}
372372
}
373+
374+
location /revocation {
375+
content_by_lua_block {
376+
ngx.req.read_body()
377+
ngx.log(ngx.ERR, "Received revocation request: " .. ngx.req.get_body_data())
378+
local auth = ngx.req.get_headers()["Authorization"]
379+
ngx.log(ngx.ERR, "revocation authorization header: " .. (auth and auth or ""))
380+
local cookie = ngx.req.get_headers()["Cookie"]
381+
if not cookie then
382+
ngx.log(ngx.ERR, "no cookie in introspection call")
383+
end
384+
ngx.header.content_type = 'application/json;charset=UTF-8'
385+
delay(REVOCATION_DELAY_RESPONSE)
386+
ngx.status = 200
387+
ngx.say('INVALID JSON.')
388+
}
389+
}
373390
}
374391
}
375392
]]
@@ -450,6 +467,7 @@ local function write_config(out, custom_config)
450467
:gsub("DISCOVERY_DELAY_RESPONSE", ((custom_config["delay_response"] or {}).discovery or DEFAULT_DELAY_RESPONSE))
451468
:gsub("USERINFO_DELAY_RESPONSE", ((custom_config["delay_response"] or {}).userinfo or DEFAULT_DELAY_RESPONSE))
452469
:gsub("INTROSPECTION_DELAY_RESPONSE", ((custom_config["delay_response"] or {}).introspection or DEFAULT_DELAY_RESPONSE))
470+
:gsub("REVOCATION_DELAY_RESPONSE", ((custom_config["delay_response"] or {}).revocation or DEFAULT_DELAY_RESPONSE))
453471
:gsub("JWK", custom_config["jwk"] or DEFAULT_JWK)
454472
:gsub("USERINFO", serpent.block(userinfo, {comment = false }))
455473
:gsub("FAKE_ACCESS_TOKEN_SIGNATURE", custom_config["fake_access_token_signature"] or DEFAULT_FAKE_ACCESS_TOKEN_SIGNATURE)

0 commit comments

Comments
 (0)