├── .gitattributes ├── .gitignore ├── LICENSE ├── README.md ├── UPGRADING_FROM_V1.md ├── curity-test-config.xml ├── docker ├── api │ ├── Dockerfile │ └── api.js ├── deploy.sh ├── docker-compose.yml ├── kong │ ├── Dockerfile │ └── kong.yml ├── openresty │ ├── Dockerfile │ └── nginx.conf ├── teardown.sh └── test.sh ├── images └── phantom-token-pattern.png ├── kong-phantom-token-2.0.1-1.rockspec ├── lua-resty-phantom-token-2.0.1-1.rockspec ├── plugin ├── access.lua ├── handler.lua └── schema.lua ├── t ├── advanced_routing.t ├── api_requests.t ├── config.t ├── failure_scenarios.t └── scope_checks.t └── test.sh /.gitattributes: -------------------------------------------------------------------------------- 1 | # The module is coded in LUA, so prevent tests changing the GitHub language displayed 2 | t/* linguist-vendored -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | response.txt 3 | t/servroot -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2021 Curity AB 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Phantom Token Plugin for NGINX LUA Systems 2 | 3 | [![Quality](https://img.shields.io/badge/quality-test-yellow)](https://curity.io/resources/code-examples/status/) 4 | [![Availability](https://img.shields.io/badge/availability-binary-blue)](https://curity.io/resources/code-examples/status/) 5 | 6 | A LUA plugin used to introspect opaque access tokens and forward JWT access tokens to APIs. 7 | 8 | ## The Phantom Token Pattern 9 | 10 | The [Phantom Token Pattern](https://curity.io/resources/learn/phantom-token-pattern/) is a privacy preserving pattern in API security.\ 11 | It ensures that access tokens returned to internet clients are kept confidential.\ 12 | It also externalizes introspection and caching from APIs, to keep the API security code simple. 13 | 14 | ![Phantom Token Pattern](images/phantom-token-pattern.png) 15 | 16 | ## Installation 17 | 18 | ### Kong API Gateway 19 | 20 | If you are using luarocks, execute the following command to install the plugin: 21 | 22 | ```bash 23 | luarocks install kong-phantom-token 2.0.1 24 | ``` 25 | 26 | Or deploy the .lua files into Kong's plugin directory, eg `/usr/local/share/lua/5.1/kong/plugins/phantom-token`. 27 | 28 | ### OpenResty 29 | 30 | If you are using luarocks, execute the following command to install the plugin: 31 | 32 | ```bash 33 | luarocks install lua-resty-phantom-token 2.0.1 34 | ``` 35 | 36 | Or deploy the `access.lua` file to `resty/phantom-token.lua`, where the resty folder is in the `lua_package_path`.\ 37 | A typical install location for LUA files is at `/usr/local/openresty/luajit/share/lua/5.1/resty`. 38 | 39 | ## Required Configuration Directives 40 | 41 | All of the settings in this section are required: 42 | 43 | #### introspection_endpoint 44 | 45 | > **Syntax**: **`introspection_endpoint`** `string` 46 | > 47 | > **Context**: `location` 48 | 49 | The URL to the introspection endpoint of the Curity Identity Server. 50 | 51 | #### client_id 52 | 53 | > **Syntax**: **`client_id`** `string` 54 | > 55 | > **Context**: `location` 56 | 57 | The ID of the introspection client configured in the Curity Identity Server. 58 | 59 | #### client_secret 60 | 61 | > **Syntax**: **`client_secret`** `string` 62 | > 63 | > **Context**: `location` 64 | 65 | The string secret of the introspection client configured in the Curity Identity Server. 66 | 67 | ## Optional Configuration Directives 68 | 69 | #### token_cache_seconds 70 | 71 | > **Syntax**: **`token_cache_seconds`** `number` 72 | > 73 | > **Context**: `location` 74 | > 75 | > **Default**: 300 76 | 77 | The maximum time for which introspected JWTs are cached by the plugin.\ 78 | This is overridden if the introspection endpoint returns a `max-age` header with a lower value.\ 79 | This header derives from the `access-token-ttl` setting for the client that sent the access token.\ 80 | This logic helps to prevent an access token from being cached for longer than its lifetime. 81 | 82 | #### scope 83 | 84 | > **Syntax**: **`scope`** `string` 85 | > 86 | > **Context**: `location` 87 | > 88 | > **Default**: *`—`* 89 | 90 | Can be configured if you want to verify scopes in the gateway.\ 91 | To do so, specify the required values(s) for the location as a space separated string, such as `read write`.\ 92 | After succesful introspection, if one or more scopes are not present, the plugin will return a 403 error. 93 | 94 | #### verify_ssl 95 | 96 | > **Syntax**: **`verify_ssl`** `boolean` 97 | > 98 | > **Context**: `location` 99 | > 100 | > **Default**: *true* 101 | 102 | A convenience option that should only be used during development.\ 103 | This setting can be set to `false` if using untrusted server certificates in the Curity Identity Server.\ 104 | Alternatively you can specify trusted CA certificates via the `lua_ssl_trusted_certificate` directive. 105 | 106 | ## Example Configurations 107 | 108 | ### Kong API Gateway 109 | 110 | For each API route, configure the plugin using configuration similar to the following: 111 | 112 | ```yaml 113 | - name: myapi 114 | url: https://api-internal.example.com:3000 115 | routes: 116 | - name: myapi-route 117 | paths: 118 | - /api 119 | plugins: 120 | - name: phantom-token 121 | config: 122 | introspection_endpoint: https://login.example.com/oauth/v2/oauth-introspect 123 | client_id: introspection-client 124 | client_secret: Password1 125 | token_cache_seconds: 900 126 | ``` 127 | 128 | When deploying Kong, set an environment variable to activate the plugin in `KONG_PLUGINS`.\ 129 | Also define a LUA shared dictionary named `phantom-token` for caching introspection results.\ 130 | This must be provided to Kong via the `KONG_NGINX_HTTP_LUA_SHARED_DICT` environment variable: 131 | 132 | ```yaml 133 | environment: 134 | KONG_DATABASE: 'off' 135 | KONG_DECLARATIVE_CONFIG: '/usr/local/kong/declarative/kong.yml' 136 | KONG_PROXY_LISTEN: '0.0.0.0:3000' 137 | KONG_LOG_LEVEL: 'info' 138 | KONG_PLUGINS: 'bundled,phantom-token' 139 | KONG_NGINX_HTTP_LUA_SHARED_DICT: 'phantom-token 10m' 140 | ``` 141 | 142 | ### OpenResty 143 | 144 | If using OpenResty, then first configure the cache for introspection results: 145 | 146 | ```nginx 147 | http { 148 | lua_shared_dict phantom-token 10m; 149 | server { 150 | ... 151 | } 152 | } 153 | ``` 154 | 155 | Then apply the plugin to one or more locations with configuration similar to the following: 156 | 157 | ```nginx 158 | location ~ ^/api { 159 | 160 | rewrite_by_lua_block { 161 | 162 | local config = { 163 | introspection_endpoint = 'https://login.example.com/oauth/v2/oauth-introspect', 164 | client_id = 'introspection-client', 165 | client_secret = 'Password1', 166 | token_cache_seconds = 900 167 | } 168 | 169 | local phantomTokenPlugin = require 'resty.phantom-token' 170 | phantomTokenPlugin.execute(config) 171 | } 172 | 173 | proxy_pass https://api-internal.example.com:3000; 174 | } 175 | ``` 176 | 177 | ### Advanced Configurations 178 | 179 | You can apply the plugin to a subset of the API routes, or use the advanced routing features of the reverse proxy.\ 180 | The following Kong configuration is for a use case where a route handles both JWTs and opaque tokens.\ 181 | This might enable a microservice developer to forward a JWT an upstream microservice behind a gateway. 182 | 183 | ```yaml 184 | - name: myapi 185 | url: https://api-internal.example.com:3000 186 | routes: 187 | - name: bypass 188 | paths: 189 | - /api 190 | headers: 191 | authorization: ["~*[Bb]earer\\s*[A-Za-z0-9-_]*.[A-Za-z0-9-_]*.[A-Za-z0-9-_]*"] 192 | 193 | - name: phantom-token 194 | paths: 195 | - /api 196 | plugins: 197 | - name: phantom-token 198 | config: 199 | introspection_endpoint: https://login.example.com/oauth/v2/oauth-introspect 200 | client_id: introspection-client 201 | client_secret: Password1 202 | token_cache_seconds: 900 203 | ``` 204 | 205 | The equivalent OpenResty configuration is shown in [these tests](/t/advanced_routing.t). 206 | 207 | ## Deployment 208 | 209 | The example [Docker Compose File](/docker/docker-compose.yml) provides OpenResty and Kong deployment examples. 210 | 211 | ## Development and Testing 212 | 213 | The following resources provide further details on how to make code changes to this repo: 214 | 215 | - [Kong Phantom Token Tutorial](https://curity.io/resources/learn/integration-kong-open-source/) 216 | - [OpenResty Phantom Token Tutorial](https://curity.io/resources/learn/integration-openresty/) 217 | - [Wiki](https://github.com/curityio/kong-phantom-token-plugin/wiki) 218 | 219 | ## More Information 220 | 221 | Please visit [curity.io](https://curity.io/) for more information about the Curity Identity Server. 222 | -------------------------------------------------------------------------------- /UPGRADING_FROM_V1.md: -------------------------------------------------------------------------------- 1 | When using the Kong API gateway you must now provide the following environment variable (see below): 2 | 3 | ```text 4 | KONG_NGINX_HTTP_LUA_SHARED_DICT: 'phantom-token 10m' 5 | ``` 6 | 7 | The `trusted_web_origins` configuration directive is no longer used.\ 8 | For OpenResty, the `time_to_live_seconds` setting has been renamed to `token_cache_seconds`. -------------------------------------------------------------------------------- /curity-test-config.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | default-admin-ssl-key 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | P3gyxHjpXcGBBSuqcDe4YKCDkF4MO_njyGmKfbNb6ydhNka6MphJfjTbRXGPMCFg 15 | 16 | 17 | 18 | default 19 | http 20 | authentication-service-anonymous 21 | authentication-service-authentication 22 | authentication-service-registration 23 | token-service-anonymous 24 | token-service-introspect 25 | token-service-revoke 26 | token-service-token 27 | 28 | 29 | 30 | 31 | 32 | 33 | authentication-service 34 | auth:authentication-service 35 | 36 | 37 | 38 | 39 | default-simple-protocol 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | authentication-service-anonymous 48 | /authn/anonymous 49 | auth-anonymous 50 | 51 | 52 | authentication-service-authentication 53 | /authn/authentication 54 | auth-authentication 55 | 56 | 57 | authentication-service-registration 58 | /authn/registration 59 | auth-registration 60 | 61 | 62 | 63 | 64 | 65 | default-signing-key 66 | 67 | default-datasource 68 | 69 | 70 | 71 | 72 | token-service 73 | as:oauth-service 74 | 75 | 76 | 77 | authentication-service 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | read 86 | Custom read scope 87 | 88 | 89 | write 90 | Custom write scope 91 | 92 | 93 | 94 | 95 | 96 | test-client 97 | secret1 98 | read 99 | write 100 | 300 101 | 102 | 103 | 104 | 105 | 106 | introspection-client 107 | secret2 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | token-service-anonymous 119 | /oauth/v2/oauth-anonymous 120 | oauth-anonymous 121 | 122 | 123 | token-service-introspect 124 | /oauth/v2/oauth-introspect 125 | oauth-introspect 126 | 127 | 128 | token-service-revoke 129 | /oauth/v2/oauth-revoke 130 | oauth-revoke 131 | 132 | 133 | token-service-token 134 | /oauth/v2/oauth-token 135 | oauth-token 136 | 137 | 138 | 139 | 140 | 141 | default-signing-key 142 | 143 | default-datasource 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | default-datasource 152 | 153 | jdbc:hsqldb:file:${se.curity:identity-server:db};ifexists=true;hsqldb.lock_file=false 154 | org.hsqldb.jdbc.JDBCDriver 155 | SA 156 | 157 | 158 | 159 | 160 | 161 | 162 | default-admin-ssl-key 163 | data:application/p12;aes,v:S.bVVGdWszVXZ1OHR1Y2FMcw==.wpz6Jv95Dn173zm2QRixkQ==.g14-qVJiZcu47LTtFQIw09htwT6PDkdXfIz3b_vlJZEKCvZq7JSAT5tkzMBypctCzEI7v2TJtgDW2FR8Ss2yJD8jb20n_J8q0wt0BzrY-BfOSCU8xas9-yE_DUoFnHxbmcdEN4jvTmiqoHAfqeLsQIOMPE9kwYOltTMOWOUjLFW1gQMLPsO2XBmYYi3XuWvTt-ewJB6NJtZ1K76KT_tk2qw7PU-mWS_i6951UXKYeyQAIFsS06zZRY6-iN4rkPXtZjbYXBbo7PzTy-W8o2v_hsc6XTDVCLOG6HT6Nner4RDIx1Jw8_gF96St6u-SCK1J10iz5mVD9NOTBAWhRVKuHs6Oz-Tt142bpY2vIniEJ6lByJT2flL-dx6PKt_3ufeI85ucydwmP4KApdHELh6GcyifzfsUyJegq0T5_5fPYytZRu--xrMqVtmAgXprM7VYzIccRB2Ghavi4HVbpiKBVnxa5f3FrBJiFo0p3RoE4FZ3CWdrf-G3x6Z3RHwZE_ujQFPIV6Svcyawz_g4eUWuAjUdDDpgg5yKLdZSCOTAI9amAXo7DC3A6ZJGjmhsGzXNzpafP7Ag4op6YMeHg2qNwKq549ycrrVPQDHGBnSIDf7_FFz-x0XEWdwpShc6y54eg231VT-jE5JSH3S8ZKRRtPvhRQzVxgGM1lgztAe6zVWi3oxfUj6q7kFn7xjbYCB011SHio-WKUawkosvd1PFaayvmC0PhomanaRqOf6dB1Y6xo3l58AbV_rTc5RO-UH1PP-4u89OXNt6sOV8xm32F8AiQzAIvRWu3Ppojo7WqH4N8QKmeyjjTx7AQXOOYsJkaig_NpAtiGS1PC328k30aFlev2AqKxCx9FkJK3wLjSiWd_jrZIToRFkYxHzzHd4KnnZLL68WZJIYrCR4h39XODWt-12-O-yoedlSzSJE6SgBFlLXahlSHZ0SWTcvpl3hZRyZofZ7_1O8OFAMG4PeHMGcBn1cetesVLsNRN4VSzzEYuPkvz11JVl3PZk3DS0qhMi1N9RC1f6y_uEJLG-sO8KcVqDsQBqEj1woYklZKZen98LGs6saLgfyyXbBkOoCd2PwwTmfKRPeq1w57wbqrZgCWTVcl0U0Rqmkot_4AzwEdQLdfWbVUCkDjfQ4HhKxAnFi069zTOng-oFIbo8IANpyZQaqiLXuvk2Xt4LKbVPax0Gv2mxiD0ZeWxVwY6LjgstUMjTLE0nAq-dK9qCLK7ICbJPvi1pCPYHY6AE2-ptN7DmgC6xdOVjv5o97EUe6TjpNNJEjlYxgJpjKuo-8qNB0ee2ivVTy_4xS-h8ywc7RlXJiUyKzHd-qt36URfUsfV2bm6H0CSSYOp9a8lBx0yBtcQWKve0IHPPjW8mISocvtWy9mHKOPK5dG4m7rGu6ZicHctUaS3o1YliEzsZ-rxpgiO06ED0S34Zj76wV3mLNdBiB4gADJ_E-wCEX4GVnvP4lIPslIW8H_wR0Os9l0xKi8sGO9FEPEQ1QuDBweOCvB4UyaAMtMRlqezHOjJNCf_BcGOWjmYVNDT7MZHbiP_zrpfDFAMI_dG5HavYUDaweN81iYWEb6IMceOkmgORp168eErBLGftveGXMEQxoWWnX7qBdsQ7BHKEJlO6ilLwBL2lqC8__iyA1BirO_UgVdqqwUnGz8CAknc-t2Ts3WMr7b7QuuJ3ma4TtSSoJSF9N5TdtNOpC876F5PSK4OklaCp-qJzVy67ATFCOVa7T2F1ZP4CDUzazSg-nLYzezpeTYynRDyiw-0TyyBTu0NkjSRlI2qBoPEfcT8YPIlFU2ayouTrNnSdJYdMOWjs_nZdopKo0yC7m_sy0a2loTFm4iAckxMUHtKTD397s_00_eIrlg5k7vm54qCJuhGWcEn97QwbqbxDgIhTimwylGfK6C4-VaJ-_r0Jad0G5c7MJxjF5Rg_J-QqlzWom3DDVsfCXxDltkwdNfbF1kbG_8Yf2PORxi4Lo2p55lp9a4_9xDLkOVvybjRcmZxrQddjfpZWXvYKwekTaVo6pefbGUpbEFZLPHphfbXpGiRsjr7h2oNHktjjsw4WRDoX0w-CapzoX0yKgmc6qWWqcENMCeJmchuL3TRpV0ZFrA_1JetUYZVzTZOgrfh8SJrYg0Nz34t54PLLCwZ4mgI7XvF3MTN25o7Sa0c_Hq50IEn8wAApEGWoP1zo18hnPHDOfr2O0B3t3bIH2Phzm7feAHnGUBlXSgGdByhQpfqFexbHsNcbOCsJnm16shrmZTbuP7uFxyKh3b4QrXTee56zi4Snseo7CFmyohyTVnoSevFoNG_DRZ5r6YrQbNFiI2E6WU-PM6HMQ4Xnp53aqQ7zbmXqAk2rewn1LI9k-xGtWsfcmXzc4tXl6U3rfV_ZmV9rApLyq2kdtB_ZD_W-9pxZmUUaesYpPh6oujKhnaGtErt0TT9tEIAuaq9JZ7mm97y9ELWP2gbKtazEYvtfL0S5aUoZrQYd0uExbRlqcYo676qh1peeGAliehRcq-f7xUeyCbF0_vaBMgkgYyeq4JP_qW1557CVTMqEN-PV9oPDgJM54EgHGa0UOlKzx7eb0R-3tJx4LLqy-6xiaCWzK8Jc7t1i-Wj9oX_pOKbZQmIaCWvsssnBS60td4XBAskrBnnzIfw_7Uyf0QackClryID33tOKv42D40ddx7SwHZun69CV6kSjyEM-ZfrIoOyBVOozldWG7Hwj58tK33oPQkfgWnoAk4mCNFFVrNOsi0NPXsvLdnIJCFz06SU6b1d7X1bdcGI72TUVAlqAQAMHw2blw4oYvbmDgmJD5H1vpg6Av8j76BFctZoqD1mwM1AR_SNyxKUzvAWeqpUvaR6zER2Fjxmn6MhqACClYtUoD5aoZugOe49E2nZW93XfaWd1Bd_8DRPbwqOCn7NmsYhhFCgzOjpW2MqNwLWuOZUQQa7PtWauDUcWKRCGOwZ1nkhihvDweQaA6E4_pOlzCCpdrIL1gHgN8a9mCAVNvdNt0VuyKm-5a4JJCAPzXLalb_t0Cp3J4LkHHfJA1I_0wZuiUezLR-Ycf2S6NxirYgFc3Z6IKh-P1Uy-mmnG8aLOI9GugSRToRxy7D13NMPjsG1EKWTH2KLxOmI_q2CTwzfqn0dmJ6sDP1OXXNV6zzEaS_jVaTEtYG3a2j7mfW2fiMxT4oYIPmhdaaO2zqQuwyswGWYzjQUqLoSuWWFpDzx1SiUj9pkhbrG-hDHBSsKf9EZT1IOcsqTigEXMtZYifYP-SHHzDjnZu1QdiFz7Fgd471Lqt0hLIowNB1e3fICKidwPLrrAIQnW6hw5n.naYHDeqoJy_Lq5kABkxbD4_OqtjFnUDUe-LeWmta-Y8= 164 | 165 | 166 | 167 | 168 | default-signing-key 169 | data:application/p12;aes,v:S.VE9pWG10ZmgyeFdCVG43MA==.RwyyFIMLUSuZG5oMcHiDoA==.vUwWWZRjS1JxinBJ_8Jo8Lpfbq28AZJoufVS659e0S0rB_dW1CESzfzuzXrSnfkxb_h7BIJlqKq99bJ0L46K1KHevBtpb-80XXGl5UV4tAh0DauGFw6sHJkkqyEZEfUQF8PPHxQfGMlUke5r6e1jcQLiIFPib6Jp6f4oSEiyZVX4ZRqlf5cNxwvzxxAiA2RtXQcF61lxMXye3E9qmlj3qDI-kw3NksrfAgUIDwZCJcGRPPjvZKkkVVxhbO-PAC2tVVAIUAFG1aixmVMcuwNZUw8vO5_jeXjMU5VZWlId-lrNm4m3BcQqffuubRAri_PFiMmwYZUFpBFvKE2zT2PZoFaGMdx-_FKtaZKnScPY1_QLgagy8CAdD8LF2IoI7VtxGaDbu-EyFOSy3IF4dr-CaCuTwMaepqEavMxEmVa3CiIK-EVzNrz1HjvCYar5FyganollCSY3j0n7dPQLWL_zNmeL6E1N3Es_sBRMRUBx8oRyCrNsgKoY9_psuC-JapNTdm2Lyppgiu82fOL7Q85pB1OhWToeHmK9thz4ZwSeKGEkpXogwHsEkAW1_cagPuq6eKJCfrgeF5XPk9UZH18L8E5AmZJSqW1OYjUjfCnxW3Vb8Xyv0agpet21ylgK6-4ugLUl6yYCX0-YWAg3PGgEOa0f3j-bIOdgavw5KMO5qWgVQqAaGrVe1wSqv7iqtVQkbA8Vnf8l_7NqENue1FKiF_u8S49IMumuIS7JiUhyvgtXGU_LbadsIPekroSSCwh3Mz5i2cPd5cxmXVW94xteuvT3y0M4kiCVWexSUTGVuOkVjCFLJ9Ngw6ZOtij37E-FL7cQ7OBUW2wAAZF40rg-cIkOH_x3Cve3Pk3cZjnPoPCyLlmR5lqzjHGMSjZknkoC_F6ds_s8m1k9yMS56ZOtxzjqe3przkUD0YWgB5lLP7gkMLwpSVM-EInUxWKiOgbQrbQ5Yc1m_6L4r4vTBFPtuFlfYOG0ToS9F0WeExwjX0OLPwGbp6Put_wCU8AoaKaO9istjP-GNylKbjcVoALldiJVsd5UydEbiKs31vqawbn0-VsV2NvgV_0d58fjACW1oAXgeKhCyFg_sEBhkuukCS95DbcZPT18SdA9rEHaLXs5gOPvR2cO81K30DA1P3zuTQtZza8Q115Z4aN8usmWGPIHs71tqQLejoFGfyheTfF3q1ixBqe_66MP7o9lhZCVftw7CIOcQytFQQ-56s5kj2N2-9PS_e6hdKVKERRVt38Diaxq3Fm8YsyIynJ11rCrsjCMD6IHGT-km4pETNKWJWBrPe8ckgBpQnQv9AUUaI-l_zBcUGtGLWOtrSRpV2qAMgQO0JyOauFV214SqXZtMR1P4KlBY0fO1j0Fgi5MZFgDJVMbK8nux6pAQIAwJgIqLuhDDDlCkK-eNfA8B8KoWZUKsZglt7AUgN4i9_5-cx6qntdLFilPqMwYF2muZSh_jmO-aQbnD_-arYk1QvDUjmP_J-tkfavoCVYKd9s2ylPD3px_B6WlcKuH-KriCZXqs0NQXqk1y99B04ykmFPzWifQz8oTxQAAY80Sjv5TJCs-rMnJS2GYyrZ1lNv5uMNR2SJTMyjrGqg2sREGI2B_rJWFMz74SYvfbdVOUAqcwDuj5QRmvyqsPHnOtmInDEd2rShDjj0m3kwBidAXc-9ec0dj9n55cJO1NQAE3nVdaM2ocqDu2xnCX-EIJq1f7_iF2QBguv63Catgm5pJPJ9XTp4qoN_Gl2c8_0pT1FDe9oqoeSx2B1cY308vh_MwgjMkRMpLohCdPVQAa532foIhM-ckTuza0G1w_iKWcM7rXZrTjXtmQ5rmp4wkUww7Sb2J5Rnai8X1eZNbjJx0SBLUEgkkuLY0rmvSs7NIGj3A-EPz1tOHNNDjyuIB8BTd2STW0oQO2jtYz51354AHTb5Ct2PwgXJldRF0UPnUXBDdpJvKkgZg9bZJQiVxoZrLPV0Cm4Vr2qVYDWpYP2JaS0-dF3y5GOe9tuJU-N0BIIVVyNtP2ipAxX2GC6zlbeJKUVH1qAoFKjFkVYvke-DHXlca6CiLInt6F2eZ7wYisrVbNWP7LYSBExKTXS6ymEfBdBazuBO5P8ya_MfqPlBKvXjhPRbBuECSD0jyuQAWREutLrgI-GrOXVL-yxZApcUHtfGSJnd7M8bj50OI3Uszj1c5U78A1tX98Xc6GMqmOk1m0xpi2iUuZN655Z711R04MGzKNkS3CvckhcWrex-mFDGhcutS-ULQPkuvTPmv2Ljf2bE-uADNrJYNRXRaoRtbCUwMHwa7ljn3KQqR7iVc1JYHwG4-jhg_6j24dR_x8gH5w2-W3Rubkzx0NeWAs4-aGygzLrDNaDLiTtZEHIjOYoYciRwd3b55fmxebYDUbhzr7I-SHCCF_6H6N1zfqTUQ4RE_1VyZHtUMYoS12owi0b990WI3WQTVGOwdSj1CP9MHIJghAuKEjJo-WOgug3LaxJGwRJiB6xB9H47UfD29IXGFKb7VawJs1Ntmx_mW8VhsLq01JLdAs2jvTN8lXMirOyCQ6wcbCeG4p-iSL47T1PBKuuNfDyjCdy-1umA9_cktcY_Nea01VCEghfum7X0_5alILSX7GmbbYv9JAdhoRHq5XP1zEzncIro79gOKFHM3Msta45t71UYo0CVYgqwlvoR-NF1pyCrr2xFO4pmfaVzyxU-hwuSO1iwscUYLJM6rmbFaqB3dLK454i332wLJdZlThxguvWFDyrV7oG1K9H8CNSSPRnEfcjoefAuRvXqntYNVbcJJUfynRr9H1nJ6v1JRvQjckcBWNVJgELFq_TKAPrDz5AfIAGvLUXce6cpJTKtAsSrl6-zBeCBqhlpAkIfbNZoocbppJslmwOCT-UsDHsw5jolB85wxT56io7Qm0FTfNPg8KBpAMwiiWXu95c2wSaw549j1McNs3FWXZinBVoo0Kxw7KlEa9xpgYZYCoKrWGEPzrCdcM31T4ws5-e3BCxQzueRwsV8u5pPFFXI4PVnnS5fYh7jVebw-O-obFUsOaU2_rR6-zOo2KFj18iZOeZQioctG5FZq8DNhG-OA1m-H4ieNs43WRgHj8rPlzgCXtGm4b5JSOgshC80fjRQHR51MXB6JHknOMmOZDi5ZsL_RCKkrdbC-ffNkUzbuoF9MJUO5FRMkecFjCtzGxzWz7AtDEhdYHdCKy-VjqcrcPfVmGr8dDBmUJexYopQqUDk1QSnl-0UApBtpAQXeS4yVc4-eFaMbda8RM715vC6K4LEURGyGkw6HgCMYn8zag13MHqti1kFA053tJafWGaJK.J_oPtpG49HlQHy4wOhbuAU1T5qxzXFs6zEZ6fhsSWdc= 170 | 171 | 172 | 173 | 174 | default-signature-verification-key 175 | data:application/pem;aes,v:S.QzVQeFFNTTRIZVlMY1B5bQ==.b5KPUKvclfiV4EXfj2OHyA==.8i_pQIl8mVe1sUeITgPCp5U7_aXQl_QivJB-sxnVcz-d22qNBq0RVlf1XrgNVyTV-7sNz1SeclGyVGllEsl0BkWsdojfL8OOqigWP0yNN6D-z6yowVn7QzYkqMLfZrCReHKCvktk4JmEXAMgJ8df0nXwDwPwA6wKJgreOhQRar8e3bSxt8TR-FiOwJ4hPF9zSBAsQHnyhQ4QCSumcyC3cnDoolObd1jU5IvlV32eoPwaxPaltOI4rJsHVy07AxQRf7XRL6uhxjkwqTfZ9mvE50GQeoOYlOWxXjGMM3JbYvgBlQbJXgqyaK75GdMS5v8bTPmQcrr9Asfm70anyydi-biHsq-PQcrEL1HNExa6yPaE1GhysXiWgzFhne6ftl2Cop7JYQkOgDSvXfT2TRRmjdBDQAw1biktwwzEH6Bjm_k-N_sYckqdnKHMniCNMDvrexH1p229jWUcluEDmEbvG2sPW2J-VQP28JUYaFdVpl6bcRa5WHTbmaKjmBmsHYLVG1KVudVDN3GEb99aIY42V_ybDIC7DOa_DFaE5eYU_QeYRDuosFAsy8oPy3n0OOr0Ah4pK7yHzumh4mPGkHzNG7SKEF1aLbtowfPhERhQFaqT_zcWNMvY1THdxKvoyPoJWWBfFzpnwOz703vP3CEP0R8h7C0aUnwkLc4QCdfdu2pr1NCFYRKM_MNMjuFkLA5YtFKXKnC-s8WDzTH_UbEGFGGxGkgHYuadunsiIj0FZ3dx4F-SpTygSI9t23M0dqceggzuR-zryjKeV9pH_1md0-yfaOfrldqCpTgHGfalyX2pj03L0L7IFMrMhrLjlDiuHd0ZZo5LnmxuexuEDMLaHzchFc0DNcozMO0XIDBMsnHQph48aKCU4gihUUHKJA9qta1wGtpp7bsJsaEXXz_2erhIkOdoXpilLBcc5ZMYIMlpdVpsZ4lXAHzEg1IwCINMtSeoVM30UUGANv89OLS3IX-3ZGSq6nut2VgRIe0nrbeAFJ8fWwbUxe0E9OPXqTMNmYDeMQfCgWThzf_JgZ8_uo3hnwnQutUskjVQTODbtzbmgnmMWFJ9R_Y8dAmWvA53vW7pe68Vgx1uIhHFOrEEiKkLmhreCSydrupWzw08ed18aZG-SA5qAeGlS6bTWVet0z2z1jbb0LpEVyG0JK42b6f7j_cAoJ5-MufUHD7W3vzAZE2yJYrWPV9-aDfQaM3lTPqlyHZV64GIKsHEMLlESyXYq3SZJFRYbyZkgvUV548luRKSorCdwyM-B6yvH4-ItM1TH-aq6jXf8nxUZbBtanCR2uPPY2VZdYXA7I8XMlWDwG-9ddr0ElgJT6rJIeh_4LhIIGoWff9T5tUGQF8KCyJ1z5UTygbFS30pQxo6ng42sMjd7Xqj1NTXaDNCc3o3b-x1-WLXzMDzGkWN_0VwAJEP6t4XMqFLlrl1VUfgWJgl5WUdp_dD2CIS4JieFiZi0i53Cr1rrl1LtoIk23lxgw==.w0yeo_HCaea5i807NoU549I3zlJAd5UHqTPUXf8nKlo= 176 | 177 | 178 | 179 | 180 | 181 | default-datasource 182 | 183 | 184 | 185 | 186 | #{LICENSE_KEY} 187 | 188 | 189 | default-account-manager 190 | 191 | no-verification 192 | 193 | default-datasource 194 | 195 | 196 | 197 | 198 | default-credential-manager 199 | 200 | 201 | 202 | default-datasource 203 | 204 | 205 | 206 | 207 | 208 | permit 209 | 210 | 211 | admin 212 | admin 213 | 214 | 215 | 216 | admin 217 | admin 218 | 219 | any-access 220 | permit 221 | 222 | 223 | 224 | any-group 225 | * 226 | 227 | tailf-aaa-authentication 228 | tailf-aaa 229 | /aaa/authentication/users/user[name='$USER'] 230 | read update 231 | permit 232 | 233 | 234 | tailf-aaa-user 235 | tailf-aaa 236 | /user[name='$USER'] 237 | create read update delete 238 | permit 239 | 240 | 241 | tailf-webui-user 242 | tailf-webui 243 | /webui/data-stores/user-profile[username='$USER'] 244 | create read update delete 245 | permit 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | admin 254 | 0 255 | 0 256 | #{ADMIN_PASSWORD} 257 | /opt/idsvr/home/admin/.ssh 258 | /opt/idsvr/home/admin 259 | 260 | 261 | 262 | 263 | 264 | 0 265 | \h> 266 | 267 | 268 | 15 269 | \h# 270 | 271 | 272 | exec 273 | 274 | 0 275 | 276 | action 277 | 278 | 279 | autowizard 280 | 281 | 282 | enable 283 | 284 | 285 | exit 286 | 287 | 288 | help 289 | 290 | 291 | startup 292 | 293 | 294 | 295 | 15 296 | 297 | configure 298 | 299 | 300 | 301 | 302 | 303 | -------------------------------------------------------------------------------- /docker/api/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:16.6.0-alpine 2 | WORKDIR /usr/api 3 | COPY docker/api/api.js /usr/api 4 | CMD ["node", "api.js"] -------------------------------------------------------------------------------- /docker/api/api.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const http = require('http'); 4 | const port = 3001; 5 | 6 | const server = http.createServer((req, res) => { 7 | 8 | const auth = req.headers['authorization']; 9 | let accessToken = '[NONE]'; 10 | if (auth && auth.toLowerCase().startsWith('bearer ')) { 11 | accessToken = auth.substring(7); 12 | } 13 | 14 | res.writeHead(200, { 'Content-Type': 'application/json' }); 15 | res.end(JSON.stringify({accessToken: accessToken})); 16 | }); 17 | 18 | server.listen(port, () => { 19 | console.log(`API listening on port ${port}`); 20 | }); -------------------------------------------------------------------------------- /docker/deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ############################################## 4 | # Deploy base infrastructure to enable testing 5 | ############################################## 6 | 7 | # 8 | # Ensure that we are in the root folder 9 | # 10 | cd "$(dirname "${BASH_SOURCE[0]}")" 11 | cd .. 12 | 13 | # 14 | # Get command line arguments 15 | # 16 | PROFILE=$1 17 | if [ "$PROFILE" != 'openresty' ] && [ "$PROFILE" != 'kong' ] && [ "$PROFILE" != 'test' ]; then 18 | echo "Please specify 'openresty', 'kong' or 'test' as a command line parameter" 19 | exit 1 20 | fi 21 | 22 | # 23 | # Prompt if required, and expand relative paths such as those containing ~ 24 | # 25 | ADMIN_PASSWORD=Password1 26 | if [ "$LICENSE_FILE_PATH" == '' ]; then 27 | read -t 60 -p 'Enter the path to the license file for the Curity Identity Server: ' LICENSE_FILE_PATH || : 28 | fi 29 | LICENSE_FILE_PATH=$(eval echo "$LICENSE_FILE_PATH") 30 | 31 | # 32 | # Check we have valid data before proceeding 33 | # 34 | if [ ! -f "$LICENSE_FILE_PATH" ]; then 35 | echo 'A valid LICENSE_FILE_PATH parameter was not supplied' 36 | exit 1 37 | fi 38 | LICENSE_KEY=$(cat "$LICENSE_FILE_PATH" | jq -r .License) 39 | if [ "$LICENSE_KEY" == '' ]; then 40 | echo 'A valid license key was not found' 41 | exit 1 42 | fi 43 | 44 | # 45 | # When deploying the reverse proxy, build the custom Docker image, and use 'luarocks make' to deploy the plugin and its dependencies 46 | # 47 | if [ "$PROFILE" == 'kong' ]; then 48 | 49 | docker build -f docker/kong/Dockerfile --no-cache -t custom_kong:3.0.0-alpine . 50 | 51 | elif [ "$PROFILE" == 'openresty' ]; then 52 | 53 | docker build -f docker/openresty/Dockerfile --no-cache -t custom_openresty:1.21.4.1-bionic . 54 | fi 55 | if [ $? -ne 0 ]; then 56 | echo "Problem encountered building the reverse proxy docker image" 57 | exit 1 58 | fi 59 | 60 | export LICENSE_KEY 61 | export ADMIN_PASSWORD 62 | if [ "$PROFILE" == 'test' ]; then 63 | 64 | # 65 | # When running the root test script, detach, wait for completion, run tests then tear down 66 | # 67 | docker compose --file ./docker/docker-compose.yml --profile "$PROFILE" --project-name phantomtoken up --build --force-recreate --detach 68 | echo 'Waiting for the Curity Identity Server ...' 69 | c=0; while [[ $c -lt 25 && "$(curl -fs -w ''%{http_code}'' localhost:8443)" != "404" ]]; do ((c++)); echo -n "."; sleep 1; done 70 | 71 | else 72 | 73 | # 74 | # When testing deployed instances of Kong and OpenResty, run Docker components interactively, to view logs 75 | # 76 | docker compose --file ./docker/docker-compose.yml --profile "$PROFILE" --project-name phantomtoken up --build --force-recreate 77 | fi 78 | if [ $? -ne 0 ]; then 79 | echo "Problem encountered running the Docker deployment" 80 | exit 1 81 | fi 82 | -------------------------------------------------------------------------------- /docker/docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | 3 | # 4 | # Use Kong Open Source as the reverse proxy when the kong profile is set on the command line 5 | # 6 | kong: 7 | image: custom_kong:3.0.0-alpine 8 | hostname: kongserver 9 | ports: 10 | - 3000:3000 11 | volumes: 12 | - ./kong/kong.yml:/usr/local/kong/declarative/kong.yml 13 | environment: 14 | KONG_DATABASE: 'off' 15 | KONG_DECLARATIVE_CONFIG: '/usr/local/kong/declarative/kong.yml' 16 | KONG_PROXY_LISTEN: '0.0.0.0:3000' 17 | KONG_LOG_LEVEL: 'info' 18 | KONG_PLUGINS: 'bundled,phantom-token' 19 | KONG_NGINX_HTTP_LUA_SHARED_DICT: 'phantom-token 10m' 20 | profiles: 21 | - kong 22 | 23 | # 24 | # Use OpenResty as the reverse proxy when the openresty profile is set on the command line 25 | # 26 | openresty: 27 | image: custom_openresty:1.21.4.1-bionic 28 | hostname: openrestyserver 29 | ports: 30 | - 3000:3000 31 | volumes: 32 | - ./openresty/nginx.conf:/usr/local/openresty/nginx/conf/nginx.conf 33 | profiles: 34 | - openresty 35 | 36 | # 37 | # A tiny API as a target for testing routing 38 | # 39 | business-api: 40 | hostname: apiserver 41 | build: 42 | context: .. 43 | dockerfile: ./docker/api/Dockerfile 44 | profiles: 45 | - kong 46 | - openresty 47 | 48 | # 49 | # The Curity Identity Server is deployed for all test configurations 50 | # 51 | curity: 52 | image: curity.azurecr.io/curity/idsvr 53 | hostname: curityserver 54 | ports: 55 | - 6749:6749 56 | - 8443:8443 57 | environment: 58 | - ADMIN=true 59 | - SERVICE_ROLE=default 60 | - LICENSE_KEY=${LICENSE_KEY} 61 | - ADMIN_PASSWORD=${ADMIN_PASSWORD} 62 | volumes: 63 | - ../curity-test-config.xml:/opt/idsvr/etc/init/curity-test-config.xml 64 | -------------------------------------------------------------------------------- /docker/kong/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM kong:3.0.0-alpine 2 | 3 | # Deploy the plugin and dependencies for local testing 4 | USER root 5 | COPY ./plugin/*.lua /tmp/phantom-token/plugin/ 6 | COPY ./kong*.rockspec /tmp/phantom-token/ 7 | RUN cd /tmp/phantom-token && luarocks make kong-phantom-token-*.rockspec 8 | 9 | USER kong -------------------------------------------------------------------------------- /docker/kong/kong.yml: -------------------------------------------------------------------------------- 1 | _format_version: '2.1' 2 | _transform: true 3 | 4 | services: 5 | 6 | - name: business-api 7 | url: http://apiserver:3001 8 | routes: 9 | - name: business-api-route 10 | paths: 11 | - / 12 | plugins: 13 | - name: phantom-token 14 | config: 15 | introspection_endpoint: http://curityserver:8443/oauth/v2/oauth-introspect 16 | client_id: introspection-client 17 | client_secret: secret2 18 | token_cache_seconds: 900 19 | -------------------------------------------------------------------------------- /docker/openresty/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM openresty/openresty:1.21.4.1-bionic 2 | 3 | # Deploy the plugin and dependencies for local testing 4 | COPY ./lua*.rockspec /tmp/phantom-token/ 5 | COPY ./plugin/access.lua /tmp/phantom-token/plugin/ 6 | RUN cd /tmp/phantom-token && luarocks make lua-resty-phantom-token-*.rockspec 7 | -------------------------------------------------------------------------------- /docker/openresty/nginx.conf: -------------------------------------------------------------------------------- 1 | # 2 | # A customized version of the default openresty file 3 | # 4 | 5 | pcre_jit on; 6 | error_log logs/error.log info; 7 | 8 | events { 9 | worker_connections 1024; 10 | } 11 | 12 | http { 13 | include mime.types; 14 | default_type application/octet-stream; 15 | client_body_temp_path /var/run/openresty/nginx-client-body; 16 | proxy_temp_path /var/run/openresty/nginx-proxy; 17 | fastcgi_temp_path /var/run/openresty/nginx-fastcgi; 18 | uwsgi_temp_path /var/run/openresty/nginx-uwsgi; 19 | scgi_temp_path /var/run/openresty/nginx-scgi; 20 | sendfile on; 21 | keepalive_timeout 65; 22 | include /etc/nginx/conf.d/*.conf; 23 | 24 | lua_shared_dict phantom-token 10m; 25 | 26 | server { 27 | listen 3000 default_server; 28 | 29 | location ~ ^/ { 30 | 31 | # Use the Docker embedded DNS server 32 | resolver 127.0.0.11; 33 | 34 | # If required, introspect an opaque access token and forward a JWT to the API 35 | rewrite_by_lua_block { 36 | 37 | local config = { 38 | introspection_endpoint = 'http://curityserver:8443/oauth/v2/oauth-introspect', 39 | client_id = 'introspection-client', 40 | client_secret = 'secret2', 41 | token_cache_seconds = 900 42 | } 43 | 44 | local phantomToken = require 'resty.phantom-token' 45 | phantomToken.run(config) 46 | } 47 | 48 | # Then proxy the updated request 49 | proxy_pass http://apiserver:3001; 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /docker/teardown.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ############################################# 4 | # Tear down base infrastructure after testing 5 | ############################################# 6 | 7 | cd "$(dirname "${BASH_SOURCE[0]}")" 8 | docker compose --profile "$PROFILE" --project-name phantomtoken down 9 | -------------------------------------------------------------------------------- /docker/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ################################################################################## 4 | # A few sanity tests that can be run in order to test with deployed infrastructure 5 | ################################################################################## 6 | 7 | API_URL='http://localhost:3000' 8 | RESPONSE_FILE=response.txt 9 | 10 | # 11 | # Ensure that we are in the folder containing this script 12 | # 13 | cd "$(dirname "${BASH_SOURCE[0]}")" 14 | 15 | # 16 | # Ensure that the Curity Identity Server is ready 17 | # 18 | echo 'Waiting for the Curity Identity Server ...' 19 | c=0; while [[ $c -lt 25 && "$(curl -fs -w ''%{http_code}'' localhost:8443)" != "404" ]]; do ((c++)); echo -n "."; sleep 1; done 20 | 21 | # 22 | # First authenticate as a client to get an opaque token 23 | # 24 | echo '1. Acting as a client to get an access token ...' 25 | HTTP_STATUS=$(curl -s -X POST http://localhost:8443/oauth/v2/oauth-token \ 26 | -H "Content-Type: application/x-www-form-urlencoded" \ 27 | -d "client_id=test-client" \ 28 | -d "client_secret=secret1" \ 29 | -d "grant_type=client_credentials" \ 30 | -o $RESPONSE_FILE -w '%{http_code}') 31 | if [ "$HTTP_STATUS" != '200' ]; then 32 | echo "*** Problem encountered authenticating as a client, status: $HTTP_STATUS" 33 | exit 1 34 | fi 35 | OPAQUE_ACCESS_TOKEN=$(cat "$RESPONSE_FILE" | jq -r .access_token) 36 | if [ "$HTTP_STATUS" != '200' ]; then 37 | echo "*** Unable to get an opaque access token" 38 | exit 1 39 | fi 40 | echo '1. Successfully authenticated the client and retrieved an access token' 41 | 42 | # 43 | # Verify that a client request without an access token fails with a 401 44 | # 45 | echo '2. Testing API request without an access token ...' 46 | HTTP_STATUS=$(curl -i -s -X GET "$API_URL" \ 47 | -o $RESPONSE_FILE -w '%{http_code}') 48 | if [ "$HTTP_STATUS" != '401' ]; then 49 | echo "*** API request without valid access token failed, status: $HTTP_STATUS" 50 | exit 51 | fi 52 | echo '2. API request received 401 when no valid access token was sent' 53 | 54 | # 55 | # Verify that a client request with an invalid access token fails with a 401 56 | # 57 | INVALID_ACCESS_TOKEN='42665300-efe8-419d-be52-07b53e208f46' 58 | echo '3. Testing API request with an invalid access token ...' 59 | HTTP_STATUS=$(curl -i -s -X GET "$API_URL" \ 60 | -H "Authorization: Bearer $INVALID_ACCESS_TOKEN" \ 61 | -o $RESPONSE_FILE -w '%{http_code}') 62 | if [ "$HTTP_STATUS" != '401' ]; then 63 | echo "*** API request with invalid access token failed, status: $HTTP_STATUS" 64 | exit 65 | fi 66 | echo '3. API request received 401 when an invalid access token was sent' 67 | 68 | # 69 | # Verify that a client request with a valid access token returns 200 70 | # 71 | echo '4. Testing initial API request with a valid access token ...' 72 | HTTP_STATUS=$(curl -i -s -X GET "$API_URL" \ 73 | -H "Authorization: Bearer $OPAQUE_ACCESS_TOKEN" \ 74 | -o $RESPONSE_FILE -w '%{http_code}') 75 | if [ "$HTTP_STATUS" != '200' ]; then 76 | echo "*** API request with a valid access token failed, status: $HTTP_STATUS" 77 | exit 78 | fi 79 | echo '4. Initial API request received a valid API response' 80 | 81 | # 82 | # Verify that a client request with a valid access token returns 200 when served from the cache 83 | # 84 | echo '5. Testing second GET request with a valid access token ...' 85 | HTTP_STATUS=$(curl -i -s -X GET "$API_URL" \ 86 | -H "Authorization: Bearer $OPAQUE_ACCESS_TOKEN" \ 87 | -o $RESPONSE_FILE -w '%{http_code}') 88 | if [ "$HTTP_STATUS" != '200' ]; then 89 | echo "*** API request with a valid access token failed, status: $HTTP_STATUS" 90 | exit 91 | fi 92 | echo '5. Second API request received a valid API response' 93 | -------------------------------------------------------------------------------- /images/phantom-token-pattern.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/curityio/nginx-lua-phantom-token-plugin/f38fa5613832856de1400df510f7566d0c4f6098/images/phantom-token-pattern.png -------------------------------------------------------------------------------- /kong-phantom-token-2.0.1-1.rockspec: -------------------------------------------------------------------------------- 1 | package = "kong-phantom-token" 2 | version = "2.0.1-1" 3 | source = { 4 | url = "git://github.com/curityio/nginx-lua-phantom-token-plugin", 5 | tag = "v2.0.1" 6 | } 7 | description = { 8 | summary = "A Lua plugin used during API requests to exchange an opaque reference token for a JWT access token", 9 | homepage = "https://curity.io/product/token-service/?=tabgroup=1?tab=microservices", 10 | license = "Apache 2.0", 11 | detailed = [[ 12 | The Curity Phantom Token plugin is a Lua library used to forward JWT access tokens to APIs. 13 | It can be used with the Kong API Gateway, including the open source version. 14 | The Identity Server issues opaque tokens to internet clients and stores the JWT access tokens. 15 | This is a privacy preserving pattern to ensure that no sensitive token related information is revealed. 16 | During API requests the plugin introspects the opaque token to get the JWT. 17 | The JWT access token is then forwarded to the API using the HTTP Authorization header. 18 | All of this keeps plumbing out of APIs, so that they are able to use simple authorization code. 19 | ]], 20 | summary = "A Lua plugin to receive incoming opaque tokens and forward JWT access tokens to APIs" 21 | } 22 | dependencies = { 23 | "lua >= 5.1", 24 | "lua-resty-http >= 0.16.1-0", 25 | "lua-resty-jwt >= 0.2.3-0" 26 | } 27 | build = { 28 | type = "builtin", 29 | modules = { 30 | ["kong.plugins.phantom-token.access"] = "plugin/access.lua", 31 | ["kong.plugins.phantom-token.handler"] = "plugin/handler.lua", 32 | ["kong.plugins.phantom-token.schema"] = "plugin/schema.lua" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /lua-resty-phantom-token-2.0.1-1.rockspec: -------------------------------------------------------------------------------- 1 | package = "lua-resty-phantom-token" 2 | version = "2.0.1-1" 3 | source = { 4 | url = "git://github.com/curityio/nginx-lua-phantom-token-plugin", 5 | tag = "v2.0.1" 6 | } 7 | description = { 8 | summary = "A Lua plugin used during API requests to exchange an opaque reference token for a JWT access token", 9 | homepage = "https://curity.io/product/token-service/?=tabgroup=1?tab=microservices", 10 | license = "Apache 2.0", 11 | detailed = [[ 12 | The Curity Phantom Token plugin is a Lua library used to forward JWT access tokens to APIs. 13 | It can be used with NGINX based systems with the Lua module enabled, such as OpenResty. 14 | The Identity Server issues opaque tokens to internet clients and stores the JWT access tokens. 15 | This is a privacy preserving pattern to ensure that no sensitive token related information is revealed. 16 | During API requests the plugin introspects the opaque token to get the JWT. 17 | The JWT access token is then forwarded to the API using the HTTP Authorization header. 18 | All of this keeps plumbing out of APIs, so that they are able to use simple authorization code. 19 | ]], 20 | summary = "A Lua plugin to receive incoming opaque tokens and forward JWT access tokens to APIs" 21 | } 22 | dependencies = { 23 | "lua >= 5.1", 24 | "lua-resty-http >= 0.16.1-0", 25 | "lua-resty-jwt >= 0.2.3-0" 26 | } 27 | build = { 28 | type = "builtin", 29 | modules = { 30 | ["resty.phantom-token"] = "plugin/access.lua" 31 | } 32 | } -------------------------------------------------------------------------------- /plugin/access.lua: -------------------------------------------------------------------------------- 1 | -- 2 | -- A Lua module to handle swapping opaque access tokens for JWTs 3 | -- 4 | 5 | local _M = {} 6 | local http = require "resty.http" 7 | local jwt = require 'resty.jwt' 8 | 9 | -- 10 | -- Get values into an array that can be iterated multiple times 11 | -- 12 | local function iterator_to_array(iterator) 13 | 14 | local i = 1; 15 | local array = {}; 16 | 17 | for item in iterator do 18 | array[i] = item; 19 | i = i + 1 20 | end 21 | 22 | return array 23 | end 24 | 25 | -- 26 | -- A utility for finding an item in an array 27 | -- 28 | local function array_has_value(arr, val) 29 | 30 | for _, item in ipairs(arr) do 31 | if val == item then 32 | return true 33 | end 34 | end 35 | 36 | return false 37 | end 38 | 39 | -- 40 | -- Verify configuration and set defaults that are the same for all requests 41 | -- 42 | local function initialize_configuration(config) 43 | 44 | if config == nil or 45 | config.introspection_endpoint == nil or 46 | config.client_id == nil or 47 | config.client_secret == nil then 48 | ngx.log(ngx.WARN, 'The phantom token configuration is invalid and must be corrected') 49 | return false 50 | end 51 | 52 | if config.token_cache_seconds == nil or config.token_cache_seconds <= 0 then 53 | config.token_cache_seconds = 300 54 | end 55 | if config.scope == nil then 56 | config.scope = '' 57 | end 58 | if config.verify_ssl == nil then 59 | config.verify_ssl = true 60 | end 61 | 62 | return true 63 | end 64 | 65 | -- 66 | -- Return errors due to invalid tokens or introspection technical problems 67 | -- 68 | local function error_response(status, code, message) 69 | 70 | local method = ngx.req.get_method():upper() 71 | if method ~= 'HEAD' then 72 | 73 | ngx.status = status 74 | ngx.header['content-type'] = 'application/json' 75 | if status == 401 then 76 | ngx.header['WWW-Authenticate'] = 'Bearer' 77 | end 78 | 79 | local jsonData = '{"code":"' .. code .. '","message":"' .. message .. '"}' 80 | ngx.say(jsonData) 81 | end 82 | 83 | ngx.exit(status) 84 | end 85 | 86 | -- 87 | -- Return a generic message for all three of these error categories 88 | -- 89 | local function unauthorized_error_response() 90 | error_response(ngx.HTTP_UNAUTHORIZED, 'unauthorized', 'Missing, invalid or expired access token') 91 | end 92 | 93 | local function server_error_response(config) 94 | error_response(ngx.HTTP_INTERNAL_SERVER_ERROR, 'server_error', 'Problem encountered processing the request') 95 | end 96 | 97 | -- 98 | -- Introspect the access token 99 | -- 100 | local function introspect_access_token(access_token, config) 101 | 102 | local httpc = http:new() 103 | local clientCredential = config.client_id .. ':' .. config.client_secret 104 | local authorizationHeader = 'Basic ' .. ngx.encode_base64(clientCredential) 105 | local result, error = httpc:request_uri(config.introspection_endpoint, { 106 | method = 'POST', 107 | body = 'token=' .. access_token, 108 | headers = { 109 | ['authorization'] = authorizationHeader, 110 | ['content-type'] = 'application/x-www-form-urlencoded', 111 | ['accept'] = 'application/jwt' 112 | }, 113 | ssl_verify = config.verify_ssl 114 | }) 115 | 116 | if error then 117 | local connectionMessage = 'A technical problem occurred during access token introspection: ' 118 | ngx.log(ngx.WARN, connectionMessage .. error) 119 | return { status = 500 } 120 | end 121 | 122 | if not result then 123 | return { status = 500 } 124 | end 125 | 126 | if result.status ~= 200 then 127 | return { status = result.status } 128 | end 129 | 130 | -- Get the time to cache from the cache-control header's max-age value 131 | local expiry = 0 132 | if result.headers then 133 | local cacheHeader = result.headers['cache-control'] 134 | if cacheHeader then 135 | local _, _, expiryMatch = string.find(cacheHeader, "max.-age=(%d+)") 136 | if expiryMatch then 137 | expiry = tonumber(expiryMatch) 138 | end 139 | end 140 | end 141 | 142 | return { status = result.status, jwt = result.body, expiry = expiry } 143 | end 144 | 145 | -- 146 | -- Optionally check scopes configured for a location 147 | -- 148 | local function verify_scope(jwt_text, required_scope) 149 | 150 | if required_scope == nil then 151 | return true 152 | end 153 | 154 | local data = jwt:load_jwt(jwt_text, nil) 155 | if not data.valid then 156 | local details = 'Unable to parse JWT access token' 157 | if data.reason then 158 | details = details .. ': ' .. data.reason 159 | end 160 | ngx.log(ngx.WARN, details) 161 | return false 162 | end 163 | 164 | if not data.payload.scope then 165 | return false 166 | end 167 | 168 | local required_scope_parts = string.gmatch(required_scope, "%S+") 169 | local actual_scope_parts = iterator_to_array(string.gmatch(data.payload.scope, "%S+")) 170 | 171 | for required_value in required_scope_parts do 172 | if not array_has_value(actual_scope_parts, required_value) then 173 | ngx.log(ngx.WARN, 'The required scope ' .. required_value .. ' was not found in the received access token') 174 | return false 175 | end 176 | end 177 | 178 | return true 179 | end 180 | 181 | -- 182 | -- Get the token from the cache or introspect it 183 | -- 184 | local function verify_access_token(access_token, config) 185 | 186 | local result = { status = 401 } 187 | 188 | -- See if there is an introspection result in the cache 189 | local dict = ngx.shared['phantom-token'] 190 | local existing_jwt = dict:get(access_token) 191 | if existing_jwt then 192 | 193 | -- Return cached introspection results for the same token 194 | result = { status = 200, jwt = existing_jwt } 195 | 196 | else 197 | 198 | -- Otherwise introspect the opaque access token 199 | result = introspect_access_token(access_token, config) 200 | if result.status == 200 then 201 | 202 | local time_to_live = config.token_cache_seconds 203 | if result.expiry > 0 and result.expiry < config.token_cache_seconds then 204 | time_to_live = result.expiry 205 | end 206 | 207 | -- Cache the result so that introspection is efficient under load 208 | -- The opaque access token is already a unique string similar to a GUID so use it as a cache key 209 | -- The cache is atomic and thread safe so is safe to use across concurrent requests 210 | -- The expiry value is a number of seconds from the current time 211 | -- https://github.com/openresty/lua-nginx-module#ngxshareddictset 212 | dict:set(access_token, result.jwt, time_to_live) 213 | end 214 | end 215 | 216 | -- If configured, verify the scope on every request in a zero-trust manner 217 | if result.status == 200 then 218 | if not verify_scope(result.jwt, config.scope) then 219 | result = { status = 403 } 220 | end 221 | end 222 | 223 | return result 224 | end 225 | 226 | -- 227 | -- The public entry point to introspect the token then forward the JWT to the API 228 | -- 229 | function _M.run(config) 230 | 231 | -- Start by validating configuration 232 | if initialize_configuration(config) == false then 233 | server_error_response(config) 234 | return 235 | end 236 | 237 | if ngx.req.get_method() == 'OPTIONS' then 238 | return 239 | end 240 | 241 | local auth_header = ngx.req.get_headers()['Authorization'] 242 | if auth_header and string.len(auth_header) > 7 and string.lower(string.sub(auth_header, 1, 7)) == 'bearer ' then 243 | 244 | local access_token_untrimmed = string.sub(auth_header, 8) 245 | local access_token = string.gsub(access_token_untrimmed, "%s+", "") 246 | local result = verify_access_token(access_token, config) 247 | 248 | if result.status == 500 then 249 | error_response(ngx.HTTP_INTERNAL_SERVER_ERROR, 'server_error', 'Problem encountered authorizing the HTTP request') 250 | end 251 | 252 | if result.status == 403 then 253 | error_response(ngx.HTTP_FORBIDDEN, 'forbidden', 'The token does not contain the required scope') 254 | end 255 | 256 | if result.status ~= 200 then 257 | ngx.log(ngx.WARN, 'Received a ' .. result.status .. ' introspection response due to the access token being invalid or expired') 258 | unauthorized_error_response() 259 | end 260 | 261 | ngx.req.set_header('Authorization', 'Bearer ' .. result.jwt) 262 | else 263 | 264 | ngx.log(ngx.WARN, 'No valid access token was found in the HTTP Authorization header') 265 | unauthorized_error_response() 266 | end 267 | end 268 | 269 | return _M 270 | -------------------------------------------------------------------------------- /plugin/handler.lua: -------------------------------------------------------------------------------- 1 | -- 2 | -- The Kong entry point handler 3 | -- 4 | 5 | local access = require "kong.plugins.phantom-token.access" 6 | 7 | -- See https://github.com/Kong/kong/discussions/7193 for more about the PRIORITY field 8 | local PhantomToken = { 9 | PRIORITY = 1000, 10 | VERSION = "2.0.1", 11 | } 12 | 13 | function PhantomToken:access(conf) 14 | access.run(conf) 15 | end 16 | 17 | return PhantomToken 18 | -------------------------------------------------------------------------------- /plugin/schema.lua: -------------------------------------------------------------------------------- 1 | return { 2 | name = "phantom-token", 3 | fields = {{ 4 | config = { 5 | type = "record", 6 | fields = { 7 | { introspection_endpoint = { type = "string", required = true } }, 8 | { client_id = { type = "string", required = true } }, 9 | { client_secret = { type = "string", required = true } }, 10 | { token_cache_seconds = { type = "number", required = true, default = 300 } }, 11 | { scope = { type = "string", required = false } }, 12 | { verify_ssl = { type = "boolean", required = true, default = true } } 13 | } 14 | }} 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /t/advanced_routing.t: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl 2 | 3 | ################################################################################################### 4 | # Runs tests focused on sending an access token and receiving the expected success or error results 5 | ################################################################################################### 6 | 7 | use strict; 8 | use warnings; 9 | use FindBin; 10 | use lib "$FindBin::Bin/lib"; 11 | use Test::Nginx::Socket 'no_plan'; 12 | 13 | SKIP: { 14 | our $token = &get_token_from_idsvr(); 15 | if ($token) { 16 | run_tests(); 17 | } 18 | else { 19 | fail("Could not get token from idsvr"); 20 | } 21 | } 22 | 23 | sub get_token_from_idsvr { 24 | use LWP::UserAgent; 25 | 26 | my $ua = LWP::UserAgent->new(); 27 | 28 | my $response = $ua->post("http://localhost:8443/oauth/v2/oauth-token", { 29 | "client_id" => "test-client", 30 | "client_secret" => "secret1", 31 | "grant_type" => "client_credentials", 32 | "scope" => "read" 33 | }); 34 | my $content = $response->decoded_content(); 35 | 36 | my ($result) = $content =~ /access_token":"([^"]+)/; 37 | 38 | return $result; 39 | } 40 | 41 | __DATA__ 42 | 43 | === TEST_ADVANCED_ROUTING_1: The plugin can be bypassed for special token patterns 44 | ########################################################################################################## 45 | # The plugin can be bypassed when there is special JWT configuration and the access token is already a JWT 46 | ########################################################################################################## 47 | 48 | --- http_config 49 | lua_shared_dict phantom-token 10m; 50 | map $http_authorization $loc { 51 | ~^[Bb]earer\s*[A-Za-z0-9-_]*\.[A-Za-z0-9-_]*\.[A-Za-z0-9-_]* loc_bypass; 52 | default loc_phantom_token; 53 | } 54 | 55 | --- config 56 | location /t { 57 | try_files $uri @$loc; 58 | } 59 | location @loc_bypass { 60 | add_header 'x-custom' 'bypass'; 61 | return 200; 62 | } 63 | location @loc_phantom_token { 64 | 65 | rewrite_by_lua_block { 66 | 67 | local config = { 68 | introspection_endpoint = 'http://127.0.0.1:8443/oauth/v2/oauth-introspect', 69 | client_id = 'introspection-client', 70 | client_secret = 'secret2', 71 | token_cache_seconds = 900 72 | } 73 | 74 | local phantomToken = require 'phantom-token' 75 | phantomToken.run(config) 76 | } 77 | 78 | add_header 'x-custom' 'phantom-token'; 79 | return 200; 80 | } 81 | 82 | --- error_code: 200 83 | 84 | --- request 85 | GET /t 86 | 87 | --- more_headers eval 88 | "Authorization: bearer eyJraWQiOiItMjAyMDIxMjM3MyIsIng1dCI6IktwdlhqdEJQeWx3RHNENWtWLTN1bkVzQXozRSIsImFsZyI6IlJTMjU2In0.eyJqdGkiOiI2Y2U0ZGE1Mi1hMTE3LTQ3MGQtYWQ5Yi0wNmVjZTcyZDA5ZGIiLCJkZWxlZ2F0aW9uSWQiOiJlMGMwYTY5MS0zZmJmLTRmN2QtYWU3Mi0zYzk2NTU3ZDI0YTAiLCJleHAiOjE2NjYwOTM0MDAsIm5iZiI6MTY2NjA5MzEwMCwic2NvcGUiOiJyZWFkIiwiaXNzIjoiaHR0cDovL2N1cml0eXNlcnZlcjo4NDQzL29hdXRoL3YyL29hdXRoLWFub255bW91cyIsInN1YiI6InRlc3QtY2xpZW50IiwiYXVkIjoidGVzdC1jbGllbnQiLCJpYXQiOjE2NjYwOTMxMDAsInB1cnBvc2UiOiJhY2Nlc3NfdG9rZW4ifQ.Iaug4CDO3T9xirPU0pmq1YVdf9CR_6iCtxDpW7BRMCFW9jO4HCdsAj9kE-Ncbk26b5_l4QdD5g0nd36iXNoPIaHPO6TYb9T-PBcuSqc7WkgK_RT-BNeNmE8RRWU47dd8JFmMLgmgnWMYeEhi8kyKFScJ4hj6-H-KqDwjszWSPH_YTYPHp69C8mu_qNWLfaP8KBPizdYO8_6vxfOkDMDEbK6KbfaLAuWjfh9MzAD7j6POQz2NXy8F3KT79X49_nIjkjjE5Nq7vzTS910XlSMRvG0kQ0-LhEYH__GqxDMC4musv6th5s929dc7FyA4zRrR8njomwk166ItmO2Y_sEZeA" 89 | 90 | --- response_headers eval 91 | "x-custom: bypass" 92 | 93 | === TEST_ADVANCED_ROUTING_2: The plugin is run when special token patterns do not result in a match 94 | ################################################################################################################## 95 | # The plugin falls back to introspection when there is special JWT configuration and the access token is not a JWT 96 | ################################################################################################################## 97 | 98 | --- http_config 99 | lua_shared_dict phantom-token 10m; 100 | map $http_authorization $loc { 101 | ~^[Bb]earer\s*[A-Za-z0-9-_]*\.[A-Za-z0-9-_]*\.[A-Za-z0-9-_]* loc_bypass; 102 | default loc_phantom_token; 103 | } 104 | 105 | --- config 106 | location /t { 107 | try_files $uri @$loc; 108 | } 109 | location @loc_bypass { 110 | add_header 'x-custom' 'bypass'; 111 | return 200; 112 | } 113 | location @loc_phantom_token { 114 | 115 | rewrite_by_lua_block { 116 | 117 | local config = { 118 | introspection_endpoint = 'http://127.0.0.1:8443/oauth/v2/oauth-introspect', 119 | client_id = 'introspection-client', 120 | client_secret = 'secret2', 121 | token_cache_seconds = 900 122 | } 123 | 124 | local phantomToken = require 'phantom-token' 125 | phantomToken.run(config) 126 | } 127 | 128 | add_header 'x-custom' 'phantom-token'; 129 | return 200; 130 | } 131 | 132 | --- error_code: 200 133 | 134 | --- request 135 | GET /t 136 | 137 | --- more_headers eval 138 | "Authorization: bearer " . $main::token; 139 | 140 | --- response_headers eval 141 | "x-custom: phantom-token" 142 | -------------------------------------------------------------------------------- /t/api_requests.t: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl 2 | 3 | ################################################################################################### 4 | # Runs tests focused on sending an access token and receiving the expected success or error results 5 | ################################################################################################### 6 | 7 | use strict; 8 | use warnings; 9 | use FindBin; 10 | use lib "$FindBin::Bin/lib"; 11 | use Test::Nginx::Socket 'no_plan'; 12 | 13 | SKIP: { 14 | our $token = &get_token_from_idsvr(); 15 | if ($token) { 16 | run_tests(); 17 | } 18 | else { 19 | fail("Could not get token from idsvr"); 20 | } 21 | } 22 | 23 | sub get_token_from_idsvr { 24 | use LWP::UserAgent; 25 | 26 | my $ua = LWP::UserAgent->new(); 27 | 28 | my $response = $ua->post("http://localhost:8443/oauth/v2/oauth-token", { 29 | "client_id" => "test-client", 30 | "client_secret" => "secret1", 31 | "grant_type" => "client_credentials", 32 | "scope" => "read" 33 | }); 34 | my $content = $response->decoded_content(); 35 | 36 | my ($result) = $content =~ /access_token":"([^"]+)/; 37 | 38 | return $result; 39 | } 40 | 41 | __DATA__ 42 | 43 | === TEST_API_REQUEST_1: An opaque token can be introspected for a phantom token 44 | ################################## 45 | # The happy case works as expected 46 | ################################## 47 | 48 | --- http_config 49 | lua_shared_dict phantom-token 10m; 50 | 51 | --- config 52 | location /t { 53 | 54 | rewrite_by_lua_block { 55 | 56 | local config = { 57 | introspection_endpoint = 'http://127.0.0.1:8443/oauth/v2/oauth-introspect', 58 | client_id = 'introspection-client', 59 | client_secret = 'secret2', 60 | token_cache_seconds = 900 61 | } 62 | 63 | local phantomToken = require 'phantom-token' 64 | phantomToken.run(config) 65 | } 66 | 67 | proxy_pass http://127.0.0.1:1984/target; 68 | } 69 | location /target { 70 | add_header 'authorization' $http_authorization; 71 | return 200; 72 | } 73 | 74 | --- error_code: 200 75 | 76 | --- request 77 | GET /t 78 | 79 | --- more_headers eval 80 | "Authorization: bearer " . $main::token; 81 | 82 | --- response_headers_like 83 | authorization: Bearer ey.* 84 | 85 | === TEST_API_REQUEST_2: Sending an invalid token that fails introspection results in an access denied error 86 | ########################################################################### 87 | # An unrecognised token is rejected when the Authorization Server is called 88 | ########################################################################### 89 | 90 | --- http_config 91 | lua_shared_dict phantom-token 10m; 92 | 93 | --- config 94 | location /t { 95 | 96 | rewrite_by_lua_block { 97 | 98 | local config = { 99 | introspection_endpoint = 'http://127.0.0.1:8443/oauth/v2/oauth-introspect', 100 | client_id = 'introspection-client', 101 | client_secret = 'secret2', 102 | token_cache_seconds = 900 103 | } 104 | 105 | local phantomToken = require 'phantom-token' 106 | phantomToken.run(config) 107 | } 108 | } 109 | 110 | --- more_headers 111 | Authorization: bearer zort 112 | 113 | --- request 114 | GET /t 115 | 116 | --- error_code: 401 117 | 118 | --- response_headers 119 | content-type: application/json 120 | WWW-Authenticate: Bearer 121 | 122 | --- response_body_like chomp 123 | {"code":"unauthorized","message":"Missing, invalid or expired access token"} 124 | 125 | === TEST_API_REQUEST_3: Sending no authorization header results in an access denied error 126 | ################################################# 127 | # A missing token is rejected by the plugin logic 128 | ################################################# 129 | 130 | --- http_config 131 | lua_shared_dict phantom-token 10m; 132 | 133 | --- config 134 | location /t { 135 | 136 | rewrite_by_lua_block { 137 | 138 | local config = { 139 | introspection_endpoint = 'http://127.0.0.1:8443/oauth/v2/oauth-introspect', 140 | client_id = 'introspection-client', 141 | client_secret = 'secret2', 142 | token_cache_seconds = 900 143 | } 144 | 145 | local phantomToken = require 'phantom-token' 146 | phantomToken.run(config) 147 | } 148 | } 149 | 150 | --- request 151 | GET /t 152 | 153 | --- error_code: 401 154 | 155 | --- response_headers 156 | content-type: application/json 157 | WWW-Authenticate: Bearer 158 | 159 | --- response_body_like chomp 160 | {"code":"unauthorized","message":"Missing, invalid or expired access token"} 161 | 162 | === TEST_API_REQUEST_4: The wrong authorization scheme results in an access denied error 163 | ############################################################## 164 | # A token supplied incorrectly is not read by the plugin logic 165 | ############################################################## 166 | 167 | --- http_config 168 | lua_shared_dict phantom-token 10m; 169 | 170 | --- config 171 | location /t { 172 | 173 | rewrite_by_lua_block { 174 | 175 | local config = { 176 | introspection_endpoint = 'http://127.0.0.1:8443/oauth/v2/oauth-introspect', 177 | client_id = 'introspection-client', 178 | client_secret = 'secret2', 179 | token_cache_seconds = 900 180 | } 181 | 182 | local phantomToken = require 'phantom-token' 183 | phantomToken.run(config) 184 | } 185 | } 186 | 187 | --- more_headers eval 188 | "Authorization: basic " . $main::token; 189 | 190 | --- request 191 | GET /t 192 | 193 | --- error_code: 401 194 | 195 | --- response_headers 196 | content-type: application/json 197 | WWW-Authenticate: Bearer 198 | 199 | --- response_body_like chomp 200 | {"code":"unauthorized","message":"Missing, invalid or expired access token"} 201 | 202 | === TEST_API_REQUEST_5: A valid token with trash after results in an access denied error 203 | ###################################################################################### 204 | # A valid token appended with other characters is rejected by the Authorization Server 205 | ###################################################################################### 206 | 207 | --- http_config 208 | lua_shared_dict phantom-token 10m; 209 | 210 | --- config 211 | location /t { 212 | 213 | rewrite_by_lua_block { 214 | 215 | local config = { 216 | introspection_endpoint = 'http://127.0.0.1:8443/oauth/v2/oauth-introspect', 217 | client_id = 'introspection-client', 218 | client_secret = 'secret2', 219 | token_cache_seconds = 900 220 | } 221 | 222 | local phantomToken = require 'phantom-token' 223 | phantomToken.run(config) 224 | } 225 | } 226 | 227 | --- more_headers eval 228 | "Authorization: bearer " . $main::token . "z"; 229 | 230 | --- request 231 | GET /t 232 | 233 | --- error_code: 401 234 | 235 | --- response_headers 236 | content-type: application/json 237 | WWW-Authenticate: Bearer 238 | 239 | --- response_body_like chomp 240 | {"code":"unauthorized","message":"Missing, invalid or expired access token"} 241 | 242 | === TEST_API_REQUEST_6: The bearer HTTP method can be in upper case 243 | ##################################################### 244 | # The plugin logic correctly handles case differences 245 | ##################################################### 246 | 247 | --- http_config 248 | lua_shared_dict phantom-token 10m; 249 | 250 | --- config 251 | location /t { 252 | 253 | rewrite_by_lua_block { 254 | 255 | local config = { 256 | introspection_endpoint = 'http://127.0.0.1:8443/oauth/v2/oauth-introspect', 257 | client_id = 'introspection-client', 258 | client_secret = 'secret2', 259 | token_cache_seconds = 900 260 | } 261 | 262 | local phantomToken = require 'phantom-token' 263 | phantomToken.run(config) 264 | } 265 | 266 | proxy_pass http://127.0.0.1:1984/target; 267 | } 268 | location /target { 269 | add_header 'authorization' $http_authorization; 270 | return 200; 271 | } 272 | 273 | --- error_code: 200 274 | 275 | --- request 276 | GET /t 277 | 278 | --- more_headers eval 279 | "Authorization: BEARER " . $main::token; 280 | 281 | --- response_headers_like 282 | authorization: Bearer ey.* 283 | 284 | === TEST_API_REQUEST_7: The bearer HTTP method can be in mixed case 285 | ##################################################### 286 | # The plugin logic correctly handles case differences 287 | ##################################################### 288 | 289 | --- http_config 290 | lua_shared_dict phantom-token 10m; 291 | 292 | --- config 293 | location /t { 294 | 295 | rewrite_by_lua_block { 296 | 297 | local config = { 298 | introspection_endpoint = 'http://127.0.0.1:8443/oauth/v2/oauth-introspect', 299 | client_id = 'introspection-client', 300 | client_secret = 'secret2', 301 | token_cache_seconds = 900 302 | } 303 | 304 | local phantomToken = require 'phantom-token' 305 | phantomToken.run(config) 306 | } 307 | 308 | proxy_pass http://127.0.0.1:1984/target; 309 | } 310 | location /target { 311 | add_header 'authorization' $http_authorization; 312 | return 200; 313 | } 314 | 315 | --- error_code: 200 316 | 317 | --- request 318 | GET /t 319 | 320 | --- more_headers eval 321 | "Authorization: bEaReR " . $main::token; 322 | 323 | --- response_headers_like 324 | authorization: Bearer ey.* 325 | 326 | === TEST_API_REQUEST_8: The bearer HTTP method can have > 1 space before it 327 | ################################################ 328 | # The plugin logic correctly handles white space 329 | ################################################ 330 | 331 | --- http_config 332 | lua_shared_dict phantom-token 10m; 333 | 334 | --- config 335 | location /t { 336 | 337 | rewrite_by_lua_block { 338 | 339 | local config = { 340 | introspection_endpoint = 'http://127.0.0.1:8443/oauth/v2/oauth-introspect', 341 | client_id = 'introspection-client', 342 | client_secret = 'secret2', 343 | token_cache_seconds = 900 344 | } 345 | 346 | local phantomToken = require 'phantom-token' 347 | phantomToken.run(config) 348 | } 349 | 350 | proxy_pass http://127.0.0.1:1984/target; 351 | } 352 | location /target { 353 | add_header 'authorization' $http_authorization; 354 | return 200; 355 | } 356 | 357 | --- error_code: 200 358 | 359 | --- request 360 | GET /t 361 | 362 | --- more_headers eval 363 | "Authorization: bearer " . $main::token 364 | 365 | --- response_headers_like 366 | authorization: Bearer ey.* 367 | -------------------------------------------------------------------------------- /t/config.t: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl 2 | 3 | ############################################################################## 4 | # Runs tests focused on detecting invalid configuration or defaulting settings 5 | ############################################################################## 6 | 7 | use strict; 8 | use warnings; 9 | use FindBin; 10 | use lib "$FindBin::Bin/lib"; 11 | use Test::Nginx::Socket 'no_plan'; 12 | 13 | SKIP: { 14 | our $token = &get_token_from_idsvr(); 15 | if ($token) { 16 | run_tests(); 17 | } 18 | else { 19 | fail("Could not get token from idsvr"); 20 | } 21 | } 22 | 23 | sub get_token_from_idsvr { 24 | use LWP::UserAgent; 25 | 26 | my $ua = LWP::UserAgent->new(); 27 | 28 | my $response = $ua->post("http://localhost:8443/oauth/v2/oauth-token", { 29 | "client_id" => "test-client", 30 | "client_secret" => "secret1", 31 | "grant_type" => "client_credentials", 32 | "scope" => "read" 33 | }); 34 | my $content = $response->decoded_content(); 35 | 36 | my ($result) = $content =~ /access_token":"([^"]+)/; 37 | 38 | return $result; 39 | } 40 | 41 | __DATA__ 42 | 43 | === TEST_CONFIG_1: Missing required properties return a 500 error 44 | ################################################################### 45 | # When no introspection endpoint is configured there is a 500 error 46 | ################################################################### 47 | 48 | --- http_config 49 | lua_shared_dict phantom-token 10m; 50 | 51 | --- config 52 | location /t { 53 | 54 | rewrite_by_lua_block { 55 | 56 | local config = { 57 | client_id = 'introspection-client', 58 | client_secret = 'secret2', 59 | token_cache_seconds = 900 60 | } 61 | 62 | local phantomToken = require 'phantom-token' 63 | phantomToken.run(config) 64 | } 65 | } 66 | 67 | --- error_code: 500 68 | 69 | --- request 70 | GET /t 71 | 72 | --- more_headers eval 73 | "Authorization: bearer " . $main::token; 74 | 75 | --- error_log 76 | The phantom token configuration is invalid and must be corrected 77 | 78 | --- response_body_like chomp 79 | {"code":"server_error","message":"Problem encountered processing the request"} 80 | 81 | === TEST_CONFIG_2: Missing required properties return a 500 error 82 | ################################################################### 83 | # When no introspection endpoint is configured there is a 500 error 84 | ################################################################### 85 | 86 | --- config 87 | location /t { 88 | 89 | rewrite_by_lua_block { 90 | 91 | local config = { 92 | client_id = 'introspection-client', 93 | client_secret = 'secret2', 94 | token_cache_seconds = 900 95 | } 96 | 97 | local phantomToken = require 'phantom-token' 98 | phantomToken.run(config) 99 | } 100 | } 101 | 102 | --- error_code: 500 103 | 104 | --- request 105 | GET /t 106 | 107 | --- more_headers eval 108 | "Authorization: bearer " . $main::token; 109 | 110 | --- error_log 111 | The phantom token configuration is invalid and must be corrected 112 | 113 | --- response_body_like chomp 114 | {"code":"server_error","message":"Problem encountered processing the request"} 115 | 116 | === TEST CONFIG_2: A deployment with missing data does not crash NGINX 117 | ####################################################################################################### 118 | # Verify that empty configuration is handled in a controlled manner rather than causing server problems 119 | ####################################################################################################### 120 | 121 | --- config 122 | location /t { 123 | 124 | rewrite_by_lua_block { 125 | 126 | local config = { 127 | } 128 | 129 | local phantomToken = require 'phantom-token' 130 | phantomToken.run(config) 131 | } 132 | } 133 | 134 | --- error_code: 500 135 | 136 | --- request 137 | GET /t 138 | 139 | --- more_headers eval 140 | "Authorization: bearer " . $main::token; 141 | 142 | --- error_log 143 | The phantom token configuration is invalid and must be corrected 144 | 145 | --- response_body_like chomp 146 | {"code":"server_error","message":"Problem encountered processing the request"} 147 | 148 | === TEST CONFIG_3: A deployment with null data does not crash NGINX 149 | ####################################################################################################### 150 | # Verify that null configuration is handled in a controlled manner rather than causing server problems 151 | ####################################################################################################### 152 | 153 | --- config 154 | location /t { 155 | 156 | rewrite_by_lua_block { 157 | 158 | local phantomToken = require 'phantom-token' 159 | phantomToken.run() 160 | } 161 | } 162 | 163 | --- error_code: 500 164 | 165 | --- request 166 | GET /t 167 | 168 | --- more_headers eval 169 | "Authorization: bearer " . $main::token; 170 | 171 | --- error_log 172 | The phantom token configuration is invalid and must be corrected 173 | 174 | --- response_body_like chomp 175 | {"code":"server_error","message":"Problem encountered processing the request"} 176 | 177 | === TEST CONFIG_4: A deployment with a misspelt field does not crash NGINX 178 | ##################################################################################################### 179 | # Verify that bad configuration is handled in a controlled manner rather than causing server problems 180 | ##################################################################################################### 181 | 182 | --- config 183 | location /t { 184 | 185 | rewrite_by_lua_block { 186 | 187 | local config = { 188 | introspectionn_endpoint = 'http://127.0.0.1:8443/oauth/v2/oauth-introspect', 189 | client_id = 'introspection-client', 190 | client_secret = 'secret2', 191 | token_cache_seconds = 900 192 | } 193 | 194 | local phantomToken = require 'phantom-token' 195 | phantomToken.run(config) 196 | } 197 | } 198 | 199 | --- error_code: 500 200 | 201 | --- request 202 | GET /t 203 | 204 | --- more_headers eval 205 | "Authorization: bearer " . $main::token; 206 | 207 | --- error_log 208 | The phantom token configuration is invalid and must be corrected 209 | 210 | --- response_body_like chomp 211 | {"code":"server_error","message":"Problem encountered processing the request"} 212 | 213 | === TEST_CONFIG_5: A deployment with all optional fields are omitted successfully introspects tokens 214 | ####################################################################### 215 | # The happy case works as expected when all optional fields are omitted 216 | ####################################################################### 217 | 218 | --- http_config 219 | lua_shared_dict phantom-token 10m; 220 | 221 | --- config 222 | location /t { 223 | 224 | rewrite_by_lua_block { 225 | 226 | local config = { 227 | introspection_endpoint = 'http://127.0.0.1:8443/oauth/v2/oauth-introspect', 228 | client_id = 'introspection-client', 229 | client_secret = 'secret2' 230 | } 231 | 232 | local phantomToken = require 'phantom-token' 233 | phantomToken.run(config) 234 | } 235 | 236 | proxy_pass http://127.0.0.1:1984/target; 237 | } 238 | location /target { 239 | add_header 'authorization' $http_authorization; 240 | return 200; 241 | } 242 | 243 | --- error_code: 200 244 | 245 | --- request 246 | GET /t 247 | 248 | --- more_headers eval 249 | "Authorization: bearer " . $main::token; 250 | 251 | --- response_headers_like 252 | authorization: Bearer ey.* -------------------------------------------------------------------------------- /t/failure_scenarios.t: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl 2 | 3 | ############################################################################# 4 | # Simulates failures and checks that the correct error responses are received 5 | ############################################################################# 6 | 7 | use strict; 8 | use warnings; 9 | use FindBin; 10 | use lib "$FindBin::Bin/lib"; 11 | use Test::Nginx::Socket 'no_plan'; 12 | 13 | SKIP: { 14 | our $token = &get_token_from_idsvr(); 15 | if ($token) { 16 | run_tests(); 17 | } 18 | else { 19 | fail("Could not get token from idsvr"); 20 | } 21 | } 22 | 23 | sub get_token_from_idsvr { 24 | use LWP::UserAgent; 25 | 26 | my $ua = LWP::UserAgent->new(); 27 | 28 | my $response = $ua->post("http://localhost:8443/oauth/v2/oauth-token", { 29 | "client_id" => "test-client", 30 | "client_secret" => "secret1", 31 | "grant_type" => "client_credentials", 32 | "scope" => "read write" 33 | }); 34 | my $content = $response->decoded_content(); 35 | 36 | my ($result) = $content =~ /access_token":"([^"]+)/; 37 | 38 | return $result; 39 | } 40 | 41 | __DATA__ 42 | 43 | 44 | === TEST FAILURE_1: Curity Identity Server not contactable returns 502 45 | ####################################################################### 46 | # Connectivity error returns a 500 to the client with useful error data 47 | ####################################################################### 48 | 49 | --- http_config 50 | lua_shared_dict phantom-token 10m; 51 | 52 | --- config 53 | location /t { 54 | 55 | rewrite_by_lua_block { 56 | 57 | local config = { 58 | introspection_endpoint = 'http://127.0.0.1:8447/oauth/v2/oauth-introspect', 59 | client_id = 'introspection-client', 60 | client_secret = 'secret2', 61 | token_cache_seconds = 900 62 | } 63 | 64 | local phantomToken = require 'phantom-token' 65 | phantomToken.run(config) 66 | } 67 | } 68 | 69 | --- error_code: 500 70 | 71 | --- request 72 | GET /t 73 | 74 | --- more_headers eval 75 | "Authorization: bearer " . $main::token 76 | 77 | --- response_body_like chomp 78 | {"code":"server_error","message":"Problem encountered authorizing the HTTP request"} 79 | 80 | === TEST FAILURE_2: Misconfigured introspection client returns 401 81 | ######################################################################## 82 | # Configuration error returns a 401 to the client with useful error data 83 | ######################################################################## 84 | 85 | --- http_config 86 | lua_shared_dict phantom-token 10m; 87 | 88 | --- config 89 | location /t { 90 | 91 | rewrite_by_lua_block { 92 | 93 | local config = { 94 | introspection_endpoint = 'http://127.0.0.1:8443/oauth/v2/oauth-introspect', 95 | client_id = 'introspection-client', 96 | client_secret = 'secret_invalid', 97 | token_cache_seconds = 900 98 | } 99 | 100 | local phantomToken = require 'phantom-token' 101 | phantomToken.run(config) 102 | } 103 | } 104 | 105 | --- error_code: 401 106 | 107 | --- request 108 | GET /t 109 | 110 | --- more_headers eval 111 | "Authorization: bearer " . $main::token 112 | 113 | --- response_body_like chomp 114 | {"code":"unauthorized","message":"Missing, invalid or expired access token"} -------------------------------------------------------------------------------- /t/scope_checks.t: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl 2 | 3 | ####################################################### 4 | # Runs tests related to enforcing scopes in the gateway 5 | ####################################################### 6 | 7 | use strict; 8 | use warnings; 9 | use FindBin; 10 | use lib "$FindBin::Bin/lib"; 11 | use Test::Nginx::Socket 'no_plan'; 12 | 13 | SKIP: { 14 | our $token = &get_token_from_idsvr(); 15 | if ($token) { 16 | run_tests(); 17 | } 18 | else { 19 | fail("Could not get token from idsvr"); 20 | } 21 | } 22 | 23 | sub get_token_from_idsvr { 24 | use LWP::UserAgent; 25 | 26 | my $ua = LWP::UserAgent->new(); 27 | 28 | my $response = $ua->post("http://localhost:8443/oauth/v2/oauth-token", { 29 | "client_id" => "test-client", 30 | "client_secret" => "secret1", 31 | "grant_type" => "client_credentials", 32 | "scope" => "read write" 33 | }); 34 | my $content = $response->decoded_content(); 35 | 36 | my ($result) = $content =~ /access_token":"([^"]+)/; 37 | 38 | return $result; 39 | } 40 | 41 | __DATA__ 42 | 43 | 44 | === TEST SCOPE_1: Correct scope is accepted when configured 45 | #################################################################### 46 | # The plugin correctly matches a single scope against the JWT scopes 47 | #################################################################### 48 | 49 | --- http_config 50 | lua_shared_dict phantom-token 10m; 51 | 52 | --- config 53 | location /t { 54 | 55 | rewrite_by_lua_block { 56 | 57 | local config = { 58 | introspection_endpoint = 'http://127.0.0.1:8443/oauth/v2/oauth-introspect', 59 | client_id = 'introspection-client', 60 | client_secret = 'secret2', 61 | token_cache_seconds = 900, 62 | scope = 'read' 63 | } 64 | 65 | local phantomToken = require 'phantom-token' 66 | phantomToken.run(config) 67 | } 68 | 69 | proxy_pass http://127.0.0.1:1984/target; 70 | } 71 | location /target { 72 | add_header 'authorization' $http_authorization; 73 | return 200; 74 | } 75 | 76 | --- error_code: 200 77 | 78 | --- request 79 | GET /t 80 | 81 | --- more_headers eval 82 | "Authorization: bearer " . $main::token 83 | 84 | --- response_headers_like 85 | authorization: Bearer ey.* 86 | 87 | === TEST SCOPE_2: Correct scopes are accepted when configured 88 | ##################################################################### 89 | # The plugin correctly matches multiple scopes against the JWT scopes 90 | ##################################################################### 91 | 92 | --- http_config 93 | lua_shared_dict phantom-token 10m; 94 | 95 | --- config 96 | location /t { 97 | 98 | rewrite_by_lua_block { 99 | 100 | local config = { 101 | introspection_endpoint = 'http://127.0.0.1:8443/oauth/v2/oauth-introspect', 102 | client_id = 'introspection-client', 103 | client_secret = 'secret2', 104 | token_cache_seconds = 900, 105 | scope = 'read write' 106 | } 107 | 108 | local phantomToken = require 'phantom-token' 109 | phantomToken.run(config) 110 | } 111 | 112 | proxy_pass http://127.0.0.1:1984/target; 113 | } 114 | location /target { 115 | add_header 'authorization' $http_authorization; 116 | return 200; 117 | } 118 | 119 | --- error_code: 200 120 | 121 | --- request 122 | GET /t 123 | 124 | --- more_headers eval 125 | "Authorization: bearer " . $main::token 126 | 127 | === TEST SCOPE_3: Access token with invalid scopes is rejected 128 | ##################################################################### 129 | # The plugin correctly detects missing scopes in the JWT access token 130 | ##################################################################### 131 | 132 | --- http_config 133 | lua_shared_dict phantom-token 10m; 134 | 135 | --- config 136 | location /t { 137 | 138 | rewrite_by_lua_block { 139 | 140 | local config = { 141 | introspection_endpoint = 'http://127.0.0.1:8443/oauth/v2/oauth-introspect', 142 | client_id = 'introspection-client', 143 | client_secret = 'secret2', 144 | token_cache_seconds = 900, 145 | scope = 'read execute' 146 | } 147 | 148 | local phantomToken = require 'phantom-token' 149 | phantomToken.run(config) 150 | } 151 | } 152 | 153 | --- error_code: 403 154 | 155 | --- request 156 | GET /t 157 | 158 | --- more_headers eval 159 | "Authorization: bearer " . $main::token 160 | 161 | --- response_body_like chomp 162 | {"code":"forbidden","message":"The token does not contain the required scope"} -------------------------------------------------------------------------------- /test.sh: -------------------------------------------------------------------------------- 1 | 2 | #!/bin/bash 3 | 4 | ################################################################################################ 5 | # After installing these prerequisites, this deploys the latest LUA code and runs all unit tests 6 | # - brew install openresty/brew/openresty 7 | # - cpan Test::Nginx 8 | # - opm install ledgetech/lua-resty-http 9 | # - opm install SkyLothar/lua-resty-jwt 10 | ################################################################################################ 11 | 12 | cd "$(dirname "${BASH_SOURCE[0]}")" 13 | 14 | # 15 | # Point to the OpenResty install 16 | # 17 | OPENRESTY_ROOT=/usr/local/Cellar/openresty/1.25.3.1_1 18 | 19 | # 20 | # Ensure that the OpenResty nginx, with LUA support, will be found by the prove tool 21 | # 22 | export PATH=${PATH}:"$OPENRESTY_ROOT/nginx/sbin" 23 | 24 | # 25 | # Copy the latest plugin to the LUA libraries folder 26 | # 27 | cp plugin/access.lua "$OPENRESTY_ROOT/lualib/phantom-token.lua" 28 | 29 | # 30 | # Deploy the Curity Identity Server and an example API 31 | # 32 | ./docker/deploy.sh 'test' 33 | if [ $? -ne 0 ]; then 34 | echo "Problem encountered deploying the Curity Identity Server" 35 | exit 1 36 | fi 37 | 38 | # 39 | # Run all Perl tests, which will call the Curity Identity Server and the API 40 | # 41 | prove -v -f t/*.t 42 | 43 | # 44 | # Free resources 45 | # 46 | ./docker/teardown.sh --------------------------------------------------------------------------------