Skip to content

Commit

Permalink
feat(openid-connect): add forbidden claim lists (#9221)
Browse files Browse the repository at this point in the history
  • Loading branch information
Tieske committed Jun 10, 2024
1 parent a89b4d1 commit 225b297
Show file tree
Hide file tree
Showing 8 changed files with 180 additions and 53 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
message: "**OpenID-connect:** Added `claims_forbidden` property to restrict access."
type: feature
scope: Plugin
6 changes: 5 additions & 1 deletion kong/clustering/compat/removed_fields.lua
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,10 @@ return {
[3008000000] = {
aws_lambda = {
"empty_arrays_mode",
}
},
-- Enterprise plugins
openid_connect = {
"claims_forbidden",
},
},
}
18 changes: 17 additions & 1 deletion plugins-ee/openid-connect/kong/plugins/openid-connect/claims.lua
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,21 @@ local function find_claim(token, search, no_transform)
end


-- @tparam table token The token to check for forbidden claims.
-- @tparam table forbidden_claim A list of forbidden claims.
-- @treturn boolean false if the token doesn't have a forbidden claim.
-- @treturn string the name of the first forbidden claim found.
local function has_forbidden_claim(token, forbidden_claims)
for _, claim in ipairs(forbidden_claims) do
if find_claim(token, claim) then
return claim
end
end

return false
end


---compares two timestamps and returns a boolean
---indicating token expiration.
---
Expand Down Expand Up @@ -131,5 +146,6 @@ end
return {
find = find_claim,
exp = get_exp,
token_is_expired = token_is_expired
token_is_expired = token_is_expired,
has_forbidden_claim = has_forbidden_claim,
}
156 changes: 105 additions & 51 deletions plugins-ee/openid-connect/kong/plugins/openid-connect/handler.lua
Original file line number Diff line number Diff line change
Expand Up @@ -1768,77 +1768,126 @@ function OICHandler.access(_, conf)
end
end

local check_required = function(name, required_name, claim_name, default)
local requirements = args.get_conf_arg(required_name)
if requirements then
log("verifying required ", name)
local claim_lookup
if claim_name then
claim_lookup = args.get_conf_arg(claim_name, default)
else
claim_lookup = default

-- @return true if allowed
-- @return nil + error message if not allowed
local check_forbidden_claims = function(optname)
local forbidden_claims = args.get_conf_arg(optname)
if not forbidden_claims or #forbidden_claims == 0 then
return true
end

log("verifying forbidden claims")
if decode_tokens and type(tokens_decoded) ~= "table" then
decode_tokens = false
tokens_decoded, err = oic.token:decode(tokens_encoded, TOKEN_DECODE_OPTS)
if err then
log("error decoding tokens (", err, ")")
end
end

if decode_tokens and type(tokens_decoded) ~= "table" then
decode_tokens = false
tokens_decoded, err = oic.token:decode(tokens_encoded, TOKEN_DECODE_OPTS)
if err then
log("error decoding tokens (", err, ")")
end
if not introspected and type(tokens_decoded) == "table" and type(tokens_decoded.access_token) ~= "table" then
log("introspecting token to verify forbidden claims")
introspection_data, err, introspection_jwt = introspect_token(tokens_encoded.access_token, ttl)
introspected = true
if err then
log("error introspecting token to verify forbidden claims (", err, ")")
end
end

if not introspected and type(tokens_decoded) == "table" and type(tokens_decoded.access_token) ~= "table" then
log("introspecting token to verify required ", name)
introspection_data, err, introspection_jwt = introspect_token(tokens_encoded.access_token, ttl)
introspected = true
if err then
log("error introspecting token to verify required ", name, " (", err, ")")
end
if type(introspection_data) == "table" then
local claim_found = claims.has_forbidden_claim(introspection_data, forbidden_claims)
if claim_found then
return nil, "forbidden claim '" .. claim_found .. "' found in introspection results"
end
end

local access_token_values
if type(introspection_data) == "table" then
access_token_values = claims.find(introspection_data, claim_lookup)
if access_token_values then
log(name, " found in introspection results")
else
log(name, " not found in introspection results")
end
if type(tokens_decoded) == "table" and type(tokens_decoded.access_token) == "table" then
local claim_found = claims.has_forbidden_claim(tokens_decoded.access_token.payload, forbidden_claims)
if claim_found then
return nil, "forbidden claim '" .. claim_found .. "' found in access token"
end
end

if not access_token_values then
if type(tokens_decoded) == "table" and type(tokens_decoded.access_token) == "table" then
access_token_values = claims.find(tokens_decoded.access_token.payload, claim_lookup)
if access_token_values then
log(name, " found in access token")
else
log(name, " not found in access token")
end
end
return true
end


-- @return true if allowed
-- @return nil + error message if not allowed
local check_required = function(name, required_name, claim_name, default)
local requirements = args.get_conf_arg(required_name)
if not requirements then
return true
end

log("verifying required ", name)
local claim_lookup
if claim_name then
claim_lookup = args.get_conf_arg(claim_name, default)
else
claim_lookup = default
end

