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]

Reply via email to