diff --git a/src/lua/api-gateway/validation/factory.lua b/src/lua/api-gateway/validation/factory.lua index 0c767aa..06d9458 100644 --- a/src/lua/api-gateway/validation/factory.lua +++ b/src/lua/api-gateway/validation/factory.lua @@ -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 diff --git a/src/lua/api-gateway/validation/oauth2/oauthTokenValidator.lua b/src/lua/api-gateway/validation/oauth2/oauthTokenValidator.lua index 0fbfe9a..36f19cf 100644 --- a/src/lua/api-gateway/validation/oauth2/oauthTokenValidator.lua +++ b/src/lua/api-gateway/validation/oauth2/oauthTokenValidator.lua @@ -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 @@ -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 @@ -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 ) @@ -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 diff --git a/src/lua/api-gateway/validation/oauth2/userProfileValidator.lua b/src/lua/api-gateway/validation/oauth2/userProfileValidator.lua index be1d479..14b057f 100644 --- a/src/lua/api-gateway/validation/oauth2/userProfileValidator.lua +++ b/src/lua/api-gateway/validation/oauth2/userProfileValidator.lua @@ -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 ) @@ -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 @@ -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) ) @@ -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 \ No newline at end of file diff --git a/src/lua/api-gateway/validation/validator.lua b/src/lua/api-gateway/validation/validator.lua index bac55ca..39c4a62 100644 --- a/src/lua/api-gateway/validation/validator.lua +++ b/src/lua/api-gateway/validation/validator.lua @@ -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 diff --git a/test/perl/api-gateway/validation/oauth2/oauthTokenValidator.t b/test/perl/api-gateway/validation/oauth2/oauthTokenValidator.t index db3fda4..ae63803 100644 --- a/test/perl/api-gateway/validation/oauth2/oauthTokenValidator.t +++ b/test/perl/api-gateway/validation/oauth2/oauthTokenValidator.t @@ -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(); @@ -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] +