Skip to content

Commit

Permalink
Added support for runtime customizaiton of the oauth token validator (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
cristianconstantin authored Sep 7, 2016
1 parent 2526999 commit 21ba004
Show file tree
Hide file tree
Showing 5 changed files with 132 additions and 44 deletions.
4 changes: 2 additions & 2 deletions src/lua/api-gateway/validation/factory.lua
Original file line number Diff line number Diff line change
Expand Up @@ -101,8 +101,8 @@ local function _generateHmacSignature()
return hmacSignatureValidator:generateSignature()
end

local function _validateOAuthToken()
local oauthTokenValidator = OAuthTokenValidator:new()
local function _validateOAuthToken(config)
local oauthTokenValidator = OAuthTokenValidator:new(config)
return oauthTokenValidator:validateRequest()
end

Expand Down
62 changes: 32 additions & 30 deletions src/lua/api-gateway/validation/oauth2/oauthTokenValidator.lua
Original file line number Diff line number Diff line change
Expand Up @@ -41,24 +41,24 @@
local BaseValidator = require "api-gateway.validation.validator"
local cjson = require "cjson"

local _M = BaseValidator:new()

local RESPONSES = {
MISSING_TOKEN = { error_code = "403010", message = "Oauth token is missing" },
INVALID_TOKEN = { error_code = "401013", message = "Oauth token is not valid" },
-- TOKEN_MISSMATCH is reserved for classes overwriting the isTokenValid method
TOKEN_MISSMATCH = { error_code = "401014", message = "Token not allowed in the current context" },
SCOPE_MISMATCH = { error_code = "401015", message = "Scope mismatch" },
UNKNOWN_ERROR = { error_code = "503010", message = "Could not validate the oauth token" }
}
local _M = BaseValidator:new({
RESPONSES = {
MISSING_TOKEN = { error_code = "403010", message = "Oauth token is missing" },
INVALID_TOKEN = { error_code = "401013", message = "Oauth token is not valid" },
-- TOKEN_MISSMATCH is reserved for classes overwriting the isTokenValid method
TOKEN_MISSMATCH = { error_code = "401014", message = "Token not allowed in the current context" },
SCOPE_MISMATCH = { error_code = "401015", message = "Scope mismatch" },
UNKNOWN_ERROR = { error_code = "503010", message = "Could not validate the oauth token" }
}
})

---
-- Maximum time in seconds specifying how long to cache a valid token in GW's memory
local LOCAL_CACHE_TTL = 60

-- Hook to override the logic verifying if a token is valid
function _M:istokenValid(json)
return json.valid or false, RESPONSES.INVALID_TOKEN
function _M:isTokenValid(json)
return json.valid or false, self.RESPONSES.INVALID_TOKEN
end

-- override this if other checks need to be in place
Expand Down Expand Up @@ -133,7 +133,7 @@ function _M:checkResponseFromAuth(res, cacheLookupKey)
local json = cjson.decode(res.body)
if json ~= nil then

local tokenValidity, error = self:istokenValid(json)
local tokenValidity, error = self:isTokenValid(json)
if not tokenValidity and error ~= nil then
return tokenValidity, error
end
Expand Down Expand Up @@ -166,14 +166,13 @@ function _M:getTokenFromCache(cacheLookupKey)
return nil;
end

-- imsAuth will validate the service token passed in "Authorization" header --
function _M:validate_ims_token()
function _M:validateOAuthToken()

local oauth_host = ngx.var.oauth_host
local oauth_token = ngx.var.authtoken
local oauth_token = self.authtoken or ngx.var.authtoken

-- ngx.var.authtoken needs to be set before calling this method
if oauth_token == nil or oauth_token == "" then
return self:exitFn(RESPONSES.MISSING_TOKEN.error_code, cjson.encode(RESPONSES.MISSING_TOKEN))
return self.RESPONSES.MISSING_TOKEN.error_code, cjson.encode(self.RESPONSES.MISSING_TOKEN)
end

--1. try to get token info from the cache first ( local or redis cache )
Expand All @@ -190,37 +189,40 @@ function _M:validate_ims_token()
ngx.log(ngx.DEBUG, "Caching locally a new token for " .. tostring(local_expire_in) .. " s, out of a total validity of " .. tostring(tokenValidity ) .. " s.")
self:setKeyInLocalCache(cacheLookupKey, cachedToken, local_expire_in , "cachedOauthTokens")
self:setContextProperties(obj)
return self:exitFn(ngx.HTTP_OK)
return ngx.HTTP_OK
end
-- at this point the cached token is not valid
ngx.log(ngx.WARN, "Invalid OAuth Token found in cache. OAuth host=" .. tostring(oauth_host))
if (error == nil) then
error = RESPONSES.INVALID_TOKEN
error = self.RESPONSES.INVALID_TOKEN
end
error.error_code = error.error_code or RESPONSES.INVALID_TOKEN.error_code
return self:exitFn(error.error_code, cjson.encode(error))
error.error_code = error.error_code or self.RESPONSES.INVALID_TOKEN.error_code
return error.error_code, cjson.encode(error)
end

-- 2. validate the token with the OAuth endpoint
local res = ngx.location.capture("/validate-token", { share_all_vars = true })
local res = ngx.location.capture("/validate-token", {
share_all_vars = true,
args = { authtoken = oauth_token}
})
if res.status == ngx.HTTP_OK then
local tokenValidity, error = self:checkResponseFromAuth(res, cacheLookupKey)
if (tokenValidity == true) then
return self:exitFn(ngx.HTTP_OK)
return ngx.HTTP_OK
end
-- at this point the token is not valid
ngx.log(ngx.WARN, "Invalid OAuth Token returned. OAuth host=" .. tostring(oauth_host))
if (error == nil) then
error = RESPONSES.INVALID_TOKEN
error = self.RESPONSES.INVALID_TOKEN
end
error.error_code = error.error_code or RESPONSES.INVALID_TOKEN.error_code
return self:exitFn(error.error_code, cjson.encode(error))
error.error_code = error.error_code or self.RESPONSES.INVALID_TOKEN.error_code
return error.error_code, cjson.encode(error)
end
return self:exitFn(res.status, cjson.encode(RESPONSES.UNKNOWN_ERROR));
return res.status, cjson.encode(self.RESPONSES.UNKNOWN_ERROR);
end

function _M:validateRequest(obj)
return self:validate_ims_token()
function _M:validateRequest()
return self:exitFn(self:validateOAuthToken())
end


Expand Down
21 changes: 12 additions & 9 deletions src/lua/api-gateway/validation/oauth2/userProfileValidator.lua
Original file line number Diff line number Diff line change
Expand Up @@ -186,12 +186,11 @@ function _M:extractContextVars(profile)
return cachingObj
end

function _M:validateRequest()
function _M:validateUserProfile()
-- ngx.var.authtoken needs to be set before calling this method
local oauth_token = ngx.var.authtoken
if oauth_token == nil or oauth_token == "" then
--return self:exitFn(ngx.HTTP_BAD_REQUEST)
return self:exitFn(RESPONSES.P_MISSING_TOKEN.error_code, cjson.encode(RESPONSES.P_MISSING_TOKEN))
return RESPONSES.P_MISSING_TOKEN.error_code, cjson.encode(RESPONSES.P_MISSING_TOKEN)
end

--1. try to get user's profile from the cache first ( local or redis cache )
Expand All @@ -205,9 +204,9 @@ function _M:validateRequest()
end
self:setContextProperties(self:getContextPropertiesObject(cachedUserProfile))
if ( self:isProfileValid(cachedUserProfile) == true ) then
return self:exitFn(ngx.HTTP_OK)
return ngx.HTTP_OK
else
return self:exitFn(RESPONSES.INVALID_PROFILE.error_code, cjson.encode(RESPONSES.INVALID_PROFILE))
return RESPONSES.INVALID_PROFILE.error_code, cjson.encode(RESPONSES.INVALID_PROFILE)
end
end

Expand All @@ -223,9 +222,9 @@ function _M:validateRequest()
self:storeProfileInCache(cacheLookupKey, cachingObj)

if ( self:isProfileValid(cachingObj) == true ) then
return self:exitFn(ngx.HTTP_OK)
return ngx.HTTP_OK
else
return self:exitFn(RESPONSES.INVALID_PROFILE.error_code, cjson.encode(RESPONSES.INVALID_PROFILE))
return RESPONSES.INVALID_PROFILE.error_code, cjson.encode(RESPONSES.INVALID_PROFILE)
end
else
ngx.log(ngx.WARN, "Could not decode /validate-user response:" .. tostring(res.body) )
Expand All @@ -234,11 +233,15 @@ function _M:validateRequest()
-- ngx.log(ngx.WARN, "Could not read /ims-profile. status=" .. res.status .. ".body=" .. res.body .. ". token=" .. ngx.var.authtoken)
ngx.log(ngx.WARN, "Could not read /validate-user. status=" .. res.status .. ".body=" .. res.body )
if ( res.status == ngx.HTTP_UNAUTHORIZED or res.status == ngx.HTTP_BAD_REQUEST ) then
return self:exitFn(RESPONSES.NOT_ALLOWED.error_code, cjson.encode(RESPONSES.NOT_ALLOWED))
return RESPONSES.NOT_ALLOWED.error_code, cjson.encode(RESPONSES.NOT_ALLOWED)
end
end
--ngx.log(ngx.WARN, "Error validating Profile for Token:" .. tostring(ngx.var.authtoken))
return self:exitFn(RESPONSES.P_UNKNOWN_ERROR.error_code, cjson.encode(RESPONSES.P_UNKNOWN_ERROR))
return RESPONSES.P_UNKNOWN_ERROR.error_code, cjson.encode(RESPONSES.P_UNKNOWN_ERROR)
end

function _M:validateRequest()
return self:exitFn(self:validateUserProfile())
end

return _M
34 changes: 32 additions & 2 deletions src/lua/api-gateway/validation/validator.lua
Original file line number Diff line number Diff line change
Expand Up @@ -91,14 +91,44 @@ function BaseValidator:getRedisUpstream(upstream_name)
end

-- retrieves a saved information from the Redis cache --
-- the method uses HGET redis command --
-- the method uses GET redis command --
-- it returns the value of the key, when found in the cache, nil otherwise --
-- for backward compatibility this method accepts a second argument, in which case it will perform a HGET instead.
function BaseValidator:getKeyFromRedis(key, hash_name)

if hash_name ~= nil then
return self:getHashValueFromRedis(key, hash_name)
end

local redisread = redis:new()
local redis_host, redis_port = self:getRedisUpstream(redis_RO_upstream)
local ok, err = redisread:connect(redis_host, redis_port)
if ok then
local result, err = redisread:get(key)
redisread:set_keepalive(30000, 100)
if ( not result and err ~= nil ) then
ngx.log(ngx.WARN, "Failed to read key " .. tostring(key) .. " from Redis cache:[", redis_host, ":", redis_port, "]. Error:", err)
return nil
else
if (type(result) == 'string') then
return result
end
end
else
ngx.log(ngx.WARN, "Failed to read key " .. tostring(key) .. " from Redis cache:[", redis_host, ":", redis_port, "]. Error:", err)
end
return nil;
end

-- retrieves a saved information from the Redis cache --
-- the method uses HGET redis command --
-- it returns the value of the key, when found in the cache, nil otherwise --
function BaseValidator:getHashValueFromRedis(key, hash_field)
local redisread = redis:new()
local redis_host, redis_port = self:getRedisUpstream(redis_RO_upstream)
local ok, err = redisread:connect(redis_host, redis_port)
if ok then
local redis_key, selecterror = redisread:hget(key, hash_name)
local redis_key, selecterror = redisread:hget(key, hash_field)
redisread:set_keepalive(30000, 100)
if (type(redis_key) == 'string') then
return redis_key
Expand Down
55 changes: 54 additions & 1 deletion test/perl/api-gateway/validation/oauth2/oauthTokenValidator.t
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ use Cwd qw(cwd);

repeat_each(2);

plan tests => repeat_each() * (blocks() * 9);
plan tests => repeat_each() * (blocks() * 9) - 6;

my $pwd = cwd();

Expand Down Expand Up @@ -350,3 +350,56 @@ GET /test-oauth-validation
--- no_error_log
[error]
=== TEST 6: test that validation behaviour can be customized
--- http_config eval: $::HttpConfig
--- config
include ../../api-gateway/default_validators.conf;
error_log ../test-logs/oauthTokenValidator_test6_error.log debug;
location /validate_custom_oauth_token {
internal;
content_by_lua_block {
ngx.apiGateway.validation.validateOAuthToken({
authtoken = ngx.var.custom_token_var,
RESPONSES = {
MISSING_TOKEN = { error_code = "401110", message = "User token is missing" },
INVALID_TOKEN = { error_code = "403113", message = "User token is not valid" },
TOKEN_MISSMATCH = { error_code = "401114", message = "User token not allowed in the current context" },
SCOPE_MISMATCH = { error_code = "401115", message = "User token scope mismatch" },
UNKNOWN_ERROR = { error_code = "503110", message = "Could not validate the user token" }
}
});
}
}
location /test-custom-oauth {
set $validate_oauth_token "on; path=/validate_custom_oauth_token; order=1;";
set $custom_token_var $arg_custom_token;
access_by_lua "ngx.apiGateway.validation.validateRequest()";
content_by_lua "ngx.say('ims token is valid.')";
}
location /validate-token {
internal;
set_by_lua $generated_expires_at 'return ((os.time() + 4) * 1000 )';
return 200 '{"valid":false,"expires_at":$generated_expires_at,"token":{"id":"1234","scope":"openid email profile","user_id":"21961FF44F97F8A10A490D36","expires_in":"86400000","client_id":"test_Client_ID","type":"access_token"}}';
}
--- pipelined_requests eval
[
"GET /test-custom-oauth",
"GET /test-custom-oauth?custom_token=SOME_OAUTH_TOKEN_TEST6"
]
--- response_body_like eval
[
'^{"error_code":"401110","message":"User token is missing"}+',
'^{"error_code":"403113","message":"User token is not valid"}+'
]
--- error_code_like eval
[401,403]
--- no_error_log
[error]

0 comments on commit 21ba004

Please sign in to comment.