Skip to content

Commit 14564dc

Browse files
committed
[token_introspection] Support client_secret_jwt authentication method
1 parent 1f8c6b9 commit 14564dc

File tree

4 files changed

+342
-10
lines changed

4 files changed

+342
-10
lines changed

gateway/src/apicast/policy/token_introspection/apicast-policy.json

+32-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
"properties": {
1212
"auth_type": {
1313
"type": "string",
14-
"enum": ["use_3scale_oidc_issuer_endpoint", "client_id+client_secret"],
14+
"enum": ["use_3scale_oidc_issuer_endpoint", "client_id+client_secret", "client_secret_jwt"],
1515
"default": "client_id+client_secret"
1616
},
1717
"max_ttl_tokens": {
@@ -61,6 +61,37 @@
6161
"required": [
6262
"client_id", "client_secret", "introspection_url"
6363
]
64+
}, {
65+
"properties": {
66+
"auth_type": {
67+
"describe": "Specify the Token Introspection Endpoint, Client ID, and Client Secret.",
68+
"enum": ["client_secret_jwt"]
69+
},
70+
"client_id": {
71+
"description": "Client ID for the Token Introspection Endpoint",
72+
"type": "string"
73+
},
74+
"client_secret": {
75+
"description": "Client Secret for the Token Introspection Endpoint",
76+
"type": "string"
77+
},
78+
"client_jwt_assertion_expires_in": {
79+
"description": "Duration of the singed JWT in seconds",
80+
"type": "integer",
81+
"default": 60
82+
},
83+
"client_jwt_assertion_audience": {
84+
"description": "Audience claim of the singed JWT",
85+
"type": "string"
86+
},
87+
"introspection_url": {
88+
"description": "Introspection Endpoint URL",
89+
"type": "string"
90+
}
91+
},
92+
"required": [
93+
"client_id", "client_secret", "introspection_url", "client_jwt_assertion_audience"
94+
]
6495
}]
6596
}
6697
}

gateway/src/apicast/policy/token_introspection/token_introspection.lua

+40-9
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ local http_ng = require 'resty.http_ng'
77
local user_agent = require 'apicast.user_agent'
88
local resty_env = require('resty.env')
99
local resty_url = require('resty.url')
10+
local resty_jwt = require('resty.jwt')
1011

1112
local tokens_cache = require('tokens_cache')
1213

