Compare commits

...

64 Commits

Author SHA1 Message Date
Timo Stark c98eb555f5
Merge pull request #104 from nginxinc/dwmcallister-patch-1
adds the missing Code of Conduct file
2023-03-04 03:12:13 -08:00
Dave McAllister a0528ea0cb
adds the missing Code of Conduct file
This adds the NGINX Code of Conduct file to the repo. This is one of the community guides required and is recognized by GitHub insights.
2023-03-01 11:46:38 -08:00
Dave McAllister 441312f180 Change README to indicate model approach
Clarify that this project is not designed for production work
2022-06-03 13:44:15 -07:00
Liam Crilly 2ef1e5cae4
Merge pull request #96 from tippexs/master
Escape Username in LDAP search filters
2022-04-19 09:34:41 +01:00
Timo Stark e00e31d949 Merge branch 'master' of https://github.com/nginxinc/nginx-ldap-auth 2022-04-17 22:38:40 +02:00
Timo Stark 0aab49006d Formatting 2022-04-17 22:38:28 +02:00
Timo Stark 23d036cb69 Formatting 2022-04-17 22:33:16 +02:00
Timo Stark c0a43f4800 Escape Username before using it in any search filter 2022-04-17 22:32:53 +02:00
Liam Crilly 763f23b297
Security improvements 2022-04-12 10:59:26 +01:00
Liam Crilly 5e5d5b1b86
Security improvements 2022-04-12 10:58:56 +01:00
Liam Crilly 3df1b7a9ea
Typo in comment 2022-04-12 08:53:14 +01:00
Liam Crilly d364261db8
Security improvements 2022-04-12 00:33:29 +01:00
Liam Crilly b60024a970
Security improvements 2022-04-12 00:32:47 +01:00
Vladimir Homutov ef8d313042
Merge pull request #73 from LPby/master
Fix for Python3
2020-01-16 13:31:16 +03:00
Pavel Lychkousky 48cdd5e593
Fix for Python3 2020-01-13 17:29:26 +03:00
Vladimir Homutov a3a04facf8 Python 3 support for the testsuite 2019-10-31 15:10:59 +03:00
Vladimir Homutov b5eca063d5 Removed trailing spaces from README. 2019-10-31 15:09:00 +03:00
Vladimir Homutov 54de6b5081
Merge pull request #71 from szuro/python3compatibility
Python3compatibility
2019-10-31 15:08:07 +03:00
Vladimir Homutov 7c164a4887 Added dockerfile for tests.
It is now possible to run testsuite in docker, using supplied file.
See instructions in t/README.

Tests are adjusted to run on alpine linux which includes modular builds
of openldap server software.
2019-09-26 19:04:32 +03:00
Robert Szulist a96bbe6a57 Add Python3 compatibility to sample app 2019-09-08 15:00:31 +02:00
Robert Szulist 8bd5c3ae21 Fix typo 2019-09-08 00:40:10 +02:00
Robert Szulist 83e28636fb Update README 2019-09-08 00:37:51 +02:00
Robert Szulist 9f01a465d8 Make code compatibile with python 2 and 3
Add chcecks for version for importing and decoding bytestring.
Inspired by PR #66
2019-09-08 00:30:55 +02:00
Robert Szulist 08fb44b66d Use python instead of python2
On used docker baseimage python is already linked to python2 or python3,
depending on image version
2019-09-07 23:03:13 +02:00
Robert Szulist d0e80bf79f Allow to select Python version at build
With this anyone can effectively use Python 2 or 3 using --build-arg
The default version is 2.
2019-09-07 17:53:27 +02:00
Vladimir Homutov 6fad4f3715
Merge pull request #64 from nikolaev-rd/patch-1
Formating fixed and optimized
2019-05-06 11:57:34 +03:00
Roman Nikolaev 8da8eef360
Formating fixed and optimized 2019-04-24 11:26:00 +03:00
Igor Ippolitov 3704dc25ff several minor fixes into spec file 2019-04-11 17:50:37 +03:00
A compound of Fe and O bbbde8d22b
Merge pull request #56 from alexjfisher/fix_spec_file
Fix logrotate and update rpm spec file
2019-02-07 13:42:01 +03:00
Alexander Fisher 61d8777204
Fix logrotate and update rpm spec file
* Create log directory in spec file
* Fix logrotate file
2018-11-16 13:01:18 +00:00
Vladimir Homutov d9a2149825 Added tests with multiple LDAP servers.
The directory is distributed on two servers, and search now may return
continuation object for specific users.
2018-10-29 11:46:59 +03:00
Vladimir Homutov 86687e2887 Added additional tests for user search results.
This fixes https://github.com/nginxinc/nginx-ldap-auth/issues/55.

It was possible to perform successful bind with unknown user with recent
versions of python-ldap, in case when LDAP server returned continuation
object and allowed anonymous bind.
2018-10-29 11:42:22 +03:00
Vladimir Homutov 57fb98b528 Added test suite.
The testsuite depends on nginx test suite, and requires an OpenLDAP server
installed.
2018-10-23 18:37:24 +03:00
Vladimir Homutov 850f5ea5ca
Merge pull request #42 from trunk-studio/fix/readme
Add Comment for HTTP basic authentication.
2018-08-20 12:47:35 +03:00
Vladimir Homutov f56178b6ee Added configuration option to disable referrals.
The options is boolean, header name is  'X-Ldap-DisableReferrals' and
the command-line switch is '--disable-referrals', default value is
false.
2018-08-20 12:31:55 +03:00
dd-han b5c580bac9 add Comment for HTTP basic authentication. 2018-04-19 15:26:59 +08:00
Igor Ippolitov 732eb15f07 fix log rotation in debian (#40) 2018-03-26 12:05:35 +03:00
A compound of Fe and O 3776f634c0
Merge pull request #34 from LMNetworks/issue_33
create missing etc/logrotate.d directory in buildroot
2018-01-16 16:30:04 +03:00
Vladimir Homutov 7ed1e2dfc9 Added StartTLS support.
This is a rebased version and slightly modified version of patch submitted by
Matthieu Cerda <matthieu.cerda@gmail.com> via pull-request #29
(https://github.com/nginxinc/nginx-ldap-auth/pull/29)
2017-12-25 13:04:42 +03:00
Vladimir Homutov b732f8c585 Fixed LDAP name of the "sAMAccountName" attribute.
The correct name starts with the lowercase 's'.

https://msdn.microsoft.com/en-us/library/ms679635
2017-12-25 13:04:42 +03:00
Vladimir Homutov d234e67497 Style: retabbed README.md 2017-12-25 13:04:42 +03:00
Vladimir Homutov 1262eaf8a3 Added Dockerfile for nginx-ldap-auth-daemon.py. 2017-12-22 19:14:10 +03:00
Vladimir Homutov cdc0abff91 Fixed typos in README.md 2017-12-22 17:22:39 +03:00
Vladimir Homutov 37be5adf9c Added default redirection destination to /dev/stdout.
When a nginx-ldap-auth-daemon.py is executed from console, its output
is set to /dev/stdout by default.  Otherwise, value of a 'LOG' variable is
used, exported by wrapper script.
2017-12-22 15:21:53 +03:00
Vladimir Homutov 8d187d9acf Replaced bash-specific redirections. 2017-12-22 15:02:12 +03:00
Vladimir Homutov 18e0b9c29c Fixed a typo in realm default value. 2017-12-22 14:21:23 +03:00
Vladimir Homutov 9df349f98e Removed trailing spaces. 2017-12-22 14:20:43 +03:00
Luca Lesinigo 38d220dd58 create missing etc/logrotate.d directory in buildroot 2017-12-20 14:23:43 +01:00
Vladimir Homutov b5de9a539c Merge pull request #28 from cawemo/kubernetes
Exit on SIGTERM for Kubernetes, print startup message
2017-10-25 12:17:39 +03:00
Christian Nicolai b860648a33 Exit on SIGTERM for Kubernetes, print startup message 2017-10-25 11:09:59 +02:00
Igor Ippolitov 8952b217a8 Put correct link to an architecture solution image 2017-09-12 12:25:18 +03:00
arozyev 937def0caa Added Template usage example to README.md 2017-05-17 14:46:03 +03:00
A compound of Fe and O f9e1a42329 Merge pull request #19 from nichivo/master
Fixed logging and typo for bind DN argument
2017-05-02 16:46:01 +03:00
Rick Hansen d66d4a04e7 Update spec file to rotate log file 2017-05-01 14:34:22 +10:00
Rick Hansen f94670848e Use unbuffered IO and redirect output to log file 2017-05-01 14:31:45 +10:00
Rick Hansen 481b02a979 Fixed typo for bind DN argument in .default 2017-05-01 14:28:41 +10:00
Vladimir Homutov 9d7cfcd1cc Merge pull request #14 from oxpa/master
Fixed a typo in .service file and fixed missing file in .install
2016-11-07 15:31:16 +04:00
Igor Ippolitov bd3f672763 Fixed a typo in .service file and fixed missing file in .install 2016-11-07 14:20:38 +03:00
Vladimir Homutov b56c9ef686 Merge pull request #13 from ArfyFR/patch-1
Quoted-string Basic realm ctx according to rfc7235
2016-11-03 14:10:01 +04:00
ArfyFR 64bb271b2e Quoted-string Basic realm ctx according to rfc7235
Hi,

I faced some problems with 401 message and an Android client.

It yelded because in the WWW-Authenticate header the
Basic ream=<ctx>
wasn't surrouned by ""

In the https://tools.ietf.org/html/rfc7235 it is written that 
 - Authentication parameters are name=value pairs
 - and "auth-param     = token BWS "=" BWS ( token / quoted-string )"
 - and "For historical reasons, a sender MUST only generate the quoted-string
   syntax.  Recipients might have to support both token and
   quoted-string syntax for maximum interoperability with existing
   clients that have been accepting both notations for a long time."

After my modification, the Android worked again (and iOs and PC clients faicing the 401 still worked ;) )

BR,
Arfy
2016-11-03 11:00:33 +01:00
Vladimir Homutov 9f7537ef34 Merge pull request #12 from oxpa/master
cli options for the daemon and basic debian packaging
2016-11-03 13:24:23 +04:00
Igor Ippolitov f824aee3ef files needed for debian packaging and minor changes into rpm spec 2016-11-02 20:06:42 +03:00
Igor Ippolitov 438518509d ensure required parameters are set at the time of a request 2016-11-02 20:06:42 +03:00
Igor Ippolitov 5fec096aa6 added options for commandline 2016-11-02 18:33:15 +03:00
24 changed files with 1240 additions and 80 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
.env
htmlcov

74
CODE_OF_CONDUCT.md Normal file
View File

@ -0,0 +1,74 @@
# Contributor Covenant Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to making participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, sex characteristics, gender identity and expression,
level of experience, education, socio-economic status, nationality, personal
appearance, race, religion, or sexual identity and orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment
include:
- Using welcoming and inclusive language
- Being respectful of differing viewpoints and experiences
- Gracefully accepting constructive criticism
- Focusing on what is best for the community
- Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
- The use of sexualized language or imagery and unwelcome sexual attention or
advances
- Trolling, insulting/derogatory comments, and personal or political attacks
- Public or private harassment
- Publishing others' private information, such as a physical or electronic
address, without explicit permission
- Other conduct which could reasonably be considered inappropriate in a
professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable
behavior and are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct, or to ban temporarily or
permanently any contributor for other behaviors that they deem inappropriate,
threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community. Examples of
representing a project or community include using an official project e-mail
address, posting via an official social media account, or acting as an appointed
representative at an online or offline event. Representation of a project may be
further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the moderation team at nginx-oss-community@f5.com. All
complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an incident.
Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other
members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org), version 1.4,
available at <https://www.contributor-covenant.org/version/1/4/code-of-conduct.html>
For answers to common questions about this code of conduct, see
<https://www.contributor-covenant.org/faq>

