Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New feature for OAuth2 Token Validation using Fiware Wilma PEP-Proxy #1

Merged
merged 6 commits into from
Oct 26, 2017
18 changes: 10 additions & 8 deletions config/default.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,9 @@ gatekeeper:
- header
- getParam
- basicAuthUsername
api_key_cache: true
api_key_cache: false
pep_host: 127.0.0.1 # ip or hostname of Pep Proxy host
pep_port: 8090 # listen port of Pep Proxy
trafficserver:
host: 127.0.0.1
port: 14009
Expand Down Expand Up @@ -282,12 +284,12 @@ apiSettings:
message: The requested URL was not found on this server.
api_key_missing:
status_code: 403
code: API_KEY_MISSING
message: No api_key was supplied. Get one at {{signup_url}}
code: API_KEY_OR_TOKEN_MISSING
message: No api_key or token was supplied. Get one at {{signup_url}}
api_key_invalid:
status_code: 403
code: API_KEY_INVALID
message: An invalid api_key was supplied. Get one at {{signup_url}}
code: API_KEY_OR_TOKEN_INVALID
message: An invalid api_key or token was supplied. Get one at {{signup_url}}
api_key_disabled:
status_code: 403
code: API_KEY_DISABLED
Expand All @@ -298,8 +300,8 @@ apiSettings:
message: The api_key supplied has not been verified yet. Please check your e-mail to verify the API key. Contact us at {{contact_url}} for assistance
api_key_unauthorized:
status_code: 403
code: API_KEY_UNAUTHORIZED
message: The api_key supplied is not authorized to access the given service. Contact us at {{contact_url}} for assistance
code: API_KEY_OR_TOKEN_UNAUTHORIZED
message: The api_key or token supplied is not authorized to access the given service. Contact us at {{contact_url}} for assistance
over_rate_limit:
status_code: 429
code: OVER_RATE_LIMIT
Expand Down Expand Up @@ -368,4 +370,4 @@ ban:
message: "Please contact us for assistance."
ember_server:
port: 14050
live_reload_port: 14051
live_reload_port: 14051
4 changes: 3 additions & 1 deletion src/api-umbrella/proxy/hooks/rewrite.lua
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,15 @@ wait_for_setup()
-- ngx.var lookups are apparently somewhat expensive.
ngx.ctx.args = ngx_var.args
ngx.ctx.arg_api_key = ngx_var.arg_api_key
ngx.ctx.arg_token = ngx_var.arg_token
if(config["router"]["match_x_forwarded_host"]) then
ngx.ctx.host = ngx_var.http_x_forwarded_host or ngx_var.http_host or ngx_var.host
else
ngx.ctx.host = ngx_var.http_host or ngx_var.host
end
ngx.ctx.host_normalized = host_normalize(ngx.ctx.host)
ngx.ctx.http_x_api_key = ngx_var.http_x_api_key
ngx.ctx.http_x_auth_token = ngx_var.http_x_auth_token
ngx.ctx.port = ngx_var.real_port
ngx.ctx.protocol = ngx_var.real_scheme
ngx.ctx.remote_addr = ngx_var.remote_addr
Expand Down Expand Up @@ -69,4 +71,4 @@ else
else
error_handler(api_err)
end
end
end
41 changes: 28 additions & 13 deletions src/api-umbrella/proxy/middleware/api_key_validator.lua
Original file line number Diff line number Diff line change
Expand Up @@ -6,32 +6,47 @@ local is_empty = types.is_empty

local function resolve_api_key()
local api_key_methods = config["gatekeeper"]["api_key_methods"]
local api_key
local key = {key_value="", key_type="" }

-- The api_key variable is a dictionary compose by two elements, the key_value which stores
-- the api_key value or the user token value and the key_type field in where is stored
-- the type of key that was provided by the user, it value could be an api_key or a token.
-- The validation process is made for all the api_key_methods (except basicAuthUsername)
-- declared in the configuration file checking if the user sends an api_key or token
-- Only the header and get_param methods are supported by the token validation.
for _, method in ipairs(api_key_methods) do
if method == "header" then
api_key = ngx.ctx.http_x_api_key
elseif method == "getParam" then
api_key = ngx.ctx.arg_api_key
elseif method == "basicAuthUsername" then
api_key = ngx.ctx.remote_user
if method == "header" and ngx.ctx.http_x_api_key then
key.key_value = ngx.ctx.http_x_api_key
key.key_type = "api_key"
elseif ngx.ctx.http_x_auth_token then
key.key_value = ngx.ctx.http_x_auth_token
key.key_type = "token"
elseif method == "getParam" and ngx.ctx.arg_api_key then
key.key_value = ngx.ctx.arg_api_key
key.key_type = "api_key"
elseif ngx.ctx.arg_token then
key.key_value = ngx.ctx.arg_token
key.key_type = "token"
elseif method == "basicAuthUsername" and ngx.ctx.remote_user then
key.key_value = ngx.ctx.remote_user
key.key_type = "api_key"
end

if not is_empty(api_key) then
if not is_empty(key["key_value"]) then
break
end
end

-- Store the api key for logging.
ngx.ctx.api_key = api_key
ngx.ctx.api_key = key["key_value"]

return api_key
return key
end