if decode_tokens and type(tokens_decoded) ~= "table" then
decode_tokens = false
tokens_decoded, err = oic.token:decode(tokens_encoded, TOKEN_DECODE_OPTS)
if err then
log("error decoding tokens (", err, ")")
end
end

if not access_token_values then
return nil, name .. " required but no " .. name .. " found"
if not introspected and type(tokens_decoded) == "table" and type(tokens_decoded.access_token) ~= "table" then
log("introspecting token to verify required ", name)
introspection_data, err, introspection_jwt = introspect_token(tokens_encoded.access_token, ttl)
introspected = true
if err then
log("error introspecting token to verify required ", name, " (", err, ")")
end
end

access_token_values = set.new(access_token_values)
local access_token_values
if type(introspection_data) == "table" then
access_token_values = claims.find(introspection_data, claim_lookup)
if access_token_values then
log(name, " found in introspection results")
else
log(name, " not found in introspection results")
end
end

local has_valid_requirements
for _, requirement in ipairs(requirements) do
if set.has(requirement, access_token_values) then
has_valid_requirements = true
break
if not access_token_values then
if type(tokens_decoded) == "table" and type(tokens_decoded.access_token) == "table" then
access_token_values = claims.find(tokens_decoded.access_token.payload, claim_lookup)
if access_token_values then
log(name, " found in access token")
else
log(name, " not found in access token")
end
end
end

if has_valid_requirements then
log("required ", name, " were found")
if not access_token_values then
return nil, name .. " required but no " .. name .. " found"
end

else
return nil, "required " .. name .. " were not found [ " .. concat(access_token_values, ", ") .. " ]"
access_token_values = set.new(access_token_values)

local has_valid_requirements
for _, requirement in ipairs(requirements) do
if set.has(requirement, access_token_values) then
has_valid_requirements = true
break
end
end

if has_valid_requirements then
log("required ", name, " were found")

else
return nil, "required " .. name .. " were not found [ " .. concat(access_token_values, ", ") .. " ]"
end

return true
end

Expand All @@ -1847,6 +1896,11 @@ function OICHandler.access(_, conf)
return unauthorized(err)
end

ok, err = check_forbidden_claims("claims_forbidden")
if not ok then
return forbidden(err)
end

ok, err = check_required("scopes", "scopes_required", "scopes_claim", { "scope" })
if not ok then
return forbidden(err)
Expand Down
10 changes: 10 additions & 0 deletions plugins-ee/openid-connect/kong/plugins/openid-connect/schema.lua
Original file line number Diff line number Diff line change
Expand Up @@ -2331,6 +2331,16 @@ local config = {
default = 300,
},
},
{
claims_forbidden = {
description = "If given, these claims are forbidden in the token payload.",
required = false,
type = "array",
elements = {
type = "string",
},
},
},
},
shorthand_fields = {
-- TODO: deprecated forms, to be removed in Kong 4.0
Expand Down
37 changes: 37 additions & 0 deletions plugins-ee/openid-connect/spec/openid-connect/05-keycloak_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -2172,6 +2172,32 @@ for _, strategy in helpers.all_strategies() do
},
}

local claimsforbidden = bp.routes:insert {
service = service,
paths = { "/claimsforbidden" },
}

bp.plugins:insert {
route = claimsforbidden,
name = PLUGIN_NAME,
config = {
issuer = ISSUER_URL,
client_id = {
KONG_CLIENT_ID,
},
client_secret = {
KONG_CLIENT_SECRET,
},
scopes_claim = {
"scope",
},
claims_forbidden = {
"preferred_username",
},
display_errors = true,
},
}

local falseaudience = bp.routes:insert {
service = service,
paths = { "/falseaudience" },
Expand Down Expand Up @@ -2520,6 +2546,17 @@ for _, strategy in helpers.all_strategies() do
assert.response(res).has.status(200)
assert.response(res).has.jsonbody()
end)

it("prohibits access if a forbidden claim is present", function()
local res = proxy_client:get("/claimsforbidden", {
headers = {
Authorization = PASSWORD_CREDENTIALS
},
})
assert.response(res).has.status(403)
local json = assert.response(res).has.jsonbody()
assert.same("Forbidden (forbidden claim 'preferred_username' found in access token)", json.message)
end)
end)

describe("[ACL plugin]",function ()
Expand Down
2 changes: 2 additions & 0 deletions spec/02-integration/02-cmd/12-hybrid_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,8 @@ for _, strategy in helpers.each_strategy() do
lazy_teardown(function()
helpers.stop_kong("servroot")
helpers.stop_kong("servroot2")
helpers.clean_prefix("servroot")
helpers.clean_prefix("servroot2")
end)

it("quits gracefully", function()
Expand Down
1 change: 1 addition & 0 deletions spec/02-integration/02-cmd/14-vault_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ local helpers = require "spec.helpers"
for _, strategy in helpers.all_strategies() do
describe("kong vault #" .. strategy, function()
lazy_setup(function()
helpers.clean_prefix()
helpers.get_db_utils(nil, {}) -- runs migrations
end)

Expand Down

0 comments on commit 225b297

Please sign in to comment.