Skip to content

Commit

Permalink
HMAC Plugin
Browse files Browse the repository at this point in the history
clock skew check
  • Loading branch information
Shashi Ranjan committed Sep 18, 2015
1 parent b1e752d commit e1018b7
Show file tree
Hide file tree
Showing 13 changed files with 578 additions and 2 deletions.
9 changes: 8 additions & 1 deletion kong-0.5.0-1.rockspec
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ dependencies = {

"luasocket ~> 2.0.2-5",
"lrexlib-pcre ~> 2.7.2-1",
"lua-llthreads2 ~> 0.1.3-1"
"lua-llthreads2 ~> 0.1.3-1",
"sha1 ~> 0.5-1"
}
build = {
type = "builtin",
Expand Down Expand Up @@ -204,6 +205,12 @@ build = {
["kong.api.routes.consumers"] = "kong/api/routes/consumers.lua",
["kong.api.routes.plugins"] = "kong/api/routes/plugins.lua",
["kong.api.routes.plugins"] = "kong/api/routes/plugins.lua",

["kong.plugins.hmac-auth.handler"] = "kong/plugins/hmac-auth/handler.lua",
["kong.plugins.hmac-auth.access"] = "kong/plugins/hmac-auth/access.lua",
["kong.plugins.hmac-auth.schema"] = "kong/plugins/hmac-auth/schema.lua",
["kong.plugins.hmac-auth.api"] = "kong/plugins/hmac-auth/api.lua",
["kong.plugins.hmac-auth.daos"] = "kong/plugins/hmac-auth/daos.lua"
},
install = {
conf = { "kong.yml" },
Expand Down
1 change: 1 addition & 0 deletions kong.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ plugins_available:
- response-transformer
- request-size-limiting
- acl
- hmac-auth

## The Kong working directory
## (Make sure you have read and write permissions)
Expand Down
134 changes: 134 additions & 0 deletions kong/plugins/hmac-auth/access.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
local cache = require "kong.tools.database_cache"
local stringy = require "stringy"
local responses = require "kong.tools.responses"
local constants = require "kong.constants"

local math_abs = math.abs
local ngx_time = ngx.time
local ngx_gmatch = ngx.re.gmatch
local ngx_decode_base64 = ngx.decode_base64
local ngx_parse_time = ngx.parse_http_time
local ngx_sha1 = ngx.hmac_sha1
local ngx_set_header = ngx.req.set_header

local AUTHORIZATION = "authorization"
local PROXY_AUTHORIZATION = "proxy-authorization"
local DATE = "date"
local SIGNATURE_NOT_VALID = "HMAC signature cannot be verified"

local _M = {}

local function retrieve_hmac_fields(request, header_name, conf)
local username, signature, algorithm
local authorization_header = request.get_headers()[header_name]
if authorization_header then
-- Authentication: hmac username:base64(hmac-sha1(Date))
local iterator, iter_err = ngx_gmatch(authorization_header, "\\s*[Hh]mac\\s*(.+)")
if not iterator then
ngx.log(ngx.ERR, iter_err)
return
end

local m, err = iterator()
if err then
ngx.log(ngx.ERR, err)
return
end

if m and table.getn(m) > 0 then
local hmac_fields = stringy.split(m[1], ":")
if hmac_fields and #hmac_fields > 2 then
username = hmac_fields[1]
signature = ngx_decode_base64(hmac_fields[2])
algorithm = hmac_fields[3]
end
end
end

if conf.hide_credentials then
request.clear_header(header_name)
end

return username, signature, algorithm
end

local function validate_signature(request, secret, signature, algorithm, defaultClockSkew)
-- validate clock skew
local date = request.get_headers()[DATE]
local requestTime = ngx_parse_time(date)
if requestTime == nil then
responses.send_HTTP_UNAUTHORIZED(SIGNATURE_NOT_VALID)
end

local skew = math_abs(ngx_time() - requestTime)
if skew > defaultClockSkew then
responses.send_HTTP_UNAUTHORIZED("HMAC signature expired")
end

-- validate signature
local digest = ngx_sha1(secret.secret, date)
if digest then
return digest == signature
end
end

local function hmacauth_credential_key(username)
return "hmacauth_credentials/"..username
end

local function load_secret(username)
local secret
if username then
secret = cache.get_or_set(hmacauth_credential_key(username), function()
local keys, err = dao.hmacauth_credentials:find_by_keys { username = username }
local result
if err then
return responses.send_HTTP_INTERNAL_SERVER_ERROR(err)
elseif #keys > 0 then
result = keys[1]
end
return result
end)
end
return secret
end

function _M.execute(conf)
-- If both headers are missing, return 401
if not (ngx.req.get_headers()[AUTHORIZATION] or ngx.req.get_headers()[PROXY_AUTHORIZATION]) then
ngx.ctx.stop_phases = true
return responses.send_HTTP_UNAUTHORIZED()
end

local username, signature, algorithm = retrieve_hmac_fields(ngx.req, PROXY_AUTHORIZATION, conf)
-- Try with the authorization header
if not username then
username, signature, algorithm = retrieve_hmac_fields(ngx.req, AUTHORIZATION, conf)
end

if not (username and signature) then
responses.send_HTTP_FORBIDDEN(SIGNATURE_NOT_VALID)
end

local secret = load_secret(username)
if not validate_signature(ngx.req, secret, signature, algorithm, conf.clock_skew) then
ngx.ctx.stop_phases = true -- interrupt other phases of this request
return responses.send_HTTP_FORBIDDEN("HMAC signature does not match")
end

-- Retrieve consumer
local consumer = cache.get_or_set(cache.consumer_key(secret.consumer_id), function()
local result, err = dao.consumers:find_by_primary_key({ id = secret.consumer_id })
if err then
return responses.send_HTTP_INTERNAL_SERVER_ERROR(err)
end
return result
end)

ngx_set_header(constants.HEADERS.CONSUMER_ID, consumer.id)
ngx_set_header(constants.HEADERS.CONSUMER_CUSTOM_ID, consumer.custom_id)
ngx_set_header(constants.HEADERS.CONSUMER_USERNAME, consumer.username)
ngx.ctx.authenticated_entity = secret
end

return _M
51 changes: 51 additions & 0 deletions kong/plugins/hmac-auth/api.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
local crud = require "kong.api.crud_helpers"

return{
["/consumers/:username_or_id/hmac-auth/"] = {
before = function(self, dao_factory, helpers)
crud.find_consumer_by_username_or_id(self, dao_factory, helpers)
self.params.consumer_id = self.consumer.id
end,

GET = function(self, dao_factory, helpers)
crud.paginated_set(self, dao_factory.hmacauth_credentials)
end,

PUT = function(self, dao_factory)
crud.put(self.params, dao_factory.hmacauth_credentials)
end,

POST = function(self, dao_factory)
crud.post(self.params, dao_factory.hmacauth_credentials)
end
},

["/consumers/:username_or_id/hmac-auth/:id"] = {
before = function(self, dao_factory, helpers)
crud.find_consumer_by_username_or_id(self, dao_factory, helpers)
self.params.consumer_id = self.consumer.id

local data, err = dao_factory.hmacauth_credentials:find_by_keys({ id = self.params.id })
if err then
return helpers.yield_error(err)
end

self.credential = data[1]
if not self.credential then
return helpers.responses.send_HTTP_NOT_FOUND()
end
end,

GET = function(self, dao_factory, helpers)
return helpers.responses.send_HTTP_OK(self.credential)
end,

PATCH = function(self, dao_factory)
crud.patch(self.params, self.credential, dao_factory.hmacauth_credentials)
end,

DELETE = function(self, dao_factory)
crud.delete(self.credential, dao_factory.hmacauth_credentials)
end
}
}
22 changes: 22 additions & 0 deletions kong/plugins/hmac-auth/daos.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
local BaseDao = require "kong.dao.cassandra.base_dao"

local SCHEMA = {
primary_key = {"id"},
fields = {
id = { type = "id", dao_insert_value = true },
created_at = { type = "timestamp", dao_insert_value = true },
consumer_id = { type = "id", required = true, queryable = true, foreign = "consumers:id" },
username = { type = "string", required = true, unique = true, queryable = true },
secret = { type = "string" }
}
}

local HMACAuthCredentials = BaseDao:extend()

function HMACAuthCredentials:new(properties)
self._table = "hmacauth_credentials"
self._schema = SCHEMA
HMACAuthCredentials.super.new(self, properties)
end

return { hmacauth_credentials = HMACAuthCredentials }
19 changes: 19 additions & 0 deletions kong/plugins/hmac-auth/handler.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
-- Copyright (C) Mashape, Inc.

local BasePlugin = require "kong.plugins.base_plugin"
local access = require "kong.plugins.hmac-auth.access"

local HMACAuthHandler = BasePlugin:extend()

function HMACAuthHandler:new()
HMACAuthHandler.super.new(self, "hmac-auth")
end

function HMACAuthHandler:access(conf)
HMACAuthHandler.super.access(self)
access.execute(conf)
end

HMACAuthHandler.PRIORITY = 999

return HMACAuthHandler
27 changes: 27 additions & 0 deletions kong/plugins/hmac-auth/migrations/cassandra.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
local Migrations = {
{
name = "2015-09-16-132400_init_hmacauth",
up = function(options)
return [[
CREATE TABLE IF NOT EXISTS hmacauth_credentials(
id uuid,
consumer_id uuid,
username text,
secret text,
created_at timestamp,
PRIMARY KEY (id)
);
CREATE INDEX IF NOT EXISTS ON hmacauth_credentials(username);
CREATE INDEX IF NOT EXISTS hmacauth_consumer_id ON hmacauth_credentials(consumer_id);
]]
end,
down = function(options)
return [[
DROP TABLE hmacauth_credentials;
]]
end
}
}

return Migrations
14 changes: 14 additions & 0 deletions kong/plugins/hmac-auth/schema.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
local function check_clock_skew_positive(v)
if v and v < 0 then
return false, "Clock Skew should be positive"
end
return true
end

return {
no_consumer = true,
fields = {
hide_credentials = { type = "boolean", default = false },
clock_skew = { type = "number", default = 300, func = check_clock_skew_positive }
}
}
2 changes: 1 addition & 1 deletion kong/tools/faker.lua
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ function Faker:insert_from_table(entities_to_insert)
-- Insert in order (for foreign relashionships)
-- 1. consumers and APIs
-- 2. credentials, which need references to inserted apis and consumers
for _, type in ipairs({ "api", "consumer", "plugin", "oauth2_credential", "basicauth_credential", "keyauth_credential", "acl" }) do
for _, type in ipairs({ "api", "consumer", "plugin", "oauth2_credential", "basicauth_credential", "keyauth_credential", "acl", "hmacauth_credential" }) do
if entities_to_insert[type] then
for i, entity in ipairs(entities_to_insert[type]) do

Expand Down
Loading

7 comments on commit e1018b7

@ejoncas
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @shashiranjan84 thanks for this HMAC plugin, it looks promising. Is there any documentation on how to configure this?

@shashiranjan84
Copy link
Contributor

@shashiranjan84 shashiranjan84 commented on e1018b7 Sep 21, 2015 via email

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@shashiranjan84
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ejoncas if you really want to try now,
Add an api

curl -i -X POST --url http://localhost:8001/apis/ --data "name=mockbin" --data "upstream_url=http://mockbin.org" --data "inbound_dns=mockbin.com"

Add hmac-auth plugin

curl -i -X POST --url http://localhost:8001/apis/mockbin/plugins/ --data "name=hmac-auth"

Add a customer

curl -i -X POST --url http://localhost:8001/consumers/ --data "username=bob"

Add hmac credentials

curl -i -X POST --url http://localhost:8001/consumers/bob/hmac-auth --data "username=bob" --data "secret=secret"

Make request

set the authorization header in following format

"hamc username:base64(hmac-sha1-raw(date header value in GMT, secret)):hmac-sha1"

curl -i -X GET --url http://localhost:8000/request --header "Host: mockbin.com" --header "date: Mon, 21 Sep 2015 05:13:00 GMT" --header "Proxy-Authorization: hmac bob:Oq1Aj0GnNT0BD3sbH9oFX390t5E=:hmac-sha1"

To update the clock skew

curl-i -X PATCH --url http://localhost:8001/apis/mockbin/plugins/<plugin uuid> --data "config.clock_skew=10000

Note: Checkout the master code(0.5.0 snapshot)

@ejoncas
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@shashiranjan84 Thanks a lot! That's exactly what I needed

@shashiranjan84
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ejoncas I just finished adding support for other headers and request line. I have not merged it but in case you want to try https://github.com/shashiranjan84/kong(not yet merged to release branch)

Now its based on https://tools.ietf.org/html/draft-cavage-http-signatures-00, so there is slight change in format.

here is the link to the test to get some idea https://github.com/shashiranjan84/kong/blob/master/spec/plugins/hmac-auth/access_spec.lua#L176

@ejoncas
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @shashiranjan84 thanks for the update.... I'm trying to write a new plugin because our HMAC Auth doesn't follow the standard so the string to sign is a little bit different but your plugin helped me a lot to understand what I need to do. Thansk a lot!

@shashiranjan84
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I glad that it helped you :)

Please sign in to comment.