├── LICENSE ├── README.md └── src ├── access.lua ├── handler.lua └── schema.lua /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Vasiliy Malyavin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Kong access token introspection plugin 2 | Simple kong plugin to use any custom jwt access token introspection, as API auth. 3 | Inspired by [mogui/kong-external-oauth](https://github.com/mogui/kong-external-oauth) 4 | 5 | # How it works 6 | Plugin is protecting Kong API service/route with introspection of Oauth2.0 JWT access-token, added to request header. Plugin does a pre-request to oauth introspection endpoint([RFC7662](https://tools.ietf.org/html/rfc7662#section-2)). 7 | 8 | # Configuration 9 | 10 | 11 | | Form Parameter | default | description | 12 | | --- | --- | --- | 13 | | `config.introspection_endpoint` | | External introspection endpoint compatible with RFC7662 | 14 | | `config.token_header` | Authorization | Name of api-request header containing access token | 15 | | `config.token_cache_time` | 0 | Cache TTL for every token introspection result(0 - no cache) | 16 | | `config.scope` | | Scope that token need to get allowed to this method. For example 'manage-profile'. Allow any scope if empty | -------------------------------------------------------------------------------- /src/access.lua: -------------------------------------------------------------------------------- 1 | local _M = { conf = {} } 2 | local http = require "resty.http" 3 | local pl_stringx = require "pl.stringx" 4 | local cjson = require "cjson.safe" 5 | 6 | function _M.error_response(message, status) 7 | local jsonStr = '{"data":[],"error":{"code":' .. status .. ',"message":"' .. message .. '"}}' 8 | ngx.header['Content-Type'] = 'application/json' 9 | ngx.status = status 10 | ngx.say(jsonStr) 11 | ngx.exit(status) 12 | end 13 | 14 | function _M.introspect_access_token_req(access_token) 15 | local httpc = http:new() 16 | local res, err = httpc:request_uri(_M.conf.introspection_endpoint, { 17 | method = "POST", 18 | ssl_verify = false, 19 | body = "token_type_hint=access_token&token=" .. access_token, 20 | headers = { ["Content-Type"] = "application/x-www-form-urlencoded", } 21 | }) 22 | 23 | if not res then 24 | return { status = 0 } 25 | end 26 | if res.status ~= 200 then 27 | return { status = res.status } 28 | end 29 | return { status = res.status, body = res.body } 30 | end 31 | 32 | function _M.introspect_access_token(access_token) 33 | if _M.conf.token_cache_time > 0 then 34 | local cache_id = "at:" .. access_token 35 | local res, err = kong.cache:get(cache_id, { ttl = _M.conf.token_cache_time }, 36 | _M.introspect_access_token_req, access_token) 37 | if err then 38 | _M.error_response("Unexpected error: " .. err, ngx.HTTP_INTERNAL_SERVER_ERROR) 39 | end 40 | -- not 200 response status isn't valid for normal caching 41 | -- TODO:optimisation 42 | if res.status ~= 200 then 43 | kong.cache:invalidate(cache_id) 44 | end 45 | 46 | return res 47 | end 48 | 49 | return _M.introspect_access_token_req(access_token) 50 | end 51 | 52 | function _M.is_scope_authorized(scope) 53 | if _M.conf.scope == nil then 54 | return true 55 | end 56 | local needed_scope = pl_stringx.strip(_M.conf.scope) 57 | if string.len(needed_scope) == 0 then 58 | return true 59 | end 60 | scope = pl_stringx.strip(scope) 61 | if string.find(scope, '*', 1, true) or string.find(scope, needed_scope, 1, true) then 62 | return true 63 | end 64 | 65 | return false 66 | end 67 | 68 | -- TODO: plugin config that will allow not authorized queries 69 | function _M.run(conf) 70 | _M.conf = conf 71 | local access_token = ngx.req.get_headers()[_M.conf.token_header] 72 | if not access_token then 73 | _M.error_response("Unauthenticated.", ngx.HTTP_UNAUTHORIZED) 74 | end 75 | -- replace Bearer prefix 76 | access_token = pl_stringx.replace(access_token, "Bearer ", "", 1) 77 | 78 | local res = _M.introspect_access_token(access_token) 79 | if not res then 80 | _M.error_response("Authorization server error.", ngx.HTTP_INTERNAL_SERVER_ERROR) 81 | end 82 | if res.status ~= 200 then 83 | _M.error_response("The resource owner or authorization server denied the request.", ngx.HTTP_UNAUTHORIZED) 84 | end 85 | local data = cjson.decode(res.body) 86 | if data["active"] ~= true then 87 | _M.error_response("The resource owner or authorization server denied the request.", ngx.HTTP_UNAUTHORIZED) 88 | end 89 | if not _M.is_scope_authorized(data["scope"]) then 90 | _M.error_response("Forbidden", ngx.HTTP_FORBIDDEN) 91 | end 92 | 93 | ngx.req.set_header("X-Credential-Sub", data["sub"]) 94 | ngx.req.set_header("X-Credential-Scope", data["scope"]) 95 | -- clear token header from req 96 | ngx.req.clear_header(_M.conf.token_header) 97 | end 98 | 99 | return _M 100 | -------------------------------------------------------------------------------- /src/handler.lua: -------------------------------------------------------------------------------- 1 | local BasePlugin = require "kong.plugins.base_plugin" 2 | local access = require "kong.plugins.access-token-introspection.access" 3 | 4 | local TokenHandler = BasePlugin:extend() 5 | 6 | function TokenHandler:new() 7 | TokenHandler.super.new(self, "access-token-introspection") 8 | end 9 | 10 | function TokenHandler:access(conf) 11 | TokenHandler.super.access(self) 12 | access.run(conf) 13 | end 14 | 15 | return TokenHandler -------------------------------------------------------------------------------- /src/schema.lua: -------------------------------------------------------------------------------- 1 | local url = require "socket.url" 2 | local function validate_url(value) 3 | local parsed_url = url.parse(value) 4 | if parsed_url.scheme and parsed_url.host then 5 | parsed_url.scheme = parsed_url.scheme:lower() 6 | if not (parsed_url.scheme == "http" or parsed_url.scheme == "https") then 7 | return false, "Supported protocols are HTTP and HTTPS" 8 | end 9 | end 10 | 11 | return true 12 | end 13 | 14 | return { 15 | fields = { 16 | introspection_endpoint = { type = "url", required = true, func = validate_url }, 17 | token_header = { type = "string", required = true, default = { "Authorization" } }, 18 | token_cache_time = { type = "number", required = true, default = 0 }, 19 | scope = { type = "string", default = "" } 20 | } 21 | } --------------------------------------------------------------------------------