From 50d698c510861f90070480f14bf5dac39dae1f75 Mon Sep 17 00:00:00 2001 From: Michal Cichra Date: Wed, 19 Sep 2018 16:23:29 +0200 Subject: [PATCH] [oidc] expose OIDC as standalone policy --- CHANGELOG.md | 1 + gateway/src/apicast/configuration/service.lua | 2 +- gateway/src/apicast/oauth/oidc.lua | 121 +++++++---- .../policy/oidc_authentication/init.lua | 1 + .../oidc_authentication.lua | 94 +++++++++ gateway/src/apicast/proxy.lua | 2 +- spec/oauth/oidc_spec.lua | 59 +++--- .../oidc_authentication_spec.lua | 199 ++++++++++++++++++ t/apicast-policy-oidc_authentication.t | 90 ++++++++ 9 files changed, 497 insertions(+), 72 deletions(-) create mode 100644 gateway/src/apicast/policy/oidc_authentication/init.lua create mode 100644 gateway/src/apicast/policy/oidc_authentication/oidc_authentication.lua create mode 100644 spec/policy/oidc_authentication/oidc_authentication_spec.lua create mode 100644 t/apicast-policy-oidc_authentication.t diff --git a/CHANGELOG.md b/CHANGELOG.md index 75f1666ba..75abc8ce6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - Prometheus metrics for the 3scale batching policy [PR #902](https://github.com/3scale/apicast/pull/902) - Support for path in the upstream URL [PR #905](https://github.com/3scale/apicast/pull/905) +- OIDC Authentication policy (only useable directly by the configuration file) [PR #904](https://github.com/3scale/apicast/pull/904) ## [3.3.0-cr2] - 2018-09-25 diff --git a/gateway/src/apicast/configuration/service.lua b/gateway/src/apicast/configuration/service.lua index 45a4e1e99..b2a29abfe 100644 --- a/gateway/src/apicast/configuration/service.lua +++ b/gateway/src/apicast/configuration/service.lua @@ -213,7 +213,7 @@ function _M:oauth() local authentication = self.authentication_method or self.backend_version if authentication == 'oidc' then - return oauth.oidc.new(self) + return oauth.oidc.new(self.oidc) elseif authentication == 'oauth' then return oauth.apicast.new(self) else diff --git a/gateway/src/apicast/oauth/oidc.lua b/gateway/src/apicast/oauth/oidc.lua index 80056978d..5b28f1c00 100644 --- a/gateway/src/apicast/oauth/oidc.lua +++ b/gateway/src/apicast/oauth/oidc.lua @@ -8,6 +8,8 @@ local setmetatable = setmetatable local ngx_now = ngx.now local format = string.format local type = type +local tostring = tostring +local assert = assert local _M = { cache_size = 10000, @@ -28,20 +30,18 @@ local mt = { local empty = {} -function _M.new(service) - local oidc = service.oidc or empty - - local issuer = oidc.issuer or "" +function _M.new(oidc_config) + local oidc = oidc_config or empty + local issuer = oidc.issuer local config = oidc.config or empty local alg_values = config.id_token_signing_alg_values_supported or empty local err - if #issuer == 0 or #alg_values == 0 then + if not issuer or #alg_values == 0 then err = 'missing OIDC configuration' end return setmetatable({ - service = service, config = config, issuer = issuer, keys = oidc.keys or empty, @@ -51,7 +51,7 @@ function _M.new(service) jwt_claims = { -- 1. The JWT MUST contain an "iss" (issuer) claim that contains a -- unique identifier for the entity that issued the JWT. - iss = jwt_validators.equals_any_of({ issuer }), + iss = jwt_validators.chain(jwt_validators.required(), issuer and jwt_validators.equals_any_of({ issuer })), -- 2. The JWT MUST contain a "sub" (subject) claim identifying the -- principal that is the subject of the JWT. @@ -74,6 +74,9 @@ function _M.new(service) -- 6. The JWT MAY contain an "iat" (issued at) claim that identifies -- the time at which the JWT was issued. iat = jwt_validators.opt_greater_than(0), + + -- This is keycloak-specific. Its tokens have a 'typ' and we need to verify + typ = jwt_validators.opt_equals_any_of({ 'Bearer' }), }, }, mt), err end @@ -92,64 +95,102 @@ end -- Parses the token - in this case we assume it's a JWT token -- Here we can extract authenticated user's claims or other information returned in the access_token -- or id_token by RH SSO -local function parse_and_verify_token(self, jwt_token) +local function parse_and_verify_token(self, namespace, jwt_token) local cache = self.cache if not cache then return nil, 'not initialized' end - local cache_key = format('%s:%s', self.service.id, jwt_token) + local cache_key = format('%s:%s', namespace or '', jwt_token) + + local jwt = self:parse(jwt_token, cache_key) + + if jwt.verified then + return jwt + end + + local _, err = self:verify(jwt, cache_key) + + return jwt, err +end + +function _M:parse_and_verify(access_token, cache_key) + local jwt_obj, err = parse_and_verify_token(self, assert(cache_key, 'missing cache key'), access_token) + + if err then + if ngx.config.debug then + ngx.log(ngx.DEBUG, 'JWT object: ', require('inspect')(jwt_obj), ' err: ', err, ' reason: ', jwt_obj.reason) + end + return nil, jwt_obj and jwt_obj.reason or err + end - local jwt_obj = cache:get(cache_key) + return jwt_obj +end - if jwt_obj then +local jwt_mt = { + __tostring = function(jwt) + return jwt.token + end +} + +local function load_jwt(token) + local jwt = JWT:load_jwt(tostring(token)) + + jwt.token = token + + return setmetatable(jwt, jwt_mt) +end + +function _M:parse(jwt, cache_key) + local cached = cache_key and self.cache:get(cache_key) + + if cached then ngx.log(ngx.DEBUG, 'found JWT in cache for ', cache_key) - return jwt_obj + return cached end - jwt_obj = JWT:load_jwt(jwt_token) + return load_jwt(jwt) +end - if not jwt_obj.valid then - ngx.log(ngx.WARN, jwt_obj.reason) - return jwt_obj, 'JWT not valid' +function _M:verify(jwt, cache_key) + if not jwt then + return false, 'JWT missing' end - if not self.alg_whitelist[jwt_obj.header.alg] then - return jwt_obj, '[jwt] invalid alg' + if not jwt.valid then + ngx.log(ngx.WARN, jwt.reason) + return false, 'JWT not valid' end - -- TODO: this should be able to use DER format instead of PEM - local pubkey = find_public_key(jwt_obj, self.keys) - -- This is keycloak-specific. Its tokens have a 'typ' and we need to verify - -- it's Bearer. - local claims = self.jwt_claims - if jwt_obj.payload and jwt_obj.payload.typ then - claims.typ = jwt_validators.equals('Bearer') + if not self.alg_whitelist[jwt.header.alg] then + return false, '[jwt] invalid alg' end - jwt_obj = JWT:verify_jwt_obj(pubkey, jwt_obj, self.jwt_claims) + -- TODO: this should be able to use DER format instead of PEM + local pubkey = find_public_key(jwt, self.keys) + + jwt = JWT:verify_jwt_obj(pubkey, jwt, self.jwt_claims) - if not jwt_obj.verified then - ngx.log(ngx.DEBUG, "[jwt] failed verification for token, reason: ", jwt_obj.reason) - return jwt_obj, "JWT not verified" + if not jwt.verified then + ngx.log(ngx.DEBUG, "[jwt] failed verification for token, reason: ", jwt.reason) + return false, "JWT not verified" end - ngx.log(ngx.DEBUG, 'adding JWT to cache ', cache_key) - local ttl = timestamp_to_seconds_from_now(jwt_obj.payload.exp, self.clock) - cache:set(cache_key, jwt_obj, ttl) + if cache_key then + ngx.log(ngx.DEBUG, 'adding JWT to cache ', cache_key) + local ttl = timestamp_to_seconds_from_now(jwt.payload.exp, self.clock) + -- use the JWT itself in case there is no cache key + self.cache:set(cache_key, jwt, ttl) + end - return jwt_obj + return true end - -function _M:transform_credentials(credentials) - local jwt_obj, err = parse_and_verify_token(self, credentials.access_token) +function _M:transform_credentials(credentials, cache_key) + local jwt_obj, err = self:parse_and_verify(credentials.access_token, cache_key or '') if err then - if ngx.config.debug then - ngx.log(ngx.DEBUG, 'JWT object: ', require('inspect')(jwt_obj), ' err: ', err, ' reason: ', jwt_obj.reason) - end - return nil, nil, nil, jwt_obj and jwt_obj.reason or err + return nil, nil, nil, err end local payload = jwt_obj.payload diff --git a/gateway/src/apicast/policy/oidc_authentication/init.lua b/gateway/src/apicast/policy/oidc_authentication/init.lua new file mode 100644 index 000000000..5438f647e --- /dev/null +++ b/gateway/src/apicast/policy/oidc_authentication/init.lua @@ -0,0 +1 @@ +return require('oidc_authentication') diff --git a/gateway/src/apicast/policy/oidc_authentication/oidc_authentication.lua b/gateway/src/apicast/policy/oidc_authentication/oidc_authentication.lua new file mode 100644 index 000000000..46d58aaa2 --- /dev/null +++ b/gateway/src/apicast/policy/oidc_authentication/oidc_authentication.lua @@ -0,0 +1,94 @@ +-- OpenID Connect Authentication policy +-- It will verify JWT signature against a list of public keys +-- discovered through OIDC Discovery from the IDP. + +local lrucache = require('resty.lrucache') +local OIDC = require('apicast.oauth.oidc') +local oidc_discovery = require('resty.oidc.discovery') +local http_authorization = require('resty.http_authorization') +local resty_url = require('resty.url') +local policy = require('apicast.policy') +local _M = policy.new('oidc_authentication') + +local tostring = tostring + +_M.cache_size = 100 + +function _M.init() + _M.cache = lrucache.new(_M.cache_size) +end + +local function valid_issuer_endpoint(endpoint) + return resty_url.parse(endpoint) and endpoint +end + +local new = _M.new +--- Initialize a oidc_authentication +-- @tparam[opt] table config Policy configuration. +function _M.new(config) + local self = new(config) + + self.issuer_endpoint = valid_issuer_endpoint(config and config.issuer_endpoint) + self.discovery = oidc_discovery.new(self.http_backend) + + self.oidc = (config and config.oidc) or OIDC.new(self.discovery:call(self.issuer_endpoint)) + + self.required = config and config.required + + return self +end + +local function bearer_token() + return http_authorization.new(ngx.var.http_authorization).token +end + +function _M:rewrite(context) + local access_token = bearer_token() + + if access_token or self.required then + local jwt, err = self.oidc:parse(access_token) + + if jwt then + context[self] = jwt + context.jwt = jwt + else + ngx.log(ngx.WARN, 'failed to parse access token ', access_token, ' err: ', err) + end + end +end + +local function exit_status(status) + ngx.status = status + -- TODO: implement content negotiation to generate proper content with an error + return ngx.exit(status) +end + +local function challenge_response() + ngx.header.www_authenticate = 'Bearer' + + return exit_status(ngx.HTTP_UNAUTHORIZED) +end + +function _M:access(context) + local jwt = context[self] + + if not jwt or not jwt.token then + if self.required then + return challenge_response() + else + return + end + end + + local ok, err = self.oidc:verify(jwt) + + if not ok then + ngx.log(ngx.INFO, 'JWT verification error: ', err, ' token: ', tostring(jwt)) + + return exit_status(ngx.HTTP_FORBIDDEN) + end + + return ok +end + +return _M diff --git a/gateway/src/apicast/proxy.lua b/gateway/src/apicast/proxy.lua index a44f9e49e..e9f8ccd97 100644 --- a/gateway/src/apicast/proxy.lua +++ b/gateway/src/apicast/proxy.lua @@ -275,7 +275,7 @@ function _M:rewrite(service, context) if self.oauth then local jwt_payload - credentials, ttl, jwt_payload, err = self.oauth:transform_credentials(credentials) + credentials, ttl, jwt_payload, err = self.oauth:transform_credentials(credentials, service.id) if err then ngx.log(ngx.DEBUG, 'oauth failed with ', err) diff --git a/spec/oauth/oidc_spec.lua b/spec/oauth/oidc_spec.lua index 6ae108ffd..113bb50e2 100644 --- a/spec/oauth/oidc_spec.lua +++ b/spec/oauth/oidc_spec.lua @@ -9,29 +9,29 @@ describe('OIDC', function() describe('.new', function() it('returns error when issuer is missing', function() - local oidc, err = _M.new({ oidc = { + local oidc, err = _M.new({ config = { id_token_signing_alg_values_supported = { 'RS256' } } - }}) + }) assert(oidc, 'still returns oidc object') assert.equal('missing OIDC configuration', err) end) it('returns error when supported algorithms are missing', function() - local oidc, err = _M.new({ oidc = { + local oidc, err = _M.new({ issuer = 'http://example.com', config = { id_token_signing_alg_values_supported = {} } - }}) + }) assert(oidc, 'still returns oidc object') assert.equal('missing OIDC configuration', err) end) it('returns no error with valid OIDC configuration', function() - local oidc, err = _M.new({ oidc = { + local oidc, err = _M.new({ issuer = 'http://example.com', config = { id_token_signing_alg_values_supported = { 'RS256' } } - }}) + }) assert(oidc, 'still returns oidc object') assert.falsy(err) @@ -39,23 +39,20 @@ describe('OIDC', function() end) describe(':transform_credentials', function() - local service = { - id = 1, - oidc = { - issuer = 'https://example.com/auth/realms/apicast', - config = { id_token_signing_alg_values_supported = { 'RS256' } }, - keys = { somekid = { pem = rsa.pub } }, - } + local oidc_config = { + issuer = 'https://example.com/auth/realms/apicast', + config = { id_token_signing_alg_values_supported = { 'RS256' } }, + keys = { somekid = { pem = rsa.pub } }, } before_each(function() jwt_validators.set_system_clock(function() return 0 end) end) it('successfully verifies token', function() - local oidc = _M.new(service) + local oidc = _M.new(oidc_config) local access_token = jwt:sign(rsa.private, { header = { typ = 'JWT', alg = 'RS256', kid = 'somekid' }, payload = { - iss = service.oidc.issuer, + iss = oidc_config.issuer, aud = 'notused', azp = 'ce3b2e5e', sub = 'someone', @@ -72,11 +69,11 @@ describe('OIDC', function() end) it('caches verification', function() - local oidc = _M.new(service) + local oidc = _M.new(oidc_config) local access_token = jwt:sign(rsa.private, { header = { typ = 'JWT', alg = 'RS256', kid = 'somekid' }, payload = { - iss = service.oidc.issuer, + iss = oidc_config.issuer, aud = {'ce3b2e5e','notused'}, sub = 'someone', exp = ngx.now() + 10, @@ -93,14 +90,16 @@ describe('OIDC', function() assert.same({ app_id = "ce3b2e5e" }, credentials) end + + assert.stub(stubbed).was_not_called() end) it('verifies iss', function() - local oidc = _M.new(service) + local oidc = _M.new(oidc_config) local access_token = jwt:sign(rsa.private, { header = { typ = 'JWT', alg = 'RS256', kid = 'somekid' }, payload = { - iss = service.oidc.issuer, + iss = oidc_config.issuer, aud = 'foobar', sub = 'someone', exp = ngx.now() + 10, @@ -117,11 +116,11 @@ describe('OIDC', function() local now = 1123744391 stub(ngx, 'now', now) - local oidc = _M.new(service) + local oidc = _M.new(oidc_config) local access_token = jwt:sign(rsa.private, { header = { typ = 'JWT', alg = 'RS256', kid = 'somekid' }, payload = { - iss = service.oidc.issuer, + iss = oidc_config.issuer, aud = 'foobar', sub = 'someone', nbf = now + 5, @@ -135,11 +134,11 @@ describe('OIDC', function() end) it('verifies iat', function() - local oidc = _M.new(service) + local oidc = _M.new(oidc_config) local access_token = jwt:sign(rsa.private, { header = { typ = 'JWT', alg = 'RS256', kid = 'somekid' }, payload = { - iss = service.oidc.issuer, + iss = oidc_config.issuer, aud = 'foobar', sub = 'someone', iat = 0, @@ -153,11 +152,11 @@ describe('OIDC', function() end) it('verifies exp', function() - local oidc = _M.new(service) + local oidc = _M.new(oidc_config) local access_token = jwt:sign(rsa.private, { header = { typ = 'JWT', alg = 'RS256', kid = 'somekid' }, payload = { - iss = service.oidc.issuer, + iss = oidc_config.issuer, aud = 'foobar', sub = 'someone', exp = 1, @@ -177,7 +176,7 @@ describe('OIDC', function() end) it('verifies alg', function() - local oidc = _M.new(service) + local oidc = _M.new(oidc_config) local access_token = jwt:sign(rsa.private, { header = { typ = 'JWT', alg = 'HS256' }, payload = { }, @@ -190,11 +189,11 @@ describe('OIDC', function() end) it('validation fails when typ is invalid', function() - local oidc = _M.new(service) + local oidc = _M.new(oidc_config) local access_token = jwt:sign(rsa.private, { header = { typ = 'JWT', alg = 'RS256', kid = 'somekid' }, payload = { - iss = service.oidc.issuer, + iss = oidc_config.issuer, aud = 'notused', azp = 'ce3b2e5e', sub = 'someone', @@ -210,11 +209,11 @@ describe('OIDC', function() end) it('validation is successful when typ is included and is Bearer', function() - local oidc = _M.new(service) + local oidc = _M.new(oidc_config) local access_token = jwt:sign(rsa.private, { header = { typ = 'JWT', alg = 'RS256', kid = 'somekid' }, payload = { - iss = service.oidc.issuer, + iss = oidc_config.issuer, aud = 'notused', azp = 'ce3b2e5e', sub = 'someone', diff --git a/spec/policy/oidc_authentication/oidc_authentication_spec.lua b/spec/policy/oidc_authentication/oidc_authentication_spec.lua new file mode 100644 index 000000000..acce3d6c4 --- /dev/null +++ b/spec/policy/oidc_authentication/oidc_authentication_spec.lua @@ -0,0 +1,199 @@ +local _M = require('apicast.policy.oidc_authentication') +local JWT = require('resty.jwt') +local rsa = require('fixtures.rsa') +local OIDC = require('apicast.oauth.oidc') +local http_ng = require 'resty.http_ng' + +local access_token = setmetatable({ + header = { typ = 'JWT', alg = 'RS256', kid = 'somekid' }, + payload = { + iss = 'http://example.com/issuer', + sub = 'some', + aud = 'one', + exp = ngx.now() + 3600, + }, +}, { __tostring = function(jwt) return JWT:sign(rsa.private, jwt) end }) + +describe('oidc_authentication policy', function() + describe('.new', function() + it('works without configuration', function() + assert(_M.new()) + end) + + it('accepts empty configuration', function() + assert(_M.new({ })) + end) + + it('accepts issuer_endpoint configuration', function() + http_ng.backend() + .expect{ url = "http://example.com/path/.well-known/openid-configuration" } + .respond_with{ status = 404 } + + assert(_M.new({ issuer_endpoint = 'http://example.com/path' })) + end) + + it('ignores invalid issuer_endpoint configuration', function() + assert(_M.new({ issuer_endpoint = 'http: example.com path' })) + end) + + it('uses discovery', function() + local issuer_endpoint = 'http://example.com' + + do + local test_backend = http_ng.backend() + local jwks_uri = issuer_endpoint .. '/jwks' + + test_backend + .expect{ url = issuer_endpoint .. "/.well-known/openid-configuration" } + .respond_with{ status = 200, headers = { content_type = 'application/json;charset=UTF-8' }, + body = [[ + { + "issuer": "]] .. issuer_endpoint .. [[", + "jwks_uri": "]] .. jwks_uri .. [[", + "id_token_signing_alg_values_supported": [ "RS256" ] + } + ]] } + + test_backend + .expect{ url = jwks_uri } + .respond_with{ + status = 200, + headers = { content_type = 'application/json' }, + body = [[ { "keys": [{ + "kid": "3g-I9PWt6NrznPLcbE4zZrakXar27FDKEpqRPlD2i2Y", + "kty": "RSA", + "n": "iqXwBiZgN2q1dCKU1P_vzyiGacdQhfqgxQST7GFlWU_PUljV9uHrLOadWadpxRAuskNpXWsrKoU_hDxtSpUIRJj6hL5YTlrvv-IbFwPNtD8LnOfKL043_ZdSOe3aT4R4NrBxUomndILUESlhqddylVMCGXQ81OB73muc9ovR68Ajzn8KzpU_qegh8iHwk-SQvJxIIvgNJCJTC6BWnwS9Bw2ns0fQOZZRjWFRVh8BjkVdqa4vCAb6zw8hpR1y9uSNG-fqUAPHy5IYQaD8k8QX0obxJ0fld61fH-Wr3ENpn9YZWYBcKvnwLm2bvxqmNVBzW4rhGEZb9mf-KrSagD5GUw", + "e": "AQAB" + }] } ]] + } + end + + _M.new{ issuer_endpoint = issuer_endpoint } + end) + end) + + describe(':rewrite', function() + before_each(function() + ngx.var = {} + end) + + it('works without config', function() + _M.new():rewrite({}) + end) + + it('works with empty config', function() + _M.new({}):rewrite({}) + end) + + it('stores parsed access token in the context', function() + local policy = _M.new() + local context = { jwt = {} } + + ngx.var.http_authorization = 'Bearer ' .. tostring(access_token) + + policy.oidc.alg_whitelist = { RS256 = true } + policy.oidc.keys = { [access_token.header.kid] = { pem = rsa.pub } } + + policy:rewrite(context) + + assert.is_table(context.jwt) + assert.same(access_token.payload, context.jwt.payload) + end) + + it('handles invalid token', function() + local policy = _M.new() + local context = { } + + ngx.var.http_authorization = 'Bearer ' .. 'invalid token value' + + policy:rewrite(context) + + assert.is_table(context.jwt) + assert.contains({reason = 'invalid jwt string', valid = false }, context.jwt) + end) + end) + + describe(':access', function() + + it('works without config', function() + _M.new():access({}) + end) + + it('works with empty config', function() + _M.new({}):access({}) + end) + + context('when OIDC is required', function () + + local policy + before_each(function() + policy = _M.new{ required = true } + end) + + it('sends a challenge when token is not sent', function() + spy.on(ngx, 'exit') + + ngx.header = { } + policy:access({}) + + assert.spy(ngx.exit).was_called_with(ngx.HTTP_UNAUTHORIZED) + assert.same('Bearer', ngx.header.www_authenticate) + end) + + it('returns forbidden on invalid token', function() + spy.on(ngx, 'exit') + local jwt = { token = 'invalid' } + local context = { [policy] = jwt } + + ngx.header = { } + policy:access(context) + + assert.spy(ngx.exit).was_called_with(ngx.HTTP_FORBIDDEN) + end) + + end) + + context('when OIDC is optional', function () + local policy + before_each(function() + policy = _M.new{ required = false } + end) + + it('does nothing when token is not sent', function() + spy.on(ngx, 'exit') + + policy:access({}) + + assert.spy(ngx.exit).was_not_called() + end) + + it('returns forbidden on invalid token', function() + spy.on(ngx, 'exit') + local jwt = { token = 'invalid' } + local context = { [policy] = jwt } + + ngx.header = { } + policy:access(context) + + assert.spy(ngx.exit).was_called_with(ngx.HTTP_FORBIDDEN) + end) + end) + + it('continues on valid token', function() + spy.on(ngx, 'exit') + + local oidc = OIDC.new{ + issuer = access_token.payload.iss, + config = { id_token_signing_alg_values_supported = { access_token.header.alg } }, + keys = { [access_token.header.kid] = { pem = rsa.pub } }, + } + local policy = _M.new{ oidc = oidc } + local jwt = oidc:parse(access_token) + local context = { [policy] = jwt } + + assert.is_true(policy:access(context)) + + assert.spy(ngx.exit).was_not_called() + end) + end) +end) diff --git a/t/apicast-policy-oidc_authentication.t b/t/apicast-policy-oidc_authentication.t new file mode 100644 index 000000000..90bd2dca7 --- /dev/null +++ b/t/apicast-policy-oidc_authentication.t @@ -0,0 +1,90 @@ +use lib 't'; +use Test::APIcast::Blackbox 'no_plan'; + +our $rsa = `cat t/fixtures/rsa.pem`; + +run_tests(); + +__DATA__ + +=== TEST 1: oidc_authentication accepts configuration +--- configuration +{ + "services": [ + { + "proxy": { + "policy_chain": [ + { "name": "apicast.policy.oidc_authentication", + "configuration": { } }, + { "name": "apicast.policy.echo" } + ] + } + } + ] +} +--- request +GET /t +--- response_body +GET /t HTTP/1.1 +--- error_code: 200 +--- no_error_log +[error] + + +=== TEST 2: Uses OIDC Discovery to load OIDC configuration and verify the JWT +--- env eval +( 'APICAST_CONFIGURATION_LOADER' => 'lazy' ) +--- backend +location = /realm/.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" } + ] } + ]]) + } +} +--- configuration +{ + "services": [ + { + "proxy": { + "policy_chain": [ + { "name": "apicast.policy.oidc_authentication", + "configuration": { + "issuer_endpoint": "http://test_backend:$TEST_NGINX_SERVER_PORT/realm" } }, + { "name": "apicast.policy.echo" } + ] + } + } + ] +} +--- 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 +GET /echo HTTP/1.1 +--- no_error_log +[error]