OAuth fixes and improvements (#353)
* Fix App ID caching issue (and add tests to match)
* Improve error messages for all OAuth providers
diff --git a/Dockerfile.test.unit b/Dockerfile.test.unit
index 598a038..7038c9f 100644
--- a/Dockerfile.test.unit
+++ b/Dockerfile.test.unit
@@ -24,11 +24,13 @@
FROM alpine:3.9
+ENV CJOSE_VERSION=0.5.1
+
RUN apk update && \
apk add \
gcc tar zlib wget make musl-dev g++ curl \
libtool readline luajit luajit-dev unzip \
- openssl openssl-dev
+ openssl openssl-dev git jansson jansson-dev
WORKDIR /tmp
RUN wget https://luarocks.org/releases/luarocks-3.1.3.tar.gz && \
@@ -38,6 +40,15 @@
make build && \
make install
+RUN echo " ... installing cjose ... " \
+ && mkdir -p /tmp/api-gateway \
+ && curl -L -k https://github.com/cisco/cjose/archive/${CJOSE_VERSION}.tar.gz -o /tmp/api-gateway/cjose-${CJOSE_VERSION}.tar.gz \
+ && tar -xf /tmp/api-gateway/cjose-${CJOSE_VERSION}.tar.gz -C /tmp/api-gateway/ \
+ && cd /tmp/api-gateway/cjose-${CJOSE_VERSION} \
+ && sh configure \
+ && make && make install \
+ && rm -rf /tmp/api-gateway
+
COPY . /etc/api-gateway
WORKDIR /etc/api-gateway/tests
diff --git a/scripts/lua/oauth/app-id.lua b/scripts/lua/oauth/app-id.lua
index 7b5af21..a756f64 100644
--- a/scripts/lua/oauth/app-id.lua
+++ b/scripts/lua/oauth/app-id.lua
@@ -23,26 +23,39 @@
local http = require 'resty.http'
local cjose = require 'resty.cjose'
-function _M.process(dataStore, token, securityObj)
- local result = dataStore:getOAuthToken('appId', token)
- local httpc = http.new()
- local json_resp
- if result ~= ngx.null then
- json_resp = cjson.decode(result)
- ngx.header['X-OIDC-Email'] = json_resp['email']
- ngx.header['X-OIDC-Sub'] = json_resp['sub']
- return json_resp
- end
- local keyUrl = utils.concatStrings({APPID_PKURL, securityObj.tenantId, '/publickeys'})
+local function inject_req_headers(token_obj)
+ ngx.header['X-OIDC-Email'] = token_obj['email']
+ ngx.header['X-OIDC-Sub'] = token_obj['sub']
+end
+
+local function fetchJWKs(tenantId)
+ local keyUrl = utils.concatStrings({APPID_PKURL, tenantId, '/publickeys'})
local request_options = {
headers = {
["Accept"] = "application/json"
},
- ssl_verify = false
+ ssl_verify = true
}
- local res, err = httpc:request_uri(keyUrl, request_options)
- if err then
- request.err(500, 'error getting app id key: ' .. err)
+ return httpc:request_uri(keyUrl, request_options)
+end
+
+function _M.process(dataStore, token, securityObj)
+ local cache_key = 'appid_' .. securityObj.tenantId
+ local result = dataStore:getOAuthToken(cache_key, token)
+ local httpc = http.new()
+ local token_obj
+
+ -- Was the token in the cache?
+ if result ~= ngx.null then
+ token_obj = cjson.decode(result)
+ inject_req_headers(token_obj)
+ return token_obj
+ end
+
+ -- Cache miss. Proceed to validate the token
+ local res, err = fetchJWKs
+ if err or res.status ~= 200 then
+ request.err(500, 'An error occurred while fetching the App ID JWK configuration: ' .. err or res.body)
end
local key
@@ -52,24 +65,26 @@
end
local result = cjose.validateJWS(token, cjson.encode(key))
if not result then
- request.err(401, 'AppId key signature verification failed.')
+ request.err(401, 'The token signature did not match any known JWK.')
return nil
end
- local jwt_obj = cjson.decode(cjose.getJWSInfo(token))
- local expireTime = jwt_obj['exp']
+
+ token_obj = cjson.decode(cjose.getJWSInfo(token))
+ local expireTime = token_obj['exp']
if expireTime < os.time() then
- request.err(401, 'Access token expired.')
+ request.err(401, 'The access token has expired.')
return nil
end
- ngx.header['X-OIDC-Email'] = jwt_obj['email']
- ngx.header['X-OIDC-Sub'] = jwt_obj['sub']
+
+ -- Add token metadata to the request headers
+ inject_req_headers(token_obj)
+
-- keep token in cache until it expires
local ttl = expireTime - os.time()
- dataStore:saveOAuthToken('appId', token, cjson.encode(jwt_obj), ttl)
- return jwt_obj
+ dataStore:saveOAuthToken(cache_key, token, cjson.encode(token_obj), ttl)
+ return token_obj
end
-
return _M
diff --git a/scripts/lua/oauth/facebook.lua b/scripts/lua/oauth/facebook.lua
index f23ddf9..a27b295 100644
--- a/scripts/lua/oauth/facebook.lua
+++ b/scripts/lua/oauth/facebook.lua
@@ -56,7 +56,7 @@
-- convert response
if not res then
ngx.log(ngx.WARN, 'Could not invoke Facebook API. Error=', err)
- request.err(500, 'OAuth provider error.')
+ request.err(500, 'Connection to the OAuth provider failed.')
return
end
local json_resp = cjson.decode(res.body)
diff --git a/scripts/lua/oauth/github.lua b/scripts/lua/oauth/github.lua
index f4f2e9b..51addf1 100644
--- a/scripts/lua/oauth/github.lua
+++ b/scripts/lua/oauth/github.lua
@@ -45,7 +45,7 @@
-- convert response
if not res then
ngx.log(ngx.WARN, utils.concatStrings({"Could not invoke Github API. Error=", err}))
- request.err(500, 'OAuth provider error.')
+ request.err(500, 'Connection to the OAuth provider failed.')
return
end
diff --git a/scripts/lua/oauth/google.lua b/scripts/lua/oauth/google.lua
index b6892b3..bd8e11b 100644
--- a/scripts/lua/oauth/google.lua
+++ b/scripts/lua/oauth/google.lua
@@ -50,7 +50,7 @@
-- convert response
if not res then
ngx.log(ngx.WARN, utils.concatStrings({"Could not invoke Google API. Error=", err}))
- request.err(500, 'OAuth provider error.')
+ request.err(500, 'Connection to the OAuth provider failed.')
return nil
end
local json_resp = cjson.decode(res.body)
diff --git a/scripts/lua/policies/security/oauth2.lua b/scripts/lua/policies/security/oauth2.lua
index 21a8e81..6b58bf9 100644
--- a/scripts/lua/policies/security/oauth2.lua
+++ b/scripts/lua/policies/security/oauth2.lua
@@ -66,7 +66,7 @@
local loaded, impl = pcall(require, utils.concatStrings({'oauth/', provider}))
if not loaded then
request.err(500, 'Error loading OAuth provider authentication module')
- print("error loading provider.")
+ print("error loading provider:", impl)
return nil
end
diff --git a/tests/install-deps.sh b/tests/install-deps.sh
index 74f124c..156cb3f 100755
--- a/tests/install-deps.sh
+++ b/tests/install-deps.sh
@@ -28,3 +28,5 @@
luarocks install --tree=lua_modules md5
luarocks install --tree=lua_modules net-url
luarocks install --tree=lua_modules luafilesystem
+luarocks install --tree=lua_modules lua-resty-http 0.10
+luarocks install --tree=lua_modules https://github.com/mhamann/lua-resty-cjose/raw/master/lua-resty-cjose-0.5-0.rockspec
diff --git a/tests/scripts/lua/security.lua b/tests/scripts/lua/security.lua
index 89533fd..e449ff9 100644
--- a/tests/scripts/lua/security.lua
+++ b/tests/scripts/lua/security.lua
@@ -213,6 +213,7 @@
assert.same(red:exists('oauth:providers:mock:tokens:test'), 1)
assert(result)
end)
+
it('Exchanges a bad token, doesn\'t cache it and returns false', function()
local red = fakeredis.new()
local token = "bad"
@@ -237,31 +238,34 @@
assert.same(red:exists('oauth:providers:mock:tokens:bad'), 0)
assert.falsy(result)
end)
- it('Loads a facebook token from the cache with a valid app id', function()
+
+ it('Has no cross-contamination between App ID caches', function()
local red = fakeredis.new()
local ds = require "lib/dataStore"
local dataStore = ds.initWithDriver(red)
- local token = "test"
+ local token = "test_token"
local appid = "app"
local ngxattrs = [[
{
"http_Authorization":"]] .. token .. [[",
- "http_x_facebook_app_token":"]] .. appid .. [[",
"tenant":"1234",
"gatewayPath":"v1/test"
}
]]
local ngx = fakengx.new()
+ ngx.config = { ngx_lua_version = 'test' }
ngx.var = cjson.decode(ngxattrs)
_G.ngx = ngx
local securityObj = [[
{
"type":"oauth2",
- "provider":"facebook",
- "scope":"resource"
+ "provider":"app-id",
+ "tenantId": "tenant1",
+ "scope":"api"
}
]]
- red:set('oauth:providers:facebook:tokens:testapp', '{"token":"good"}')
+ red:set('oauth:providers:appid_tenant2:tokens:test_token', '{"token":"good"}')
+ red:set('oauth:providers:appid_tenant1:tokens:test_token', '{"token":"good"}')
local result = oauth.process(dataStore, cjson.decode(securityObj))
assert.truthy(result)
end)