diff --git a/kong/plugins/key-token/access.lua b/kong/plugins/key-token/access.lua new file mode 100755 index 000000000000..950f48d3d501 --- /dev/null +++ b/kong/plugins/key-token/access.lua @@ -0,0 +1,75 @@ +local http = require "kong.plugins.key-token.auth_service" +local error = error + + +local _M = {} + + +function _M:execute(plugin_conf) + -- Here is the main logic + -- 1. check if the client has the auth_key in the header. + -- 1.1 if user has no auth_key, return 412 + -- 1.2 if user has auth_key use the key to auth authentication server + -- 2.1 if the JWT token is valid then go to 4 + -- 2.2 request the authentication server with the auth_key + -- 2.2.1 authentication server reply 200 with the JWT token is the auth_key is valid, + -- 2.2.2 authentication server reply 403 reject the request if the auth_key is invalid + -- 3 cache the JWT token + -- 4.1 if none 200, return immediately, no access to the upstream + -- 4.2 if 200, access the upstream with the Authentication header + local auth_key = kong.request.get_header(plugin_conf.request_key_name) + local auth_server = plugin_conf.auth_server + local ttl = plugin_conf.ttl + local cached_token, err = self:get_cached_token(auth_key, auth_server, ttl) + if err then + kong.log.err("Failed to acquire token associates with the key. Error: " .. err) + return + end + + if cached_token then + self:inject_token_to_service_header(cached_token) + return + end + +end + + +function _M:get_cached_token(auth_key, auth_server, ttl) + -- return the cached token if it is not out of life. or else return nil + local cache = kong.cache + local credential_cache_key = kong.db.keyauth_credentials:cache_key(auth_key) + local credential, err = cache:get(credential_cache_key, { resurrent_ttl = ttl }, load_auth_token, auth_key, auth_server) + if err then + return nil, err + else + return credential, nil + end +end + + +function _M:inject_token_to_service_header(token) + kong.service.request.set_header("Authorizaion", "Bearer " .. token) +end + + +function _M:save_token_to_cache(auth_key, token) +end + +function load_auth_token(auth_key, auth_server) + -- Maybe to set TIMEOUT variable to control the timeout? + local auth_headers = { auth_key = auth_key } + local body, status_code, headers, status_text = http.request { + url = auth_server, + headers = auth_headers, + } + + kong.log.debug("return code from auth service " .. status_code) + if status_code == 200 then + return body, nil + else + return nil, error("Auth server return non 200 code " .. status_code) + end +end + + +return _M diff --git a/kong/plugins/key-token/auth_service.lua b/kong/plugins/key-token/auth_service.lua new file mode 100755 index 000000000000..5fdfbb333dae --- /dev/null +++ b/kong/plugins/key-token/auth_service.lua @@ -0,0 +1,18 @@ +local http = require "socket.http" + + +local default_payload = "123456" +local _M = {} + +function _M.request(req) + kong.log.inspect(req) + if string.find(req.url, "localhost") then + -- mock the http request + return default_payload, 200, req.headers, "200 OK" + else + return http.request(req) + end +end + + +return _M diff --git a/kong/plugins/key-token/handler.lua b/kong/plugins/key-token/handler.lua new file mode 100755 index 000000000000..c6d1760bef34 --- /dev/null +++ b/kong/plugins/key-token/handler.lua @@ -0,0 +1,17 @@ +local access = require "kong.plugins.key-token.access" + + +local plugin = { + PRIORITY = 1255, -- Execute before key-auth + VERSION = "0.1.0", -- The initial version +} + + +-- runs in the 'access_by_lua_block' +function plugin:access(plugin_conf) + access:execute(plugin_conf) +end --]] + + +-- return our plugin object +return plugin diff --git a/kong/plugins/key-token/schema.lua b/kong/plugins/key-token/schema.lua new file mode 100755 index 000000000000..82582fad652b --- /dev/null +++ b/kong/plugins/key-token/schema.lua @@ -0,0 +1,40 @@ +local typedefs = require "kong.db.schema.typedefs" + + +local PLUGIN_NAME = "key-token" + + +local schema = { + name = PLUGIN_NAME, + fields = { + -- the 'fields' array is the top-level entry with fields defined by Kong + { consumer = typedefs.no_consumer }, -- this plugin cannot be configured on a consumer (typical for auth plugins) + { protocols = typedefs.protocols_http }, + { config = { + -- The 'config' record is the custom part of the plugin schema + type = "record", + fields = { + -- a standard defined field (typedef), with some customizations + { request_key_name = typedefs.header_name { + description = "The header name that is used to send to backend authentication service.", + type = "string", + required = true, + default = "auth_key" }, }, + { auth_server = typedefs.url { + description = "The authenticaiton/authorization service URL. please note that 'localhost' is reserved for integration test.", + required = true, + default = "http://auth_server.com" }, }, + { ttl = { + description = "TTL for cached token from auth server.", + type = "integer", + default = 600, + required = true, + gt = 0, }, }, + }, + entity_checks = { }, + }, + }, + }, +} + +return schema diff --git a/spec/02-integration/23-key-token_plugins/01-integration_spec.lua b/spec/02-integration/23-key-token_plugins/01-integration_spec.lua new file mode 100755 index 000000000000..9d3103aa15bc --- /dev/null +++ b/spec/02-integration/23-key-token_plugins/01-integration_spec.lua @@ -0,0 +1,86 @@ +local helpers = require "spec.helpers" + + +local PLUGIN_NAME = "key-token" + + +for _, strategy in helpers.all_strategies() do if strategy ~= "cassandra" then + describe(PLUGIN_NAME .. ": (access) [#" .. strategy .. "]", function() + local client + + lazy_setup(function() + + local bp = helpers.get_db_utils(strategy == "off" and "postgres" or strategy, nil, { PLUGIN_NAME }) + + -- Inject a test route. No need to create a service, there is a default + -- service which will echo the request. + local route_auth_server = bp.routes:insert({ + hosts = { "auth_server.com" }, + }) + + local route_upstream = bp.routes:insert({ + hosts = { "resource1.com" }, + }) + -- add the plugin to test to the route we created + bp.plugins:insert { + name = PLUGIN_NAME, + route = { id = route_auth_server.id }, + config = {}, + } + + bp.plugins:insert { + name = PLUGIN_NAME, + route = { id = route_upstream.id }, + config = {auth_server = "http://localhost:9001"}, + } + -- start kong + assert(helpers.start_kong({ + -- set the strategy + database = strategy, + -- use the custom test template to create a local mock server + nginx_conf = "spec/fixtures/custom_nginx.template", + -- make sure our plugin gets loaded + plugins = "bundled," .. PLUGIN_NAME, + -- write & load declarative config, only if 'strategy=off' + declarative_config = strategy == "off" and helpers.make_yaml_file() or nil, + })) + end) + + lazy_teardown(function() + helpers.stop_kong(nil, true) + end) + + before_each(function() + client = helpers.proxy_client() + end) + + after_each(function() + if client then client:close() end + end) + + + + describe("request", function() + it("gets a auth key header", function() + local r = client:get("/request", { + headers = { + host = "resource1.com", + auth_key = "123456" + } + }) + -- validate that the request succeeded, response status 200 + assert.response(r).has.status(200) + -- now check the request (as echoed by the mock backend) to have the header + local header_value = assert.request(r).has.header("auth_key") + -- validate the value of that header + assert.equal("123456", header_value) + -- The mock server returns 123456. The ideal auth server would issue JWT token + local auth_token = assert.request(r).has.header("Authorizaion") + assert.equal("Bearer 123456", auth_token) + end) + end) + + + end) + +end end diff --git a/spec/03-plugins/46-key-token/01-unit_spec.lua b/spec/03-plugins/46-key-token/01-unit_spec.lua new file mode 100755 index 000000000000..dbad0396b38b --- /dev/null +++ b/spec/03-plugins/46-key-token/01-unit_spec.lua @@ -0,0 +1,36 @@ +local PLUGIN_NAME = "key-token" + + +-- helper function to validate data against a schema +local validate do + local validate_entity = require("spec.helpers").validate_plugin_config_schema + local plugin_schema = require("kong.plugins."..PLUGIN_NAME..".schema") + + function validate(data) + return validate_entity(data, plugin_schema) + end +end + + +describe(PLUGIN_NAME .. ": (schema)", function() + + + it("accepts request key, auth server and ttl", function() + local ok, err = validate({ + request_key_name = "My-Request-Header", + auth_server = "http://my-auth-service/", + ttl = 300 + }) + assert.is_nil(err) + assert.is_truthy(ok) + end) + + + it("accepts default configs", function() + local ok, err = validate({ }) + assert.is_nil(err) + assert.is_truthy(ok) + end) + + +end)