initial version
This commit is contained in:
187
duo_api.py
Executable file
187
duo_api.py
Executable file
@@ -0,0 +1,187 @@
|
||||
#! /usr/bin/env python3
|
||||
|
||||
#
|
||||
# duo_api.py - pure python3 implementation of an DUO API client
|
||||
#
|
||||
# Version 1.0, latest version, documentation and bugtracker available at:
|
||||
# https://gitlab.lindenaar.net/scripts/duo
|
||||
#
|
||||
# Copyright (c) 2019 Frederik Lindenaar
|
||||
#
|
||||
# This script is free software: you can redistribute and/or modify it under the
|
||||
# terms of version 3 of the GNU General Public License as published by the Free
|
||||
# Software Foundation, or (at your option) any later version of the license.
|
||||
#
|
||||
# This script is distributed in the hope that it will be useful but WITHOUT ANY
|
||||
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
# A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# this program. If not, visit <http://www.gnu.org/licenses/> to download it.
|
||||
#
|
||||
|
||||
""" Python3 DUO API client implementation using standard libraries.
|
||||
|
||||
This python script implements the client side of the DUO API using only
|
||||
python3 libaries installed by default (i.e. no DUO library) and can be used
|
||||
without the need to install any other libraries and outside a virtualenv.
|
||||
|
||||
The script could be used as library itself though that would defeat the
|
||||
whole purpose (apart from it being very simple/small) but it intended as
|
||||
starting point / template to implement you own custom logic directly. It
|
||||
contains a number of examples of the implemented direct and config-based
|
||||
method to get you started.
|
||||
|
||||
To run the config-file implementation of the client, simply create a config
|
||||
file (duo_api.conf) from the template (duo_api.tmpl) and configure it for
|
||||
your environment in the same directory as this script and run it. To use it
|
||||
without config file, comment out the code below and provide your values.
|
||||
|
||||
See https://duo.com/docs/authapi and https://duo.com/docs/adminapi for the
|
||||
available endpoints, parameters and responses.
|
||||
"""
|
||||
|
||||
|
||||
from base64 import b64encode
|
||||
from configparser import ConfigParser
|
||||
from email.utils import formatdate as timestamp
|
||||
from hashlib import sha1 as SHA1
|
||||
from hmac import new as hmac
|
||||
from json import loads as parse_json
|
||||
from os.path import splitext
|
||||
from sys import argv
|
||||
from urllib.error import HTTPError
|
||||
from urllib.parse import quote as urlquote
|
||||
from urllib.request import urlopen, Request
|
||||
|
||||
|
||||
def urlencode(p):
|
||||
""" URL-encodes HTTP paramaters provided in the passed dict / (k,v) list """
|
||||
return '&'.join([ urlquote(k,'~') + '=' + urlquote(v,'~')
|
||||
for (k, v) in (p.items() if isinstance(p, dict) else p if p else [] )])
|
||||
|
||||
|
||||
def duo_api(host, ikey, skey, path, params=None, method=None, proto='https'):
|
||||
""" Performs an authenticated DUO API request returning its JSON response
|
||||
|
||||
Parameters:
|
||||
host: API Hostname (from DUO Admin application config)
|
||||
ikey: API Integration Key (from DUO Admin application config)
|
||||
skey: API Secret Key (from DUO Admin application config)
|
||||
path: REST request path, e.g. '/auth/v2/check'
|
||||
params: dict with unencoded request parameters, will be URL-encoded
|
||||
method: http method, auto-detected (POST with params, GET otherwise)
|
||||
proto: protocol, defaults to 'https'
|
||||
|
||||
Returns: the API response (JSON Object)
|
||||
"""
|
||||
ts, method = timestamp(), method if method else 'POST' if params else 'GET'
|
||||
data = urlencode(sorted(params.items())) if params else ''
|
||||
auth = '\n'.join([ts, method.upper(), host.lower(), path, data]).encode()
|
||||
sig = (ikey + ':' + hmac(skey.encode(), auth, SHA1).hexdigest()).encode()
|
||||
return parse_json(urlopen(Request(proto + '://' + host + path, headers={
|
||||
'Date': ts, 'Authorization': 'Basic ' + b64encode(sig).decode()
|
||||
}, method=method, data=data.encode())).read())['response']
|
||||
|
||||
|
||||
def duo_api_config(config, req, vars=None):
|
||||
""" Performs an authenticated DUO API request with duo_api() using a dict()
|
||||
or ConfigParser Object containing at least the key (section) 'API'.
|
||||
|
||||
Parameters:
|
||||
config: ConfigParser object (or dict) with at least an API section
|
||||
section: name of the DUO API endpoint and config section to use
|
||||
vars: (optional) dict with variables for substitution in values
|
||||
|
||||
Returns: the API response (JSON Object)
|
||||
|
||||
To pass parameters with the request, provide a dict (section) with a key
|
||||
equal to req. Below sample ConfigParser config file for an auth request:
|
||||
|
||||
[API]
|
||||
host=api-XXXXXXXX.duosecurity.com
|
||||
ikey=XXXXXXXXXXXXXXXXXXXX
|
||||
skey=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
|
||||
path=/auth/v2/
|
||||
|
||||
[auth]
|
||||
username=username
|
||||
factor=push
|
||||
device=auto
|
||||
type=Network Access
|
||||
display_username=Test User
|
||||
pushinfo=explanation=Text+section(s)+shown+to+the+user&mode=TEST
|
||||
|
||||
Sections correspond with the API endpoint / request in req. For multiple
|
||||
instances of the same requests, append a suffix to the section name with
|
||||
an underscore ('_') as everything after that will be ignored as request
|
||||
name. Within the section keywords correspond to the request parameters.
|
||||
In case no parameters are required (e.g. for 'check' only API suffices).
|
||||
When using a ConfigParser object, variable substitution is supported for
|
||||
with the parameters passed in vars to make things dynamic.
|
||||
"""
|
||||
apiconf = config['API']
|
||||
path = (apiconf['path'] + '/' + req.split('_')[0]).replace('//', '/') \
|
||||
if 'path' in apiconf and req[0] != '/' else req.split('_')[0]
|
||||
params = config.get(req, None) if not isinstance(config, ConfigParser) \
|
||||
else dict(config.items(req, vars=vars)) if req in config else None
|
||||
return duo_api(apiconf['host'],apiconf['ikey'],apiconf['skey'],path,params)
|
||||
|
||||
|
||||
# Main logic to execute is case this is the called script
|
||||
if __name__ == '__main__':
|
||||
try:
|
||||
# Example using duo_api() directly, make sure you set parameters below!
|
||||
# host='api-XXXXXXXX.duosecurity.com'
|
||||
# ikey='XXXXXXXXXXXXXXXXXXXX'
|
||||
# skey='XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'
|
||||
# response = duo_api(host, ikey, skey, '/auth/v2/check')
|
||||
# print(response)
|
||||
|
||||
# More complex example, request DUO token authorization with parameters
|
||||
# params = { 'username': 'username',
|
||||
# 'factor': 'push',
|
||||
# 'device': 'auto',
|
||||
# 'type': 'Network Access',
|
||||
# 'display_username': 'Test User',
|
||||
# 'pushinfo': urlencode({
|
||||
# 'explanation': 'Text section(s) shown to the user',
|
||||
# 'mode': 'TEST'
|
||||
# })
|
||||
# }
|
||||
# response = duo_api(host, ikey, skey, '/auth/v2/auth', params)
|
||||
# print(response)
|
||||
# print('Access', response['result'])
|
||||
|
||||
# Same example as above, now using a config file, for this to work copy
|
||||
# the file duo_api.tmpl to duo_api.conf and update for your environment
|
||||
config = ConfigParser()
|
||||
if not config.read(splitext(argv[0])[0] + '.conf'):
|
||||
print("Missing/unreadable config file",splitext(argv[0])[0]+'.conf')
|
||||
exit(1)
|
||||
|
||||
apiconf = config['API']
|
||||
print(apiconf.get('aaa', fallback='bbb'))
|
||||
exit()
|
||||
|
||||
|
||||
response = duo_api_config(config, 'check') # Check connectivity/auth.
|
||||
print(response)
|
||||
|
||||
response = duo_api_config(config, 'auth') # trigger DUO authorization
|
||||
print(response)
|
||||
print('Access', response['result'])
|
||||
|
||||
# Example of a second authorization definition with dynamic parameters
|
||||
context = { # Populate context from the command line (if provided)
|
||||
'login': argv[1] if len(argv) > 1 else 'username',
|
||||
'name': argv[2] if len(argv) > 2 else 'Dynamic User',
|
||||
'banner': argv[3] if len(argv) > 3 else 'Access Request'
|
||||
}
|
||||
response = duo_api_config(config, 'auth_dynamic', vars=context)
|
||||
print(response)
|
||||
print('Access', response['result'])
|
||||
|
||||
except HTTPError as e:
|
||||
print('Configuration Error', e, '(invalid parameters?)')
|
||||
|
||||
Reference in New Issue
Block a user