189 lines
5.7 KiB
Python
189 lines
5.7 KiB
Python
#!/usr/bin/python -tt
|
|
##
|
|
# Copyright (C) 2012 by Konstantin Ryabitsev and contributors
|
|
#
|
|
# This program is free software; you can redistribute it and/or
|
|
# modify it under the terms of the GNU General Public License
|
|
# as published by the Free Software Foundation; either version 2
|
|
# of the License, or (at your option) any later version.
|
|
#
|
|
# This program 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, write to the Free Software
|
|
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA
|
|
# 02111-1307, USA.
|
|
#
|
|
import os
|
|
import re
|
|
import sys
|
|
import cgi
|
|
import syslog
|
|
import logging
|
|
import urllib2
|
|
|
|
import cgitb
|
|
cgitb.enable()
|
|
|
|
import totpcgi
|
|
import totpcgi.backends
|
|
|
|
if len(sys.argv) > 1:
|
|
# blindly assume it's the config file
|
|
config_file = sys.argv[1]
|
|
else:
|
|
config_file = '/etc/totpcgi/totpcgi.conf'
|
|
|
|
import ConfigParser
|
|
|
|
from fedora.client import AuthError
|
|
from fedora.client.fasproxy import FasProxyClient
|
|
|
|
config = ConfigParser.RawConfigParser()
|
|
config.read(config_file)
|
|
|
|
require_pincode = config.getboolean('main', 'require_pincode')
|
|
success_string = config.get('main', 'success_string')
|
|
|
|
fas_url = config.get('main', 'fas_url')
|
|
try:
|
|
fas = FasProxyClient(fas_url)
|
|
except Exception, e:
|
|
syslog.syslog(syslog.LOG_CRIT, 'Problem connecting to fas %s' % e)
|
|
sys.exit(1)
|
|
|
|
backends = totpcgi.backends.Backends()
|
|
|
|
try:
|
|
backends.load_from_config(config)
|
|
except totpcgi.backends.BackendNotSupported, ex:
|
|
syslog.syslog(syslog.LOG_CRIT,
|
|
'Backend engine not supported: %s' % ex)
|
|
sys.exit(1)
|
|
|
|
syslog.openlog('totp.cgi', syslog.LOG_PID, syslog.LOG_AUTH)
|
|
|
|
### Begin custom Fedora Functions
|
|
|
|
def google_auth_fas_pincode_verify(user, pincode):
|
|
if not fas.verify_password(user, pincode):
|
|
raise totpcgi.UserPincodeError('User Password Error')
|
|
|
|
backends.pincode_backend.verify_user_pincode = google_auth_fas_pincode_verify
|
|
|
|
client_id = '1'
|
|
|
|
def parse_token(token):
|
|
if token > 44:
|
|
otp = token[-44:]
|
|
if otp.startswith('ccccc'):
|
|
return token[:-44], otp
|
|
|
|
# Not a password + yubikey
|
|
return False
|
|
|
|
class YubikeyAuthenticator(object):
|
|
auth_regex = re.compile('^status=(?P<rc>\w{2})')
|
|
def __init__(self, require_pincode=False):
|
|
self.require_pincode = require_pincode
|
|
|
|
def verify_user_token(self, user, token):
|
|
# Parse the token apart into a password and token
|
|
password, otp = parse_token(token)
|
|
|
|
# Verify token against yubikey server
|
|
server_prefix = 'http://localhost/yk-val/verify?id='
|
|
server_url = server_prefix + client_id + "&otp=" + otp
|
|
|
|
fh = urllib2.urlopen(server_url)
|
|
|
|
for line in fh:
|
|
match = self.auth_regex.search(line.strip('\n'))
|
|
if match:
|
|
if match.group('rc') == 'OK':
|
|
# Yubikey token is valid
|
|
break
|
|
raise totpcgi.VerifyFailed(line.split('=')[1])
|
|
else:
|
|
raise totpcgi.VerifyFailed('yk-val returned malformed response')
|
|
|
|
|
|
# Verify that the yubikey token belongs to the user
|
|
# As a side effect, verify the password is good as well
|
|
# if the user+password are wrong, this will raise a fedora.client.AuthError
|
|
try:
|
|
response = fas.send_request('/config/list/%s/yubikey' % user,
|
|
auth_params={'username': user, 'password': password})
|
|
except AuthError, e:
|
|
raise totpcgi.VerifyFailed('User Password Error: %s' % e)
|
|
if not response[1].configs.prefix or not response[1].configs.enabled:
|
|
raise totpcgi.VerifyFailed('Yubikey OTP unconfigured')
|
|
elif len(response[1].configs.prefix) != 12:
|
|
raise totpcgi.VerifyFailed('Invalid Yubikey OTP prefix')
|
|
if not otp.startswith(response[1].configs.prefix):
|
|
raise totpcgi.VerifyFailed('Unauthorized/Invalid OTP')
|
|
|
|
# Okay, everything passed
|
|
return 'Valid yubikey returned'
|
|
|
|
|
|
### End of custom Fedora Functions
|
|
|
|
def bad_request(why):
|
|
output = 'ERR\n' + why + '\n'
|
|
sys.stdout.write('Status: 400 BAD REQUEST\n')
|
|
sys.stdout.write('Content-type: text/plain\n')
|
|
sys.stdout.write('Content-Length: %s\n' % len(output))
|
|
sys.stdout.write('\n')
|
|
|
|
sys.stdout.write(output)
|
|
sys.exit(0)
|
|
|
|
def cgimain():
|
|
form = cgi.FieldStorage()
|
|
|
|
must_keys = ('user', 'token', 'mode')
|
|
|
|
for must_key in must_keys:
|
|
if must_key not in form:
|
|
bad_request("Missing field: %s" % must_key)
|
|
|
|
user = form.getfirst('user')
|
|
token = form.getfirst('token')
|
|
mode = form.getfirst('mode')
|
|
|
|
remote_host = os.environ['REMOTE_ADDR']
|
|
|
|
if mode != 'PAM_SM_AUTH':
|
|
bad_request('We only support PAM_SM_AUTH')
|
|
|
|
if parse_token(token):
|
|
ga = YubikeyAuthenticator(require_pincode)
|
|
else:
|
|
# totp/googleauth
|
|
ga = totpcgi.GoogleAuthenticator(backends, require_pincode)
|
|
|
|
try:
|
|
status = ga.verify_user_token(user, token)
|
|
except Exception, ex:
|
|
syslog.syslog(syslog.LOG_NOTICE,
|
|
'Failure: user=%s, mode=%s, host=%s, message=%s' % (user, mode,
|
|
remote_host, str(ex)))
|
|
bad_request(str(ex))
|
|
|
|
syslog.syslog(syslog.LOG_NOTICE,
|
|
'Success: user=%s, mode=%s, host=%s, message=%s' % (user, mode,
|
|
remote_host, status))
|
|
|
|
sys.stdout.write('Status: 200 OK\n')
|
|
sys.stdout.write('Content-type: text/plain\n')
|
|
sys.stdout.write('Content-Length: %s\n' % len(success_string))
|
|
sys.stdout.write('\n')
|
|
|
|
sys.stdout.write(success_string)
|
|
|
|
if __name__ == '__main__':
|
|
cgimain()
|