Skip to content

Commit

Permalink
feat(key-auth) optionally search the request body for credentials
Browse files Browse the repository at this point in the history
This commit provides an optional plugin configuration field to
search the request body for the authentication credentials. This
behavior respects the hide_credentials field.

This commit also expands the access test suite to cover a few more
use cases.
  • Loading branch information
p0pr0ck5 committed May 4, 2017
1 parent 7abd13b commit d3e9071
Show file tree
Hide file tree
Showing 5 changed files with 193 additions and 27 deletions.
21 changes: 21 additions & 0 deletions kong/plugins/key-auth/handler.lua
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,17 @@ local cache = require "kong.tools.database_cache"
local responses = require "kong.tools.responses"
local constants = require "kong.constants"
local singletons = require "kong.singletons"
local public_tools = require "kong.tools.public"
local BasePlugin = require "kong.plugins.base_plugin"

local ngx_set_header = ngx.req.set_header
local ngx_get_headers = ngx.req.get_headers
local set_uri_args = ngx.req.set_uri_args
local get_uri_args = ngx.req.get_uri_args
local clear_header = ngx.req.clear_header
local ngx_req_read_body = ngx.req.read_body
local ngx_req_set_body_data = ngx.req.set_body_data
local ngx_encode_args = ngx.encode_args
local type = type

local _realm = 'Key realm="'.._KONG._NAME..'"'
Expand Down Expand Up @@ -66,6 +70,13 @@ local function do_authentication(conf)
local key
local headers = ngx_get_headers()
local uri_args = get_uri_args()
local body_data

-- read in the body if we want to examine POST args
if conf.key_in_body then
ngx_req_read_body()
body_data = public_tools.get_post_args()
end

-- search in headers & querystring
for i = 1, #conf.key_names do
Expand All @@ -76,12 +87,22 @@ local function do_authentication(conf)
v = uri_args[name]
end

-- search the body, if we asked to
if not v and conf.key_in_body then
v = body_data[name]
end

if type(v) == "string" then
key = v
if conf.hide_credentials then
uri_args[name] = nil
set_uri_args(uri_args)
clear_header(name)

if conf.key_in_body then
body_data[name] = nil
ngx_req_set_body_data(ngx_encode_args(body_data))
end
end
break
elseif type(v) == "table" then
Expand Down
4 changes: 4 additions & 0 deletions kong/plugins/key-auth/schema.lua
Original file line number Diff line number Diff line change
Expand Up @@ -48,5 +48,9 @@ return {
default = "",
func = check_user,
},
key_in_body = {
type = "boolean",
default = "false",
},
}
}
2 changes: 1 addition & 1 deletion spec/01-unit/07-entities_schemas_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -637,7 +637,7 @@ describe("Entities Schemas", function()
-- Insert key-auth, whose config has some default values that should be set
local plugin = {name = "key-auth", api_id = "stub"}
local valid = validate_entity(plugin, plugins_schema, {dao = dao_stub})
assert.same({key_names = {"apikey"}, hide_credentials = false, anonymous = ""}, plugin.config)
assert.same({key_names = {"apikey"}, hide_credentials = false, anonymous = "", key_in_body = false}, plugin.config)
assert.is_true(valid)
end)
it("should be valid if no value is specified for a subfield and if the config schema has default as empty array", function()
Expand Down
4 changes: 2 additions & 2 deletions spec/02-integration/02-dao/04-constraints_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ helpers.for_each_dao(function(kong_config)
assert.falsy(err)
assert.is_table(plugin)
assert.equal(api_fixture.id, plugin.api_id)
assert.same({hide_credentials = false, key_names = {"apikey"}, anonymous = ""}, plugin.config)
assert.same({hide_credentials = false, key_names = {"apikey"}, anonymous = "", key_in_body = false,}, plugin.config)
end)
it("insert a valid plugin bis", function()
plugin_fixture.api_id = api_fixture.id
Expand All @@ -55,7 +55,7 @@ helpers.for_each_dao(function(kong_config)
assert.falsy(err)
assert.is_table(plugin)
assert.equal(api_fixture.id, plugin.api_id)
assert.same({hide_credentials = false, key_names = {"api-key"}, anonymous = ""}, plugin.config)
assert.same({hide_credentials = false, key_names = {"api-key"}, anonymous = "", key_in_body = false}, plugin.config)
end)
describe("unique per API/Consumer", function()
it("API/Plugin", function()
Expand Down
189 changes: 165 additions & 24 deletions spec/03-plugins/10-key-auth/02-access_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,33 @@ describe("Plugin: key-auth (access)", function()
}
})

local api5 = assert(helpers.dao.apis:insert {
name = "api-5",
hosts = { "key-auth5.com" },
upstream_url = "http://mockbin.com"
})
assert(helpers.dao.plugins:insert {
name = "key-auth",
api_id = api5.id,
config = {
key_in_body = true,
}
})

local api6 = assert(helpers.dao.apis:insert {
name = "api-6",
hosts = { "key-auth6.com" },
upstream_url = "http://mockbin.com"
})
assert(helpers.dao.plugins:insert {
name = "key-auth",
api_id = api6.id,
config = {
key_in_body = true,
hide_credentials = true,
}
})

assert(helpers.start_kong())
client = helpers.proxy_client()
end)
Expand Down Expand Up @@ -138,6 +165,67 @@ describe("Plugin: key-auth (access)", function()
end)
end)

