You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@apisix.apache.org by me...@apache.org on 2020/06/16 05:51:46 UTC

[incubator-apisix] branch master updated: feature: support authorization Plugin for Keycloak Identity Server (#1701)

This is an automated email from the ASF dual-hosted git repository.

membphis pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/incubator-apisix.git


The following commit(s) were added to refs/heads/master by this push:
     new cd98a2b  feature: support authorization Plugin for Keycloak Identity Server (#1701)
cd98a2b is described below

commit cd98a2bec8bee2a2cea5b0bbbb892f25e7394cf0
Author: Nirojan Selvanathan <ss...@gmail.com>
AuthorDate: Tue Jun 16 07:51:36 2020 +0200

    feature: support authorization Plugin for Keycloak Identity Server (#1701)
---
 .travis/linux_openresty_runner.sh    |   2 +
 .travis/linux_tengine_runner.sh      |   2 +
 apisix/plugins/authz-keycloak.lua    | 165 +++++++++++++++++
 conf/config.yaml                     |   1 +
 doc/images/plugin/authz-keycloak.png | Bin 0 -> 51957 bytes
 doc/plugins/authz-keycloak.md        | 135 ++++++++++++++
 t/admin/plugins.t                    |   2 +-
 t/debug/debug-mode.t                 |   1 +
 t/plugin/authz-keycloak.t            | 349 +++++++++++++++++++++++++++++++++++
 9 files changed, 656 insertions(+), 1 deletion(-)

diff --git a/.travis/linux_openresty_runner.sh b/.travis/linux_openresty_runner.sh
index ea5dce9..d569983 100755
--- a/.travis/linux_openresty_runner.sh
+++ b/.travis/linux_openresty_runner.sh
@@ -38,6 +38,8 @@ before_install() {
     docker pull redis:3.0-alpine
     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 sshniro/keycloak-apisix
     # 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/.travis/linux_tengine_runner.sh b/.travis/linux_tengine_runner.sh
index 74aa909..472e86f 100755
--- a/.travis/linux_tengine_runner.sh
+++ b/.travis/linux_tengine_runner.sh
@@ -39,6 +39,8 @@ before_install() {
     docker pull redis:3.0-alpine
     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 sshniro/keycloak-apisix
     # 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/plugins/authz-keycloak.lua b/apisix/plugins/authz-keycloak.lua
new file mode 100644
index 0000000..2704f4e
--- /dev/null
+++ b/apisix/plugins/authz-keycloak.lua
@@ -0,0 +1,165 @@
+--
+-- Licensed to the Apache Software Foundation (ASF) under one or more
+-- contributor license agreements.  See the NOTICE file distributed with
+-- this work for additional information regarding copyright ownership.
+-- The ASF licenses this file to You under the Apache License, Version 2.0
+-- (the "License"); you may not use this file except in compliance with
+-- the License.  You may obtain a copy of the License at
+--
+--     http://www.apache.org/licenses/LICENSE-2.0
+--
+-- Unless required by applicable law or agreed to in writing, software
+-- distributed under the License is distributed on an "AS IS" BASIS,
+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+-- See the License for the specific language governing permissions and
+-- limitations under the License.
+--
+local core      = require("apisix.core")
+local http      = require "resty.http"
+local sub_str   = string.sub
+local url       = require "net.url"
+local tostring  = tostring
+local ngx       = ngx
+local plugin_name = "authz-keycloak"
+
+
+local schema = {
+    type = "object",
+    properties = {
+        token_endpoint = {type = "string", minLength = 1, maxLength = 4096},
+        permissions = {
+            type = "array",
+            items = {
+                type = "string",
+                minLength = 1, maxLength = 100
+            },
+            uniqueItems = true
+        },
+        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"
+        },
+        keepalive = {type = "boolean", default = true},
+        keepalive_timeout = {type = "integer", minimum = 1000, default = 60000},
+        keepalive_pool = {type = "integer", minimum = 1, default = 5},
+
+    },
+    required = {"token_endpoint"}
+}
+
+
+local _M = {
+    version = 0.1,
+    priority = 2000,
+    type = 'auth',
+    name = plugin_name,
+    schema = schema,
+}
+
+function _M.check_schema(conf)
+    return core.schema.check(schema, conf)
+end
+
+local function is_path_protected(conf)
+    -- TODO if permissions are empty lazy load paths from Keycloak
+    if conf.permissions == nil then
+        return false
+    end
+    return true
+end
+
+
+local function evaluate_permissions(conf, token)
+    local url_decoded = url.parse(conf.token_endpoint)
+    local host = url_decoded.host
+    local port = url_decoded.port
+
+    if not port then
+        if url_decoded.scheme == "https" then
+            port = 443
+        else
+            port = 80
+        end
+    end
+
+    if not is_path_protected(conf) and conf.policy_enforcement_mode == "ENFORCING" then
+        core.response.exit(403)
+        return
+    end
+
+    local httpc = http.new()
+    httpc:set_timeout(conf.timeout)
+
+    local params = {
+        method = "POST",
+        body =  ngx.encode_args({
+            grant_type = conf.grant_type,
+            audience = conf.audience,
+            response_mode = "decision",
+            permission = conf.permissions
+        }),
+        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
+
+    local httpc_res, httpc_err = httpc:request_uri(conf.token_endpoint, params)
+
+    if not httpc_res then
+        core.log.error("error while sending authz request to [", host ,"] port[",
+                        tostring(port), "] ", httpc_err)
+        core.response.exit(500, httpc_err)
+        return
+    end
+
+    if httpc_res.status >= 400 then
+        core.log.error("status code: ", httpc_res.status, " msg: ", httpc_res.body)
+        core.response.exit(httpc_res.status, httpc_res.body)
+    end
+end
+
+
+local function fetch_jwt_token(ctx)
+    local token = core.request.header(ctx, "authorization")
+    if not token then
+        return nil, "authorization header not available"
+    end
+
+    local prefix = sub_str(token, 1, 7)
+    if prefix ~= 'Bearer ' and prefix ~= 'bearer ' then
+        return "Bearer " .. token
+    end
+    return token
+end
+
+
+function _M.rewrite(conf, ctx)
+    core.log.debug("hit keycloak-auth rewrite")
+    local jwt_token, err = fetch_jwt_token(ctx)
+    if not jwt_token then
+        core.log.error("failed to fetch JWT token: ", err)
+        return 401, {message = "Missing JWT token in request"}
+    end
+
+    evaluate_permissions(conf, jwt_token)
+end
+
+
+return _M
diff --git a/conf/config.yaml b/conf/config.yaml
index a418386..d640ef7 100644
--- a/conf/config.yaml
+++ b/conf/config.yaml
@@ -167,6 +167,7 @@ plugins:                          # plugin list
   - http-logger
   - skywalking
   - echo
+  - authz-keycloak
 
 stream_plugins:
   - mqtt-proxy
diff --git a/doc/images/plugin/authz-keycloak.png b/doc/images/plugin/authz-keycloak.png
new file mode 100644
index 0000000..6b6ae84
Binary files /dev/null and b/doc/images/plugin/authz-keycloak.png differ
diff --git a/doc/plugins/authz-keycloak.md b/doc/plugins/authz-keycloak.md
new file mode 100644
index 0000000..47b19ae
--- /dev/null
+++ b/doc/plugins/authz-keycloak.md
@@ -0,0 +1,135 @@
+<!--
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+-->
+
+[Chinese](authz-keycloak-cn.md)
+
+# Summary
+- [**Name**](#name)
+- [**Attributes**](#attributes)
+- [**How To Enable**](#how-to-enable)
+- [**Test Plugin**](#test-plugin)
+- [**Disable Plugin**](#disable-plugin)
+- [**Examples**](#examples)
+
+
+## Name
+
+`authz-keycloak` is an authorization plugin to be used with the Keycloak Identity Server. Keycloak is an OAuth/OIDC and
+UMA compliant Ideneity Server. Although, its developed to working in conjunction with Keycloak it should work with any
+OAuth/OIDC and UMA compliant identity providers as well.
+
+For more information on JWT, refer to [Keycloak Authorization Docs](https://www.keycloak.org/docs/latest/authorization_services) for more information.
+
+## Attributes
+
+|Name           |Requirement    |Description|
+|---------      |--------       |-----------|
+| token_endpoint|required       |A OAuth2-compliant Token Endpoint that supports the urn:ietf:params:oauth:grant-type:uma-ticket grant type.|
+| grant_type    |optional       |Default value is `urn:ietf:params:oauth:grant-type:uma-ticket`.|
+| audience      |optional       |The client identifier of the resource server to which the client is seeking access. This parameter is mandatory in case the permission parameter is defined.|
+| permissions   |optional       |This parameter is 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       |optional       |Timeout for the http connection with the Identity Server. Default is 3 seconds|
+| policy_enforcement_mode|required     |Enforcing or Permissive.|
+
+
+### Policy Enforcement Mode
+
+Specifies how policies are enforced when processing authorization requests sent to the server.
+
+**Enforcing**
+
+- (default mode) Requests are denied by default even when there is no policy associated with a given resource.
+
+**Permissive**
+
+- Requests are allowed even when there is no policy associated with a given resource.
+
+
+## How To Enable
+
+Create a route and enable the authz-keycloak plugin on the route:
+
+```shell
+curl http://127.0.0.1:9080/apisix/admin/routes/5 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d '
+{
+    "uri": "/get",
+    "plugins": {
+        "authz-keycloak": {
+        	"token_endpoint": "http://127.0.0.1:8090/auth/realms/{client_id}/protocol/openid-connect/token",
+        	"permissions": ["resource name#scope name"],
+            "audience": "Client ID"
+        }
+    },
+    "upstream": {
+    	"type": "roundrobin",
+    	"nodes": {
+        	"127.0.0.1:8080": 1
+    	}
+    }
+}
+```
+
+
+## Test Plugin
+
+```shell
+curl http://127.0.0.1:9080/get -H 'Authorization: Bearer {JWT Token}'
+```
+
+
+## Disable Plugin
+
+Remove the corresponding json configuration in the plugin configuration to disable the `authz-keycloak`.
+APISIX plugins are hot-reloaded, therefore no need to restart APISIX.
+
+```shell
+curl http://127.0.0.1:9080/apisix/admin/routes/5 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d '
+{
+    "uri": "/get",
+    "plugins": {
+    },
+    "upstream": {
+    	"type": "roundrobin",
+    	"nodes": {
+        	"127.0.0.1:8080": 1
+    	}
+    }
+}
+```
+
+## Examples
+
+Checkout the unit test for of the authz-keycloak.t to understand how the authorization policies can be integrated into your
+API workflows. Run the following docker image and visit `http://localhost:8090` to view the associated policies for the unit tests.
+
+```bash
+docker run -e KEYCLOAK_USER=admin -e KEYCLOAK_PASSWORD=123456 -p 8090:8080 sshniro/keycloak-apisix
+```
+
+The following image shows how the policies are configures in the Keycloak server.
+
+![Keycloak policy design](../images/plugin/authz-keycloak.png)
+
+## Future Development
+
+- Currently the authz-plugin requires to define the resource name and required scopes inorder to enforce policies for the routes.
+However, Keycloak's official adapters (Java, JS) also provides path matching by querying Keycloak paths dynamically, and
+lazy loading the paths to identify resources. Future version on authz-plugin will support this functionality.
+
+- Support to read scope and configurations from the Keycloak JSON File
diff --git a/t/admin/plugins.t b/t/admin/plugins.t
index fcaf082..1a5f06e 100644
--- a/t/admin/plugins.t
+++ b/t/admin/plugins.t
@@ -30,7 +30,7 @@ __DATA__
 --- request
 GET /apisix/admin/plugins/list
 --- response_body_like eval
-qr/\["limit-req","limit-count","limit-conn","key-auth","basic-auth","prometheus","node-status","jwt-auth","zipkin","ip-restriction","grpc-transcode","serverless-pre-function","serverless-post-function","openid-connect","proxy-rewrite","redirect","response-rewrite","fault-injection","udp-logger","wolf-rbac","proxy-cache","tcp-logger","proxy-mirror","kafka-logger","cors","consumer-restriction","syslog","batch-requests","http-logger","skywalking","echo"\]/
+qr/\["limit-req","limit-count","limit-conn","key-auth","basic-auth","prometheus","node-status","jwt-auth","zipkin","ip-restriction","grpc-transcode","serverless-pre-function","serverless-post-function","openid-connect","proxy-rewrite","redirect","response-rewrite","fault-injection","udp-logger","wolf-rbac","proxy-cache","tcp-logger","proxy-mirror","kafka-logger","cors","consumer-restriction","syslog","batch-requests","http-logger","skywalking","echo","authz-keycloak"\]/
 --- no_error_log
 [error]
 
diff --git a/t/debug/debug-mode.t b/t/debug/debug-mode.t
index 7babd48..2924cdc 100644
--- a/t/debug/debug-mode.t
+++ b/t/debug/debug-mode.t
@@ -66,6 +66,7 @@ loaded plugin and sort by priority: 2520 name: basic-auth
 loaded plugin and sort by priority: 2510 name: jwt-auth
 loaded plugin and sort by priority: 2500 name: key-auth
 loaded plugin and sort by priority: 2400 name: consumer-restriction
+loaded plugin and sort by priority: 2000 name: authz-keycloak
 loaded plugin and sort by priority: 1010 name: proxy-mirror
 loaded plugin and sort by priority: 1009 name: proxy-cache
 loaded plugin and sort by priority: 1008 name: proxy-rewrite
diff --git a/t/plugin/authz-keycloak.t b/t/plugin/authz-keycloak.t
new file mode 100644
index 0000000..c3361b5
--- /dev/null
+++ b/t/plugin/authz-keycloak.t
@@ -0,0 +1,349 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+use t::APISIX 'no_plan';
+
+log_level('debug');
+repeat_each(1);
+no_long_string();
+no_root_location();
+run_tests;
+
+__DATA__
+
+=== TEST 1: sanity
+--- config
+    location /t {
+        content_by_lua_block {
+            local plugin = require("apisix.plugins.authz-keycloak")
+            local ok, err = plugin.check_schema({
+                                   token_endpoint = "https://efactory-security-portal.salzburgresearch.at/",
+                                   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: full schema check
+--- config
+    location /t {
+        content_by_lua_block {
+            local plugin = require("apisix.plugins.authz-keycloak")
+            local ok, err = plugin.check_schema({token_endpoint = "https://efactory-security-portal.salzburgresearch.at/",
+                                                 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 3: token_endpoint 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
+property "token_endpoint" is required
+done
+--- no_error_log
+[error]
+
+
+=== TEST 4: add plugin with view course permissions
+--- 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
+            end
+            ngx.say(body)
+        }
+    }
+--- request
+GET /t
+--- response_body
+passed
+--- no_error_log
+[error]
+
+
+
+=== TEST 5: Get access token for teacher and access view course route
+--- config
+    location /t {
+        content_by_lua_block {
+            local json_decode = require("cjson").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&username=teacher@gmail.com&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)
+            end
+        }
+    }
+--- request
+GET /t
+--- response_body
+true
+--- no_error_log
+[error]
+
+
+=== TEST 6: invalid access token
+--- 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)
+            end
+        }
+    }
+--- request
+GET /t
+--- response_body
+true
+--- error_log
+Invalid bearer token
+
+
+
+=== TEST 7: 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)
+        }
+    }
+--- request
+GET /t
+--- response_body
+passed
+--- no_error_log
+[error]
+
+
+
+=== TEST 8: Get access token for student and delete course
+--- config
+    location /t {
+        content_by_lua_block {
+            local json_decode = require("cjson").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&username=student@gmail.com&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"}