├── .github ├── ISSUE_TEMPLATE.md ├── ISSUE_TEMPLATE │ └── bug_report.md ├── PULL_REQUEST_TEMPLATE.md ├── stale.yml └── workflows │ └── build.yml ├── .gitignore ├── CHANGES.rst ├── CONFIG.rst ├── LICENSE ├── README.rst ├── config ├── includes ├── shib_clear_headers └── shib_fastcgi_params ├── ngx_http_shibboleth_module.c └── t └── shibboleth.t /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | **Note that support requests for Shibboleth configuration and Nginx or web 2 | server setup should be directed to the Shibboleth community users mailing 3 | list. See for details.** 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Report an issue with the Nginx Shibboleth integration module 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Note that support requests for Shibboleth configuration and Nginx or web 11 | server setup should be directed to the Shibboleth community users mailing 12 | list. See for details.** 13 | 14 | ### Description the bug 15 | A clear and concise description of what the bug is. 16 | 17 | ### Expected behaviour 18 | A clear and concise description of what you expected to happen. 19 | 20 | ### Steps to Reproduce Issue 21 | 22 | 1. Go to '...' 23 | 2. Click on '....' 24 | 3. Scroll down to '....' 25 | 4. See error 26 | 27 | ### Setup & Logs 28 | * Please provide relevant configuration files; be sure to remove sensitive 29 | info 30 | * Include debug logs wherever possible, see 31 | https://nginx.org/en/docs/debugging_log.html; be sure to remove sensitive info 32 | 33 | ### Versions and Systems 34 | (`nginx -V`, `shibd -v` (and compile options), OS type and version) 35 | 36 | ### Additional context 37 | Add any other context about the problem here. 38 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### What does this PR do? 2 | 3 | ### What issue/feature does this PR fix or reference? 4 | 5 | ### Previous Behaviour 6 | Remove this section if not relevant 7 | 8 | ### New Behaviour 9 | Remove this section if not relevant 10 | 11 | ### Tests written? 12 | 13 | Yes/No 14 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Number of days of inactivity before an issue becomes stale 2 | daysUntilStale: 60 3 | # Number of days of inactivity before a stale issue is closed 4 | daysUntilClose: 7 5 | markComment: > 6 | This issue has been automatically marked as stale because it has not had 7 | recent activity. It will be closed if no further activity occurs. Thank you 8 | for your contributions. 9 | closeComment: false 10 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | 9 | strategy: 10 | matrix: 11 | include: 12 | # Mainline 13 | - nginx: 1.23.2 14 | dynamic_module: true 15 | - nginx: 1.23.2 16 | dynamic_module: false 17 | # Stable 18 | - nginx: 1.22.1 19 | dynamic_module: true 20 | - nginx: 1.22.1 21 | dynamic_module: false 22 | # Past stable versions 23 | - nginx: 1.20.2 24 | dynamic_module: true 25 | - nginx: 1.20.2 26 | dynamic_module: false 27 | - nginx: 1.18.0 28 | dynamic_module: true 29 | - nginx: 1.18.0 30 | dynamic_module: false 31 | - nginx: 1.16.1 32 | dynamic_module: true 33 | - nginx: 1.16.1 34 | dynamic_module: false 35 | - nginx: 1.14.2 36 | dynamic_module: false 37 | 38 | steps: 39 | - uses: actions/checkout@v3 40 | - name: Install prerequisites 41 | run: | 42 | sudo apt install -y git wget gcc cmake 43 | sudo apt install -y libpcre3-dev libssl-dev zlib1g-dev 44 | - name: Build 45 | env: 46 | _NGINX_VERSION: ${{ matrix.nginx }} 47 | SHIB_DYNAMIC_MODULE: ${{ matrix.dynamic_module }} 48 | run: | 49 | wget -O - "https://nginx.org/download/nginx-$_NGINX_VERSION.tar.gz" | tar -xzf - 50 | cd "nginx-$_NGINX_VERSION" 51 | git clone https://github.com/openresty/headers-more-nginx-module.git -b v0.34 52 | if [ "$SHIB_DYNAMIC_MODULE" = true ]; then 53 | ./configure --with-debug --add-dynamic-module=.. --add-dynamic-module=./headers-more-nginx-module 54 | else 55 | ./configure --with-debug --add-module=.. --add-module=./headers-more-nginx-module 56 | fi 57 | make 58 | echo "$(pwd)/objs" >> $GITHUB_PATH 59 | if [ "$SHIB_DYNAMIC_MODULE" = true ]; then 60 | echo "SHIB_MODULE_PATH=$(pwd)/objs" >> $GITHUB_ENV 61 | fi 62 | - name: Test 63 | env: 64 | SHIB_DYNAMIC_MODULE: ${{ matrix.dynamic_module }} 65 | SHIB_MODULE_PATH: ${{ env.SHIB_MODULE_PATH }} 66 | run: | 67 | sudo apt install -y cpanminus 68 | cpanm --notest --local-lib=$HOME/perl5 Test::Nginx 69 | PERL5LIB=$HOME/perl5/lib/perl5 TEST_NGINX_VERBOSE=true prove -v 70 | - name: Output debugging info on failure 71 | if: ${{ failure() }} 72 | run: | 73 | cat t/servroot/conf/nginx.conf 74 | cat t/servroot/access.log 75 | cat t/servroot/error.log 76 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | t/servroot 3 | nginx-* 4 | perl5/ 5 | -------------------------------------------------------------------------------- /CHANGES.rst: -------------------------------------------------------------------------------- 1 | CHANGES 2 | ======= 3 | 4 | Unreleased 5 | ---------- 6 | 7 | 2.0.2 (2023-05-26) 8 | ------------------ 9 | 10 | * bugfix: nginx crash when accessing uninitialized pointer 11 | * Fix compatibility with nginx 1.23.0+ - change handling of multiple headers 12 | * Switch to GitHub Actions for CI. 13 | * Documentation improvements 14 | 15 | 2.0.1 (2017-04-06) 16 | ------------------ 17 | 18 | * Add further standard SP variables and correct capitalisation in environment 19 | params. 20 | * Update Travis CI version tests. 21 | * Document preferred configuration of module. 22 | 23 | 2.0.0 (2016-05-18) 24 | ------------------ 25 | 26 | * **Backwards incompatibility**: Added ``shib_request_use_headers`` directive 27 | to require explicit configuration of copying attributes as headers. To 28 | restore pre-v2.0.0 behaviour add ``shib_request_use_headers on`` to your 29 | configuration. 30 | * Module can now be built as a dynamic module in Nginx 1.9.11+. 31 | Static compilation is always possible (and tested). 32 | * Added Travis CI tests. 33 | 34 | 1.0.0 (2016-02-18) 35 | ------------------ 36 | 37 | - Initial release 38 | -------------------------------------------------------------------------------- /CONFIG.rst: -------------------------------------------------------------------------------- 1 | Configuration 2 | ============= 3 | 4 | .. contents:: 5 | :local: 6 | :backlinks: none 7 | 8 | Steps 9 | ----- 10 | 11 | #. Obtain/rebuild Shibboleth SP with FastCGI support. 12 | #. Recompile Nginx with the ``nginx-http-shibboleth`` custom module. 13 | #. Configure Shibboleth FastCGI authorizer and reponsder applicatons to run. 14 | #. Configure Nginx to talk to both FastCGI authorizer and responder. 15 | #. Configure your Nginx application ``location`` block with ``shib_request 16 | /shibauthorizer``, where ``/shibauthorizer`` is the path to your Shibboleth 17 | authorizer location inside Nginx. 18 | #. Configure Shibboleth's ``shibboleth2.xml`` so the authorizer and responder are 19 | aware of which paths to protect. 20 | #. Ensure your application code accepts the relevant incoming headers for 21 | authN/authZ. 22 | 23 | Background 24 | ---------- 25 | 26 | Shibboleth supports Apache and IIS by default, but not Nginx. The closest one 27 | gets to support is via FastCGI, which Shibboleth `does have 28 | `_ 29 | but the default distribution needs to be rebuilt to support it. Nginx has 30 | support for FastCGI responders, but not for `FastCGI authorizers 31 | `_. This current module, 32 | ``nginx-http-shibboleth``, bridges this gap using sub-requests within Nginx. 33 | 34 | The design of Nginx is such that when handling sub-requests, it currently 35 | cannot forward the original request body, and likewise, cannot pass a 36 | sub-request response back to the client. As such, this module does not fully 37 | comply with the FastCGI authorizer specification. However, for Shibboleth, 38 | these two factors are inconsequential as only HTTP redirections and HTTP 39 | headers (cookies) are used for authentication to succeed and, only 40 | HTTP headers (attributes/variables) are required to be passed onto a backend 41 | application from the Shibboleth authorizer. 42 | 43 | 44 | Shibboleth SP with FastCGI Support 45 | ---------------------------------- 46 | 47 | For Debian-based distributions, your ``shibboleth-sp-utils`` package has 48 | likely already been built with FastCGI support, since default repositories 49 | feature the required FastCGI dev packages. 50 | 51 | For RPM-based distributions, you will either need to obtain a pre-built 52 | package with FastCGI support or build your own. Since the ``fcgi-devel`` 53 | libraries aren't present in RHEL or CentOS repositories, you likely require a 54 | thirty-party repository such as EPEL (or compile from source yourself). 55 | Recompilation of ``shibboleth-sp`` is simple, however, and an example script 56 | can be found at https://github.com/jcu-eresearch/shibboleth-fastcgi. 57 | 58 | 59 | Running the FastCGI authorizer and responder 60 | -------------------------------------------- 61 | 62 | Nginx does not manage FastCGI applications and thus they must be running 63 | before Nginx can talk to them. 64 | 65 | A simple option is to use `Supervisor `_ or another 66 | FastCGI controller to manage the applications. An example Supervisor 67 | configuration to work with a rebuilt ``shibboleth-sp`` on 64-bit RHEL/CentOS 68 | looks like:: 69 | 70 | [fcgi-program:shibauthorizer] 71 | command=/usr/lib64/shibboleth/shibauthorizer 72 | socket=unix:///opt/shibboleth/shibauthorizer.sock 73 | socket_owner=shibd:shibd 74 | socket_mode=0660 75 | user=shibd 76 | stdout_logfile=/var/log/supervisor/shibauthorizer.log 77 | stderr_logfile=/var/log/supervisor/shibauthorizer.error.log 78 | 79 | [fcgi-program:shibresponder] 80 | command=/usr/lib64/shibboleth/shibresponder 81 | socket=unix:///opt/shibboleth/shibresponder.sock 82 | socket_owner=shibd:shibd 83 | socket_mode=0660 84 | user=shibd 85 | stdout_logfile=/var/log/supervisor/shibresponder.log 86 | stderr_logfile=/var/log/supervisor/shibresponder.error.log 87 | 88 | Paths, users and permissions may need adjusting for different distributions or 89 | operating environments. The socket paths are arbitrary; make note of these 90 | socket locations as you will use them to configure Nginx. 91 | 92 | In the example above, the web server user (e.g. ``nginx``) would need to be 93 | made part of the ``shibd`` group in order to communicate correctly given the 94 | socket permissions of ``660``. Permissions and ownership can be changed to suit 95 | one's own environment, provided the web server can communicate with the FastCGI 96 | applications sockets and that those applications can correctly access the 97 | Shibboleth internals (e.g. ``shibd``). 98 | 99 | Note that the above configuration requires Supervisor 3.0 or above. If you 100 | are using RHEL/CentOS 6 with EPEL, note that their packaging is only providing 101 | version Supervisor 2. If this is the case, you will either need to upgrade OSes, 102 | install Supervisor from source (or PyPI), or package the RPMs yourself. 103 | 104 | 105 | Compile Nginx with Shibboleth module 106 | ------------------------------------ 107 | 108 | Compile Nginx with the ``nginx-http-shibboleth`` custom third-party module, 109 | following instructions at http://wiki.nginx.org/3rdPartyModules. How you do 110 | this depends on your Nginx installation processes and existing workflow. In 111 | general, however, you can clone this module from GitHub:: 112 | 113 | git clone https://github.com/nginx-shib/nginx-http-shibboleth.git 114 | 115 | and add it into your ``configure`` step of Nginx:: 116 | 117 | ./configure --add-module=/path/to/nginx-http-shibboleth 118 | 119 | Note that you'll almost certainly have other options being passed to 120 | ``configure`` at the same time. It may be easiest to re-build Nginx from your 121 | existing packages for your distribution, and patch the above ``configure`` 122 | argument into the build processes. 123 | 124 | Also, you will likely need the Nginx module `nginx_headers_more 125 | `_ in order to prevent header 126 | spoofing from the client, unless you already have a separate solution in 127 | place. 128 | 129 | If you wish to confirm the build was successful, install a version of Nginx 130 | with debugging support, configure full trace logging, and the example 131 | configuration below. You should notice ``shib request ...`` lines in the 132 | output showing where ``nginx-http-shibboleth`` is up to during a request. 133 | 134 | 135 | Configure Nginx 136 | --------------- 137 | 138 | Nginx now needs to be configured with ``location`` blocks that point to both 139 | the FastCGI authorizer and responder. Specify your FastCGI socket locations, 140 | where required. Note that the ``more_clear_input_headers`` directive is 141 | required to prevent header spoofing from the client, since the Shibboleth 142 | variables are passed around as headers. 143 | 144 | .. code:: nginx 145 | 146 | server { 147 | listen 443 ssl; 148 | server_name example.org; 149 | ... 150 | 151 | #FastCGI authorizer for Auth Request module 152 | location = /shibauthorizer { 153 | internal; 154 | include fastcgi_params; 155 | fastcgi_pass unix:/opt/shibboleth/shibauthorizer.sock; 156 | } 157 | 158 | #FastCGI responder 159 | location /Shibboleth.sso { 160 | include fastcgi_params; 161 | fastcgi_pass unix:/opt/shibboleth/shibresponder.sock; 162 | } 163 | 164 | #Resources for the Shibboleth error pages. This can be customised. 165 | location /shibboleth-sp { 166 | alias /usr/share/shibboleth/; 167 | } 168 | 169 | #A secured location. Here all incoming requests query the 170 | #FastCGI authorizer. Watch out for performance issues and spoofing. 171 | location /secure { 172 | include shib_clear_headers; 173 | #Add your attributes here. They get introduced as headers 174 | #by the FastCGI authorizer so we must prevent spoofing. 175 | more_clear_input_headers 'displayName' 'mail' 'persistent-id'; 176 | shib_request /shibauthorizer; 177 | shib_request_use_headers on; 178 | proxy_pass http://localhost:8080; 179 | } 180 | 181 | #A secured location, but only a specific sub-path causes Shibboleth 182 | #authentication. 183 | location /secure2 { 184 | proxy_pass http://localhost:8080; 185 | 186 | location = /secure2/shibboleth { 187 | include shib_clear_headers; 188 | #Add your attributes here. They get introduced as headers 189 | #by the FastCGI authorizer so we must prevent spoofing. 190 | more_clear_input_headers 'displayName' 'mail' 'persistent-id'; 191 | shib_request /shibauthorizer; 192 | shib_request_use_headers on; 193 | proxy_pass http://localhost:8080; 194 | } 195 | } 196 | } 197 | 198 | Notes 199 | ~~~~~ 200 | 201 | * ``proxy_pass`` can be replaced with any application or configuration that 202 | should receive the Shibboleth attributes as headers. Essentially, this is 203 | what would normally be the backend configured against ``AuthType 204 | shibboleth`` in Apache. 205 | 206 | * The first 3 locations are pure boilerplate for any host that requires 207 | Shibboleth authentication, so you may wish to template these for reuse 208 | between hosts. 209 | 210 | * The ``/shibboleth-sp`` location provides web resources for default 211 | Shibboleth error messages. If you customise error pages, or don't care for 212 | images or styles on error pages, delete this location. 213 | 214 | * Take note of the ``more_clear_input_headers`` calls. As the Shibboleth 215 | authorizer will inject headers into the request before passing the 216 | request onto the final upstream endpoint, you **must** 217 | use these directives to protect from spoofing. You should expand the 218 | second call to this directive when you have more incoming attributes 219 | from the Shibboleth authorizer. Or else beware... 220 | 221 | * The ``/secure`` location will ask the FastCGI authorizer for attributes for 222 | **every** request that comes in. This may or may not be desirable. Keep in 223 | mind this means that each request will have Shibboleth attributes add before 224 | being sent onto a backend, and this will happen every time. 225 | 226 | * You may wish to consider only securing a path that creates an application 227 | session (such as the ``/secure2`` location block), and letting your 228 | application handle the rest. Only upon the user hitting this specific URL 229 | will the authentication process be triggered. This is a authentication 230 | technique to avoid extra overhead -- set the upstream for the specific 231 | sub-path to be somewhere an application session is created, and have that 232 | application session capture the Shibboleth attributes. 233 | 234 | Notice how the rest of the application doesn't refer to the authorizer. 235 | This means the application can be used anonymously, too. Alternatively, 236 | you can configure the ``requireSession`` option to be fa 237 | 238 | * Adding the ``shib_request`` line into a location isn't all you need to 239 | do to get the FastCGI authorizer to recognise your path as Shibboleth 240 | protected. You need also need to ensure that ``shibd`` is configured to 241 | accept your paths as well, following the next set of instructions. 242 | 243 | 244 | Configuring Shibboleth's shibboleth2.xml to recognise secured paths 245 | ------------------------------------------------------------------- 246 | 247 | Within Apache, you can tell Shibboleth which paths to secure by 248 | using configuration like so in your web server's configuration: 249 | 250 | .. code:: apache 251 | 252 | 253 | ShibRequestSetting authType shibboleth 254 | ShibRequestSetting requireSession false 255 | 256 | 257 | With this, Shibboleth is made aware of this configuration automatically. 258 | 259 | However, the FastCGI authorizer for Shibboleth operates without such 260 | directives in the web server. Path protection and request mapping needs to 261 | be configured like it would be for IIS, using the XML-based 262 | ```` configuration. The same options from 263 | Apache are accepted within the ``RequestMapper`` section of the 264 | ``shibboleth2.xml`` configuration file, like this truncated example shows. 265 | This example corresponds to the sample Nginx configuration given above. 266 | 267 | .. code:: xml 268 | 269 | 270 | 271 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | Notes 284 | ~~~~~ 285 | 286 | * When used with nginx, the ``RequestMapper`` will work with either 287 | ``type="native"`` or ``type="XML"``. The latter is recommended 288 | as nginx has no native commands or ``.htaccess`` so skipping 289 | those checks leads to performance gains (see `NativeSPRequestMapper 290 | docs `_). 291 | 292 | * The Shibboleth FastCGI authorizer must have both ``authType`` **and** 293 | ``requireSession`` configured for the resultant path. If they are not 294 | present, then the authorizer will ignore the path it is passed and the user 295 | will not be prompted for authentication (and no logging will take place). 296 | 297 | * ```` names are **case sensitive**. 298 | 299 | * You can use other configuration items like ```` and 300 | ```` and ```` to configure how Shibboleth handles 301 | incoming requests. There is no limit on the number of hosts/paths configured. 302 | 303 | * Configuration is inherited **downwards** in the XML tree. So, configure ``authType`` 304 | on a ```` element will see it apply to all paths beneath it. This is 305 | not required, however; attributes can be placed anywhere you desire. 306 | 307 | * Nested ```` elements are greedy. Putting a path with 308 | ``name="shibboleth"`` within a path with ``name="secure"`` really translates 309 | to a path with ``name="secure/shibboleth"``. 310 | 311 | * Upon changing this configuration, ensure the ``shibauthorizer`` and 312 | ``shibresponder`` applications are hard-restarted, as well as ``shibd``. 313 | 314 | Gotchas 315 | ------- 316 | 317 | If you're experiencing issues with the Shibboleth authorizer or Shibboleth 318 | responder appearing to fail to be invoked, check the following: 319 | 320 | * The authorizer requires a ```` element in ``shib2.xml`` to be 321 | *correctly* configured with ``authType`` and ``requireSession`` for auth to 322 | take place. If you don't (or say forget to restart ``shibd``), then the 323 | authorizer will return a ``200 OK`` status response, which equates to 324 | unconditionally allowing access. 325 | 326 | * The authorizer and responder require a correctly-configured FastCGI request 327 | environment in order to accept, match and process requests. The `default 328 | fastcgi_params file `_ 329 | provides a suitable configuration. If your ``fastcgi_params`` differs from the 330 | default, check this first. 331 | 332 | * If the environment is not correct, the authorizer and responder will respond with 333 | ``500 Server Error``, reporting this to the browser:: 334 | 335 | FastCGI Shibboleth responder should only be used for Shibboleth protocol requests. 336 | 337 | As well as this to the ``stderr`` from FastCGI:: 338 | 339 | shib: doHandler failed to handle the request 340 | 341 | In this case, check all the FastCGI environment variables to ensure they're right, 342 | particularly ``REQUEST_URI`` and ``SERVER_PORT``. 343 | 344 | Also check your ``shibboleth2.xml`` configuration's ```` 345 | as the FastCGI applications will error in the same way if your ``handlerURL`` and 346 | its protocol, port and path don't match what's configured within Nginx. This is 347 | especially true if using an absolute URL, custom port number or different path to 348 | the standard `/Shibboleth.sso`. 349 | 350 | * No logs will get issued *anywhere* for anything related to the FastCGI 351 | applications (standard ``shibd`` logging does apply, however). If you're 352 | testing for why the authentication cycle doesn't start, try killing your 353 | FastCGI authorizer and make sure you see a ``502`` error come back from 354 | Nginx. If you still get a ``200``, then your ``shib_request`` configuration 355 | in Nginx is probably wrong and the authorizer isn't being contacted. 356 | 357 | * When in doubt, hard restart the entire stack, and use something like ``curl`` 358 | to ensure you avoid any browser caching. 359 | 360 | * If still in doubt that the Nginx installation has been successfully built 361 | with the ``nginx-http-shibboleth`` module, run Nginx in debug mode, 362 | and trace the request accordingly through the logs or console output. 363 | 364 | 365 | Resources 366 | --------- 367 | 368 | * http://wiki.nginx.org/HttpHeadersMoreModule 369 | * https://wiki.shibboleth.net/confluence/display/SHIB2/NativeSPRequestMapper 370 | * https://wiki.shibboleth.net/confluence/display/SHIB2/NativeSPRequestMap 371 | * https://github.com/nginx-shib/nginx-http-shibboleth 372 | * http://davidjb.com/blog/2013/04/setting-up-a-shibboleth-sp-with-fastcgi-support/ 373 | * https://github.com/jcu-eresearch/shibboleth-fastcgi/ 374 | * https://github.com/jcu-eresearch/nginx-custom-build 375 | 376 | Deprecated documentation: 377 | 378 | * http://davidjb.com/blog/2013/04/integrating-nginx-and-a-shibboleth-sp-with-fastcgi/ 379 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2013-present, David Beitey (davidjb) 3 | * Copyright (c) 2014, Luca Bruno 4 | * All rights reserved. 5 | * 6 | * Redistribution and use in source and binary forms, with or without 7 | * modification, are permitted provided that the following conditions 8 | * are met: 9 | * 1. Redistributions of source code must retain the above copyright 10 | * notice, this list of conditions and the following disclaimer. 11 | * 2. Redistributions in binary form must reproduce the above copyright 12 | * notice, this list of conditions and the following disclaimer in the 13 | * documentation and/or other materials provided with the distribution. 14 | * 15 | * THIS SOFTWARE IS PROVIDED BY AUTHOR AND CONTRIBUTORS ``AS IS'' AND 16 | * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 17 | * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 18 | * ARE DISCLAIMED. IN NO EVENT SHALL AUTHOR OR CONTRIBUTORS BE LIABLE 19 | * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 20 | * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 21 | * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 22 | * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 23 | * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 24 | * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 25 | * SUCH DAMAGE. 26 | */ 27 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Shibboleth auth request module for Nginx 2 | ======================================== 3 | 4 | .. image:: https://github.com/nginx-shib/nginx-http-shibboleth/actions/workflows/build.yml/badge.svg 5 | :target: https://github.com/nginx-shib/nginx-http-shibboleth/actions/workflows/build.yml 6 | 7 | This module allows Nginx to work with Shibboleth, by way of Shibboleth's 8 | FastCGI authorizer. This module requires specific configuration in order to 9 | work correctly, as well as Shibboleth's FastCGI authorizer application 10 | available on the system. It aims to be similar to parts of Apache's 11 | `mod_shib`_, though Shibboleth authorisation and authentication settings are 12 | configured via `shibboleth2.xml`_ rather than in the web server configuration. 13 | 14 | With this module configured against a ``location`` block, incoming requests 15 | are authorized within Nginx based upon the result of a subrequest to 16 | Shibboleth's FastCGI authorizer. In this process, this module can be used to 17 | copy user attributes from a successful authorizer response into Nginx's 18 | original request as headers or environment parameters for use by any backend 19 | application. If authorization is not successful, the authorizer response 20 | status and headers are returned to the client, denying access or redirecting 21 | the user's browser accordingly (such as to a WAYF page, if so configured). 22 | 23 | This module works at access phase and therefore may be combined with other 24 | access modules (such as ``access``, ``auth_basic``) via the ``satisfy`` 25 | directive. This module can be also compiled alongside 26 | ``ngx_http_auth_request_module``, though use of both of these modules in the 27 | same ``location`` block is untested and not advised. 28 | 29 | Read more about the `Behaviour`_ below and consult `Configuration`_ for 30 | important notes on avoiding spoofing if using headers for attributes. 31 | 32 | For further information on why this is a dedicated module, see 33 | https://forum.nginx.org/read.php?2,238523,238523#msg-238523 34 | 35 | Directives 36 | ---------- 37 | 38 | The following directives are added into your Nginx configuration files. The 39 | contexts mentioned below show where they may be added. 40 | 41 | 42 | shib_request |off 43 | | **Context:** ``http``, ``server``, ``location`` 44 | | **Default:** ``off`` 45 | 46 | Switches the Shibboleth auth request module on and sets URI which will be 47 | asked for authorization. The configured URI should refer to a Nginx 48 | location block that points to your Shibboleth FastCGI authorizer. 49 | 50 | The HTTP status and headers of the response resulting 51 | from the sub-request to the configured URI will be returned to the user, 52 | in accordance with the `FastCGI Authorizer specification`_. 53 | The one (potentially significant) caveat is that due to the way 54 | Nginx operates at present with regards to subrequests (what 55 | an Authorizer effectively requires), the request body will *not* be 56 | forwarded to the authorizer, and similarly, the response body from 57 | the authorizer will *not* be returned to the client. 58 | 59 | Configured URIs are not restricted to using a FastCGI backend 60 | to generate a response, however. This may be useful during 61 | testing or otherwise, as you can use Nginx's built in ``return`` 62 | and ``rewrite`` directives to produce a suitable response. 63 | Additionally, this module may be used with *any* FastCGI 64 | authorizer, although operation may be affected by the above caveat. 65 | 66 | .. warning:: 67 | 68 | The ``shib_request`` directive no longer requires the ``shib_authorizer`` 69 | flag. This must be removed for Nginx to start. No other changes are 70 | required. 71 | 72 | shib_request_set 73 | | **Context:** ``http``, ``server``, ``location`` 74 | | **Default:** ``none`` 75 | 76 | Set the ``variable`` to the specified ``value`` after the auth request has 77 | completed. The ``value`` may contain variables from the auth request's 78 | response. For instance, ``$upstream_http_*``, ``$upstream_status``, and 79 | any other variables mentioned in the `nginx_http_upstream_module 80 | `_ 81 | documentation. 82 | 83 | This directive can be used to introduce Shibboleth attributes into the 84 | environment of the backend application, such as `$_SERVER` for a FastCGI 85 | PHP application and is the recommended method of doing so. See the 86 | `Configuration`_ documentation for an example. 87 | 88 | shib_request_use_headers on|off 89 | | **Context:** ``http``, ``server``, ``location`` 90 | | **Default:** ``off`` 91 | 92 | .. note:: 93 | 94 | Added in v2.0.0. 95 | 96 | Copy attributes from the Shibboleth authorizer response into the main 97 | request as headers, making them available to upstream servers and 98 | applications. Use this option only if your upstream/application does not 99 | support server parameters via ``shib_request_set``. 100 | 101 | With this setting enabled, Authorizer response headers beginning with 102 | ``Variable-\*`` are extracted, stripping the ``Variable-`` substring from 103 | the header name, and copied into the main request before it is sent to the 104 | backend. For example, an authorizer response header such as 105 | ``Variable-Commonname: John Smith`` would result in ``Commonname: John 106 | Smith`` being added to the main request, and thus sent to the backend. 107 | 108 | **Beware of spoofing** - you must ensure that your backend application is 109 | protected from injection of headers. Consult the `Configuration`_ example 110 | on how to achieve this. 111 | 112 | 113 | Installation 114 | ------------ 115 | 116 | This module can either be compiled statically or dynamically, since the 117 | introduction of `dynamic modules 118 | `_ in Nginx 119 | 1.9.11. The practical upshot of dynamic modules is that they can be loaded, 120 | as opposed to static modules which are permanently present and enabled. 121 | 122 | The easiest way to obtain a packaged version of this module is to use the 123 | `pkg-oss `_ tool from Nginx, which provides for 124 | packaging of dynamic modules for installation alongside the official releases 125 | of Nginx from the `main repositories `_ 126 | and helps avoid the need to compile Nginx by hand. 127 | 128 | Otherwise, to compile Nginx with this module dynamically, pass the following 129 | option to ``./configure`` when building Nginx:: 130 | 131 | --add-dynamic-module= 132 | 133 | You will need to explicitly load the module in your ``nginx.conf`` by 134 | including:: 135 | 136 | load_module /path/to/modules/ngx_http_shibboleth_module.so; 137 | 138 | and reload or restart Nginx. 139 | 140 | To compile Nginx with this module statically, pass the following option to 141 | ``./configure`` when building Nginx:: 142 | 143 | --add-module= 144 | 145 | With a static build, no additional loading is required as the module is 146 | built-in to Nginx. 147 | 148 | 149 | Configuration 150 | ------------- 151 | 152 | For full details about configuring the Nginx/Shibboleth environment, 153 | see the documentation at 154 | https://github.com/nginx-shib/nginx-http-shibboleth/blob/master/CONFIG.rst. 155 | 156 | An example ``server`` block consists of the following: 157 | 158 | .. code-block:: nginx 159 | 160 | #FastCGI authorizer for Auth Request module 161 | location = /shibauthorizer { 162 | internal; 163 | include fastcgi_params; 164 | fastcgi_pass unix:/opt/shibboleth/shibauthorizer.sock; 165 | } 166 | 167 | #FastCGI responder 168 | location /Shibboleth.sso { 169 | include fastcgi_params; 170 | fastcgi_pass unix:/opt/shibboleth/shibresponder.sock; 171 | } 172 | 173 | # Using the ``shib_request_set`` directive, we can introduce attributes as 174 | # environment variables for the backend application. In this example, we 175 | # set ``fastcgi_param`` but this could be any type of Nginx backend that 176 | # supports parameters (by using the appropriate *_param option) 177 | # 178 | # The ``shib_fastcgi_params`` is an optional set of default parameters, 179 | # available in the ``includes/`` directory in this repository. 180 | # 181 | # Choose this type of configuration unless your backend application 182 | # doesn't support server parameters or specifically requires headers. 183 | location /secure-environment-vars { 184 | shib_request /shibauthorizer; 185 | include shib_fastcgi_params; 186 | shib_request_set $shib_commonname $upstream_http_variable_commonname; 187 | shib_request_set $shib_email $upstream_http_variable_email; 188 | fastcgi_param COMMONNAME $shib_commonname; 189 | fastcgi_param EMAIL $shib_email; 190 | fastcgi_pass unix:/path/to/backend.socket; 191 | } 192 | 193 | # A secured location. All incoming requests query the Shibboleth FastCGI authorizer. 194 | # Watch out for performance issues and spoofing! 195 | # 196 | # Choose this type of configuration for ``proxy_pass`` applications 197 | # or backends that don't support server parameters. 198 | location /secure { 199 | shib_request /shibauthorizer; 200 | shib_request_use_headers on; 201 | 202 | # Attributes from Shibboleth are introduced as headers by the FastCGI 203 | # authorizer so we must prevent spoofing. The 204 | # ``shib_clear_headers`` is a set of default header directives, 205 | # available in the `includes/` directory in this repository. 206 | include shib_clear_headers; 207 | 208 | # Add *all* attributes that your application uses, including all 209 | #variations. 210 | more_clear_input_headers 'displayName' 'mail' 'persistent-id'; 211 | 212 | # This backend application will receive Shibboleth variables as request 213 | # headers (from Shibboleth's FastCGI authorizer) 214 | proxy_pass http://localhost:8080; 215 | } 216 | 217 | Note that we use the `headers-more-nginx-module 218 | `_ to clear 219 | potentially dangerous input headers and avoid the potential for spoofing. The 220 | latter example with environment variables isn't susceptible to header 221 | spoofing, as long as the backend reads data from the environment parameters 222 | **only**. 223 | 224 | A `default configuration 225 | `_ 226 | is available to clear the basic headers from the Shibboleth authorizer, but 227 | you must ensure you write your own clear directives for all attributes your 228 | application uses. Bear in mind that some applications will try to read a 229 | Shibboleth attribute from the environment and then fall back to headers, so 230 | review your application's code even if you are not using 231 | ``shib_request_use_headers``. 232 | 233 | 234 | With use of ``shib_request_set``, a `default params 235 | `_ 236 | file is available which you can use as an nginx ``include`` to ensure all core 237 | Shibboleth variables get passed from the FastCGI authorizer to the 238 | application. Numerous default attributes are included so remove the ones that 239 | aren't required by your application and add Federation or IDP attributes that 240 | you need. This default params file can be re-used for upstreams that aren't 241 | FastCGI by simply changing the ``fastcgi_param`` directives to 242 | ``uwsgi_param``, ``scgi_param`` or so forth. 243 | 244 | Gotchas 245 | ~~~~~~~ 246 | 247 | * Subrequests, such as the Shibboleth auth request, aren't processed through header filters. 248 | This means that built-in directives like ``add_header`` will **not** work if configured 249 | as part of the a ``/shibauthorizer`` block. If you need to manipulate subrequest headers, 250 | use ``more_set_headers`` from the module ``headers-more``. 251 | 252 | See https://forum.nginx.org/read.php?29,257271,257272#msg-257272. 253 | 254 | Behaviour 255 | --------- 256 | 257 | This module follows the `FastCGI Authorizer specification`_ where possible, 258 | but has some notable deviations - with good reason. The behaviour is thus: 259 | 260 | * An authorizer subrequest is comprised of all aspects of the original 261 | request, excepting the request body as Nginx does not support buffering of 262 | request bodies. As the Shibboleth FastCGI authorizer does not consider the 263 | request body, this is not an issue. 264 | 265 | * If an authorizer subrequest returns a ``200`` status, access is allowed. 266 | 267 | If ``shib_request_use_headers`` is enabled, and response headers beginning 268 | with ``Variable-\*`` are extracted, stripping the ``Variable-`` substring 269 | from the header name, and copied into the main request. Other authorizer 270 | response headers not prefixed with ``Variable-`` and the response body are 271 | ignored. The FastCGI spec calls for ``Variable-*`` name-value pairs to be 272 | included in the FastCGI environment, but we make them headers so as they may 273 | be used with *any* backend (such as ``proxy_pass``) and not just restrict 274 | ourselves to FastCGI applications. By passing the ``Variable-*`` data as 275 | headers instead, we end up following the behaviour of ``ShibUseHeaders On`` 276 | in ``mod_shib`` for Apache, which passes these user attributes as headers. 277 | 278 | In order to pass attributes as environment variables (the equivalent to 279 | ``ShibUseEnvironment On`` in ``mod_shib``), attributes must be manually 280 | extracted using ``shib_request_set`` directives for each attribute. This 281 | cannot (currently) be done *en masse* for all attributes as each backend may 282 | accept parameters in a different way (``fastcgi_param``, ``uwsgi_param`` 283 | etc). Pull requests are welcome to automate this behaviour. 284 | 285 | * If the authorizer subrequest returns *any* other status (including redirects 286 | or errors), the authorizer response's status and headers are returned to the 287 | client. 288 | 289 | This means that on ``401 Unauthorized`` or ``403 Forbidden``, access will be 290 | denied and headers (such as ``WWW-Authenticate``) from the authorizer will be 291 | passed to client. All other authorizer responses (such as ``3xx`` 292 | redirects) are passed back to the client, including status and headers, 293 | allowing redirections such as those to WAYF pages and the Shibboleth 294 | responder (``Shibboleth.sso``) to work correctly. 295 | 296 | The FastCGI Authorizer spec calls for the response body to be returned to 297 | the client, but as Nginx does not currently support buffering subrequest 298 | responses (``NGX_HTTP_SUBREQUEST_IN_MEMORY``), the authorizer response body 299 | is effectively ignored. A workaround is to have Nginx serve an 300 | ``error_page`` of its own, like so: 301 | 302 | .. code-block:: nginx 303 | 304 | location /secure { 305 | shib_request /shibauthorizer; 306 | error_page 403 /shibboleth-forbidden.html; 307 | ... 308 | } 309 | 310 | This serves the given error page if the Shibboleth authorizer denies the 311 | user access to this location. Without ``error_page`` specified, Nginx will 312 | serve its generic error pages. 313 | 314 | Note that this does *not* apply to the Shibboleth responder (typically hosted at 315 | ``Shibboleth.sso``) as it is a FastCGI responder and Nginx is fully compatible 316 | with this as no subrequests are used. 317 | 318 | For more details, see https://forum.nginx.org/read.php?2,238444,238453. 319 | 320 | Whilst this module is geared specifically for Shibboleth's FastCGI authorizer, 321 | it will likely work with other authorizers, bearing in mind the deviations 322 | from the spec above. 323 | 324 | Tests 325 | ----- 326 | 327 | Tests are automatically run on GitHub Actions (using `this configuration 328 | `_) 329 | whenever new commits are made to the repository or when new pull requests 330 | are opened. If something breaks, you'll be informed and the results will be 331 | reported on GitHub. 332 | 333 | Tests are written using a combination of a simple Bash script for compilation 334 | of our module with different versions and configurations of Nginx and the 335 | `Test::Nginx `_ Perl test 336 | scaffolding for integration testing. Consult the previous link for 337 | information on how to extend the tests, and also refer to the underlying 338 | `Test::Base `_ 339 | documentation on aspects like the `blocks()` function. 340 | 341 | Integration tests are run automatically by CI but can also be run manually 342 | (requires Perl & CPAN to be installed): 343 | 344 | .. code-block:: bash 345 | 346 | cd nginx-http-shibboleth 347 | cpanm --notest --local-lib=$HOME/perl5 Test::Nginx 348 | # nginx must be present in PATH and built with debugging symbols 349 | PERL5LIB=$HOME/perl5/lib/perl5 prove 350 | 351 | Help & Support 352 | -------------- 353 | 354 | Support requests for Shibboleth configuration and Nginx or web server setup 355 | should be directed to the Shibboleth community users mailing list. See 356 | https://www.shibboleth.net/community/lists/ for details. 357 | 358 | Debugging 359 | --------- 360 | 361 | Because of the complex nature of the nginx/FastCGI/Shibboleth stack, debugging 362 | configuration issues can be difficult. Here's some key points: 363 | 364 | #. Confirm that ``nginx-http-shibboleth`` is successfully built and installed 365 | within nginx. You can check by running ``nginx -V`` and inspecting the 366 | output for ``--add-module=[path]/nginx-http-shibboleth`` or 367 | ``--add-dynamic-module=[path]/nginx-http-shibboleth``. 368 | #. If using dynamic modules for nginx, confirm you have used the 369 | ``load_module`` directive to load this module. Your use of ``shib_request`` 370 | and other directives will fail if you have forgotten to load the module. 371 | #. If using a version of nginx that is different to those we 372 | `test with `_ 373 | or if you are using other third-party modules, you should run 374 | the test suite above to confirm compatibility. If any tests fail, then check 375 | your configuration or consider updating your nginx version. 376 | #. Shibboleth configuration: check your ``shibboleth2.xml`` and associated 377 | configuration to ensure your hosts, paths and attributes are being correctly 378 | released. An `example configuration `_ 379 | can help you identify key "gotchas" to configuring ``shibboleth2.xml`` to work 380 | with the FastCGI authorizer. 381 | #. Application-level: within your code, always start with the simplest possible 382 | debugging output (such as printing the request environment) and work 383 | up from there. If you want to create a basic, stand-alone app, take 384 | a look at the `Bottle `_ 385 | configuration on the wiki. 386 | #. Debugging module internals: if you've carefully checked all of the above, then 387 | you can also debug the behaviour of this module itself. You will need to have 388 | compiled nginx with debugging support (via ``./auto/configure --with-debug ...``) 389 | and when running nginx, it is easiest if you're able run in the foreground with 390 | debug logging enabled. Add the following to your ``nginx.conf``: 391 | 392 | .. code-block:: nginx 393 | 394 | daemon off; 395 | error_log stderr debug; 396 | 397 | and run nginx. Upon starting nginx you should see lines containing `[debug]` and 398 | as you make requests, console logging will continue. If this doesn't happen, 399 | then check your nginx configuration and compilation process. 400 | 401 | When you eventually make a request that hits (or should invoke) the 402 | ``shib_request`` location block, you will see lines like so in the output: 403 | 404 | .. code-block:: nginx 405 | 406 | [debug] 1234#0: shib request handler 407 | [debug] 1234#0: shib request set variables 408 | [debug] 1234#0: shib request authorizer handler 409 | [debug] 1234#0: shib request authorizer allows access 410 | [debug] 1234#0: shib request authorizer copied header: "AUTH_TYPE: shibboleth" 411 | [debug] 1234#0: shib request authorizer copied header: "REMOTE_USER: john.smith@example.com" 412 | ... 413 | 414 | If you don't see these types of lines containing `shib request ...`, 415 | or if you see *some* of the lines above but not where headers/variables are being 416 | copied, then double-check your nginx configuration. If you're still not getting 417 | anywhere, then you can add your own debugging lines into the source (follow 418 | this module's examples) to eventually determine what is going wrong and when. 419 | If doing this, don't forget to recompile nginx and/or ``nginx-http-shibboleth`` 420 | whenever you make a change. 421 | 422 | If you believe you've found a bug in the core module code, then please 423 | `create an issue `_. 424 | 425 | You can also search existing issues as it is likely someone else has 426 | encountered a similar issue before. 427 | 428 | Versioning 429 | ---------- 430 | 431 | This module uses `Semantic Versioning `_ and all releases 432 | are tagged on GitHub, which allows package downloads of individual tags. 433 | 434 | License 435 | ------- 436 | 437 | This project is licensed under the same license that nginx is, the 438 | `2-clause BSD-like license `_. 439 | 440 | .. _FastCGI Authorizer specification: https://web.archive.org/web/20160306081510/http://fastcgi.com/drupal/node/6?q=node/22#S6.3 441 | .. _mod_shib: https://wiki.shibboleth.net/confluence/display/SHIB2/NativeSPApacheConfig 442 | .. _shibboleth2.xml: https://wiki.shibboleth.net/confluence/display/SHIB2/NativeSPShibbolethXML 443 | -------------------------------------------------------------------------------- /config: -------------------------------------------------------------------------------- 1 | ngx_addon_name=ngx_http_shibboleth_module 2 | 3 | if test -n "$ngx_module_link"; then 4 | ngx_module_type=HTTP 5 | ngx_module_name=ngx_http_shibboleth_module 6 | ngx_module_srcs="$ngx_addon_dir/ngx_http_shibboleth_module.c" 7 | 8 | . auto/module 9 | else 10 | HTTP_MODULES="$HTTP_MODULES ngx_http_shibboleth_module" 11 | NGX_ADDON_SRCS="$NGX_ADDON_SRCS $ngx_addon_dir/ngx_http_shibboleth_module.c" 12 | fi 13 | -------------------------------------------------------------------------------- /includes/shib_clear_headers: -------------------------------------------------------------------------------- 1 | # Ensure that you add directives to clear input headers for *all* attributes 2 | # that your backend application uses. This may also include variations on these 3 | # headers, such as differing capitalisations and replacing hyphens with 4 | # underscores etc -- it all depends on what your application is reading. 5 | # 6 | # Note that Nginx silently drops headers with underscores 7 | # unless the non-default `underscores_in_headers` is enabled. 8 | 9 | more_clear_input_headers 10 | Auth-Type 11 | 'Shib-*' 12 | Remote-User; 13 | 14 | # more_clear_input_headers 15 | # EPPN 16 | # Affiliation 17 | # Unscoped-Affiliation 18 | # Entitlement 19 | # Targeted-Id 20 | # Persistent-Id 21 | # Transient-Name 22 | # Commonname 23 | # DisplayName 24 | # Email 25 | # OrganizationName; 26 | -------------------------------------------------------------------------------- /includes/shib_fastcgi_params: -------------------------------------------------------------------------------- 1 | # vim: set filetype=conf : 2 | 3 | # Replace `fastcgi_param` with `sgci_param`, `uwsgi_param` or similar 4 | # directive for use with different upstreams. Consult the relevant upstream 5 | # documentation for more information on environment parameters. 6 | # 7 | # Auth-Type is configured as authType in 8 | # https://wiki.shibboleth.net/confluence/display/SHIB2/NativeSPContentSettings. 9 | # Other default SP variables are as per 10 | # https://wiki.shibboleth.net/confluence/display/SHIB2/NativeSPAttributeAccess#NativeSPAttributeAccess-CustomSPVariables 11 | 12 | shib_request_set $shib_auth_type $upstream_http_variable_auth_type; 13 | fastcgi_param Auth-Type $shib_auth_type; 14 | 15 | shib_request_set $shib_shib_application_id $upstream_http_variable_shib_application_id; 16 | fastcgi_param Shib-Application-ID $shib_shib_application_id; 17 | 18 | shib_request_set $shib_shib_authentication_instant $upstream_http_variable_shib_authentication_instant; 19 | fastcgi_param Shib-Authentication-Instant $shib_shib_authentication_instant; 20 | 21 | shib_request_set $shib_shib_authentication_method $upstream_http_variable_shib_authentication_method; 22 | fastcgi_param Shib-Authentication-Method $shib_shib_authentication_method; 23 | 24 | shib_request_set $shib_shib_authncontext_class $upstream_http_variable_shib_authncontext_class; 25 | fastcgi_param Shib-AuthnContext-Class $shib_shib_authncontext_class; 26 | 27 | shib_request_set $shib_shib_authncontext_decl $upstream_http_variable_shib_authncontext_decl; 28 | fastcgi_param Shib-AuthnContext-Decl $shib_shib_authncontext_decl; 29 | 30 | shib_request_set $shib_shib_identity_provider $upstream_http_variable_shib_identity_provider; 31 | fastcgi_param Shib-Identity-Provider $shib_shib_identity_provider; 32 | 33 | shib_request_set $shib_shib_session_id $upstream_http_variable_shib_session_id; 34 | fastcgi_param Shib-Session-ID $shib_shib_session_id; 35 | 36 | shib_request_set $shib_shib_session_index $upstream_http_variable_shib_session_index; 37 | fastcgi_param Shib-Session-Index $shib_shib_session_index; 38 | 39 | shib_request_set $shib_remote_user $upstream_http_variable_remote_user; 40 | fastcgi_param Remote-User $shib_remote_user; 41 | 42 | 43 | # Uncomment any of the following core attributes. Consult your Shibboleth 44 | # Service Provider (SP) attribute-map.xml file for details about attribute 45 | # IDs. Add additional directives for any Shibboleth attributes released to 46 | # your SP. 47 | 48 | # shib_request_set $shib_eppn $upstream_http_variable_eppn; 49 | # fastcgi_param EPPN $shib_eppn; 50 | # 51 | # shib_request_set $shib_affliation $upstream_http_variable_affiliation; 52 | # fastcgi_param Affiliation $shib_affiliation; 53 | # 54 | # shib_request_set $shib_unscoped_affliation $upstream_http_variable_unscoped_affiliation; 55 | # fastcgi_param Unscoped-Affiliation $shib_unscoped_affiliation; 56 | # 57 | # shib_request_set $shib_entitlement $upstream_http_variable_entitlement; 58 | # fastcgi_param Entitlement $shib_entitlement; 59 | 60 | 61 | # shib_request_set $shib_targeted_id $upstream_http_variable_targeted_id; 62 | # fastcgi_param Targeted-Id $shib_targeted_id; 63 | # 64 | # shib_request_set $shib_persistent_id $upstream_http_variable_persistent_id; 65 | # fastcgi_param Persistent-Id $shib_persistent_id; 66 | # 67 | # shib_request_set $shib_transient_name $upstream_http_variable_transient_name; 68 | # fastcgi_param Transient-Name $shib_transient_name; 69 | 70 | 71 | # shib_request_set $shib_commonname $upstream_http_variable_commonname; 72 | # fastcgi_param Commonname $shib_commonname; 73 | # 74 | # shib_request_set $shib_displayname $upstream_http_variable_displayname; 75 | # fastcgi_param DisplayName $shib_displayname; 76 | # 77 | # shib_request_set $shib_email $upstream_http_variable_email; 78 | # fastcgi_param Email $shib_email; 79 | # 80 | # shib_request_set $shib_organizationname $upstream_http_variable_organizationname; 81 | # fastcgi_param OrganizationName $shib_organizationname; 82 | -------------------------------------------------------------------------------- /ngx_http_shibboleth_module.c: -------------------------------------------------------------------------------- 1 | 2 | /* 3 | * Original ngx_http_auth_request module: 4 | * Copyright (C) Maxim Dounin 5 | * Copyright (C) Nginx, Inc. 6 | * Forked Shibboleth dedicated module: 7 | * Copyright (C) 2013-2016, David Beitey (davidjb) 8 | * Copyright (C) 2014, Luca Bruno 9 | * Contains elements adapted from ngx_lua: 10 | * Copyright (C) 2009-2015, by Xiaozhe Wang (chaoslawful) chaoslawful@gmail.com. 11 | * Copyright (C) 2009-2015, by Yichun "agentzh" Zhang (章亦春) agentzh@gmail.com, CloudFlare Inc. 12 | * Contains elements adapted from ngx_headers_more: 13 | * Copyright (c) 2009-2017, Yichun "agentzh" Zhang (章亦春) agentzh@gmail.com, OpenResty Inc. 14 | * Copyright (c) 2010-2013, Bernd Dorn. 15 | * 16 | * Distributed under 2-clause BSD license, see LICENSE file. 17 | */ 18 | 19 | 20 | #include 21 | #include 22 | #include 23 | 24 | 25 | typedef struct ngx_http_shib_request_header_val_s ngx_http_shib_request_header_val_t; 26 | 27 | typedef ngx_int_t (*ngx_http_shib_request_set_header_pt)(ngx_http_request_t *r, 28 | ngx_http_shib_request_header_val_t *hv, ngx_str_t *value); 29 | 30 | 31 | typedef struct { 32 | ngx_str_t name; 33 | ngx_uint_t offset; 34 | ngx_http_shib_request_set_header_pt handler; 35 | } ngx_http_shib_request_set_header_t; 36 | 37 | 38 | struct ngx_http_shib_request_header_val_s { 39 | ngx_http_complex_value_t value; 40 | ngx_uint_t hash; 41 | ngx_str_t key; 42 | ngx_http_shib_request_set_header_pt handler; 43 | ngx_uint_t offset; 44 | }; 45 | 46 | 47 | typedef struct { 48 | ngx_str_t uri; 49 | ngx_array_t *vars; 50 | ngx_flag_t use_headers; 51 | } ngx_http_auth_request_conf_t; 52 | 53 | 54 | typedef struct { 55 | ngx_uint_t done; 56 | ngx_uint_t status; 57 | ngx_http_request_t *subrequest; 58 | } ngx_http_auth_request_ctx_t; 59 | 60 | 61 | typedef struct { 62 | ngx_int_t index; 63 | ngx_http_complex_value_t value; 64 | ngx_http_set_variable_pt set_handler; 65 | } ngx_http_auth_request_variable_t; 66 | 67 | 68 | static ngx_int_t ngx_http_auth_request_handler(ngx_http_request_t *r); 69 | static ngx_int_t ngx_http_auth_request_done(ngx_http_request_t *r, 70 | void *data, ngx_int_t rc); 71 | static ngx_int_t ngx_http_auth_request_set_variables(ngx_http_request_t *r, 72 | ngx_http_auth_request_conf_t *arcf, ngx_http_auth_request_ctx_t *ctx); 73 | static ngx_int_t ngx_http_auth_request_variable(ngx_http_request_t *r, 74 | ngx_http_variable_value_t *v, uintptr_t data); 75 | static void *ngx_http_auth_request_create_conf(ngx_conf_t *cf); 76 | static char *ngx_http_auth_request_merge_conf(ngx_conf_t *cf, 77 | void *parent, void *child); 78 | static ngx_int_t ngx_http_auth_request_init(ngx_conf_t *cf); 79 | static char *ngx_http_auth_request(ngx_conf_t *cf, ngx_command_t *cmd, 80 | void *conf); 81 | static char *ngx_http_auth_request_set(ngx_conf_t *cf, ngx_command_t *cmd, 82 | void *conf); 83 | 84 | /* Functions replicated from ngx_lua */ 85 | static ngx_int_t ngx_http_set_output_header(ngx_http_request_t *r, 86 | ngx_str_t key, ngx_str_t value); 87 | static ngx_int_t ngx_http_set_header(ngx_http_request_t *r, 88 | ngx_http_shib_request_header_val_t *hv, ngx_str_t *value); 89 | static ngx_int_t ngx_http_set_header_helper(ngx_http_request_t *r, 90 | ngx_http_shib_request_header_val_t *hv, ngx_str_t *value, 91 | ngx_table_elt_t **output_header, unsigned no_create); 92 | static ngx_int_t ngx_http_set_builtin_header(ngx_http_request_t *r, 93 | ngx_http_shib_request_header_val_t *hv, ngx_str_t *value); 94 | static ngx_int_t ngx_http_set_builtin_multi_header(ngx_http_request_t *r, 95 | ngx_http_shib_request_header_val_t *hv, ngx_str_t *value); 96 | static ngx_int_t ngx_http_set_last_modified_header(ngx_http_request_t *r, 97 | ngx_http_shib_request_header_val_t *hv, ngx_str_t *value); 98 | static ngx_int_t ngx_http_clear_builtin_header(ngx_http_request_t *r, 99 | ngx_http_shib_request_header_val_t *hv, ngx_str_t *value); 100 | static ngx_int_t ngx_http_clear_last_modified_header(ngx_http_request_t *r, 101 | ngx_http_shib_request_header_val_t *hv, ngx_str_t *value); 102 | static ngx_int_t ngx_http_set_location_header(ngx_http_request_t *r, 103 | ngx_http_shib_request_header_val_t *hv, ngx_str_t *value); 104 | 105 | 106 | /* Content-centric headers are ignored from being set since subrequest 107 | * response bodies aren't currently supported by Nginx. 108 | */ 109 | static ngx_http_shib_request_set_header_t ngx_http_shib_request_set_handlers[] = { 110 | 111 | { ngx_string("Server"), 112 | offsetof(ngx_http_headers_out_t, server), 113 | ngx_http_set_builtin_header }, 114 | 115 | { ngx_string("Date"), 116 | offsetof(ngx_http_headers_out_t, date), 117 | ngx_http_set_builtin_header }, 118 | 119 | { ngx_string("Content-Encoding"), 120 | offsetof(ngx_http_headers_out_t, content_encoding), 121 | NULL }, 122 | 123 | { ngx_string("Location"), 124 | offsetof(ngx_http_headers_out_t, location), 125 | ngx_http_set_location_header }, 126 | 127 | { ngx_string("Refresh"), 128 | offsetof(ngx_http_headers_out_t, refresh), 129 | ngx_http_set_builtin_header }, 130 | 131 | { ngx_string("Last-Modified"), 132 | offsetof(ngx_http_headers_out_t, last_modified), 133 | ngx_http_set_last_modified_header }, 134 | 135 | { ngx_string("Content-Range"), 136 | offsetof(ngx_http_headers_out_t, content_range), 137 | NULL }, 138 | 139 | { ngx_string("Accept-Ranges"), 140 | offsetof(ngx_http_headers_out_t, accept_ranges), 141 | ngx_http_set_builtin_header }, 142 | 143 | { ngx_string("WWW-Authenticate"), 144 | offsetof(ngx_http_headers_out_t, www_authenticate), 145 | ngx_http_set_builtin_header }, 146 | 147 | { ngx_string("Expires"), 148 | offsetof(ngx_http_headers_out_t, expires), 149 | ngx_http_set_builtin_header }, 150 | 151 | { ngx_string("ETag"), 152 | offsetof(ngx_http_headers_out_t, etag), 153 | ngx_http_set_builtin_header }, 154 | 155 | { ngx_string("Content-Length"), 156 | offsetof(ngx_http_headers_out_t, content_length), 157 | NULL }, 158 | 159 | { ngx_string("Content-Type"), 160 | offsetof(ngx_http_headers_out_t, content_type), 161 | NULL }, 162 | 163 | { ngx_string("Cache-Control"), 164 | offsetof(ngx_http_headers_out_t, cache_control), 165 | ngx_http_set_builtin_multi_header }, 166 | 167 | { ngx_null_string, 0, ngx_http_set_header } 168 | }; 169 | 170 | 171 | static ngx_command_t ngx_http_auth_request_commands[] = { 172 | 173 | { ngx_string("shib_request"), 174 | NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1, 175 | ngx_http_auth_request, 176 | NGX_HTTP_LOC_CONF_OFFSET, 177 | 0, 178 | NULL }, 179 | 180 | { ngx_string("shib_request_set"), 181 | NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE2, 182 | ngx_http_auth_request_set, 183 | NGX_HTTP_LOC_CONF_OFFSET, 184 | 0, 185 | NULL }, 186 | 187 | { ngx_string("shib_request_use_headers"), 188 | NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_FLAG, 189 | ngx_conf_set_flag_slot, 190 | NGX_HTTP_LOC_CONF_OFFSET, 191 | offsetof(ngx_http_auth_request_conf_t, use_headers), 192 | NULL }, 193 | 194 | ngx_null_command 195 | }; 196 | 197 | 198 | static ngx_http_module_t ngx_http_shibboleth_module_ctx = { 199 | NULL, /* preconfiguration */ 200 | ngx_http_auth_request_init, /* postconfiguration */ 201 | 202 | NULL, /* create main configuration */ 203 | NULL, /* init main configuration */ 204 | 205 | NULL, /* create server configuration */ 206 | NULL, /* merge server configuration */ 207 | 208 | ngx_http_auth_request_create_conf, /* create location configuration */ 209 | ngx_http_auth_request_merge_conf /* merge location configuration */ 210 | }; 211 | 212 | 213 | ngx_module_t ngx_http_shibboleth_module = { 214 | NGX_MODULE_V1, 215 | &ngx_http_shibboleth_module_ctx, /* module context */ 216 | ngx_http_auth_request_commands, /* module directives */ 217 | NGX_HTTP_MODULE, /* module type */ 218 | NULL, /* init master */ 219 | NULL, /* init module */ 220 | NULL, /* init process */ 221 | NULL, /* init thread */ 222 | NULL, /* exit thread */ 223 | NULL, /* exit process */ 224 | NULL, /* exit master */ 225 | NGX_MODULE_V1_PADDING 226 | }; 227 | 228 | 229 | static ngx_int_t 230 | ngx_http_auth_request_handler(ngx_http_request_t *r) 231 | { 232 | ngx_uint_t i; 233 | ngx_int_t rc; 234 | ngx_list_part_t *part; 235 | ngx_table_elt_t *h, *hi; 236 | ngx_http_request_t *sr; 237 | ngx_http_post_subrequest_t *ps; 238 | ngx_http_auth_request_ctx_t *ctx; 239 | ngx_http_auth_request_conf_t *arcf; 240 | 241 | arcf = ngx_http_get_module_loc_conf(r, ngx_http_shibboleth_module); 242 | 243 | if (arcf->uri.len == 0) { 244 | return NGX_DECLINED; 245 | } 246 | 247 | ngx_log_debug0(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, 248 | "shib request handler"); 249 | 250 | ctx = ngx_http_get_module_ctx(r, ngx_http_shibboleth_module); 251 | 252 | if (ctx != NULL) { 253 | if (!ctx->done) { 254 | return NGX_AGAIN; 255 | } 256 | 257 | /* 258 | * as soon as we are done - explicitly set variables to make 259 | * sure they will be available after internal redirects 260 | */ 261 | 262 | if (ngx_http_auth_request_set_variables(r, arcf, ctx) != NGX_OK) { 263 | return NGX_ERROR; 264 | } 265 | 266 | /* 267 | * Handle the subrequest 268 | * as per the FastCGI authorizer specification. 269 | */ 270 | ngx_log_debug0(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, 271 | "shib request authorizer handler"); 272 | sr = ctx->subrequest; 273 | 274 | if (ctx->status == NGX_HTTP_OK) { 275 | ngx_log_debug0(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, 276 | "shib request authorizer allows access"); 277 | 278 | if (arcf->use_headers) { 279 | /* 280 | * 200 response may include headers prefixed with `Variable-`, 281 | * copy these into main request headers 282 | */ 283 | part = &sr->headers_out.headers.part; 284 | h = part->elts; 285 | 286 | for (i = 0; /* void */; i++) { 287 | 288 | if (i >= part->nelts) { 289 | if (part->next == NULL) { 290 | break; 291 | } 292 | 293 | part = part->next; 294 | h = part->elts; 295 | i = 0; 296 | } 297 | 298 | if (h[i].hash == 0) { 299 | continue; 300 | } 301 | 302 | if (h[i].key.len >= 9 && 303 | ngx_strncasecmp(h[i].key.data, (u_char *) "Variable-", 9) == 0) { 304 | /* copy header into original request */ 305 | hi = ngx_list_push(&r->headers_in.headers); 306 | 307 | if (hi == NULL) { 308 | return NGX_HTTP_INTERNAL_SERVER_ERROR; 309 | } 310 | 311 | /* Strip the Variable- prefix */ 312 | hi->key.len = h[i].key.len - 9; 313 | hi->key.data = h[i].key.data + 9; 314 | hi->hash = ngx_hash_key(hi->key.data, hi->key.len); 315 | hi->value = h[i].value; 316 | 317 | hi->lowcase_key = ngx_pnalloc(r->pool, hi->key.len); 318 | if (hi->lowcase_key == NULL) { 319 | return NGX_HTTP_INTERNAL_SERVER_ERROR; 320 | } 321 | ngx_strlow(hi->lowcase_key, hi->key.data, hi->key.len); 322 | 323 | ngx_log_debug2( 324 | NGX_LOG_DEBUG_HTTP, r->connection->log, 0, 325 | "shib request authorizer copied header: \"%V: %V\"", 326 | &hi->key, &hi->value); 327 | } 328 | } 329 | 330 | } else { 331 | ngx_log_debug0(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, 332 | "shib request authorizer not using headers"); 333 | } 334 | 335 | 336 | return NGX_OK; 337 | } 338 | 339 | /* 340 | * Unconditionally return subrequest response status, headers 341 | * and content as per FastCGI spec (section 6.3). 342 | * 343 | * The subrequest response body cannot be returned as Nginx does not 344 | * currently support NGX_HTTP_SUBREQUEST_IN_MEMORY. 345 | */ 346 | ngx_log_debug0(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, 347 | "shib request authorizer returning sub-response"); 348 | 349 | /* copy status */ 350 | r->headers_out.status = sr->headers_out.status; 351 | 352 | /* copy headers */ 353 | part = &sr->headers_out.headers.part; 354 | h = part->elts; 355 | 356 | for (i = 0; /* void */; i++) { 357 | 358 | if (i >= part->nelts) { 359 | if (part->next == NULL) { 360 | break; 361 | } 362 | 363 | part = part->next; 364 | h = part->elts; 365 | i = 0; 366 | } 367 | 368 | rc = ngx_http_set_output_header(r, h[i].key, h[i].value); 369 | if (rc == NGX_ERROR) { 370 | return NGX_ERROR; 371 | } 372 | 373 | ngx_log_debug2( 374 | NGX_LOG_DEBUG_HTTP, r->connection->log, 0, 375 | "shib request authorizer returning header: \"%V: %V\"", 376 | &h[i].key, &h[i].value); 377 | } 378 | 379 | return ctx->status; 380 | } 381 | 382 | ctx = ngx_pcalloc(r->pool, sizeof(ngx_http_auth_request_ctx_t)); 383 | if (ctx == NULL) { 384 | return NGX_ERROR; 385 | } 386 | 387 | ps = ngx_palloc(r->pool, sizeof(ngx_http_post_subrequest_t)); 388 | if (ps == NULL) { 389 | return NGX_ERROR; 390 | } 391 | 392 | ps->handler = ngx_http_auth_request_done; 393 | ps->data = ctx; 394 | 395 | if (ngx_http_subrequest(r, &arcf->uri, NULL, &sr, ps, 396 | NGX_HTTP_SUBREQUEST_WAITED) 397 | != NGX_OK) 398 | { 399 | return NGX_ERROR; 400 | } 401 | 402 | /* 403 | * allocate fake request body to avoid attempts to read it and to make 404 | * sure real body file (if already read) won't be closed by upstream 405 | */ 406 | 407 | sr->request_body = ngx_pcalloc(r->pool, sizeof(ngx_http_request_body_t)); 408 | if (sr->request_body == NULL) { 409 | return NGX_ERROR; 410 | } 411 | 412 | /* 413 | * true FastCGI authorizers should always return the subrequest 414 | * response body but the Nginx FastCGI handler does not support 415 | * NGX_HTTP_SUBREQUEST_IN_MEMORY at present. 416 | */ 417 | sr->header_only = 1; 418 | 419 | ctx->subrequest = sr; 420 | 421 | ngx_http_set_ctx(r, ctx, ngx_http_shibboleth_module); 422 | 423 | return NGX_AGAIN; 424 | } 425 | 426 | 427 | static ngx_int_t 428 | ngx_http_auth_request_done(ngx_http_request_t *r, void *data, ngx_int_t rc) 429 | { 430 | ngx_http_auth_request_ctx_t *ctx = data; 431 | 432 | ngx_log_debug1(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, 433 | "shib request done s:%ui", r->headers_out.status); 434 | 435 | ctx->done = 1; 436 | ctx->status = r->headers_out.status; 437 | 438 | return rc; 439 | } 440 | 441 | 442 | static ngx_int_t 443 | ngx_http_auth_request_set_variables(ngx_http_request_t *r, 444 | ngx_http_auth_request_conf_t *arcf, ngx_http_auth_request_ctx_t *ctx) 445 | { 446 | ngx_str_t val; 447 | ngx_http_variable_t *v; 448 | ngx_http_variable_value_t *vv; 449 | ngx_http_auth_request_variable_t *av, *last; 450 | ngx_http_core_main_conf_t *cmcf; 451 | 452 | ngx_log_debug0(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, 453 | "shib request set variables"); 454 | 455 | if (arcf->vars == NULL) { 456 | return NGX_OK; 457 | } 458 | 459 | cmcf = ngx_http_get_module_main_conf(r, ngx_http_core_module); 460 | v = cmcf->variables.elts; 461 | 462 | av = arcf->vars->elts; 463 | last = av + arcf->vars->nelts; 464 | 465 | while (av < last) { 466 | /* 467 | * explicitly set new value to make sure it will be available after 468 | * internal redirects 469 | */ 470 | 471 | vv = &r->variables[av->index]; 472 | 473 | if (ngx_http_complex_value(ctx->subrequest, &av->value, &val) 474 | != NGX_OK) 475 | { 476 | return NGX_ERROR; 477 | } 478 | 479 | vv->valid = 1; 480 | vv->not_found = 0; 481 | vv->data = val.data; 482 | vv->len = val.len; 483 | 484 | if (av->set_handler) { 485 | /* 486 | * set_handler only available in cmcf->variables_keys, so we store 487 | * it explicitly 488 | */ 489 | 490 | av->set_handler(r, vv, v[av->index].data); 491 | } 492 | 493 | av++; 494 | } 495 | 496 | return NGX_OK; 497 | } 498 | 499 | 500 | static ngx_int_t 501 | ngx_http_auth_request_variable(ngx_http_request_t *r, 502 | ngx_http_variable_value_t *v, uintptr_t data) 503 | { 504 | ngx_log_debug0(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, 505 | "shib request variable"); 506 | 507 | v->not_found = 1; 508 | 509 | return NGX_OK; 510 | } 511 | 512 | 513 | static void * 514 | ngx_http_auth_request_create_conf(ngx_conf_t *cf) 515 | { 516 | ngx_http_auth_request_conf_t *conf; 517 | 518 | conf = ngx_pcalloc(cf->pool, sizeof(ngx_http_auth_request_conf_t)); 519 | if (conf == NULL) { 520 | return NULL; 521 | } 522 | 523 | /* 524 | * set by ngx_pcalloc(): 525 | * 526 | * conf->uri = { 0, NULL }; 527 | */ 528 | 529 | conf->vars = NGX_CONF_UNSET_PTR; 530 | conf->use_headers = NGX_CONF_UNSET; 531 | 532 | return conf; 533 | } 534 | 535 | 536 | static char * 537 | ngx_http_auth_request_merge_conf(ngx_conf_t *cf, void *parent, void *child) 538 | { 539 | ngx_http_auth_request_conf_t *prev = parent; 540 | ngx_http_auth_request_conf_t *conf = child; 541 | 542 | ngx_conf_merge_str_value(conf->uri, prev->uri, ""); 543 | ngx_conf_merge_ptr_value(conf->vars, prev->vars, NULL); 544 | ngx_conf_merge_value(conf->use_headers, prev->use_headers, 0); 545 | 546 | return NGX_CONF_OK; 547 | } 548 | 549 | 550 | static ngx_int_t 551 | ngx_http_auth_request_init(ngx_conf_t *cf) 552 | { 553 | ngx_http_handler_pt *h; 554 | ngx_http_core_main_conf_t *cmcf; 555 | 556 | cmcf = ngx_http_conf_get_module_main_conf(cf, ngx_http_core_module); 557 | 558 | h = ngx_array_push(&cmcf->phases[NGX_HTTP_ACCESS_PHASE].handlers); 559 | if (h == NULL) { 560 | return NGX_ERROR; 561 | } 562 | 563 | *h = ngx_http_auth_request_handler; 564 | 565 | return NGX_OK; 566 | } 567 | 568 | 569 | static char * 570 | ngx_http_auth_request(ngx_conf_t *cf, ngx_command_t *cmd, void *conf) 571 | { 572 | ngx_http_auth_request_conf_t *arcf = conf; 573 | 574 | ngx_str_t *value; 575 | 576 | if (arcf->uri.data != NULL) { 577 | return "is duplicate"; 578 | } 579 | 580 | value = cf->args->elts; 581 | 582 | if (ngx_strcmp(value[1].data, "off") == 0) { 583 | arcf->uri.len = 0; 584 | arcf->uri.data = (u_char *) ""; 585 | 586 | return NGX_CONF_OK; 587 | } 588 | 589 | arcf->uri = value[1]; 590 | 591 | return NGX_CONF_OK; 592 | } 593 | 594 | 595 | static char * 596 | ngx_http_auth_request_set(ngx_conf_t *cf, ngx_command_t *cmd, void *conf) 597 | { 598 | ngx_http_auth_request_conf_t *arcf = conf; 599 | 600 | ngx_str_t *value; 601 | ngx_http_variable_t *v; 602 | ngx_http_auth_request_variable_t *av; 603 | ngx_http_compile_complex_value_t ccv; 604 | 605 | value = cf->args->elts; 606 | 607 | if (value[1].data[0] != '$') { 608 | ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, 609 | "invalid variable name \"%V\"", &value[1]); 610 | return NGX_CONF_ERROR; 611 | } 612 | 613 | value[1].len--; 614 | value[1].data++; 615 | 616 | if (arcf->vars == NGX_CONF_UNSET_PTR) { 617 | arcf->vars = ngx_array_create(cf->pool, 1, 618 | sizeof(ngx_http_auth_request_variable_t)); 619 | if (arcf->vars == NULL) { 620 | return NGX_CONF_ERROR; 621 | } 622 | } 623 | 624 | av = ngx_array_push(arcf->vars); 625 | if (av == NULL) { 626 | return NGX_CONF_ERROR; 627 | } 628 | 629 | v = ngx_http_add_variable(cf, &value[1], NGX_HTTP_VAR_CHANGEABLE); 630 | if (v == NULL) { 631 | return NGX_CONF_ERROR; 632 | } 633 | 634 | av->index = ngx_http_get_variable_index(cf, &value[1]); 635 | if (av->index == NGX_ERROR) { 636 | return NGX_CONF_ERROR; 637 | } 638 | 639 | if (v->get_handler == NULL) { 640 | v->get_handler = ngx_http_auth_request_variable; 641 | v->data = (uintptr_t) av; 642 | } 643 | 644 | av->set_handler = v->set_handler; 645 | 646 | ngx_memzero(&ccv, sizeof(ngx_http_compile_complex_value_t)); 647 | 648 | ccv.cf = cf; 649 | ccv.value = &value[2]; 650 | ccv.complex_value = &av->value; 651 | 652 | if (ngx_http_compile_complex_value(&ccv) != NGX_OK) { 653 | return NGX_CONF_ERROR; 654 | } 655 | 656 | return NGX_CONF_OK; 657 | } 658 | 659 | 660 | /* Implementation adapted from ngx_lua/ngx_http_lua_headers_out.c. 661 | * 662 | * Primary difference is that header handling here ignores any headers 663 | * that have no handler configured, whereas the original code returns 664 | * an NGX_ERROR. 665 | */ 666 | 667 | static ngx_int_t 668 | ngx_http_set_header(ngx_http_request_t *r, ngx_http_shib_request_header_val_t *hv, 669 | ngx_str_t *value) 670 | { 671 | return ngx_http_set_header_helper(r, hv, value, NULL, 0); 672 | } 673 | 674 | 675 | static ngx_int_t 676 | ngx_http_set_header_helper(ngx_http_request_t *r, ngx_http_shib_request_header_val_t *hv, 677 | ngx_str_t *value, ngx_table_elt_t **output_header, 678 | unsigned no_create) 679 | { 680 | ngx_table_elt_t *h; 681 | ngx_list_part_t *part; 682 | ngx_uint_t i; 683 | unsigned matched = 0; 684 | 685 | #if 1 686 | if (r->headers_out.location 687 | && r->headers_out.location->value.len 688 | && r->headers_out.location->value.data[0] == '/') 689 | { 690 | /* XXX ngx_http_core_find_config_phase, for example, 691 | * may not initialize the "key" and "hash" fields 692 | * for a nasty optimization purpose, and 693 | * we have to work-around it here */ 694 | 695 | r->headers_out.location->hash = ngx_hash_key((u_char *) "location", 8); 696 | ngx_str_set(&r->headers_out.location->key, "Location"); 697 | } 698 | #endif 699 | 700 | part = &r->headers_out.headers.part; 701 | h = part->elts; 702 | 703 | for (i = 0; /* void */; i++) { 704 | 705 | if (i >= part->nelts) { 706 | if (part->next == NULL) { 707 | break; 708 | } 709 | 710 | part = part->next; 711 | h = part->elts; 712 | i = 0; 713 | } 714 | 715 | if (h[i].hash != 0 716 | && h[i].key.len == hv->key.len 717 | && ngx_strncasecmp(hv->key.data, h[i].key.data, h[i].key.len) == 0) 718 | { 719 | 720 | if (value->len == 0 || matched) { 721 | 722 | h[i].value.len = 0; 723 | h[i].hash = 0; 724 | 725 | } else { 726 | h[i].value = *value; 727 | h[i].hash = hv->hash; 728 | } 729 | 730 | if (output_header) { 731 | *output_header = &h[i]; 732 | } 733 | 734 | /* return NGX_OK; */ 735 | matched = 1; 736 | } 737 | } 738 | 739 | if (matched) { 740 | return NGX_OK; 741 | } 742 | 743 | if (no_create && value->len == 0) { 744 | return NGX_OK; 745 | } 746 | 747 | /* XXX we still need to create header slot even if the value 748 | * is empty because some builtin headers like Last-Modified 749 | * relies on this to get cleared */ 750 | 751 | h = ngx_list_push(&r->headers_out.headers); 752 | 753 | if (h == NULL) { 754 | return NGX_ERROR; 755 | } 756 | 757 | if (value->len == 0) { 758 | h->hash = 0; 759 | 760 | } else { 761 | h->hash = hv->hash; 762 | } 763 | 764 | h->key = hv->key; 765 | h->value = *value; 766 | #if defined(nginx_version) && nginx_version >= 1023000 767 | h->next = NULL; 768 | #endif 769 | 770 | h->lowcase_key = ngx_pnalloc(r->pool, h->key.len); 771 | if (h->lowcase_key == NULL) { 772 | return NGX_ERROR; 773 | } 774 | 775 | ngx_strlow(h->lowcase_key, h->key.data, h->key.len); 776 | 777 | if (output_header) { 778 | *output_header = h; 779 | } 780 | 781 | return NGX_OK; 782 | } 783 | 784 | 785 | static ngx_int_t 786 | ngx_http_set_location_header(ngx_http_request_t *r, 787 | ngx_http_shib_request_header_val_t *hv, ngx_str_t *value) 788 | { 789 | ngx_int_t rc; 790 | ngx_table_elt_t *h; 791 | 792 | rc = ngx_http_set_builtin_header(r, hv, value); 793 | if (rc != NGX_OK) { 794 | return rc; 795 | } 796 | 797 | /* 798 | * we do not set r->headers_out.location here to avoid the handling 799 | * the local redirects without a host name by ngx_http_header_filter() 800 | */ 801 | 802 | h = r->headers_out.location; 803 | if (h && h->value.len && h->value.data[0] == '/') { 804 | r->headers_out.location = NULL; 805 | } 806 | 807 | return NGX_OK; 808 | } 809 | 810 | 811 | static ngx_int_t 812 | ngx_http_set_builtin_header(ngx_http_request_t *r, 813 | ngx_http_shib_request_header_val_t *hv, ngx_str_t *value) 814 | { 815 | ngx_table_elt_t *h, **old; 816 | 817 | if (hv->offset) { 818 | old = (ngx_table_elt_t **) ((char *) &r->headers_out + hv->offset); 819 | 820 | } else { 821 | old = NULL; 822 | } 823 | 824 | if (old == NULL || *old == NULL) { 825 | return ngx_http_set_header_helper(r, hv, value, old, 0); 826 | } 827 | 828 | h = *old; 829 | 830 | if (value->len == 0) { 831 | h->hash = 0; 832 | h->value = *value; 833 | 834 | return NGX_OK; 835 | } 836 | 837 | h->hash = hv->hash; 838 | h->key = hv->key; 839 | h->value = *value; 840 | 841 | return NGX_OK; 842 | } 843 | 844 | 845 | static ngx_int_t 846 | ngx_http_set_builtin_multi_header(ngx_http_request_t *r, 847 | ngx_http_shib_request_header_val_t *hv, ngx_str_t *value) 848 | { 849 | #if defined(nginx_version) && nginx_version >= 1023000 850 | ngx_table_elt_t **headers, *h, *ho, **ph; 851 | 852 | headers = (ngx_table_elt_t **) ((char *) &r->headers_out + hv->offset); 853 | 854 | if (*headers) { 855 | for (h = (*headers)->next; h; h = h->next) { 856 | h->hash = 0; 857 | h->value.len = 0; 858 | } 859 | 860 | h = *headers; 861 | 862 | h->value = *value; 863 | 864 | if (value->len == 0) { 865 | h->hash = 0; 866 | 867 | } else { 868 | h->hash = hv->hash; 869 | } 870 | 871 | return NGX_OK; 872 | } 873 | 874 | for (ph = headers; *ph; ph = &(*ph)->next) { /* void */ } 875 | 876 | ho = ngx_list_push(&r->headers_out.headers); 877 | if (ho == NULL) { 878 | return NGX_ERROR; 879 | } 880 | 881 | ho->value = *value; 882 | ho->hash = hv->hash; 883 | ngx_str_set(&ho->key, "Cache-Control"); 884 | ho->next = NULL; 885 | *ph = ho; 886 | 887 | return NGX_OK; 888 | #else 889 | ngx_array_t *pa; 890 | ngx_table_elt_t *ho, **ph; 891 | ngx_uint_t i; 892 | 893 | pa = (ngx_array_t *) ((char *) &r->headers_out + hv->offset); 894 | 895 | if (pa->elts == NULL) { 896 | if (ngx_array_init(pa, r->pool, 2, sizeof(ngx_table_elt_t *)) 897 | != NGX_OK) 898 | { 899 | return NGX_ERROR; 900 | } 901 | } 902 | 903 | /* override old values (if any) */ 904 | 905 | if (pa->nelts > 0) { 906 | ph = pa->elts; 907 | for (i = 1; i < pa->nelts; i++) { 908 | ph[i]->hash = 0; 909 | ph[i]->value.len = 0; 910 | } 911 | 912 | ph[0]->value = *value; 913 | 914 | if (value->len == 0) { 915 | ph[0]->hash = 0; 916 | 917 | } else { 918 | ph[0]->hash = hv->hash; 919 | } 920 | 921 | return NGX_OK; 922 | } 923 | 924 | ph = ngx_array_push(pa); 925 | if (ph == NULL) { 926 | return NGX_ERROR; 927 | } 928 | 929 | ho = ngx_list_push(&r->headers_out.headers); 930 | if (ho == NULL) { 931 | return NGX_ERROR; 932 | } 933 | 934 | ho->value = *value; 935 | 936 | if (value->len == 0) { 937 | ho->hash = 0; 938 | 939 | } else { 940 | ho->hash = hv->hash; 941 | } 942 | 943 | ho->key = hv->key; 944 | *ph = ho; 945 | 946 | return NGX_OK; 947 | #endif 948 | } 949 | 950 | 951 | static ngx_int_t ngx_http_set_last_modified_header(ngx_http_request_t *r, 952 | ngx_http_shib_request_header_val_t *hv, ngx_str_t *value) 953 | { 954 | if (value->len == 0) { 955 | return ngx_http_clear_last_modified_header(r, hv, value); 956 | } 957 | 958 | r->headers_out.last_modified_time = ngx_http_parse_time(value->data, 959 | value->len); 960 | 961 | return ngx_http_set_builtin_header(r, hv, value); 962 | } 963 | 964 | 965 | static ngx_int_t 966 | ngx_http_clear_last_modified_header(ngx_http_request_t *r, 967 | ngx_http_shib_request_header_val_t *hv, ngx_str_t *value) 968 | { 969 | r->headers_out.last_modified_time = -1; 970 | 971 | return ngx_http_clear_builtin_header(r, hv, value); 972 | } 973 | 974 | 975 | static ngx_int_t 976 | ngx_http_clear_builtin_header(ngx_http_request_t *r, 977 | ngx_http_shib_request_header_val_t *hv, ngx_str_t *value) 978 | { 979 | value->len = 0; 980 | 981 | return ngx_http_set_builtin_header(r, hv, value); 982 | } 983 | 984 | 985 | static ngx_int_t 986 | ngx_http_set_output_header(ngx_http_request_t *r, ngx_str_t key, 987 | ngx_str_t value) 988 | { 989 | ngx_http_shib_request_header_val_t hv; 990 | ngx_http_shib_request_set_header_t *handlers = ngx_http_shib_request_set_handlers; 991 | ngx_uint_t i; 992 | 993 | hv.hash = ngx_hash_key_lc(key.data, key.len); 994 | hv.key = key; 995 | 996 | hv.offset = 0; 997 | hv.handler = NULL; 998 | 999 | for (i = 0; handlers[i].name.len; i++) { 1000 | if (hv.key.len != handlers[i].name.len 1001 | || ngx_strncasecmp(hv.key.data, handlers[i].name.data, 1002 | handlers[i].name.len) != 0) 1003 | { 1004 | continue; 1005 | } 1006 | 1007 | hv.offset = handlers[i].offset; 1008 | hv.handler = handlers[i].handler; 1009 | 1010 | break; 1011 | } 1012 | 1013 | if (handlers[i].name.len == 0 && handlers[i].handler) { 1014 | hv.offset = handlers[i].offset; 1015 | hv.handler = handlers[i].handler; 1016 | } 1017 | 1018 | /* if there is no handler, skip the header (eg Content-* headers) */ 1019 | if (hv.handler == NULL) { 1020 | return NGX_OK; 1021 | } 1022 | 1023 | return hv.handler(r, &hv, &value); 1024 | } 1025 | -------------------------------------------------------------------------------- /t/shibboleth.t: -------------------------------------------------------------------------------- 1 | # vi:filetype=perl 2 | 3 | use lib 'lib'; 4 | use Test::Nginx::Socket; 5 | 6 | # Choose how many times to run each request in a test block 7 | repeat_each(1); 8 | 9 | # Each `TEST` in __DATA__ below generates a block for each pattern match 10 | # count. Increase the magic number accordingly if adding new tests or 11 | # expanding checks in existing tests (this will add more blocks). 12 | plan tests => repeat_each() * (50); 13 | 14 | # Populate config for the dynamic module, if requested 15 | our $main_config = ''; 16 | my $SHIB_DYNAMIC_MODULE = $ENV{'SHIB_DYNAMIC_MODULE'}; 17 | if ($SHIB_DYNAMIC_MODULE && $SHIB_DYNAMIC_MODULE eq 'true') { 18 | my $SHIB_MODULE_PATH = $ENV{'SHIB_MODULE_PATH'} ? $ENV{'SHIB_MODULE_PATH'} : 'modules'; 19 | $main_config = "load_module $SHIB_MODULE_PATH/ngx_http_headers_more_filter_module.so; 20 | load_module $SHIB_MODULE_PATH/ngx_http_shibboleth_module.so;"; 21 | } 22 | 23 | our $config = <<'_EOC_'; 24 | # 401 must be returned with WWW-Authenticate header 25 | location /test1 { 26 | shib_request /noauth; 27 | } 28 | 29 | # 401 must be returned with WWW-Authenticate header 30 | # X-From-Main-Request header **must** be returned. 31 | location /test2 { 32 | more_set_headers 'X-From-Main-Request: true'; 33 | shib_request /noauth; 34 | } 35 | 36 | # 403 must be returned 37 | # X-Must-Not-Be-Present header **must not** be returned. 38 | location /test3 { 39 | shib_request /noauth-forbidden; 40 | } 41 | 42 | # 403 must be returned and final response have custom header. 43 | location /test4 { 44 | more_set_headers 'X-From-Request: true'; 45 | shib_request /noauth-forbidden; 46 | } 47 | 48 | # 301 must be returned and Location header set 49 | location /test5 { 50 | add_header X-Main-Request-Add-Header Foobar; 51 | shib_request /noauth-redir; 52 | } 53 | 54 | # 301 must be returned and custom header set 55 | # This proves that a subrequest's headers can be manipulated as 56 | # part of the main request. 57 | location /test6 { 58 | more_set_headers 'X-From-Main-Request: true'; 59 | shib_request /noauth-redir; 60 | } 61 | 62 | # 404 must be returned; a 200 here is incorrect 63 | # Check the console output from ``nginx.debug`` ensure lines 64 | # stating ``shib request authorizer copied header:`` are present. 65 | # Variable-* headers **must not** be present. 66 | location /test7 { 67 | shib_request /auth; 68 | shib_request_use_headers on; 69 | } 70 | 71 | # 200 for successful auth is required 72 | # X-From-Main-Request header **must** be returned. 73 | location /test8 { 74 | more_set_headers 'X-From-Main-Request: true'; 75 | shib_request /auth; 76 | shib_request_use_headers on; 77 | } 78 | 79 | # 403 must be returned with correct Content-Encoding, Content-Length, 80 | # Content-Type, and no Content-Range 81 | location /test9 { 82 | shib_request /noauth-ignored-headers; 83 | } 84 | 85 | # 403 must be returned with overwritten Server and Date headers 86 | location /test10 { 87 | shib_request /noauth-builtin-headers; 88 | } 89 | 90 | # 200 for successful auth is required 91 | # X-From-Main-Request header **must** be returned. 92 | # Headers MUST NOT be copied to the backend 93 | location /test11 { 94 | more_set_headers 'X-From-Main-Request: true'; 95 | shib_request /auth; 96 | shib_request_use_headers off; 97 | } 98 | 99 | #################### 100 | # Internal locations 101 | #################### 102 | 103 | # Mock backend authentication endpoints, simulating shibauthorizer 104 | # more_set_headers is used as Nginx header filters (add_header) ignore subrequests 105 | location /noauth { 106 | internal; 107 | more_set_headers 'WWW-Authenticate: noauth-block' 'X-From-Subrequest: true'; 108 | return 401 'Not authenticated'; 109 | } 110 | 111 | # more_set_headers is used as Nginx header filters (add_header) ignore subrequests 112 | location /noauth-redir { 113 | internal; 114 | more_set_headers 'X-From-Subrequest: true'; 115 | return 301 https://sp.example.org; 116 | } 117 | 118 | # more_set_headers is used as Nginx header filters (add_header) ignore subrequests 119 | location /noauth-forbidden { 120 | more_set_headers 'X-From-Subrequest: true'; 121 | return 403 "Not allowed"; 122 | } 123 | 124 | # more_set_headers is used as Nginx header filters (add_header) ignore subrequests 125 | location /noauth-ignored-headers { 126 | more_set_headers 'Content-Encoding: wrong'; 127 | more_set_headers 'Content-Length: 100'; 128 | more_set_headers 'Content-Type: etc/wrong'; 129 | more_set_headers 'Content-Range: 0-100'; 130 | return 403 "Not allowed"; 131 | } 132 | 133 | # more_set_headers is used as Nginx header filters (add_header) ignore subrequests 134 | location /noauth-builtin-headers { 135 | more_set_headers 'Server: FastCGI'; 136 | more_set_headers 'Date: today'; 137 | more_set_headers 'Location: https://sp.example.org'; 138 | return 403 "Not allowed"; 139 | } 140 | 141 | # more_set_headers is used as Nginx header filters (add_header) ignore subrequests 142 | location /auth { 143 | internal; 144 | more_set_headers "Variable-Email: david@example.org"; 145 | more_set_headers "Variable-Commonname: davidjb"; 146 | return 200 'Authenticated'; 147 | } 148 | _EOC_ 149 | 150 | worker_connections(128); 151 | no_shuffle(); 152 | no_diff(); 153 | ok(1 eq 1, "Dummy test, no Nginx"); 154 | run_tests(); 155 | 156 | __DATA__ 157 | 158 | === TEST 1: Testing 401 response 159 | --- config eval: $::config 160 | --- main_config eval: $::main_config 161 | --- request 162 | GET /test1 163 | --- error_code: 401 164 | --- response_headers 165 | WWW-Authenticate: noauth-block 166 | --- timeout: 10 167 | --- no_error_log eval 168 | qr/\[(warn|error|crit|alert|emerg)\]/ 169 | 170 | === TEST 2: Testing 401 response with main request header 171 | --- config eval: $::config 172 | --- main_config eval: $::main_config 173 | --- request 174 | GET /test2 175 | --- error_code: 401 176 | --- response_headers 177 | X-From-Main-Request: true 178 | WWW-Authenticate: noauth-block 179 | --- timeout: 10 180 | --- no_error_log eval 181 | qr/\[(warn|error|crit|alert|emerg)\]/ 182 | 183 | === TEST 3: Testing 403 response with main request header 184 | --- config eval: $::config 185 | --- main_config eval: $::main_config 186 | --- request 187 | GET /test3 188 | --- error_code: 403 189 | --- response_headers 190 | X-Must-Not-Be-Present: 191 | --- timeout: 10 192 | --- no_error_log eval 193 | qr/\[(warn|error|crit|alert|emerg)\]/ 194 | 195 | === TEST 4: Testing 403 response with main request header 196 | --- config eval: $::config 197 | --- main_config eval: $::main_config 198 | --- request 199 | GET /test4 200 | --- error_code: 403 201 | --- response_headers 202 | X-From-Request: true 203 | --- timeout: 10 204 | --- no_error_log eval 205 | qr/\[(warn|error|crit|alert|emerg)\]/ 206 | 207 | === TEST 5: Testing redirection with in-built header addition 208 | --- config eval: $::config 209 | --- main_config eval: $::main_config 210 | --- request 211 | GET /test5 212 | --- error_code: 301 213 | --- response_headers 214 | Location: https://sp.example.org 215 | X-Main-Request-Add-Header: Foobar 216 | --- timeout: 10 217 | --- no_error_log eval 218 | qr/\[(warn|error|crit|alert|emerg)\]/ 219 | 220 | === TEST 6: Testing redirection with subrequest header manipulation in main request 221 | --- config eval: $::config 222 | --- main_config eval: $::main_config 223 | --- request 224 | GET /test6 225 | --- error_code: 301 226 | --- response_headers 227 | Location: https://sp.example.org 228 | X-From-Main-Request: true 229 | X-From-Subrequest: true 230 | --- timeout: 10 231 | --- no_error_log eval 232 | qr/\[(warn|error|crit|alert|emerg)\]/ 233 | 234 | === TEST 7: Testing successful auth, no leaked variables 235 | --- config eval: $::config 236 | --- main_config eval: $::main_config 237 | --- user_files 238 | >>> test7 239 | Hello, world 240 | --- request 241 | GET /test7 242 | --- error_code: 200 243 | --- response_headers 244 | Variable-Email: 245 | Variable-Commonname: 246 | --- timeout: 10 247 | --- no_error_log eval 248 | qr/\[(warn|error|crit|alert|emerg)\]/ 249 | --- grep_error_log eval 250 | qr/shib request.*/ 251 | --- grep_error_log_out eval 252 | qr/copied header/ 253 | 254 | === TEST 8: Testing successful auth, no leaked variables, main request headers set 255 | --- config eval: $::config 256 | --- main_config eval: $::main_config 257 | --- user_files 258 | >>> test8 259 | Hello, world 260 | --- request 261 | GET /test8 262 | --- error_code: 200 263 | --- response_headers 264 | Variable-Email: 265 | Variable-Commonname: 266 | X-From-Main-Request: true 267 | --- timeout: 10 268 | --- no_error_log eval 269 | qr/\[(warn|error|crit|alert|emerg)\]/ 270 | --- grep_error_log eval 271 | qr/shib request.*/ 272 | --- grep_error_log_out eval 273 | qr/shib request authorizer copied header:/ 274 | 275 | === TEST 9: Testing no auth with correct headers; subrequest header changes are ignored 276 | --- config eval: $::config 277 | --- main_config eval: $::main_config 278 | --- request 279 | GET /test9 280 | --- error_code: 403 281 | --- response_headers 282 | Content-Encoding: 283 | Content-Type: text/html 284 | Content-Range: 285 | --- timeout: 10 286 | --- no_error_log eval 287 | qr/\[(warn|error|crit|alert|emerg)\]/ 288 | 289 | === TEST 10: Testing no auth with overwritten headers; subrequest header changes are ignored 290 | --- config eval: $::config 291 | --- main_config eval: $::main_config 292 | --- request 293 | GET /test10 294 | --- error_code: 403 295 | --- response_headers_like 296 | Server: FastCGI 297 | Date: today 298 | Location: https://sp.example.org 299 | --- timeout: 10 300 | --- no_error_log eval 301 | qr/\[(warn|error|crit|alert|emerg)\]/ 302 | 303 | === TEST 11: Testing successful auth, no leaked variables, no headers set 304 | --- config eval: $::config 305 | --- main_config eval: $::main_config 306 | --- user_files 307 | >>> test11 308 | Hello, world 309 | --- request 310 | GET /test11 311 | --- error_code: 200 312 | --- response_headers 313 | Variable-Email: 314 | Variable-Commonname: 315 | X-From-Main-Request: true 316 | --- timeout: 10 317 | --- no_error_log eval 318 | qr/\[(warn|error|crit|alert|emerg)\]/ 319 | --- grep_error_log eval 320 | qr/shib request.*/ 321 | --- grep_error_log_out eval 322 | qr/shib request authorizer not using headers/ 323 | --------------------------------------------------------------------------------