Skip to content

Commit

Permalink
[token_introspection] Support client_secret_jwt authentication method
Browse files Browse the repository at this point in the history
  • Loading branch information
tkan145 committed May 30, 2024
1 parent 1f8c6b9 commit 14564dc
Show file tree
Hide file tree
Showing 4 changed files with 342 additions and 10 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"],
"enum": ["use_3scale_oidc_issuer_endpoint", "client_id+client_secret", "client_secret_jwt"],
"default": "client_id+client_secret"
},
"max_ttl_tokens": {
Expand Down Expand Up @@ -61,6 +61,37 @@
"required": [
"client_id", "client_secret", "introspection_url"
]
}, {
"properties": {
"auth_type": {
"describe": "Specify the Token Introspection Endpoint, Client ID, and Client Secret.",
"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 claim of the singed JWT",
"type": "string"
},
"introspection_url": {
"description": "Introspection Endpoint URL",
"type": "string"
}
},
"required": [
"client_id", "client_secret", "introspection_url", "client_jwt_assertion_audience"
]
}]
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ 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 tokens_cache = require('tokens_cache')

Expand All @@ -27,11 +28,12 @@ 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.client_id = self.config.client_id or ''
self.client_secret = self.config.client_secret or ''
self.introspection_url = config.introspection_url
end
-- TODO: check what if multiple values if provided
self.client_id = self.config.client_id or ''
self.client_secret = self.config.client_secret or ''
self.introspection_url = config.introspection_url
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.http_client = http_ng.new{
backend = config.client,
options = {
Expand Down Expand Up @@ -62,14 +64,43 @@ local function introspect_token(self, token)
if cached_token_info then return cached_token_info end

local headers = {}
if self.client_id and self.client_secret then
headers['Authorization'] = create_credential(self.client_id or '', self.client_secret or '')

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
if self.client_id and self.client_secret then
headers['Authorization'] = create_credential(self.client_id or '', self.client_secret or '')
end
elseif self.auth_type == "client_secret_jwt" then
local key = self.client_secret
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",
},
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
}
}

body.client_assertion = resty_jwt:sign(key, 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 = headers}
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
89 changes: 89 additions & 0 deletions spec/policy/token_introspection/token_introspection_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ local TokensCache = require('apicast.policy.token_introspection.tokens_cache')
local format = string.format
local test_backend_client = require('resty.http_ng.backend.test')
local cjson = require('cjson')
local resty_jwt = require "resty.jwt"
describe("token introspection policy", function()
describe("execute introspection", function()
local context
Expand All @@ -22,6 +23,7 @@ describe("token introspection policy", function()
test_backend = test_backend_client.new()
ngx.var = {}
ngx.var.http_authorization = "Bearer "..test_access_token
ngx.var.request_id = "1234"
context = {
service = {
auth_failed_status = 403,
Expand Down Expand Up @@ -330,6 +332,93 @@ describe("token introspection policy", function()

end)

describe('client_secret_jwt introspection auth type', function()
local auth_type = "client_secret_jwt"
local introspection_url = "http://example/token/introspection"
local audience = "http://example/auth/realm/basic"
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,
}

describe('success with valid token', function()
local token_policy = TokenIntrospection.new(policy_config)
before_each(function()
test_backend
.expect{
url = introspection_url,
method = 'POST',
}
.respond_with{
status = 200,
body = cjson.encode({
active = true
})
}
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, "HS256")
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()
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 14564dc

Please sign in to comment.