17
Dockerfile Normal file
View File

@ -0,0 +1,17 @@
ARG PYTHON_VERSION=2
FROM python:${PYTHON_VERSION}-alpine
COPY nginx-ldap-auth-daemon.py /usr/src/app/
WORKDIR /usr/src/app/
# Install required software
RUN \
apk --no-cache add openldap-dev && \
apk --no-cache add --virtual build-dependencies build-base && \
pip install python-ldap && \
apk del build-dependencies
EXPOSE 8888
CMD ["python", "/usr/src/app/nginx-ldap-auth-daemon.py", "--host", "0.0.0.0", "--port", "8888"]

50
Dockerfile.test Normal file
View File

@ -0,0 +1,50 @@
ARG PYTHON_VERSION=2
FROM python:${PYTHON_VERSION}-alpine
WORKDIR /usr/src/app/
COPY nginx-ldap-auth-daemon.py /usr/src/app/
WORKDIR /tests
COPY t/ldap-auth.t /tests
COPY t/runtests.sh /tests
# Install required software
RUN \
apk --no-cache add openldap-dev && \
apk --no-cache add openldap && \
apk --no-cache add openldap-back-hdb && \
apk --no-cache add openldap-clients && \
apk --no-cache add openssl && \
apk --no-cache add nginx && \
apk --no-cache add nginx-mod-http-geoip && \
apk --no-cache add nginx-mod-stream-geoip && \
apk --no-cache add nginx-mod-http-image-filter && \
apk --no-cache add nginx-mod-stream && \
apk --no-cache add nginx-mod-mail && \
apk --no-cache add nginx-mod-http-perl && \
apk --no-cache add nginx-mod-http-xslt-filter && \
apk --no-cache add mercurial && \
apk --no-cache add perl && \
apk --no-cache add --virtual build-dependencies build-base && \
pip install python-ldap && \
pip install coverage && \
apk del build-dependencies
# Install tests
RUN \
cd /tests && \
hg clone https://hg.nginx.org/nginx-tests && \
mv ldap-auth.t nginx-tests
WORKDIR /usr/src/app/
ENV TEST_LDAP_DAEMON=/usr/sbin/slapd
ENV TEST_LDAP_AUTH_DAEMON=/usr/src/app/nginx-ldap-auth-daemon.py
ENV TEST_NGINX_BINARY=/usr/sbin/nginx
ENV TEST_NGINX_MODULES=/usr/lib/nginx/modules
ENV LDAPTLS_REQCERT=never
WORKDIR /tests/nginx-tests
CMD ["/tests/runtests.sh"]

View File

