├── .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 |
--------------------------------------------------------------------------------