diff --git a/backend-sample-app.py b/backend-sample-app.py
new file mode 100755
index 0000000..32fc910
--- /dev/null
+++ b/backend-sample-app.py
@@ -0,0 +1,135 @@
+#!/bin/sh
+''''which python2 >/dev/null && exec python2 "$0" "$@" # '''
+''''which python >/dev/null && exec python "$0" "$@" # '''
+
+# Example of an application working on port 9000
+# To interact with nginx-ldap-auth-daemon this application
+# 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
+from BaseHTTPServer import HTTPServer, BaseHTTPRequestHandler
+
+Listen = ('localhost', 9000)
+
+import threading
+from SocketServer import ThreadingMixIn
+class AuthHTTPServer(ThreadingMixIn, HTTPServer):
+ pass
+
+class AppHandler(BaseHTTPRequestHandler):
+
+ def do_GET(self):
+
+ url = urlparse.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')
+
+
+ # send login form html
+ def auth_form(self, target = None):
+
+ # try to get target location from header
+ if target == None:
+ target = self.headers.get('X-Target')
+
+ # form cannot be generated if target is unknown
+ if target == None:
+ self.log_error('target url is not passed')
+ self.send_response(500)
+ return
+
+ html="""
+
+
+
+
+ Auth form example
+
+
+
+
+"""
+
+ self.send_response(200)
+ self.end_headers()
+ self.wfile.write(html.replace('TARGET', target))
+
+
+ # processes posted form and sets the cookie with login/password
+ def do_POST(self):
+
+ # prepare arguments for cgi module to read posted form
+ env = {'REQUEST_METHOD':'POST',
+ 'CONTENT_TYPE': self.headers['Content-Type'],}
+
+ # read the form contents
+ form = cgi.FieldStorage(fp = self.rfile, headers = self.headers,
+ environ = env)
+
+ # extract required fields
+ user = form.getvalue('username')
+ passwd = form.getvalue('password')
+ target = form.getvalue('target')
+
+ if user != None and passwd != None and target != None:
+
+ # form is filled, set the cookie and redirect to target
+ # so that auth daemon will be able to use information from cookie
+
+ self.send_response(302)
+
+ # WARNING WARNING WARNING
+ #
+ # base64 is just an example method that allows to pack data into
+ # a cookie. You definitely want to perform some encryption here
+ # 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')
+
+ self.send_header('Location', target)
+ self.end_headers()
+
+ return
+
+ self.log_error('some form fields are not provided')
+ self.auth_form(target)
+
+
+ def log_message(self, format, *args):
+ if len(self.client_address) > 0:
+ addr = BaseHTTPRequestHandler.address_string(self)
+ else:
+ addr = "-"
+
+ sys.stdout.write("%s - - [%s] %s\n" % (addr,
+ self.log_date_time_string(), format % args))
+
+ def log_error(self, format, *args):
+ self.log_message(format, *args)
+
+
+def exit_handler(signal, frame):
+ sys.exit(0)
+
+if __name__ == '__main__':
+ server = AuthHTTPServer(Listen, AppHandler)
+ signal.signal(signal.SIGINT, exit_handler)
+ server.serve_forever()
diff --git a/nginx-ldap-auth-daemon-ctl.sh b/nginx-ldap-auth-daemon-ctl.sh
new file mode 100755
index 0000000..c79a4a5
--- /dev/null
+++ b/nginx-ldap-auth-daemon-ctl.sh
@@ -0,0 +1,17 @@
+#!/bin/sh
+
+CMD=./ngx-ldap-auth-daemon.py
+PIDFILE=./ngx-ldap-auth-daemon.pid
+LOGFILE=./ngx-ldap-auth-daemon.log
+
+case $1 in
+ "start")
+ start-stop-daemon -S -x $CMD -b -m -p $PIDFILE -1 $LOGFILE
+ ;;
+ "stop")
+ start-stop-daemon -K -p $PIDFILE
+ ;;
+ *)
+ echo "Usage: $0 "
+ ;;
+esac
diff --git a/nginx-ldap-auth-daemon.py b/nginx-ldap-auth-daemon.py
new file mode 100755
index 0000000..8de118a
--- /dev/null
+++ b/nginx-ldap-auth-daemon.py
@@ -0,0 +1,223 @@
+#!/bin/sh
+''''which python2 >/dev/null && exec python2 "$0" "$@" # '''
+''''which python >/dev/null && exec python "$0" "$@" # '''
+
+import sys, os, signal, base64, ldap, Cookie
+from BaseHTTPServer import HTTPServer, BaseHTTPRequestHandler
+
+Listen = ('localhost', 8888)
+#Listen = "/tmp/auth.sock" # uncomment unix sockets section below to use
+
+# -----------------------------------------------------------------------------
+# Different request processing models: select one
+# -----------------------------------------------------------------------------
+# requests are processed in separate thread
+import threading
+from SocketServer import ThreadingMixIn
+class AuthHTTPServer(ThreadingMixIn, HTTPServer):
+ pass
+# -----------------------------------------------------------------------------
+# requests are processed in separate process
+#from SocketServer import ForkingMixIn
+#class AuthHTTPServer(ForkingMixIn, HTTPServer):
+# pass
+# -----------------------------------------------------------------------------
+# unix sockets
+#import threading
+#from SocketServer import ThreadingUnixStreamServer
+#class AuthHTTPServer(ThreadingUnixStreamServer, HTTPServer):
+# pass
+# -----------------------------------------------------------------------------
+
+class AuthHandler(BaseHTTPRequestHandler):
+
+ # returns True if request processed and response sent, otherwise False
+ # sets ctx['user'] and ctx['pass'] for authentication
+ def do_GET(self):
+
+ ctx = self.ctx
+
+ ctx['action'] = 'input parameters check'
+ for k, v in self.get_params().items():
+ ctx[k] = self.headers.get(v[0], v[1])
+ if ctx[k] == None:
+ self.auth_failed(ctx, 'required "%s" header is not passed' % k)
+ return True
+
+ ctx['action'] = 'performing authoriazation'
+ auth_header = self.headers.get('Authorization')
+ auth_cookie = self.get_cookie(ctx['cookiename'])
+
+ if auth_cookie != None and auth_cookie != '':
+ auth_header = "Basic " + auth_cookie
+ self.log_message("using login/password from cookie %s" %
+ ctx['cookiename'])
+ else:
+ self.log_message("using login/password from authorization header")
+
+ 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('Cache-Control', 'no-cache')
+ self.end_headers()
+
+ return True
+
+ ctx['action'] = 'decoding credentials'
+
+ try:
+ auth_decoded = base64.b64decode(auth_header[6:])
+ user, passwd = auth_decoded.split(':', 2)
+
+ except:
+ self.auth_failed(ctx)
+ return True
+
+ ctx['user'] = user
+ ctx['pass'] = passwd
+
+ # continue request processing
+ return False
+
+ def get_cookie(self, name):
+ cookies = self.headers.get('Cookie')
+ if cookies:
+ authcookie = Cookie.BaseCookie(cookies).get(name)
+ if authcookie:
+ return authcookie.value
+ else:
+ return None
+ else:
+ return None
+
+
+ # Logs the error and completes the request with apropriate status
+ def auth_failed(self, ctx, errmsg = None):
+
+ msg = 'Error while ' + ctx['action']
+ if errmsg:
+ msg += ': ' + errmsg
+
+ ex, value, trace = sys.exc_info()
+
+ if ex != None:
+ msg += ": " + str(value)
+
+ if ctx.get('url'):
+ msg += ', server="%s"' % ctx['url']
+
+ if ctx.get('user'):
+ msg += ', login="%s"' % ctx['user']
+
+ self.log_error(msg)
+ self.send_response(403)
+ self.end_headers()
+
+ def get_params(self):
+ return {}
+
+ def log_message(self, format, *args):
+ if len(self.client_address) > 0:
+ addr = BaseHTTPRequestHandler.address_string(self)
+ else:
+ addr = "-"
+
+ sys.stdout.write("%s - %s [%s] %s\n" % (addr, self.ctx['user'],
+ self.log_date_time_string(), format % args))
+
+ def log_error(self, format, *args):
+ self.log_message(format, *args)
+
+
+# verifies user/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 {
+ # parameter header default
+ 'realm': ('X-Ldap-Realm', 'Restricted'),
+ 'url': ('X-Ldap-URL', None),
+ 'basedn': ('X-Ldap-BaseDN', None),
+ 'template': ('X-Ldap-Template', '(cn=%(username)s)'),
+ 'binddn': ('X-Ldap-BindDN', 'cn=anonymous'),
+ 'bindpasswd': ('X-Ldap-BindPass', ''),
+ 'cookiename': ('X-CookieName', '')
+ }
+
+ # GET handler for the authentication request
+ def do_GET(self):
+
+ ctx = dict()
+ self.ctx = ctx
+
+ ctx['action'] = 'initializing basic auth handler'
+ ctx['user'] = '-'
+
+ if AuthHandler.do_GET(self):
+ # request already processed
+ return
+
+ ctx['action'] = 'empty password check'
+ if not ctx['pass']:
+ self.auth_failed(ctx, 'attempt to use empty password')
+ return
+
+ try:
+ 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)
+
+ ctx['action'] = 'binding as search user'
+ ldap_obj.bind_s(ctx['binddn'], ctx['bindpasswd'], ldap.AUTH_SIMPLE)
+
+ ctx['action'] = 'preparing search filter'
+ searchfilter = ctx['template'] % { 'username': ctx['user'] }
+
+ self.log_message(('searching on server "%s" with base dn ' + \
+ '"%s" with filter "%s"') %
+ (ctx['url'], ctx['basedn'], searchfilter))
+
+ ctx['action'] = 'running search query'
+ results = ldap_obj.search_s(ctx['basedn'], ldap.SCOPE_SUBTREE,
+ searchfilter, ['objectclass'], 1)
+
+ ctx['action'] = 'verifying search query results'
+ if len(results) < 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
+ ldap_obj.bind_s(ldap_dn, ctx['pass'], ldap.AUTH_SIMPLE)
+
+ self.log_message('Auth OK for user "%s"' % (ctx['user']))
+
+ # successfully authenticated
+ self.send_response(200)
+ self.end_headers()
+
+ except:
+ self.auth_failed(ctx)
+
+def exit_handler(signal, frame):
+ global Listen
+
+ if isinstance(Listen, basestring):
+ try:
+ os.unlink(Listen)
+ except:
+ ex, value, trace = sys.exc_info()
+ sys.stderr.write('Failed to remove socket "%s": %s\n' %
+ (Listen, str(value)))
+ sys.exit(0)
+
+if __name__ == '__main__':
+ server = AuthHTTPServer(Listen, LDAPAuthHandler)
+ signal.signal(signal.SIGINT, exit_handler)
+ server.serve_forever()
diff --git a/nginx-ldap-auth.conf b/nginx-ldap-auth.conf
new file mode 100644
index 0000000..2e2aa4c
--- /dev/null
+++ b/nginx-ldap-auth.conf
@@ -0,0 +1,70 @@
+error_log logs/error.log debug;
+
+events { }
+
+http {
+
+ proxy_cache_path cache/ keys_zone=auth_cache:10m;
+
+ upstream backend {
+ server 127.0.0.1:9000;
+ }
+
+ server {
+
+ listen 127.0.0.1:8080;
+
+ server_name localhost;
+
+ location / {
+ auth_request /auth-proxy;
+
+ # redirect 401 and 403 to login form
+ error_page 401 =200 /login;
+ error_page 403 =200 /login;
+
+ proxy_pass http://backend/;
+ }
+
+ location /login {
+ proxy_pass http://backend/login;
+ # login service will return a redirect for user to original URI
+ # and set cookie for auth daemon
+ proxy_set_header X-TARGET $request_uri;
+ }
+
+ location = /auth-proxy {
+ internal;
+ # authorization daemon listens here
+ 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://example.com:636";
+ #proxy_set_header X-Ldap-BaseDN "ou=Users,dc=test,dc=local";
+
+ # user to search in directory, default is 'cn=anonymous'
+ #proxy_set_header X-Ldap-BindDN "cn=root,dc=test,dc=local";
+ # and password, default is no password
+ #proxy_set_header X-Ldap-BindPass "secret";
+
+ # Template to search for users: 'username' will be replaced
+ # default is for OpenLDAP:
+ # proxy_set_header X-Ldap-Template "(cn=%(username)s)";
+ # this one works for MS Active Directory
+ # proxy_set_header X-Ldap-Template "(SAMAccountName=%(username)s)";
+
+ # realm to present during basic auth, default is 'Restricted'
+ #proxy_set_header X-Ldap-Realm "PrivateArea";
+
+ # if form is used, pass cookie and its name
+ proxy_set_header X-CookieName "nginxauth";
+ proxy_set_header Cookie nginxauth=$cookie_nginxauth;
+
+ proxy_cache auth_cache;
+ # note that cookie is added to cache key
+ #proxy_cache_key "$http_authorization$cookie_nginxauth";
+ #proxy_cache_valid 200 403 10m;
+ }
+ }
+}