describe("key in request body", function()
it("authenticates valid credentials", function()
local res = assert(client:send {
path = "/request",
headers = {
["Host"] = "key-auth5.com",
["Content-Type"] = "application/www-form-urlencoded",
},
body = {
apikey = "kong",
}
})
assert.res_status(200, res)
end)
it("returns 403 Forbidden on invalid key", function()
local res = assert(client:send {
path = "/status/200",
headers = {
["Host"] = "key-auth5.com",
["Content-Type"] = "application/www-form-urlencoded",
},
body = {
apikey = "123",
}
})
local body = assert.res_status(403, res)
local json = cjson.decode(body)
assert.same({ message = "Invalid authentication credentials" }, json)
end)
it("handles duplicated key", function()
local res = assert(client:send {
path = "/status/200",
headers = {
["Host"] = "key-auth5.com",
["Content-Type"] = "application/www-form-urlencoded",
},
body = {
apikey = { "kong", "kong" },
},
})
local body = assert.res_status(401, res)
local json = cjson.decode(body)
assert.same({ message = "Duplicate API key found" }, json)
end)
it("only handles application/www-form-urlencoded bodies", function()
local res = assert(client:send {
path = "/status/200",
headers = {
["Host"] = "key-auth5.com",
["Content-Type"] = "application/json",
},
body = {
apikey = { "kong", "kong" },
},
})
local body = assert.res_status(401, res)
local json = cjson.decode(body)
assert.same({ message = "No API key found in headers or querystring" }, json)
end)
end)

describe("key in headers", function()
it("authenticates valid credentials", function()
local res = assert(client:send {
Expand Down Expand Up @@ -183,32 +271,85 @@ describe("Plugin: key-auth (access)", function()
end)

describe("config.hide_credentials", function()
it("false sends key to upstream", function()
local res = assert(client:send {
method = "GET",
path = "/request",
headers = {
["Host"] = "key-auth1.com",
["apikey"] = "kong"

local harness = {
queryString = {
{
headers = { Host = "key-auth1.com" },
path = "/request?apikey=kong",
method = "GET",
},
{
headers = { Host = "key-auth2.com" },
path = "/request?apikey=kong",
method = "GET",
}
})
local body = assert.res_status(200, res)
local json = cjson.decode(body)
assert.equal("kong", json.headers.apikey)
end)
it("true doesn't send key to upstream", function()
local res = assert(client:send {
method = "GET",
path = "/request",
headers = {
["Host"] = "key-auth2.com",
["apikey"] = "kong"
},
headers = {
{
headers = { Host = "key-auth1.com", ["apikey"] = "kong" },
path = "/request",
method = "GET",
},
{
headers = { Host = "key-auth2.com", ["apikey"] = "kong" },
path = "/request",
method = "GET",
}
})
local body = assert.res_status(200, res)
local json = cjson.decode(body)
assert.is_nil(json.headers.apikey)
end)
},
postData = {
{
headers = { ["Host"] = "key-auth5.com", ["Content-Type"] = "application/www-form-urlencoded" },
body = { apikey = "kong" },
method = "POST",
path = "/request",
},
{
headers = { ["Host"] = "key-auth6.com", ["Content-Type"] = "application/www-form-urlencoded" },
body = { apikey = "kong" },
method = "POST",
path = "/request",
}
}
}

for type, _ in pairs(harness) do
describe(type, function()
it("false sends key to upstream", function()
local res = assert(client:send(harness[type][1]))
local body = assert.res_status(200, res)
local json = cjson.decode(body)

-- small workaround for how mockbin sends body data
local field
if type == "postData" then
local t = json[type].text:sub(8)
field = { apikey = t ~= "" and t or nil }

else
field = json[type]
end

assert.equal("kong", field.apikey)
end)
it("true doesn't send key to upstream", function()
local res = assert(client:send(harness[type][2]))
local body = assert.res_status(200, res)
local json = cjson.decode(body)

local field
if type == "postData" then
local t = json[type].text:sub(8)
field = { apikey = t ~= "" and t or nil }

else
field = json[type]
end

assert.is_nil(field.apikey)
end)
end)
end
end)

describe("config.anonymous", function()
Expand Down

0 comments on commit d3e9071

Please sign in to comment.