diff --git a/CHANGELOG.md b/CHANGELOG.md index 97fc02977..2a4a8c91c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -44,6 +44,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - Fixed issue where request path is stripped for proxied https requests [PR #1342](https://github.com/3scale/APIcast/pull/1342) [THREESCALE-8426](https://issues.redhat.com/browse/THREESCALE-8426) - Bumped liquid-lua to version 0.2.0-2 [PR #1369](https://github.com/3scale/APIcast/pull/1369) - includes: [THREESCALE-8483](https://issues.redhat.com/browse/THREESCALE-8483) and [THREESCALE-8484](https://issues.redhat.com/browse/THREESCALE-8484) - Fixed: APIcast could not retrieve the latest version of the proxy config [PR #1370](https://github.com/3scale/APIcast/pull/1370) [THREESCALE-8485](https://issues.redhat.com/browse/THREESCALE-8485) +- Fixed: JWKs without alg field cause the JWT validation process to fail [PR #1371](https://github.com/3scale/APIcast/pull/1371) [THREESCALE-8601](https://issues.redhat.com/browse/THREESCALE-8601) ### Added diff --git a/gateway/src/apicast/oauth/oidc.lua b/gateway/src/apicast/oauth/oidc.lua index 60591b2d7..a61cabad6 100644 --- a/gateway/src/apicast/oauth/oidc.lua +++ b/gateway/src/apicast/oauth/oidc.lua @@ -186,7 +186,9 @@ function _M:verify(jwt, cache_key) local jwk_obj = find_jwk(jwt, self.keys) local pubkey = jwk_obj.pem - if jwk_obj.alg ~= jwt.header.alg then + -- Check the jwk for the alg field and if not present skip the validation as it is + -- OPTIONAL according to https://www.rfc-editor.org/rfc/rfc7517#section-4.4 + if jwk_obj.alg and jwk_obj.alg ~= jwt.header.alg then return false, '[jwt] alg mismatch' end diff --git a/spec/oauth/oidc_spec.lua b/spec/oauth/oidc_spec.lua index f65f22276..551996dec 100644 --- a/spec/oauth/oidc_spec.lua +++ b/spec/oauth/oidc_spec.lua @@ -45,6 +45,11 @@ describe('OIDC', function() config = { id_token_signing_alg_values_supported = { 'RS256', 'HS256' } }, keys = { somekid = { pem = rsa.pub, alg = 'RS256' } }, } + local oidc_config_no_alg = { + issuer = 'https://example.com/auth/realms/apicast', + config = { id_token_signing_alg_values_supported = { 'RS256', 'HS256' } }, + keys = { somekid = { pem = rsa.pub } }, + } before_each(function() jwt_validators.set_system_clock(function() return 0 end) end) @@ -268,6 +273,25 @@ describe('OIDC', function() assert(credentials, err) end) + it('validation passes when jwk.alg does not exist', function() + local oidc = _M.new(oidc_config_no_alg) + local access_token = jwt:sign(rsa.private, { + header = { typ = 'JWT', alg = 'RS256', kid = 'somekid' }, + payload = { + iss = oidc_config.issuer, + aud = 'notused', + azp = 'ce3b2e5e', + sub = 'someone', + nbf = 0, + exp = ngx.now() + 10, + typ = 'Bearer' + }, + }) + + local credentials, _, _, err = oidc:transform_credentials({ access_token = access_token }) + assert(credentials, err) + end) + describe('getting client_id from any JWT claim', function() before_each(function() diff --git a/t/apicast-oidc.t b/t/apicast-oidc.t index ada1469eb..6cddbbdba 100644 --- a/t/apicast-oidc.t +++ b/t/apicast-oidc.t @@ -264,9 +264,7 @@ my $jwt = encode_jwt(payload => { --- no_error_log [error] - - -=== TEST 2: JWT verification fails when no alg is present in the jwk to match against jwt.header.alg +=== TEST 6: JWT verification does not fail when no alg is present in the jwk to match against jwt.header.alg --- configuration env eval use JSON qw(to_json); @@ -303,7 +301,7 @@ to_json({ } } --- request: GET /test ---- error_code: 403 +--- error_code: 200 --- more_headers eval use Crypt::JWT qw(encode_jwt); my $jwt = encode_jwt(payload => { @@ -313,5 +311,54 @@ my $jwt = encode_jwt(payload => { iss => 'https://example.com/auth/realms/apicast', exp => time + 3600 }, key => \$::private_key, alg => 'RS256', extra_headers => { kid => 'somekid' }); "Authorization: Bearer $jwt" +--- no_error_log + +=== TEST 7: JWT verification fails when jwk.alg exists AND does not match jwt.header.alg +(see THREESCALE-8249 for steps to generate tampered JWT. rsa.pub from fixtures used to sign) +--- configuration env eval +use JSON qw(to_json); + +to_json({ + services => [{ + id => 42, + backend_version => 'oauth', + backend_authentication_type => 'provider_key', + backend_authentication_value => 'fookey', + proxy => { + authentication_method => 'oidc', + oidc_issuer_endpoint => 'https://example.com/auth/realms/apicast', + api_backend => "http://test:$TEST_NGINX_SERVER_PORT/", + proxy_rules => [ + { pattern => '/', http_method => 'GET', metric_system_name => 'hits', delta => 1 } + ] + } + }], + oidc => [{ + issuer => 'https://example.com/auth/realms/apicast', + config => { id_token_signing_alg_values_supported => [ 'RS256', 'HS256' ] }, + keys => { somekid => { pem => $::public_key, alg => 'RS256' } }, + }] +}); +--- upstream + location /test { + echo "yes"; + } +--- backend + location = /transactions/oauth_authrep.xml { + content_by_lua_block { + local expected = "provider_key=fookey&service_id=42&usage%5Bhits%5D=1&app_id=appid" + require('luassert').same(ngx.decode_args(expected), ngx.req.get_uri_args(0)) + } + } +--- request: GET /test +--- error_code: 403 +--- more_headers eval +use Crypt::JWT qw(encode_jwt); +my $jwt = 'eyJraWQiOiJzb21la2lkIiwiYWxnIjoiSFMyNTYifQ.'. +'eyJleHAiOjcxNzA1MzE2NDMwLCJhenAiOiJhcHBpZCIsInN1YiI6In'. +'NvbWVvbmUiLCJhdWQiOiJzb21ldGhpbmciLCJpc3MiOiJodHRwczov'. +'L2V4YW1wbGUuY29tL2F1dGgvcmVhbG1zL2FwaWNhc3QifQ.1rFq5QN'. +'b99W6aqQjsx7GJGLDpdkDLI6-huZLzMAmxGQ'; +"Authorization: Bearer $jwt" --- error_log [jwt] alg mismatch