return function(settings)
-- Find the API key in the header, query string, or HTTP auth.
local api_key = resolve_api_key()
if is_empty(api_key) then
if is_empty(api_key["key_value"]) then
if settings and settings["disable_api_key"] then
return nil
else
Expand All @@ -47,7 +62,7 @@ return function(settings)

-- Store the api key on the user object for easier access (the user object
-- doesn't contain it directly, to save memory storage in the lookup table).
user["api_key"] = api_key
user["api_key"] = api_key["key_value"]

-- Store user details for logging.
ngx.ctx.user_id = user["id"]
Expand Down Expand Up @@ -76,4 +91,4 @@ return function(settings)
end

return user
end
end
56 changes: 43 additions & 13 deletions src/api-umbrella/proxy/user_store.lua
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,33 @@ local mongo = require "api-umbrella.utils.mongo"
local shcache = require "shcache"
local types = require "pl.types"
local utils = require "api-umbrella.proxy.utils"
local pep = require "api-umbrella.utils.pep"

local cache_computed_settings = utils.cache_computed_settings
local is_empty = types.is_empty

local function lookup_user(api_key)
local raw_user, err = mongo.first("api_users", {
query = {
api_key = api_key,
},
})

if err then
ngx.log(ngx.ERR, "failed to fetch user from mongodb: ", err)
local raw_user
local db_err
local pep_err

-- Checking the field of api_key ["key_type"], if the key_type is api_key
-- the api_key value is checked in the database and retrieve the user information
-- else if the key_type is token, the token is checked using PEP Proxy and
-- the user information is retrieved
if not api_key["key_type"] or api_key["key_type"] == "api_key" then
raw_user, db_err = mongo.first("api_users", {
query = {
api_key = api_key["key_value"],
},
})
elseif api_key["key_type"] == "token" then
raw_user, pep_err = pep.first(config["gatekeeper"]["pep_host"],config["gatekeeper"]["pep_port"],api_key["key_value"])
end
if pep_err then
ngx.log(ngx.ERR, "failed to autenticate , status code:", pep_err)
elseif db_err then
ngx.log(ngx.ERR, "failed to fetch user from mongodb", db_err)
elseif raw_user then
local user = utils.pick_where_present(raw_user, {
"created_at",
Expand All @@ -36,15 +50,31 @@ local function lookup_user(api_key)
-- Ensure IDs get stored as strings, even if Mongo ObjectIds are in use.
if raw_user["_id"] and raw_user["_id"]["$oid"] then
user["id"] = raw_user["_id"]["$oid"]
elseif raw_user.Nick_Name then
user["id"] = raw_user.Nick_Name
if not raw_user.Email then
user["email"] = raw_user.Nick_Name
else
user["email"] = raw_user.Email
end
else
user["id"] = raw_user["_id"]
user["id"] = raw_user["_id"]
end
-- If the validation was made using a token, the Nick_Name associate to the token
-- is assigned to the id attribute of the user
if raw_user.Nick_Name then
user["id"] = raw_user.Nick_Name
end

-- Invert the array of roles into a hashy table for more optimized
-- lookups (so we can just check if the key exists, rather than
-- looping over each value).
-- Moreover, in case that the user information have been retrieved using a token validation,
-- the roles associated with the token are stored in user ["roles"]
if user["roles"] then
user["roles"] = invert_table(user["roles"])
elseif raw_user.Roles then
user["roles"] = invert_table(raw_user.Roles)
end

if user["created_at"] and user["created_at"]["$date"] then
Expand Down Expand Up @@ -103,14 +133,14 @@ function _M.get(api_key)
return nil
end

user = shared_cache:load(api_key)
user = shared_cache:load(api_key["key_value"])
if user then
local_cache:set(api_key, user, 2)
local_cache:set(api_key["key_value"], user, 2)
else
local_cache:set(api_key, EMPTY_DATA, 2)
local_cache:set(api_key["key_value"], EMPTY_DATA, 2)
end

return user
end

return _M
return _M
16 changes: 13 additions & 3 deletions src/api-umbrella/utils/invert_table.lua
Original file line number Diff line number Diff line change
@@ -1,8 +1,18 @@
return function(table)
local numItems = 0
local inverted = {}
for key, value in pairs(table) do
inverted[value] = key
if type(value)=="string" then
inverted[value] = key
else
for k,v in pairs(value) do
numItems = numItems + 1
end
if numItems > 1 then
value = value["name"]
end
inverted[value] = key
end
end

return inverted
end
end
27 changes: 27 additions & 0 deletions src/api-umbrella/utils/pep.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
local http = require "resty.http"
local cjson = require "cjson"
local _M = {}
-- Function to connect with the Pep Proxy service for checking if the token is valid and retrieve
-- the user properties. The function takes the PEP Proxy host and port as parameters
-- and sends a request with the header X-Auth-Token with the value of the token provided
-- by the user. If the token is valid, PEP proxy sends a response with the user information
-- asociated to the token, otherwise, it sends a message indicating the result of the
-- validation process with his status, 404 , 402, etc.
function _M.first(host, port, token)
local result
local httpc = http.new()
httpc:set_timeout(45000)
httpc:connect(host,port)
local res, err = httpc:request({headers = {["X-Auth-Token"] = token}})
if res and res.status == 200 then
local body, body_err = res:read_body()
if not body then
return nil, body_err
end
result = cjson.decode(body)
end

return result, err
end

return _M