initial version of daemons and configuration
This commit is contained in:
parent
809b608814
commit
21a68af1aa
|
|
@ -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="""
|
||||
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv=Content-Type content="text/html;charset=UTF-8">
|
||||
<title>Auth form example</title>
|
||||
</head>
|
||||
<body>
|
||||
<form action="/login" method="post">
|
||||
<table>
|
||||
<tr>
|
||||
<td>Username: <input type="text" name="username"/></td>
|
||||
<tr>
|
||||
<td>Password: <input type="text" name="password"/></td>
|
||||
<tr>
|
||||
<td><input type="submit"></td>
|
||||
</table>
|
||||
<input type="hidden" name="target" value="TARGET">
|
||||
</form>
|
||||
</body>
|
||||
</html>"""
|
||||
|
||||
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()
|
||||
|
|
@ -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 <start|stop>"
|
||||
;;
|
||||
esac
|
||||
|
|
@ -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()
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue