Skip to content
This repository has been archived by the owner on Apr 18, 2024. It is now read-only.

Bearer JWT verify, auth/acl chaining, group/credential handling fixes #2

Merged
merged 9 commits into from
Jan 13, 2021
4 changes: 2 additions & 2 deletions .env
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -97,6 +99,12 @@ 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 |
| `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

Expand Down Expand Up @@ -153,6 +161,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
Expand Down
2 changes: 1 addition & 1 deletion kong-oidc-1.2.1-1.rockspec
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
83 changes: 81 additions & 2 deletions kong/plugins/oidc/handler.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -27,21 +35,48 @@ 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.injectHeaders(oidcConfig.header_names, oidcConfig.header_claims, { response })
if not oidcConfig.disable_userinfo_header then
utils.injectUser(response, oidcConfig.userinfo_header_name)
end
return
end
end

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.injectHeaders(oidcConfig.header_names, oidcConfig.header_claims, { response })
if not oidcConfig.disable_userinfo_header then
utils.injectUser(response, oidcConfig.userinfo_header_name)
end
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
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)
utils.injectGroups(response.user, oidcConfig.groups_claim)
end
if (not oidcConfig.disable_access_token_header
and response.access_token) then
Expand Down Expand Up @@ -94,4 +129,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
8 changes: 7 additions & 1 deletion kong/plugins/oidc/schema.lua
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,12 @@ 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" },
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 = {} },
}
}
107 changes: 103 additions & 4 deletions kong/plugins/oidc/utils.lua
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
local cjson = require("cjson")
local constants = require "kong.constants"

local M = {}

Expand Down Expand Up @@ -70,7 +71,13 @@ 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,
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,
header_names = config.header_names or {},
header_claims = config.header_claims or {}
}
end

Expand All @@ -80,6 +87,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
Expand All @@ -95,12 +147,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
ngx.ctx.authenticated_credential = tmp_user
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
Expand All @@ -111,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
Expand All @@ -122,4 +198,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
3 changes: 2 additions & 1 deletion test/docker/integration/Dockerfile
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
6 changes: 4 additions & 2 deletions test/docker/unit/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
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

# 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

Expand Down
Loading