Skip to content

Commit

Permalink
[token_introspection] Support private_key_jwt authentication method
Browse files Browse the repository at this point in the history
  • Loading branch information
tkan145 committed Jun 4, 2024
1 parent 7aa2216 commit 78c63f8
Show file tree
Hide file tree
Showing 4 changed files with 449 additions and 6 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"properties": {
"auth_type": {
"type": "string",
"enum": ["use_3scale_oidc_issuer_endpoint", "client_id+client_secret", "client_secret_jwt"],
"enum": ["use_3scale_oidc_issuer_endpoint", "client_id+client_secret", "client_secret_jwt", "private_key_jwt"],
"default": "client_id+client_secret"
},
"max_ttl_tokens": {
Expand Down Expand Up @@ -84,6 +84,11 @@
"description": "Audience. The aud claim of the singed JWT. The audience SHOULD be the URL of the Authorization Server’s Token Endpoint.",
"type": "string"
},
"client_jwt_assertion_algorithm": {
"type": "string",
"enum": ["HS256"],
"default": "HS256"
},
"introspection_url": {
"description": "Introspection Endpoint URL",
"type": "string"
Expand All @@ -92,6 +97,78 @@
"required": [
"client_id", "client_secret", "introspection_url", "client_jwt_assertion_audience"
]
}, {
"properties": {
"auth_type": {
"describe": "Authenticate with client_secret_jwt method defined in https://openid.net/specs/openid-connect-core-1_0.html#ClientAuthentication",
"enum": ["private_key_jwt"]
},
"client_id": {
"description": "Client ID for the Token Introspection Endpoint",
"type": "string"
},
"introspection_url": {
"description": "Introspection Endpoint URL",
"type": "string"
},
"client_jwt_assertion_expires_in": {
"description": "Duration of the singed JWT in seconds",
"type": "integer",
"default": 60
},
"client_jwt_assertion_audience": {
"description": "Audience. The aud claim of the singed JWT. The audience SHOULD be the URL of the Authorization Server’s Token Endpoint.",
"type": "string"
},
"client_jwt_assertion_algorithm": {
"type": "string",
"enum": ["RS256"],
"default": "RS256"
},
"certificate_type": {
"title": "Certificate type",
"type": "string",
"enum": [
"path",
"embedded"
],
"default": "path"
}
},
"dependencies": {
"certificate_type": {
"oneOf": [
{
"properties": {
"certificate_type": {
"const": "embedded"
},
"certificate": {
"title": "Certificate",
"description": "Client RSA private key used to sign JWT.",
"format": "data-url",
"type": "string"
}
}
},
{
"properties": {
"certificate_type": {
"const": "path"
},
"certificate": {
"title": "Certificate",
"description": "Client RSA private key used to sign JWT.",
"type": "string"
}
}
}
]
}
},
"required": [
"client_id", "introspection_url", "client_jwt_assertion_audience", "certificate_type"
]
}]
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ local user_agent = require 'apicast.user_agent'
local resty_env = require('resty.env')
local resty_url = require('resty.url')
local resty_jwt = require('resty.jwt')
local data_url = require('resty.data_url')
local util = require 'apicast.util'
local pcall = pcall

local tokens_cache = require('tokens_cache')

Expand All @@ -17,6 +20,35 @@ local new = _M.new

local noop = function() end
local noop_cache = { get = noop, set = noop }
local path_type = "path"
local embedded_type = "embedded"

local function get_cert(value, value_type)
if value_type == path_type then
ngx.log(ngx.DEBUG, "reading path:", value)
return util.read_file(value)
end

if value_type == embedded_type then
local parsed_data, err = data_url.parse(value)
if err then
ngx.log(ngx.ERR, "Cannot parse certificate content: ", err)
return nil

Check warning on line 36 in gateway/src/apicast/policy/token_introspection/token_introspection.lua

View check run for this annotation

Codecov / codecov/patch

gateway/src/apicast/policy/token_introspection/token_introspection.lua#L35-L36

Added lines #L35 - L36 were not covered by tests
end
return parsed_data.data
end
end

-- lua-resty-jwt expects certificate as a string, thereforce we will return the certificate key
-- as is
local function read_certificate_key(value, value_type)
local data, err = get_cert(value, value_type)
if data == nil then
ngx.log(ngx.ERR, "Certificate value is invalid, err: ", err)
return

Check warning on line 48 in gateway/src/apicast/policy/token_introspection/token_introspection.lua

View check run for this annotation

Codecov / codecov/patch

