From 2f2d6d96380efbc121917bfff5f75666a951a6e2 Mon Sep 17 00:00:00 2001 From: An Tran Date: Fri, 14 Jun 2024 13:32:42 +1000 Subject: [PATCH 1/2] [fapi] Support OAuth 2.0 MTLS and Certificate-Bound Access Tokens --- CHANGELOG.md | 4 +- gateway/src/apicast/policy/fapi/README.md | 19 + .../apicast/policy/fapi/apicast-config.json | 9 +- gateway/src/apicast/policy/fapi/fapi.lua | 40 +- spec/policy/fapi/fapi_spec.lua | 113 +++++- t/apicast-policy-fapi.t | 343 +++++++++++++++++- 6 files changed, 509 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 31def373e..f9526f771 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,7 +17,9 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - Bump openresty to 1.21.4.3 [PR #1461](https://github.com/3scale/APIcast/pull/1461) [THREESCALE-10601](https://issues.redhat.com/browse/THREESCALE-10601) -- Support Financial-grade API (FAPI) - Baseline profile [PR #1465](https://github.com/3scale/APIcast/pull/1465) [THREESCALE-10973](https://issues.redhat.com/browse/THREESCALE-10973) +- Support Financial-grade API (FAPI) 1.0 - Baseline profile [PR #1465](https://github.com/3scale/APIcast/pull/1465) [THREESCALE-10973](https://issues.redhat.com/browse/THREESCALE-10973) + +- Support Financial-grade API (FAPI) 1.0 - Advance profile [PR #1465](https://github.com/3scale/APIcast/pull/1466) [THREESCALE-11019](https://issues.redhat.com/browse/THREESCALE-11019) ## [3.15.0] 2024-04-04 diff --git a/gateway/src/apicast/policy/fapi/README.md b/gateway/src/apicast/policy/fapi/README.md index 8b630c428..e9bb619ae 100644 --- a/gateway/src/apicast/policy/fapi/README.md +++ b/gateway/src/apicast/policy/fapi/README.md @@ -4,6 +4,9 @@ The FAPI policy supports various features of the Financial-grade API (FAPI) standard. +* FAPI 1.0 Baseline Profile +* FAPI 1.0 Advance Profile + ## Example configuration ``` @@ -30,3 +33,19 @@ The FAPI policy supports various features of the Financial-grade API (FAPI) stan } ] ``` + +### Validate certificate-bound access tokens + +``` +"policy_chain": [ + { + "name": "apicast.policy.fapi", + "configuration": { + "enable_oauth2_mtls": true + } + }, + { + "name": "apicast.policy.apicast" + } +] +``` diff --git a/gateway/src/apicast/policy/fapi/apicast-config.json b/gateway/src/apicast/policy/fapi/apicast-config.json index 9d7ecb340..d3631f6f9 100644 --- a/gateway/src/apicast/policy/fapi/apicast-config.json +++ b/gateway/src/apicast/policy/fapi/apicast-config.json @@ -9,7 +9,14 @@ "type": "object", "properties": { "validate_x_fapi_customer_ip_address": { - "description": "Validate x-fapi-customer-ip-address header", + "description": "Validate x-fapi-customer-ip-address header. If the verification fails, the request will be rejected with 403", + "title": "Validate x-fapi-customer-ip-address header", + "type": "boolean", + "default": "false" + }, + "validate_oauth2_certificate_bound_access_token ": { + "description": "Validate OAuth 2.0 Mutual TLS Certificate Bound access token. If enable, all tokens are verified and must contain the certificate hash claim (cnf). If the verification fails, the request will be rejected with 401.", + "title": "Validate OAuth 2.0 Mutual TLS Certificate Bound access token", "type": "boolean", "default": "false" } diff --git a/gateway/src/apicast/policy/fapi/fapi.lua b/gateway/src/apicast/policy/fapi/fapi.lua index d42c64884..ce908f47a 100644 --- a/gateway/src/apicast/policy/fapi/fapi.lua +++ b/gateway/src/apicast/policy/fapi/fapi.lua @@ -1,16 +1,27 @@ --- Financial-grade API (FAPI) policy +-- Current support the following profile: +-- FAPI 1.0 - Baseline profile +-- FAPI 1.0 - Advance profile local policy = require('apicast.policy') local _M = policy.new('Financial-grade API (FAPI) Policy', 'builtin') local uuid = require 'resty.jit-uuid' local ipmatcher = require "resty.ipmatcher" +local X509 = require('resty.openssl.x509') +local b64 = require('ngx.base64') local fmt = string.format local new = _M.new local X_FAPI_TRANSACTION_ID_HEADER = "x-fapi-transaction-id" local X_FAPI_CUSTOMER_IP_ADDRESS = "x-fapi-customer-ip-address" +-- The "x5t#S256" (X.509 Certificate SHA-256 Thumbprint) Header +-- Parameter is a base64url encoded SHA-256 thumbprint (a.k.a. digest) +-- of the DER encoding of the X.509 certificate. +-- https://tools.ietf.org/html/rfc7515#section-4.1.8 +local header_parameter = 'x5t#S256' + local function is_valid_ip(ip) if type(ip) ~= "string" then return false @@ -29,16 +40,43 @@ local function error(status_code, msg) ngx.exit(ngx.status) end +-- https://datatracker.ietf.org/doc/html/rfc8705#name-jwt-certificate-thumbprint- +local function check_certificate(cert, cnf) + if not cert then + ngx.log(ngx.DEBUG, "client certificate is not available") + return false + end + + if not cnf then + ngx.log(ngx.DEBUG, "cnf claim is not available") + return false + end + return b64.encode_base64url(cert:digest('SHA256')) == cnf[header_parameter] +end + --- Initialize FAPI policy -- @tparam[config] table config -- @field[config] validate_x_fapi_customer_ip_address Boolean +-- @field[config] validate_oauth2_certificate_bound_access_token Boolean function _M.new(config) local self = new(config) self.validate_customer_ip_address = config and config.validate_x_fapi_customer_ip_address + self.validate_oauth2_certificate_bound_access_token = config and config.validate_oauth2_certificate_bound_access_token return self end -function _M:access() +function _M:access(context) + if self.validate_oauth2_certificate_bound_access_token then + if not context.jwt then return error(context.service or {}) end + + local cert = X509.parse_pem_cert(ngx.var.ssl_client_raw_cert) + + if not check_certificate(cert, context.jwt.cnf) then + ngx.log(ngx.WARN, 'fapi oauth_mtls failed for service ', context.service and context.service.id) + return error(ngx.HTTP_UNAUTHORIZED, "invalid_token") + end + end + --- 6.2.1.13 -- shall not reject requests with a x-fapi-customer-ip-address header containing a valid IPv4 or IPv6 address. if self.validate_customer_ip_address then diff --git a/spec/policy/fapi/fapi_spec.lua b/spec/policy/fapi/fapi_spec.lua index 1423ec135..144adda68 100644 --- a/spec/policy/fapi/fapi_spec.lua +++ b/spec/policy/fapi/fapi_spec.lua @@ -1,5 +1,15 @@ local FAPIPolicy = require('apicast.policy.fapi') local uuid = require('resty.jit-uuid') +local X509 = require('resty.openssl.x509') +local b64 = require('ngx.base64') +local clientCert = assert(fixture('CA', 'client.crt')) + +local header_parameter = 'x5t#S256' + +local function jwt_cnf() + local cnf = b64.encode_base64url(X509.parse_pem_cert(clientCert):digest('SHA256')) + return { [header_parameter] = b64.encode_base64url(X509.parse_pem_cert(clientCert):digest('SHA256')) } +end describe('fapi_1_baseline_profile policy', function() local ngx_req_headers = {} @@ -27,29 +37,29 @@ describe('fapi_1_baseline_profile policy', function() describe('.header_filter', function() it('Use value from request', function() ngx_req_headers['x-fapi-transaction-id'] = 'abc' - local transaction_id_policy = FAPIPolicy.new({}) - transaction_id_policy:header_filter() + local fapi_policy = FAPIPolicy.new({}) + fapi_policy:header_filter() assert.same('abc', ngx.header['x-fapi-transaction-id']) end) it('Only use x-fapi-transaction-id from request if the header also exist in response from upstream', function() ngx_req_headers['x-fapi-transaction-id'] = 'abc' ngx_resp_headers['x-fapi-transaction-id'] = 'bdf' - local transaction_id_policy = FAPIPolicy.new({}) - transaction_id_policy:header_filter() + local fapi_policy = FAPIPolicy.new({}) + fapi_policy:header_filter() assert.same('abc', ngx.header['x-fapi-transaction-id']) end) it('Use x-fapi-transaction-id from upstream response', function() ngx_resp_headers['x-fapi-transaction-id'] = 'abc' - local transaction_id_policy = FAPIPolicy.new({}) - transaction_id_policy:header_filter() + local fapi_policy = FAPIPolicy.new({}) + fapi_policy:header_filter() assert.same('abc', ngx.header['x-fapi-transaction-id']) end) it('generate uuid if header does not exist in both request and response', function() - local transaction_id_policy = FAPIPolicy.new({}) - transaction_id_policy:header_filter() + local fapi_policy = FAPIPolicy.new({}) + fapi_policy:header_filter() assert.is_true(uuid.is_valid(ngx.header['x-fapi-transaction-id'])) end) end) @@ -57,25 +67,100 @@ describe('fapi_1_baseline_profile policy', function() describe('x-fapi-customer-ip-address', function() it('Allow request with valid IPv4', function() ngx_req_headers['x-fapi-customer-ip-address'] = '127.0.0.1' - local transaction_id_policy = FAPIPolicy.new({validate_x_fapi_customer_ip_address=true}) - transaction_id_policy:access() + local fapi_policy = FAPIPolicy.new({validate_x_fapi_customer_ip_address=true}) + fapi_policy:access() assert.stub(ngx.exit).was_not.called_with(403) end) it('Allow request with valid IPv6', function() ngx_req_headers['x-fapi-customer-ip-address'] = '2001:db8::123:12:1' - local transaction_id_policy = FAPIPolicy.new({validate_x_fapi_customer_ip_address=true}) - transaction_id_policy:access() + local fapi_policy = FAPIPolicy.new({validate_x_fapi_customer_ip_address=true}) + fapi_policy:access() assert.stub(ngx.exit).was_not.called_with(403) end) it('Reject request if header contains more than 1 IP', function() ngx_req_headers['x-fapi-customer-ip-address'] = {"2001:db8::123:12:1", "127.0.0.1"} - local transaction_id_policy = FAPIPolicy.new({validate_x_fapi_customer_ip_address=true}) - transaction_id_policy:access() + local fapi_policy = FAPIPolicy.new({validate_x_fapi_customer_ip_address=true}) + fapi_policy:access() assert.same(ngx.status, 403) assert.stub(ngx.print).was.called_with('{"error": "invalid_request"}') assert.stub(ngx.exit).was.called_with(403) end) end) end) + +describe('fapi_1 advance profile', function() + local context = {} + before_each(function() + context = { + jwt = {}, + service = { + id = 4, + auth_failed_status = 403, + error_auth_failed = 'auth failed', + auth_failed_headers = 'text/plain; charset=utf-8' + } + } + + ngx.header = {} + stub(ngx, 'print') + stub(ngx, 'exit') + context.jwt = {} + end) + + it('accepts when the digest equals cnf claim', function() + ngx.var = { ssl_client_raw_cert = clientCert } + context.jwt.cnf = jwt_cnf() + + local fapi_policy = FAPIPolicy.new({validate_oauth2_certificate_bound_access_token=true}) + fapi_policy:access(context) + assert.stub(ngx.exit).was_not.called_with(403) + end) + + it('rejects when the client certificate not found', function() + ngx.var = { ssl_client_raw_cert = nil } + context.jwt.cnf = jwt_cnf() + + local fapi_policy = FAPIPolicy.new({validate_oauth2_certificate_bound_access_token=true}) + fapi_policy:access(context) + + assert.stub(ngx.exit).was_called_with(ngx.HTTP_UNAUTHORIZED) + assert.stub(ngx.print).was_called_with('{"error": "invalid_token"}') + end) + + it('rejects when the cnf claim not defined', function() + ngx.var = { ssl_client_raw_cert = clientCert } + + local fapi_policy = FAPIPolicy.new({validate_oauth2_certificate_bound_access_token=true}) + fapi_policy:access(context) + + assert.stub(ngx.exit).was_called_with(ngx.HTTP_UNAUTHORIZED) + assert.stub(ngx.print).was_called_with('{"error": "invalid_token"}') + end) + + it('rejects when context.service is nil', function() + ngx.var = { ssl_client_raw_cert = clientCert } + + local fapi_policy = FAPIPolicy.new({validate_oauth2_certificate_bound_access_token=true}) + context.service = nil + fapi_policy:access(context) + + assert.stub(ngx.exit).was_called_with(ngx.HTTP_UNAUTHORIZED) + assert.stub(ngx.print).was_called_with('{"error": "invalid_token"}') + end) + + it('rejects when the digest not equals cnf claim', function() + ngx.var = { ssl_client_raw_cert = clientCert } + + context.jwt.cnf = {} + context.jwt.cnf[header_parameter] = 'invalid_digest' + + local fapi_policy = FAPIPolicy.new({validate_oauth2_certificate_bound_access_token=true}) + context.service = nil + fapi_policy:access(context) + + assert.stub(ngx.exit).was_called_with(ngx.HTTP_UNAUTHORIZED) + assert.stub(ngx.print).was_called_with('{"error": "invalid_token"}') + end) +end) diff --git a/t/apicast-policy-fapi.t b/t/apicast-policy-fapi.t index 1efa14247..662d9c1db 100644 --- a/t/apicast-policy-fapi.t +++ b/t/apicast-policy-fapi.t @@ -1,8 +1,23 @@ use lib 't'; use Test::APIcast::Blackbox 'no_plan'; +use Crypt::JWT qw(encode_jwt); -# Test::Nginx does not allow to grep access logs, so we redirect them to -# stderr to be able to use "grep_error_log" by setting APICAST_ACCESS_LOG_FILE +our $private_key = `cat t/fixtures/rsa.pem`; +our $public_key = `cat t/fixtures/rsa.pub`; + +sub authorization_bearer_jwt (@) { + my ($aud, $payload, $kid) = @_; + + my $jwt = encode_jwt(payload => { + aud => $aud, + sub => 'someone', + iss => 'https://example.com/auth/realms/apicast', + exp => time + 3600, + %$payload, + }, key => \$private_key, alg => 'RS256', extra_headers => { kid => $kid }); + + return "Bearer $jwt"; +} run_tests(); @@ -308,3 +323,327 @@ GET /?user_key=value {"error": "invalid_request"} --- no_error_log [error] + + + +=== TEST 7: With oauth2_mtls enabled and digest of TLS Client Certificate equals to cnf claim +--- env random_port eval +( + 'APICAST_HTTPS_PORT' => "$Test::Nginx::Util::ServerPortForClient", + 'APICAST_HTTPS_CERTIFICATE' => "$Test::Nginx::Util::ServRoot/html/server.crt", + 'APICAST_HTTPS_CERTIFICATE_KEY' => "$Test::Nginx::Util::ServRoot/html/server.key", + 'BACKEND_ENDPOINT_OVERRIDE' => '' # disable override by Test::APIcast::Blackbox +) +--- configuration env +{ + "oidc": [ + { + "issuer": "https://example.com/auth/realms/apicast", + "config": { "id_token_signing_alg_values_supported": [ "RS256" ] }, + "keys": { "somekid": { "pem": "-----BEGIN PUBLIC KEY-----\nMFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBALClz96cDQ965ENYMfZzG+Acu25lpx2K\nNpAALBQ+catCA59us7+uLY5rjQR6SOgZpCz5PJiKNAdRPDJMXSmXqM0CAwEAAQ==\n-----END PUBLIC KEY-----", "alg": "RS256" } } + } + ], + "services": [ + { + "id": 42, + "backend_version": "oauth", + "backend_authentication_type": "service_token", + "backend_authentication_value": "token-value", + "proxy": { + "authentication_method": "oidc", + "oidc_issuer_endpoint": "https://example.com/auth/realms/apicast", + "api_backend": "http://test_backend:$TEST_NGINX_RANDOM_PORT", + "backend": { + "endpoint": "http://test_backend:$TEST_NGINX_RANDOM_PORT/", + "host": "localhost" + }, + "proxy_rules": [ + { "pattern": "/", "http_method": "GET", "metric_system_name": "hits", "delta": 1 } + ], + "policy_chain": [ + { + "name": "apicast.policy.fapi", + "configuration": { + "validate_oauth2_certificate_bound_access_token": true + } + }, + { "name": "apicast.policy.apicast" } + ] + } + } + ] +} +--- backend random_port env + listen $TEST_NGINX_RANDOM_PORT; + location /transactions/oauth_authrep.xml { + content_by_lua_block { + ngx.exit(200) + } + } + location /t { + content_by_lua_block { + ngx.say('yay, api backend') + } + } +--- test eval +my $jwt = ::authorization_bearer_jwt('audience', { + cnf => { 'x5t#S256' => "Y4_LVlkpE6qkscPbtoKm3iiKBgfwbOfbdKBEdnZ6ZPY" +}}, 'somekid'); +< "$Test::Nginx::Util::ServerPortForClient", + 'APICAST_HTTPS_CERTIFICATE' => "$Test::Nginx::Util::ServRoot/html/server.crt", + 'APICAST_HTTPS_CERTIFICATE_KEY' => "$Test::Nginx::Util::ServRoot/html/server.key", + 'BACKEND_ENDPOINT_OVERRIDE' => '' # disable override by Test::APIcast::Blackbox +) +--- configuration env +{ + "oidc": [ + { + "issuer": "https://example.com/auth/realms/apicast", + "config": { "id_token_signing_alg_values_supported": [ "RS256" ] }, + "keys": { "somekid": { "pem": "-----BEGIN PUBLIC KEY-----\nMFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBALClz96cDQ965ENYMfZzG+Acu25lpx2K\nNpAALBQ+catCA59us7+uLY5rjQR6SOgZpCz5PJiKNAdRPDJMXSmXqM0CAwEAAQ==\n-----END PUBLIC KEY-----", "alg": "RS256" } } + } + ], + "services": [ + { + "id": 42, + "backend_version": "oauth", + "backend_authentication_type": "service_token", + "backend_authentication_value": "token-value", + "proxy": { + "authentication_method": "oidc", + "oidc_issuer_endpoint": "https://example.com/auth/realms/apicast", + "api_backend": "http://test_backend:$TEST_NGINX_RANDOM_PORT", + "backend": { + "endpoint": "http://test_backend:$TEST_NGINX_RANDOM_PORT/", + "host": "localhost" + }, + "proxy_rules": [ + { "pattern": "/", "http_method": "GET", "metric_system_name": "hits", "delta": 1 } + ], + "policy_chain": [ + { + "name": "apicast.policy.fapi", + "configuration": { + "validate_oauth2_certificate_bound_access_token": true + } + }, + { "name": "apicast.policy.apicast" } + ] + } + } + ] +} +--- backend random_port env + listen $TEST_NGINX_RANDOM_PORT; + location /transactions/oauth_authrep.xml { + content_by_lua_block { + ngx.exit(200) + } + } + location /t { + content_by_lua_block { + ngx.say('yay, api backend') + } + } +--- test eval +my $jwt = ::authorization_bearer_jwt('audience', { + cnf => { 'x5t#S256' => "Y4_LVlkpE6qkscPbtoKm3iiKBgfwbOfbdKBEdnZ6ZPY" +}}, 'somekid'); +< "$Test::Nginx::Util::ServerPortForClient", + 'APICAST_HTTPS_CERTIFICATE' => "$Test::Nginx::Util::ServRoot/html/server.crt", + 'APICAST_HTTPS_CERTIFICATE_KEY' => "$Test::Nginx::Util::ServRoot/html/server.key", + 'BACKEND_ENDPOINT_OVERRIDE' => '' # disable override by Test::APIcast::Blackbox +) +--- configuration env +{ + "oidc": [ + { + "issuer": "https://example.com/auth/realms/apicast", + "config": { "id_token_signing_alg_values_supported": [ "RS256" ] }, + "keys": { "somekid": { "pem": "-----BEGIN PUBLIC KEY-----\nMFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBALClz96cDQ965ENYMfZzG+Acu25lpx2K\nNpAALBQ+catCA59us7+uLY5rjQR6SOgZpCz5PJiKNAdRPDJMXSmXqM0CAwEAAQ==\n-----END PUBLIC KEY-----", "alg": "RS256" } } + } + ], + "services": [ + { + "id": 42, + "backend_version": "oauth", + "backend_authentication_type": "service_token", + "backend_authentication_value": "token-value", + "proxy": { + "authentication_method": "oidc", + "oidc_issuer_endpoint": "https://example.com/auth/realms/apicast", + "api_backend": "http://test_backend:$TEST_NGINX_RANDOM_PORT", + "backend": { + "endpoint": "http://test_backend:$TEST_NGINX_RANDOM_PORT/", + "host": "localhost" + }, + "proxy_rules": [ + { "pattern": "/", "http_method": "GET", "metric_system_name": "hits", "delta": 1 } + ], + "policy_chain": [ + { + "name": "apicast.policy.fapi", + "configuration": { + "validate_oauth2_certificate_bound_access_token": true + } + }, + { "name": "apicast.policy.apicast" } + ] + } + } + ] +} +--- backend random_port env + listen $TEST_NGINX_RANDOM_PORT; + location /transactions/oauth_authrep.xml { + content_by_lua_block { + ngx.exit(200) + } + } + location /t { + content_by_lua_block { + ngx.say('yay, api backend') + } + } +--- test eval +my $jwt = ::authorization_bearer_jwt('audience', { + foo => "1", +}, 'somekid'); +< "$Test::Nginx::Util::ServerPortForClient", + 'APICAST_HTTPS_CERTIFICATE' => "$Test::Nginx::Util::ServRoot/html/server.crt", + 'APICAST_HTTPS_CERTIFICATE_KEY' => "$Test::Nginx::Util::ServRoot/html/server.key", + 'BACKEND_ENDPOINT_OVERRIDE' => '' # disable override by Test::APIcast::Blackbox +) +--- configuration env +{ + "oidc": [ + { + "issuer": "https://example.com/auth/realms/apicast", + "config": { "id_token_signing_alg_values_supported": [ "RS256" ] }, + "keys": { "somekid": { "pem": "-----BEGIN PUBLIC KEY-----\nMFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBALClz96cDQ965ENYMfZzG+Acu25lpx2K\nNpAALBQ+catCA59us7+uLY5rjQR6SOgZpCz5PJiKNAdRPDJMXSmXqM0CAwEAAQ==\n-----END PUBLIC KEY-----", "alg": "RS256" } } + } + ], + "services": [ + { + "id": 42, + "backend_version": "oauth", + "backend_authentication_type": "service_token", + "backend_authentication_value": "token-value", + "proxy": { + "authentication_method": "oidc", + "oidc_issuer_endpoint": "https://example.com/auth/realms/apicast", + "api_backend": "http://test_backend:$TEST_NGINX_RANDOM_PORT", + "backend": { + "endpoint": "http://test_backend:$TEST_NGINX_RANDOM_PORT/", + "host": "localhost" + }, + "proxy_rules": [ + { "pattern": "/", "http_method": "GET", "metric_system_name": "hits", "delta": 1 } + ], + "policy_chain": [ + { + "name": "apicast.policy.fapi", + "configuration": { + "validate_oauth2_certificate_bound_access_token": true + } + }, + { "name": "apicast.policy.apicast" } + ] + } + } + ] +} +--- backend random_port env + listen $TEST_NGINX_RANDOM_PORT; + location /transactions/oauth_authrep.xml { + content_by_lua_block { + ngx.exit(200) + } + } + location /t { + content_by_lua_block { + ngx.say('yay, api backend') + } + } +--- test eval +my $jwt = ::authorization_bearer_jwt('audience', { + cnf => { 'x5t#S256' => "invalid" +}}, 'somekid'); +< Date: Tue, 23 Jul 2024 11:48:31 +1000 Subject: [PATCH 2/2] [fapi] Fix incorrect example in README --- gateway/src/apicast/policy/fapi/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gateway/src/apicast/policy/fapi/README.md b/gateway/src/apicast/policy/fapi/README.md index e9bb619ae..a2fe27807 100644 --- a/gateway/src/apicast/policy/fapi/README.md +++ b/gateway/src/apicast/policy/fapi/README.md @@ -41,7 +41,7 @@ The FAPI policy supports various features of the Financial-grade API (FAPI) stan { "name": "apicast.policy.fapi", "configuration": { - "enable_oauth2_mtls": true + "validate_oauth2_certificate_bound_access_token": true } }, {