diff --git a/README.md b/README.md index 6bd933e..2b97ef9 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ It also externalizes introspection and caching from APIs, to keep the API securi If you are using luarocks, execute the following command to install the plugin: ```bash -luarocks install kong-phantom-token 2.0.1 +luarocks install kong-phantom-token 3.0.0 ``` Or deploy the .lua files into Kong's plugin directory, eg `/usr/local/share/lua/5.1/kong/plugins/phantom-token`. @@ -30,7 +30,7 @@ Or deploy the .lua files into Kong's plugin directory, eg `/usr/local/share/lua/ If you are using luarocks, execute the following command to install the plugin: ```bash -luarocks install lua-resty-phantom-token 2.0.1 +luarocks install lua-resty-phantom-token 3.0.0 ``` Or deploy the `access.lua` file to `resty/phantom-token.lua`, where the resty folder is in the `lua_package_path`.\ @@ -157,7 +157,7 @@ Then apply the plugin to one or more locations with configuration similar to the ```nginx location ~ ^/api { - rewrite_by_lua_block { + access_by_lua_block { local config = { introspection_endpoint = 'https://login.example.com/oauth/v2/oauth-introspect', diff --git a/curity-test-config.xml b/curity-test-config.xml index fd31032..19f0de1 100644 --- a/curity-test-config.xml +++ b/curity-test-config.xml @@ -151,6 +151,8 @@ default-datasource jdbc:hsqldb:file:${se.curity:identity-server:db};ifexists=true;hsqldb.lock_file=false + + org.hsqldb.jdbc.JDBCDriver SA diff --git a/docker/deploy.sh b/docker/deploy.sh index 358ce7e..b771552 100755 --- a/docker/deploy.sh +++ b/docker/deploy.sh @@ -46,11 +46,11 @@ fi # if [ "$PROFILE" == 'kong' ]; then - docker build -f docker/kong/Dockerfile --no-cache -t custom_kong:3.0.0-alpine . + docker build -f docker/kong/Dockerfile --no-cache -t custom_kong:3.9.0-ubuntu . elif [ "$PROFILE" == 'openresty' ]; then - docker build -f docker/openresty/Dockerfile --no-cache -t custom_openresty:1.21.4.1-bionic . + docker build -f docker/openresty/Dockerfile --no-cache -t custom_openresty:1.31.1.1-bookworm . fi if [ $? -ne 0 ]; then echo "Problem encountered building the reverse proxy docker image" diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 4138b52..3cc9981 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -4,7 +4,7 @@ services: # Use Kong Open Source as the reverse proxy when the kong profile is set on the command line # kong: - image: custom_kong:3.0.0-alpine + image: custom_kong:3.9.0-ubuntu hostname: kongserver ports: - 3000:3000 @@ -24,7 +24,7 @@ services: # Use OpenResty as the reverse proxy when the openresty profile is set on the command line # openresty: - image: custom_openresty:1.21.4.1-bionic + image: custom_openresty:1.31.1.1-bookworm hostname: openrestyserver ports: - 3000:3000 @@ -49,7 +49,7 @@ services: # The Curity Identity Server is deployed for all test configurations # curity: - image: curity.azurecr.io/curity/idsvr + image: curity.azurecr.io/curity/idsvr:latest hostname: curityserver ports: - 6749:6749 diff --git a/docker/kong/Dockerfile b/docker/kong/Dockerfile index 91835b8..bcc7a4a 100644 --- a/docker/kong/Dockerfile +++ b/docker/kong/Dockerfile @@ -1,4 +1,4 @@ -FROM kong:3.0.0-alpine +FROM kong:3.9-ubuntu # Deploy the plugin and dependencies for local testing USER root diff --git a/docker/openresty/Dockerfile b/docker/openresty/Dockerfile index 6044595..4ecb481 100644 --- a/docker/openresty/Dockerfile +++ b/docker/openresty/Dockerfile @@ -1,4 +1,4 @@ -FROM openresty/openresty:1.21.4.1-bionic +FROM openresty/openresty:1.31.1.1-bookworm-fat # Deploy the plugin and dependencies for local testing COPY ./lua*.rockspec /tmp/phantom-token/ diff --git a/docker/openresty/nginx.conf b/docker/openresty/nginx.conf index b2ebfdb..ee6cd78 100644 --- a/docker/openresty/nginx.conf +++ b/docker/openresty/nginx.conf @@ -32,7 +32,7 @@ http { resolver 127.0.0.11; # If required, introspect an opaque access token and forward a JWT to the API - rewrite_by_lua_block { + access_by_lua_block { local config = { introspection_endpoint = 'http://curityserver:8443/oauth/v2/oauth-introspect', @@ -42,7 +42,17 @@ http { } local phantomToken = require 'resty.phantom-token' - phantomToken.run(config) + local ok, err = phantomToken.validate(config) + if ok then + phantomToken.run(config) + else + + ngx.log(ngx.ERR, err) + ngx.status = ngx.HTTP_INTERNAL_SERVER_ERROR + ngx.header.content_type = 'application/json' + ngx.say('{"code":"server_error","message":"Problem encountered processing the request"}') + return ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR) + end } # Then proxy the updated request diff --git a/kong-phantom-token-2.0.1-1.rockspec b/kong-phantom-token-3.0.0-1.rockspec similarity index 97% rename from kong-phantom-token-2.0.1-1.rockspec rename to kong-phantom-token-3.0.0-1.rockspec index a9f5094..c794cf2 100644 --- a/kong-phantom-token-2.0.1-1.rockspec +++ b/kong-phantom-token-3.0.0-1.rockspec @@ -1,8 +1,8 @@ package = "kong-phantom-token" -version = "2.0.1-1" +version = "3.0.0-1" source = { url = "git://github.com/curityio/nginx-lua-phantom-token-plugin", - tag = "v2.0.1" + tag = "v3.0.0" } description = { summary = "A Lua plugin used during API requests to exchange an opaque reference token for a JWT access token", diff --git a/lua-resty-phantom-token-2.0.1-1.rockspec b/lua-resty-phantom-token-3.0.0-1.rockspec similarity index 97% rename from lua-resty-phantom-token-2.0.1-1.rockspec rename to lua-resty-phantom-token-3.0.0-1.rockspec index 2d9ed82..4b18fea 100644 --- a/lua-resty-phantom-token-2.0.1-1.rockspec +++ b/lua-resty-phantom-token-3.0.0-1.rockspec @@ -1,8 +1,8 @@ package = "lua-resty-phantom-token" -version = "2.0.1-1" +version = "3.0.0-1" source = { url = "git://github.com/curityio/nginx-lua-phantom-token-plugin", - tag = "v2.0.1" + tag = "v3.0.0" } description = { summary = "A Lua plugin used during API requests to exchange an opaque reference token for a JWT access token", diff --git a/plugin/access.lua b/plugin/access.lua index e1af18e..dda1b78 100644 --- a/plugin/access.lua +++ b/plugin/access.lua @@ -36,36 +36,10 @@ local function array_has_value(arr, val) return false end --- --- Verify configuration and set defaults that are the same for all requests --- -local function initialize_configuration(config) - - if config == nil or - config.introspection_endpoint == nil or - config.client_id == nil or - config.client_secret == nil then - ngx.log(ngx.WARN, 'The phantom token configuration is invalid and must be corrected') - return false - end - - if config.token_cache_seconds == nil or config.token_cache_seconds <= 0 then - config.token_cache_seconds = 300 - end - if config.scope == nil then - config.scope = '' - end - if config.verify_ssl == nil then - config.verify_ssl = true - end - - return true -end - -- -- Return errors due to invalid tokens or introspection technical problems -- -local function error_response(status, code, message) +local function error_response(scheme, status, code, message) local method = ngx.req.get_method():upper() if method ~= 'HEAD' then @@ -73,7 +47,7 @@ local function error_response(status, code, message) ngx.status = status ngx.header['content-type'] = 'application/json' if status == 401 then - ngx.header['WWW-Authenticate'] = 'Bearer' + ngx.header['WWW-Authenticate'] = string.format('%s error="%s", error_description="%s"', scheme, code, message) end local jsonData = '{"code":"' .. code .. '","message":"' .. message .. '"}' @@ -86,12 +60,12 @@ end -- -- Return a generic message for all three of these error categories -- -local function unauthorized_error_response() - error_response(ngx.HTTP_UNAUTHORIZED, 'unauthorized', 'Missing, invalid or expired access token') +local function unauthorized_error_response(scheme) + error_response(scheme, ngx.HTTP_UNAUTHORIZED, 'invalid_token', 'Missing, invalid or expired access token') end -local function server_error_response(config) - error_response(ngx.HTTP_INTERNAL_SERVER_ERROR, 'server_error', 'Problem encountered processing the request') +local function server_error_response() + error_response('', ngx.HTTP_INTERNAL_SERVER_ERROR, 'server_error', 'Problem encountered processing the request') end -- @@ -224,45 +198,101 @@ local function verify_access_token(access_token, config) end -- --- The public entry point to introspect the token then forward the JWT to the API +-- Apply default configuration settings, e.g. when running in OpenResty -- -function _M.run(config) +local function apply_default_configuration(config) - -- Start by validating configuration - if initialize_configuration(config) == false then - server_error_response(config) - return + if config.token_cache_seconds == nil or config.token_cache_seconds <= 0 then + config.token_cache_seconds = 300 + end + + if config.verify_ssl == nil then + config.verify_ssl = true + end + + if not config.scheme then + config.scheme = "Bearer" + end +end + +-- +-- Validate incorrect configuration before running in OpenResty +-- +function _M.validate(config) + + if not config then + return nil, "The phantom token plugin requires configuration" + end + + if not config.client_id then + return nil, "The phantom token plugin requires a client_id parameter" + end + + if not config.client_secret then + return nil, "The phantom token plugin requires a client_secret parameter" + end + + if not config.introspection_endpoint or not config.introspection_endpoint:match("^https?://") then + return nil, "The phantom token plugin requires an introspection endpoint that starts with http:// or https://" end + if config.scheme and config.scheme ~= "Bearer" and config.scheme ~= "DPoP" then + return nil, "The phantom token plugin requires a scheme of Bearer or DPoP" + end + + return true +end + +-- +-- The public entry point to introspect the token then forward the JWT to the API +-- +function _M.run(config) + if ngx.req.get_method() == 'OPTIONS' then return end + apply_default_configuration(config) + local auth_header = ngx.req.get_headers()['Authorization'] - if auth_header and string.len(auth_header) > 7 and string.lower(string.sub(auth_header, 1, 7)) == 'bearer ' then + if not auth_header then + ngx.log(ngx.WARN, 'No HTTP Authorization header was found') + unauthorized_error_response(config.scheme) + end - local access_token_untrimmed = string.sub(auth_header, 8) - local access_token = string.gsub(access_token_untrimmed, "%s+", "") - local result = verify_access_token(access_token, config) - - if result.status == 500 then - error_response(ngx.HTTP_INTERNAL_SERVER_ERROR, 'server_error', 'Problem encountered authorizing the HTTP request') - end + -- Read either an Authorization: Bearer or Authorization: DPoP value + local scheme, access_token = auth_header:match("^%s*(%S+)%s+(.+)%s*$") + if not scheme or scheme:lower() ~= config.scheme:lower() then + ngx.log(ngx.WARN, 'No valid scheme was found in the HTTP Authorization header') + unauthorized_error_response(config.scheme) + end - if result.status == 403 then - error_response(ngx.HTTP_FORBIDDEN, 'forbidden', 'The token does not contain the required scope') - end + if not access_token then + ngx.log(ngx.WARN, 'No valid access token was found in the HTTP Authorization header') + unauthorized_error_response(config.scheme) + end - if result.status ~= 200 then - ngx.log(ngx.WARN, 'Received a ' .. result.status .. ' introspection response due to the access token being invalid or expired') - unauthorized_error_response() - end + local result = verify_access_token(access_token, config) - ngx.req.set_header('Authorization', 'Bearer ' .. result.jwt) - else + if result.status == 500 then + error_response(config.scheme, ngx.HTTP_INTERNAL_SERVER_ERROR, 'server_error', 'Problem encountered authorizing the HTTP request') + end - ngx.log(ngx.WARN, 'No valid access token was found in the HTTP Authorization header') - unauthorized_error_response() + if result.status == 403 then + error_response(config.scheme, ngx.HTTP_FORBIDDEN, 'forbidden', 'The token does not contain the required scope') + end + + if result.status ~= 200 then + ngx.log(ngx.WARN, 'Received a ' .. result.status .. ' introspection response due to the access token being invalid or expired') + unauthorized_error_response(config.scheme) + end + + -- Pass the JWT to the next stage for processing + ngx.req.set_header('Authorization', config.scheme .. ' ' .. result.jwt) + + -- For DPoP, make the original opaque access token available in a variable, to enable verification of the DPoP ath claim + if config.scheme == 'DPoP' then + ngx.var.original_access_token = access_token end end diff --git a/plugin/handler.lua b/plugin/handler.lua index 5ba2ec0..f9ff1a3 100644 --- a/plugin/handler.lua +++ b/plugin/handler.lua @@ -7,11 +7,11 @@ local access = require "kong.plugins.phantom-token.access" -- See https://github.com/Kong/kong/discussions/7193 for more about the PRIORITY field local PhantomToken = { PRIORITY = 1000, - VERSION = "2.0.1", + VERSION = "3.0.0", } -function PhantomToken:access(conf) - access.run(conf) +function PhantomToken:access(config) + access.run(config) end -return PhantomToken +return PhantomToken \ No newline at end of file diff --git a/plugin/schema.lua b/plugin/schema.lua index f1b9437..1f576b8 100644 --- a/plugin/schema.lua +++ b/plugin/schema.lua @@ -3,13 +3,15 @@ return { fields = {{ config = { type = "record", + required = true, fields = { - { introspection_endpoint = { type = "string", required = true } }, + { introspection_endpoint = { type = "string", required = true, match = "^https?://" } }, { client_id = { type = "string", required = true } }, { client_secret = { type = "string", required = true } }, - { token_cache_seconds = { type = "number", required = true, default = 300 } }, + { token_cache_seconds = { type = "number", required = false } }, { scope = { type = "string", required = false } }, - { verify_ssl = { type = "boolean", required = true, default = true } } + { verify_ssl = { type = "boolean", required = false } }, + { scheme = { type = "string", required = false, one_of = { "Bearer", "DPoP" } } } } }} } diff --git a/t/advanced_routing.t b/t/advanced_routing.t index 537a493..44e9028 100644 --- a/t/advanced_routing.t +++ b/t/advanced_routing.t @@ -62,7 +62,7 @@ location @loc_bypass { } location @loc_phantom_token { - rewrite_by_lua_block { + access_by_lua_block { local config = { introspection_endpoint = 'http://127.0.0.1:8443/oauth/v2/oauth-introspect', @@ -112,7 +112,7 @@ location @loc_bypass { } location @loc_phantom_token { - rewrite_by_lua_block { + access_by_lua_block { local config = { introspection_endpoint = 'http://127.0.0.1:8443/oauth/v2/oauth-introspect', diff --git a/t/api_requests.t b/t/api_requests.t index 6ef777c..e658d80 100644 --- a/t/api_requests.t +++ b/t/api_requests.t @@ -51,7 +51,7 @@ lua_shared_dict phantom-token 10m; --- config location /t { - rewrite_by_lua_block { + access_by_lua_block { local config = { introspection_endpoint = 'http://127.0.0.1:8443/oauth/v2/oauth-introspect', @@ -93,7 +93,7 @@ lua_shared_dict phantom-token 10m; --- config location /t { - rewrite_by_lua_block { + access_by_lua_block { local config = { introspection_endpoint = 'http://127.0.0.1:8443/oauth/v2/oauth-introspect', @@ -117,10 +117,12 @@ GET /t --- response_headers content-type: application/json -WWW-Authenticate: Bearer + +--- response_headers_like +WWW-Authenticate: ^Bearer --- response_body_like chomp -{"code":"unauthorized","message":"Missing, invalid or expired access token"} +{"code":"invalid_token","message":"Missing, invalid or expired access token"} === TEST_API_REQUEST_3: Sending no authorization header results in an access denied error ################################################# @@ -133,7 +135,7 @@ lua_shared_dict phantom-token 10m; --- config location /t { - rewrite_by_lua_block { + access_by_lua_block { local config = { introspection_endpoint = 'http://127.0.0.1:8443/oauth/v2/oauth-introspect', @@ -154,10 +156,12 @@ GET /t --- response_headers content-type: application/json -WWW-Authenticate: Bearer + +--- response_headers_like +WWW-Authenticate: ^Bearer --- response_body_like chomp -{"code":"unauthorized","message":"Missing, invalid or expired access token"} +{"code":"invalid_token","message":"Missing, invalid or expired access token"} === TEST_API_REQUEST_4: The wrong authorization scheme results in an access denied error ############################################################## @@ -170,7 +174,7 @@ lua_shared_dict phantom-token 10m; --- config location /t { - rewrite_by_lua_block { + access_by_lua_block { local config = { introspection_endpoint = 'http://127.0.0.1:8443/oauth/v2/oauth-introspect', @@ -194,10 +198,12 @@ GET /t --- response_headers content-type: application/json -WWW-Authenticate: Bearer + +--- response_headers_like +WWW-Authenticate: ^Bearer --- response_body_like chomp -{"code":"unauthorized","message":"Missing, invalid or expired access token"} +{"code":"invalid_token","message":"Missing, invalid or expired access token"} === TEST_API_REQUEST_5: A valid token with trash after results in an access denied error ###################################################################################### @@ -210,7 +216,7 @@ lua_shared_dict phantom-token 10m; --- config location /t { - rewrite_by_lua_block { + access_by_lua_block { local config = { introspection_endpoint = 'http://127.0.0.1:8443/oauth/v2/oauth-introspect', @@ -234,10 +240,12 @@ GET /t --- response_headers content-type: application/json -WWW-Authenticate: Bearer + +--- response_headers_like +WWW-Authenticate: ^Bearer --- response_body_like chomp -{"code":"unauthorized","message":"Missing, invalid or expired access token"} +{"code":"invalid_token","message":"Missing, invalid or expired access token"} === TEST_API_REQUEST_6: The bearer HTTP method can be in upper case ##################################################### @@ -250,7 +258,7 @@ lua_shared_dict phantom-token 10m; --- config location /t { - rewrite_by_lua_block { + access_by_lua_block { local config = { introspection_endpoint = 'http://127.0.0.1:8443/oauth/v2/oauth-introspect', @@ -292,7 +300,7 @@ lua_shared_dict phantom-token 10m; --- config location /t { - rewrite_by_lua_block { + access_by_lua_block { local config = { introspection_endpoint = 'http://127.0.0.1:8443/oauth/v2/oauth-introspect', @@ -334,7 +342,7 @@ lua_shared_dict phantom-token 10m; --- config location /t { - rewrite_by_lua_block { + access_by_lua_block { local config = { introspection_endpoint = 'http://127.0.0.1:8443/oauth/v2/oauth-introspect', diff --git a/t/config.t b/t/config.t index 14fd7c9..d91f0b1 100644 --- a/t/config.t +++ b/t/config.t @@ -51,7 +51,7 @@ lua_shared_dict phantom-token 10m; --- config location /t { - rewrite_by_lua_block { + access_by_lua_block { local config = { client_id = 'introspection-client', @@ -60,7 +60,16 @@ location /t { } local phantomToken = require 'phantom-token' - phantomToken.run(config) + local ok, err = phantomToken.validate(config) + if ok then + phantomToken.run(config) + else + ngx.log(ngx.ERR, err) + ngx.status = ngx.HTTP_INTERNAL_SERVER_ERROR + ngx.header.content_type = 'application/json' + ngx.say('{"code":"server_error","message":"Problem encountered processing the request"}') + return ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR) + end } } @@ -72,8 +81,8 @@ GET /t --- more_headers eval "Authorization: bearer " . $main::token; ---- error_log -The phantom token configuration is invalid and must be corrected +--- error_log_like +The phantom token plugin requires^ --- response_body_like chomp {"code":"server_error","message":"Problem encountered processing the request"} @@ -86,7 +95,7 @@ The phantom token configuration is invalid and must be corrected --- config location /t { - rewrite_by_lua_block { + access_by_lua_block { local config = { client_id = 'introspection-client', @@ -95,7 +104,16 @@ location /t { } local phantomToken = require 'phantom-token' - phantomToken.run(config) + local ok, err = phantomToken.validate(config) + if ok then + phantomToken.run(config) + else + ngx.log(ngx.ERR, err) + ngx.status = ngx.HTTP_INTERNAL_SERVER_ERROR + ngx.header.content_type = 'application/json' + ngx.say('{"code":"server_error","message":"Problem encountered processing the request"}') + return ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR) + end } } @@ -107,27 +125,36 @@ GET /t --- more_headers eval "Authorization: bearer " . $main::token; ---- error_log -The phantom token configuration is invalid and must be corrected +--- error_log_like +The phantom token plugin requires^ --- response_body_like chomp {"code":"server_error","message":"Problem encountered processing the request"} -=== TEST CONFIG_2: A deployment with missing data does not crash NGINX -####################################################################################################### -# Verify that empty configuration is handled in a controlled manner rather than causing server problems -####################################################################################################### +=== TEST CONFIG_3: A deployment with missing data fails validation +################################################################## +# Verify that empty configuration is handled by failing validation +################################################################## --- config location /t { - rewrite_by_lua_block { + access_by_lua_block { local config = { } local phantomToken = require 'phantom-token' - phantomToken.run(config) + local ok, err = phantomToken.validate(config) + if ok then + phantomToken.run(config) + else + ngx.log(ngx.ERR, err) + ngx.status = ngx.HTTP_INTERNAL_SERVER_ERROR + ngx.header.content_type = 'application/json' + ngx.say('{"code":"server_error","message":"Problem encountered processing the request"}') + return ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR) + end } } @@ -139,24 +166,34 @@ GET /t --- more_headers eval "Authorization: bearer " . $main::token; ---- error_log -The phantom token configuration is invalid and must be corrected +--- error_log_like +The phantom token plugin requires^ --- response_body_like chomp {"code":"server_error","message":"Problem encountered processing the request"} -=== TEST CONFIG_3: A deployment with null data does not crash NGINX -####################################################################################################### -# Verify that null configuration is handled in a controlled manner rather than causing server problems -####################################################################################################### +=== TEST CONFIG_4: A deployment with null data fails validation +################################################################# +# Verify that null configuration is handled by failing validation +################################################################# --- config location /t { - rewrite_by_lua_block { + access_by_lua_block { + local config = nil local phantomToken = require 'phantom-token' - phantomToken.run() + local ok, err = phantomToken.validate(config) + if ok then + phantomToken.run(config) + else + ngx.log(ngx.ERR, err) + ngx.status = ngx.HTTP_INTERNAL_SERVER_ERROR + ngx.header.content_type = 'application/json' + ngx.say('{"code":"server_error","message":"Problem encountered processing the request"}') + return ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR) + end } } @@ -168,13 +205,13 @@ GET /t --- more_headers eval "Authorization: bearer " . $main::token; ---- error_log -The phantom token configuration is invalid and must be corrected +--- error_log_like +The phantom token plugin requires^ --- response_body_like chomp {"code":"server_error","message":"Problem encountered processing the request"} -=== TEST CONFIG_4: A deployment with a misspelt field does not crash NGINX +=== TEST CONFIG_5: A deployment with a misspelt field fails validation ##################################################################################################### # Verify that bad configuration is handled in a controlled manner rather than causing server problems ##################################################################################################### @@ -182,7 +219,7 @@ The phantom token configuration is invalid and must be corrected --- config location /t { - rewrite_by_lua_block { + access_by_lua_block { local config = { introspectionn_endpoint = 'http://127.0.0.1:8443/oauth/v2/oauth-introspect', @@ -192,7 +229,16 @@ location /t { } local phantomToken = require 'phantom-token' - phantomToken.run(config) + local ok, err = phantomToken.validate(config) + if ok then + phantomToken.run(config) + else + ngx.log(ngx.ERR, err) + ngx.status = ngx.HTTP_INTERNAL_SERVER_ERROR + ngx.header.content_type = 'application/json' + ngx.say('{"code":"server_error","message":"Problem encountered processing the request"}') + return ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR) + end } } @@ -204,13 +250,13 @@ GET /t --- more_headers eval "Authorization: bearer " . $main::token; ---- error_log -The phantom token configuration is invalid and must be corrected +--- error_log_like +The phantom token plugin requires^ --- response_body_like chomp {"code":"server_error","message":"Problem encountered processing the request"} -=== TEST_CONFIG_5: A deployment with all optional fields are omitted successfully introspects tokens +=== TEST_CONFIG_6: A deployment with all optional fields are omitted successfully introspects tokens ####################################################################### # The happy case works as expected when all optional fields are omitted ####################################################################### @@ -221,7 +267,7 @@ lua_shared_dict phantom-token 10m; --- config location /t { - rewrite_by_lua_block { + access_by_lua_block { local config = { introspection_endpoint = 'http://127.0.0.1:8443/oauth/v2/oauth-introspect', @@ -230,7 +276,16 @@ location /t { } local phantomToken = require 'phantom-token' - phantomToken.run(config) + local ok, err = phantomToken.validate(config) + if ok then + phantomToken.run(config) + else + ngx.log(ngx.ERR, err) + ngx.status = ngx.HTTP_INTERNAL_SERVER_ERROR + ngx.header.content_type = 'application/json' + ngx.say('{"code":"server_error","message":"Problem encountered processing the request"}') + return ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR) + end } proxy_pass http://127.0.0.1:1984/target; diff --git a/t/failure_scenarios.t b/t/failure_scenarios.t index 4f78610..2435469 100644 --- a/t/failure_scenarios.t +++ b/t/failure_scenarios.t @@ -52,7 +52,7 @@ lua_shared_dict phantom-token 10m; --- config location /t { - rewrite_by_lua_block { + access_by_lua_block { local config = { introspection_endpoint = 'http://127.0.0.1:8447/oauth/v2/oauth-introspect', @@ -88,7 +88,7 @@ lua_shared_dict phantom-token 10m; --- config location /t { - rewrite_by_lua_block { + access_by_lua_block { local config = { introspection_endpoint = 'http://127.0.0.1:8443/oauth/v2/oauth-introspect', @@ -111,4 +111,4 @@ GET /t "Authorization: bearer " . $main::token --- response_body_like chomp -{"code":"unauthorized","message":"Missing, invalid or expired access token"} \ No newline at end of file +{"code":"invalid_token","message":"Missing, invalid or expired access token"} \ No newline at end of file diff --git a/t/schemes.t b/t/schemes.t new file mode 100644 index 0000000..91a4170 --- /dev/null +++ b/t/schemes.t @@ -0,0 +1,214 @@ +#!/usr/bin/perl + +################################################################ +# Runs tests focused on authorization schemes of Bearer and DPoP +################################################################ + +use strict; +use warnings; +use FindBin; +use lib "$FindBin::Bin/lib"; +use Test::Nginx::Socket 'no_plan'; + +SKIP: { + our $token = &get_token_from_idsvr(); + if ($token) { + run_tests(); + } + else { + fail("Could not get token from idsvr"); + } +} + +sub get_token_from_idsvr { + use LWP::UserAgent; + + my $ua = LWP::UserAgent->new(); + + my $response = $ua->post("http://localhost:8443/oauth/v2/oauth-token", { + "client_id" => "test-client", + "client_secret" => "secret1", + "grant_type" => "client_credentials", + "scope" => "read" + }); + my $content = $response->decoded_content(); + + my ($result) = $content =~ /access_token":"([^"]+)/; + + return $result; +} + +__DATA__ + +=== TEST_SCHEME_1: An opaque bearer token can be introspected for a phantom token +######################################################## +# The happy case works as expected when you set a scheme +######################################################## + +--- http_config +lua_shared_dict phantom-token 10m; + +--- config +location /t { + + access_by_lua_block { + + local config = { + introspection_endpoint = 'http://127.0.0.1:8443/oauth/v2/oauth-introspect', + client_id = 'introspection-client', + client_secret = 'secret2', + token_cache_seconds = 900, + scheme = 'Bearer' + } + + local phantomToken = require 'phantom-token' + phantomToken.run(config) + } + + proxy_pass http://127.0.0.1:1984/target; +} +location /target { + add_header 'authorization' $http_authorization; + return 200; +} + +--- error_code: 200 + +--- request +GET /t + +--- more_headers eval +"Authorization: bearer " . $main::token; + +--- response_headers_like +authorization: Bearer ey.* + +=== TEST_SCHEME_2: Sending an invalid bearer token that fails introspection results in an access denied error +######################################################### +# An unrecognised token is rejected when you set a scheme +######################################################### + +--- http_config +lua_shared_dict phantom-token 10m; + +--- config +location /t { + + access_by_lua_block { + + local config = { + introspection_endpoint = 'http://127.0.0.1:8443/oauth/v2/oauth-introspect', + client_id = 'introspection-client', + client_secret = 'secret2', + token_cache_seconds = 900, + scheme = 'Bearer' + } + + local phantomToken = require 'phantom-token' + phantomToken.run(config) + } +} + +--- more_headers +Authorization: bearer zort + +--- request +GET /t + +--- error_code: 401 + +--- response_headers +content-type: application/json + +--- response_headers_like +WWW-Authenticate: ^Bearer + +--- response_body_like chomp +{"code":"invalid_token","message":"Missing, invalid or expired access token"} + +=== TEST_SCHEME_3: An opaque DPoP token can be introspected for a JWT +############################################################# +# The happy case works as expected when you set a DPoP scheme +############################################################# + +--- http_config +lua_shared_dict phantom-token 10m; + +--- config +location /t { + + set $original_access_token ""; + access_by_lua_block { + + local config = { + introspection_endpoint = 'http://127.0.0.1:8443/oauth/v2/oauth-introspect', + client_id = 'introspection-client', + client_secret = 'secret2', + token_cache_seconds = 900, + scheme = 'DPoP' + } + + local phantomToken = require 'phantom-token' + phantomToken.run(config) + } + + proxy_pass http://127.0.0.1:1984/target; +} +location /target { + add_header 'authorization' $http_authorization; + return 200; +} + +--- error_code: 200 + +--- request +GET /t + +--- more_headers eval +"Authorization: dpop " . $main::token; + +--- response_headers_like +authorization: DPoP ey.* + +=== TEST_SCHEME_4: Sending an invalid DPoP token that fails introspection results in an access denied error +############################################################## +# An unrecognised token is rejected when you set a DPoP scheme +############################################################## + +--- http_config +lua_shared_dict phantom-token 10m; + +--- config +location /t { + + access_by_lua_block { + + local config = { + introspection_endpoint = 'http://127.0.0.1:8443/oauth/v2/oauth-introspect', + client_id = 'introspection-client', + client_secret = 'secret2', + token_cache_seconds = 900, + scheme = 'DPoP' + } + + local phantomToken = require 'phantom-token' + phantomToken.run(config) + } +} + +--- more_headers +Authorization: dpop zort + +--- request +GET /t + +--- error_code: 401 + +--- response_headers +content-type: application/json + +--- response_headers_like +WWW-Authenticate: ^DPoP + +--- response_body_like chomp +{"code":"invalid_token","message":"Missing, invalid or expired access token"} diff --git a/t/scope_checks.t b/t/scope_checks.t index b4dc1b4..745f43e 100644 --- a/t/scope_checks.t +++ b/t/scope_checks.t @@ -52,7 +52,7 @@ lua_shared_dict phantom-token 10m; --- config location /t { - rewrite_by_lua_block { + access_by_lua_block { local config = { introspection_endpoint = 'http://127.0.0.1:8443/oauth/v2/oauth-introspect', @@ -95,7 +95,7 @@ lua_shared_dict phantom-token 10m; --- config location /t { - rewrite_by_lua_block { + access_by_lua_block { local config = { introspection_endpoint = 'http://127.0.0.1:8443/oauth/v2/oauth-introspect', @@ -135,7 +135,7 @@ lua_shared_dict phantom-token 10m; --- config location /t { - rewrite_by_lua_block { + access_by_lua_block { local config = { introspection_endpoint = 'http://127.0.0.1:8443/oauth/v2/oauth-introspect', diff --git a/test.sh b/test.sh index 298a763..a081b2f 100755 --- a/test.sh +++ b/test.sh @@ -14,7 +14,7 @@ cd "$(dirname "${BASH_SOURCE[0]}")" # # Point to the OpenResty install # -OPENRESTY_ROOT=/usr/local/Cellar/openresty/1.25.3.1_1 +OPENRESTY_ROOT=/opt/homebrew/opt/openresty/ # # Ensure that the OpenResty nginx, with LUA support, will be found by the prove tool