Skip to content

Commit e4f7cd7

Browse files
authored
Merge pull request #1391 from 3scale/backport-THREESCALE-9009-fix-oidc-issuer-verification
Backport 2.12 THREESCALE-9009 fix OIDC issuer verification
2 parents 109811b + 91c0b46 commit e4f7cd7

File tree

4 files changed

+218
-8
lines changed

4 files changed

+218
-8
lines changed

CHANGELOG.md

+20-2
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,26 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](http://keepachangelog.com/)
66
and this project adheres to [Semantic Versioning](http://semver.org/).
77

8-
9-
## [3.11.0] 2021-09-03
108
## [Unreleased]
119

10+
## [3.12.2] 2023-02-21
11+
12+
- Fixed: OIDC jwt key verification [PR #1391](https://github.com/3scale/APIcast/pull/1391) [THREESCALE-9009](https://issues.redhat.com/browse/THREESCALE-9009)
13+
14+
## [3.12.0] 2022-07-07
15+
16+
### Fixed
17+
18+
- Fixed warning messages [PR #1318](https://github.com/3scale/APIcast/pull/1318) [THREESCALE-7906](https://issues.redhat.com/browse/THREESCALE-7906)
19+
- Fixed dirty context [PR #1328](https://github.com/3scale/APIcast/pull/1328) [THREESCALE-8000](https://issues.redhat.com/browse/THREESCALE-8000) [THREESCALE-8007](https://issues.redhat.com/browse/THREESCALE-8007)
20+
- Fixed jwk alg confusion [PR #1329](https://github.com/3scale/APIcast/pull/1329) [THREESCALE-8249](https://issues.redhat.com/browse/THREESCALE-8249)
21+
- Fixed issue with resolving target server hostnames to IP when using CONNECT method [PR #1323](https://github.com/3scale/APIcast/pull/1323) [THREESCALE-7967](https://issues.redhat.com/browse/THREESCALE-7967)
22+
- Fixed issue with resolving target server hostnames to IPs when forwarding requests through http/s proxy [PR #1323](https://github.com/3scale/APIcast/pull/1323) [THREESCALE-7967](https://issues.redhat.com/browse/THREESCALE-7967)
23+
- Fixed dirty context [PR #1328](https://github.com/3scale/APIcast/pull/1328) [THREESCALE-8000](https://issues.redhat.com/browse/THREESCALE-8000) [THREESCALE-8007](https://issues.redhat.com/browse/THREESCALE-8007) [THREESCALE-8252](https://issues.redhat.com/browse/THREESCALE-8252)
24+
- Fixed dirty context (part 2 of PR #1328) when tls termination policy is in the policy chain [PR #1333](https://github.com/3scale/APIcast/pull/1333)
25+
26+
## [3.11.0] 2022-02-17
27+
1228
### Fixed
1329

1430
- Fixed hostname_rewrite incompatibility with Routing Policy [PR #1263](https://github.com/3scale/APIcast/pull/1263) [THREESCALE-6723](https://issues.redhat.com/browse/THREESCALE-6723)
@@ -948,3 +964,5 @@ Apart from the changes mentioned in this section, this version also includes the
948964
[3.10.0-beta1]: https://github.com/3scale/apicast/compare/v3.10.0-alpha2..v3.10.0-beta1
949965
[3.10.0]: https://github.com/3scale/apicast/compare/v3.10.0-beta1..v3.10.0
950966
[3.11.0]: https://github.com/3scale/apicast/compare/v3.10.0..v3.11.0
967+
[3.12.0]: https://github.com/3scale/apicast/compare/v3.11.0..v3.12.0
968+
[3.12.2]: https://github.com/3scale/apicast/compare/v3.12.0..v3.12.2

gateway/src/apicast/oauth/oidc.lua

+9-2
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ end
105105

106106
local function find_jwk(jwt, keys)
107107
local jwk = keys and keys[jwt.header.kid]
108-
if jwk then return jwk end
108+
return jwk
109109
end
110110

111111
-- Parses the token - in this case we assume it's a JWT token
@@ -185,8 +185,15 @@ function _M:verify(jwt, cache_key)
185185
-- Find jwk with matching kid for current JWT in request
186186
local jwk_obj = find_jwk(jwt, self.keys)
187187

188+
if jwk_obj == nil then
189+
ngx.log(ngx.ERR, "[jwt] failed verification for kid: ", jwt.header.kid)
190+
return false, '[jwk] not found, token might belong to a different realm'
191+
end
192+
188193
local pubkey = jwk_obj.pem
189-
if jwk_obj.alg ~= jwt.header.alg then
194+
-- Check the jwk for the alg field and if not present skip the validation as it is
195+
-- OPTIONAL according to https://www.rfc-editor.org/rfc/rfc7517#section-4.4
196+
if jwk_obj.alg and jwk_obj.alg ~= jwt.header.alg then
190197
return false, '[jwt] alg mismatch'
191198
end
192199

spec/oauth/oidc_spec.lua

+60
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,11 @@ describe('OIDC', function()
4545
config = { id_token_signing_alg_values_supported = { 'RS256', 'HS256' } },
4646
keys = { somekid = { pem = rsa.pub, alg = 'RS256' } },
4747
}
48+
local oidc_config_no_alg = {
49+
issuer = 'https://example.com/auth/realms/apicast',
50+
config = { id_token_signing_alg_values_supported = { 'RS256', 'HS256' } },
51+
keys = { somekid = { pem = rsa.pub } },
52+
}
4853

4954
before_each(function() jwt_validators.set_system_clock(function() return 0 end) end)
5055

@@ -268,6 +273,61 @@ describe('OIDC', function()
268273
assert(credentials, err)
269274
end)
270275

276+
it('validation passes when jwk.alg does not exist', function()
277+
local oidc = _M.new(oidc_config_no_alg)
278+
local access_token = jwt:sign(rsa.private, {
279+
header = { typ = 'JWT', alg = 'RS256', kid = 'somekid' },
280+
payload = {
281+
iss = oidc_config.issuer,
282+
aud = 'notused',
283+
azp = 'ce3b2e5e',
284+
sub = 'someone',
285+
nbf = 0,
286+
exp = ngx.now() + 10,
287+
typ = 'Bearer'
288+
},
289+
})
290+
291+
local credentials, _, _, err = oidc:transform_credentials({ access_token = access_token })
292+
assert(credentials, err)
293+
end)
294+
295+
it('token was signed by a different key', function()
296+
local oidc = _M.new(oidc_config)
297+
local access_token = jwt:sign(rsa.private, {
298+
header = { typ = 'JWT', alg = 'RS256', kid = 'otherkid' },
299+
payload = {
300+
iss = oidc_config.issuer,
301+
aud = 'notused',
302+
azp = 'ce3b2e5e',
303+
sub = 'someone',
304+
exp = ngx.now() + 10,
305+
},
306+
})
307+
308+
local credentials, _, _, err = oidc:transform_credentials({ access_token = access_token })
309+
310+
assert.match('[jwk] not found, token might belong to a different realm', err, nil, true)
311+
end)
312+
313+
it('token was signed by a different issuer', function()
314+
local oidc = _M.new(oidc_config)
315+
local access_token = jwt:sign(rsa.private, {
316+
header = { typ = 'JWT', alg = 'RS256', kid = 'somekid' },
317+
payload = {
318+
iss = 'other_issuer',
319+
aud = 'notused',
320+
azp = 'ce3b2e5e',
321+
sub = 'someone',
322+
exp = ngx.now() + 10,
323+
},
324+
})
325+
326+
local credentials, _, _, err = oidc:transform_credentials({ access_token = access_token })
327+
328+
assert.match('Claim \'iss\' (\'other_issuer\') returned failure', err, nil, true)
329+
end)
330+
271331
describe('getting client_id from any JWT claim', function()
272332

273333
before_each(function()

t/apicast-oidc.t

+129-4
Original file line numberDiff line numberDiff line change
@@ -264,9 +264,7 @@ my $jwt = encode_jwt(payload => {
264264
--- no_error_log
265265
[error]
266266
267-
268-
269-
=== TEST 2: JWT verification fails when no alg is present in the jwk to match against jwt.header.alg
267+
=== TEST 6: JWT verification does not fail when no alg is present in the jwk to match against jwt.header.alg
270268
--- configuration env eval
271269
use JSON qw(to_json);
272270
@@ -303,7 +301,7 @@ to_json({
303301
}
304302
}
305303
--- request: GET /test
306-
--- error_code: 403
304+
--- error_code: 200
307305
--- more_headers eval
308306
use Crypt::JWT qw(encode_jwt);
309307
my $jwt = encode_jwt(payload => {
@@ -313,5 +311,132 @@ my $jwt = encode_jwt(payload => {
313311
iss => 'https://example.com/auth/realms/apicast',
314312
exp => time + 3600 }, key => \$::private_key, alg => 'RS256', extra_headers => { kid => 'somekid' });
315313
"Authorization: Bearer $jwt"
314+
--- no_error_log
315+
316+
=== TEST 7: JWT verification fails when jwk.alg exists AND does not match jwt.header.alg
317+
(see THREESCALE-8249 for steps to generate tampered JWT. rsa.pub from fixtures used to sign)
318+
--- configuration env eval
319+
use JSON qw(to_json);
320+
321+
to_json({
322+
services => [{
323+
id => 42,
324+
backend_version => 'oauth',
325+
backend_authentication_type => 'provider_key',
326+
backend_authentication_value => 'fookey',
327+
proxy => {
328+
authentication_method => 'oidc',
329+
oidc_issuer_endpoint => 'https://example.com/auth/realms/apicast',
330+
api_backend => "http://test:$TEST_NGINX_SERVER_PORT/",
331+
proxy_rules => [
332+
{ pattern => '/', http_method => 'GET', metric_system_name => 'hits', delta => 1 }
333+
]
334+
}
335+
}],
336+
oidc => [{
337+
issuer => 'https://example.com/auth/realms/apicast',
338+
config => { id_token_signing_alg_values_supported => [ 'RS256', 'HS256' ] },
339+
keys => { somekid => { pem => $::public_key, alg => 'RS256' } },
340+
}]
341+
});
342+
--- upstream
343+
location /test {
344+
echo "yes";
345+
}
346+
--- backend
347+
location = /transactions/oauth_authrep.xml {
348+
content_by_lua_block {
349+
local expected = "provider_key=fookey&service_id=42&usage%5Bhits%5D=1&app_id=appid"
350+
require('luassert').same(ngx.decode_args(expected), ngx.req.get_uri_args(0))
351+
}
352+
}
353+
--- request: GET /test
354+
--- error_code: 403
355+
--- more_headers eval
356+
use Crypt::JWT qw(encode_jwt);
357+
my $jwt = 'eyJraWQiOiJzb21la2lkIiwiYWxnIjoiSFMyNTYifQ.'.
358+
'eyJleHAiOjcxNzA1MzE2NDMwLCJhenAiOiJhcHBpZCIsInN1YiI6In'.
359+
'NvbWVvbmUiLCJhdWQiOiJzb21ldGhpbmciLCJpc3MiOiJodHRwczov'.
360+
'L2V4YW1wbGUuY29tL2F1dGgvcmVhbG1zL2FwaWNhc3QifQ.1rFq5QN'.
361+
'b99W6aqQjsx7GJGLDpdkDLI6-huZLzMAmxGQ';
362+
"Authorization: Bearer $jwt"
316363
--- error_log
317364
[jwt] alg mismatch
365+
366+
=== TEST 8: Token was signed by a different key
367+
--- configuration env eval
368+
use JSON qw(to_json);
369+
370+
to_json({
371+
services => [{
372+
id => 42,
373+
backend_version => 'oauth',
374+
backend_authentication_type => 'provider_key',
375+
backend_authentication_value => 'fookey',
376+
proxy => {
377+
authentication_method => 'oidc',
378+
oidc_issuer_endpoint => 'https://example.com/auth/realms/a',
379+
api_backend => "http://test:$TEST_NGINX_SERVER_PORT/",
380+
proxy_rules => [
381+
{ pattern => '/', http_method => 'GET', metric_system_name => 'hits', delta => 1 }
382+
]
383+
}
384+
}],
385+
oidc => [{
386+
issuer => 'https://example.com/auth/realms/a',
387+
config => { id_token_signing_alg_values_supported => [ 'RS256' ] },
388+
keys => { somekid => { pem => $::public_key, alg => 'RS256' } },
389+
}]
390+
});
391+
--- request: GET /test
392+
--- error_code: 403
393+
--- more_headers eval
394+
use Crypt::JWT qw(encode_jwt);
395+
my $jwt = encode_jwt(payload => {
396+
aud => 'something',
397+
azp => 'appid',
398+
sub => 'someone',
399+
iss => 'https://example.com/auth/realms/b',
400+
exp => time + 3600 }, key => \$::private_key, alg => 'RS256', extra_headers => { kid => 'otherkid' });
401+
"Authorization: Bearer $jwt"
402+
--- error_log
403+
[jwk] not found, token might belong to a different realm
404+
405+
=== TEST 9: Token was signed by a different issuer
406+
--- configuration env eval
407+
use JSON qw(to_json);
408+
409+
to_json({
410+
services => [{
411+
id => 42,
412+
backend_version => 'oauth',
413+
backend_authentication_type => 'provider_key',
414+
backend_authentication_value => 'fookey',
415+
proxy => {
416+
authentication_method => 'oidc',
417+
oidc_issuer_endpoint => 'https://example.com/auth/realms/apicast',
418+
api_backend => "http://test:$TEST_NGINX_SERVER_PORT/",
419+
proxy_rules => [
420+
{ pattern => '/', http_method => 'GET', metric_system_name => 'hits', delta => 1 }
421+
]
422+
}
423+
}],
424+
oidc => [{
425+
issuer => 'https://example.com/auth/realms/apicast',
426+
config => { id_token_signing_alg_values_supported => [ 'RS256' ] },
427+
keys => { somekid => { pem => $::public_key, alg => 'RS256' } },
428+
}]
429+
});
430+
--- request: GET /test
431+
--- error_code: 403
432+
--- more_headers eval
433+
use Crypt::JWT qw(encode_jwt);
434+
my $jwt = encode_jwt(payload => {
435+
aud => 'something',
436+
azp => 'appid',
437+
sub => 'someone',
438+
iss => 'unexpected_issuer',
439+
exp => time + 3600 }, key => \$::private_key, alg => 'RS256', extra_headers => { kid => 'somekid' });
440+
"Authorization: Bearer $jwt"
441+
--- error_log eval
442+
[ qr/Claim 'iss' \('unexpected_issuer'\) returned failure/ ]

0 commit comments

Comments
 (0)