This is an automated email from the ASF dual-hosted git repository.
spacewander pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/apisix.git
The following commit(s) were added to refs/heads/master by this push:
new b5374bb feat(authz-keycloak): dynamic scope and resource mapping.
(#3308)
b5374bb is described below
commit b5374bb66c5df5ec00a5497e7ad4c472510be9f6
Author: Jens Keiner <[email protected]>
AuthorDate: Wed Jan 27 14:50:18 2021 +0100
feat(authz-keycloak): dynamic scope and resource mapping. (#3308)
---
.github/workflows/centos7-ci.yml | 2 +-
.travis/linux_openresty_common_runner.sh | 2 +-
apisix/cli/ngx_tpl.lua | 11 +-
apisix/plugins/authz-keycloak.lua | 647 ++++++++++++++++++-----
doc/plugins/authz-keycloak.md | 91 +++-
t/APISIX.pm | 2 +
t/lib/server.lua | 6 +
t/plugin/authz-keycloak.t | 450 ++++------------
t/plugin/{authz-keycloak.t => authz-keycloak2.t} | 449 ++++++++++------
9 files changed, 988 insertions(+), 672 deletions(-)
diff --git a/.github/workflows/centos7-ci.yml b/.github/workflows/centos7-ci.yml
index 14b6067..dc8e0da 100644
--- a/.github/workflows/centos7-ci.yml
+++ b/.github/workflows/centos7-ci.yml
@@ -68,7 +68,7 @@ jobs:
run: |
docker run --rm -itd -p 6379:6379 --name apisix_redis redis:3.0-alpine
docker run --rm -itd -e HTTP_PORT=8888 -e HTTPS_PORT=9999 -p 8888:8888
-p 9999:9999 mendhak/http-https-echo
- docker run --rm -itd -e KEYCLOAK_USER=admin -e
KEYCLOAK_PASSWORD=123456 -p 8090:8080 -p 8443:8443 sshniro/keycloak-apisix
+ docker run --rm -itd -e KEYCLOAK_USER=admin -e
KEYCLOAK_PASSWORD=123456 -p 8090:8080 -p 8443:8443 sshniro/keycloak-apisix:1.0.0
docker network create kafka-net --driver bridge
docker run --name zookeeper-server -d -p 2181:2181 --network kafka-net
-e ALLOW_ANONYMOUS_LOGIN=yes bitnami/zookeeper:3.6.0
docker run --name kafka-server1 -d --network kafka-net -e
ALLOW_PLAINTEXT_LISTENER=yes -e
KAFKA_CFG_ZOOKEEPER_CONNECT=zookeeper-server:2181 -e
KAFKA_CFG_ADVERTISED_LISTENERS=PLAINTEXT://127.0.0.1:9092 -p 9092:9092 -e
KAFKA_CFG_AUTO_CREATE_TOPICS_ENABLE=true bitnami/kafka:latest
diff --git a/.travis/linux_openresty_common_runner.sh
b/.travis/linux_openresty_common_runner.sh
index 04e2d36..cd73724 100755
--- a/.travis/linux_openresty_common_runner.sh
+++ b/.travis/linux_openresty_common_runner.sh
@@ -24,7 +24,7 @@ before_install() {
docker run --rm -itd -p 6379:6379 --name apisix_redis redis:3.0-alpine
docker run --rm -itd -e HTTP_PORT=8888 -e HTTPS_PORT=9999 -p 8888:8888 -p
9999:9999 mendhak/http-https-echo
# Runs Keycloak version 10.0.2 with inbuilt policies for unit tests
- docker run --rm -itd -e KEYCLOAK_USER=admin -e KEYCLOAK_PASSWORD=123456 -p
8090:8080 -p 8443:8443 sshniro/keycloak-apisix
+ docker run --rm -itd -e KEYCLOAK_USER=admin -e KEYCLOAK_PASSWORD=123456 -p
8090:8080 -p 8443:8443 sshniro/keycloak-apisix:1.0.0
# spin up kafka cluster for tests (1 zookeper and 1 kafka instance)
docker pull bitnami/zookeeper:3.6.0
docker pull bitnami/kafka:latest
diff --git a/apisix/cli/ngx_tpl.lua b/apisix/cli/ngx_tpl.lua
index 113bec5..38081ad 100644
--- a/apisix/cli/ngx_tpl.lua
+++ b/apisix/cli/ngx_tpl.lua
@@ -148,6 +148,9 @@ http {
lua_shared_dict jwks 1m; # cache for JWKs
lua_shared_dict introspection 10m; # cache for JWT verification
results
+ # for authz-keycloak
+ lua_shared_dict access_tokens 1m; # cache for service account
access tokens
+
# for custom shared dict
{% if http.lua_shared_dicts then %}
{% for cache_key, cache_size in pairs(http.lua_shared_dicts) do %}
@@ -385,16 +388,16 @@ http {
{% end %}
{% end %} {% -- if enable_ipv6 %}
+ {% if ssl.ssl_trusted_certificate ~= nil then %}
+ lua_ssl_trusted_certificate {* ssl.ssl_trusted_certificate *};
+ {% end %}
+
{% if ssl.enable then %}
ssl_certificate {* ssl.ssl_cert *};
ssl_certificate_key {* ssl.ssl_cert_key *};
ssl_session_cache shared:SSL:20m;
ssl_session_timeout 10m;
- {% if ssl.ssl_trusted_certificate ~= nil then %}
- lua_ssl_trusted_certificate {* ssl.ssl_trusted_certificate *};
- {% end %}
-
ssl_protocols {* ssl.ssl_protocols *};
ssl_ciphers {* ssl.ssl_ciphers *};
ssl_prefer_server_ciphers on;
diff --git a/apisix/plugins/authz-keycloak.lua
b/apisix/plugins/authz-keycloak.lua
index 8c2bee3..60240a5 100644
--- a/apisix/plugins/authz-keycloak.lua
+++ b/apisix/plugins/authz-keycloak.lua
@@ -22,41 +22,81 @@ local ngx = ngx
local plugin_name = "authz-keycloak"
local log = core.log
+local pairs = pairs
local schema = {
type = "object",
properties = {
discovery = {type = "string", minLength = 1, maxLength = 4096},
token_endpoint = {type = "string", minLength = 1, maxLength = 4096},
- permissions = {
- type = "array",
- items = {
- type = "string",
- minLength = 1, maxLength = 100
- },
- uniqueItems = true
- },
+ resource_registration_endpoint = {type = "string", minLength = 1,
maxLength = 4096},
+ client_id = {type = "string", minLength = 1, maxLength = 100},
+ audience = {type = "string", minLength = 1, maxLength = 100,
+ description = "Deprecated, use `client_id` instead."},
+ client_secret = {type = "string", minLength = 1, maxLength = 100},
grant_type = {
type = "string",
default="urn:ietf:params:oauth:grant-type:uma-ticket",
enum = {"urn:ietf:params:oauth:grant-type:uma-ticket"},
minLength = 1, maxLength = 100
},
- audience = {type = "string", minLength = 1, maxLength = 100},
- timeout = {type = "integer", minimum = 1000, default = 3000},
policy_enforcement_mode = {
type = "string",
enum = {"ENFORCING", "PERMISSIVE"},
default = "ENFORCING"
},
+ permissions = {
+ type = "array",
+ items = {
+ type = "string",
+ minLength = 1, maxLength = 100
+ },
+ uniqueItems = true
+ },
+ lazy_load_paths = {type = "boolean", default = false},
+ http_method_as_scope = {type = "boolean", default = false},
+ timeout = {type = "integer", minimum = 1000, default = 3000},
+ ssl_verify = {type = "boolean", default = true},
+ cache_ttl_seconds = {type = "integer", minimum = 1, default = 24 * 60
* 60},
keepalive = {type = "boolean", default = true},
keepalive_timeout = {type = "integer", minimum = 1000, default =
60000},
- keepalive_pool = {type = "integer", minimum = 1, default = 5},
- ssl_verify = {type = "boolean", default = true},
+ keepalive_pool = {type = "integer", minimum = 1, default = 5}
},
- anyOf = {
- {required = {"discovery"}},
- {required = {"token_endpoint"}}}
+ allOf = {
+ -- Require discovery or token endpoint.
+ {
+ anyOf = {
+ {required = {"discovery"}},
+ {required = {"token_endpoint"}}
+ }
+ },
+ -- Require client_id or audience.
+ {
+ anyOf = {
+ {required = {"client_id"}},
+ {required = {"audience"}}
+ }
+ },
+ -- If lazy_load_paths is true, require discovery or resource
registration endpoint.
+ {
+ anyOf = {
+ {
+ properties = {
+ lazy_load_paths = {enum = {false}},
+ }
+ },
+ {
+ properties = {
+ lazy_load_paths = {enum = {true}},
+ },
+ anyOf = {
+ {required = {"discovery"}},
+ {required = {"resource_registration_endpoint"}}
+ }
+ }
+ }
+ }
+ }
}
@@ -69,225 +109,566 @@ local _M = {
function _M.check_schema(conf)
+ -- Check for deprecated audience attribute and emit warnings if used.
+ if conf.audience then
+ log.warn("Plugin attribute `audience` is deprecated, use `client_id`
instead.")
+ if conf.client_id then
+ log.warn("Ignoring `audience` attribute in favor of `client_id`.")
+ end
+ end
return core.schema.check(schema, conf)
end
+-- Return the configured client ID parameter.
+local function authz_keycloak_get_client_id(conf)
+ if conf.client_id then
+ -- Prefer client_id, if given.
+ return conf.client_id
+ end
+
+ return conf.audience
+end
+
+
-- Some auxiliary functions below heavily inspired by the excellent
-- lua-resty-openidc module; see https://github.com/zmartzone/lua-resty-openidc
-- Retrieve value from server-wide cache, if available.
local function authz_keycloak_cache_get(type, key)
- local dict = ngx.shared[type]
- local value
- if dict then
- value = dict:get(key)
- if value then log.debug("cache hit: type=", type, " key=", key) end
- end
- return value
+ local dict = ngx.shared[type]
+ local value
+ if dict then
+ value = dict:get(key)
+ if value then log.debug("cache hit: type=", type, " key=", key) end
+ end
+ return value
end
-- Set value in server-wide cache, if available.
local function authz_keycloak_cache_set(type, key, value, exp)
- local dict = ngx.shared[type]
- if dict and (exp > 0) then
- local success, err, forcible = dict:set(key, value, exp)
- if err then
- log.error("cache set: success=", success, " err=", err, " forcible=",
forcible)
+ local dict = ngx.shared[type]
+ if dict and (exp > 0) then
+ local success, err, forcible = dict:set(key, value, exp)
+ if err then
+ log.error("cache set: success=", success, " err=", err, "
forcible=", forcible)
+ else
+ log.debug("cache set: success=", success, " err=", err, "
forcible=", forcible)
+ end
+ end
+end
+
+
+-- Configure request parameters.
+local function authz_keycloak_configure_params(params, conf)
+ -- Keepalive options.
+ if conf.keepalive then
+ params.keepalive_timeout = conf.keepalive_timeout
+ params.keepalive_pool = conf.keepalive_pool
else
- log.debug("cache set: success=", success, " err=", err, " forcible=",
forcible)
+ params.keepalive = conf.keepalive
end
- end
+
+ -- TLS verification.
+ params.ssl_verify = conf.ssl_verify
+
+ -- Decorate parameters, maybe, and return.
+ return conf.http_request_decorator and conf.http_request_decorator(params)
or params
end
-- Configure timeouts.
local function authz_keycloak_configure_timeouts(httpc, timeout)
- if timeout then
- if type(timeout) == "table" then
- httpc:set_timeouts(timeout.connect or 0, timeout.send or 0, timeout.read
or 0)
- else
- httpc:set_timeout(timeout)
+ if timeout then
+ if type(timeout) == "table" then
+ httpc:set_timeouts(timeout.connect or 0, timeout.send or 0,
timeout.read or 0)
+ else
+ httpc:set_timeout(timeout)
+ end
end
- end
end
-- Set outgoing proxy options.
local function authz_keycloak_configure_proxy(httpc, proxy_opts)
- if httpc and proxy_opts and type(proxy_opts) == "table" then
- log.debug("authz_keycloak_configure_proxy : use http proxy")
- httpc:set_proxy_options(proxy_opts)
- else
- log.debug("authz_keycloak_configure_proxy : don't use http proxy")
- end
+ if httpc and proxy_opts and type(proxy_opts) == "table" then
+ log.debug("authz_keycloak_configure_proxy : use http proxy")
+ httpc:set_proxy_options(proxy_opts)
+ else
+ log.debug("authz_keycloak_configure_proxy : don't use http proxy")
+ end
+end
+
+
+-- Get and configure HTTP client.
+local function authz_keycloak_get_http_client(conf)
+ local httpc = http.new()
+ authz_keycloak_configure_timeouts(httpc, conf.timeout)
+ authz_keycloak_configure_proxy(httpc, conf.proxy_opts)
+ return httpc
end
-- Parse the JSON result from a call to the OP.
local function authz_keycloak_parse_json_response(response)
- local err
- local res
+ local err
+ local res
- -- Check the response from the OP.
- if response.status ~= 200 then
- err = "response indicates failure, status=" .. response.status .. ",
body=" .. response.body
- else
- -- Decode the response and extract the JSON object.
- res, err = core.json.decode(response.body)
+ -- Check the response from the OP.
+ if response.status ~= 200 then
+ err = "response indicates failure, status=" .. response.status .. ",
body=" .. response.body
+ else
+ -- Decode the response and extract the JSON object.
+ res, err = core.json.decode(response.body)
- if not res then
- err = "JSON decoding failed: " .. err
+ if not res then
+ err = "JSON decoding failed: " .. err
+ end
end
- end
- return res, err
-end
-
-
-local function decorate_request(http_request_decorator, req)
- return http_request_decorator and http_request_decorator(req) or req
+ return res, err
end
-- Get the Discovery metadata from the specified URL.
-local function authz_keycloak_discover(url, ssl_verify, keepalive, timeout,
- exptime, proxy_opts,
http_request_decorator)
- log.debug("authz_keycloak_discover: URL is: " .. url)
-
- local json, err
- local v = authz_keycloak_cache_get("discovery", url)
- if not v then
-
- log.debug("Discovery data not in cache, making call to discovery
endpoint.")
- -- Make the call to the discovery endpoint.
- local httpc = http.new()
- authz_keycloak_configure_timeouts(httpc, timeout)
- authz_keycloak_configure_proxy(httpc, proxy_opts)
- local res, error = httpc:request_uri(url,
decorate_request(http_request_decorator, {
- ssl_verify = (ssl_verify ~= "no"),
- keepalive = (keepalive ~= "no")
- }))
- if not res then
- err = "accessing discovery url (" .. url .. ") failed: " .. error
- log.error(err)
+local function authz_keycloak_discover(conf)
+ log.debug("authz_keycloak_discover: URL is: " .. conf.discovery)
+
+ local json, err
+ local v = authz_keycloak_cache_get("discovery", conf.discovery)
+
+ if not v then
+ log.debug("Discovery data not in cache, making call to discovery
endpoint.")
+
+ -- Make the call to the discovery endpoint.
+ local httpc = authz_keycloak_get_http_client(conf)
+
+ local params = authz_keycloak_configure_params({}, conf)
+
+ local res, error = httpc:request_uri(conf.discovery, params)
+
+ if not res then
+ err = "Accessing discovery URL (" .. conf.discovery .. ") failed:
" .. error
+ log.error(err)
+ else
+ log.debug("Response data: " .. res.body)
+ json, err = authz_keycloak_parse_json_response(res)
+ if json then
+ authz_keycloak_cache_set("discovery", conf.discovery,
core.json.encode(json),
+ conf.cache_ttl_seconds)
+ else
+ err = "could not decode JSON from Discovery data" .. (err and
(": " .. err) or '')
+ log.error(err)
+ end
+ end
else
- log.debug("response data: " .. res.body)
- json, err = authz_keycloak_parse_json_response(res)
- if json then
- authz_keycloak_cache_set("discovery", url, core.json.encode(json),
exptime or 24 * 60 * 60)
- else
- err = "could not decode JSON from Discovery data" .. (err and (": " ..
err) or '')
- log.error(err)
- end
+ json = core.json.decode(v)
end
- else
- json = core.json.decode(v)
- end
-
- return json, err
+ return json, err
end
--- Turn a discovery url set in the opts dictionary into the discovered
information.
-local function authz_keycloak_ensure_discovered_data(opts)
- local err
- if type(opts.discovery) == "string" then
- local discovery
- discovery, err = authz_keycloak_discover(opts.discovery, opts.ssl_verify,
opts.keepalive,
- opts.timeout,
opts.jwk_expires_in, opts.proxy_opts,
- opts.http_request_decorator)
- if not err then
- opts.discovery = discovery
+-- Turn a discovery url set in the conf dictionary into the discovered
information.
+local function authz_keycloak_ensure_discovered_data(conf)
+ local err
+ if type(conf.discovery) == "string" then
+ local discovery
+ discovery, err = authz_keycloak_discover(conf)
+ if not err then
+ conf.discovery = discovery
+ end
end
- end
- return err
+ return err
end
+-- Get an endpoint from the configuration.
local function authz_keycloak_get_endpoint(conf, endpoint)
if conf and conf[endpoint] then
+ -- Use explicit entry.
return conf[endpoint]
elseif conf and conf.discovery and type(conf.discovery) == "table" then
+ -- Use discovery data.
return conf.discovery[endpoint]
end
+ -- Unable to obtain endpoint.
return nil
end
+-- Return the token endpoint from the configuration.
local function authz_keycloak_get_token_endpoint(conf)
return authz_keycloak_get_endpoint(conf, "token_endpoint")
end
-local function is_path_protected(conf)
- -- TODO if permissions are empty lazy load paths from Keycloak
- if conf.permissions == nil then
- return false
+-- Return the resource registration endpoint from the configuration.
+local function authz_keycloak_get_resource_registration_endpoint(conf)
+ return authz_keycloak_get_endpoint(conf, "resource_registration_endpoint")
+end
+
+
+-- Return access_token expires_in value (in seconds).
+local function authz_keycloak_access_token_expires_in(conf, expires_in)
+ return (expires_in or conf.access_token_expires_in or 300)
+ - 1 - (conf.access_token_expires_leeway or 0)
+end
+
+
+-- Return refresh_token expires_in value (in seconds).
+local function authz_keycloak_refresh_token_expires_in(conf, expires_in)
+ return (expires_in or conf.refresh_token_expires_in or 3600)
+ - 1 - (conf.refresh_token_expires_leeway or 0)
+end
+
+
+-- Ensure a valid service account access token is available for the configured
client.
+local function authz_keycloak_ensure_sa_access_token(conf)
+ local client_id = authz_keycloak_get_client_id(conf)
+ local ttl = conf.cache_ttl_seconds
+ local token_endpoint = authz_keycloak_get_token_endpoint(conf)
+
+ if not token_endpoint then
+ log.error("Unable to determine token endpoint.")
+ return 500, "Unable to determine token endpoint."
+ end
+
+ local session = authz_keycloak_cache_get("access_tokens", token_endpoint
.. ":"
+ .. client_id)
+
+ if session then
+ -- Decode session string.
+ local err
+ session, err = core.json.decode(session)
+
+ if not session then
+ -- Should never happen.
+ return 500, err
+ end
+
+ local current_time = ngx.time()
+
+ if current_time < session.access_token_expiration then
+ -- Access token is still valid.
+ log.debug("Access token is still valid.")
+ return session.access_token
+ else
+ -- Access token has expired.
+ log.debug("Access token has expired.")
+ if session.refresh_token
+ and (not session.refresh_token_expiration
+ or current_time < session.refresh_token_expiration) then
+ -- Try to get a new access token, using the refresh token.
+ log.debug("Trying to get new access token using refresh
token.")
+
+ local httpc = authz_keycloak_get_http_client(conf)
+
+ local params = {
+ method = "POST",
+ body = ngx.encode_args({
+ grant_type = "refresh_token",
+ client_id = client_id,
+ client_secret = conf.client_secret,
+ refresh_token = session.refresh_token,
+ }),
+ headers = {
+ ["Content-Type"] = "application/x-www-form-urlencoded"
+ }
+ }
+
+ params = authz_keycloak_configure_params(params, conf)
+
+ local res, err = httpc:request_uri(token_endpoint, params)
+
+ if not res then
+ err = "Accessing token endpoint URL (" .. token_endpoint
+ .. ") failed: " .. err
+ log.error(err)
+ return nil, err
+ end
+
+ log.debug("Response data: " .. res.body)
+ local json, err = authz_keycloak_parse_json_response(res)
+
+ if not json then
+ err = "Could not decode JSON from token endpoint"
+ .. (err and (": " .. err) or '.')
+ log.error(err)
+ return nil, err
+ end
+
+ if not json.access_token then
+ -- Clear session.
+ log.debug("Answer didn't contain a new access token.
Clearing session.")
+ session = nil
+ else
+ log.debug("Got new access token.")
+ -- Save access token.
+ session.access_token = json.access_token
+
+ -- Calculate and save access token expiry time.
+ session.access_token_expiration = current_time
+ + authz_keycloak_access_token_expires_in(conf,
json.expires_in)
+
+ -- Save refresh token, maybe.
+ if json.refresh_token ~= nil then
+ log.debug("Got new refresh token.")
+ session.refresh_token = json.refresh_token
+
+ -- Calculate and save refresh token expiry time.
+ session.refresh_token_expiration = current_time
+ + authz_keycloak_refresh_token_expires_in(conf,
+
json.refresh_expires_in)
+ end
+
+ authz_keycloak_cache_set("access_tokens",
+ token_endpoint .. ":" ..
client_id,
+ core.json.encode(session), ttl)
+ end
+ else
+ -- No refresh token available, or it has expired. Clear
session.
+ log.debug("No or expired refresh token. Clearing session.")
+ session = nil
+ end
+ end
end
- return true
+
+ if not session then
+ -- No session available. Create a new one.
+
+ core.log.debug("Getting access token for Protection API from token
endpoint.")
+ local httpc = authz_keycloak_get_http_client(conf)
+
+ local params = {
+ method = "POST",
+ body = ngx.encode_args({
+ grant_type = "client_credentials",
+ client_id = client_id,
+ client_secret = conf.client_secret,
+ }),
+ headers = {
+ ["Content-Type"] = "application/x-www-form-urlencoded"
+ }
+ }
+
+ params = authz_keycloak_configure_params(params, conf)
+
+ local current_time = ngx.time()
+
+ local res, err = httpc:request_uri(token_endpoint, params)
+
+ if not res then
+ err = "Accessing token endpoint URL (" .. token_endpoint .. ")
failed: " .. err
+ log.error(err)
+ return nil, err
+ end
+
+ log.debug("Response data: " .. res.body)
+ local json, err = authz_keycloak_parse_json_response(res)
+
+ if not json then
+ err = "Could not decode JSON from token endpoint" .. (err and (": "
.. err) or '.')
+ log.error(err)
+ return nil, err
+ end
+
+ if not json.access_token then
+ err = "Response does not contain access_token field."
+ log.error(err)
+ return nil, err
+ end
+
+ session = {}
+
+ -- Save access token.
+ session.access_token = json.access_token
+
+ -- Calculate and save access token expiry time.
+ session.access_token_expiration = current_time
+ + authz_keycloak_access_token_expires_in(conf, json.expires_in)
+
+ -- Save refresh token, maybe.
+ if json.refresh_token ~= nil then
+ session.refresh_token = json.refresh_token
+
+ -- Calculate and save refresh token expiry time.
+ session.refresh_token_expiration = current_time
+ + authz_keycloak_refresh_token_expires_in(conf,
json.refresh_expires_in)
+ end
+
+ authz_keycloak_cache_set("access_tokens", token_endpoint .. ":" ..
client_id,
+ core.json.encode(session), ttl)
+ end
+
+ return session.access_token
end
-local function evaluate_permissions(conf, token)
- if not is_path_protected(conf) and conf.policy_enforcement_mode ==
"ENFORCING" then
- return 403
+-- Resolve a URI to one or more resource IDs.
+local function authz_keycloak_resolve_resource(conf, uri, sa_access_token)
+ -- Get resource registration endpoint URL.
+ local resource_registration_endpoint =
authz_keycloak_get_resource_registration_endpoint(conf)
+
+ if not resource_registration_endpoint then
+ local err = "Unable to determine registration endpoint."
+ log.error(err)
+ return 500, err
end
+ log.debug("Resource registration endpoint: ",
resource_registration_endpoint)
+
+ local httpc = authz_keycloak_get_http_client(conf)
+
+ local params = {
+ method = "GET",
+ query = {uri = uri, matchingUri = "true"},
+ headers = {
+ ["Authorization"] = "Bearer " .. sa_access_token
+ }
+ }
+
+ params = authz_keycloak_configure_params(params, conf)
+
+ local res, err = httpc:request_uri(resource_registration_endpoint, params)
+
+ if not res then
+ err = "Accessing resource registration endpoint URL (" ..
resource_registration_endpoint
+ .. ") failed: " .. err
+ log.error(err)
+ return nil, err
+ end
+
+ log.debug("Response data: " .. res.body)
+ res.body = '{"resources": ' .. res.body .. '}'
+ local json, err = authz_keycloak_parse_json_response(res)
+
+ if not json then
+ err = "Could not decode JSON from resource registration endpoint"
+ .. (err and (": " .. err) or '.')
+ log.error(err)
+ return nil, err
+ end
+
+ return json.resources
+end
+
+
+local function evaluate_permissions(conf, ctx, token)
-- Ensure discovered data.
local err = authz_keycloak_ensure_discovered_data(conf)
if err then
- return 500, err
+ return 500, err
+ end
+
+ local permission
+
+ if conf.lazy_load_paths then
+ -- Ensure service account access token.
+ local sa_access_token, err =
authz_keycloak_ensure_sa_access_token(conf)
+ if err then
+ return 500, err
+ end
+
+ -- Resolve URI to resource(s).
+ permission, err = authz_keycloak_resolve_resource(conf,
ctx.var.request_uri,
+ sa_access_token)
+
+ -- Check result.
+ if permission == nil then
+ -- No result back from resource registration endpoint.
+ return 500, err
+ end
+ else
+ -- Use statically configured permissions.
+ permission = conf.permissions
+ end
+
+ -- Return 403 if permission is empty and enforcement mode is "ENFORCING".
+ if #permission == 0 and conf.policy_enforcement_mode == "ENFORCING" then
+ -- Return Keycloak-style message for consistency.
+ return 403,
'{"error":"access_denied","error_description":"not_authorized"}'
+ end
+
+ -- Determine scope from HTTP method, maybe.
+ local scope
+ if conf.http_method_as_scope then
+ scope = ctx.var.request_method
+ end
+
+ if scope then
+ -- Loop over permissions and add scope.
+ for k, v in pairs(permission) do
+ if v:find("#", 1, true) then
+ -- Already contains scope.
+ permission[k] = v .. ", " .. scope
+ else
+ -- Doesn't contain scope yet.
+ permission[k] = v .. "#" .. scope
+ end
+ end
+ end
+
+ for k, v in pairs(permission) do
+ log.debug("Requesting permission ", v, ".")
end
-- Get token endpoint URL.
local token_endpoint = authz_keycloak_get_token_endpoint(conf)
if not token_endpoint then
- log.error("Unable to determine token endpoint.")
- return 500, "Unable to determine token endpoint."
+ err = "Unable to determine token endpoint."
+ log.error(err)
+ return 500, err
end
log.debug("Token endpoint: ", token_endpoint)
- local httpc = http.new()
- httpc:set_timeout(conf.timeout)
+ local httpc = authz_keycloak_get_http_client(conf)
local params = {
method = "POST",
body = ngx.encode_args({
grant_type = conf.grant_type,
- audience = conf.audience,
+ audience = authz_keycloak_get_client_id(conf),
response_mode = "decision",
- permission = conf.permissions
+ permission = permission
}),
- ssl_verify = conf.ssl_verify,
headers = {
["Content-Type"] = "application/x-www-form-urlencoded",
["Authorization"] = token
}
}
- if conf.keepalive then
- params.keepalive_timeout = conf.keepalive_timeout
- params.keepalive_pool = conf.keepalive_pool
- else
- params.keepalive = conf.keepalive
- end
+ params = authz_keycloak_configure_params(params, conf)
- local httpc_res, httpc_err = httpc:request_uri(token_endpoint, params)
+ local res, err = httpc:request_uri(token_endpoint, params)
- if not httpc_res then
- log.error("error while sending authz request to ", token_endpoint, ":
", httpc_err)
- return 500, httpc_err
+ if not res then
+ err = "Error while sending authz request to " .. token_endpoint .. ":
" .. err
+ log.error(err)
+ return 500, err
end
- if httpc_res.status >= 400 then
- log.error("status code: ", httpc_res.status, " msg: ", httpc_res.body)
- return httpc_res.status, httpc_res.body
+ log.debug("Response status: ", res.status, ", data: ", res.body)
+
+ if res.status == 403 then
+ -- Request permanently denied, e.g. due to lacking permissions.
+ log.debug('Request denied: HTTP 403 Forbidden. Body: ', res.body)
+ return res.status, res.body
+ elseif res.status == 401 then
+ -- Request temporarily denied, e.g access token not valid.
+ log.debug('Request denied: HTTP 401 Unauthorized. Body: ', res.body)
+ return res.status, res.body
+ elseif res.status >= 400 then
+ -- Some other error. Log full response.
+ log.error('Request denied: Token endpoint returned an error (status: ',
+ res.status, ', body: ', res.body, ').')
+ return res.status, res.body
end
+
+ -- Request accepted.
end
@@ -313,7 +694,7 @@ function _M.access(conf, ctx)
return 401, {message = "Missing JWT token in request"}
end
- local status, body = evaluate_permissions(conf, jwt_token)
+ local status, body = evaluate_permissions(conf, ctx, jwt_token)
if status then
return status, body
end
diff --git a/doc/plugins/authz-keycloak.md b/doc/plugins/authz-keycloak.md
index d9d8a51..f2979c7 100644
--- a/doc/plugins/authz-keycloak.md
+++ b/doc/plugins/authz-keycloak.md
@@ -38,24 +38,50 @@ For more information on Keycloak, refer to [Keycloak
Authorization Docs](https:/
## Attributes
-| Name | Type | Requirement | Default
| Valid
| Description
|
-| ----------------------- | ------------- | ----------- |
--------------------------------------------- |
------------------------------------------------------------------ |
-----------------------------------------------------------------------------------------------------------------------------------------------------------
|
-| discovery | string | optional |
|
https://host.domain/auth/realms/foo/.well-known/uma2-configuration | URL to
discovery document for Keycloak Authorization Services.
|
-| token_endpoint | string | optional |
|
https://host.domain/auth/realms/foo/protocol/openid-connect/token | A
OAuth2-compliant Token Endpoint that supports the
`urn:ietf:params:oauth:grant-type:uma-ticket` grant type. Overrides value from
discovery, if given. |
-| grant_type | string | optional |
"urn:ietf:params:oauth:grant-type:uma-ticket" |
["urn:ietf:params:oauth:grant-type:uma-ticket"] |
|
-| audience | string | optional |
|
| The client identifier of the resource server to which the
client is seeking access. <br>This parameter is mandatory when parameter
permission is defined. |
-| permissions | array[string] | optional |
|
| A string representing a set of one or more resources and scopes
the client is seeking access. The format of the string must be:
`RESOURCE_ID#SCOPE_ID`. |
-| timeout | integer | optional | 3000
| [1000, ...]
| Timeout(ms) for the http connection with the Identity Server.
|
-| ssl_verify | boolean | optional | true
|
| Verify if SSL cert matches hostname.
|
-| policy_enforcement_mode | string | optional | "ENFORCING"
| ["ENFORCING", "PERMISSIVE"]
|
|
-
-### Endpoints
-
-Endpoints can optionally be discovered by providing a URL pointing to
Keycloak's discovery document for Authorization Services for the realm
-in the `discovery` attribute. The token endpoint URL will then be determined
from that document. Alternatively, the token endpoint can be
-specified explicitly via the `token_endpoint` attribute.
-
-One of `discovery` and `token_endpoint` has to be set. If both are given, the
value from `token_endpoint` is used.
+| Name | Type | Requirement | Default
| Valid
| Description
|
+| ------------------------------ | ------------- | ----------- |
--------------------------------------------- |
------------------------------------------------------------------ |
-----------------------------------------------------------------------------------------------------------------------------------------------------------
|
+| discovery | string | optional |
|
https://host.domain/auth/realms/foo/.well-known/uma2-configuration | URL to
discovery document for Keycloak Authorization Services.
|
+| token_endpoint | string | optional |
|
https://host.domain/auth/realms/foo/protocol/openid-connect/token | A
OAuth2-compliant Token Endpoint that supports the
`urn:ietf:params:oauth:grant-type:uma-ticket` grant type. Overrides value from
discovery, if given. |
+| resource_registration_endpoint | string | optional |
|
https://host.domain/auth/realms/foo/authz/protection/resource_set | A Keycloak
Protection API-compliant resource registration endpoint. Overrides value from
discovery, if given. |
+| client_id | string | optional |
|
| The client identifier of the resource server to which
the client is seeking access. One of `client_id` or `audience` is required.
|
+| audience | string | optional |
|
| Legacy parameter now replaced by `client_id`. Kept for
backwards compatibility. One of `client_id` or `audience` is required.
|
+| client_secret | string | optional |
|
| The client secret, if required.
|
+| grant_type | string | optional |
"urn:ietf:params:oauth:grant-type:uma-ticket" |
["urn:ietf:params:oauth:grant-type:uma-ticket"] |
|
+| policy_enforcement_mode | string | optional | "ENFORCING"
| ["ENFORCING", "PERMISSIVE"]
|
|
+| permissions | array[string] | optional |
|
| Static permission to request, an array of strings each
representing a resources and optionally one or more scopes the client is
seeking access. |
+| lazy_load_paths | boolean | optional | false
|
| Dynamically resolve the request URI to resource(s) using
the resource registration endpoint instead of using the static permission.
|
+| http_method_as_scope | boolean | optional | false
|
| Map HTTP request type to scope of same name and add to
all permissions requested.
|
+| timeout | integer | optional | 3000
| [1000, ...]
| Timeout(ms) for the http connection with the Identity
Server.
|
+| ssl_verify | boolean | optional | true
|
| Verify if TLS certificate matches hostname.
|
+| cache_ttl_seconds | integer | optional | 86400
(equivalent to 24h) | positive integer >= 1
| The maximum period in seconds up to which the
plugin caches discovery documents and tokens, used by the plugin to
authenticate to Keycloak. |
+| keepalive | boolean | optional | true
|
| Enable HTTP keep-alive to keep connections open after
use. Set to `true` if you expect a lot of requests to Keycloak.
|
+| keepalive_timeout | integer | optional | 60000
| positive integer >= 1000
| Idle timeout after which established HTTP connections
will be closed.
|
+| keepalive_pool | integer | optional | 5
| positive integer >= 1
| Maximum number of connections in the connection pool.
|
+
+### Discovery and Endpoints
+
+The plugin can discover Keycloak API endpoints from a URL in the `discovery`
attribute that points to
+Keycloak's discovery document for Authorization Services for the respective
realm. This is the recommended
+option and typically most convenient.
+
+If the discovery document is available, the plugin determines the token
endpoint URL from it. If present, the
+`token_endpoint` attribute overrides the URL.
+
+Analogously, the plugin determines the registration endpoint from the
discovery document. The
+`resource_registration_endpoint` overrides, if present.
+
+### Client ID and Secret
+
+The plugin needs the `client_id` attribute to identify itself when interacting
with Keycloak.
+For backwards compatibility, you can still use the `audience` attribute as
well instead. The plugin
+prefers `client_id` over `audience` if both are configured.
+
+The plugin always needs the `client_id` or `audience` to specify the context
in which Keycloak
+should evaluate permissions.
+
+If `lazy_load_paths` is `true` then the plugin additionally needs to obtain an
access token for
+itself from Keycloak. In this case, if the client access to Keycloak is
confidential, the plugin
+needs the `client_secret` attribute as well.
### Policy Enforcement Mode
@@ -69,6 +95,35 @@ Specifies how policies are enforced when processing
authorization requests sent
- Requests are allowed even when there is no policy associated with a given
resource.
+### Permissions
+
+When handling an incoming request, the plugin can determine the permissions to
check with Keycloak either
+statically, or dynamically from properties of the request.
+
+If `lazy_load_paths` is `false`, the plugin takes the permissions from the
`permissions` attribute. Each entry
+needs to be formatted as expected by the token endpoint's `permission`
parameter;
+see
https://www.keycloak.org/docs/latest/authorization_services/index.html#_service_obtaining_permissions.
+Note that a valid permission can be a single resource, or a resource paired
with one or more scopes.
+
+if `lazy_load_paths` is `true`, the plugin resolves the request URI to one or
more resources, as configured
+in Keycloak. It uses the resource registration endpoint to do so. The plugin
uses the resolved resources
+as the permissions to check.
+
+Note that this requires that the plugin can obtain a separate access token for
itself from the token endpoint.
+Therefore, in the respective client settings in Keycloak, make sure to set the
`Service Accounts Enabled`
+option. Also make sure that the issued access token contains the
`resource_access` claim with the
+`uma_protection` role. Otherwise, plugin may be unable to query resources
through the Protection API.
+
+### Automatic Mapping of HTTP Method to Scope
+
+This option is often used together with `lazy_load_paths`, but can also be
used with a static permission list.
+
+If the `http_method_as_scope` attribute is set to `true`, the plugin maps the
request's HTTP method to a scope
+of the same name. The scope is then added to every permission to check.
+
+If `lazy_load_paths` is `false`, the plugin adds the mapped scope to any of
the static permissions configured
+in the `permissions` attribute, even if they contain one or more scopes alreay.
+
## How To Enable
Create a `route` and enable the `authz-keycloak` plugin on the route:
diff --git a/t/APISIX.pm b/t/APISIX.pm
index 227012f..304327d 100644
--- a/t/APISIX.pm
+++ b/t/APISIX.pm
@@ -369,6 +369,8 @@ _EOC_
lua_shared_dict balancer_ewma_last_touched_at 1m;
lua_shared_dict plugin-limit-count-redis-cluster-slot-lock 1m;
lua_shared_dict tracing_buffer 10m; # plugin skywalking
+ lua_shared_dict access_tokens 1m; # plugin authz-keycloak
+ lua_shared_dict discovery 1m; # plugin authz-keycloak
lua_shared_dict plugin-api-breaker 10m;
lua_capture_error_log 1m; # plugin error-log-logger
diff --git a/t/lib/server.lua b/t/lib/server.lua
index e5edde9..588a569 100644
--- a/t/lib/server.lua
+++ b/t/lib/server.lua
@@ -56,6 +56,12 @@ function _M.hello_()
end
+-- Fake endpoint, needed for testing authz-keycloak plugin.
+function _M.course_foo()
+ ngx.say("course foo")
+end
+
+
function _M.server_port()
ngx.print(ngx.var.server_port)
end
diff --git a/t/plugin/authz-keycloak.t b/t/plugin/authz-keycloak.t
index 06b613c..02c9c5c 100644
--- a/t/plugin/authz-keycloak.t
+++ b/t/plugin/authz-keycloak.t
@@ -24,15 +24,15 @@ run_tests;
__DATA__
-=== TEST 1: sanity (using token endpoint)
+=== TEST 1: minimal valid configuration w/o discovery
--- config
location /t {
content_by_lua_block {
local plugin = require("apisix.plugins.authz-keycloak")
local ok, err = plugin.check_schema({
- token_endpoint =
"https://host.domain/auth/realms/foo/protocol/openid-connect/token",
- grant_type =
"urn:ietf:params:oauth:grant-type:uma-ticket"
- })
+ client_id = "foo",
+ token_endpoint =
"https://host.domain/auth/realms/foo/protocol/openid-connect/token"
+ })
if not ok then
ngx.say(err)
end
@@ -49,15 +49,15 @@ done
-=== TEST 2: sanity (using discovery endpoint)
+=== TEST 2: minimal valid configuration with discovery
--- config
location /t {
content_by_lua_block {
local plugin = require("apisix.plugins.authz-keycloak")
local ok, err = plugin.check_schema({
- discovery =
"https://host.domain/auth/realms/foo/.well-known/uma2-configuration",
- grant_type =
"urn:ietf:params:oauth:grant-type:uma-ticket"
- })
+ client_id = "foo",
+ discovery =
"https://host.domain/auth/realms/foo/.well-known/uma2-configuration"
+ })
if not ok then
ngx.say(err)
end
@@ -74,18 +74,15 @@ done
-=== TEST 3: full schema check
+=== TEST 3: minimal valid configuration with audience
--- config
location /t {
content_by_lua_block {
local plugin = require("apisix.plugins.authz-keycloak")
- local ok, err = plugin.check_schema({discovery =
"https://host.domain/auth/realms/foo/.well-known/uma2-configuration",
- token_endpoint =
"https://host.domain/auth/realms/foo/protocol/openid-connect/token",
- permissions =
{"res:customer#scopes:view"},
- timeout = 1000,
- audience = "University",
- grant_type =
"urn:ietf:params:oauth:grant-type:uma-ticket"
- })
+ local ok, err = plugin.check_schema({
+ audience = "foo",
+ discovery =
"https://host.domain/auth/realms/foo/.well-known/uma2-configuration"
+ })
if not ok then
ngx.say(err)
end
@@ -102,12 +99,17 @@ done
-=== TEST 4: token_endpoint and discovery both missing
+=== TEST 4: minimal valid configuration w/o discovery when lazy_load_paths=true
--- config
location /t {
content_by_lua_block {
local plugin = require("apisix.plugins.authz-keycloak")
- local ok, err = plugin.check_schema({permissions =
{"res:customer#scopes:view"}})
+ local ok, err = plugin.check_schema({
+ client_id = "foo",
+ lazy_load_paths = true,
+ token_endpoint =
"https://host.domain/auth/realms/foo/protocol/openid-connect/token",
+ resource_registration_endpoint =
"https://host.domain/auth/realms/foo/authz/protection/resource_set"
+ })
if not ok then
ngx.say(err)
end
@@ -118,410 +120,152 @@ done
--- request
GET /t
--- response_body
-object matches none of the requireds: ["discovery"] or ["token_endpoint"]
done
--- no_error_log
[error]
-=== TEST 5: add plugin with view course permissions (using token endpoint)
+=== TEST 5: minimal valid configuration with discovery when
lazy_load_paths=true
--- config
location /t {
content_by_lua_block {
- local t = require("lib.test_admin").test
- local code, body = t('/apisix/admin/routes/1',
- ngx.HTTP_PUT,
- [[{
- "plugins": {
- "authz-keycloak": {
- "token_endpoint":
"http://127.0.0.1:8090/auth/realms/University/protocol/openid-connect/token",
- "permissions": ["course_resource#view"],
- "audience": "course_management",
- "grant_type":
"urn:ietf:params:oauth:grant-type:uma-ticket",
- "timeout": 3000
- }
- },
- "upstream": {
- "nodes": {
- "127.0.0.1:1982": 1
- },
- "type": "roundrobin"
- },
- "uri": "/hello1"
- }]],
- [[{
- "node": {
- "value": {
- "plugins": {
- "authz-keycloak": {
- "token_endpoint":
"http://127.0.0.1:8090/auth/realms/University/protocol/openid-connect/token",
- "permissions": ["course_resource#view"],
- "audience": "course_management",
- "grant_type":
"urn:ietf:params:oauth:grant-type:uma-ticket",
- "timeout": 3000
- }
- },
- "upstream": {
- "nodes": {
- "127.0.0.1:1982": 1
- },
- "type": "roundrobin"
- },
- "uri": "/hello1"
- },
- "key": "/apisix/routes/1"
- },
- "action": "set"
- }]]
- )
-
- if code >= 300 then
- ngx.status = code
+ local plugin = require("apisix.plugins.authz-keycloak")
+ local ok, err = plugin.check_schema({
+ client_id = "foo",
+ lazy_load_paths = true,
+ discovery =
"https://host.domain/auth/realms/foo/.well-known/uma2-configuration"
+ })
+ if not ok then
+ ngx.say(err)
end
- ngx.say(body)
+
+ ngx.say("done")
}
}
--- request
GET /t
--- response_body
-passed
+done
--- no_error_log
[error]
-=== TEST 6: Get access token for teacher and access view course route
+=== TEST 6: full schema check
--- config
location /t {
content_by_lua_block {
- local json_decode = require("toolkit.json").decode
- local http = require "resty.http"
- local httpc = http.new()
- local uri =
"http://127.0.0.1:8090/auth/realms/University/protocol/openid-connect/token"
- local res, err = httpc:request_uri(uri, {
- method = "POST",
- body =
"grant_type=password&client_id=course_management&client_secret=d1ec69e9-55d2-4109-a3ea-befa071579d5&[email protected]&password=123456",
- headers = {
- ["Content-Type"] = "application/x-www-form-urlencoded"
- }
- })
-
- if res.status == 200 then
- local body = json_decode(res.body)
- local accessToken = body["access_token"]
-
-
- uri = "http://127.0.0.1:" .. ngx.var.server_port .. "/hello1"
- local res, err = httpc:request_uri(uri, {
- method = "GET",
- headers = {
- ["Authorization"] = "Bearer " .. accessToken,
- }
- })
-
- if res.status == 200 then
- ngx.say(true)
- else
- ngx.say(false)
- end
- else
- ngx.say(false)
+ local plugin = require("apisix.plugins.authz-keycloak")
+ local ok, err = plugin.check_schema({
+ discovery =
"https://host.domain/auth/realms/foo/.well-known/uma2-configuration",
+ token_endpoint =
"https://host.domain/auth/realms/foo/protocol/openid-connect/token",
+ resource_registration_endpoint =
"https://host.domain/auth/realms/foo/authz/protection/resource_set",
+ client_id = "University",
+ audience = "University",
+ client_secret = "secret",
+ grant_type =
"urn:ietf:params:oauth:grant-type:uma-ticket",
+ policy_enforcement_mode = "ENFORCING",
+ permissions = {"res:customer#scopes:view"},
+ lazy_load_paths = false,
+ http_method_as_scope = false,
+ timeout = 1000,
+ ssl_verify = false,
+ cache_ttl_seconds = 1000,
+ keepalive = true,
+ keepalive_timeout = 10000,
+ keepalive_pool = 5
+ })
+ if not ok then
+ ngx.say(err)
end
+
+ ngx.say("done")
}
}
--- request
GET /t
--- response_body
-true
+done
--- no_error_log
[error]
-=== TEST 7: invalid access token
+=== TEST 7: token_endpoint and discovery both missing
--- config
location /t {
content_by_lua_block {
- local http = require "resty.http"
- local httpc = http.new()
- local uri = "http://127.0.0.1:" .. ngx.var.server_port .. "/hello1"
- local res, err = httpc:request_uri(uri, {
- method = "GET",
- headers = {
- ["Authorization"] = "Bearer wrong_token",
- }
- })
- if res.status == 401 then
- ngx.say(true)
+ local plugin = require("apisix.plugins.authz-keycloak")
+ local ok, err = plugin.check_schema({client_id = "foo"})
+ if not ok then
+ ngx.say(err)
end
- }
- }
---- request
-GET /t
---- response_body
-true
---- error_log
-Invalid bearer token
-
-
-
-=== TEST 8: add plugin with view course permissions (using discovery)
---- config
- location /t {
- content_by_lua_block {
- local t = require("lib.test_admin").test
- local code, body = t('/apisix/admin/routes/1',
- ngx.HTTP_PUT,
- [[{
- "plugins": {
- "authz-keycloak": {
- "discovery":
"http://127.0.0.1:8090/auth/realms/University/.well-known/uma2-configuration",
- "permissions": ["course_resource#view"],
- "audience": "course_management",
- "grant_type":
"urn:ietf:params:oauth:grant-type:uma-ticket",
- "timeout": 3000
- }
- },
- "upstream": {
- "nodes": {
- "127.0.0.1:1982": 1
- },
- "type": "roundrobin"
- },
- "uri": "/hello1"
- }]],
- [[{
- "node": {
- "value": {
- "plugins": {
- "authz-keycloak": {
- "discovery":
"http://127.0.0.1:8090/auth/realms/University/.well-known/uma2-configuration",
- "permissions": ["course_resource#view"],
- "audience": "course_management",
- "grant_type":
"urn:ietf:params:oauth:grant-type:uma-ticket",
- "timeout": 3000
- }
- },
- "upstream": {
- "nodes": {
- "127.0.0.1:1982": 1
- },
- "type": "roundrobin"
- },
- "uri": "/hello1"
- },
- "key": "/apisix/routes/1"
- },
- "action": "set"
- }]]
- )
- if code >= 300 then
- ngx.status = code
- end
- ngx.say(body)
+ ngx.say("done")
}
}
--- request
GET /t
--- response_body
-passed
+allOf 1 failed: object matches none of the requireds: ["discovery"] or
["token_endpoint"]
+done
--- no_error_log
[error]
-=== TEST 9: Get access token for teacher and access view course route
+=== TEST 8: client_id and audience both missing
--- config
location /t {
content_by_lua_block {
- local json_decode = require("toolkit.json").decode
- local http = require "resty.http"
- local httpc = http.new()
- local uri =
"http://127.0.0.1:8090/auth/realms/University/protocol/openid-connect/token"
- local res, err = httpc:request_uri(uri, {
- method = "POST",
- body =
"grant_type=password&client_id=course_management&client_secret=d1ec69e9-55d2-4109-a3ea-befa071579d5&[email protected]&password=123456",
- headers = {
- ["Content-Type"] = "application/x-www-form-urlencoded"
- }
- })
-
- if res.status == 200 then
- local body = json_decode(res.body)
- local accessToken = body["access_token"]
-
-
- uri = "http://127.0.0.1:" .. ngx.var.server_port .. "/hello1"
- local res, err = httpc:request_uri(uri, {
- method = "GET",
- headers = {
- ["Authorization"] = "Bearer " .. accessToken,
- }
- })
-
- if res.status == 200 then
- ngx.say(true)
- else
- ngx.say(false)
- end
- else
- ngx.say(false)
+ local plugin = require("apisix.plugins.authz-keycloak")
+ local ok, err = plugin.check_schema({discovery =
"https://host.domain/auth/realms/foo/.well-known/uma2-configuration"})
+ if not ok then
+ ngx.say(err)
end
+
+ ngx.say("done")
}
}
--- request
GET /t
--- response_body
-true
+allOf 2 failed: object matches none of the requireds: ["client_id"] or
["audience"]
+done
--- no_error_log
[error]
-=== TEST 10: invalid access token
+=== TEST 9: resource_registration_endpoint and discovery both missing and
lazy_load_paths is true
--- config
location /t {
content_by_lua_block {
- local http = require "resty.http"
- local httpc = http.new()
- local uri = "http://127.0.0.1:" .. ngx.var.server_port .. "/hello1"
- local res, err = httpc:request_uri(uri, {
- method = "GET",
- headers = {
- ["Authorization"] = "Bearer wrong_token",
- }
- })
- if res.status == 401 then
- ngx.say(true)
+ local plugin = require("apisix.plugins.authz-keycloak")
+ local ok, err = plugin.check_schema({
+ client_id = "foo",
+ token_endpoint =
"https://host.domain/auth/realms/foo/protocol/openid-connect/token",
+ lazy_load_paths = true
+ })
+ if not ok then
+ ngx.say(err)
end
- }
- }
---- request
-GET /t
---- response_body
-true
---- error_log
-Invalid bearer token
-
-
-=== TEST 11: add plugin for delete course route
---- config
- location /t {
- content_by_lua_block {
- local t = require("lib.test_admin").test
- local code, body = t('/apisix/admin/routes/1',
- ngx.HTTP_PUT,
- [[{
- "plugins": {
- "authz-keycloak": {
- "token_endpoint":
"http://127.0.0.1:8090/auth/realms/University/protocol/openid-connect/token",
- "permissions": ["course_resource#delete"],
- "audience": "course_management",
- "grant_type":
"urn:ietf:params:oauth:grant-type:uma-ticket",
- "timeout": 3000
- }
- },
- "upstream": {
- "nodes": {
- "127.0.0.1:1982": 1
- },
- "type": "roundrobin"
- },
- "uri": "/hello1"
- }]],
- [[{
- "node": {
- "value": {
- "plugins": {
- "authz-keycloak": {
- "token_endpoint":
"http://127.0.0.1:8090/auth/realms/University/protocol/openid-connect/token",
- "permissions": ["course_resource#delete"],
- "audience": "course_management",
- "grant_type":
"urn:ietf:params:oauth:grant-type:uma-ticket",
- "timeout": 3000
- }
- },
- "upstream": {
- "nodes": {
- "127.0.0.1:1982": 1
- },
- "type": "roundrobin"
- },
- "uri": "/hello1"
- },
- "key": "/apisix/routes/1"
- },
- "action": "set"
- }]]
- )
-
- if code >= 300 then
- ngx.status = code
- end
- ngx.say(body)
+ ngx.say("done")
}
}
--- request
GET /t
--- response_body
-passed
+allOf 3 failed: object matches none of the requireds
+done
--- no_error_log
[error]
-=== TEST 12: Get access token for student and delete course
---- config
- location /t {
- content_by_lua_block {
- local json_decode = require("toolkit.json").decode
- local http = require "resty.http"
- local httpc = http.new()
- local uri =
"http://127.0.0.1:8090/auth/realms/University/protocol/openid-connect/token"
- local res, err = httpc:request_uri(uri, {
- method = "POST",
- body =
"grant_type=password&client_id=course_management&client_secret=d1ec69e9-55d2-4109-a3ea-befa071579d5&[email protected]&password=123456",
- headers = {
- ["Content-Type"] = "application/x-www-form-urlencoded"
- }
- })
-
- if res.status == 200 then
- local body = json_decode(res.body)
- local accessToken = body["access_token"]
-
-
- uri = "http://127.0.0.1:" .. ngx.var.server_port .. "/hello1"
- local res, err = httpc:request_uri(uri, {
- method = "GET",
- headers = {
- ["Authorization"] = "Bearer " .. accessToken,
- }
- })
-
- if res.status == 403 then
- ngx.say(true)
- else
- ngx.say(false)
- end
- else
- ngx.say(false)
- end
- }
- }
---- request
-GET /t
---- response_body
-true
---- error_log
-{"error":"access_denied","error_description":"not_authorized"}
-
-
-
-=== TEST 13: Add https endpoint with ssl_verify true (default)
+=== TEST 10: Add https endpoint with ssl_verify true (default)
--- config
location /t {
content_by_lua_block {
@@ -533,7 +277,7 @@ true
"authz-keycloak": {
"token_endpoint":
"https://127.0.0.1:8443/auth/realms/University/protocol/openid-connect/token",
"permissions": ["course_resource#delete"],
- "audience": "course_management",
+ "client_id": "course_management",
"grant_type":
"urn:ietf:params:oauth:grant-type:uma-ticket",
"timeout": 3000
}
@@ -553,7 +297,7 @@ true
"authz-keycloak": {
"token_endpoint":
"https://127.0.0.1:8443/auth/realms/University/protocol/openid-connect/token",
"permissions": ["course_resource#delete"],
- "audience": "course_management",
+ "client_id": "course_management",
"grant_type":
"urn:ietf:params:oauth:grant-type:uma-ticket",
"timeout": 3000
}
@@ -587,7 +331,7 @@ passed
-=== TEST 14: TEST with fake token and https endpoint
+=== TEST 11: TEST with fake token and https endpoint
--- config
location /t {
content_by_lua_block {
@@ -613,11 +357,11 @@ GET /t
--- response_body
false
--- error_log
-error while sending authz request to
https://127.0.0.1:8443/auth/realms/University/protocol/openid-connect/token:
18: self signed certificate
+Error while sending authz request to
https://127.0.0.1:8443/auth/realms/University/protocol/openid-connect/token:
18: self signed certificate
-=== TEST 15: Add htttps endpoint with ssl_verify false
+=== TEST 12: Add https endpoint with ssl_verify false
--- config
location /t {
content_by_lua_block {
@@ -629,7 +373,7 @@ error while sending authz request to
https://127.0.0.1:8443/auth/realms/Universi
"authz-keycloak": {
"token_endpoint":
"https://127.0.0.1:8443/auth/realms/University/protocol/openid-connect/token",
"permissions": ["course_resource#delete"],
- "audience": "course_management",
+ "client_id": "course_management",
"grant_type":
"urn:ietf:params:oauth:grant-type:uma-ticket",
"timeout": 3000,
"ssl_verify": false
@@ -650,7 +394,7 @@ error while sending authz request to
https://127.0.0.1:8443/auth/realms/Universi
"authz-keycloak": {
"token_endpoint":
"https://127.0.0.1:8443/auth/realms/University/protocol/openid-connect/token",
"permissions": ["course_resource#delete"],
- "audience": "course_management",
+ "client_id": "course_management",
"grant_type":
"urn:ietf:params:oauth:grant-type:uma-ticket",
"timeout": 3000,
"ssl_verify": false
@@ -685,7 +429,7 @@ passed
-=== TEST 16: TEST for https based token verification with ssl_verify false
+=== TEST 13: TEST for https based token verification with ssl_verify false
--- config
location /t {
content_by_lua_block {
@@ -711,4 +455,4 @@ GET /t
--- response_body
false
--- error_log
-status code: 401 msg: {"error":"HTTP 401 Unauthorized"}
+Request denied: HTTP 401 Unauthorized. Body: {"error":"HTTP 401 Unauthorized"}
diff --git a/t/plugin/authz-keycloak.t b/t/plugin/authz-keycloak2.t
similarity index 62%
copy from t/plugin/authz-keycloak.t
copy to t/plugin/authz-keycloak2.t
index 06b613c..953828d 100644
--- a/t/plugin/authz-keycloak.t
+++ b/t/plugin/authz-keycloak2.t
@@ -24,108 +24,7 @@ run_tests;
__DATA__
-=== TEST 1: sanity (using token endpoint)
---- config
- location /t {
- content_by_lua_block {
- local plugin = require("apisix.plugins.authz-keycloak")
- local ok, err = plugin.check_schema({
- token_endpoint =
"https://host.domain/auth/realms/foo/protocol/openid-connect/token",
- grant_type =
"urn:ietf:params:oauth:grant-type:uma-ticket"
- })
- if not ok then
- ngx.say(err)
- end
-
- ngx.say("done")
- }
- }
---- request
-GET /t
---- response_body
-done
---- no_error_log
-[error]
-
-
-
-=== TEST 2: sanity (using discovery endpoint)
---- config
- location /t {
- content_by_lua_block {
- local plugin = require("apisix.plugins.authz-keycloak")
- local ok, err = plugin.check_schema({
- discovery =
"https://host.domain/auth/realms/foo/.well-known/uma2-configuration",
- grant_type =
"urn:ietf:params:oauth:grant-type:uma-ticket"
- })
- if not ok then
- ngx.say(err)
- end
-
- ngx.say("done")
- }
- }
---- request
-GET /t
---- response_body
-done
---- no_error_log
-[error]
-
-
-
-=== TEST 3: full schema check
---- config
- location /t {
- content_by_lua_block {
- local plugin = require("apisix.plugins.authz-keycloak")
- local ok, err = plugin.check_schema({discovery =
"https://host.domain/auth/realms/foo/.well-known/uma2-configuration",
- token_endpoint =
"https://host.domain/auth/realms/foo/protocol/openid-connect/token",
- permissions =
{"res:customer#scopes:view"},
- timeout = 1000,
- audience = "University",
- grant_type =
"urn:ietf:params:oauth:grant-type:uma-ticket"
- })
- if not ok then
- ngx.say(err)
- end
-
- ngx.say("done")
- }
- }
---- request
-GET /t
---- response_body
-done
---- no_error_log
-[error]
-
-
-
-=== TEST 4: token_endpoint and discovery both missing
---- config
- location /t {
- content_by_lua_block {
- local plugin = require("apisix.plugins.authz-keycloak")
- local ok, err = plugin.check_schema({permissions =
{"res:customer#scopes:view"}})
- if not ok then
- ngx.say(err)
- end
-
- ngx.say("done")
- }
- }
---- request
-GET /t
---- response_body
-object matches none of the requireds: ["discovery"] or ["token_endpoint"]
-done
---- no_error_log
-[error]
-
-
-
-=== TEST 5: add plugin with view course permissions (using token endpoint)
+=== TEST 1: add plugin with view course permissions (using token endpoint)
--- config
location /t {
content_by_lua_block {
@@ -137,7 +36,7 @@ done
"authz-keycloak": {
"token_endpoint":
"http://127.0.0.1:8090/auth/realms/University/protocol/openid-connect/token",
"permissions": ["course_resource#view"],
- "audience": "course_management",
+ "client_id": "course_management",
"grant_type":
"urn:ietf:params:oauth:grant-type:uma-ticket",
"timeout": 3000
}
@@ -157,7 +56,7 @@ done
"authz-keycloak": {
"token_endpoint":
"http://127.0.0.1:8090/auth/realms/University/protocol/openid-connect/token",
"permissions": ["course_resource#view"],
- "audience": "course_management",
+ "client_id": "course_management",
"grant_type":
"urn:ietf:params:oauth:grant-type:uma-ticket",
"timeout": 3000
}
@@ -191,7 +90,7 @@ passed
-=== TEST 6: Get access token for teacher and access view course route
+=== TEST 2: Get access token for teacher and access view course route
--- config
location /t {
content_by_lua_block {
@@ -239,7 +138,7 @@ true
-=== TEST 7: invalid access token
+=== TEST 3: invalid access token
--- config
location /t {
content_by_lua_block {
@@ -266,7 +165,7 @@ Invalid bearer token
-=== TEST 8: add plugin with view course permissions (using discovery)
+=== TEST 4: add plugin with view course permissions (using discovery)
--- config
location /t {
content_by_lua_block {
@@ -278,7 +177,7 @@ Invalid bearer token
"authz-keycloak": {
"discovery":
"http://127.0.0.1:8090/auth/realms/University/.well-known/uma2-configuration",
"permissions": ["course_resource#view"],
- "audience": "course_management",
+ "client_id": "course_management",
"grant_type":
"urn:ietf:params:oauth:grant-type:uma-ticket",
"timeout": 3000
}
@@ -298,7 +197,7 @@ Invalid bearer token
"authz-keycloak": {
"discovery":
"http://127.0.0.1:8090/auth/realms/University/.well-known/uma2-configuration",
"permissions": ["course_resource#view"],
- "audience": "course_management",
+ "client_id": "course_management",
"grant_type":
"urn:ietf:params:oauth:grant-type:uma-ticket",
"timeout": 3000
}
@@ -332,7 +231,7 @@ passed
-=== TEST 9: Get access token for teacher and access view course route
+=== TEST 5: Get access token for teacher and access view course route
--- config
location /t {
content_by_lua_block {
@@ -380,7 +279,7 @@ true
-=== TEST 10: invalid access token
+=== TEST 6: invalid access token
--- config
location /t {
content_by_lua_block {
@@ -407,7 +306,7 @@ Invalid bearer token
-=== TEST 11: add plugin for delete course route
+=== TEST 7: add plugin for delete course route
--- config
location /t {
content_by_lua_block {
@@ -419,7 +318,7 @@ Invalid bearer token
"authz-keycloak": {
"token_endpoint":
"http://127.0.0.1:8090/auth/realms/University/protocol/openid-connect/token",
"permissions": ["course_resource#delete"],
- "audience": "course_management",
+ "client_id": "course_management",
"grant_type":
"urn:ietf:params:oauth:grant-type:uma-ticket",
"timeout": 3000
}
@@ -439,7 +338,7 @@ Invalid bearer token
"authz-keycloak": {
"token_endpoint":
"http://127.0.0.1:8090/auth/realms/University/protocol/openid-connect/token",
"permissions": ["course_resource#delete"],
- "audience": "course_management",
+ "client_id": "course_management",
"grant_type":
"urn:ietf:params:oauth:grant-type:uma-ticket",
"timeout": 3000
}
@@ -473,7 +372,7 @@ passed
-=== TEST 12: Get access token for student and delete course
+=== TEST 8: Get access token for student and delete course
--- config
location /t {
content_by_lua_block {
@@ -521,7 +420,7 @@ true
-=== TEST 13: Add https endpoint with ssl_verify true (default)
+=== TEST 9: add plugin with lazy_load_paths and http_method_as_scope
--- config
location /t {
content_by_lua_block {
@@ -531,11 +430,11 @@ true
[[{
"plugins": {
"authz-keycloak": {
- "token_endpoint":
"https://127.0.0.1:8443/auth/realms/University/protocol/openid-connect/token",
- "permissions": ["course_resource#delete"],
- "audience": "course_management",
- "grant_type":
"urn:ietf:params:oauth:grant-type:uma-ticket",
- "timeout": 3000
+ "discovery":
"http://127.0.0.1:8090/auth/realms/University/.well-known/uma2-configuration",
+ "client_id": "course_management",
+ "client_secret":
"d1ec69e9-55d2-4109-a3ea-befa071579d5",
+ "lazy_load_paths": true,
+ "http_method_as_scope": true
}
},
"upstream": {
@@ -544,18 +443,18 @@ true
},
"type": "roundrobin"
},
- "uri": "/hello1"
+ "uri": "/course/foo"
}]],
[[{
"node": {
"value": {
"plugins": {
"authz-keycloak": {
- "token_endpoint":
"https://127.0.0.1:8443/auth/realms/University/protocol/openid-connect/token",
- "permissions": ["course_resource#delete"],
- "audience": "course_management",
- "grant_type":
"urn:ietf:params:oauth:grant-type:uma-ticket",
- "timeout": 3000
+ "discovery":
"http://127.0.0.1:8090/auth/realms/University/.well-known/uma2-configuration",
+ "client_id": "course_management",
+ "client_secret":
"d1ec69e9-55d2-4109-a3ea-befa071579d5",
+ "lazy_load_paths": true,
+ "http_method_as_scope": true
}
},
"upstream": {
@@ -564,7 +463,7 @@ true
},
"type": "roundrobin"
},
- "uri": "/hello1"
+ "uri": "/course/foo"
},
"key": "/apisix/routes/1"
},
@@ -587,22 +486,184 @@ passed
-=== TEST 14: TEST with fake token and https endpoint
+=== TEST 10: Get access token for teacher and access view course route.
--- config
location /t {
content_by_lua_block {
+ local json_decode = require("toolkit.json").decode
local http = require "resty.http"
local httpc = http.new()
- local uri = "http://127.0.0.1:" .. ngx.var.server_port .. "/hello1"
+ local uri =
"http://127.0.0.1:8090/auth/realms/University/protocol/openid-connect/token"
local res, err = httpc:request_uri(uri, {
- method = "GET",
- headers = {
- ["Authorization"] = "Bearer " .. "fake access token",
- }
- })
+ method = "POST",
+ body =
"grant_type=password&client_id=course_management&client_secret=d1ec69e9-55d2-4109-a3ea-befa071579d5&[email protected]&password=123456",
+ headers = {
+ ["Content-Type"] = "application/x-www-form-urlencoded"
+ }
+ })
if res.status == 200 then
- ngx.say(true)
+ local body = json_decode(res.body)
+ local accessToken = body["access_token"]
+
+
+ uri = "http://127.0.0.1:" .. ngx.var.server_port ..
"/course/foo"
+ local res, err = httpc:request_uri(uri, {
+ method = "GET",
+ headers = {
+ ["Authorization"] = "Bearer " .. accessToken,
+ }
+ })
+
+ if res.status == 200 then
+ ngx.say(true)
+ else
+ ngx.say(false)
+ end
+ else
+ ngx.say(false)
+ end
+ }
+ }
+--- request
+GET /t
+--- response_body
+true
+--- no_error_log
+[error]
+
+
+
+=== TEST 11: Get access token for student and access view course route.
+--- config
+ location /t {
+ content_by_lua_block {
+ local json_decode = require("toolkit.json").decode
+ local http = require "resty.http"
+ local httpc = http.new()
+ local uri =
"http://127.0.0.1:8090/auth/realms/University/protocol/openid-connect/token"
+ local res, err = httpc:request_uri(uri, {
+ method = "POST",
+ body =
"grant_type=password&client_id=course_management&client_secret=d1ec69e9-55d2-4109-a3ea-befa071579d5&[email protected]&password=123456",
+ headers = {
+ ["Content-Type"] = "application/x-www-form-urlencoded"
+ }
+ })
+
+ if res.status == 200 then
+ local body = json_decode(res.body)
+ local accessToken = body["access_token"]
+
+
+ uri = "http://127.0.0.1:" .. ngx.var.server_port ..
"/course/foo"
+ local res, err = httpc:request_uri(uri, {
+ method = "GET",
+ headers = {
+ ["Authorization"] = "Bearer " .. accessToken,
+ }
+ })
+
+ if res.status == 200 then
+ ngx.say(true)
+ else
+ ngx.say(false)
+ end
+ else
+ ngx.say(false)
+ end
+ }
+ }
+--- request
+GET /t
+--- response_body
+true
+--- no_error_log
+[error]
+
+
+
+=== TEST 12: Get access token for teacher and delete course.
+--- config
+ location /t {
+ content_by_lua_block {
+ local json_decode = require("toolkit.json").decode
+ local http = require "resty.http"
+ local httpc = http.new()
+ local uri =
"http://127.0.0.1:8090/auth/realms/University/protocol/openid-connect/token"
+ local res, err = httpc:request_uri(uri, {
+ method = "POST",
+ body =
"grant_type=password&client_id=course_management&client_secret=d1ec69e9-55d2-4109-a3ea-befa071579d5&[email protected]&password=123456",
+ headers = {
+ ["Content-Type"] = "application/x-www-form-urlencoded"
+ }
+ })
+
+ if res.status == 200 then
+ local body = json_decode(res.body)
+ local accessToken = body["access_token"]
+
+
+ uri = "http://127.0.0.1:" .. ngx.var.server_port ..
"/course/foo"
+ local res, err = httpc:request_uri(uri, {
+ method = "DELETE",
+ headers = {
+ ["Authorization"] = "Bearer " .. accessToken,
+ }
+ })
+
+ if res.status == 200 then
+ ngx.say(true)
+ else
+ ngx.say(false)
+ end
+ else
+ ngx.say(false)
+ end
+ }
+ }
+--- request
+GET /t
+--- response_body
+true
+--- no_error_log
+[error]
+
+
+
+=== TEST 13: Get access token for student and try to delete course. Should
fail.
+--- config
+ location /t {
+ content_by_lua_block {
+ local json_decode = require("toolkit.json").decode
+ local http = require "resty.http"
+ local httpc = http.new()
+ local uri =
"http://127.0.0.1:8090/auth/realms/University/protocol/openid-connect/token"
+ local res, err = httpc:request_uri(uri, {
+ method = "POST",
+ body =
"grant_type=password&client_id=course_management&client_secret=d1ec69e9-55d2-4109-a3ea-befa071579d5&[email protected]&password=123456",
+ headers = {
+ ["Content-Type"] = "application/x-www-form-urlencoded"
+ }
+ })
+
+ if res.status == 200 then
+ local body = json_decode(res.body)
+ local accessToken = body["access_token"]
+
+
+ uri = "http://127.0.0.1:" .. ngx.var.server_port ..
"/course/foo"
+ local res, err = httpc:request_uri(uri, {
+ method = "DELETE",
+ headers = {
+ ["Authorization"] = "Bearer " .. accessToken,
+ }
+ })
+
+ if res.status == 403 then
+ ngx.say(true)
+ else
+ ngx.say(false)
+ end
else
ngx.say(false)
end
@@ -611,13 +672,13 @@ passed
--- request
GET /t
--- response_body
-false
+true
--- error_log
-error while sending authz request to
https://127.0.0.1:8443/auth/realms/University/protocol/openid-connect/token:
18: self signed certificate
+{"error":"access_denied","error_description":"not_authorized"}
-=== TEST 15: Add htttps endpoint with ssl_verify false
+=== TEST 14: add plugin with lazy_load_paths and http_method_as_scope (using
audience)
--- config
location /t {
content_by_lua_block {
@@ -627,12 +688,11 @@ error while sending authz request to
https://127.0.0.1:8443/auth/realms/Universi
[[{
"plugins": {
"authz-keycloak": {
- "token_endpoint":
"https://127.0.0.1:8443/auth/realms/University/protocol/openid-connect/token",
- "permissions": ["course_resource#delete"],
+ "discovery":
"http://127.0.0.1:8090/auth/realms/University/.well-known/uma2-configuration",
"audience": "course_management",
- "grant_type":
"urn:ietf:params:oauth:grant-type:uma-ticket",
- "timeout": 3000,
- "ssl_verify": false
+ "client_secret":
"d1ec69e9-55d2-4109-a3ea-befa071579d5",
+ "lazy_load_paths": true,
+ "http_method_as_scope": true
}
},
"upstream": {
@@ -641,19 +701,18 @@ error while sending authz request to
https://127.0.0.1:8443/auth/realms/Universi
},
"type": "roundrobin"
},
- "uri": "/hello1"
+ "uri": "/course/foo"
}]],
[[{
"node": {
"value": {
"plugins": {
"authz-keycloak": {
- "token_endpoint":
"https://127.0.0.1:8443/auth/realms/University/protocol/openid-connect/token",
- "permissions": ["course_resource#delete"],
+ "discovery":
"http://127.0.0.1:8090/auth/realms/University/.well-known/uma2-configuration",
"audience": "course_management",
- "grant_type":
"urn:ietf:params:oauth:grant-type:uma-ticket",
- "timeout": 3000,
- "ssl_verify": false
+ "client_secret":
"d1ec69e9-55d2-4109-a3ea-befa071579d5",
+ "lazy_load_paths": true,
+ "http_method_as_scope": true
}
},
"upstream": {
@@ -662,7 +721,7 @@ error while sending authz request to
https://127.0.0.1:8443/auth/realms/Universi
},
"type": "roundrobin"
},
- "uri": "/hello1"
+ "uri": "/course/foo"
},
"key": "/apisix/routes/1"
},
@@ -685,22 +744,40 @@ passed
-=== TEST 16: TEST for https based token verification with ssl_verify false
+=== TEST 15: Get access token for teacher and access view course route.
--- config
location /t {
content_by_lua_block {
+ local json_decode = require("toolkit.json").decode
local http = require "resty.http"
local httpc = http.new()
- local uri = "http://127.0.0.1:" .. ngx.var.server_port .. "/hello1"
+ local uri =
"http://127.0.0.1:8090/auth/realms/University/protocol/openid-connect/token"
local res, err = httpc:request_uri(uri, {
- method = "GET",
- headers = {
- ["Authorization"] = "Bearer " .. "fake access token",
- }
- })
+ method = "POST",
+ body =
"grant_type=password&client_id=course_management&client_secret=d1ec69e9-55d2-4109-a3ea-befa071579d5&[email protected]&password=123456",
+ headers = {
+ ["Content-Type"] = "application/x-www-form-urlencoded"
+ }
+ })
if res.status == 200 then
- ngx.say(true)
+ local body = json_decode(res.body)
+ local accessToken = body["access_token"]
+
+
+ uri = "http://127.0.0.1:" .. ngx.var.server_port ..
"/course/foo"
+ local res, err = httpc:request_uri(uri, {
+ method = "GET",
+ headers = {
+ ["Authorization"] = "Bearer " .. accessToken,
+ }
+ })
+
+ if res.status == 200 then
+ ngx.say(true)
+ else
+ ngx.say(false)
+ end
else
ngx.say(false)
end
@@ -709,6 +786,54 @@ passed
--- request
GET /t
--- response_body
-false
---- error_log
-status code: 401 msg: {"error":"HTTP 401 Unauthorized"}
+true
+--- no_error_log
+[error]
+
+
+
+=== TEST 16: Get access token for student and access view course route.
+--- config
+ location /t {
+ content_by_lua_block {
+ local json_decode = require("toolkit.json").decode
+ local http = require "resty.http"
+ local httpc = http.new()
+ local uri =
"http://127.0.0.1:8090/auth/realms/University/protocol/openid-connect/token"
+ local res, err = httpc:request_uri(uri, {
+ method = "POST",
+ body =
"grant_type=password&client_id=course_management&client_secret=d1ec69e9-55d2-4109-a3ea-befa071579d5&[email protected]&password=123456",
+ headers = {
+ ["Content-Type"] = "application/x-www-form-urlencoded"
+ }
+ })
+
+ if res.status == 200 then
+ local body = json_decode(res.body)
+ local accessToken = body["access_token"]
+
+
+ uri = "http://127.0.0.1:" .. ngx.var.server_port ..
"/course/foo"
+ local res, err = httpc:request_uri(uri, {
+ method = "GET",
+ headers = {
+ ["Authorization"] = "Bearer " .. accessToken,
+ }
+ })
+
+ if res.status == 200 then
+ ngx.say(true)
+ else
+ ngx.say(false)
+ end
+ else
+ ngx.say(false)
+ end
+ }
+ }
+--- request
+GET /t
+--- response_body
+true
+--- no_error_log
+[error]