diff --git a/gateway/src/apicast/policy/token_introspection/token_introspection.lua b/gateway/src/apicast/policy/token_introspection/token_introspection.lua index 3158e21cd..749866a67 100644 --- a/gateway/src/apicast/policy/token_introspection/token_introspection.lua +++ b/gateway/src/apicast/policy/token_introspection/token_introspection.lua @@ -94,7 +94,10 @@ function _M:access(context) local components = resty_url.parse(context.service.oidc.issuer_endpoint) self.credential = create_credential(components.user, components.password) - self.introspection_url = context.proxy.oauth.config.token_introspection_endpoint + local oauth_config = context.proxy.oauth.config + -- token_introspection_endpoint being deprecated in RH SSO 7.4 and removed in 7.5 + -- https://access.redhat.com/documentation/en-us/red_hat_single_sign-on/7.5/html-single/upgrading_guide/index#non_standard_token_introspection_endpoint_removed + self.introspection_url = oauth_config.introspection_endpoint or oauth_config.token_introspection_endpoint end if self.introspection_url then @@ -102,13 +105,19 @@ function _M:access(context) local access_token = authorization.token --- Introspection Response must have an "active" boolean value. -- https://tools.ietf.org/html/rfc7662#section-2.2 - if not introspect_token(self, access_token).active == true then - ngx.log(ngx.INFO, 'token introspection for access token ', access_token, ': token not active') - ngx.status = context.service.auth_failed_status - ngx.say(context.service.error_auth_failed) - return ngx.exit(ngx.status) + if introspect_token(self, access_token).active == true then + -- access granted + return end + + ngx.log(ngx.INFO, 'token introspection for access token ', access_token, ': token not active') + else + ngx.log(ngx.WARN, 'token instropection cannot be performed as introspection endpoint is not available') end + + ngx.status = context.service.auth_failed_status + ngx.say(context.service.error_auth_failed) + return ngx.exit(ngx.status) end return _M diff --git a/spec/policy/token_introspection/token_introspection_spec.lua b/spec/policy/token_introspection/token_introspection_spec.lua index 1a63a249a..31833b5bd 100644 --- a/spec/policy/token_introspection/token_introspection_spec.lua +++ b/spec/policy/token_introspection/token_introspection_spec.lua @@ -1,6 +1,6 @@ local TokenIntrospection = require('apicast.policy.token_introspection') 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') describe("token introspection policy", function() @@ -30,168 +30,304 @@ describe("token introspection policy", function() } end) - it('success with valid token', function() + describe('client_id+client_secret introspection auth type', function() + local auth_type = "client_id+client_secret" local introspection_url = "http://example/token/introspection" local policy_config = { - auth_type = "client_id+client_secret", + auth_type = auth_type, introspection_url = introspection_url, client_id = test_client_id, client_secret = test_client_secret } - test_backend - .expect{ - url = introspection_url, - method = 'POST', - headers = { - ['Authorization'] = test_basic_auth + + it('success with valid token', function() + test_backend + .expect{ + url = introspection_url, + method = 'POST', + headers = { + ['Authorization'] = test_basic_auth + } + } + .respond_with{ + status = 200, + body = cjson.encode({ + active = true + }) } - } - .respond_with{ - status = 200, - body = cjson.encode({ - active = true - }) - } - local token_policy = TokenIntrospection.new(policy_config) - token_policy.http_client.backend = test_backend - token_policy:access(context) - assert.are.same(ngx.decode_args(test_backend.get_requests()[1].body), - { token = "test", token_type_hint = "access_token" }) + local token_policy = TokenIntrospection.new(policy_config) + token_policy.http_client.backend = test_backend + token_policy:access(context) + assert.are.same(ngx.decode_args(test_backend.get_requests()[1].body), + { token = "test", token_type_hint = "access_token" }) + end) - end) + it('failed with invalid token', function() + test_backend + .expect{ + url = introspection_url, + method = 'POST', + headers = { + ['Authorization'] = test_basic_auth + } + } + .respond_with{ + status = 200, + body = cjson.encode({ + active = false + }) + } + stub(ngx, 'say') + stub(ngx, 'exit') - it('failed with invalid token', function() - local introspection_url = "http://example/token/introspection" - local policy_config = { - auth_type = "client_id+client_secret", - introspection_url = introspection_url, - client_id = "client", - client_secret = "secret" - } + local token_policy = TokenIntrospection.new(policy_config) + token_policy.http_client.backend = test_backend + token_policy:access(context) + assert_authentication_failed() + + assert.are.same(ngx.decode_args(test_backend.get_requests()[1].body), + { token = "test", token_type_hint = "access_token" }) + end) - test_backend - .expect{ - url = introspection_url, - method = 'POST', - headers = { - ['Authorization'] = test_basic_auth + it('failed with bad status code', function() + test_backend + .expect{ + url = introspection_url, + method = 'POST', + headers = { + ['Authorization'] = test_basic_auth + } } - } - .respond_with{ - status = 200, - body = cjson.encode({ - active = false - }) - } - stub(ngx, 'say') - stub(ngx, 'exit') + .respond_with{ + status = 404, + } + 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() + local token_policy = TokenIntrospection.new(policy_config) + token_policy.http_client.backend = test_backend + token_policy:access(context) + assert_authentication_failed() - assert.are.same(ngx.decode_args(test_backend.get_requests()[1].body), - { token = "test", token_type_hint = "access_token" }) - end) + assert.are.same(ngx.decode_args(test_backend.get_requests()[1].body), + { token = "test", token_type_hint = "access_token" }) + end) - it('failed with bad status code', function() - local introspection_url = "http://example/token/introspection" - local policy_config = { - auth_type = "client_id+client_secret", - introspection_url = introspection_url, - client_id = "client", - client_secret = "secret" - } + it('failed with null response', function() + test_backend + .expect{ + url = introspection_url, + method = 'POST', + headers = { + ['Authorization'] = test_basic_auth + } + } + .respond_with{ + status = 200, + body = 'null' + } + 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() + + assert.are.same(ngx.decode_args(test_backend.get_requests()[1].body), + { token = "test", token_type_hint = "access_token" }) + end) - test_backend - .expect{ - url = introspection_url, - method = 'POST', - headers = { - ['Authorization'] = test_basic_auth + it('failed with active null response', function() + test_backend + .expect{ + url = introspection_url, + method = 'POST', + headers = { + ['Authorization'] = test_basic_auth + } } - } - .respond_with{ - status = 404, - } - stub(ngx, 'say') - stub(ngx, 'exit') + .respond_with{ + status = 200, + body = '{ "active": null }' + } + 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() + local token_policy = TokenIntrospection.new(policy_config) + token_policy.http_client.backend = test_backend + token_policy:access(context) + assert_authentication_failed() - assert.are.same(ngx.decode_args(test_backend.get_requests()[1].body), - { token = "test", token_type_hint = "access_token" }) - end) + assert.are.same(ngx.decode_args(test_backend.get_requests()[1].body), + { token = "test", token_type_hint = "access_token" }) + end) - it('failed with null response', function() - local introspection_url = "http://example/token/introspection" - local policy_config = { - auth_type = "client_id+client_secret", - introspection_url = introspection_url, - client_id = "client", - client_secret = "secret" - } + it('failed with missing active response', function() + test_backend + .expect{ + url = introspection_url, + method = 'POST', + headers = { + ['Authorization'] = test_basic_auth + } + } + .respond_with{ + status = 200, + body = '{}' + } + 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() + + assert.are.same(ngx.decode_args(test_backend.get_requests()[1].body), + { token = "test", token_type_hint = "access_token" }) + end) - test_backend - .expect{ - url = introspection_url, - method = 'POST', - headers = { - ['Authorization'] = test_basic_auth + it('failed with bad contents type', function() + test_backend + .expect{ + url = introspection_url, + method = 'POST', + headers = { + ['Authorization'] = test_basic_auth + } } - } - .respond_with{ - status = 200, - body = 'null' - } - stub(ngx, 'say') - stub(ngx, 'exit') + .respond_with{ + status = 200, + body = "" + } + 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() + local token_policy = TokenIntrospection.new(policy_config) + token_policy.http_client.backend = test_backend + token_policy:access(context) + assert_authentication_failed() - assert.are.same(ngx.decode_args(test_backend.get_requests()[1].body), - { token = "test", token_type_hint = "access_token" }) + assert.are.same(ngx.decode_args(test_backend.get_requests()[1].body), + { token = "test", token_type_hint = "access_token" }) + end) end) - it('failed with bad contents type', function() - local introspection_url = "http://example/token/introspection" + describe('use_3scale_oidc_issuer_endpoint auth type', function() + local auth_type = "use_3scale_oidc_issuer_endpoint" local policy_config = { - auth_type = "client_id+client_secret", - introspection_url = introspection_url, - client_id = "client", - client_secret = "secret" + auth_type = auth_type, } - test_backend - .expect{ - url = introspection_url, - method = 'POST', - headers = { - ['Authorization'] = test_basic_auth + it('when no oauth content in the context', function() + context = { + service = { + auth_failed_status = 403, + error_auth_failed = "auth failed" + }, + proxy = { } } - .respond_with{ - status = 200, - body = "" + + 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) + + it('using deprecated token_introspection_endpoint', function() + test_backend + .expect{ + url = "http://example.com/token/introspection", + method = 'POST', + headers = { + ['Authorization'] = test_basic_auth + } + } + .respond_with{ + status = 200, + body = cjson.encode({ + active = true + }) + } + context = { + service = { + auth_failed_status = 403, + error_auth_failed = "auth failed", + oidc = { + issuer_endpoint = format('http://%s:%s@example.com/issuer/endpoint', test_client_id, test_client_secret) + } + }, + proxy = { + oauth = { + config = { + token_introspection_endpoint = "http://example.com/token/introspection" + } + } + } } - 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() + 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.stub(ngx.exit).was_not.called_with(403) + assert.are.same(ngx.decode_args(test_backend.get_requests()[1].body), + { token = "test", token_type_hint = "access_token" }) + end) + + it('using introspection_endpoint', function() + test_backend + .expect{ + url = "http://example.com/token/introspection", + method = 'POST', + headers = { + ['Authorization'] = test_basic_auth + } + } + .respond_with{ + status = 200, + body = cjson.encode({ + active = true + }) + } + context = { + service = { + auth_failed_status = 403, + error_auth_failed = "auth failed", + oidc = { + issuer_endpoint = format('http://%s:%s@example.com/issuer/endpoint', test_client_id, test_client_secret) + } + }, + proxy = { + oauth = { + config = { + introspection_endpoint = "http://example.com/token/introspection", + --- deprecated field + token_introspection_endpoint = "http://example.com/token/deprecated_introspection" + } + } + } + } + + 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.stub(ngx.exit).was_not.called_with(403) + assert.are.same(ngx.decode_args(test_backend.get_requests()[1].body), + { token = "test", token_type_hint = "access_token" }) + end) - assert.are.same(ngx.decode_args(test_backend.get_requests()[1].body), - { token = "test", token_type_hint = "access_token" }) end) describe('when caching is enabled', function() diff --git a/t/apicast-policy-token-introspection.t b/t/apicast-policy-token-introspection.t index 89f1f6175..6fc95726a 100644 --- a/t/apicast-policy-token-introspection.t +++ b/t/apicast-policy-token-introspection.t @@ -30,7 +30,7 @@ Token introspection policy check access token. "proxy": { "policy_chain": [ { - "name": "apicast.policy.token_introspection", + "name": "apicast.policy.token_introspection", "configuration": { "auth_type": "client_id+client_secret", "client_id": "app", @@ -80,7 +80,7 @@ Token introspection policy return "403 Unauthorized" if access token is already "proxy": { "policy_chain": [ { - "name": "apicast.policy.token_introspection", + "name": "apicast.policy.token_introspection", "configuration": { "auth_type": "client_id+client_secret", "client_id": "app", @@ -133,7 +133,7 @@ Token introspection policy return "403 Unauthorized" if IdP response error statu "proxy": { "policy_chain": [ { - "name": "apicast.policy.token_introspection", + "name": "apicast.policy.token_introspection", "configuration": { "auth_type": "client_id+client_secret", "client_id": "app", @@ -164,6 +164,7 @@ Authorization: Bearer testaccesstoken --- error_code: 403 --- no_error_log [error] + === TEST 4: Token introspection request is failed with bad response value Token introspection policy return "403 Unauthorized" if IdP response invalid contents type. --- backend @@ -271,7 +272,7 @@ Authorization: Bearer testaccesstoken [error] === TEST 6: Token introspection request success with oidc issuer endpoint -Token introspection policy retrieves client_id and client_secret and +Token introspection policy retrieves client_id and client_secret and introspection endpoint from the oidc_issuer_endpoint of the service configuration. --- backend location /token/introspection { @@ -293,7 +294,10 @@ introspection endpoint from the oidc_issuer_endpoint of the service configuratio "oidc": [ { "issuer": "https://example.com/auth/realms/apicast", - "config": { "id_token_signing_alg_values_supported": [ "RS256" ] }, + "config": { + "id_token_signing_alg_values_supported": [ "RS256" ], + "introspection_endpoint": "http://test_backend:$TEST_NGINX_SERVER_PORT/token/introspection" + }, "keys": { "somekid": { "pem": "-----BEGIN PUBLIC KEY-----\nMFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBALClz96cDQ965ENYMfZzG+Acu25lpx2K\nNpAALBQ+catCA59us7+uLY5rjQR6SOgZpCz5PJiKNAdRPDJMXSmXqM0CAwEAAQ==\n-----END PUBLIC KEY-----", "alg": "RS256" } } } ], @@ -347,7 +351,7 @@ yay, api backend oauth failed with === TEST 7: Token introspection request fails with app_key -Token introspection policy retrieves client_id and client_secret and +Token introspection policy retrieves client_id and client_secret and introspection endpoint from the oidc_issuer_endpoint of the service configuration. When authentication_method = 1, the request fails. --- backend @@ -400,11 +404,104 @@ Authentication failed [error] oauth failed with +=== TEST 8: Token introspection request success with oidc issuer endpoint loaded from the IDP +Token introspection policy retrieves client_id and client_secret and +introspection endpoint from the oidc_issuer_endpoint of the service configuration. +--- env eval +( 'APICAST_CONFIGURATION_LOADER' => 'lazy' ) +--- backend +location = /issuer/endpoint/.well-known/openid-configuration { + content_by_lua_block { + local base = "http://" .. ngx.var.host .. ':' .. ngx.var.server_port + ngx.header.content_type = 'application/json;charset=utf-8' + ngx.say(require('cjson').encode { + issuer = 'https://example.com/auth/realms/apicast', + id_token_signing_alg_values_supported = { 'RS256' }, + jwks_uri = base .. '/jwks', + introspection_endpoint = base .. '/token/introspection', + }) + } +} +location = /jwks { + content_by_lua_block { + ngx.header.content_type = 'application/json;charset=utf-8' + ngx.say([[ + { "keys": [ + { "kty":"RSA","kid":"somekid", + "n":"sKXP3pwND3rkQ1gx9nMb4By7bmWnHYo2kAAsFD5xq0IDn26zv64tjmuNBHpI6BmkLPk8mIo0B1E8MkxdKZeozQ","e":"AQAB", + "alg":"RS256" } + ] } + ]]) + } +} -=== TEST 8: Token introspection request success with oidc issuer endpoint loaded from the IDP +location = /token/introspection { + content_by_lua_block { + local credential = ngx.decode_base64(require('ngx.re').split(ngx.req.get_headers()['Authorization'], ' ', 'oj')[2]) + require('luassert').are.equal('app:appsec', credential) + ngx.say('{"active": true}') + } +} + +location = /transactions/oauth_authrep.xml { + content_by_lua_block { ngx.exit(200) } +} + +--- configuration +{ + "services": [ + { + "backend_version": "oauth", + "proxy": { + "authentication_method": "oidc", + "oidc_issuer_endpoint": "http://app:appsec@test_backend:$TEST_NGINX_SERVER_PORT/issuer/endpoint", + "api_backend": "http://test:$TEST_NGINX_SERVER_PORT/", + "proxy_rules": [ + { "pattern": "/", "http_method": "GET", "metric_system_name": "hits", "delta": 1 } + ], + "policy_chain": [ + { + "name": "apicast.policy.token_introspection", + "configuration": { + "auth_type": "use_3scale_oidc_issuer_endpoint" + } + }, + { "name": "apicast.policy.apicast" } + ] + } + } + ] +} +--- upstream + location /echo { + content_by_lua_block { + ngx.say('yay, api backend'); + } + } +--- request +GET /echo +--- more_headers eval +use Crypt::JWT qw(encode_jwt); +my $jwt = encode_jwt(payload => { + aud => 'the_token_audience', + sub => 'someone', + iss => 'https://example.com/auth/realms/apicast', + exp => time + 3600 }, key => \$::rsa, alg => 'RS256', extra_headers => { kid => 'somekid' }); +"Authorization: Bearer $jwt" +--- error_code: 200 +--- response_body +yay, api backend +--- no_error_log +[error] +oauth failed with + +=== TEST 9: Token introspection request success with oidc issuer endpoint returning deprecated introspection attribute Token introspection policy retrieves client_id and client_secret and introspection endpoint from the oidc_issuer_endpoint of the service configuration. +But the service configuration returns deprecated "token_introspection_endpoint" attribute +instead of "introspection_endpoint" attribute. This is for backward compatibility. + --- env eval ( 'APICAST_CONFIGURATION_LOADER' => 'lazy' ) --- backend @@ -416,6 +513,7 @@ location = /issuer/endpoint/.well-known/openid-configuration { issuer = 'https://example.com/auth/realms/apicast', id_token_signing_alg_values_supported = { 'RS256' }, jwks_uri = base .. '/jwks', + token_introspection_endpoint = base .. '/token/introspection', }) } } @@ -492,3 +590,79 @@ yay, api backend --- no_error_log [error] oauth failed with + +=== TEST 10: Token introspection request success with oidc issuer endpoint +Token introspection policy retrieves introspection endpoint from the oidc_issuer_endpoint +of the service configuration. However, the introspection endpoint is not in the response +--- env eval +( 'APICAST_CONFIGURATION_LOADER' => 'lazy' ) +--- backend +location = /issuer/endpoint/.well-known/openid-configuration { + content_by_lua_block { + local base = "http://" .. ngx.var.host .. ':' .. ngx.var.server_port + ngx.header.content_type = 'application/json;charset=utf-8' + ngx.say(require('cjson').encode { + issuer = 'https://example.com/auth/realms/apicast', + id_token_signing_alg_values_supported = { 'RS256' }, + jwks_uri = base .. '/jwks', + }) + } +} + +location = /jwks { + content_by_lua_block { + ngx.header.content_type = 'application/json;charset=utf-8' + ngx.say([[ + { "keys": [ + { "kty":"RSA","kid":"somekid", + "n":"sKXP3pwND3rkQ1gx9nMb4By7bmWnHYo2kAAsFD5xq0IDn26zv64tjmuNBHpI6BmkLPk8mIo0B1E8MkxdKZeozQ","e":"AQAB", + "alg":"RS256" } + ] } + ]]) + } +} + +--- configuration +{ + "services": [ + { + "backend_version": "oauth", + "proxy": { + "authentication_method": "oidc", + "oidc_issuer_endpoint": "http://app:appsec@test_backend:$TEST_NGINX_SERVER_PORT/issuer/endpoint", + "api_backend": "http://test:$TEST_NGINX_SERVER_PORT/", + "proxy_rules": [ + { "pattern": "/", "http_method": "GET", "metric_system_name": "hits", "delta": 1 } + ], + "policy_chain": [ + { + "name": "apicast.policy.token_introspection", + "configuration": { + "auth_type": "use_3scale_oidc_issuer_endpoint" + } + }, + { "name": "apicast.policy.apicast" } + ] + } + } + ] +} +--- upstream + location /echo { + content_by_lua_block { + ngx.say('yay, api backend'); + } + } +--- request +GET /echo +--- more_headers eval +use Crypt::JWT qw(encode_jwt); +my $jwt = encode_jwt(payload => { + aud => 'the_token_audience', + sub => 'someone', + iss => 'https://example.com/auth/realms/apicast', + exp => time + 3600 }, key => \$::rsa, alg => 'RS256', extra_headers => { kid => 'somekid' }); +"Authorization: Bearer $jwt" +--- error_code: 403 +--- no_error_log +[error]