@@ -27,11 +28,12 @@ function _M.new(config)
2728
self.auth_type = config.auth_type or "client_id+client_secret"
2829
--- authorization for the token introspection endpoint.
2930
-- https://tools.ietf.org/html/rfc7662#section-2.2
30-
if self.auth_type == "client_id+client_secret" then
31-
self.client_id = self.config.client_id or ''
32-
self.client_secret = self.config.client_secret or ''
33-
self.introspection_url = config.introspection_url
34-
end
31+
-- TODO: check what if multiple values if provided
32+
self.client_id = self.config.client_id or ''
33+
self.client_secret = self.config.client_secret or ''
34+
self.introspection_url = config.introspection_url
35+
self.client_jwt_assertion_expires_in = self.config.client_jwt_assertion_expires_in or 60
36+
self.client_aud = config.client_jwt_assertion_audience or ''
3537
self.http_client = http_ng.new{
3638
backend = config.client,
3739
options = {
@@ -62,14 +64,43 @@ local function introspect_token(self, token)
6264
if cached_token_info then return cached_token_info end
6365

6466
local headers = {}
65-
if self.client_id and self.client_secret then
66-
headers['Authorization'] = create_credential(self.client_id or '', self.client_secret or '')
67+
68+
local body = {
69+
token = token,
70+
token_type_hint = 'access_token'
71+
}
72+
73+
if self.auth_type == "client_id+client_secret" or self.auth_type == "use_3scale_oidc_issuer_endpoint" then
74+
if self.client_id and self.client_secret then
75+
headers['Authorization'] = create_credential(self.client_id or '', self.client_secret or '')
76+
end
77+
elseif self.auth_type == "client_secret_jwt" then
78+
local key = self.client_secret
79+
body.client_id = self.client_id
80+
body.client_assertion_type = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer"
81+
local now = ngx.time()
82+
83+
local assertion = {
84+
header = {
85+
typ = "JWT",
86+
alg = "HS256",
87+
},
88+
payload = {
89+
iss = self.client_id,
90+
sub = self.client_id,
91+
aud = self.client_aud,
92+
jti = ngx.var.request_id,
93+
exp = now + (self.client_jwt_assertion_expires_in and self.client_jwt_assertion_expires_in or 60),
94+
iat = now
95+
}
96+
}
97+
98+
body.client_assertion = resty_jwt:sign(key, assertion)
6799
end
68100

69101
--- Parameters for the token introspection endpoint.
70102
-- https://tools.ietf.org/html/rfc7662#section-2.1
71-
local res, err = self.http_client.post{self.introspection_url , { token = token, token_type_hint = 'access_token'},
72-
headers = headers}
103+
local res, err = self.http_client.post{self.introspection_url , body, headers = headers}
73104
if err then
74105
ngx.log(ngx.WARN, 'token introspection error: ', err, ' url: ', self.introspection_url)
75106
return { active = false }

spec/policy/token_introspection/token_introspection_spec.lua

+89
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ local TokensCache = require('apicast.policy.token_introspection.tokens_cache')
33
local format = string.format
44
local test_backend_client = require('resty.http_ng.backend.test')
55
local cjson = require('cjson')
6+
local resty_jwt = require "resty.jwt"
67
describe("token introspection policy", function()
78
describe("execute introspection", function()
89
local context
@@ -22,6 +23,7 @@ describe("token introspection policy", function()
2223
test_backend = test_backend_client.new()
2324
ngx.var = {}
2425
ngx.var.http_authorization = "Bearer "..test_access_token
26+
ngx.var.request_id = "1234"
2527
context = {
2628
service = {
2729
auth_failed_status = 403,
@@ -330,6 +332,93 @@ describe("token introspection policy", function()
330332

331333
end)
332334

335+
describe('client_secret_jwt introspection auth type', function()
336+
local auth_type = "client_secret_jwt"
337+
local introspection_url = "http://example/token/introspection"
338+
local audience = "http://example/auth/realm/basic"
339+
local policy_config = {
340+
auth_type = auth_type,
341+
introspection_url = introspection_url,
342+
client_id = test_client_id,
343+
client_secret = test_client_secret,
344+
client_jwt_assertion_audience = audience,
345+
}
346+
347+
describe('success with valid token', function()
348+
local token_policy = TokenIntrospection.new(policy_config)
349+
before_each(function()
350+
test_backend
351+
.expect{
352+
url = introspection_url,
353+
method = 'POST',
354+
}
355+
.respond_with{
356+
status = 200,
357+
body = cjson.encode({
358+
active = true
359+
})
360+
}
361+
token_policy.http_client.backend = test_backend
362+
token_policy:access(context)
363+
end)
364+
365+
it('the request does not contains basic auth header', function()
366+
assert.is_nil(test_backend.get_requests()[1].headers['Authorization'])
367+
end)
368+
369+
it('the request does not contains client_secret in body', function()
370+
local body = ngx.decode_args(test_backend.get_requests()[1].body)
371+
assert.is_nil(body.client_secret)
372+
end)
373+
374+
it('the request contains correct fields in body', function()
375+
local body = ngx.decode_args(test_backend.get_requests()[1].body)
376+
assert.same(body.client_id, test_client_id)
377+
assert.same(body.client_assertion_type, "urn:ietf:params:oauth:client-assertion-type:jwt-bearer")
378+
assert.is_not_nil(body.client_assertion)
379+
end)
380+
381+
it("has correct JWT headers", function()
382+
local body = ngx.decode_args(test_backend.get_requests()[1].body)
383+
local jwt_obj = resty_jwt:load_jwt(body.client_assertion)
384+
assert.same(jwt_obj.header.typ, "JWT")
385+
assert.same(jwt_obj.header.alg, "HS256")
386+
end)
387+
388+
it("has correct JWT body", function()
389+
local body = ngx.decode_args(test_backend.get_requests()[1].body)
390+
local jwt_obj = resty_jwt:load_jwt(body.client_assertion)
391+
assert.same(jwt_obj.payload.sub, test_client_id)
392+
assert.same(jwt_obj.payload.iss, test_client_id)
393+
assert.truthy(jwt_obj.signature)
394+
assert.truthy(jwt_obj.payload.jti)
395+
assert.truthy(jwt_obj.payload.exp)
396+
assert.is_true(jwt_obj.payload.exp > os.time())
397+
end)
398+
end)
399+
400+
it('failed with invalid token', function()
401+
test_backend
402+
.expect{
403+
url = introspection_url,
404+
method = 'POST',
405+
}
406+
.respond_with{
407+
status = 200,
408+
body = cjson.encode({
409+
active = false
410+
})
411+
}
412+
stub(ngx, 'say')
413+
stub(ngx, 'exit')
414+
415+
local token_policy = TokenIntrospection.new(policy_config)
416+
token_policy.http_client.backend = test_backend
417+
token_policy:access(context)
418+
assert_authentication_failed()
419+
end)
420+
end)
421+
333422
describe('when caching is enabled', function()
334423
local introspection_url = "http://example/token/introspection"
335424
local policy_config = {

0 commit comments

Comments
 (0)