Merge pull request #88 from taylorking/clientsecret

Support for client secret validation
diff --git a/Dockerfile b/Dockerfile
index 81fb111..18c7c26 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -152,6 +152,23 @@
     && $INSTALL lib/resty/*.lua ${LUA_LIB_DIR}/resty/ \
     && rm -rf /tmp/api-gateway
 
+
+
+ENV LUA_RESTY_STRING_VERSION 0.09
+RUN echo " ... installing lua-resty-string..." \
+    && apk update \
+    && apk add make \
+    && mkdir -p /tmp/api-gateway \
+    && curl -k -L https://github.com/openresty/lua-resty-string/archive/v${LUA_RESTY_STRING_VERSION}.tar.gz -o /tmp/api-gateway/lua-resty-string-${LUA_RESTY_STRING_VERSION}.tar.gz \
+    && tar -xf /tmp/api-gateway/lua-resty-string-${LUA_RESTY_STRING_VERSION}.tar.gz -C /tmp/api-gateway/ \
+    && cd /tmp/api-gateway/lua-resty-string-${LUA_RESTY_STRING_VERSION} \
+    && make install \
+            LUA_LIB_DIR=${_prefix}/api-gateway/lualib \
+            INSTALL=${_prefix}/api-gateway/bin/resty-install \
+    && rm -rf /tmp/api-gateway
+
+
+
 ENV NETURL_LUA_VERSION 0.9-1
 RUN echo " ... installing neturl.lua ... " \
     && mkdir -p /tmp/api-gateway \
diff --git a/api-gateway-config/scripts/lua/policies/security/clientSecret.lua b/api-gateway-config/scripts/lua/policies/security/clientSecret.lua
new file mode 100644
index 0000000..6151e71
--- /dev/null
+++ b/api-gateway-config/scripts/lua/policies/security/clientSecret.lua
@@ -0,0 +1,95 @@
+-- Copyright (c) 2016 IBM. All rights reserved.
+--
+--   Permission is hereby granted, free of charge, to any person obtaining a
+--   copy of this software and associated documentation files (the "Software"),
+--   to deal in the Software without restriction, including without limitation
+--   the rights to use, copy, modify, merge, publish, distribute, sublicense,
+--   and/or sell copies of the Software, and to permit persons to whom the
+--   Software is furnished to do so, subject to the following conditions:
+--
+--   The above copyright notice and this permission notice shall be included in
+--   all copies or substantial portions of the Software.
+--
+--   THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+--   IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+--   FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+--   AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+--   LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+--   FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+--   DEALINGS IN THE SOFTWARE.
+
+--- @module mapping
+-- Process mapping object, turning implementation details into request transformations
+-- @author Cody Walker (cmwalker), Alex Song (songs)
+
+local _M = {}
+
+local redis = require "lib/redis"
+local utils = require "lib/utils"
+local request = require "lib/request"
+
+local REDIS_HOST = os.getenv("REDIS_HOST")
+local REDIS_PORT = os.getenv("REDIS_PORT")
+local REDIS_PASS = os.getenv("REDIS_PASS")
+
+local resty_sha256 = require "resty.sha256"
+local resty_str = require "resty.string" 
+
+function process(securityObj) 
+  local tenant = ngx.var.tenant
+  local gatewayPath = ngx.var.gatewayPath
+  local apiId = ngx.var.apiId
+  local scope = securityObj.scope
+
+  local location = (securityObj.keyLocation == nil) and 'http_' or securityObj.keyLocation  
+  if location == 'header' then
+    location = 'http_'
+  end
+  
+  local clientIdName = (securityObj.idFieldName == nil) and 'X-Client-ID' or securityObj.idFieldName
+  
+  local clientId = ngx.var[utils.concatStrings({location, clientIdName}):gsub("-", "_")]
+  
+  if not clientId then
+    request.err(401, clientIdName .. " required")
+  end
+
+  local clientSecretName = (securityObj.secretFieldName == nil) and 'X-Client-Secret' or securityObj.secretFieldName 
+
+  local clientSecret = ngx.var[utils.concatStrings({location, clientSecretName}):gsub("-","_")]
+  if not clientSecret then
+    request.err(401, clientSecretName .. " required")
+  end
+  
+  local sha256 = resty_sha256:new() 
+  sha256:update(clientSecret)  
+  local digest = sha256:final()
+  local result = validate(tenant, gatewayPath, apiId, scope, clientId, resty_str.to_hex(digest))
+  if not result then
+    request.err(401, "Secret mismatch or not subscribed to this api.")
+  end
+
+end
+
+function validate(tenant, gatewayPath, apiId, scope, clientId, clientSecret)
+  -- Open connection to redis or use one from connection pool
+  local red = redis.init(REDIS_HOST, REDIS_PORT, REDIS_PASS, 1000)
+  local k
+  if scope == 'tenant' then
+    k = utils.concatStrings({'subscriptions:tenant:', tenant})
+  elseif scope == 'resource' then
+    k = utils.concatStrings({'subscriptions:tenant:', tenant, ':resource:', gatewayPath})
+  elseif scope == 'api' then
+    k = utils.concatStrings({'subscriptions:tenant:', tenant, ':api:', apiId})
+  end
+
+
+
+  k = utils.concatStrings({k, ':clientsecret:', clientId, ':', clientSecret})
+  local exists = red:exists(k)
+  redis.close(red)
+  return exists == 1
+end
+
+_M.process = process
+return _M 
diff --git a/doc/policies.md b/doc/policies.md
index eaf6d30..b9494d2 100644
--- a/doc/policies.md
+++ b/doc/policies.md
@@ -109,10 +109,14 @@
 
 
 ##Security
-Supported types: `apiKey`.  
+Supported types: `apiKey, clientSecret`.  
 _scope:_ `api`, `tenant`, `resource`.  
 _header:_ _(optional)_ custom name of auth header (default is x-api-key)  
 
+_keyLocation:_ _(optional)_ custom location for client secret keys. header, query_string (default is header)
+_idFieldName:_ _(optional)_ key for locating client id. default (X-Client-ID)
+_secretFieldName:_ _(optional)_ key for locating client secret. default (X-Client-Secret)
+
 ```
 "security":[{
         "type":"apiKey",