Skip to content

Commit

Permalink
Merge pull request #1464 from tkan145/token_introspection_auth
Browse files Browse the repository at this point in the history
THREESCALE-11015 - Support client_secret_jwt and private_key_jwt as authentication type for token introspection policy
  • Loading branch information
tkan145 committed Jul 8, 2024
2 parents 907db14 + 3a7aee4 commit bbc60a8
Show file tree
Hide file tree
Showing 4 changed files with 779 additions and 6 deletions.
100 changes: 99 additions & 1 deletion gateway/src/apicast/policy/token_introspection/apicast-policy.json
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"],
"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 @@ -61,6 +61,104 @@
"required": [
"client_id", "client_secret", "introspection_url"
]
}, {
"properties": {
"auth_type": {
"describe": "Authenticate with client_secret_jwt method defined in https://openid.net/specs/openid-connect-core-1_0.html#ClientAuthentication",
"enum": ["client_secret_jwt"]
},
"client_id": {
"description": "Client ID for the Token Introspection Endpoint",
"type": "string"
},
"client_secret": {
"description": "Client Secret for the Token Introspection Endpoint",
"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"
},
"introspection_url": {
"description": "Introspection Endpoint URL",
"type": "string"
}
},
"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"
},
"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 @@ -7,6 +7,10 @@ local http_ng = require 'resty.http_ng'
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 @@ -16,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
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
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 @@ -27,9 +60,21 @@ function _M.new(config)
self.auth_type = config.auth_type or "client_id+client_secret"
--- authorization for the token introspection endpoint.
-- https://tools.ietf.org/html/rfc7662#section-2.2
if self.auth_type == "client_id+client_secret" then
self.credential = create_credential(self.config.client_id or '', self.config.client_secret or '')
if self.auth_type ~= "use_3scale_oidc_issuer_endpoint" then
self.client_id = self.config.client_id or ''
self.client_secret = self.config.client_secret or ''
self.introspection_url = config.introspection_url

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 ''
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{
backend = config.client,
Expand Down Expand Up @@ -60,10 +105,55 @@ local function introspect_token(self, token)
local cached_token_info = self.tokens_cache:get(token)
if cached_token_info then return cached_token_info end

local headers = {}

local body = {
token = token,
token_type_hint = 'access_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" 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}
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 = self.auth_type == "client_secret_jwt" and "HS256" or "RS256",
},
payload = {
iss = self.client_id,
sub = self.client_id,
aud = self.client_aud,
jti = ngx.var.request_id,
exp = now + (self.client_jwt_assertion_expires_in and self.client_jwt_assertion_expires_in or 60),
iat = now
}
}

-- 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}
end

body.client_assertion = client_assertion
end

--- Parameters for the token introspection endpoint.
-- https://tools.ietf.org/html/rfc7662#section-2.1
local res, err = self.http_client.post{self.introspection_url , { token = token, token_type_hint = 'access_token'},
headers = {['Authorization'] = self.credential}}
local res, err = self.http_client.post{self.introspection_url , body, headers = headers}
if err then
ngx.log(ngx.WARN, 'token introspection error: ', err, ' url: ', self.introspection_url)
return { active = false }
Expand Down Expand Up @@ -93,7 +183,8 @@ function _M:access(context)
end

local components = resty_url.parse(context.service.oidc.issuer_endpoint)
self.credential = create_credential(components.user, components.password)
self.client_id = components.user
self.client_secret = components.password
local oauth_config = context.proxy.oauth.config
-- token_introspection_endpoint being deprecated in RH SSO 7.4 and removed in 7.5
-- https://access.redhat.com/documentation/en-us/red_hat_single_sign-on/7.5/html-single/upgrading_guide/index#non_standard_token_introspection_endpoint_removed
Expand Down
Loading

0 comments on commit bbc60a8

Please sign in to comment.