Skip to content

Commit 31f02f1

Browse files
authored
Merge pull request #1392 from 3scale/backport-2.13-THREESCALE-9009-fix-oidc-issuer-verification
Backport 2.13 threescale 9009 fix OIDC issuer verification
2 parents 45b1756 + 7a4a5b9 commit 31f02f1

File tree

6 files changed

+143
-9
lines changed

6 files changed

+143
-9
lines changed

.circleci/config.yml

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,9 @@ commands:
1717
steps:
1818
- restore_cache:
1919
keys:
20-
- apicast-rocks-{{ arch }}-{{ checksum "gateway/Roverfile.lock" }}
21-
- apicast-rocks-{{ arch }}-{{ .Branch }}
22-
- apicast-rocks-{{ arch }}-master
20+
- apicast-rocks-v2.13-{{ arch }}-{{ checksum "gateway/Roverfile.lock" }}
21+
- apicast-rocks-v2.13-{{ arch }}-{{ .Branch }}
22+
- apicast-rocks-v2.13-{{ arch }}-master
2323

2424
restore-perl-cache:
2525
steps:
@@ -106,12 +106,13 @@ executors:
106106
docker:
107107
- image: docker:stable
108108
environment:
109+
COMPOSE_TLS_VERSION: "TLSv1_2"
109110
DOCKER_COMPOSE_VERSION: "1.16.1"
110111

111112
openresty:
112113
working_directory: /opt/app-root/apicast
113114
docker:
114-
- image: quay.io/3scale/apicast-ci:openresty-1.19.3
115+
- image: quay.io/3scale/apicast-ci:openresty-1.19.3-pr1381
115116
- image: redis:3.2.8-alpine
116117
environment:
117118
TEST_NGINX_BINARY: openresty
@@ -170,7 +171,7 @@ jobs:
170171
- restore-lua-cache
171172
- run: make lua_modules
172173
- save_cache:
173-
key: apicast-rocks-{{ arch }}-{{ checksum "gateway/Roverfile.lock" }}
174+
key: apicast-rocks-v2.13-{{ arch }}-{{ checksum "gateway/Roverfile.lock" }}
174175
<<: *lua-cache-paths
175176
- persist_to_workspace:
176177
root: .

CHANGELOG.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
77

88
## [Unreleased]
99

10+
## [3.13.2] 2023-02-21
11+
12+
### Fixed
13+
14+
- Fixed: OIDC jwt key verification [PR #1392](https://github.com/3scale/APIcast/pull/1392) [THREESCALE-9009](https://issues.redhat.com/browse/THREESCALE-9009)
15+
16+
## [3.13.0] 2023-02-07
17+
1018
### Fixed
1119

1220
- Fixed NGINX filters policy error [PR #1339](https://github.com/3scale/APIcast/pull/1339) [THREESCALE-7349](https://issues.redhat.com/browse/THREESCALE-7349)
@@ -18,6 +26,10 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
1826

1927
### Added
2028

29+
## [3.12.2] 2023-02-21
30+
31+
- Fixed: OIDC jwt key verification [PR #1391](https://github.com/3scale/APIcast/pull/1391) [THREESCALE-9009](https://issues.redhat.com/browse/THREESCALE-9009)
32+
2133
## [3.12.0] 2022-07-07
2234

2335
### Fixed
@@ -965,3 +977,5 @@ Apart from the changes mentioned in this section, this version also includes the
965977
[3.10.0]: https://github.com/3scale/apicast/compare/v3.10.0-beta1..v3.10.0
966978
[3.11.0]: https://github.com/3scale/apicast/compare/v3.10.0..v3.11.0
967979
[3.12.0]: https://github.com/3scale/apicast/compare/v3.11.0..v3.12.0
980+
[3.12.2]: https://github.com/3scale/apicast/compare/v3.12.0..v3.12.2
981+
[3.13.0]: https://github.com/3scale/apicast/compare/v3.12.0..v3.13.0

gateway/src/apicast/oauth/oidc.lua

Lines changed: 7 additions & 2 deletions
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,13 @@ 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-
-- Check the jwk for the alg field and if not present skip the validation as it is
194+
-- Check the jwk for the alg field and if not present skip the validation as it is
190195
-- OPTIONAL according to https://www.rfc-editor.org/rfc/rfc7517#section-4.4
191196
if jwk_obj.alg and jwk_obj.alg ~= jwt.header.alg then
192197
return false, '[jwt] alg mismatch'

gateway/src/apicast/version.lua

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
return "latest"
1+
return "3.13.2"

spec/oauth/oidc_spec.lua

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,42 @@ describe('OIDC', function()
292292
assert(credentials, err)
293293
end)
294294

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+
295331
describe('getting client_id from any JWT claim', function()
296332

297333
before_each(function()

t/apicast-oidc.t

Lines changed: 79 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -313,7 +313,7 @@ my $jwt = encode_jwt(payload => {
313313
"Authorization: Bearer $jwt"
314314
--- no_error_log
315315
316-
=== TEST 7: JWT verification fails when jwk.alg exists AND does not match jwt.header.alg
316+
=== TEST 7: JWT verification fails when jwk.alg exists AND does not match jwt.header.alg
317317
(see THREESCALE-8249 for steps to generate tampered JWT. rsa.pub from fixtures used to sign)
318318
--- configuration env eval
319319
use JSON qw(to_json);
@@ -362,3 +362,81 @@ my $jwt = 'eyJraWQiOiJzb21la2lkIiwiYWxnIjoiSFMyNTYifQ.'.
362362
"Authorization: Bearer $jwt"
363363
--- error_log
364364
[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)