Skip to content

Commit

Permalink
Add plugin key-token.
Browse files Browse the repository at this point in the history
The plugin takes the given key from a configured header name of the
incoming traffic, then accesses the configured authentication service
for a (JWT) token. The token is cached as the secified TTL and inserted
to the request header to access the target service.

TODOs:
- Add more test coverage for both unit and integration.
- Understand where to put the doc for this plugin.
- Need more detailed granular control of accessing authN/authZ
  server. For example the timeout, return code handling.
- Need more discussion on the high level design of this plugin. Does the
  actual use case exist? And how to make it more reliable and flexible.
  • Loading branch information
BinJu committed Nov 29, 2024
1 parent 35aa605 commit e7ba09b
Show file tree
Hide file tree
Showing 6 changed files with 272 additions and 0 deletions.
75 changes: 75 additions & 0 deletions kong/plugins/key-token/access.lua
Original file line number Diff line number Diff line change
@@ -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
18 changes: 18 additions & 0 deletions kong/plugins/key-token/auth_service.lua
Original file line number Diff line number Diff line change
@@ -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
17 changes: 17 additions & 0 deletions kong/plugins/key-token/handler.lua
Original file line number Diff line number Diff line change
@@ -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
40 changes: 40 additions & 0 deletions kong/plugins/key-token/schema.lua
Original file line number Diff line number Diff line change
@@ -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
86 changes: 86 additions & 0 deletions spec/02-integration/23-key-token_plugins/01-integration_spec.lua
Original file line number Diff line number Diff line change
@@ -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
36 changes: 36 additions & 0 deletions spec/03-plugins/46-key-token/01-unit_spec.lua
Original file line number Diff line number Diff line change
@@ -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)

0 comments on commit e7ba09b

Please sign in to comment.