diff --git a/apisix/plugins/openid-connect.lua b/apisix/plugins/openid-connect.lua index eb0d3b526c58..0d5d6258f5a8 100644 --- a/apisix/plugins/openid-connect.lua +++ b/apisix/plugins/openid-connect.lua @@ -156,6 +156,13 @@ local schema = { redis = { type = "object", properties = { + mode = { + type = "string", + enum = {"storage", "revocation"}, + description = + "Whether this Redis connection stores session data " + .. "or the session revocation denylist.", + }, host = { type = "string", minLength = 2, default = "127.0.0.1" }, @@ -206,7 +213,15 @@ local schema = { description = "keepalive timeout in milliseconds", }, } - } + }, + revocation_fail_mode = { + type = "string", + enum = {"open", "closed"}, + default = "open", + description = + "When the revocation store is unreachable: open allows requests " + .. "based on JWT expiry only; closed always denies requests.", + }, }, required = {"secret"}, ["if"] = { diff --git a/docs/en/latest/plugins/openid-connect.md b/docs/en/latest/plugins/openid-connect.md index e196d6922206..2e5270cfb692 100644 --- a/docs/en/latest/plugins/openid-connect.md +++ b/docs/en/latest/plugins/openid-connect.md @@ -79,7 +79,9 @@ The `openid-connect` Plugin supports the integration with [OpenID Connect (OIDC) | session.absolute_timeout | integer | False | | | Absolute session lifetime in seconds. Forwarded to lua-resty-session as `absolute_timeout`. | | session.cookie.lifetime | integer | False | | | Deprecated. Mapped to `session.absolute_timeout` at runtime when `absolute_timeout` is not set. Use `session.absolute_timeout` instead. | | session.storage | string | False | cookie | ["cookie", "redis"] | Session storage method. | -| session.redis | object | False | | | Redis configuration when `storage` is `redis`. | +| session.redis | object | False | | | Redis connection. Required when `storage` is `redis`. Optional when `storage` is `cookie` and session revocation is enabled. | +| session.redis.mode | string | False | | ["storage", "revocation"] | Role of this Redis connection. Use `storage` when `session.storage` is `redis`; use `revocation` when `session.storage` is `cookie`. Defaults to `storage` or `revocation` respectively when omitted. | +| session.revocation_fail_mode | string | False | open | ["open", "closed"] | When the revocation store is unreachable: `open` allows requests based on JWT expiry only; `closed` always denies requests. Only applies when `session.redis.mode` is `revocation`. | | session.redis.host | string | False | 127.0.0.1 | | Redis host. | | session.redis.port | integer | False | 6379 | | Redis port. | | session.redis.username | string | False | | | Redis username. | diff --git a/t/plugin/openid-connect-revocation.t b/t/plugin/openid-connect-revocation.t new file mode 100644 index 000000000000..9b51599a0eb7 --- /dev/null +++ b/t/plugin/openid-connect-revocation.t @@ -0,0 +1,517 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +use t::APISIX 'no_plan'; + +repeat_each(1); +no_long_string(); +no_root_location(); +no_shuffle(); + +add_block_preprocessor(sub { + my ($block) = @_; + + if ((!defined $block->error_log) && (!defined $block->no_error_log)) { + $block->set_value("no_error_log", "[error]"); + } + + if (!defined $block->request) { + $block->set_value("request", "GET /t"); + } +}); + +run_tests(); + +__DATA__ + +=== TEST 1: typical cookie session with redis revocation config passes schema +--- config + location /t { + content_by_lua_block { + local plugin = require("apisix.plugins.openid-connect") + local conf = { + client_id = "a", + client_secret = "b", + discovery = "c", + session = { + secret = "jwcE5v3pM9VhqLxmxFOH9uZaLo8u7KQK", + storage = "cookie", + cookie_name = "oidc_session", + absolute_timeout = 3600, + redis = { + host = "redis.internal", + port = 6379, + password = "secret", + database = 1, + prefix = "oidc:session:", + mode = "revocation", + ssl = true, + connect_timeout = 1000, + }, + } + } + local ok, err = plugin.check_schema(conf) + if not ok then + ngx.say(err) + else + ngx.say("revocation_fail_mode=", conf.session.revocation_fail_mode) + end + } + } +--- response_body +revocation_fail_mode=open + + + +=== TEST 2: build_session_opts passes typical revocation config through unchanged +--- config + location /t { + content_by_lua_block { + local plugin = require("apisix.plugins.openid-connect") + local build = plugin._build_session_opts + local opts = build({ + secret = "jwcE5v3pM9VhqLxmxFOH9uZaLo8u7KQK", + storage = "cookie", + cookie_name = "oidc_session", + absolute_timeout = 3600, + redis = { + host = "redis.internal", + port = 6379, + password = "secret", + database = 1, + prefix = "oidc:session:", + mode = "revocation", + }, + }) + ngx.say("cookie_name=", opts.cookie_name) + ngx.say("absolute_timeout=", opts.absolute_timeout) + ngx.say("redis.prefix=", opts.redis.prefix) + ngx.say("redis.mode=", opts.redis.mode) + } + } +--- response_body +cookie_name=oidc_session +absolute_timeout=3600 +redis.prefix=oidc:session: +redis.mode=revocation + + + +=== TEST 3: typical redis-backed session storage config passes schema +--- config + location /t { + content_by_lua_block { + local plugin = require("apisix.plugins.openid-connect") + local ok, err = plugin.check_schema({ + client_id = "a", + client_secret = "b", + discovery = "c", + session = { + secret = "jwcE5v3pM9VhqLxmxFOH9uZaLo8u7KQK", + storage = "redis", + cookie_name = "oidc_session", + redis = { + host = "127.0.0.1", + port = 6379, + password = "secret", + prefix = "sessions", + mode = "storage", + }, + } + }) + if not ok then + ngx.say(err) + else + ngx.say("done") + end + } + } +--- response_body +done + + + +=== TEST 4: build_session_opts passes redis mode through for session storage +--- config + location /t { + content_by_lua_block { + local plugin = require("apisix.plugins.openid-connect") + local build = plugin._build_session_opts + local opts = build({ + secret = "jwcE5v3pM9VhqLxmxFOH9uZaLo8u7KQK", + storage = "redis", + redis = { + host = "127.0.0.1", + port = 6379, + mode = "storage", + }, + }) + ngx.say("storage=", opts.storage) + ngx.say("redis.host=", opts.redis.host) + ngx.say("redis.mode=", tostring(opts.redis.mode)) + } + } +--- response_body +storage=redis +redis.host=127.0.0.1 +redis.mode=storage + + + +=== TEST 5: cookie session without redis block is valid +--- config + location /t { + content_by_lua_block { + local plugin = require("apisix.plugins.openid-connect") + local ok, err = plugin.check_schema({ + client_id = "a", + client_secret = "b", + discovery = "c", + session = { + secret = "jwcE5v3pM9VhqLxmxFOH9uZaLo8u7KQK", + storage = "cookie", + } + }) + if not ok then + ngx.say(err) + else + ngx.say("done") + end + } + } +--- response_body +done + + + +=== TEST 6: valid schema with revocation_fail_mode closed +--- config + location /t { + content_by_lua_block { + local plugin = require("apisix.plugins.openid-connect") + local ok, err = plugin.check_schema({ + client_id = "a", + client_secret = "b", + discovery = "c", + session = { + secret = "jwcE5v3pM9VhqLxmxFOH9uZaLo8u7KQK", + storage = "cookie", + redis = { + host = "127.0.0.1", + mode = "revocation", + }, + revocation_fail_mode = "closed", + } + }) + if not ok then + ngx.say(err) + else + ngx.say("done") + end + } + } +--- response_body +done + + + +=== TEST 7: build_session_opts passes revocation_fail_mode closed +--- config + location /t { + content_by_lua_block { + local plugin = require("apisix.plugins.openid-connect") + local build = plugin._build_session_opts + local opts = build({ + secret = "jwcE5v3pM9VhqLxmxFOH9uZaLo8u7KQK", + storage = "cookie", + redis = { + host = "127.0.0.1", + mode = "revocation", + }, + revocation_fail_mode = "closed", + }) + ngx.say("revocation_fail_mode=", opts.revocation_fail_mode) + ngx.say("storage=", tostring(opts.storage)) + } + } +--- response_body +revocation_fail_mode=closed +storage=cookie + + + +=== TEST 8: cookie storage with redis block omitting mode is valid +--- config + location /t { + content_by_lua_block { + local plugin = require("apisix.plugins.openid-connect") + local ok, err = plugin.check_schema({ + client_id = "a", + client_secret = "b", + discovery = "c", + session = { + secret = "jwcE5v3pM9VhqLxmxFOH9uZaLo8u7KQK", + storage = "cookie", + redis = { + host = "127.0.0.1", + }, + } + }) + if not ok then + ngx.say(err) + else + ngx.say("done") + end + } + } +--- response_body +done + + + +=== TEST 9: redis storage with redis block omitting mode is valid +--- config + location /t { + content_by_lua_block { + local plugin = require("apisix.plugins.openid-connect") + local ok, err = plugin.check_schema({ + client_id = "a", + client_secret = "b", + discovery = "c", + session = { + secret = "jwcE5v3pM9VhqLxmxFOH9uZaLo8u7KQK", + storage = "redis", + redis = { + host = "127.0.0.1", + port = 6379, + }, + } + }) + if not ok then + ngx.say(err) + else + ngx.say("done") + end + } + } +--- response_body +done + + + +=== TEST 10: build_session_opts passes redis block through when mode is omitted +--- config + location /t { + content_by_lua_block { + local plugin = require("apisix.plugins.openid-connect") + local build = plugin._build_session_opts + local opts = build({ + secret = "jwcE5v3pM9VhqLxmxFOH9uZaLo8u7KQK", + storage = "cookie", + redis = { + host = "127.0.0.1", + }, + revocation_fail_mode = "closed", + }) + ngx.say("storage=", tostring(opts.storage)) + ngx.say("redis.host=", opts.redis.host) + ngx.say("redis.mode=", tostring(opts.redis.mode)) + ngx.say("revocation_fail_mode=", opts.revocation_fail_mode) + } + } +--- response_body +storage=cookie +redis.host=127.0.0.1 +redis.mode=nil +revocation_fail_mode=closed + + + +=== TEST 11: cookie storage with redis mode storage is valid +--- config + location /t { + content_by_lua_block { + local plugin = require("apisix.plugins.openid-connect") + local ok, err = plugin.check_schema({ + client_id = "a", + client_secret = "b", + discovery = "c", + session = { + secret = "jwcE5v3pM9VhqLxmxFOH9uZaLo8u7KQK", + storage = "cookie", + redis = { + host = "127.0.0.1", + mode = "storage", + }, + } + }) + if not ok then + ngx.say(err) + else + ngx.say("done") + end + } + } +--- response_body +done + + + +=== TEST 12: revocation_fail_mode without redis block on cookie storage is valid +--- config + location /t { + content_by_lua_block { + local plugin = require("apisix.plugins.openid-connect") + local ok, err = plugin.check_schema({ + client_id = "a", + client_secret = "b", + discovery = "c", + session = { + secret = "jwcE5v3pM9VhqLxmxFOH9uZaLo8u7KQK", + storage = "cookie", + revocation_fail_mode = "closed", + } + }) + if not ok then + ngx.say(err) + else + ngx.say("done") + end + } + } +--- response_body +done + + + +=== TEST 13: redis storage with redis mode revocation is valid schema +--- config + location /t { + content_by_lua_block { + local plugin = require("apisix.plugins.openid-connect") + local ok, err = plugin.check_schema({ + client_id = "a", + client_secret = "b", + discovery = "c", + session = { + secret = "jwcE5v3pM9VhqLxmxFOH9uZaLo8u7KQK", + storage = "redis", + redis = { + host = "127.0.0.1", + port = 6379, + mode = "revocation", + }, + } + }) + if not ok then + ngx.say(err) + else + ngx.say("done") + end + } + } +--- response_body +done + + + +=== TEST 14: invalid revocation_fail_mode value is rejected +--- config + location /t { + content_by_lua_block { + local plugin = require("apisix.plugins.openid-connect") + local ok, err = plugin.check_schema({ + client_id = "a", + client_secret = "b", + discovery = "c", + session = { + secret = "jwcE5v3pM9VhqLxmxFOH9uZaLo8u7KQK", + storage = "cookie", + redis = { + host = "127.0.0.1", + mode = "revocation", + }, + revocation_fail_mode = "bogus", + } + }) + if not ok then + ngx.say(err) + else + ngx.say("done") + end + } + } +--- response_body_like +.*revocation_fail_mode.* + + + +=== TEST 15: invalid redis.mode value is rejected +--- config + location /t { + content_by_lua_block { + local plugin = require("apisix.plugins.openid-connect") + local ok, err = plugin.check_schema({ + client_id = "a", + client_secret = "b", + discovery = "c", + session = { + secret = "jwcE5v3pM9VhqLxmxFOH9uZaLo8u7KQK", + storage = "cookie", + redis = { + host = "127.0.0.1", + mode = "bogus", + }, + } + }) + if not ok then + ngx.say(err) + else + ngx.say("done") + end + } + } +--- response_body_like +.*redis\.mode.* + + + +=== TEST 16: session.revocation is rejected (use session.redis instead) +--- config + location /t { + content_by_lua_block { + local plugin = require("apisix.plugins.openid-connect") + local ok, err = plugin.check_schema({ + client_id = "a", + client_secret = "b", + discovery = "c", + session = { + secret = "jwcE5v3pM9VhqLxmxFOH9uZaLo8u7KQK", + revocation = { + redis = { host = "127.0.0.1" }, + }, + } + }) + if not ok then + ngx.say(err) + else + ngx.say("done") + end + } + } +--- response_body_like +.*additional properties forbidden.*revocation.*