diff options
| author | Jon Oberheide <jon@oberheide.org> | 2011-09-19 12:11:25 -0400 |
|---|---|---|
| committer | Jon Oberheide <jon@oberheide.org> | 2011-09-19 12:11:25 -0400 |
| commit | d0bee24da4f193b0b570055e363147f50ba84a16 (patch) | |
| tree | 76514edbd5a05fbffd9a0d673ae07c533ad0f52c | |
| parent | 8a179fa9476c3fd9535f66fdb204d404ccf97c4b (diff) | |
| download | openvpn-duo-plugin-d0bee24da4f193b0b570055e363147f50ba84a16.tar.gz openvpn-duo-plugin-d0bee24da4f193b0b570055e363147f50ba84a16.zip | |
initial openvpn integration
| -rw-r--r-- | README | 0 | ||||
| -rw-r--r-- | README.md | 24 | ||||
| -rwxr-xr-x | duo_openvpn.py | 162 |
3 files changed, 186 insertions, 0 deletions
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() |