@ -1,16 +1,18 @@
# nginx-ldap-auth
# PLEASE note that this project is *not designed or hardened* for production. It is intended as a model for such connector daemons
Reference implementation of method for authenticating users on behalf of servers proxied by NGINX or NGINX Plus
## The nginx-ldap-auth project
## Description
This project provides a reference model implementation of a method for authenticating users on behalf of servers proxied by NGINX or NGINX Plus.
**Note:** For ease of reading, this document refers to [NGINX Plus](http://www.nginx.com/products/), but it also applies to [open source NGINX](http://www.nginx.org/en). The prerequisite [ngx_http_auth_request_module](http://nginx.org/en/docs/http/ngx_http_auth_request_module.html) module is included both in [NGINX Plus packages](http://cs.nginx.com/repo_setup) and [prebuilt open source NGINX binaries](http://nginx.org/en/linux_packages.html).
### Note: ###
For ease of reading, this document refers to NGINX Plus, but it also applies to open source NGINX. The prerequisite ngx_http_auth_request_module module is included both in NGINX Plus packages and prebuilt open source NGINX binaries.
The ngx-ldap-auth software is a reference implementation of a method for authenticating users who request protected resources from servers proxied by NGINX Plus. It includes a daemon (*ldap-auth*) that communicates with an authentication server, and a sample daemon that stands in for an actual back-end server during testing, by generating an authentication cookie based on the users credentials. The daemons are written in Python for use with a Lightweight Directory Access Protocol (LDAP) authentication server (OpenLDAP or Microsoft Windows Active Directory 2003 and 2012).
### Description: ###
The nginx-ldap-auth software is a reference model implementation of a method for authenticating users who request protected resources from servers proxied by NGINX Plus. It includes a daemon (ldap-auth) that communicates with an authentication server, and a sample daemon that stands in for an actual back-end server during testing, by generating an authentication cookie based on the users credentials. The daemons are written in Python for use with a Lightweight Directory Access Protocol (LDAP) authentication server (OpenLDAP or Microsoft Windows Active Directory 2003 and 2012).
The ldap-auth daemon, which mediates between NGINX Plus and the LDAP server, is intended to serve as a model for "connector" daemons written in other languages, for different authentication systems, or both. [NGINX, Inc. Professional Services](http://nginx.com/services/) is available to assist with such adaptations.
The ldap-auth daemon, which mediates between NGINX Plus and the LDAP server, is intended to serve as a model for "connector" daemons written in other languages, for different authentication systems, or both. NGINX, Inc. Professional Services is available to assist with such adaptations.
![NGINX LDAP Architecture](http://nginx.wpengine.com/wp-content/uploads/2015/06/components-e1434577427617.jpg)
![NGINX LDAP Architecture](https://cdn-1.wp.nginx.com/wp-content/uploads/2016/02/ldap-auth-components.jpg)
For a step-by-step description of the authentication process in the reference implementation, see [How Authentication Works in the Reference Implementation](https://nginx.com/blog/nginx-plus-authenticate-users#ldap-auth-flow) in [NGINX Plus and NGINX Can Authenticate Application Users](https://nginx.com/blog/nginx-plus-authenticate-users).
@ -32,13 +34,25 @@ To install and configure the reference implementation, perform the following ste
1. On the host where the ldap-auth daemon is to run, install the following additional software. We recommend using the versions that are distributed with the operating system, instead of downloading the software from an open source repository.
- Python version 2. Version 3 is not supported.
- Python versions 2 and 3 are supported.
- The Python LDAP module, **python-ldap** (created by the [python-ldap.org](http://www.python-ldap.org) open source project).
1. Copy the following files from your repository clone to the indicated hosts:
- **nginx-ldap-auth.conf** NGINX Plus configuration file, which contains the minimal set of directives for testing the reference implementation. Install on the NGINX Plus host (in the **/etc/nginx/conf.d** directory if using the conventional configuration scheme). To avoid configuration conflicts, remember to move or rename any default configuration files installed with NGINX Plus.
- **nginx-ldap-auth-daemon.py** Python code for the ldap-auth daemon. Install on the host of your choice.
Alternatively, use provided Dockerfile to build Docker image:
```
docker build -t nginx-ldap-auth-daemon .
docker run nginx-ldap-auth-daemon
```
If you desire to use a container with Python3, you can supply an appropriate build argument:
```
docker build -t nginx-ldap-auth-daemon --build-arg PYTHON_VERSION=3 .
```
- **nginx-ldap-auth-daemon-ctl.sh** Sample shell script for starting and stopping the daemon. Install on the same host as the ldap-auth daemon.
- **backend-sample-app.py** Python code for the daemon that during testing stands in for a real back-end application server. Install on the host of your choice.
1. Modify the NGINX Plus configuration file as described in [Required Modifications to the NGINX Plus Configuration File](#required-mods) below. For information about customizing your deployment, see [Customization](#customization) below. We recommend running the `nginx -t` command after making your changes to verify that the file is syntactically valid.
@ -47,21 +61,25 @@ To install and configure the reference implementation, perform the following ste
<pre>root# <strong>nginx -s reload</strong></pre>
1. Run the following commands to start the ldap-auth daemon and the back-end daemon.
<pre>root# <strong>nginx-ldap-auth-daemon-ctl.sh start</strong>
root# <strong>python backend-sample-app.py</strong></pre>
<pre>
root# <strong>nginx-ldap-auth-daemon-ctl.sh start</strong>
root# <strong>python backend-sample-app.py</strong>
</pre>
1. To test the reference implementation, use a web browser to access **http://*nginx-server-address*:8081**. Verify that the browser presents a login form. After you fill out the form and submit it, verify that the server returns the expected response to valid credentials. The sample back-end daemon returns this:
<pre>Hello, world! Requested URL: <em>URL</em></pre>
<pre>
Hello, world! Requested URL: <em>URL</em>
</pre>
<a name="required-mods">
<a name="required-mods"></a>
### Required Modifications to the NGINX Plus Configuration File
</a>
Modify the **nginx-ldap-auth.conf** file, by changing values as appropriate for your deployment for the terms shown in bold font in the following configuration.
For detailed instructions, see [Configuring the Reference Implementation](https://nginx.com/blog/nginx-plus-authenticate-users#ldap-auth-configure) in the [NGINX Plus and NGINX Can Authenticate Application Users](https://nginx.com/blog/nginx-plus-authenticate-users) blog post. The **nginx-ldap-auth.conf** file includes detailed instructions (in comments not shown here) for setting the `proxy-set-header` directives; for information about other directives, see the [NGINX reference documentation](http://nginx.org/en/docs/).
<pre>http {
<pre>
http {
...
proxy_cache_path <strong>cache/</strong> keys_zone=<strong>auth_cache</strong>:<strong>10m</strong>;
@ -74,11 +92,17 @@ For detailed instructions, see [Configuring the Reference Implementation](https:
location = /auth-proxy {
proxy_pass http://<strong>127.0.0.1</strong>:8888;
proxy_pass_request_body off;
proxy_pass_request_headers off;
proxy_set_header Content-Length "";
proxy_cache <strong>auth_cache</strong>; # Must match the name in the proxy_cache_path directive above
proxy_cache_valid 200 <strong>10m</strong>;
# URL and port for connecting to the LDAP server
proxy_set_header X-Ldap-URL "<strong>ldaps</strong>://<strong>example.com</strong>:<strong>636</strong>";
proxy_set_header X-Ldap-URL "<strong>ldap</strong>://<strong>example.com</strong>";
# Negotiate a TLS-enabled (STARTTLS) connection before sending credentials
proxy_set_header X-Ldap-Starttls "true";
# Base DN
proxy_set_header X-Ldap-BaseDN "<strong>cn=Users,dc=test,dc=local</strong>";
@ -90,25 +114,38 @@ For detailed instructions, see [Configuring the Reference Implementation](https:
proxy_set_header X-Ldap-BindPass "<strong>secret</strong>";
}
}
}</pre>
}
</pre>
If the authentication server runs Active Directory rather than OpenLDAP, uncomment the following directive as shown:
```
proxy_set_header X-Ldap-Template "(SAMAccountName=%(username)s)";
proxy_set_header X-Ldap-Template "(sAMAccountName=%(username)s)";
```
The reference implementation uses cookie-based authentication. If you are using HTTP basic authentication instead, comment out the following directives as shown:
In addition, the **X-Ldap-Template** header can be used to create complex LDAP searches. The code in ldap-auth-daemon creates a search filter that is based on this template header. By default, template is empty, and does not make any effect on LDAP search. However, you may decide for instance to authenticate only users from a specific user group (see LDAP documentation for more information regarding filters).
<pre><strong>#</strong>proxy_set_header X-CookieName "nginxauth";
<strong>#</strong>proxy_set_header Cookie nginxauth=$cookie_nginxauth;</pre>
Suppose, your web resource should only be available for users from `group1` group.
In such a case you can define `X-Ldap-Template` template as follows:
```
proxy_set_header X-Ldap-Template "(&(cn=%(username)s)(memberOf=cn=group1,cn=Users,dc=example,dc=com))";
```
The search filters can be combined from less complex filters using boolean operations and can be rather complex.
The reference implementation uses cookie-based authentication. If you are using HTTP basic authentication instead, comment out the following directives, and enable the Authorization header as shown:
<pre>
<strong>#</strong>proxy_set_header X-CookieName "nginxauth";
<strong>#</strong>proxy_set_header Cookie nginxauth=$cookie_nginxauth;
<strong>proxy_set_header Authorization $http_authorization;</strong>
</pre>
## Customization
### Caching
The **nginx-ldap-auth.conf** file enables caching of both data and credentials. To disable caching, comment out the four `proxy_cache*` directives as shown:
<pre>http {
<pre>
http {
...
<strong>#</strong>proxy_cache_path cache/ keys_zone=auth_cache:10m;
...
@ -121,26 +158,28 @@ The **nginx-ldap-auth.conf** file enables caching of both data and credentials.
<strong>#</strong>proxy_cache_valid 200 10m;
}
}
}</pre>
}
</pre>
### Optional LDAP Parameters
If you want to change the value for the `template` parameter that the ldap-auth daemon passes to the OpenLDAP server by default, uncomment the following directive as shown, and change the value:
<pre>proxy_set_header X-Ldap-Template "<strong>(cn=%(username)s)</strong>";</pre>
<pre>
proxy_set_header X-Ldap-Template "<strong>(cn=%(username)s)</strong>";
</pre>
If you want to change the realm name from the default value (**Restricted**), uncomment and change the following directive:
<pre>proxy_set_header X-Ldap-Realm "<strong>Restricted</strong>";</pre>
<pre>
proxy_set_header X-Ldap-Realm "<strong>Restricted</strong>";
</pre>
### Authentication Server
To modify the ldap-auth daemon to communicate with a different (non-LDAP) type of authentication server, write a new authentication-handler class to replace `LDAPAuthHandler` in the **ngx-ldap-auth-daemon.py** script.
To modify the ldap-auth daemon to communicate with a different (non-LDAP) type of authentication server, write a new authentication-handler class to replace `LDAPAuthHandler` in the **nginx-ldap-auth-daemon.py** script.
## Compatibility
The auth daemon was tested against default configurations of the following LDAP servers:
* [OpenLDAP](http://www.openldap.org/)</li>
* Microsoft Windows Server Active Directory 2003</li>
* Microsoft Windows Server Active Directory 2012</li>

View File

@ -1,5 +1,4 @@
#!/bin/sh
''''which python2 >/dev/null && exec python2 "$0" "$@" # '''
''''which python >/dev/null && exec python "$0" "$@" # '''
# Copyright (C) 2014-2015 Nginx, Inc.
@ -9,13 +8,29 @@
# 1) accepts GET requests on /login and responds with a login form
# 2) accepts POST requests on /login, sets a cookie, and responds with redirect
import sys, os, signal, base64, Cookie, cgi, urlparse
import sys, os, signal, base64, cgi
if sys.version_info.major == 2:
from urlparse import urlparse
from Cookie import BaseCookie
from BaseHTTPServer import HTTPServer, BaseHTTPRequestHandler
elif sys.version_info.major == 3:
from urllib.parse import urlparse
from http.cookies import BaseCookie
from http.server import HTTPServer, BaseHTTPRequestHandler
Listen = ('localhost', 9000)
import threading
if sys.version_info.major == 2:
from SocketServer import ThreadingMixIn
elif sys.version_info.major == 3:
from socketserver import ThreadingMixIn
def ensure_bytes(data):
return data if sys.version_info.major == 2 else data.encode("utf-8")
class AuthHTTPServer(ThreadingMixIn, HTTPServer):
pass
@ -23,14 +38,14 @@ class AppHandler(BaseHTTPRequestHandler):
def do_GET(self):
url = urlparse.urlparse(self.path)
url = urlparse(self.path)
if url.path.startswith("/login"):
return self.auth_form()
self.send_response(200)
self.end_headers()
self.wfile.write('Hello, world! Requested URL: ' + self.path + '\n')
self.wfile.write(ensure_bytes('Hello, world! Requested URL: ' + self.path + '\n'))
# send login form html
@ -59,7 +74,7 @@ class AppHandler(BaseHTTPRequestHandler):
<tr>
<td>Username: <input type="text" name="username"/></td>
<tr>
<td>Password: <input type="text" name="password"/></td>
<td>Password: <input type="password" name="password"/></td>
<tr>
<td><input type="submit" value="Login"></td>
</table>
@ -70,7 +85,7 @@ class AppHandler(BaseHTTPRequestHandler):
self.send_response(200)
self.end_headers()
self.wfile.write(html.replace('TARGET', target))
self.wfile.write(ensure_bytes(html.replace('TARGET', target)))
# processes posted form and sets the cookie with login/password
@ -103,8 +118,10 @@ class AppHandler(BaseHTTPRequestHandler):
# and share a key with auth daemon that extracts this information
#
# WARNING WARNING WARNING
enc = base64.b64encode(user + ':' + passwd)
self.send_header('Set-Cookie', 'nginxauth=' + enc + '; httponly')
enc = base64.b64encode(ensure_bytes(user + ':' + passwd))
if sys.version_info.major == 3:
enc = enc.decode()
self.send_header('Set-Cookie', b'nginxauth=' + enc + b'; httponly')
self.send_header('Location', target)
self.end_headers()

5
debian/changelog vendored Normal file
View File

@ -0,0 +1,5 @@
nginx-ldap-auth (0.0.3-1) UNRELEASED; urgency=low
* Initial release
-- Ippolitov Igor <iippolitov@nginx.com> Wed, 02 Nov 2016 14:32:15 +0300

1
debian/compat vendored Normal file
View File

@ -0,0 +1 @@
9

14
debian/control vendored Normal file
View File

@ -0,0 +1,14 @@
Source: nginx-ldap-auth
Maintainer: Ippolitov Igor <iippolitov@nginx.com>
Section: misc
Priority: optional
Standards-Version: 3.9.7
Build-Depends: debhelper (>= 9), dh-systemd, python, dh-python, dh-exec
Package: nginx-ldap-auth
Architecture: all
Depends: systemd, python(>=2.6), python-ldap, python-argparse
Description: a reference implementation of an authentication helper for Nginx
This is a reference implementation of an authentication helper for Nginx.
It listens for incoming requests and uses parameters from headers
to bind to a remote LDAP directory and try authenticating a person.

0
debian/copyright vendored Normal file
View File

80
debian/nginx-ldap-auth.init vendored Executable file
View File

@ -0,0 +1,80 @@
#! /bin/sh
### BEGIN INIT INFO
# Provides: nginx-ldap-auth
# Required-Start: $syslog $remote_fs
# Required-Stop: $syslog $remote_fs
# Should-Start: $local_fs
# Should-Stop: $local_fs
# Default-Start: 2 3 4 5
# Default-Stop: 0 1 6
# Short-Description: nginx-ldap-auth - nginx helper for LDAP authentication
# Description: nginx-ldap-auth - nginx helper for LDAP authentication
### END INIT INFO
DAEMON=/usr/bin/nginx-ldap-auth-daemon
NAME=nginx-ldap-auth
test -x $DAEMON || exit 0
if [ -r /etc/default/$NAME ]
then
. /etc/default/$NAME
fi
. /lib/lsb/init-functions
set -e
case "$1" in
start)
echo -n "Starting $DESC: "
mkdir -p $RUNDIR
touch $PIDFILE
chown $USER:$GROUP $RUNDIR $PIDFILE
chmod 755 $RUNDIR
if [ -n "$ULIMIT" ]
then
ulimit -n $ULIMIT
fi
SSDOPTS="--quiet --oknodo --background --no-close --make-pidfile --pidfile $PIDFILE --chuid $USER:$GROUP --exec $DAEMON"
DAEMON_ARGS="$URL $BASE $BIND_DN $BIND_PASS $COOKIE $FILTER $REALM"
if start-stop-daemon --start $SSDOPTS -- $DAEMON_ARGS > $LOG 2>&1
then
echo "$NAME."
else
echo "failed"
fi
;;
stop)
echo -n "Stopping $DESC: "
if start-stop-daemon --stop --retry forever/TERM/1 --quiet --oknodo --remove-pidfile --pidfile $PIDFILE --exec $DAEMON
then
echo "$NAME."
else
echo "failed"
fi
sleep 1
;;
restart|force-reload)
${0} stop
${0} start
;;
status)
status_of_proc -p ${PIDFILE} ${DAEMON} ${NAME}
;;
*)
echo "Usage: /etc/init.d/$NAME {start|stop|restart|force-reload|status}" >&2
exit 1
;;
esac
exit 0

4
debian/nginx-ldap-auth.install vendored Executable file
View File

@ -0,0 +1,4 @@
#!/usr/bin/dh-exec
nginx-ldap-auth-daemon.py => usr/bin/nginx-ldap-auth-daemon
nginx-ldap-auth.default => etc/default/nginx-ldap-auth
nginx-ldap-auth.service => lib/systemd/system/nginx-ldap-auth.service

8
debian/nginx-ldap-auth.logrotate vendored Normal file
View File

@ -0,0 +1,8 @@
/var/log/nginx-ldap-auth/daemon.log {
daily
missingok
rotate 7
compress
notifempty
copytruncate
}

12
debian/nginx-ldap-auth.postinst vendored Normal file
View File

@ -0,0 +1,12 @@
#!/bin/sh
set -e
getent group nginx-ldap-auth > /dev/null || groupadd -r nginx-ldap-auth
getent passwd nginx-ldap-auth > /dev/null || \
useradd -r -d /var/run -g nginx-ldap-auth \
-s /sbin/nologin -c "Nginx auth helper" nginx-ldap-auth
install -d -m755 -o nginx-ldap-auth -g nginx-ldap-auth /var/log/nginx-ldap-auth
#DEBHELPER#

3
debian/rules vendored Executable file
View File

@ -0,0 +1,3 @@
#!/usr/bin/make -f
%:
dh $@ --with python2 --with systemd

View File

@ -1,13 +1,27 @@
#!/bin/sh
''''which python2 >/dev/null && exec python2 "$0" "$@" # '''
''''which python >/dev/null && exec python "$0" "$@" # '''
''''[ -z $LOG ] && export LOG=/dev/stdout # '''
''''which python >/dev/null && exec python -u "$0" "$@" >> $LOG 2>&1 # '''
# Copyright (C) 2014-2015 Nginx, Inc.
# Copyright (C) 2014-2022 Nginx, Inc.
import sys, os, signal, base64, ldap, Cookie
import sys
import os
import signal
import base64
import ldap
from ldap.filter import escape_filter_chars
import argparse
if sys.version_info.major == 2:
from Cookie import BaseCookie
from BaseHTTPServer import HTTPServer, BaseHTTPRequestHandler
elif sys.version_info.major == 3:
from http.cookies import BaseCookie
from http.server import HTTPServer, BaseHTTPRequestHandler
Listen = ('localhost', 8888)
if not hasattr(__builtins__, "basestring"): basestring = (str, bytes)
#Listen = ('localhost', 8888)
#Listen = "/tmp/auth.sock" # Also uncomment lines in 'Requests are
# processed with UNIX sockets' section below
@ -16,7 +30,12 @@ Listen = ('localhost', 8888)
# -----------------------------------------------------------------------------
# Requests are processed in separate thread
import threading
if sys.version_info.major == 2:
from SocketServer import ThreadingMixIn
elif sys.version_info.major == 3:
from socketserver import ThreadingMixIn
class AuthHTTPServer(ThreadingMixIn, HTTPServer):
pass
# -----------------------------------------------------------------------------
@ -61,7 +80,7 @@ class AuthHandler(BaseHTTPRequestHandler):
if auth_header is None or not auth_header.lower().startswith('basic '):
self.send_response(401)
self.send_header('WWW-Authenticate', 'Basic realm=' + ctx['realm'])
self.send_header('WWW-Authenticate', 'Basic realm="' + ctx['realm'] + '"')
self.send_header('Cache-Control', 'no-cache')
self.end_headers()
@ -71,14 +90,15 @@ class AuthHandler(BaseHTTPRequestHandler):
try:
auth_decoded = base64.b64decode(auth_header[6:])
if sys.version_info.major == 3: auth_decoded = auth_decoded.decode("utf-8")
user, passwd = auth_decoded.split(':', 1)
except:
self.auth_failed(ctx)
return True
ctx['user'] = user
ctx['pass'] = passwd
ctx['user'] = ldap.filter.escape_filter_chars(user)
# Continue request processing
return False
@ -86,7 +106,7 @@ class AuthHandler(BaseHTTPRequestHandler):
def get_cookie(self, name):
cookies = self.headers.get('Cookie')
if cookies:
authcookie = Cookie.BaseCookie(cookies).get(name)
authcookie = BaseCookie(cookies).get(name)
if authcookie:
return authcookie.value
else:
@ -115,7 +135,7 @@ class AuthHandler(BaseHTTPRequestHandler):
self.log_error(msg)
self.send_response(401)
self.send_header('WWW-Authenticate', 'Basic realm=' + ctx['realm'])
self.send_header('WWW-Authenticate', 'Basic realm="' + ctx['realm'] + '"')
self.send_header('Cache-Control', 'no-cache')
self.end_headers()
@ -142,13 +162,13 @@ class AuthHandler(BaseHTTPRequestHandler):
# Verify username/password against LDAP server
class LDAPAuthHandler(AuthHandler):
# Parameters to put into self.ctx from the HTTP header of auth request
def get_params(self):
return {
params = {
# parameter header default
'realm': ('X-Ldap-Realm', 'Restricted'),
'url': ('X-Ldap-URL', None),
'starttls': ('X-Ldap-Starttls', 'false'),
'disable_referrals': ('X-Ldap-DisableReferrals', 'false'),
'basedn': ('X-Ldap-BaseDN', None),
'template': ('X-Ldap-Template', '(cn=%(username)s)'),
'binddn': ('X-Ldap-BindDN', ''),
@ -156,6 +176,13 @@ class LDAPAuthHandler(AuthHandler):
'cookiename': ('X-CookieName', '')
}
@classmethod
def set_params(cls, params):
cls.params = params
def get_params(self):
return self.params
# GET handler for the authentication request
def do_GET(self):
@ -175,12 +202,35 @@ class LDAPAuthHandler(AuthHandler):
return
try:
# check that uri and baseDn are set
# either from cli or a request
if not ctx['url']:
self.log_message('LDAP URL is not set!')
return
if not ctx['basedn']:
self.log_message('LDAP baseDN is not set!')
return
ctx['action'] = 'initializing LDAP connection'
ldap_obj = ldap.initialize(ctx['url']);
# See http://www.python-ldap.org/faq.shtml
# uncomment, if required
# ldap_obj.set_option(ldap.OPT_REFERRALS, 0)
# Python-ldap module documentation advises to always
# explicitely set the LDAP version to use after running
# initialize() and recommends using LDAPv3. (LDAPv2 is
# deprecated since 2003 as per RFC3494)
#
# Also, the STARTTLS extension requires the
# use of LDAPv3 (RFC2830).
ldap_obj.protocol_version=ldap.VERSION3
# Establish a STARTTLS connection if required by the
# headers.
if ctx['starttls'] == 'true':
ldap_obj.start_tls_s()
# See https://www.python-ldap.org/en/latest/faq.html
if ctx['disable_referrals'] == 'true':
ldap_obj.set_option(ldap.OPT_REFERRALS, 0)
ctx['action'] = 'binding as search user'
ldap_obj.bind_s(ctx['binddn'], ctx['bindpasswd'], ldap.AUTH_SIMPLE)
@ -197,13 +247,27 @@ class LDAPAuthHandler(AuthHandler):
searchfilter, ['objectclass'], 1)
ctx['action'] = 'verifying search query results'
if len(results) < 1:
nres = len(results)
if nres < 1:
self.auth_failed(ctx, 'no objects found')
return
ctx['action'] = 'binding as an existing user'
ldap_dn = results[0][0]
ctx['action'] += ' "%s"' % ldap_dn
if nres > 1:
self.log_message("note: filter match multiple objects: %d, using first" % nres)
user_entry = results[0]
ldap_dn = user_entry[0]
if ldap_dn == None:
self.auth_failed(ctx, 'matched object has no dn')
return
self.log_message('attempting to bind using dn "%s"' % (ldap_dn))
ctx['action'] = 'binding as an existing user "%s"' % ldap_dn
ldap_obj.bind_s(ldap_dn, ctx['pass'], ldap.AUTH_SIMPLE)
self.log_message('Auth OK for user "%s"' % (ctx['user']))
@ -225,9 +289,64 @@ def exit_handler(signal, frame):
ex, value, trace = sys.exc_info()
sys.stderr.write('Failed to remove socket "%s": %s\n' %
(Listen, str(value)))
sys.stderr.flush()
sys.exit(0)
if __name__ == '__main__':
parser = argparse.ArgumentParser(
description="""Simple Nginx LDAP authentication helper.""")
# Group for listen options:
group = parser.add_argument_group("Listen options")
group.add_argument('--host', metavar="hostname",
default="localhost", help="host to bind (Default: localhost)")
group.add_argument('-p', '--port', metavar="port", type=int,
default=8888, help="port to bind (Default: 8888)")
# ldap options:
group = parser.add_argument_group(title="LDAP options")
group.add_argument('-u', '--url', metavar="URL",
default="ldap://localhost:389",
help=("LDAP URI to query (Default: ldap://localhost:389)"))
group.add_argument('-s', '--starttls', metavar="starttls",
default="false",
help=("Establish a STARTTLS protected session (Default: false)"))
group.add_argument('--disable-referrals', metavar="disable_referrals",
default="false",
help=("Sets ldap.OPT_REFERRALS to zero (Default: false)"))
group.add_argument('-b', metavar="baseDn", dest="basedn", default='',
help="LDAP base dn (Default: unset)")
group.add_argument('-D', metavar="bindDn", dest="binddn", default='',
help="LDAP bind DN (Default: anonymous)")
group.add_argument('-w', metavar="passwd", dest="bindpw", default='',
help="LDAP password for the bind DN (Default: unset)")
group.add_argument('-f', '--filter', metavar='filter',
default='(cn=%(username)s)',
help="LDAP filter (Default: cn=%%(username)s)")
# http options:
group = parser.add_argument_group(title="HTTP options")
group.add_argument('-R', '--realm', metavar='"Restricted Area"',
default="Restricted", help='HTTP auth realm (Default: "Restricted")')
group.add_argument('-c', '--cookie', metavar="cookiename",
default="", help="HTTP cookie name to set in (Default: unset)")
args = parser.parse_args()
global Listen
Listen = (args.host, args.port)
auth_params = {
'realm': ('X-Ldap-Realm', args.realm),
'url': ('X-Ldap-URL', args.url),
'starttls': ('X-Ldap-Starttls', args.starttls),
'disable_referrals': ('X-Ldap-DisableReferrals', args.disable_referrals),
'basedn': ('X-Ldap-BaseDN', args.basedn),
'template': ('X-Ldap-Template', args.filter),
'binddn': ('X-Ldap-BindDN', args.binddn),
'bindpasswd': ('X-Ldap-BindPass', args.bindpw),
'cookiename': ('X-CookieName', args.cookie)
}
LDAPAuthHandler.set_params(auth_params)
server = AuthHTTPServer(Listen, LDAPAuthHandler)
signal.signal(signal.SIGINT, exit_handler)
signal.signal(signal.SIGTERM, exit_handler)
sys.stdout.write("Start listening on %s:%d...\n" % Listen)
sys.stdout.flush()
server.serve_forever()

View File

@ -23,6 +23,8 @@ http {
auth_request /auth-proxy;
# redirect 401 to login form
# Comment them out if using HTTP basic authentication.
# or authentication popup won't show
error_page 401 =200 /login;
proxy_pass http://backend/;
@ -45,6 +47,7 @@ http {
proxy_pass http://127.0.0.1:8888;
proxy_pass_request_body off;
proxy_pass_request_headers off;
proxy_set_header Content-Length "";
proxy_cache auth_cache;
proxy_cache_valid 200 10m;
@ -53,7 +56,7 @@ http {
proxy_cache_key "$http_authorization$cookie_nginxauth";
# As implemented in nginx-ldap-auth-daemon.py, the ldap-auth daemon
# communicates with an OpenLDAP server, passing in the following
# communicates with a LDAP server, passing in the following
# parameters to specify which user account to authenticate. To
# eliminate the need to modify the Python code, this file contains
# 'proxy_set_header' directives that set the values of the
@ -61,17 +64,25 @@ http {
#
# Parameter Proxy header
# ----------- ----------------
# url X-Ldap-URL
# starttls X-Ldap-Starttls
# basedn X-Ldap-BaseDN
# binddn X-Ldap-BindDN
# bindpasswd X-Ldap-BindPass
# cookiename X-CookieName
# realm X-Ldap-Realm
# template X-Ldap-Template
# url X-Ldap-URL
# (Required) Set the URL and port for connecting to the LDAP server,
# by replacing 'example.com' and '636'.
proxy_set_header X-Ldap-URL "ldaps://example.com:636";
# by replacing 'example.com'.
# Do not mix ldaps-style URL and X-Ldap-Starttls as it will not work.
proxy_set_header X-Ldap-URL "ldap://example.com";
# (Optional) Establish a TLS-enabled LDAP session after binding to the
# LDAP server.
# This is the 'proper' way to establish encrypted TLS connections, see
# http://www.openldap.org/faq/data/cache/185.html
#proxy_set_header X-Ldap-Starttls "true";
# (Required) Set the Base DN, by replacing the value enclosed in
# double quotes.
@ -91,9 +102,17 @@ http {
proxy_set_header X-CookieName "nginxauth";
proxy_set_header Cookie nginxauth=$cookie_nginxauth;
# (Optional) Uncomment if using HTTP basic authentication
#proxy_set_header Authorization $http_authorization;
# (Required if using Microsoft Active Directory as the LDAP server)
# Set the LDAP template by uncommenting the following directive.
#proxy_set_header X-Ldap-Template "(SAMAccountName=%(username)s)";
#proxy_set_header X-Ldap-Template "(sAMAccountName=%(username)s)";
# (May be required if using Microsoft Active Directory and
# getting "In order to perform this operation a successful bind
# must be completed on the connection." errror)
#proxy_set_header X-Ldap-DisableReferrals "true";
# (Optional if using OpenLDAP as the LDAP server) Set the LDAP
# template by uncommenting the following directive and replacing

18
nginx-ldap-auth.default Normal file
View File

@ -0,0 +1,18 @@
#
# these are used with systemd too
# so please keep options names inside variables
#
#URL="--url ldap://example.com:389"
#BASE="-b dc=nodomain"
#BIND_DN="-D cn=admin,dc=nodomain"
#BIND_PASS="-w secret"
#COOKIE="-c nginxauth"
#FILTER="-f (cn=%(username)s)"
#REALM="-R 'Restricted Area'"
# these are used with init scripts only
LOG=/var/log/nginx-ldap-auth/daemon.log
RUNDIR=/var/run/nginx-ldap-auth/
PIDFILE=/var/run/nginx-ldap-auth/nginx-ldap-auth.pid
USER=nginx-ldap-auth
GROUP=nginx-ldap-auth

View File

@ -0,0 +1,9 @@
/var/log/nginx-ldap-auth/daemon.log {
compress
delaycompress
create 0644 nginx-ldap-auth nginx-ldap-auth
su nginx-ldap-auth nginx-ldap-auth
postrotate
/usr/bin/systemctl restart nginx-ldap-auth
endscript
}

View File

@ -4,11 +4,11 @@ After=network.target network-online.target
[Service]
Type=simple
User=nobody
Group=nobody
User=nginx-ldap-auth
Group=nginx-ldap-auth
WorkingDirectory=/var/run
PIDFile=/run/nginx-ldap-auth/nginx-ldap-auth.pid
ExecStart=/usr/bin/nginx-ldap-auth-daemon
EnvironmentFile=/etc/default/nginx-ldap-auth
ExecStart=/usr/bin/nginx-ldap-auth-daemon $URL $BASE $BIND_DN $BIND_PASS $COOKIE $FILTER $REALM
KillMode=process
KillSignal=SIGINT
Restart=on-failure

View File

@ -1,5 +1,7 @@
%global logdir /var/log/%name
Name: nginx-ldap-auth
Version: 0.0.3
Version: 0.0.5
Release: 1%{?dist}
Summary: NGINX Plus LDAP authentication daemon
@ -11,6 +13,8 @@ Source0: nginx-ldap-auth-release-%{version}.tar.gz
BuildRequires: systemd
Requires: systemd
Requires: python-ldap
Requires: python-argparse
Requires: logrotate
%description
Reference implementation of method for authenticating users on behalf of
@ -20,22 +24,41 @@ servers proxied by NGINX or NGINX Plus.
%setup -q
%install
ls
mkdir -p %buildroot%_bindir
install -m755 nginx-ldap-auth-daemon.py %buildroot%_bindir/nginx-ldap-auth-daemon
mkdir -p %buildroot%_unitdir
install -m644 nginx-ldap-auth.service %buildroot%_unitdir/
install -m644 %name.service %buildroot%_unitdir/
install -d -m755 %buildroot/etc/default
install -m644 %name.default %buildroot/etc/default/%name
install -d -m755 %buildroot/etc/logrotate.d
install -m644 %name.logrotate %buildroot%_sysconfdir/logrotate.d/%name
install -d -m755 %{buildroot}%{logdir}
%files
%doc README.md nginx-ldap-auth.conf backend-sample-app.py LICENSE
%config(noreplace) /etc/default/%name
%config(noreplace) %_sysconfdir/logrotate.d/%name
%_bindir/nginx-ldap-auth-daemon
%_unitdir/nginx-ldap-auth.service
%_unitdir/%name.service
%attr(750,nginx-ldap-auth,nginx-ldap-auth) %dir %{logdir}
%pre
getent group nginx-ldap-auth > /dev/null || groupadd -r nginx-ldap-auth
getent passwd nginx-ldap-auth > /dev/null || \
useradd -r -d /var/lib/nginx -g nginx-ldap-auth \
-s /sbin/nologin -c "Nginx auth helper" nginx-ldap-auth
%post
/usr/bin/systemctl preset nginx-ldap-auth.service
if [ $1 -eq 1 ]; then
/usr/bin/systemctl preset nginx-ldap-auth.service >/dev/null 2>&1 ||:
fi;
%preun
if [ $1 -eq 0 ]; then
/usr/bin/systemctl --no-reload disable nginx-ldap-auth.service >/dev/null 2>&1 ||:
/usr/bin/systemctl stop nginx-ldap-auth.service >/dev/null 2>&1 ||:
fi;
%postun
/usr/bin/systemctl daemon-reload >/dev/null 2>&1 ||:

38
t/README.md Normal file
View File

@ -0,0 +1,38 @@
To run tests use supplied Dockerfile.test:
```shell
docker build -t my-tag -f Dockerfile.test .
```
If you desire to use a container with Python3, you can supply an appropriate
build argument:
```shell
docker build -f Dockerfile.test -t my-tag --build-arg PYTHON_VERSION=3 .
docker run my-tag
```
To run without Docker:
Test suite is available at http://hg.nginx.org/nginx-tests.
Check the http://hg.nginx.org/nginx-tests/file/tip/README file
for instructions on how to use it.
Additionally, the test requires a working installation
of OpenLDAP server and utilities (http://www.openldap.org/),
and python's coverage tool (https://coverage.readthedocs.io)
copy ldap-auth.t into testsuite, setup environment variables:
$ export TEST_LDAP_DAEMON=/usr/lib64/openldap/slapd
$ export TEST_LDAP_AUTH_DAEMON=/path/to/nginx-ldap-auth-daemon.py
$ prove 'ldap-auth.t'
to get coverage report:
$ export TEST_NGINX_LEAVE=1
$ prove 'ldap-auth.t'
$ cd /tmp/nginx-test-xxxx
$ coverage2 html
report is now generated in htmlcov/

595
t/ldap-auth.t Normal file
View File

@ -0,0 +1,595 @@
#!/usr/bin/perl
# (C) Nginx, Inc.
# Test for nginx-ldap-auth daemon with OpenLDAP.
###############################################################################
use warnings;
use strict;
use Test::More;
use MIME::Base64;
BEGIN { use FindBin; chdir($FindBin::Bin); }
use lib 'lib';
use Test::Nginx;
###############################################################################
select STDERR; $| = 1;
select STDOUT; $| = 1;
my $t = Test::Nginx->new()->has(qw/http proxy rewrite auth_request/)
->write_file_expand('nginx.conf', <<'EOF');
%%TEST_GLOBALS%%
events { }
daemon off;
http {
%%TEST_GLOBALS_HTTP%%
#proxy_cache_path cache/ keys_zone=auth_cache:10m;
server {
listen 127.0.0.1:8082;
location / {
return 200 "ACCESS GRANTED\n";
}
location /login {
return 200 "LOGIN PAGE\n";
}
}
upstream backend {
server 127.0.0.1:8082;
}
server {
listen 127.0.0.1:8080;
location / {
auth_request /auth-proxy;
error_page 401 =200 /login;
proxy_pass http://backend/;
}
location /ssl {
auth_request /auth-proxy-ssl;
error_page 401 =200 /login;
proxy_pass http://backend/;
}
location /starttls {
auth_request /auth-proxy-starttls;
error_page 401 =200 /login;
proxy_pass http://backend/;
}
location /nodn {
auth_request /auth-nodn;
error_page 401 =200 /login;
proxy_pass http://backend/;
}
location /nourl {
auth_request /auth-nourl;
error_page 401 =200 /login;
proxy_pass http://backend/;
}
location /ref1 {
auth_request /auth-ref1;
error_page 401 =200 /login;
proxy_pass http://backend/;
}
location /query-injection {
auth_request /auth-query-injection;
error_page 401 =200 /login;
proxy_pass http://backend/;
}
location /login {
proxy_pass http://backend/login;
proxy_set_header X-Target $request_uri;
}
location = /auth-proxy {
internal;
proxy_pass http://127.0.0.1:8888;
proxy_pass_request_body off;
proxy_set_header Content-Length "";
#proxy_cache auth_cache;
#proxy_cache_valid 200 10m;
#proxy_cache_key "$http_authorization$cookie_nginxauth";
proxy_set_header X-Ldap-URL "ldap://127.0.0.1:8083";
proxy_set_header X-Ldap-BaseDN "ou=Users,dc=test,dc=local";
proxy_set_header X-Ldap-BindDN "cn=root,dc=test,dc=local";
proxy_set_header X-Ldap-BindPass "secret";
proxy_set_header X-CookieName "nginxauth";
proxy_set_header Cookie nginxauth=$cookie_nginxauth;
#proxy_set_header X-Ldap-Starttls "true";
#proxy_set_header X-Ldap-Template "(sAMAccountName=%(username)s)";
#proxy_set_header X-Ldap-DisableReferrals "true";
#proxy_set_header X-Ldap-Template "(cn=%(username)s)";
#proxy_set_header X-Ldap-Realm "Restricted";
}
location = /auth-proxy-ssl {
internal;
proxy_pass http://127.0.0.1:8888;
proxy_pass_request_body off;
proxy_set_header Content-Length "";
proxy_set_header X-Ldap-URL "ldaps://127.0.0.1:8084";
proxy_set_header X-Ldap-BaseDN "ou=Users,dc=test,dc=local";
proxy_set_header X-Ldap-BindDN "cn=root,dc=test,dc=local";
proxy_set_header X-Ldap-BindPass "secret";
proxy_set_header X-CookieName "nginxauth";
proxy_set_header Cookie nginxauth=$cookie_nginxauth;
#proxy_set_header X-Ldap-Starttls "true";
}
location = /auth-proxy-starttls {
internal;
proxy_pass http://127.0.0.1:8888;
proxy_pass_request_body off;
proxy_set_header Content-Length "";
proxy_set_header X-Ldap-URL "ldap://127.0.0.1:8083";
proxy_set_header X-Ldap-BaseDN "ou=Users,dc=test,dc=local";
proxy_set_header X-Ldap-BindDN "cn=root,dc=test,dc=local";
proxy_set_header X-Ldap-BindPass "secret";
proxy_set_header X-CookieName "nginxauth";
proxy_set_header Cookie nginxauth=$cookie_nginxauth;
proxy_set_header X-Ldap-Starttls "true";
}
location = /auth-nodn {
internal;
proxy_pass http://127.0.0.1:8888;
proxy_pass_request_body off;
proxy_set_header Content-Length "";
proxy_set_header X-Ldap-URL "ldap://127.0.0.1:8083";
proxy_set_header X-Ldap-BindDN "cn=root,dc=test,dc=local";
proxy_set_header X-Ldap-BindPass "secret";
}
location = /auth-nourl {
internal;
proxy_pass http://127.0.0.1:8888;
proxy_pass_request_body off;
proxy_set_header Content-Length "";
proxy_set_header X-Ldap-BaseDN "ou=Users,dc=test,dc=local";
proxy_set_header X-Ldap-BindDN "cn=root,dc=test,dc=local";
proxy_set_header X-Ldap-BindPass "secret";
}
location = /auth-ref1 {
internal;
proxy_pass http://127.0.0.1:8888;
proxy_pass_request_body off;
proxy_set_header Content-Length "";
proxy_set_header X-Ldap-URL "ldap://127.0.0.1:8083";
proxy_set_header X-Ldap-BaseDN "ou=Users,dc=test,dc=local";
proxy_set_header X-Ldap-BindDN "cn=root,dc=test,dc=local";
proxy_set_header X-Ldap-BindPass "secret";
proxy_set_header X-CookieName "nginxauth";
proxy_set_header Cookie nginxauth=$cookie_nginxauth;
}
location = /auth-query-injection {
internal;
proxy_pass http://127.0.0.1:8888;
proxy_pass_request_body off;
proxy_set_header Content-Length "";
proxy_set_header X-Ldap-URL "ldap://127.0.0.1:8083";
proxy_set_header X-Ldap-BaseDN "ou=Users,dc=test,dc=local";
proxy_set_header X-Ldap-BindDN "cn=root,dc=test,dc=local";
proxy_set_header X-Ldap-BindPass "secret";
proxy_set_header X-CookieName "nginxauth";
proxy_set_header Cookie nginxauth=$cookie_nginxauth;
proxy_set_header X-Ldap-Template '(|(&(memberOf=superadmin)(cn=%(username)s))(&(memberOf=admin)(cn=%(username)s)))';
}
}
}
EOF
my $d = $t->testdir();
$t->write_file('openssl.conf', <<EOF);
[ req ]
default_bits = 1024
encrypt_key = no
distinguished_name = req_distinguished_name
[ req_distinguished_name ]
EOF
foreach my $name ('localhost') {
system('openssl req -x509 -new '
. "-config $d/openssl.conf -subj /CN=$name/ "
. "-out $d/$name.crt -keyout $d/$name.key "
. ">>$d/openssl.out 2>&1") == 0
or die "Can't create certificate for $name: $!\n";
}
$t->write_file_expand("slapd.conf", <<"EOF");
include /etc/openldap/schema/core.schema
include /etc/openldap/schema/cosine.schema
include /etc/openldap/schema/inetorgperson.schema
include /etc/openldap/schema/nis.schema
include /etc/openldap/schema/misc.schema
pidfile $d/slapd.pid
argsfile $d/slapd.args
logfile $d/slapd.log
loglevel 256 64
access to dn.base="" by * read
access to dn.base="cn=Subschema" by * read
access to *
by self write
by users read
by anonymous read
moduleload back_hdb
database hdb
suffix "dc=test,dc=local"
rootdn "cn=root,dc=test,dc=local"
rootpw secret
directory $d/openldap-data
index objectClass eq
TLSCipherSuite HIGH:MEDIUM:+SSLv2
TLSCACertificateFile $d/localhost.crt
TLSCertificateFile $d/localhost.crt
TLSCertificateKeyFile $d/localhost.key
EOF
$t->write_file_expand("slapd2.conf", <<"EOF");
include /etc/openldap/schema/core.schema
include /etc/openldap/schema/cosine.schema
include /etc/openldap/schema/inetorgperson.schema
include /etc/openldap/schema/nis.schema
include /etc/openldap/schema/misc.schema
pidfile $d/slapd2.pid
argsfile $d/slapd2.args
logfile $d/slapd2.log
loglevel 256 64
access to dn.base="" by * read
access to dn.base="cn=Subschema" by * read
access to *
by self write
by users read
by anonymous read
moduleload back_hdb
database hdb
suffix "ou=Users, dc=test,dc=local"
rootdn "cn=root, ou=Users, dc=test,dc=local"
rootpw secret
directory $d/openldap2-data
index objectClass eq
TLSCipherSuite HIGH:MEDIUM:+SSLv2
TLSCACertificateFile $d/localhost.crt
TLSCertificateFile $d/localhost.crt
TLSCertificateKeyFile $d/localhost.key
# our upstream
referral ldap://127.0.0.1:%%PORT_8083%%/
EOF
$t->write_file_expand("initial.ldif", <<'EOF');
dn: dc=test,dc=local
dc: test
description: Test-OU
objectClass: dcObject
objectClass: organization
o: Example, Inc.
dn: ou=Users, dc=test,dc=local
ou: Users
description: All people in organisation
objectclass: organizationalunit
dn: cn=user1,ou=Users,dc=test,dc=local
objectclass: inetOrgPerson
cn: User1
sn: u1
uid: user1
userpassword: user1secret
mail: user1@example.com
description: user1
ou: Users
dn: cn=user2,ou=Users,dc=test,dc=local
objectclass: inetOrgPerson
cn: User2
sn: u2
uid: user2
userpassword: user2secret
mail: user2@example.com
description: user2
ou: Users
dn: cn=user3,ou=Users,dc=test,dc=local
objectclass: inetOrgPerson
cn: User3
sn: u3
uid: user3
userpassword: user3secret
mail: user3@example.com
description: user3
ou: Users
dn: ou=more,ou=Users,dc=test,dc=local
objectClass: referral
objectClass: extensibleObject
dc: subtree
ref: ldap://127.0.0.1:%%PORT_8085%%/ou=more,ou=Users,dc=test,dc=local
EOF
$t->write_file_expand("initial2.ldif", <<'EOF');
dn: ou=Users, dc=test,dc=local
ou: Users
description: All people in organisation
objectclass: organizationalunit
dn: ou=more,ou=Users,dc=test,dc=local
dc: test
description: Test-OU
objectClass: dcObject
objectClass: organizationalUnit
dn: cn=user4, ou=more, ou=Users,dc=test,dc=local
objectclass: inetOrgPerson
cn: User4
sn: u4
uid: user4
userpassword: user4secret
mail: user4@example.com
description: user4
ou: Users
EOF
# -u ldap -g ldap
my $SLAPD = defined $ENV{TEST_LDAP_DAEMON} ? $ENV{TEST_LDAP_DAEMON}
: '/usr/lib64/openldap/slapd';
my $AUTHD = defined $ENV{TEST_LDAP_AUTH_DAEMON} ? $ENV{TEST_LDAP_AUTH_DAEMON}
: 'nginx-ldap-auth-daemon.py';
$t->has_daemon($SLAPD);
$t->has_daemon($AUTHD);
mkdir("$d/openldap-data");
mkdir("$d/openldap2-data");
my $p3 = port(8083);
my $p4 = port(8084);
my $p5 = port(8085);
# change '0' to '1' or more to get debug from slapd
$t->run_daemon($SLAPD, '-d', '0', '-f', "$d/slapd.conf",
'-h', "ldap://127.0.0.1:$p3 ldaps://127.0.0.1:$p4");
$t->run_daemon($SLAPD, '-d', '0', '-f', "$d/slapd2.conf",
'-h', "ldap://127.0.0.1:$p5");
$t->waitforsocket("127.0.0.1:$p3") or die "Can't start slapd";
$t->waitforsocket("127.0.0.1:$p5") or die "Can't start slapd2";
system("ldapadd -H ldap://127.0.0.1:$p3 -x -D \"cn=root,dc=test,dc=local\""
. " -f $d/initial.ldif -w secret >> $d/ldif.log 2>&1") == 0
or die "Can't import initial LDIF\n";
system("ldapadd -H ldap://127.0.0.1:$p5 -x -D \"cn=root,ou=Users,dc=test,dc=local\""
. " -f $d/initial2.ldif -w secret >> $d/ldif2.log 2>&1") == 0
or die "Can't import initial2 LDIF\n";
$t->write_file_expand("auth_daemon.sh", <<"EOF");
AUTHBIN=\$(realpath $AUTHD)
cd $d
exec coverage run \$AUTHBIN --host 127.0.0.1 \\
-p %%PORT_8888%% >$d/nginx-ldap-auth-dameon.stdlog 2>&1
EOF
$t->run_daemon('/bin/sh', "$d/auth_daemon.sh");
$t->waitforsocket('127.0.0.1:' . port(8888))
or die "Can't start auth daemon";
$t->plan(22);
$t->run();
###############################################################################
like(http_get_auth('/', 'user1', 'user1secret'), qr!ACCESS GRANTED!,
'proper user with proper pass');
like(http_get_auth('/', 'user1', 'randompass'), qr!LOGIN PAGE!,
'proper user with incorrect pass');
like(http_get_auth('/', 'user111', 'user1secret'), qr!LOGIN PAGE!,
'similar user with user1 pass');
like(http_get_auth('/', 'randomuser', 'randompass'), qr!LOGIN PAGE!,
'random user with random pass');
like(http_get_auth('/', 'user2', 'user2secret'), qr!ACCESS GRANTED!,
'user2 with proper pass');
like(http_get_auth('/', 'user3', 'user3secret'), qr!ACCESS GRANTED!,
'user3 with proper pass');
like(http_get_auth('/', '', ''), qr!LOGIN PAGE!, 'empty user no password');
like(http_get('/'), qr!LOGIN PAGE!, 'no auth header');
like(http_get_cookie('/', 'user1', 'user1secret'), qr!ACCESS GRANTED!,
'proper user with proper pass cookie');
like(http_get_cookie('/', 'user1', 'randompasz'), qr!LOGIN PAGE!,
'proper user with incorrect pass cookie');
like(http_get_cookie('/', 'randomuser', 'randompass'), qr!LOGIN PAGE!,
'random user with random pass cookie');
like(http_get_cookie('/', 'user2', 'user2secret'), qr!ACCESS GRANTED!,
'user2 with proper pass cookie');
like(http_get_cookie('/', 'user3', 'user3secret'), qr!ACCESS GRANTED!,
'user3 with proper pass cookie');
like(http_get_auth_broken_base64('/', 'user3', 'user3secret'), qr!LOGIN PAGE!,
'user3 with proper pass broken base64');
like(http_get_cookie_broken_base64('/', 'user3', 'user3secret'), qr!LOGIN PAGE!,
'user3 with proper pass broken cookie');
like(http_get_auth('/ssl', 'user1', 'user1secret'), qr!ACCESS GRANTED!,
'proper user with proper pass with ssl');
like(http_get_auth('/starttls', 'user1', 'user1secret'), qr!ACCESS GRANTED!,
'proper user with proper pass with starttls');
# dn is not set, no default, daemon error => 502
like(http_get_auth('/nodn', 'user1', 'user1secret'), qr!Internal Server Error!,
'dn must be set');
# url is not set, default is used, which is not accessible => login page
like(http_get_auth('/nourl', 'user1', 'user1secret'), qr!LOGIN PAGE!,
'url must be set');
# LDAP referrals
# user can be found, but bind happens on 1st server, instead of the found
# the behaviour may change with different servers
like(http_get_auth('/ref1', 'user4', 'user4secret'), qr!LOGIN PAGE!,
'server2 user via referral on server1');
# unknown user on referred server, result is empty dn
like(http_get_auth('/ref1', 'unknow_user', 'unknowpassword'), qr!LOGIN PAGE!,
'unknown user with referral on server1');
# LDAP Query Injection result in 401
like(http_get_auth('/query-injection', 'user1))(|(cn=user1', 'user1secret'), qr!LOGIN PAGE!,
'Injection Attempt in Username will be escaped and blocked.');
###############################################################################
sub http_get_auth {
my ($url, $user, $password) = @_;
my $auth = encode_base64($user . ':' . $password, '');
return http(<<EOF);
GET $url HTTP/1.0
Host: localhost
Authorization: Basic $auth
EOF
}
# do not encode auth with base64, send plain
sub http_get_auth_broken_base64 {
my ($url, $user, $password) = @_;
my $auth = $user . ':' . $password;
return http(<<EOF);
GET $url HTTP/1.0
Host: localhost
Authorization: Basic $auth
EOF
}
sub http_get_cookie {
my ($url, $user, $password) = @_;
my $auth = encode_base64($user . ':' . $password, '');
return http(<<EOF);
GET $url HTTP/1.0
Host: localhost
Cookie: nginxauth=$auth
EOF
}
sub http_get_cookie_broken_base64 {
my ($url, $user, $password) = @_;
my $auth = $user . ':' . $password;
return http(<<EOF);
GET $url HTTP/1.0
Host: localhost
Cookie: nginxauth=$auth
EOF
}

13
t/runtests.sh Executable file
View File

@ -0,0 +1,13 @@
#!/bin/sh
# shell script to start testsuite and run coverage
# to be executed as Dockerfile CMD
export TEST_NGINX_LEAVE=1
rm -rf /tmp/nginx-test-*
perl ldap-auth.t
testdir=$(find /tmp -name 'nginx-test-*' -print -quit)
cd $testdir
coverage html && printf "Coverage report: docker cp <cid>:$testdir/htmlcov <hostdir>\n"