gateway/src/apicast/policy/token_introspection/token_introspection.lua#L47-L48

Added lines #L47 - L48 were not covered by tests
end
return data
end

local function create_credential(client_id, client_secret)
return 'Basic ' .. ngx.encode_base64(table.concat({ client_id, client_secret }, ':'))
Expand All @@ -33,9 +65,16 @@ function _M.new(config)
self.client_secret = self.config.client_secret or ''
self.introspection_url = config.introspection_url

if self.auth_type == "client_secret_jwt" then
if self.auth_type == "client_secret_jwt" or self.auth_type == "private_key_jwt" then
self.client_jwt_assertion_expires_in = self.config.client_jwt_assertion_expires_in or 60
self.client_aud = config.client_jwt_assertion_audience or ''
self.client_algorithm = config.client_jwt_assertion_algorithm
end

if self.auth_type == "private_key_jwt" then
self.client_rsa_private_key = read_certificate_key(
config.certificate,
config.certificate_type or path_type)
end
end
self.http_client = http_ng.new{
Expand Down Expand Up @@ -76,16 +115,21 @@ local function introspect_token(self, token)

if self.auth_type == "client_id+client_secret" or self.auth_type == "use_3scale_oidc_issuer_endpoint" then
headers['Authorization'] = create_credential(self.client_id or '', self.client_secret or '')
elseif self.auth_type == "client_secret_jwt" then
local key = self.client_secret
elseif self.auth_type == "client_secret_jwt" or self.auth_type == "private_key_jwt" then
local key = self.auth_type == "client_secret_jwt" and self.client_secret or self.client_rsa_private_key
if not key then
ngx.log(ngx.WARN, "token introspection can't use " .. self.auth_type .. " without a key.")
return {active = false}

Check warning on line 122 in gateway/src/apicast/policy/token_introspection/token_introspection.lua

View check run for this annotation

Codecov / codecov/patch

gateway/src/apicast/policy/token_introspection/token_introspection.lua#L121-L122

Added lines #L121 - L122 were not covered by tests
end

body.client_id = self.client_id
body.client_assertion_type = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer"
local now = ngx.time()

local assertion = {
header = {
typ = "JWT",
alg = "HS256",
alg = self.client_algorithm,
},
payload = {
iss = self.client_id,
Expand All @@ -97,7 +141,15 @@ local function introspect_token(self, token)
}
}

body.client_assertion = resty_jwt:sign(key, assertion)
-- use pcall here to catch error throw by lua-rest-jwt sign method.
local ok, client_assertion = pcall(resty_jwt.sign, _M, key, assertion)

if not ok then
ngx.log(ngx.WARN, "token introspection failed to sign JWT token ,err: ", client_assertion.reason)
return {active = false}

Check warning on line 149 in gateway/src/apicast/policy/token_introspection/token_introspection.lua

View check run for this annotation

Codecov / codecov/patch

gateway/src/apicast/policy/token_introspection/token_introspection.lua#L148-L149

Added lines #L148 - L149 were not covered by tests
end

body.client_assertion = client_assertion
end

--- Parameters for the token introspection endpoint.
Expand Down
130 changes: 130 additions & 0 deletions spec/policy/token_introspection/token_introspection_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ local format = string.format
local test_backend_client = require('resty.http_ng.backend.test')
local cjson = require('cjson')
local resty_jwt = require "resty.jwt"
local util = require 'apicast.util'

describe("token introspection policy", function()
describe("execute introspection", function()
local context
Expand Down Expand Up @@ -419,6 +421,134 @@ describe("token introspection policy", function()
end)
end)

describe('private_key_jwt introspection auth type', function()
local auth_type = "private_key_jwt"
local introspection_url = "http://example/token/introspection"
local audience = "http://example/auth/realm/basic"
local certificate_path = 't/fixtures/rsa.pem'
local path_type = "path"
local embedded_type = "embedded"

describe("certificate validation", function()
it("Reads correctly the file path", function()
local policy_config = {
auth_type = auth_type,
introspection_url = introspection_url,
client_id = test_client_id,
client_jwt_assertion_audience = audience,
certificate = certificate_path,
certificate_type = path_type,
}
local token_policy = TokenIntrospection.new(policy_config)
assert.truthy(token_policy.client_rsa_private_key)
end)

it("Read correctly the embedded path", function()
local policy_config = {
auth_type = auth_type,
introspection_url = introspection_url,
client_id = test_client_id,
client_jwt_assertion_audience = audience,
certificate_type = embedded_type,
certificate = "data:application/x-x509-ca-cert;name=rsa.pem;base64,LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlCUEFJQkFBSkJBTENsejk2Y0RROTY1RU5ZTWZaekcrQWN1MjVscHgyS05wQUFMQlErY2F0Q0E1OXVzNyt1CkxZNXJqUVI2U09nWnBDejVQSmlLTkFkUlBESk1YU21YcU0wQ0F3RUFBUUpCQUpud1phNEJJQUNWZjhhUVhUb0EKSmhLdjkwYkZuMVRHMWJXMzhMSFRtUXM4RU05WENtZ2hMV0NqZTdkL05iVXJVY2VvdElPbmp0di94SFR5d0d0MgpOd0VDSVFEaHZNWkRRK1pSUmJid09OY3ZPOUc3aDZoRmd5MG9raXY2SmNpWmNjdnR4UUloQU1oVVRBV2dWMWhRCk8yeVdUUllSUVpvc0VJc0ZCM2taZnNMTWVUS2prOGRwQWlFQXNsc1o5Mm05bjNkS3JKRHNqRmhpUlI1Uk9PTUYKR2lvcjd4Qk5aOWUrdmRVQ0lEc2pmNG5OcXR0Y1hCNlRSRkIyYWFwc3hibDBrNTh4WXBWNUxYSkFqZmk1QWlFQQp2UmFTYXVCZlJDUDNKZ1hITmdjRFNXMDE3L0J0YndHaXo4YUlUdjZCMEZ3PQotLS0tLUVORCBSU0EgUFJJVkFURSBLRVktLS0tLQo="
}
local token_policy = TokenIntrospection.new(policy_config)
assert.truthy(token_policy.client_rsa_private_key)
end)
end)

describe('success with valid token', function()
local policy_config = {
auth_type = auth_type,
introspection_url = introspection_url,
client_id = test_client_id,
client_jwt_assertion_audience = audience,
certificate = certificate_path,
certificate_type = path_type,
}
before_each(function()
test_backend
.expect{
url = introspection_url,
method = 'POST',
}
.respond_with{
status = 200,
body = cjson.encode({
active = true
})
}
local token_policy = TokenIntrospection.new(policy_config)
token_policy.http_client.backend = test_backend
token_policy:access(context)
end)

it('the request does not contains basic auth header', function()
assert.is_nil(test_backend.get_requests()[1].headers['Authorization'])
end)

it('the request does not contains client_secret in body', function()
local body = ngx.decode_args(test_backend.get_requests()[1].body)
assert.is_nil(body.client_secret)
end)

it('the request contains correct fields in body', function()
local body = ngx.decode_args(test_backend.get_requests()[1].body)
assert.same(body.client_id, test_client_id)
assert.same(body.client_assertion_type, "urn:ietf:params:oauth:client-assertion-type:jwt-bearer")
assert.is_not_nil(body.client_assertion)
end)

it("has correct JWT headers", function()
local body = ngx.decode_args(test_backend.get_requests()[1].body)
local jwt_obj = resty_jwt:load_jwt(body.client_assertion)
assert.same(jwt_obj.header.typ, "JWT")
assert.same(jwt_obj.header.alg, "RS256")
end)

it("has correct JWT body", function()
local body = ngx.decode_args(test_backend.get_requests()[1].body)
local jwt_obj = resty_jwt:load_jwt(body.client_assertion)
assert.same(jwt_obj.payload.sub, test_client_id)
assert.same(jwt_obj.payload.iss, test_client_id)
assert.truthy(jwt_obj.signature)
assert.truthy(jwt_obj.payload.jti)
assert.truthy(jwt_obj.payload.exp)
assert.is_true(jwt_obj.payload.exp > os.time())
end)
end)

it('failed with invalid token', function()
local policy_config = {
auth_type = auth_type,
introspection_url = introspection_url,
client_id = test_client_id,
client_secret = test_client_secret,
client_jwt_assertion_audience = audience,
certificate_type= "path",
certificate = certificate_path
}
test_backend
.expect{
url = introspection_url,
method = 'POST',
}
.respond_with{
status = 200,
body = cjson.encode({
active = false
})
}
stub(ngx, 'say')
stub(ngx, 'exit')

local token_policy = TokenIntrospection.new(policy_config)
token_policy.http_client.backend = test_backend
token_policy:access(context)
assert_authentication_failed()
end)
end)

describe('when caching is enabled', function()
local introspection_url = "http://example/token/introspection"
local policy_config = {
Expand Down
Loading

0 comments on commit 78c63f8

Please sign in to comment.