From 17289c22c1cd642a24817aa53c982e93dd3e1994 Mon Sep 17 00:00:00 2001 From: Cristian <6584521+ricristian@users.noreply.github.com> Date: Sun, 3 Nov 2024 16:40:19 +0200 Subject: [PATCH] Compile latest update of the plugin that could be found on internet & fix request to the redirect_uri path but there's no session state found --- .env | 11 +- .gitignore | 12 +- .travis.yml | 6 +- AUTHORS | 21 ++ README.md | 148 ++++++-- bin/build-env.sh | 3 +- bin/my-ip.sh | 9 + ci/setup.sh | 3 +- ...kspec => kong-plugin-oidc-1.4.0-1.rockspec | 14 +- kong/plugins/oidc/handler.lua | 154 ++++++-- kong/plugins/oidc/schema.lua | 328 +++++++++++++++++- kong/plugins/oidc/session.lua | 5 +- kong/plugins/oidc/utils.lua | 163 ++++++++- test/docker/integration/Dockerfile | 8 +- test/docker/integration/docker-compose.yml | 9 +- test/docker/integration/keycloak_client.py | 48 +++ test/docker/integration/kong_client.py | 59 ++++ test/docker/integration/nginx-redis.kong.conf | 4 + test/docker/integration/setup.py | 108 +----- test/docker/unit/Dockerfile | 6 +- test/unit/mockable_case.lua | 31 ++ test/unit/run.sh | 6 +- test/unit/test_already_auth.lua | 44 +++ test/unit/test_bearer_jwt_auth.lua | 68 ++++ test/unit/test_filters_advanced.lua | 8 +- test/unit/test_handler_mocking_openidc.lua | 148 +++++--- test/unit/test_header_claims.lua | 33 ++ test/unit/test_introspect.lua | 34 +- test/unit/test_utils.lua | 35 +- 29 files changed, 1238 insertions(+), 288 deletions(-) create mode 100644 AUTHORS create mode 100644 bin/my-ip.sh rename kong-oidc-1.1.0-0.rockspec => kong-plugin-oidc-1.4.0-1.rockspec (86%) create mode 100644 test/docker/integration/keycloak_client.py create mode 100644 test/docker/integration/kong_client.py create mode 100644 test/docker/integration/nginx-redis.kong.conf create mode 100644 test/unit/test_already_auth.lua create mode 100644 test/unit/test_bearer_jwt_auth.lua create mode 100644 test/unit/test_header_claims.lua diff --git a/.env b/.env index 681c806f..099ff02c 100644 --- a/.env +++ b/.env @@ -2,17 +2,18 @@ BUILD_IMG_NAME=nokia/kong-oidc INTEGRATION_PATH=test/docker/integration UNIT_PATH=test/docker/unit -KONG_BASE_TAG=:1.0-centos +KONG_BASE_TAG=:2.8.0-ubuntu KONG_TAG= -KONG_DB_TAG=:10.1 +KONG_DB_TAG=:14 KONG_DB_PORT=5432 KONG_DB_USER=kong KONG_DB_PW=kong KONG_DB_NAME=kong +KONG_SESSION_STORE_PORT=6379 KONG_HTTP_PROXY_PORT=8000 KONG_HTTP_ADMIN_PORT=8001 -KEYCLOAK_TAG=:4.8.3.Final -KEYCLOAK_PORT=8080 +KEYCLOAK_TAG=:16.1.1 +KEYCLOAK_PORT=8081 KEYCLOAK_USER=admin -KEYCLOAK_PW=password \ No newline at end of file +KEYCLOAK_PW=password diff --git a/.gitignore b/.gitignore index b59fc222..c9009a1c 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,14 @@ \#*\# .\#* lua_install -luacov.stats.out \ No newline at end of file +luacov.stats.out +venv/ +bin/venv/ +**/__pycache__/ +# Visual Studio Code +.vscode/ + +# IntelliJ IDEA +*.iml +.idea +.dccache \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index 1df2c948..26ce344c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,10 +5,7 @@ language: python sudo: true env: - - LUA_VERSION="5.1" KONG_VERSION="0.13.0-0" LUA_RESTY_OPENIDC_VERSION="1.6.1-1" - - LUA_VERSION="5.1" KONG_VERSION="0.12.3-0" LUA_RESTY_OPENIDC_VERSION="1.6.1-1" - - LUA_VERSION="5.1" KONG_VERSION="0.11.2-0" LUA_RESTY_OPENIDC_VERSION="1.6.1-1" - - LUA_VERSION="5.1" KONG_VERSION="1.0.2-0" LUA_RESTY_OPENIDC_VERSION="1.6.1-1" + - LUA_VERSION="5.1" KONG_VERSION="1.5.0-0" LUA_RESTY_OPENIDC_VERSION="1.7.2-1" script: - sudo -E bash ci/root.sh @@ -18,4 +15,3 @@ script: after_success: - luarocks install luacov-coveralls - luacov-coveralls - diff --git a/AUTHORS b/AUTHORS new file mode 100644 index 00000000..53663ab8 --- /dev/null +++ b/AUTHORS @@ -0,0 +1,21 @@ +List of authors based on the git log: + +Adam Płaczek +Damian Czaja +Geoff Kassel +Gergely Csatari +Hannu Laurila +Joshua Erney +Lars Wilhelmsen +Luka Lodrant +Micah Silverman +Michal Kulik +Nazarii Makarenko +Pavel Mikhalchuk +pekka.hirvonen +The Gitter Badger +Tom Milligan +Trojan295 +Tuomo Syrjanen +Yoriyasu Yano +Yuan Cheung diff --git a/README.md b/README.md index 82a65bf2..e594c0bb 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,16 @@ +`Finally we have a fix for request to the redirect_uri path but there's no session state found by upgrading lua-resty-openidc to 1.8.0-1` + +## This project wouldn’t be possible without the amazing work of nokia/kong-oidc and revomatico/kong-oidc. Thank you!" +https://github.com/nokia/kong-oidc + +https://github.com/revomatico/kong-oidc + + # What is Kong OIDC plugin [![Join the chat at https://gitter.im/nokia/kong-oidc](https://badges.gitter.im/nokia/kong-oidc.svg)](https://gitter.im/nokia/kong-oidc?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) -**Continuous Integration:** [![Build Status](https://travis-ci.org/nokia/kong-oidc.svg?branch=master)](https://travis-ci.org/nokia/kong-oidc) +**Continuous Integration:** [![Build Status](https://travis-ci.org/nokia/kong-oidc.svg?branch=master)](https://travis-ci.org/nokia/kong-oidc) [![Coverage Status](https://coveralls.io/repos/github/nokia/kong-oidc/badge.svg?branch=master)](https://coveralls.io/github/nokia/kong-oidc?branch=master)
**kong-oidc** is a plugin for [Kong](https://github.com/Mashape/kong) implementing the @@ -16,14 +24,16 @@ It maintains sessions for authenticated users by leveraging `lua-resty-openidc` a configurable choice between storing the session state in a client-side browser cookie or use in of the server-side storage mechanisms `shared-memory|memcache|redis`. +> **Note:** at the moment, there is an issue using memcached/redis, probably due to session locking: the sessions freeze. Help to debug this is appreciated. I am currently using shared memory to store sessions. + It supports server-wide caching of resolved Discovery documents and validated Access Tokens. It can be used as a reverse proxy terminating OAuth/OpenID Connect in front of an origin server so that the origin server/services can be protected with the relevant standards without implementing those on the server itself. -Introspection functionality add capability for already authenticated users and/or applications that -already posses acces token to go through kong. The actual token verification is then done by Resource Server. +The introspection functionality adds capability for already authenticated users and/or applications that +already possess access token to go through kong. The actual token verification is then done by Resource Server. ## How does it work @@ -33,25 +43,28 @@ The diagram below shows the message exchange between the involved parties. The `X-Userinfo` header contains the payload from the Userinfo Endpoint -``` +```json X-Userinfo: {"preferred_username":"alice","id":"60f65308-3510-40ca-83f0-e9c0151cc680","sub":"60f65308-3510-40ca-83f0-e9c0151cc680"} ``` -The plugin also sets the `ngx.ctx.authenticated_consumer` variable, which can be using in other Kong plugins: -``` -ngx.ctx.authenticated_consumer = { +The plugin also sets the `ngx.ctx.authenticated_credential` variable, which can be using in other Kong plugins: + +```lua +ngx.ctx.authenticated_credential = { id = "60f65308-3510-40ca-83f0-e9c0151cc680", -- sub field from Userinfo username = "alice" -- preferred_username from Userinfo } ``` +For successfully authenticated request, possible (anonymous) consumer identity set by higher priority plugin is cleared as part of setting the credentials. + +The plugin will try to retrieve the user's groups from a field in the token (default `groups`) and set `kong.ctx.shared.authenticated_groups` so that Kong authorization plugins can make decisions based on the user's group membership. ## Dependencies **kong-oidc** depends on the following package: -- [`lua-resty-openidc`](https://github.com/pingidentity/lua-resty-openidc/) - +- [`lua-resty-openidc`](https://github.com/zmartzone/lua-resty-openidc/) ## Installation @@ -59,35 +72,55 @@ If you're using `luarocks` execute the following: luarocks install kong-oidc -You also need to set the `KONG_PLUGINS` environment variable +[Kong >= 0.14] Since `KONG_CUSTOM_PLUGINS` has been removed, you also need to set the `KONG_PLUGINS` environment variable to include besides the bundled ones, oidc + + export KONG_PLUGINS=bundled,oidc - export KONG_PLUGINS=oidc - ## Usage ### Parameters -| Parameter | Default | Required | description | -| --- | --- | --- | --- | -| `name` || true | plugin name, has to be `oidc` | -| `config.client_id` || true | OIDC Client ID | -| `config.client_secret` || true | OIDC Client secret | -| `config.discovery` | https://.well-known/openid-configuration | false | OIDC Discovery Endpoint (`/.well-known/openid-configuration`) | -| `config.scope` | openid | false| OAuth2 Token scope. To use OIDC it has to contains the `openid` scope | -| `config.ssl_verify` | false | false | Enable SSL verification to OIDC Provider | -| `config.session_secret` | | false | Additional parameter, which is used to encrypt the session cookie. Needs to be random | -| `config.introspection_endpoint` | | false | Token introspection endpoint | -| `config.timeout` | | false | OIDC endpoint calls timeout | -| `config.introspection_endpoint_auth_method` | client_secret_basic | false | Token introspection auth method. resty-openidc supports `client_secret_(basic|post)` | -| `config.bearer_only` | no | false | Only introspect tokens without redirecting | -| `config.realm` | kong | false | Realm used in WWW-Authenticate response header | -| `config.logout_path` | /logout | false | Absolute path used to logout from the OIDC RP | - -### Enabling +| Parameter | Default | Required | description | +| ------------------------------------------- | ------------------------------------------ | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `name` | | true | plugin name, has to be `oidc` | +| `config.client_id` | | true | OIDC Client ID | +| `config.client_secret` | | true | OIDC Client secret | +| `config.discovery` | | false | OIDC Discovery Endpoint (`/.well-known/openid-configuration`) | +| `config.scope` | openid | false | OAuth2 Token scope. To use OIDC it has to contains the `openid` scope | +| `config.ssl_verify` | false | false | Enable SSL verification to OIDC Provider | +| `config.session_secret` | | false | Additional parameter, which is used to encrypt the session cookie. Needs to be random | +| `config.introspection_endpoint` | | false | Token introspection endpoint | +| `config.timeout` | | false | OIDC endpoint calls timeout | +| `config.introspection_endpoint_auth_method` | client_secret_basic | false | Token introspection authentication method. `resty-openidc` supports `client_secret_(basic\|post)` | +| `config.bearer_only` | no | false | Only introspect tokens without redirecting | +| `config.realm` | kong | false | Realm used in WWW-Authenticate response header | +| `config.logout_path` | /logout | false | Absolute path used to logout from the OIDC RP | +| `config.unauth_action` | auth | false | What action to take when unauthenticated
- `auth` to redirect to the login page and attempt (re)authenticatation,
- `deny` to stop with 401 | +| `config.recovery_page_path` | | false | Path of a recovery page to redirect the user when error occurs (except 401). To not show any error, you can use '/' to redirect immediately home. The error will be logged server side. | +| `config.ignore_auth_filters` | | false | A comma-separated list of endpoints to bypass authentication for | +| `config.redirect_uri` | | false | A relative or absolute URI the OP will redirect to after successful authentication | +| `config.userinfo_header_name` | `X-Userinfo` | false | The name of the HTTP header to use when passing the UserInfo to the upstream server | +| `config.id_token_header_name` | `X-ID-Token` | false | The name of the HTTP header to use when passing the ID Token to the upstream server | +| `config.access_token_header_name` | `X-Access-Token` | false | The name of the HTTP header to use when passing the Access Token to the upstream server | +| `config.access_token_as_bearer` | no | false | Whether or not the access token should be passed as a Bearer token | +| `config.disable_userinfo_header` | no | false | Disable passing the Userinfo to the upstream server | +| `config.disable_id_token_header` | no | false | Disable passing the ID Token to the upstream server | +| `config.disable_access_token_header` | no | false | Disable passing the Access Token to the upstream server | +| `config.groups_claim` | groups | false | Name of the claim in the token to get groups from | +| `config.skip_already_auth_requests` | no | false | Ignore requests where credentials have already been set by a higher priority plugin such as basic-auth | +| `config.bearer_jwt_auth_enable` | no | false | Authenticate based on JWT (ID) token provided in Authorization (Bearer) header. Checks iss, sub, aud, exp, iat (as in ID token). `config.discovery` must be defined to discover JWKS | +| `config.bearer_jwt_auth_allowed_auds` | | false | List of JWT token `aud` values allowed when validating JWT token in Authorization header. If not provided, uses value from `config.client_id` | +| `config.bearer_jwt_auth_signing_algs` | [ 'RS256' ] | false | List of allowed signing algorithms for Authorization header JWT token validation. Must match to OIDC provider and `resty-openidc` supported algorithms | +| `config.header_names` | | false | List of custom upstream HTTP headers to be added based on claims. Must have same number of elements as `config.header_claims`. Example: `[ 'x-oidc-email', 'x-oidc-email-verified' ]` | +| `config.header_claims` | | false | List of claims to be used as source for custom upstream headers. Claims are sourced from Userinfo, ID Token, Bearer JWT, Introspection, depending on auth method. Use only claims containing simple string values. Example: `[ 'email', 'email_verified'` | +| `config.http_proxy` || false | http proxy url | +| `config.https_proxy` || false | https proxy url (only supports url format __http__://proxy and not __https__://proxy) | + +### Enabling kong-oidc To enable the plugin only for one API: -``` +```http POST /apis//plugins/ HTTP/1.1 Host: localhost:8001 Content-Type: application/x-www-form-urlencoded @@ -97,7 +130,8 @@ name=oidc&config.client_id=kong-oidc&config.client_secret=29d98bf7-168c-4874-b8e ``` To enable the plugin globally: -``` + +```http POST /plugins HTTP/1.1 Host: localhost:8001 Content-Type: application/x-www-form-urlencoded @@ -107,7 +141,8 @@ name=oidc&config.client_id=kong-oidc&config.client_secret=29d98bf7-168c-4874-b8e ``` A successful response: -``` + +```http HTTP/1.1 201 Created Date: Tue, 24 Oct 2017 19:37:38 GMT Content-Type: application/json; charset=utf-8 @@ -136,9 +171,11 @@ Server: kong/0.11.0 ### Upstream API request +For successfully authenticated request, the plugin will set upstream header `X-Credential-Identifier` to contain `sub` claim from user info, ID token or introspection result. Header `X-Anonymous-Consumer` is cleared. + The plugin adds a additional `X-Userinfo`, `X-Access-Token` and `X-Id-Token` headers to the upstream request, which can be consumer by upstream server. All of them are base64 encoded: -``` +```http GET / HTTP/1.1 Host: netcat:9000 Connection: keep-alive @@ -159,6 +196,45 @@ X-Access-Token: eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJGenFSY0N1Ry13 X-Id-Token: eyJuYmYiOjAsImF6cCI6ImtvbmciLCJpYXQiOjE1NDg1MTA3NjksImlzcyI6Imh0dHA6XC9cLzE5Mi4xNjguMC45OjgwODBcL2F1dGhcL3JlYWxtc1wvbWFzdGVyIiwiYXVkIjoia29uZyIsIm5vbmNlIjoiZjRkZDQ1NmMwY2U2OGZhZmFiZjRmOGQwN2I0NGFhODYiLCJwcmVmZXJyZWRfdXNlcm5hbWUiOiJhZG1pbiIsImF1dGhfdGltZSI6MTU0ODUxMDY5NywiYWNyIjoiMSIsInNlc3Npb25fc3RhdGUiOiJiNDZmODU2Ny0zODA3LTQ0YmMtYmU1Mi1iMTNiNWQzODI5MTQiLCJleHAiOjE1NDg1MTA4MjksImVtYWlsX3ZlcmlmaWVkIjpmYWxzZSwianRpIjoiMjI1ZDRhNDItM2Y3ZC00Y2I2LTkxMmMtOGNkYzM0Y2JiNTk2Iiwic3ViIjoiYTZhNzhkOTEtNTQ5NC00Y2UzLTk1NTUtODc4YTE4NWNhNGI5IiwidHlwIjoiSUQifQ== ``` +### Standard OpenID Connect Scopes and Claims + +The OpenID Connect Core 1.0 profile specifies the following standard scopes and claims: + +| Scope | Claim(s) | +| --------- | -------------------------------------------------------------------------------------------------------------------------------------- | +| `openid` | `sub`. In an ID Token, `iss`, `aud`, `exp`, `iat` will also be provided. | +| `profile` | Typically claims like `name`, `family_name`, `given_name`, `middle_name`, `preferred_username`, `nickname`, `picture` and `updated_at` | +| `email` | `email` and `email_verified` (_boolean_) indicating if the email address has been verified by the user | + +_Note that the `openid` scope is a mandatory designator scope._ + +#### Description of the standard claims + +| Claim | Type | Description | +| -------------------- | -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `iss` | URI | The Uniform Resource Identifier uniquely identifying the OpenID Connect Provider (_OP_) | +| `aud` | string / array | The intended audiences. For ID tokens, the identity token is one or more clients. For Access tokens, the audience is typically one or more Resource Servers | +| `nbf` | integer | _Not before_ timestamp in Unix Epoch time\*. May be omitted or set to 0 to indicate that the audience can disregard the claim | +| `exp` | integer | _Expires_ timestamp in Unix Epoch time\* | +| `name` | string | Preferred display name. Ex. `John Doe` | +| `family_name` | string | Last name. Ex. `Doe` | +| `given_name` | string | First name. Ex. `John` | +| `middle_name` | string | Middle name. Ex. `Donald` | +| `nickname` | string | Nick name. Ex. `Johnny` | +| `preferred_username` | string | Preferred user name. Ex. `johdoe` | +| `picture` | base64 | A Base-64 encoded picture (typically PNG or JPEG) of the subject | +| `updated_at` | integer | A timestamp in Unix Epoch time\* | + +`*` (Seconds since January 1st 1970). + +### Passing the Access token as a normal Bearer token + +To pass the access token to the upstream server as a normal Bearer token, configure the plugin as follows: + +| Key | Value | +| -------------------------------------- | --------------- | +| `config.access_token_header_name` | `Authorization` | +| `config.access_token_as_bearer` | `yes` | ## Development @@ -166,7 +242,7 @@ X-Id-Token: eyJuYmYiOjAsImF6cCI6ImtvbmciLCJpYXQiOjE1NDg1MTA3NjksImlzcyI6Imh0dHA6 To run unit tests, run the following command: -``` +```shell ./bin/run-unit-tests.sh ``` @@ -176,13 +252,13 @@ This may take a while for the first run, as the docker image will need to be bui To build the integration environment (Kong with the oidc plugin enabled, and Keycloak as the OIDC Provider), you will first need to find your computer's IP, and assign that to the environment variable `IP`. Finally, you will run the `./bin/build-env.sh` command. Here's an example: -``` +```shell export IP=192.168.0.1 ./bin/build-env.sh ``` To tear the environment down: -``` +```shell ./bin/teardown-env.sh ``` diff --git a/bin/build-env.sh b/bin/build-env.sh index 903a7d4a..42b52057 100755 --- a/bin/build-env.sh +++ b/bin/build-env.sh @@ -12,10 +12,11 @@ # Tear down environment if it is running docker-compose -f ${INTEGRATION_PATH}/docker-compose.yml down docker build --build-arg KONG_BASE_TAG=${KONG_BASE_TAG} -t nokia/kong-oidc -f ${INTEGRATION_PATH}/Dockerfile . - docker-compose -f ${INTEGRATION_PATH}/docker-compose.yml up -d kong-db + docker-compose -f ${INTEGRATION_PATH}/docker-compose.yml up -d kong-db kong-session-store ) _wait_for_listener localhost:${KONG_DB_PORT} + _wait_for_listener localhost:${KONG_SESSION_STORE_PORT} (set -x docker-compose -f ${INTEGRATION_PATH}/docker-compose.yml run --rm kong kong migrations bootstrap diff --git a/bin/my-ip.sh b/bin/my-ip.sh new file mode 100644 index 00000000..8eeda110 --- /dev/null +++ b/bin/my-ip.sh @@ -0,0 +1,9 @@ +#!/bin/sh +ADAPTER="" +if [ -z "$*" ]; then + ADAPTER="wifi0" +else + ADAPTER=$1 +fi + +ip addr show $ADAPTER | grep "inet\b" | awk '{print $2}' | cut -d/ -f1 diff --git a/ci/setup.sh b/ci/setup.sh index 31c159e1..e89967bb 100644 --- a/ci/setup.sh +++ b/ci/setup.sh @@ -3,7 +3,7 @@ set -e export LUA_VERSION=${LUA_VERSION:-5.1} export KONG_VERSION=${KONG_VERSION:-0.13.1-0} -export LUA_RESTY_OPENIDC_VERSION=${LUA_RESTY_OPENIDC_VERSION:-1.6.1-1} +export LUA_RESTY_OPENIDC_VERSION=${LUA_RESTY_OPENIDC_VERSION:-1.7.1-1} pip install hererocks hererocks lua_install -r^ --lua=${LUA_VERSION} @@ -14,4 +14,3 @@ luarocks install lua-resty-openidc ${LUA_RESTY_OPENIDC_VERSION} luarocks install lua-cjson luarocks install luaunit luarocks install luacov - diff --git a/kong-oidc-1.1.0-0.rockspec b/kong-plugin-oidc-1.4.0-1.rockspec similarity index 86% rename from kong-oidc-1.1.0-0.rockspec rename to kong-plugin-oidc-1.4.0-1.rockspec index f363bb32..85321578 100644 --- a/kong-oidc-1.1.0-0.rockspec +++ b/kong-plugin-oidc-1.4.0-1.rockspec @@ -1,9 +1,9 @@ -package = "kong-oidc" -version = "1.1.0-0" +package = "kong-oidc-v3" +version = "1.4.0-1" source = { - url = "git://github.com/nokia/kong-oidc", - tag = "v1.1.0", - dir = "kong-oidc" + url = "git://github.com/ricristian/kong-oidc-v3", + tag = "master", + dir = "kong-oidc-v3" } description = { summary = "A Kong plugin for implementing the OpenID Connect Relying Party (RP) functionality", @@ -18,11 +18,11 @@ description = { It can be used as a reverse proxy terminating OAuth/OpenID Connect in front of an origin server so that the origin server/services can be protected with the relevant standards without implementing those on the server itself. ]], - homepage = "https://github.com/nokia/kong-oidc", + homepage = "git://github.com/ricristian/kong-oidc-v3", license = "Apache 2.0" } dependencies = { - "lua-resty-openidc ~> 1.6.1-1" + "lua-resty-openidc ~> 1.8.0-1" } build = { type = "builtin", diff --git a/kong/plugins/oidc/handler.lua b/kong/plugins/oidc/handler.lua index 07f05af5..abd6710b 100644 --- a/kong/plugins/oidc/handler.lua +++ b/kong/plugins/oidc/handler.lua @@ -1,20 +1,23 @@ -local BasePlugin = require "kong.plugins.base_plugin" -local OidcHandler = BasePlugin:extend() +local OidcHandler = { + VERSION = "1.3.0", + PRIORITY = 1000, +} local utils = require("kong.plugins.oidc.utils") local filter = require("kong.plugins.oidc.filter") local session = require("kong.plugins.oidc.session") -OidcHandler.PRIORITY = 1000 - - -function OidcHandler:new() - OidcHandler.super.new(self, "oidc") -end function OidcHandler:access(config) - OidcHandler.super.access(self) local oidcConfig = utils.get_options(config, ngx) + -- partial support for plugin chaining: allow skipping requests, where higher priority + -- plugin has already set the credentials. The 'config.anomyous' approach to define + -- "and/or" relationship between auth plugins is not utilized + if oidcConfig.skip_already_auth_requests and kong.client.get_credential() then + ngx.log(ngx.DEBUG, "OidcHandler ignoring already auth request: " .. ngx.var.request_uri) + return + end + if filter.shouldProcessRequest(oidcConfig) then session.configure(config) handle(oidcConfig) @@ -27,24 +30,56 @@ end function handle(oidcConfig) local response + + if oidcConfig.bearer_jwt_auth_enable then + response = verify_bearer_jwt(oidcConfig) + if response then + utils.setCredentials(response) + utils.injectGroups(response, oidcConfig.groups_claim) + utils.injectHeaders(oidcConfig.header_names, oidcConfig.header_claims, { response }) + if not oidcConfig.disable_userinfo_header then + utils.injectUser(response, oidcConfig.userinfo_header_name) + end + return + end + end + if oidcConfig.introspection_endpoint then response = introspect(oidcConfig) if response then - utils.injectUser(response) + utils.setCredentials(response) + utils.injectGroups(response, oidcConfig.groups_claim) + utils.injectHeaders(oidcConfig.header_names, oidcConfig.header_claims, { response }) + if not oidcConfig.disable_userinfo_header then + utils.injectUser(response, oidcConfig.userinfo_header_name) + end end end if response == nil then response = make_oidc(oidcConfig) if response then - if (response.user) then - utils.injectUser(response.user) + if response.user or response.id_token then + -- is there any scenario where lua-resty-openidc would not provide id_token? + utils.setCredentials(response.user or response.id_token) end - if (response.access_token) then - utils.injectAccessToken(response.access_token) + if response.user and response.user[oidcConfig.groups_claim] ~= nil then + utils.injectGroups(response.user, oidcConfig.groups_claim) + elseif response.id_token then + utils.injectGroups(response.id_token, oidcConfig.groups_claim) end - if (response.id_token) then - utils.injectIDToken(response.id_token) + utils.injectHeaders(oidcConfig.header_names, oidcConfig.header_claims, { response.user, response.id_token }) + if (not oidcConfig.disable_userinfo_header + and response.user) then + utils.injectUser(response.user, oidcConfig.userinfo_header_name) + end + if (not oidcConfig.disable_access_token_header + and response.access_token) then + utils.injectAccessToken(response.access_token, oidcConfig.access_token_header_name, oidcConfig.access_token_as_bearer) + end + if (not oidcConfig.disable_id_token_header + and response.id_token) then + utils.injectIDToken(response.id_token, oidcConfig.id_token_header_name) end end end @@ -52,32 +87,105 @@ end function make_oidc(oidcConfig) ngx.log(ngx.DEBUG, "OidcHandler calling authenticate, requested path: " .. ngx.var.request_uri) - local res, err = require("resty.openidc").authenticate(oidcConfig) + local unauth_action = oidcConfig.unauth_action + if unauth_action ~= "auth" then + -- constant for resty.oidc library + unauth_action = "deny" + end + local res, err = require("resty.openidc").authenticate(oidcConfig, ngx.var.request_uri, unauth_action) + if err then - if oidcConfig.recovery_page_path then - ngx.log(ngx.DEBUG, "Entering recovery page: " .. oidcConfig.recovery_page_path) - ngx.redirect(oidcConfig.recovery_page_path) + if err == 'unauthorized request' then + return kong.response.error(ngx.HTTP_UNAUTHORIZED) + else + if oidcConfig.recovery_page_path then + ngx.log(ngx.DEBUG, "Redirecting to recovery page: " .. oidcConfig.recovery_page_path) + ngx.redirect(oidcConfig.recovery_page_path) + end + return kong.response.error(ngx.HTTP_INTERNAL_SERVER_ERROR) end - utils.exit(500, err, ngx.HTTP_INTERNAL_SERVER_ERROR) end return res end function introspect(oidcConfig) if utils.has_bearer_access_token() or oidcConfig.bearer_only == "yes" then - local res, err = require("resty.openidc").introspect(oidcConfig) + local res, err + if oidcConfig.use_jwks == "yes" then + res, err = require("resty.openidc").bearer_jwt_verify(oidcConfig) + else + res, err = require("resty.openidc").introspect(oidcConfig) + end if err then if oidcConfig.bearer_only == "yes" then ngx.header["WWW-Authenticate"] = 'Bearer realm="' .. oidcConfig.realm .. '",error="' .. err .. '"' - utils.exit(ngx.HTTP_UNAUTHORIZED, err, ngx.HTTP_UNAUTHORIZED) + return kong.response.error(ngx.HTTP_UNAUTHORIZED) end return nil end + if oidcConfig.validate_scope == "yes" then + local validScope = false + if res.scope then + for scope in res.scope:gmatch("([^ ]+)") do + if scope == oidcConfig.scope then + validScope = true + break + end + end + end + if not validScope then + kong.log.err("Scope validation failed") + return kong.response.error(ngx.HTTP_FORBIDDEN) + end + end ngx.log(ngx.DEBUG, "OidcHandler introspect succeeded, requested path: " .. ngx.var.request_uri) return res end return nil end +function verify_bearer_jwt(oidcConfig) + if not utils.has_bearer_access_token() then + return nil + end + -- setup controlled configuration for bearer_jwt_verify + local opts = { + accept_none_alg = false, + accept_unsupported_alg = false, + token_signing_alg_values_expected = oidcConfig.bearer_jwt_auth_signing_algs, + discovery = oidcConfig.discovery, + timeout = oidcConfig.timeout, + ssl_verify = oidcConfig.ssl_verify + } + + local discovery_doc, err = require("resty.openidc").get_discovery_doc(opts) + if err then + kong.log.err('Discovery document retrieval for Bearer JWT verify failed') + return nil + end + + local allowed_auds = oidcConfig.bearer_jwt_auth_allowed_auds or oidcConfig.client_id + + local jwt_validators = require "resty.jwt-validators" + jwt_validators.set_system_leeway(120) + local claim_spec = { + -- mandatory for id token: iss, sub, aud, exp, iat + iss = jwt_validators.equals(discovery_doc.issuer), + sub = jwt_validators.required(), + aud = function(val) return utils.has_common_item(val, allowed_auds) end, + exp = jwt_validators.is_not_expired(), + iat = jwt_validators.required(), + -- optional validations + nbf = jwt_validators.opt_is_not_before(), + } + + local json, err, token = require("resty.openidc").bearer_jwt_verify(opts, claim_spec) + if err then + kong.log.err('Bearer JWT verify failed: ' .. err) + return nil + end + + return json +end return OidcHandler diff --git a/kong/plugins/oidc/schema.lua b/kong/plugins/oidc/schema.lua index ffb55b37..976720be 100644 --- a/kong/plugins/oidc/schema.lua +++ b/kong/plugins/oidc/schema.lua @@ -1,23 +1,313 @@ +local typedefs = require "kong.db.schema.typedefs" + return { - no_consumer = true, + name = "kong-oidc", fields = { - client_id = { type = "string", required = true }, - client_secret = { type = "string", required = true }, - discovery = { type = "string", required = true, default = "https://.well-known/openid-configuration" }, - introspection_endpoint = { type = "string", required = false }, - timeout = { type = "number", required = false }, - introspection_endpoint_auth_method = { type = "string", required = false }, - bearer_only = { type = "string", required = true, default = "no" }, - realm = { type = "string", required = true, default = "kong" }, - redirect_uri_path = { type = "string" }, - scope = { type = "string", required = true, default = "openid" }, - response_type = { type = "string", required = true, default = "code" }, - ssl_verify = { type = "string", required = true, default = "no" }, - token_endpoint_auth_method = { type = "string", required = true, default = "client_secret_post" }, - session_secret = { type = "string", required = false }, - recovery_page_path = { type = "string" }, - logout_path = { type = "string", required = false, default = '/logout' }, - redirect_after_logout_uri = { type = "string", required = false, default = '/' }, - filters = { type = "string" } + { + -- this plugin will only be applied to Services or Routes + consumer = typedefs.no_consumer + }, + { + -- this plugin will only run within Nginx HTTP module + protocols = typedefs.protocols_http + }, + { + config = { + type = "record", + fields = { + { + client_id = { + type = "string", + required = true + } + }, + { + client_secret = { + type = "string", + required = true + } + }, + { + discovery = { + type = "string", + required = true, + default = "https://.well-known/openid-configuration" + } + }, + { + introspection_endpoint = { + type = "string", + required = false + } + }, + { + introspection_endpoint_auth_method = { + type = "string", + required = false + } + }, + { + introspection_cache_ignore = { + type = "string", + required = true, + default = "no" + } + }, + { + timeout = { + type = "number", + required = false + } + }, + { + bearer_only = { + type = "string", + required = true, + default = "no" + } + }, + { + realm = { + type = "string", + required = true, + default = "kong" + } + }, + { + redirect_uri = { + type = "string" + } + }, + { + scope = { + type = "string", + required = true, + default = "openid" + } + }, + { + validate_scope = { + type = "string", + required = true, + default = "no" + } + }, + { + response_type = { + type = "string", + required = true, + default = "code" + } + }, + { + ssl_verify = { + type = "string", + required = true, + default = "no" + } + }, + { + use_jwks = { + type = "string", + required = true, + default = "no" + } + }, + { + token_endpoint_auth_method = { + type = "string", + required = true, + default = "client_secret_post" + } + }, + { + session_secret = { + type = "string", + required = false + } + }, + { + recovery_page_path = { + type = "string" + } + }, + { + logout_path = { + type = "string", + required = false, + default = "/logout" + } + }, + { + redirect_after_logout_uri = { + type = "string", + required = false, + default = "/" + } + }, + { + redirect_after_logout_with_id_token_hint = { + type = "string", + required = false, + default = "no" + } + }, + { + post_logout_redirect_uri = { + type = "string", + required = false + } + }, + { + unauth_action = { + type = "string", + required = false, + default = "auth" + } + }, + { + filters = { + type = "string" + } + }, + { + ignore_auth_filters = { + type = "string", + required = false + } + }, + { + userinfo_header_name = { + type = "string", + required = false, + default = "X-USERINFO" + } + }, + { + id_token_header_name = { + type = "string", + required = false, + default = "X-ID-Token" + } + }, + { + access_token_header_name = { + type = "string", + required = false, + default = "X-Access-Token" + } + }, + { + access_token_as_bearer = { + type = "string", + required = false, + default = "no" + } + }, + { + disable_userinfo_header = { + type = "string", + required = false, + default = "no" + } + }, + { + disable_id_token_header = { + type = "string", + required = false, + default = "no" + } + }, + { + disable_access_token_header = { + type = "string", + required = false, + default = "no" + } + }, + { + revoke_tokens_on_logout = { + type = "string", + required = false, + default = "no" + } + }, + { + groups_claim = { + type = "string", + required = false, + default = "groups" + } + }, + { + skip_already_auth_requests = { + type = "string", + required = false, + default = "no" + } + }, + { + bearer_jwt_auth_enable = { + type = "string", + required = false, + default = "no" + } + }, + { + bearer_jwt_auth_allowed_auds = { + type = "array", + required = false, + elements = { + type = "string" + }, + } + }, + { + bearer_jwt_auth_signing_algs = { + type = "array", + required = true, + elements = { + type = "string" + }, + default = { + "RS256" + } + } + }, + { + header_names = { + type = "array", + required = true, + elements = { + type = "string" + }, + default = {} + } + }, + { + header_claims = { + type = "array", + required = true, + elements = { + type = "string" + }, + default = {} + } + }, + { + http_proxy = { + type = "string", + required = false + } + }, + { + https_proxy = { + type = "string", + required = false + } + } + } + } + } } } diff --git a/kong/plugins/oidc/session.lua b/kong/plugins/oidc/session.lua index 18875546..0dcc50b6 100644 --- a/kong/plugins/oidc/session.lua +++ b/kong/plugins/oidc/session.lua @@ -1,12 +1,11 @@ -local utils = require("kong.plugins.oidc.utils") - local M = {} function M.configure(config) if config.session_secret then local decoded_session_secret = ngx.decode_base64(config.session_secret) if not decoded_session_secret then - utils.exit(500, "invalid OIDC plugin configuration, session secret could not be decoded", ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR)) + kong.log.err("Invalid plugin configuration, session secret could not be decoded") + return kong.response.error(ngx.HTTP_INTERNAL_SERVER_ERROR) end ngx.var.session_secret = decoded_session_secret end diff --git a/kong/plugins/oidc/utils.lua b/kong/plugins/oidc/utils.lua index 3686bbf6..cac95b13 100644 --- a/kong/plugins/oidc/utils.lua +++ b/kong/plugins/oidc/utils.lua @@ -1,10 +1,11 @@ local cjson = require("cjson") +local constants = require "kong.constants" local M = {} local function parseFilters(csvFilters) local filters = {} - if (not (csvFilters == nil)) then + if (not (csvFilters == nil)) and (not (csvFilters == ",")) then for pattern in string.gmatch(csvFilters, "[^,]+") do table.insert(filters, pattern) end @@ -12,7 +13,11 @@ local function parseFilters(csvFilters) return filters end -function M.get_redirect_uri_path(ngx) +local function formatAsBearerToken(token) + return "Bearer " .. token +end + +function M.get_redirect_uri(ngx) local function drop_query() local uri = ngx.var.request_uri local x = uri:find("?") @@ -45,44 +50,147 @@ function M.get_options(config, ngx) client_secret = config.client_secret, discovery = config.discovery, introspection_endpoint = config.introspection_endpoint, - timeout = config.timeout, introspection_endpoint_auth_method = config.introspection_endpoint_auth_method, + introspection_cache_ignore = config.introspection_cache_ignore, + timeout = config.timeout, bearer_only = config.bearer_only, realm = config.realm, - redirect_uri_path = config.redirect_uri_path or M.get_redirect_uri_path(ngx), + redirect_uri = config.redirect_uri or M.get_redirect_uri(ngx), scope = config.scope, + validate_scope = config.validate_scope, response_type = config.response_type, ssl_verify = config.ssl_verify, + use_jwks = config.use_jwks, token_endpoint_auth_method = config.token_endpoint_auth_method, recovery_page_path = config.recovery_page_path, - filters = parseFilters(config.filters), + filters = parseFilters((config.filters or "") .. "," .. (config.ignore_auth_filters or "")), logout_path = config.logout_path, + revoke_tokens_on_logout = config.revoke_tokens_on_logout == "yes", redirect_after_logout_uri = config.redirect_after_logout_uri, + redirect_after_logout_with_id_token_hint = config.redirect_after_logout_with_id_token_hint == "yes", + post_logout_redirect_uri = config.post_logout_redirect_uri, + unauth_action = config.unauth_action, + userinfo_header_name = config.userinfo_header_name, + id_token_header_name = config.id_token_header_name, + access_token_header_name = config.access_token_header_name, + access_token_as_bearer = config.access_token_as_bearer == "yes", + disable_userinfo_header = config.disable_userinfo_header == "yes", + disable_id_token_header = config.disable_id_token_header == "yes", + disable_access_token_header = config.disable_access_token_header == "yes", + groups_claim = config.groups_claim, + skip_already_auth_requests = config.skip_already_auth_requests == "yes", + bearer_jwt_auth_enable = config.bearer_jwt_auth_enable == "yes", + bearer_jwt_auth_allowed_auds = config.bearer_jwt_auth_allowed_auds, + bearer_jwt_auth_signing_algs = config.bearer_jwt_auth_signing_algs, + header_names = config.header_names or {}, + header_claims = config.header_claims or {}, + proxy_opts = { + http_proxy = config.http_proxy, + https_proxy = config.https_proxy + } } end -function M.exit(httpStatusCode, message, ngxCode) - ngx.status = httpStatusCode - ngx.say(message) - ngx.exit(ngxCode) +-- Function set_consumer is derived from the following kong auth plugins: +-- https://github.com/Kong/kong/blob/3.0.0/kong/plugins/ldap-auth/access.lua +-- https://github.com/Kong/kong/blob/3.0.0/kong/plugins/oauth2/access.lua +-- Copyright 2016-2022 Kong Inc. Licensed under the Apache License, Version 2.0 +-- https://github.com/Kong/kong/blob/3.0.0/LICENSE +local function set_consumer(consumer, credential) + kong.client.authenticate(consumer, credential) + + local set_header = kong.service.request.set_header + local clear_header = kong.service.request.clear_header + + if consumer and consumer.id then + set_header(constants.HEADERS.CONSUMER_ID, consumer.id) + else + clear_header(constants.HEADERS.CONSUMER_ID) + end + + if consumer and consumer.custom_id then + set_header(constants.HEADERS.CONSUMER_CUSTOM_ID, consumer.custom_id) + else + clear_header(constants.HEADERS.CONSUMER_CUSTOM_ID) + end + + if consumer and consumer.username then + set_header(constants.HEADERS.CONSUMER_USERNAME, consumer.username) + else + clear_header(constants.HEADERS.CONSUMER_USERNAME) + end + + if credential and credential.username then + set_header(constants.HEADERS.CREDENTIAL_IDENTIFIER, credential.username) + else + clear_header(constants.HEADERS.CREDENTIAL_IDENTIFIER) + end + + if credential then + clear_header(constants.HEADERS.ANONYMOUS) + else + set_header(constants.HEADERS.ANONYMOUS, true) + end end -function M.injectAccessToken(accessToken) - ngx.req.set_header("X-Access-Token", accessToken) +function M.injectAccessToken(accessToken, headerName, bearerToken) + ngx.log(ngx.DEBUG, "Injecting " .. headerName) + local token = accessToken + if (bearerToken) then + token = formatAsBearerToken(token) + end + kong.service.request.set_header(headerName, token) end -function M.injectIDToken(idToken) +function M.injectIDToken(idToken, headerName) + ngx.log(ngx.DEBUG, "Injecting " .. headerName) local tokenStr = cjson.encode(idToken) - ngx.req.set_header("X-ID-Token", ngx.encode_base64(tokenStr)) + kong.service.request.set_header(headerName, ngx.encode_base64(tokenStr)) end -function M.injectUser(user) +function M.setCredentials(user) local tmp_user = user tmp_user.id = user.sub tmp_user.username = user.preferred_username - ngx.ctx.authenticated_credential = tmp_user + set_consumer(nil, tmp_user) +end + +function M.injectUser(user, headerName) + ngx.log(ngx.DEBUG, "Injecting " .. headerName) local userinfo = cjson.encode(user) - ngx.req.set_header("X-Userinfo", ngx.encode_base64(userinfo)) + kong.service.request.set_header(headerName, ngx.encode_base64(userinfo)) +end + +function M.injectGroups(user, claim) + if user[claim] ~= nil then + kong.ctx.shared.authenticated_groups = user[claim] + end +end + +function M.injectHeaders(header_names, header_claims, sources) + if #header_names ~= #header_claims then + kong.log.err('Different number of elements provided in header_names and header_claims. Headers will not be added.') + return + end + for i = 1, #header_names do + local header, claim + header = header_names[i] + claim = header_claims[i] + kong.service.request.clear_header(header) + for j = 1, #sources do + local source, claim_value + source = sources[j] + claim_value = source[claim] + -- Convert table to string if claim is a table + if type(claim_value) == "table" then + claim_value = table.concat(claim_value, ", ") + end + if (source and source[claim]) then + kong.service.request.set_header(header, claim_value) + break + end + end + end end function M.has_bearer_access_token() @@ -96,4 +204,27 @@ function M.has_bearer_access_token() return false end +-- verify if tables t1 and t2 have at least one common string item +-- instead of table, also string can be provided as t1 or t2 +function M.has_common_item(t1, t2) + if t1 == nil or t2 == nil then + return false + end + if type(t1) == "string" then + t1 = { t1 } + end + if type(t2) == "string" then + t2 = { t2 } + end + local i1, i2 + for _, i1 in pairs(t1) do + for _, i2 in pairs(t2) do + if type(i1) == "string" and type(i2) == "string" and i1 == i2 then + return true + end + end + end + return false +end + return M diff --git a/test/docker/integration/Dockerfile b/test/docker/integration/Dockerfile index b77e83e2..f6226135 100644 --- a/test/docker/integration/Dockerfile +++ b/test/docker/integration/Dockerfile @@ -1,17 +1,19 @@ ARG KONG_BASE_TAG FROM kong${KONG_BASE_TAG} +USER root ENV LUA_PATH /usr/local/share/lua/5.1/?.lua;/usr/local/kong-oidc/?.lua;; # For lua-cjson ENV LUA_CPATH /usr/local/lib/lua/5.1/?.so;; # Install unzip for luarocks, gcc for lua-cjson -RUN yum install -y unzip gcc +RUN apt update && apt install -y unzip gcc curl RUN luarocks install luacov RUN luarocks install luaunit RUN luarocks install lua-cjson +RUN luarocks install luaossl OPENSSL_DIR=/usr/local/kong CRYPTO_DIR=/usr/local/kong # Change openidc version when version in rockspec changes -RUN luarocks install lua-resty-openidc 1.6.0 +RUN luarocks install lua-resty-openidc 1.7.5-1 -COPY . /usr/local/kong-oidc \ No newline at end of file +COPY . /usr/local/kong-oidc diff --git a/test/docker/integration/docker-compose.yml b/test/docker/integration/docker-compose.yml index 6e374080..01c192ed 100644 --- a/test/docker/integration/docker-compose.yml +++ b/test/docker/integration/docker-compose.yml @@ -18,6 +18,11 @@ services: KEYCLOAK_USER: ${KEYCLOAK_USER} KEYCLOAK_PASSWORD: ${KEYCLOAK_PW} + kong-session-store: + image: redis + ports: + - 6379:6379 + kong: image: nokia/kong-oidc${KONG_TAG} ports: @@ -26,6 +31,7 @@ services: - 8001:8001 - 8444:8444 environment: + KONG_NGINX_PROXY_INCLUDE: /usr/local/kong-oidc/test/docker/integration/nginx-redis.kong.conf KONG_DATABASE: postgres KONG_PG_HOST: kong-db KONG_PG_DATABASE: ${KONG_DB_NAME} @@ -39,4 +45,5 @@ services: KONG_ADMIN_ERROR_LOG: /dev/stderr KONG_PLUGINS: oidc depends_on: - - kong-db \ No newline at end of file + - kong-db + - kong-session-store diff --git a/test/docker/integration/keycloak_client.py b/test/docker/integration/keycloak_client.py new file mode 100644 index 00000000..40a0ff83 --- /dev/null +++ b/test/docker/integration/keycloak_client.py @@ -0,0 +1,48 @@ +import requests + +class KeycloakClient: + def __init__(self, url, realm, username, password): + self._endpoint = url + self._realm = realm + self._session = requests.session() + self._username = username + self._password = password + + def discover(self, config_type = "openid-configuration"): + res = self._session.get("{}/auth/realms/{}/.well-known/{}".format(self._endpoint, self._realm, config_type)) + res.raise_for_status() + return res.json() + + def create_client(self, name, secret): + url = "{}/auth/admin/realms/master/clients".format(self._endpoint) + payload = { + "clientId": name, + "secret": secret, + "redirectUris": ["*"], + } + + headers = self.get_auth_header() + res = self._session.post(url, json=payload, headers=headers) + + if res.status_code not in [201, 409]: + raise Exception("Cannot Keycloak create client") + + def get_auth_header(self): + return { + "Authorization": f'Bearer {self.get_token("admin-cli")}' + } + + def get_token(self, client_id): + url = "{}/auth/realms/{}/protocol/openid-connect/token".format(self._endpoint, self._realm) + + payload = f'client_id={client_id}&grant_type=password' + \ + f'&username={self._username}&password={self._password}' + + headers = { + "Content-Type": "application/x-www-form-urlencoded" + } + + res = self._session.post(url, data=payload, headers=headers) + res.raise_for_status() + + return res.json()["access_token"] diff --git a/test/docker/integration/kong_client.py b/test/docker/integration/kong_client.py new file mode 100644 index 00000000..48748c49 --- /dev/null +++ b/test/docker/integration/kong_client.py @@ -0,0 +1,59 @@ +import requests + +class KongClient: + def __init__(self, url): + self._endpoint = url + self._session = requests.session() + + def create_service(self, name, upstream_url): + url = "{}/services".format(self._endpoint) + payload = { + "name": name, + "url": upstream_url, + } + res = self._session.post(url, json=payload) + res.raise_for_status() + return res.json() + + def create_route(self, service_name, paths): + url = "{}/services/{}/routes".format(self._endpoint, service_name) + payload = { + "paths": paths, + } + res = self._session.post(url, json=payload) + res.raise_for_status() + return res.json() + + def create_plugin(self, plugin_name, service_name, config): + url = "{}/services/{}/plugins".format(self._endpoint, service_name) + payload = { + "name": plugin_name, + "config": config, + } + res = self._session.post(url, json=payload) + try: + res.raise_for_status() + except Exception as e: + print(res.text) + raise e + return res.json() + + def delete_service(self, name): + try: + routes = self.get_routes(name) + for route in routes: + self.delete_route(route) + except requests.exceptions.HTTPError: + pass + url = "{}/services/{}".format(self._endpoint, name) + self._session.delete(url).raise_for_status() + + def delete_route(self, route_id): + url = "{}/routes/{}".format(self._endpoint, route_id) + self._session.delete(url).raise_for_status() + + def get_routes(self, service_name): + url = "{}/services/{}/routes".format(self._endpoint, service_name) + res = self._session.get(url) + res.raise_for_status() + return map(lambda x: x['id'], res.json()['data']) diff --git a/test/docker/integration/nginx-redis.kong.conf b/test/docker/integration/nginx-redis.kong.conf new file mode 100644 index 00000000..7ff7299e --- /dev/null +++ b/test/docker/integration/nginx-redis.kong.conf @@ -0,0 +1,4 @@ +set $session_storage redis; +set $session_redis_prefix sessions; +set $session_redis_host kong-session-store; +set $session_redis_port 6379; diff --git a/test/docker/integration/setup.py b/test/docker/integration/setup.py index 1fec06c5..23237ce2 100644 --- a/test/docker/integration/setup.py +++ b/test/docker/integration/setup.py @@ -2,15 +2,18 @@ import os import requests - from collections import namedtuple +from keycloak_client import KeycloakClient +from kong_client import KongClient + local_ip = os.getenv("IP", default="") host = "localhost" env_file_path = ".env" Config = namedtuple("Config", [ "keycloak_endpoint", + "keycloak_realm", "keycloak_admin", "keycloak_password", "client_id", @@ -42,6 +45,7 @@ def get_config(env): return Config( keycloak_endpoint = keycloak_url, + keycloak_realm = "master", keycloak_admin = env["KEYCLOAK_USER"], keycloak_password = env["KEYCLOAK_PW"], client_id = "kong", @@ -52,107 +56,6 @@ def get_config(env): kong_endpoint = "http://{}:{}".format(host, env["KONG_HTTP_ADMIN_PORT"]) ) - -class KeycloakClient: - def __init__(self, url, username, password): - self._endpoint = url - self._session = requests.session() - self._username = username - self._password = password - - def create_client(self, name, secret): - url = "{}/auth/admin/realms/master/clients".format(self._endpoint) - payload = { - "clientId": name, - "secret": secret, - "redirectUris": ["*"], - } - - headers = self.get_auth_header() - res = self._session.post(url, json=payload, headers=headers) - - if res.status_code not in [201, 409]: - raise Exception("Cannot Keycloak create client") - - def get_auth_header(self): - return { - "Authorization": "Bearer {}".format(self.get_admin_token()) - } - - def get_admin_token(self): - url = "{}/auth/realms/master/protocol/openid-connect/token".format(self._endpoint) - - payload = "client_id=admin-cli&grant_type=password" + \ - "&username={}&password={}".format(self._username, self._password) - - headers = { - "Content-Type": "application/x-www-form-urlencoded" - } - - res = self._session.post(url, data=payload, headers=headers) - res.raise_for_status() - - return res.json()["access_token"] - - -class KongClient: - def __init__(self, url): - self._endpoint = url - self._session = requests.session() - - def create_service(self, name, upstream_url): - url = "{}/services".format(self._endpoint) - payload = { - "name": name, - "url": upstream_url, - } - res = self._session.post(url, json=payload) - res.raise_for_status() - return res.json() - - def create_route(self, service_name, paths): - url = "{}/services/{}/routes".format(self._endpoint, service_name) - payload = { - "paths": paths, - } - res = self._session.post(url, json=payload) - res.raise_for_status() - return res.json() - - def create_plugin(self, plugin_name, service_name, config): - url = "{}/services/{}/plugins".format(self._endpoint, service_name) - payload = { - "name": plugin_name, - "config": config, - } - res = self._session.post(url, json=payload) - try: - res.raise_for_status() - except Exception as e: - print(res.text) - raise e - return res.json() - - def delete_service(self, name): - try: - routes = self.get_routes(name) - for route in routes: - self.delete_route(route) - except requests.exceptions.HTTPError: - pass - url = "{}/services/{}".format(self._endpoint, name) - self._session.delete(url).raise_for_status() - - def delete_route(self, route_id): - url = "{}/routes/{}".format(self._endpoint, route_id) - self._session.delete(url).raise_for_status() - - def get_routes(self, service_name): - url = "{}/services/{}/routes".format(self._endpoint, service_name) - res = self._session.get(url) - res.raise_for_status() - return map(lambda x: x['id'], res.json()['data']) - if __name__ == '__main__': validate_ip_set() @@ -162,6 +65,7 @@ def get_routes(self, service_name): print("Creating Keycloak HTTP Client at {}".format(config.keycloak_endpoint)) kc_client = KeycloakClient(config.keycloak_endpoint, + config.keycloak_realm, config.keycloak_admin, config.keycloak_password) diff --git a/test/docker/unit/Dockerfile b/test/docker/unit/Dockerfile index 53eedbfd..ed414800 100644 --- a/test/docker/unit/Dockerfile +++ b/test/docker/unit/Dockerfile @@ -1,18 +1,20 @@ ARG KONG_BASE_TAG FROM kong${KONG_BASE_TAG} +USER root ENV LUA_PATH /usr/local/share/lua/5.1/?.lua;/usr/local/kong-oidc/?.lua # For lua-cjson ENV LUA_CPATH /usr/local/lib/lua/5.1/?.so # Install unzip for luarocks, gcc for lua-cjson -RUN yum install -y unzip gcc +RUN apt update && apt install -y unzip gcc curl RUN luarocks install luacov RUN luarocks install luaunit RUN luarocks install lua-cjson +RUN luarocks install luaossl OPENSSL_DIR=/usr/local/kong CRYPTO_DIR=/usr/local/kong # Change openidc version when version in rockspec changes -RUN luarocks install lua-resty-openidc 1.6.1-1 +RUN luarocks install lua-resty-openidc 1.7.5-1 WORKDIR /usr/local/kong-oidc diff --git a/test/unit/mockable_case.lua b/test/unit/mockable_case.lua index 66aa367c..613b7799 100644 --- a/test/unit/mockable_case.lua +++ b/test/unit/mockable_case.lua @@ -9,6 +9,8 @@ function MockableCase:setUp() DEBUG = "debug", ERR = "error", HTTP_UNAUTHORIZED = 401, + HTTP_FORBIDDEN = 403, + HTTP_INTERNAL_SERVER_ERROR = 500, ctx = {}, header = {}, var = {request_uri = "/"}, @@ -31,6 +33,34 @@ function MockableCase:setUp() self.ngx = _G.ngx _G.ngx = self.mocked_ngx + self.mocked_kong = { + client = { + authenticate = function(consumer, credential) + ngx.ctx.authenticated_consumer = consumer + ngx.ctx.authenticated_credential = credential + end + }, + service = { + request = { + clear_header = function(...) end, + set_header = function(...) end + } + }, + response = { + error = function(status) + ngx.status = status + end + }, + log = { + err = function(...) end + }, + ctx = { + shared = {} + } + } + self.kong = _G.kong + _G.kong = self.mocked_kong + self.resty = package.loaded.resty package.loaded["resty.http"] = nil package.preload["resty.http"] = function() @@ -50,6 +80,7 @@ end function MockableCase:tearDown() MockableCase.super:tearDown() _G.ngx = self.ngx + _G.kong = self.kong package.loaded.resty = self.resty package.loaded.cjson = self.cjson end diff --git a/test/unit/run.sh b/test/unit/run.sh index a54ebf63..dc803181 100755 --- a/test/unit/run.sh +++ b/test/unit/run.sh @@ -1,9 +1,13 @@ #!/bin/bash set -e +rm -f luacov.stats.out + # Run all test_*.lua files in test/unit for f in test/unit/test_*.lua; do (set -x lua -lluacov ${f} -o TAP --failure ) -done \ No newline at end of file +done +luacov +cat luacov.report.out diff --git a/test/unit/test_already_auth.lua b/test/unit/test_already_auth.lua new file mode 100644 index 00000000..86403dd4 --- /dev/null +++ b/test/unit/test_already_auth.lua @@ -0,0 +1,44 @@ +local lu = require("luaunit") +TestHandler = require("test.unit.mockable_case"):extend() + + +function TestHandler:setUp() + TestHandler.super:setUp() + + package.loaded["resty.openidc"] = nil + self.module_resty = { openidc = {} } + package.preload["resty.openidc"] = function() + return self.module_resty.openidc + end + + self.handler = require("kong.plugins.oidc.handler")() +end + +function TestHandler:tearDown() + TestHandler.super:tearDown() +end + +function TestHandler:test_skip_already_auth_has_cred() + kong.client.get_credential = function() return { consumer_id = "user" } end + local called_authenticate + self.module_resty.openidc.authenticate = function(opts) + called_authenticate = true + return nil, "error" + end + self.handler:access({ skip_already_auth_requests = "yes" }) + lu.assertNil(called_authenticate) +end + +function TestHandler:test_skip_already_auth_has_no_cred() + kong.client.get_credential = function() return nil end + local called_authenticate + self.module_resty.openidc.authenticate = function(opts) + called_authenticate = true + return nil, "error" + end + self.handler:access({ skip_already_auth_requests = "yes" }) + lu.assertTrue(called_authenticate) +end + + +lu.run() diff --git a/test/unit/test_bearer_jwt_auth.lua b/test/unit/test_bearer_jwt_auth.lua new file mode 100644 index 00000000..edd7a37c --- /dev/null +++ b/test/unit/test_bearer_jwt_auth.lua @@ -0,0 +1,68 @@ +local lu = require("luaunit") +TestHandler = require("test.unit.mockable_case"):extend() + + +function TestHandler:setUp() + TestHandler.super:setUp() + + package.loaded["resty.openidc"] = nil + self.module_resty = { openidc = {} } + package.preload["resty.openidc"] = function() + return self.module_resty.openidc + end + + self.handler = require("kong.plugins.oidc.handler")() +end + +function TestHandler:tearDown() + TestHandler.super:tearDown() +end + +function TestHandler:test_bearer_jwt_auth_success() + ngx.req.get_headers = function() return {Authorization = "Bearer xxx"} end + ngx.encode_base64 = function(x) return "eyJzdWIiOiJzdWIifQ==" end + + self.module_resty.openidc.get_discovery_doc = function(opts) + return { issuer = "https://oidc" } + end + + self.module_resty.openidc.bearer_jwt_verify = function(opts) + token = { + iss = "https://oidc", + sub = "sub111", + aud = "aud222", + groups = { "users" } + } + return token, nil, "xxx" + end + + self.handler:access({ + bearer_jwt_auth_enable = "yes", + client_id = "aud222", + groups_claim = "groups", + userinfo_header_name = "x-userinfo" + }) + lu.assertEquals(ngx.ctx.authenticated_credential.id, "sub111") + lu.assertEquals(kong.ctx.shared.authenticated_groups, { "users" }) +end + +function TestHandler:test_bearer_jwt_auth_fail() + ngx.req.get_headers = function() return {Authorization = "Bearer xxx"} end + local called_authenticate + self.module_resty.openidc.get_discovery_doc = function(opts) + return { issuer = "https://oidc" } + end + + self.module_resty.openidc.bearer_jwt_verify = function(opts) + return nil, "JWT expired" + end + + self.module_resty.openidc.authenticate = function(opts) + called_authenticate = true + return nil, "error" + end + self.handler:access({bearer_jwt_auth_enable = "yes", client_id = "aud222"}) + lu.assertTrue(called_authenticate) +end + +lu.run() diff --git a/test/unit/test_filters_advanced.lua b/test/unit/test_filters_advanced.lua index 28ff2a4e..1e2806fe 100644 --- a/test/unit/test_filters_advanced.lua +++ b/test/unit/test_filters_advanced.lua @@ -18,7 +18,13 @@ end local config = { - filters = { "^/auth$","^/auth[^%w_%-%.~]","^/arc$","^/arc[^%w_%-%.~]","^/projects/%d+/zeppelin[^%w_%-%.~]","^/projects/%d+/zeppelin$"} + filters = { + "^/auth$", + "^/auth[^%w_%-%.~]", + "^/arc$","^/arc[^%w_%-%.~]", + "^/projects/%d+/zeppelin[^%w_%-%.~]", + "^/projects/%d+/zeppelin$" + } } function TestFilter:testIgnoreRequestWhenUriIsAuth() diff --git a/test/unit/test_handler_mocking_openidc.lua b/test/unit/test_handler_mocking_openidc.lua index 1898d85d..47ae6ec5 100644 --- a/test/unit/test_handler_mocking_openidc.lua +++ b/test/unit/test_handler_mocking_openidc.lua @@ -22,10 +22,10 @@ end function TestHandler:test_authenticate_ok_no_userinfo() self.module_resty.openidc.authenticate = function(opts) - return {}, false + return { id_token = { sub = "sub"}}, false end - self.handler:access({}) + self.handler:access({disable_id_token_header = "yes"}) lu.assertTrue(self:log_contains("calling authenticate")) end @@ -36,13 +36,11 @@ function TestHandler:test_authenticate_ok_with_userinfo() ngx.encode_base64 = function(x) return "eyJzdWIiOiJzdWIifQ==" end - + local headers = {} - ngx.req.set_header = function(h, v) - headers[h] = v - end + kong.service.request.set_header = function(name, value) headers[name] = value end - self.handler:access({}) + self.handler:access({userinfo_header_name = 'X-Userinfo'}) lu.assertTrue(self:log_contains("calling authenticate")) lu.assertEquals(ngx.ctx.authenticated_credential.id, "sub") lu.assertEquals(headers['X-Userinfo'], "eyJzdWIiOiJzdWIifQ==") @@ -50,43 +48,37 @@ end function TestHandler:test_authenticate_ok_with_no_accesstoken() self.module_resty.openidc.authenticate = function(opts) - return {}, true + return {id_token = {sub = "sub"}}, true end - + local headers = {} - ngx.req.set_header = function(h, v) - headers[h] = v - end + kong.service.request.set_header = function(name, value) headers[name] = value end - self.handler:access({}) + self.handler:access({disable_id_token_header = "yes"}) lu.assertTrue(self:log_contains("calling authenticate")) lu.assertNil(headers['X-Access-Token']) end function TestHandler:test_authenticate_ok_with_accesstoken() self.module_resty.openidc.authenticate = function(opts) - return {access_token = "ACCESS_TOKEN"}, true + return {id_token = { sub = "sub" } , access_token = "ACCESS_TOKEN"}, false end - + local headers = {} - ngx.req.set_header = function(h, v) - headers[h] = v - end + kong.service.request.set_header = function(name, value) headers[name] = value end - self.handler:access({}) + self.handler:access({access_token_header_name = 'X-Access-Token', disable_id_token_header = "yes"}) lu.assertTrue(self:log_contains("calling authenticate")) lu.assertEquals(headers['X-Access-Token'], "ACCESS_TOKEN") end function TestHandler:test_authenticate_ok_with_no_idtoken() self.module_resty.openidc.authenticate = function(opts) - return {}, true + return {}, false end - + local headers = {} - ngx.req.set_header = function(h, v) - headers[h] = v - end + kong.service.request.set_header = function(name, value) headers[name] = value end self.handler:access({}) lu.assertTrue(self:log_contains("calling authenticate")) @@ -95,35 +87,45 @@ end function TestHandler:test_authenticate_ok_with_idtoken() self.module_resty.openidc.authenticate = function(opts) - return {id_token = {sub = "sub"}}, true + return {id_token = {sub = "sub"}}, false end ngx.encode_base64 = function(x) return "eyJzdWIiOiJzdWIifQ==" end - + local headers = {} - ngx.req.set_header = function(h, v) - headers[h] = v - end + kong.service.request.set_header = function(name, value) headers[name] = value end - self.handler:access({}) + self.handler:access({id_token_header_name = 'X-ID-Token'}) lu.assertTrue(self:log_contains("calling authenticate")) lu.assertEquals(headers['X-ID-Token'], "eyJzdWIiOiJzdWIifQ==") end function TestHandler:test_authenticate_nok_no_recovery() self.module_resty.openidc.authenticate = function(opts) - return {}, true + return nil, true end self.handler:access({}) lu.assertTrue(self:log_contains("calling authenticate")) end +function TestHandler:test_authenticate_nok_deny() + self.module_resty.openidc.authenticate = function(opts) + if opts.unauth_action == "deny" then + return nil, "unauthorized request" + end + return {}, true + end + + self.handler:access({unauth_action = "deny"}) + lu.assertEquals(ngx.status, ngx.HTTP_UNAUTHORIZED) +end + function TestHandler:test_authenticate_nok_with_recovery() self.module_resty.openidc.authenticate = function(opts) - return {}, true + return nil, true end self.handler:access({recovery_page_path = "x"}) @@ -151,11 +153,9 @@ function TestHandler:test_introspect_ok_with_userinfo() end local headers = {} - ngx.req.set_header = function(h, v) - headers[h] = v - end + kong.service.request.set_header = function(name, value) headers[name] = value end - self.handler:access({introspection_endpoint = "x"}) + self.handler:access({introspection_endpoint = "x", userinfo_header_name = "X-Userinfo"}) lu.assertTrue(self:log_contains("introspect succeeded")) lu.assertEquals(headers['X-Userinfo'], "eyJzdWIiOiJzdWIifQ==") end @@ -171,11 +171,9 @@ function TestHandler:test_bearer_only_with_good_token() end local headers = {} - ngx.req.set_header = function(h, v) - headers[h] = v - end + kong.service.request.set_header = function(name, value) headers[name] = value end - self.handler:access({introspection_endpoint = "x", bearer_only = "yes", realm = "kong"}) + self.handler:access({introspection_endpoint = "x", bearer_only = "yes", realm = "kong", userinfo_header_name = "X-Userinfo"}) lu.assertTrue(self:log_contains("introspect succeeded")) lu.assertEquals(headers['X-Userinfo'], "eyJzdWIiOiJzdWIifQ==") end @@ -186,13 +184,79 @@ function TestHandler:test_bearer_only_with_bad_token() end ngx.req.get_headers = function() return {Authorization = "Bearer xxx"} end - self.handler:access({introspection_endpoint = "x", bearer_only = "yes", realm = "kong"}) + self.handler:access({introspection_endpoint = "x", bearer_only = "yes", realm = "kong", userinfo_header_name = 'X-Userinfo'}) lu.assertEquals(ngx.header["WWW-Authenticate"], 'Bearer realm="kong",error="validation failed"') lu.assertEquals(ngx.status, ngx.HTTP_UNAUTHORIZED) lu.assertFalse(self:log_contains("introspect succeeded")) end -lu.run() +function TestHandler:test_introspect_bearer_token_and_property_mapping() + self.module_resty.openidc.bearer_jwt_verify = function(opts) + return {foo = "bar"}, false + end + ngx.req.get_headers = function() return {Authorization = "Bearer xxx"} end + ngx.encode_base64 = function(x) return "x" end + local headers = {} + kong.service.request.set_header = function(name, value) headers[name] = value end + + self.handler:access({introspection_endpoint = "x", bearer_only = "yes", use_jwks = "yes", disable_userinfo_header = "yes", header_names = {'X-Foo', 'present'}, header_claims = {'foo', 'not'}}) + lu.assertEquals(headers["X-Foo"], 'bar') + lu.assertNil(headers["present"]) +end + +function TestHandler:test_introspect_bearer_token_and_incorrect_property_mapping() + self.module_resty.openidc.bearer_jwt_verify = function(opts) + return {foo = "bar"}, false + end + ngx.req.get_headers = function() return {Authorization = "Bearer xxx"} end + + ngx.encode_base64 = function(x) return "x" end + + local headers = {} + kong.service.request.set_header = function(name, value) headers[name] = value end + + self.handler:access({introspection_endpoint = "x", bearer_only = "yes", use_jwks = "yes", disable_userinfo_header = "yes", header_names = {'X-Foo'}, header_claims = {'foo', 'incorrect'}}) + lu.assertNil(headers["X-Foo"]) +end + +function TestHandler:test_introspect_bearer_token_and_scope_nok() + self.module_resty.openidc.bearer_jwt_verify = function(opts) + return {scope = "foo"}, false + end + ngx.req.get_headers = function() return {Authorization = "Bearer xxx"} end + + ngx.encode_base64 = function(x) return "x" end + + self.handler:access({introspection_endpoint = "x", bearer_only = "yes", use_jwks = "yes", userinfo_header_name = "X-Userinfo", validate_scope = "yes", scope = "bar"}) + lu.assertEquals(ngx.status, ngx.HTTP_FORBIDDEN) +end + +function TestHandler:test_introspect_bearer_token_and_empty_scope_nok() + self.module_resty.openidc.bearer_jwt_verify = function(opts) + return {foo = "bar"}, false + end + ngx.req.get_headers = function() return {Authorization = "Bearer xxx"} end + + ngx.encode_base64 = function(x) return "x" end + + self.handler:access({introspection_endpoint = "x", bearer_only = "yes", use_jwks = "yes", userinfo_header_name = "X-Userinfo", validate_scope = "yes", scope = "bar"}) + lu.assertEquals(ngx.status, ngx.HTTP_FORBIDDEN) +end + +function TestHandler:test_introspect_bearer_token_and_scope_ok() + self.module_resty.openidc.bearer_jwt_verify = function(opts) + return {scope = "foo bar"}, false + end + ngx.req.get_headers = function() return {Authorization = "Bearer xxx"} end + + ngx.encode_base64 = function(x) return "x" end + + self.handler:access({introspection_endpoint = "x", bearer_only = "yes", use_jwks = "yes", userinfo_header_name = "X-Userinfo", validate_scope = "yes", scope = "bar"}) + lu.assertNotEquals(ngx.status, ngx.HTTP_FORBIDDEN) + lu.assertNotEquals(ngx.status, ngx.HTTP_INTERNAL_SERVER_ERROR) +end + +lu.run() diff --git a/test/unit/test_header_claims.lua b/test/unit/test_header_claims.lua new file mode 100644 index 00000000..07fea5f9 --- /dev/null +++ b/test/unit/test_header_claims.lua @@ -0,0 +1,33 @@ +local lu = require("luaunit") +TestHandler = require("test.unit.mockable_case"):extend() + +function TestHandler:setUp() + TestHandler.super:setUp() + + package.loaded["resty.openidc"] = nil + self.module_resty = { openidc = {} } + package.preload["resty.openidc"] = function() + return self.module_resty.openidc + end + + self.handler = require("kong.plugins.oidc.handler")() +end + +function TestHandler:tearDown() + TestHandler.super:tearDown() +end + +function TestHandler:test_header_add() + self.module_resty.openidc.authenticate = function(opts) + return { user = {sub = "sub", email = "ghost@localhost"}, id_token = { sub = "sub", aud = "aud123"} }, false + end + local headers = {} + kong.service.request.set_header = function(name, value) headers[name] = value end + + self.handler:access({ disable_id_token_header = "yes", disable_userinfo_header = "yes", + header_names = { "X-Email", "X-Aud"}, header_claims = { "email", "aud" } }) + lu.assertEquals(headers["X-Email"], "ghost@localhost") + lu.assertEquals(headers["X-Aud"], "aud123") +end + +lu.run() diff --git a/test/unit/test_introspect.lua b/test/unit/test_introspect.lua index 7d472634..062da8be 100644 --- a/test/unit/test_introspect.lua +++ b/test/unit/test_introspect.lua @@ -13,35 +13,43 @@ function TestIntrospect:tearDown() end function TestIntrospect:test_access_token_exists() + package.loaded["resty.openidc"] = nil + self.module_resty = { + openidc = { + introspect = function(...) return { sub = "sub" }, nil end, + } + } + package.preload["resty.openidc"] = function() return self.module_resty.openidc end + ngx.req.get_headers = function() return {Authorization = "Bearer xxx"} end - local dict = {} - function dict:get(key) return key end - _G.ngx.shared = {introspection = dict } ngx.encode_base64 = function(x) return "eyJzdWIiOiJzdWIifQ==" end local headers = {} - ngx.req.set_header = function(h, v) - headers[h] = v - end - - self.handler:access({introspection_endpoint = "x"}) + kong.service.request.set_header = function(name, value) headers[name] = value end + + self.handler:access({introspection_endpoint = "x", userinfo_header_name = "X-Userinfo"}) lu.assertTrue(self:log_contains("introspect succeeded")) lu.assertEquals(headers['X-Userinfo'], "eyJzdWIiOiJzdWIifQ==") end function TestIntrospect:test_no_authorization_header() - package.loaded["resty.openidc"].authenticate = function(...) return {}, nil end + package.loaded["resty.openidc"] = nil + self.module_resty = { + openidc = { + authenticate = function(...) return {}, nil end + } + } + package.preload["resty.openidc"] = function() return self.module_resty.openidc end + ngx.req.get_headers = function() return {} end local headers = {} - ngx.req.set_header = function(h, v) - headers[h] = v - end + kong.service.request.set_header = function(name, value) headers[name] = value end - self.handler:access({introspection_endpoint = "x"}) + self.handler:access({introspection_endpoint = "x", userinfo_header_name = "X-Userinfo"}) lu.assertFalse(self:log_contains(self.mocked_ngx.ERR)) lu.assertEquals(headers['X-Userinfo'], nil) end diff --git a/test/unit/test_utils.lua b/test/unit/test_utils.lua index 2b260f39..469acc08 100644 --- a/test/unit/test_utils.lua +++ b/test/unit/test_utils.lua @@ -16,13 +16,13 @@ function TestUtils:testRedirectUriPath() } } ngx.var.request_uri = "/path?some=stuff" - lu.assertEquals(utils.get_redirect_uri_path(ngx), "/path/") + lu.assertEquals(utils.get_redirect_uri(ngx), "/path/") ngx.var.request_uri = "/long/path/" - lu.assertEquals(utils.get_redirect_uri_path(ngx), "/long/path") + lu.assertEquals(utils.get_redirect_uri(ngx), "/long/path") ngx.req.get_uri_args = function() return {code = 1}end - lu.assertEquals(utils.get_redirect_uri_path(ngx), "/long/path/") + lu.assertEquals(utils.get_redirect_uri(ngx), "/long/path/") end function TestUtils:testOptions() @@ -37,7 +37,14 @@ function TestUtils:testOptions() introspection_endpoint_auth_method = "client_secret_basic", filters = "pattern1,pattern2,pattern3", logout_path = "/logout", - redirect_after_logout_uri = "/login" + redirect_after_logout_uri = "/login", + userinfo_header_name = "X-UI", + id_token_header_name = "X-ID", + access_token_header_name = "Authorization", + access_token_as_bearer = "yes", + disable_userinfo_header = "yes", + disable_id_token_header = "yes", + disable_access_token_header = "yes" }, {var = {request_uri = "/path"}, req = {get_uri_args = function() return nil end}}) @@ -49,9 +56,16 @@ function TestUtils:testOptions() lu.assertEquals(opts.ssl_verify, "no") lu.assertEquals(opts.token_endpoint_auth_method, "client_secret_post") lu.assertEquals(opts.introspection_endpoint_auth_method, "client_secret_basic") - lu.assertEquals(opts.redirect_uri_path, "/path/") + lu.assertEquals(opts.redirect_uri, "/path/") lu.assertEquals(opts.logout_path, "/logout") lu.assertEquals(opts.redirect_after_logout_uri, "/login") + lu.assertEquals(opts.userinfo_header_name, "X-UI") + lu.assertEquals(opts.id_token_header_name, "X-ID") + lu.assertEquals(opts.access_token_header_name, "Authorization") + lu.assertEquals(opts.access_token_as_bearer, true) + lu.assertEquals(opts.disable_userinfo_header, true) + lu.assertEquals(opts.disable_id_token_header, true) + lu.assertEquals(opts.disable_access_token_header, true) local expectedFilters = { "pattern1", @@ -63,5 +77,16 @@ function TestUtils:testOptions() end +function TestUtils:testCommonItem() + lu.assertFalse(utils.has_common_item(nil, "aud1")) + lu.assertTrue(utils.has_common_item("aud1", "aud1")) + lu.assertFalse(utils.has_common_item("aud1", "aud2")) + lu.assertFalse(utils.has_common_item({"aud1", "aud2"}, "aud3")) + lu.assertTrue(utils.has_common_item({"aud1", "aud2"}, "aud2")) + lu.assertFalse(utils.has_common_item("aud1", {"aud2", "aud3"})) + lu.assertTrue(utils.has_common_item("aud2", {"aud2", "aud3"})) + lu.assertTrue(utils.has_common_item({"aud2","aud3","aud4"}, {"aud4", "aud5"})) + lu.assertFalse(utils.has_common_item({"aud2","aud3","aud4"}, {"aud5", "aud6"})) +end lu.run()