summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJon Oberheide <jon@oberheide.org>2011-09-19 12:11:25 -0400
committerJon Oberheide <jon@oberheide.org>2011-09-19 12:11:25 -0400
commitd0bee24da4f193b0b570055e363147f50ba84a16 (patch)
tree76514edbd5a05fbffd9a0d673ae07c533ad0f52c
parent8a179fa9476c3fd9535f66fdb204d404ccf97c4b (diff)
downloadopenvpn-duo-plugin-d0bee24da4f193b0b570055e363147f50ba84a16.tar.gz
openvpn-duo-plugin-d0bee24da4f193b0b570055e363147f50ba84a16.zip
initial openvpn integration
-rw-r--r--README0
-rw-r--r--README.md24
-rwxr-xr-xduo_openvpn.py162
3 files changed, 186 insertions, 0 deletions
diff --git a/README b/README
deleted file mode 100644
index e69de29..0000000
--- a/README
+++ /dev/null
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..7befa2d
--- /dev/null
+++ b/README.md
@@ -0,0 +1,24 @@
+# Overview
+
+**duo_openvpn** - Duo two-factor authentication for OpenVPN
+
+Duo provides simple two-factor authentication as a service via:
+
+1. Phone callback
+2. SMS-delivered one-time passcodes
+3. Duo mobile app to generate one-time passcodes
+4. Duo mobile app for smartphone push authentication
+5. Duo hardware token to generate one-time passcodes
+
+# Usage
+
+OpenVPN integration instructions: <http://www.duosecurity.com/docs/openvpn>
+
+# Support
+
+Report any bugs, feature requests, etc. to us directly:
+<https://github.com/duosecurity/duo_openvpn/issues>
+
+Have fun!
+
+<http://www.duosecurity.com>
diff --git a/duo_openvpn.py b/duo_openvpn.py
new file mode 100755
index 0000000..3a7424d
--- /dev/null
+++ b/duo_openvpn.py
@@ -0,0 +1,162 @@
+#!/usr/bin/env python
+#
+# duo_openvpn.py
+# Duo OpenVPN v1
+# Copyright 2011 Duo Security, Inc.
+#
+
+import os, sys, urllib, hashlib, httplib, hmac, base64, json, syslog
+
+IKEY = ''
+SKEY = ''
+HOST = ''
+
+API_RESULT_AUTH = 'auth'
+API_RESULT_ALLOW = 'allow'
+API_RESULT_DENY = 'deny'
+API_RESULT_ENROLL = 'enroll'
+
+def canonicalize(method, host, uri, params):
+ canon = [method.upper(), host.lower(), uri]
+
+ args = []
+ for key in sorted(params.keys()):
+ val = params[key]
+ arg = '%s=%s' % (urllib.quote(key, '~'), urllib.quote(val, '~'))
+ args.append(arg)
+ canon.append('&'.join(args))
+
+ return '\n'.join(canon)
+
+def sign(ikey, skey, method, host, uri, params):
+ sig = hmac.new(skey, canonicalize(method, host, uri, params), hashlib.sha1)
+ auth = '%s:%s' % (ikey, sig.hexdigest())
+ return 'Basic %s' % base64.b64encode(auth)
+
+def call(ikey, skey, host, method, path, **kwargs):
+ headers = {'Authorization':sign(ikey, skey, method, host, path, kwargs)}
+
+ if method in [ 'POST', 'PUT' ]:
+ headers['Content-type'] = 'application/x-www-form-urlencoded'
+ body = urllib.urlencode(kwargs, doseq=True)
+ uri = path
+ else:
+ body = None
+ uri = path + '?' + urllib.urlencode(kwargs, doseq=True)
+
+ conn = httplib.HTTPSConnection(host, 443)
+ conn.request(method, uri, body, headers)
+ response = conn.getresponse()
+ data = response.read()
+ conn.close()
+
+ return (response.status, response.reason, data)
+
+def api(ikey, skey, host, method, path, **kwargs):
+ (status, reason, data) = call(ikey, skey, host, method, path, **kwargs)
+ if status != 200:
+ raise RuntimeError('Received %s %s: %s' % (status, reason, data))
+
+ try:
+ data = json.loads(data)
+ if data['stat'] != 'OK':
+ raise RuntimeError('Received error response: %s' % data)
+ return data['response']
+ except (ValueError, KeyError):
+ raise RuntimeError('Received bad response: %s' % data)
+
+def log(msg):
+ msg = 'Duo OpenVPN: %s' % msg
+ syslog.syslog(msg)
+
+def preauth(username):
+ log('pre-authentication for %s' % username)
+
+ args = {
+ 'user': username,
+ }
+
+ response = api(IKEY, SKEY, HOST, 'POST', '/rest/v1/preauth', **args)
+
+ result = response.get('result')
+
+ if not result:
+ log('invalid API response: %s' % response)
+ sys.exit(1)
+
+ if result == API_RESULT_AUTH:
+ return
+
+ status = response.get('status')
+
+ if not status:
+ log('invalid API response: %s' % response)
+ sys.exit(1)
+
+ if result == API_RESULT_ENROLL:
+ log('user %s is not enrolled: %s' % (username, status))
+ sys.exit(1)
+ elif result == API_RESULT_DENY:
+ log('preauth failure for %s: %s' % (username, status))
+ sys.exit(1)
+ elif result == API_RESULT_ALLOW:
+ log('preauth success for %s: %s' % (username, status))
+ sys.exit(0)
+ else:
+ log('unknown preauth result: %s' % result)
+ sys.exit(1)
+
+def auth(username, password, ipaddr):
+ log('authentication for %s' % username)
+
+ args = {
+ 'user': username,
+ 'factor': 'auto',
+ 'auto': password,
+ 'ipaddr': ipaddr
+ }
+
+ response = api(IKEY, SKEY, HOST, 'POST', '/rest/v1/auth', **args)
+
+ result = response.get('result')
+ status = response.get('status')
+
+ if not result or not status:
+ log('invalid API response: %s' % response)
+ sys.exit(1)
+
+ if result == API_RESULT_ALLOW:
+ log('auth success for %s: %s' % (username, status))
+ sys.exit(0)
+ elif result == API_RESULT_DENY:
+ log('auth failure for %s: %s' % (username, status))
+ sys.exit(1)
+ else:
+ log('unknown auth result: %s' % result)
+ sys.exit(1)
+
+def main():
+ username = os.environ.get('common_name')
+ password = os.environ.get('password')
+ ipaddr = os.environ.get('untrusted_ip', '0.0.0.0')
+
+ if not username or not password:
+ log('environment variables not found')
+ sys.exit(1)
+
+ try:
+ preauth(username)
+ except Exception, e:
+ log(str(e))
+ sys.exit(1)
+
+ try:
+ auth(username, password, ipaddr)
+ except Exception, e:
+ log(str(e))
+ sys.exit(1)
+
+ sys.exit(1)
+
+if __name__ == '__main__':
+ main()