From 79ccc4fe309ee2af86408db594ff7c67642ff1e0 Mon Sep 17 00:00:00 2001 From: Hannu Laurila Date: Wed, 13 Jan 2021 16:06:53 +0200 Subject: [PATCH 1/9] Fix test environment set-up to work with current kong --- .env | 4 ++-- test/docker/integration/Dockerfile | 3 ++- test/docker/unit/Dockerfile | 6 ++++-- test/unit/run.sh | 6 +++++- test/unit/test_introspect.lua | 20 ++++++++++++++++---- 5 files changed, 29 insertions(+), 10 deletions(-) diff --git a/.env b/.env index d5a16be2..3af9ffcb 100644 --- a/.env +++ b/.env @@ -2,9 +2,9 @@ BUILD_IMG_NAME=nokia/kong-oidc INTEGRATION_PATH=test/docker/integration UNIT_PATH=test/docker/unit -KONG_BASE_TAG=:1.0-centos +KONG_BASE_TAG=:2.2.1-centos KONG_TAG= -KONG_DB_TAG=:10.1 +KONG_DB_TAG=:12 KONG_DB_PORT=5432 KONG_DB_USER=kong KONG_DB_PW=kong diff --git a/test/docker/integration/Dockerfile b/test/docker/integration/Dockerfile index 1c65a096..48d721c2 100644 --- a/test/docker/integration/Dockerfile +++ b/test/docker/integration/Dockerfile @@ -1,5 +1,6 @@ ARG KONG_BASE_TAG FROM kong${KONG_BASE_TAG} +USER root ENV LUA_PATH /usr/local/share/lua/5.1/?.lua;/usr/local/kong-oidc/?.lua;; # For lua-cjson @@ -12,6 +13,6 @@ RUN luarocks install luaunit RUN luarocks install lua-cjson # Change openidc version when version in rockspec changes -RUN luarocks install lua-resty-openidc 1.7.2-1 +RUN luarocks install lua-resty-openidc 1.7.4-1 COPY . /usr/local/kong-oidc diff --git a/test/docker/unit/Dockerfile b/test/docker/unit/Dockerfile index 75729b6e..2bacadaf 100644 --- a/test/docker/unit/Dockerfile +++ b/test/docker/unit/Dockerfile @@ -1,6 +1,6 @@ ARG KONG_BASE_TAG FROM kong${KONG_BASE_TAG} - +USER root ENV LUA_PATH /usr/local/share/lua/5.1/?.lua;/usr/local/kong-oidc/?.lua # For lua-cjson ENV LUA_CPATH /usr/local/lib/lua/5.1/?.so @@ -8,7 +8,9 @@ ENV LUA_CPATH /usr/local/lib/lua/5.1/?.so # Install unzip for luarocks, gcc for lua-cjson RUN echo "ip_resolve=4" >> /etc/yum.conf && yum install -y unzip gcc # Change openidc version when version in rockspec changes -RUN luarocks install lua-resty-openidc 1.7.2-1 +RUN luarocks install lua-resty-openidc 1.7.4-1 +RUN luarocks install luacov +RUN luarocks install luaunit WORKDIR /usr/local/kong-oidc diff --git a/test/unit/run.sh b/test/unit/run.sh index a54ebf63..dc803181 100755 --- a/test/unit/run.sh +++ b/test/unit/run.sh @@ -1,9 +1,13 @@ #!/bin/bash set -e +rm -f luacov.stats.out + # Run all test_*.lua files in test/unit for f in test/unit/test_*.lua; do (set -x lua -lluacov ${f} -o TAP --failure ) -done \ No newline at end of file +done +luacov +cat luacov.report.out diff --git a/test/unit/test_introspect.lua b/test/unit/test_introspect.lua index 53b7a7b0..31949334 100644 --- a/test/unit/test_introspect.lua +++ b/test/unit/test_introspect.lua @@ -13,10 +13,15 @@ function TestIntrospect:tearDown() end function TestIntrospect:test_access_token_exists() + package.loaded["resty.openidc"] = nil + self.module_resty = { + openidc = { + introspect = function(...) return { sub = "sub" }, nil end, + } + } + package.preload["resty.openidc"] = function() return self.module_resty.openidc end + ngx.req.get_headers = function() return {Authorization = "Bearer xxx"} end - local dict = {} - function dict:get(key) return key end - _G.ngx.shared = {introspection = dict } ngx.encode_base64 = function(x) return "eyJzdWIiOiJzdWIifQ==" @@ -33,7 +38,14 @@ function TestIntrospect:test_access_token_exists() end function TestIntrospect:test_no_authorization_header() - package.loaded["resty.openidc"].authenticate = function(...) return {}, nil end + package.loaded["resty.openidc"] = nil + self.module_resty = { + openidc = { + authenticate = function(...) return {}, nil end + } + } + package.preload["resty.openidc"] = function() return self.module_resty.openidc end + ngx.req.get_headers = function() return {} end local headers = {} From ec27c62a0b2f55648a1c9201dd7173e8edcdaa42 Mon Sep 17 00:00:00 2001 From: Hannu Laurila Date: Wed, 13 Jan 2021 16:07:01 +0200 Subject: [PATCH 2/9] Fix use of groups_claim config parameter --- kong/plugins/oidc/utils.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kong/plugins/oidc/utils.lua b/kong/plugins/oidc/utils.lua index 7ec3cca8..f2be5e11 100644 --- a/kong/plugins/oidc/utils.lua +++ b/kong/plugins/oidc/utils.lua @@ -70,7 +70,7 @@ function M.get_options(config, ngx) disable_userinfo_header = config.disable_userinfo_header == "yes", disable_id_token_header = config.disable_id_token_header == "yes", disable_access_token_header = config.disable_access_token_header == "yes", - groups_claim = config.groups_claim == "groups" + groups_claim = config.groups_claim } end From 63a0725bb2d210ff6f37ee53593fae5b62fb2ceb Mon Sep 17 00:00:00 2001 From: Hannu Laurila Date: Wed, 13 Jan 2021 16:07:05 +0200 Subject: [PATCH 3/9] Add option skip_already_auth_requests for partial plugin chaining support Partial support for plugin chaining: allow skipping requests, where higher priority plugin has already set the credentials. The 'config.anomyous' approach to define "and/or" relationship between auth plugins is not utilized. --- README.md | 1 + kong/plugins/oidc/handler.lua | 8 ++++++ kong/plugins/oidc/schema.lua | 3 ++- kong/plugins/oidc/utils.lua | 3 ++- test/unit/test_already_auth.lua | 44 +++++++++++++++++++++++++++++++++ 5 files changed, 57 insertions(+), 2 deletions(-) create mode 100644 test/unit/test_already_auth.lua diff --git a/README.md b/README.md index b8c6ad95..9364b206 100644 --- a/README.md +++ b/README.md @@ -97,6 +97,7 @@ If you're using `luarocks` execute the following: | `config.disable_id_token_header` | no | false | Disable passing the ID Token to the upstream server | | `config.disable_access_token_header` | no | false | Disable passing the Access Token to the upstream server | | `config.groups_claim` | groups | false | Name of the claim in the token to get groups from | +| `config.skip_already_auth_requests` | no | false | Ignore requests where credentials have already been set by a higher priority plugin such as basic-auth | ### Enabling kong-oidc diff --git a/kong/plugins/oidc/handler.lua b/kong/plugins/oidc/handler.lua index 207e0726..9347d0da 100644 --- a/kong/plugins/oidc/handler.lua +++ b/kong/plugins/oidc/handler.lua @@ -15,6 +15,14 @@ function OidcHandler:access(config) OidcHandler.super.access(self) local oidcConfig = utils.get_options(config, ngx) + -- partial support for plugin chaining: allow skipping requests, where higher priority + -- plugin has already set the credentials. The 'config.anomyous' approach to define + -- "and/or" relationship between auth plugins is not utilized + if oidcConfig.skip_already_auth_requests and kong.client.get_credential() then + ngx.log(ngx.DEBUG, "OidcHandler ignoring already auth request: " .. ngx.var.request_uri) + return + end + if filter.shouldProcessRequest(oidcConfig) then session.configure(config) handle(oidcConfig) diff --git a/kong/plugins/oidc/schema.lua b/kong/plugins/oidc/schema.lua index f1cfe955..55922b34 100644 --- a/kong/plugins/oidc/schema.lua +++ b/kong/plugins/oidc/schema.lua @@ -29,6 +29,7 @@ return { disable_id_token_header = { type = "string", required = false, default = "no" }, disable_access_token_header = { type = "string", required = false, default = "no" }, revoke_tokens_on_logout = { type = "string", required = false, default = "no" }, - groups_claim = { type = "string", required = false, default = "groups" } + groups_claim = { type = "string", required = false, default = "groups" }, + skip_already_auth_requests = { type = "string", required = false, default = "no" } } } diff --git a/kong/plugins/oidc/utils.lua b/kong/plugins/oidc/utils.lua index f2be5e11..8e399047 100644 --- a/kong/plugins/oidc/utils.lua +++ b/kong/plugins/oidc/utils.lua @@ -70,7 +70,8 @@ function M.get_options(config, ngx) disable_userinfo_header = config.disable_userinfo_header == "yes", disable_id_token_header = config.disable_id_token_header == "yes", disable_access_token_header = config.disable_access_token_header == "yes", - groups_claim = config.groups_claim + groups_claim = config.groups_claim, + skip_already_auth_requests = config.skip_already_auth_requests == "yes" } end diff --git a/test/unit/test_already_auth.lua b/test/unit/test_already_auth.lua new file mode 100644 index 00000000..86403dd4 --- /dev/null +++ b/test/unit/test_already_auth.lua @@ -0,0 +1,44 @@ +local lu = require("luaunit") +TestHandler = require("test.unit.mockable_case"):extend() + + +function TestHandler:setUp() + TestHandler.super:setUp() + + package.loaded["resty.openidc"] = nil + self.module_resty = { openidc = {} } + package.preload["resty.openidc"] = function() + return self.module_resty.openidc + end + + self.handler = require("kong.plugins.oidc.handler")() +end + +function TestHandler:tearDown() + TestHandler.super:tearDown() +end + +function TestHandler:test_skip_already_auth_has_cred() + kong.client.get_credential = function() return { consumer_id = "user" } end + local called_authenticate + self.module_resty.openidc.authenticate = function(opts) + called_authenticate = true + return nil, "error" + end + self.handler:access({ skip_already_auth_requests = "yes" }) + lu.assertNil(called_authenticate) +end + +function TestHandler:test_skip_already_auth_has_no_cred() + kong.client.get_credential = function() return nil end + local called_authenticate + self.module_resty.openidc.authenticate = function(opts) + called_authenticate = true + return nil, "error" + end + self.handler:access({ skip_already_auth_requests = "yes" }) + lu.assertTrue(called_authenticate) +end + + +lu.run() From c4e41f126458db796065fd531ccd3c318ec02521 Mon Sep 17 00:00:00 2001 From: Hannu Laurila Date: Wed, 13 Jan 2021 16:07:11 +0200 Subject: [PATCH 4/9] Use kong.client.authenticate and set_consumer to inject user This way possible anonymous consumer identity set by higher priority plugin is cleared in case of OIDC authentiation. Makes it easier to use simultaneously for example basic-auth plugin, oidc plugin and acl plugin. This behavior is more in line with Kong-bundled auth plugins. There could be some upgrade impact, since previously the consumer identity was not touched and headers were not managed. --- README.md | 4 ++++ kong/plugins/oidc/utils.lua | 48 ++++++++++++++++++++++++++++++++++++- test/unit/mockable_case.lua | 18 ++++++++++++++ 3 files changed, 69 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 9364b206..42910012 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,8 @@ ngx.ctx.authenticated_credential = { } ``` +For successfully authenticated request, possible (anonymous) consumer identity set by higher priority plugin is cleared as part of setting the credentials. + The plugin will try to retrieve the user's groups from a field in the token (default `groups`) and set `kong.ctx.shared.authenticated_groups` so that Kong authorization plugins can make decisions based on the user's group membership. ## Dependencies @@ -154,6 +156,8 @@ Server: kong/0.11.0 ### Upstream API request +For successfully authenticated request, the plugin will set upstream header `X-Credential-Identifier` to contain `sub` claim from user info, ID token or introspection result. Header `X-Anonymous-Consumer` is cleared. + The plugin adds a additional `X-Userinfo`, `X-Access-Token` and `X-Id-Token` headers to the upstream request, which can be consumer by upstream server. All of them are base64 encoded: ```http diff --git a/kong/plugins/oidc/utils.lua b/kong/plugins/oidc/utils.lua index 8e399047..19dc58ec 100644 --- a/kong/plugins/oidc/utils.lua +++ b/kong/plugins/oidc/utils.lua @@ -1,4 +1,5 @@ local cjson = require("cjson") +local constants = require "kong.constants" local M = {} @@ -81,6 +82,51 @@ function M.exit(httpStatusCode, message, ngxCode) ngx.exit(ngxCode) end + +-- Function set_consumer is derived from the following kong auth plugins: +-- https://github.com/Kong/kong/blob/2.2.0/kong/plugins/ldap-auth/access.lua +-- https://github.com/Kong/kong/blob/2.2.0/kong/plugins/oauth2/access.lua +-- Copyright 2016-2020 Kong Inc. Licensed under the Apache License, Version 2.0 +-- https://github.com/Kong/kong/blob/2.2.0/LICENSE +local function set_consumer(consumer, credential) + kong.client.authenticate(consumer, credential) + + local set_header = kong.service.request.set_header + local clear_header = kong.service.request.clear_header + + if consumer and consumer.id then + set_header(constants.HEADERS.CONSUMER_ID, consumer.id) + else + clear_header(constants.HEADERS.CONSUMER_ID) + end + + if consumer and consumer.custom_id then + set_header(constants.HEADERS.CONSUMER_CUSTOM_ID, consumer.custom_id) + else + clear_header(constants.HEADERS.CONSUMER_CUSTOM_ID) + end + + if consumer and consumer.username then + set_header(constants.HEADERS.CONSUMER_USERNAME, consumer.username) + else + clear_header(constants.HEADERS.CONSUMER_USERNAME) + end + + if credential and credential.sub then + set_header(constants.HEADERS.CREDENTIAL_IDENTIFIER, credential.sub) + else + clear_header(constants.HEADERS.CREDENTIAL_IDENTIFIER) + end + + clear_header(constants.HEADERS.CREDENTIAL_USERNAME) + + if credential then + clear_header(constants.HEADERS.ANONYMOUS) + else + set_header(constants.HEADERS.ANONYMOUS, true) + end +end + function M.injectAccessToken(accessToken, headerName, bearerToken) ngx.log(ngx.DEBUG, "Injecting " .. headerName) local token = accessToken @@ -101,7 +147,7 @@ function M.injectUser(user, headerName) local tmp_user = user tmp_user.id = user.sub tmp_user.username = user.preferred_username - ngx.ctx.authenticated_credential = tmp_user + set_consumer(nil, tmp_user) local userinfo = cjson.encode(user) ngx.req.set_header(headerName, ngx.encode_base64(userinfo)) end diff --git a/test/unit/mockable_case.lua b/test/unit/mockable_case.lua index 66aa367c..0afa4754 100644 --- a/test/unit/mockable_case.lua +++ b/test/unit/mockable_case.lua @@ -31,6 +31,23 @@ function MockableCase:setUp() self.ngx = _G.ngx _G.ngx = self.mocked_ngx + self.mocked_kong = { + client = { + authenticate = function(consumer, credential) + ngx.ctx.authenticated_consumer = consumer + ngx.ctx.authenticated_credential = credential + end + }, + service = { + request = { + clear_header = function(...) end, + set_header = function(...) end + } + } + } + self.kong = _G.kong + _G.kong = self.mocked_kong + self.resty = package.loaded.resty package.loaded["resty.http"] = nil package.preload["resty.http"] = function() @@ -50,6 +67,7 @@ end function MockableCase:tearDown() MockableCase.super:tearDown() _G.ngx = self.ngx + _G.kong = self.kong package.loaded.resty = self.resty package.loaded.cjson = self.cjson end From 0d7026821c8ce82f5ae9b91af53eee54ed6a5347 Mon Sep 17 00:00:00 2001 From: Hannu Laurila Date: Wed, 13 Jan 2021 16:07:15 +0200 Subject: [PATCH 5/9] Inject groups and set credentials regardless of disable_userinfo_header value Previously disable_userinfo_header impacted also setting of credential/group. This commit has some upgrade impact, if the previous behavior was relied upon. --- kong/plugins/oidc/handler.lua | 13 +++++++++++-- kong/plugins/oidc/utils.lua | 7 +++++-- test/unit/test_handler_mocking_openidc.lua | 18 +++++++++--------- 3 files changed, 25 insertions(+), 13 deletions(-) diff --git a/kong/plugins/oidc/handler.lua b/kong/plugins/oidc/handler.lua index 9347d0da..ecddb7da 100644 --- a/kong/plugins/oidc/handler.lua +++ b/kong/plugins/oidc/handler.lua @@ -38,18 +38,27 @@ function handle(oidcConfig) if oidcConfig.introspection_endpoint then response = introspect(oidcConfig) if response then - utils.injectUser(response, oidcConfig.userinfo_header_name) + utils.setCredentials(response) utils.injectGroups(response, oidcConfig.groups_claim) + utils.injectUser(response, oidcConfig.userinfo_header_name) end end if response == nil then response = make_oidc(oidcConfig) if response then + if response.user or response.id_token then + -- is there any scenario where lua-resty-openidc would not provide id_token? + utils.setCredentials(response.user or response.id_token) + end + if response.user and response.user[oidcConfig.groups_claim] ~= nil then + utils.injectGroups(response.user, oidcConfig.groups_claim) + elseif response.id_token then + utils.injectGroups(response.id_token, oidcConfig.groups_claim) + end if (not oidcConfig.disable_userinfo_header and response.user) then utils.injectUser(response.user, oidcConfig.userinfo_header_name) - utils.injectGroups(response.user, oidcConfig.groups_claim) end if (not oidcConfig.disable_access_token_header and response.access_token) then diff --git a/kong/plugins/oidc/utils.lua b/kong/plugins/oidc/utils.lua index 19dc58ec..76930a3d 100644 --- a/kong/plugins/oidc/utils.lua +++ b/kong/plugins/oidc/utils.lua @@ -142,12 +142,15 @@ function M.injectIDToken(idToken, headerName) ngx.req.set_header(headerName, ngx.encode_base64(tokenStr)) end -function M.injectUser(user, headerName) - ngx.log(ngx.DEBUG, "Injecting " .. headerName) +function M.setCredentials(user) local tmp_user = user tmp_user.id = user.sub tmp_user.username = user.preferred_username set_consumer(nil, tmp_user) +end + +function M.injectUser(user, headerName) + ngx.log(ngx.DEBUG, "Injecting " .. headerName) local userinfo = cjson.encode(user) ngx.req.set_header(headerName, ngx.encode_base64(userinfo)) end diff --git a/test/unit/test_handler_mocking_openidc.lua b/test/unit/test_handler_mocking_openidc.lua index 74a10d55..e988b746 100644 --- a/test/unit/test_handler_mocking_openidc.lua +++ b/test/unit/test_handler_mocking_openidc.lua @@ -22,10 +22,10 @@ end function TestHandler:test_authenticate_ok_no_userinfo() self.module_resty.openidc.authenticate = function(opts) - return {}, false + return { id_token = { sub = "sub"}}, false end - self.handler:access({}) + self.handler:access({disable_id_token_header = "yes"}) lu.assertTrue(self:log_contains("calling authenticate")) end @@ -50,7 +50,7 @@ end function TestHandler:test_authenticate_ok_with_no_accesstoken() self.module_resty.openidc.authenticate = function(opts) - return {}, true + return {id_token = {sub = "sub"}}, true end local headers = {} @@ -58,14 +58,14 @@ function TestHandler:test_authenticate_ok_with_no_accesstoken() headers[h] = v end - self.handler:access({}) + self.handler:access({disable_id_token_header = "yes"}) lu.assertTrue(self:log_contains("calling authenticate")) lu.assertNil(headers['X-Access-Token']) end function TestHandler:test_authenticate_ok_with_accesstoken() self.module_resty.openidc.authenticate = function(opts) - return {access_token = "ACCESS_TOKEN"}, true + return {id_token = { sub = "sub" } , access_token = "ACCESS_TOKEN"}, true end local headers = {} @@ -73,7 +73,7 @@ function TestHandler:test_authenticate_ok_with_accesstoken() headers[h] = v end - self.handler:access({access_token_header_name = 'X-Access-Token'}) + self.handler:access({access_token_header_name = 'X-Access-Token', disable_id_token_header = "yes"}) lu.assertTrue(self:log_contains("calling authenticate")) lu.assertEquals(headers['X-Access-Token'], "ACCESS_TOKEN") end @@ -114,7 +114,7 @@ end function TestHandler:test_authenticate_nok_no_recovery() self.module_resty.openidc.authenticate = function(opts) - return {}, true + return nil, true end self.handler:access({}) @@ -124,7 +124,7 @@ end function TestHandler:test_authenticate_nok_deny() self.module_resty.openidc.authenticate = function(opts) if opts.unauth_action == "deny" then - return {}, "unauthorized request" + return nil, "unauthorized request" end return {}, true end @@ -135,7 +135,7 @@ end function TestHandler:test_authenticate_nok_with_recovery() self.module_resty.openidc.authenticate = function(opts) - return {}, true + return nil, true end self.handler:access({recovery_page_path = "x"}) From 3739291992720f4456f05a6e62aca4b5c0c3dba2 Mon Sep 17 00:00:00 2001 From: Hannu Laurila Date: Wed, 13 Jan 2021 16:07:19 +0200 Subject: [PATCH 6/9] Add feature to auth based on JWT Bearer (ID) token in Authorization header --- README.md | 3 ++ kong/plugins/oidc/handler.lua | 55 ++++++++++++++++++++++++ kong/plugins/oidc/schema.lua | 5 ++- kong/plugins/oidc/utils.lua | 28 +++++++++++- test/unit/mockable_case.lua | 6 +++ test/unit/test_bearer_jwt_auth.lua | 68 ++++++++++++++++++++++++++++++ test/unit/test_utils.lua | 11 +++++ 7 files changed, 174 insertions(+), 2 deletions(-) create mode 100644 test/unit/test_bearer_jwt_auth.lua diff --git a/README.md b/README.md index 42910012..82721647 100644 --- a/README.md +++ b/README.md @@ -100,6 +100,9 @@ If you're using `luarocks` execute the following: | `config.disable_access_token_header` | no | false | Disable passing the Access Token to the upstream server | | `config.groups_claim` | groups | false | Name of the claim in the token to get groups from | | `config.skip_already_auth_requests` | no | false | Ignore requests where credentials have already been set by a higher priority plugin such as basic-auth | +| `config.bearer_jwt_auth_enable` | no | false | Authenticate based on JWT (ID) token provided in Authorization (Bearer) header. Checks iss, sub, aud, exp, iat (as in ID token). `config.discovery` must be defined to discover JWKS | +| `config.bearer_jwt_auth_allowed_auds` | | false | List of JWT token `aud` values allowed when validating JWT token in Authorization header. If not provided, uses value from `config.client_id` | +| `config.bearer_jwt_auth_signing_algs` | [ 'RS256' ] | false | List of allowed signing algorithms for Authorization header JWT token validation. Must match to OIDC provider and `resty-openidc` supported algorithms | ### Enabling kong-oidc diff --git a/kong/plugins/oidc/handler.lua b/kong/plugins/oidc/handler.lua index ecddb7da..d23ff0ef 100644 --- a/kong/plugins/oidc/handler.lua +++ b/kong/plugins/oidc/handler.lua @@ -35,6 +35,17 @@ end function handle(oidcConfig) local response + + if oidcConfig.bearer_jwt_auth_enable then + response = verify_bearer_jwt(oidcConfig) + if response then + utils.setCredentials(response) + utils.injectGroups(response, oidcConfig.groups_claim) + utils.injectUser(response, oidcConfig.userinfo_header_name) + return + end + end + if oidcConfig.introspection_endpoint then response = introspect(oidcConfig) if response then @@ -111,4 +122,48 @@ function introspect(oidcConfig) return nil end +function verify_bearer_jwt(oidcConfig) + if not utils.has_bearer_access_token() then + return nil + end + -- setup controlled configuration for bearer_jwt_verify + local opts = { + accept_none_alg = false, + accept_unsupported_alg = false, + token_signing_alg_values_expected = oidcConfig.bearer_jwt_auth_signing_algs, + discovery = oidcConfig.discovery, + timeout = oidcConfig.timeout, + ssl_verify = oidcConfig.ssl_verify + } + + local discovery_doc, err = require("resty.openidc").get_discovery_doc(opts) + if err then + kong.log.err('Discovery document retrieval for Bearer JWT verify failed') + return nil + end + + local allowed_auds = oidcConfig.bearer_jwt_auth_allowed_auds or oidcConfig.client_id + + local jwt_validators = require "resty.jwt-validators" + jwt_validators.set_system_leeway(120) + local claim_spec = { + -- mandatory for id token: iss, sub, aud, exp, iat + iss = jwt_validators.equals(discovery_doc.issuer), + sub = jwt_validators.required(), + aud = function(val) return utils.has_common_item(val, allowed_auds) end, + exp = jwt_validators.is_not_expired(), + iat = jwt_validators.required(), + -- optional validations + nbf = jwt_validators.opt_is_not_before(), + } + + local json, err, token = require("resty.openidc").bearer_jwt_verify(opts, claim_spec) + if err then + kong.log.err('Bearer JWT verify failed: ' .. err) + return nil + end + + return json +end + return OidcHandler diff --git a/kong/plugins/oidc/schema.lua b/kong/plugins/oidc/schema.lua index 55922b34..0be1026b 100644 --- a/kong/plugins/oidc/schema.lua +++ b/kong/plugins/oidc/schema.lua @@ -30,6 +30,9 @@ return { disable_access_token_header = { type = "string", required = false, default = "no" }, revoke_tokens_on_logout = { type = "string", required = false, default = "no" }, groups_claim = { type = "string", required = false, default = "groups" }, - skip_already_auth_requests = { type = "string", required = false, default = "no" } + skip_already_auth_requests = { type = "string", required = false, default = "no" }, + bearer_jwt_auth_enable = { type = "string", required = false, default = "no" }, + bearer_jwt_auth_allowed_auds = { type = "array", required = false }, + bearer_jwt_auth_signing_algs = { type = "array", required = true, default = { "RS256" } }, } } diff --git a/kong/plugins/oidc/utils.lua b/kong/plugins/oidc/utils.lua index 76930a3d..4e855fe7 100644 --- a/kong/plugins/oidc/utils.lua +++ b/kong/plugins/oidc/utils.lua @@ -72,7 +72,10 @@ function M.get_options(config, ngx) disable_id_token_header = config.disable_id_token_header == "yes", disable_access_token_header = config.disable_access_token_header == "yes", groups_claim = config.groups_claim, - skip_already_auth_requests = config.skip_already_auth_requests == "yes" + skip_already_auth_requests = config.skip_already_auth_requests == "yes", + bearer_jwt_auth_enable = config.bearer_jwt_auth_enable == "yes", + bearer_jwt_auth_allowed_auds = config.bearer_jwt_auth_allowed_auds, + bearer_jwt_auth_signing_algs = config.bearer_jwt_auth_signing_algs } end @@ -172,4 +175,27 @@ function M.has_bearer_access_token() return false end +-- verify if tables t1 and t2 have at least one common string item +-- instead of table, also string can be provided as t1 or t2 +function M.has_common_item(t1, t2) + if t1 == nil or t2 == nil then + return false + end + if type(t1) == "string" then + t1 = { t1 } + end + if type(t2) == "string" then + t2 = { t2 } + end + local i1, i2 + for _, i1 in pairs(t1) do + for _, i2 in pairs(t2) do + if type(i1) == "string" and type(i2) == "string" and i1 == i2 then + return true + end + end + end + return false +end + return M diff --git a/test/unit/mockable_case.lua b/test/unit/mockable_case.lua index 0afa4754..ebc90699 100644 --- a/test/unit/mockable_case.lua +++ b/test/unit/mockable_case.lua @@ -43,6 +43,12 @@ function MockableCase:setUp() clear_header = function(...) end, set_header = function(...) end } + }, + log = { + err = function(...) end + }, + ctx = { + shared = {} } } self.kong = _G.kong diff --git a/test/unit/test_bearer_jwt_auth.lua b/test/unit/test_bearer_jwt_auth.lua new file mode 100644 index 00000000..edd7a37c --- /dev/null +++ b/test/unit/test_bearer_jwt_auth.lua @@ -0,0 +1,68 @@ +local lu = require("luaunit") +TestHandler = require("test.unit.mockable_case"):extend() + + +function TestHandler:setUp() + TestHandler.super:setUp() + + package.loaded["resty.openidc"] = nil + self.module_resty = { openidc = {} } + package.preload["resty.openidc"] = function() + return self.module_resty.openidc + end + + self.handler = require("kong.plugins.oidc.handler")() +end + +function TestHandler:tearDown() + TestHandler.super:tearDown() +end + +function TestHandler:test_bearer_jwt_auth_success() + ngx.req.get_headers = function() return {Authorization = "Bearer xxx"} end + ngx.encode_base64 = function(x) return "eyJzdWIiOiJzdWIifQ==" end + + self.module_resty.openidc.get_discovery_doc = function(opts) + return { issuer = "https://oidc" } + end + + self.module_resty.openidc.bearer_jwt_verify = function(opts) + token = { + iss = "https://oidc", + sub = "sub111", + aud = "aud222", + groups = { "users" } + } + return token, nil, "xxx" + end + + self.handler:access({ + bearer_jwt_auth_enable = "yes", + client_id = "aud222", + groups_claim = "groups", + userinfo_header_name = "x-userinfo" + }) + lu.assertEquals(ngx.ctx.authenticated_credential.id, "sub111") + lu.assertEquals(kong.ctx.shared.authenticated_groups, { "users" }) +end + +function TestHandler:test_bearer_jwt_auth_fail() + ngx.req.get_headers = function() return {Authorization = "Bearer xxx"} end + local called_authenticate + self.module_resty.openidc.get_discovery_doc = function(opts) + return { issuer = "https://oidc" } + end + + self.module_resty.openidc.bearer_jwt_verify = function(opts) + return nil, "JWT expired" + end + + self.module_resty.openidc.authenticate = function(opts) + called_authenticate = true + return nil, "error" + end + self.handler:access({bearer_jwt_auth_enable = "yes", client_id = "aud222"}) + lu.assertTrue(called_authenticate) +end + +lu.run() diff --git a/test/unit/test_utils.lua b/test/unit/test_utils.lua index 095ae42e..469acc08 100644 --- a/test/unit/test_utils.lua +++ b/test/unit/test_utils.lua @@ -77,5 +77,16 @@ function TestUtils:testOptions() end +function TestUtils:testCommonItem() + lu.assertFalse(utils.has_common_item(nil, "aud1")) + lu.assertTrue(utils.has_common_item("aud1", "aud1")) + lu.assertFalse(utils.has_common_item("aud1", "aud2")) + lu.assertFalse(utils.has_common_item({"aud1", "aud2"}, "aud3")) + lu.assertTrue(utils.has_common_item({"aud1", "aud2"}, "aud2")) + lu.assertFalse(utils.has_common_item("aud1", {"aud2", "aud3"})) + lu.assertTrue(utils.has_common_item("aud2", {"aud2", "aud3"})) + lu.assertTrue(utils.has_common_item({"aud2","aud3","aud4"}, {"aud4", "aud5"})) + lu.assertFalse(utils.has_common_item({"aud2","aud3","aud4"}, {"aud5", "aud6"})) +end lu.run() From 78bcd9ebc0c3584b9b842174e8b1b9e52db4f886 Mon Sep 17 00:00:00 2001 From: Hannu Laurila Date: Wed, 13 Jan 2021 16:07:24 +0200 Subject: [PATCH 7/9] Honor disable_userinfo_header on bearer token auth Previously the parameter was not honored for token introspection case. This commit has some upgrade impact, if the previous behavior was relied upon. --- kong/plugins/oidc/handler.lua | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/kong/plugins/oidc/handler.lua b/kong/plugins/oidc/handler.lua index d23ff0ef..32cc8d13 100644 --- a/kong/plugins/oidc/handler.lua +++ b/kong/plugins/oidc/handler.lua @@ -41,7 +41,9 @@ function handle(oidcConfig) if response then utils.setCredentials(response) utils.injectGroups(response, oidcConfig.groups_claim) - utils.injectUser(response, oidcConfig.userinfo_header_name) + if not oidcConfig.disable_userinfo_header then + utils.injectUser(response, oidcConfig.userinfo_header_name) + end return end end @@ -51,7 +53,9 @@ function handle(oidcConfig) if response then utils.setCredentials(response) utils.injectGroups(response, oidcConfig.groups_claim) - utils.injectUser(response, oidcConfig.userinfo_header_name) + if not oidcConfig.disable_userinfo_header then + utils.injectUser(response, oidcConfig.userinfo_header_name) + end end end From 140c90dc2263cb63b143a6912986eed30461b481 Mon Sep 17 00:00:00 2001 From: Hannu Laurila Date: Wed, 13 Jan 2021 16:07:29 +0200 Subject: [PATCH 8/9] Add feature to inject custom headers based on claims --- README.md | 2 ++ kong/plugins/oidc/handler.lua | 3 +++ kong/plugins/oidc/schema.lua | 2 ++ kong/plugins/oidc/utils.lua | 25 ++++++++++++++++++++++- test/unit/test_header_claims.lua | 34 ++++++++++++++++++++++++++++++++ 5 files changed, 65 insertions(+), 1 deletion(-) create mode 100644 test/unit/test_header_claims.lua diff --git a/README.md b/README.md index 82721647..a26c988e 100644 --- a/README.md +++ b/README.md @@ -103,6 +103,8 @@ If you're using `luarocks` execute the following: | `config.bearer_jwt_auth_enable` | no | false | Authenticate based on JWT (ID) token provided in Authorization (Bearer) header. Checks iss, sub, aud, exp, iat (as in ID token). `config.discovery` must be defined to discover JWKS | | `config.bearer_jwt_auth_allowed_auds` | | false | List of JWT token `aud` values allowed when validating JWT token in Authorization header. If not provided, uses value from `config.client_id` | | `config.bearer_jwt_auth_signing_algs` | [ 'RS256' ] | false | List of allowed signing algorithms for Authorization header JWT token validation. Must match to OIDC provider and `resty-openidc` supported algorithms | +| `config.header_names` | | false | List of custom upstream HTTP headers to be added based on claims. Must have same number of elements as `config.header_claims`. Example: `[ 'x-oidc-email', 'x-oidc-email-verified' ]` | +| `config.header_claims` | | false | List of claims to be used as source for custom upstream headers. Claims are sourced from Userinfo, ID Token, Bearer JWT, Introspection, depending on auth method. Use only claims containing simple string values. Example: `[ 'email', 'email_verified'` | ### Enabling kong-oidc diff --git a/kong/plugins/oidc/handler.lua b/kong/plugins/oidc/handler.lua index 32cc8d13..2b2e652b 100644 --- a/kong/plugins/oidc/handler.lua +++ b/kong/plugins/oidc/handler.lua @@ -41,6 +41,7 @@ function handle(oidcConfig) if response then utils.setCredentials(response) utils.injectGroups(response, oidcConfig.groups_claim) + utils.injectHeaders(oidcConfig.header_names, oidcConfig.header_claims, { response }) if not oidcConfig.disable_userinfo_header then utils.injectUser(response, oidcConfig.userinfo_header_name) end @@ -53,6 +54,7 @@ function handle(oidcConfig) if response then utils.setCredentials(response) utils.injectGroups(response, oidcConfig.groups_claim) + utils.injectHeaders(oidcConfig.header_names, oidcConfig.header_claims, { response }) if not oidcConfig.disable_userinfo_header then utils.injectUser(response, oidcConfig.userinfo_header_name) end @@ -71,6 +73,7 @@ function handle(oidcConfig) elseif response.id_token then utils.injectGroups(response.id_token, oidcConfig.groups_claim) end + utils.injectHeaders(oidcConfig.header_names, oidcConfig.header_claims, { response.user, response.id_token }) if (not oidcConfig.disable_userinfo_header and response.user) then utils.injectUser(response.user, oidcConfig.userinfo_header_name) diff --git a/kong/plugins/oidc/schema.lua b/kong/plugins/oidc/schema.lua index 0be1026b..7b5e58bd 100644 --- a/kong/plugins/oidc/schema.lua +++ b/kong/plugins/oidc/schema.lua @@ -34,5 +34,7 @@ return { bearer_jwt_auth_enable = { type = "string", required = false, default = "no" }, bearer_jwt_auth_allowed_auds = { type = "array", required = false }, bearer_jwt_auth_signing_algs = { type = "array", required = true, default = { "RS256" } }, + header_names = { type = "array", required = true, default = {} }, + header_claims = { type = "array", required = true, default = {} }, } } diff --git a/kong/plugins/oidc/utils.lua b/kong/plugins/oidc/utils.lua index 4e855fe7..c531fc5b 100644 --- a/kong/plugins/oidc/utils.lua +++ b/kong/plugins/oidc/utils.lua @@ -75,7 +75,9 @@ function M.get_options(config, ngx) skip_already_auth_requests = config.skip_already_auth_requests == "yes", bearer_jwt_auth_enable = config.bearer_jwt_auth_enable == "yes", bearer_jwt_auth_allowed_auds = config.bearer_jwt_auth_allowed_auds, - bearer_jwt_auth_signing_algs = config.bearer_jwt_auth_signing_algs + bearer_jwt_auth_signing_algs = config.bearer_jwt_auth_signing_algs, + header_names = config.header_names or {}, + header_claims = config.header_claims or {} } end @@ -164,6 +166,27 @@ function M.injectGroups(user, claim) end end +function M.injectHeaders(header_names, header_claims, sources) + if #header_names ~= #header_claims then + kong.log.err('Different number of elements provided in header_names and header_claims. Headers will not be added.') + return + end + for i = 1, #header_names do + local header, claim + header = header_names[i] + claim = header_claims[i] + kong.service.request.clear_header(header) + for j = 1, #sources do + local source + source = sources[j] + if (source and source[claim]) then + kong.service.request.set_header(header, source[claim]) + break + end + end + end +end + function M.has_bearer_access_token() local header = ngx.req.get_headers()['Authorization'] if header and header:find(" ") then diff --git a/test/unit/test_header_claims.lua b/test/unit/test_header_claims.lua new file mode 100644 index 00000000..d7f03eca --- /dev/null +++ b/test/unit/test_header_claims.lua @@ -0,0 +1,34 @@ +local lu = require("luaunit") +TestHandler = require("test.unit.mockable_case"):extend() + +function TestHandler:setUp() + TestHandler.super:setUp() + + package.loaded["resty.openidc"] = nil + self.module_resty = { openidc = {} } + package.preload["resty.openidc"] = function() + return self.module_resty.openidc + end + + self.handler = require("kong.plugins.oidc.handler")() +end + +function TestHandler:tearDown() + TestHandler.super:tearDown() +end + +function TestHandler:test_header_add() + self.module_resty.openidc.authenticate = function(opts) + return { user = {sub = "sub", email = "ghost@localhost"}, id_token = { sub = "sub", aud = "aud123"} }, false + end + local headers + headers = {} + kong.service.request.set_header = function(name, value) headers[name] = value end + + self.handler:access({ disable_id_token_header = "yes", disable_userinfo_header = "yes", + header_names = { "X-Email", "X-Aud"}, header_claims = { "email", "aud" } }) + lu.assertEquals(headers["X-Email"], "ghost@localhost") + lu.assertEquals(headers["X-Aud"], "aud123") +end + +lu.run() From 0f32935cf4e729d38826b24312cd3e1cf7887f91 Mon Sep 17 00:00:00 2001 From: Hannu Laurila Date: Wed, 13 Jan 2021 16:07:34 +0200 Subject: [PATCH 9/9] Upgrade lua-resty-openidc to 1.7.4-1 --- kong-oidc-1.2.1-1.rockspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kong-oidc-1.2.1-1.rockspec b/kong-oidc-1.2.1-1.rockspec index 5706880d..6e05cd56 100644 --- a/kong-oidc-1.2.1-1.rockspec +++ b/kong-oidc-1.2.1-1.rockspec @@ -22,7 +22,7 @@ description = { license = "Apache 2.0" } dependencies = { - "lua-resty-openidc ~> 1.7.2-1" + "lua-resty-openidc ~> 1.7.4-1" } build = { type = "builtin",