Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand All @@ -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`.\
Expand Down Expand Up @@ -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',
Expand Down
2 changes: 2 additions & 0 deletions curity-test-config.xml
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,8 @@
<id>default-datasource</id>
<jdbc xmlns="https://curity.se/ns/ext-conf/jdbc">
<connection-string>jdbc:hsqldb:file:${se.curity:identity-server:db};ifexists=true;hsqldb.lock_file=false</connection-string>
<standard-credentials-mode>
</standard-credentials-mode>
<driver>org.hsqldb.jdbc.JDBCDriver</driver>
<username>SA</username>
</jdbc>
Expand Down
4 changes: 2 additions & 2 deletions docker/deploy.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
6 changes: 3 additions & 3 deletions docker/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion docker/kong/Dockerfile
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 1 addition & 1 deletion docker/openresty/Dockerfile
Original file line number Diff line number Diff line change
@@ -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/
Expand Down
14 changes: 12 additions & 2 deletions docker/openresty/nginx.conf
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
144 changes: 87 additions & 57 deletions plugin/access.lua
Original file line number Diff line number Diff line change
Expand Up @@ -36,44 +36,18 @@ 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

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 .. '"}'
Expand All @@ -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

--
Expand Down Expand Up @@ -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

Expand Down
8 changes: 4 additions & 4 deletions plugin/handler.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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
8 changes: 5 additions & 3 deletions plugin/schema.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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" } } }
}
}}
}
Expand Down
4 changes: 2 additions & 2 deletions t/advanced_routing.t
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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',
Expand Down
Loading