From d0bee24da4f193b0b570055e363147f50ba84a16 Mon Sep 17 00:00:00 2001 From: Jon Oberheide Date: Mon, 19 Sep 2011 12:11:25 -0400 Subject: initial openvpn integration --- README | 0 README.md | 24 +++++++++ duo_openvpn.py | 162 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 186 insertions(+) delete mode 100644 README create mode 100644 README.md create mode 100755 duo_openvpn.py diff --git a/README b/README deleted file mode 100644 index e69de29..0000000 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: + +# Support + +Report any bugs, feature requests, etc. to us directly: + + +Have fun! + + 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() -- cgit v1.2.3