1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159
|
"""CAS 1.0 and 2.0 SSO Implementation
This Authentication middleware handles CAS 1.0 and 2.0 authentication. The
latter of which supports proxy service tickets in addition to direct client
service tickets.
Note that this does not implement proxy granting ticket requests, merely
CAS 2.0 service validation (which can take proxy tickets).
`Protocol Reference <http://www.ja-sig.org/products/cas/overview/protocol/index.html>`_
"""
import logging
import urllib
from authkit.authenticate.sso.api import *
log = logging.getLogger(__name__)
class AuthCASHandler(RedirectingAuthHandler):
"""CAS 1.0 and 2.0 Redirect Handler
This small middleware piece handles generating the redirect URL for the
CAS
"""
def __init__(self, app, authority, path='', use_cas2=False, protect=None):
self.app = app
self.authority = authority
self.path = path
self._cas2 = use_cas2
def redirect_url(self, environ):
kwargs = {'service': construct_url(environ, querystring='type=cas')}
# XXX TODO: Store this for the middleware below, also look for a proxy
# granting ticket option to store that its desired
if 'authkit.cas.renew' in environ:
kwargs['renew'] = 'true'
if 'authkit.cas.gateway' in environ:
kwargs['gateway'] = 'true'
# renew and gateway should not both be set at once according to
# CAS protocol
assert 'renew' not in kwargs
args = urllib.urlencode(kwargs)
return self.authority + "login?" + args
class AuthCASMiddleware(RedirectingAuthMiddleware):
"""CAS 1.0 and 2.0 Capable Authentication Handler"""
def __init__(self, app, authority, use_cas2=False, path='', protect=None):
self.app = app
self.authority = authority
self.type = 'cas'
self._cas2 = use_cas2
if use_cas2:
self._authtype = 'CAS 2.0'
else:
self._authtype = 'CAS 1.0'
self.protect = protect or []
self.dispatch = {path + '/verify':'verify'}
def verify(self, environ, start_response):
req = WSGIRequest(environ)
if 'ticket' not in req.GET:
log.debug("No ticket found in request, unable to verify, returning"
"404 Not Found.")
return HTTPNotFound().wsgi_application(environ, start_response)
ticket = req.GET['ticket']
service = construct_url(environ, querystring='type=cas')
kwargs = {'service': service, 'ticket':ticket}
if req.environ.get('authkit.sso.cas.renew'):
kwargs['renew'] = 'true'
args = urllib.urlencode(kwargs)
# XXX TODO: Store whether renew was used for this request to ensure
# that the validation asks for it as well
# Also store whether a proxy ticket was requested and ask for
# it during validation
if self._cas2:
log.debug("Validating using CAS 2.0")
# We use proxyValidate for CAS 2.0 because it will handle both
# service and proxy ticket validation
requrl = self.authority + "proxyValidate?" + args
response = urllib.urlopen(requrl).read()
log.debug("Raw response of auth verification: \n\t%s", response)
tree = ElementTree.fromstring(response)
valid = tree[0].tag.endswith('authenticationSuccess')
results = {}
if valid:
log.debug("Successfully authenticated")
user_kwargs = {}
results['user'] = tree[0][0].text
results['extra_environ'] = {}
# Did we get a proxy ticket?
if len(tree[0]) > 1 and tree[0][1].tag.endswith('proxyGrantingTicket'):
results['authkit.cas.proxyTicket'] = tree[0][1].text.strip()
# Any proxies returned as well?
if len(tree[0] > 2):
proxies = [x.text.strip() for x in tree[0][2]]
results['authkit.cas.proxies'] = proxies
else:
log.info('Authentication failed for auth: %s, ticket %s, '
'response: %s', self._authtype, ticket,
tree[0].attrib['code'])
else:
log.debug("Validating using CAS 1.0")
requrl = self.authority + "validate?" + args
result = urllib.urlopen(requrl).read().split("\n")
log.debug("Raw response of auth verification: \n\t%s", result)
valid = 'yes' == result[0]
results = {}
if valid:
results['user'] = result[1]
else:
log.info('Authentication failed for auth: %s, ticket %s, '
'response: %s', self._authtype, ticket, result[0])
if not valid:
log.debug("Invalid response, returning login failure.")
return LoginFailure().wsgi_application(environ, start_response)
environ['AUTH_TYPE'] = self._authtype
environ['REMOTE_USER'] = results['user']
set_user = req.environ.get('paste.auth_tkt.set_user')
user_data = self._authtype
if set_user:
set_user(results['user'], user_data=user_data)
# Add in optional environ data from the auth system
if 'extra_environ' in results:
environ.update(results['extra_environ'])
log.debug("Authentication success, calling app.")
return self.app(environ, start_response)
def make_cas_handler(app, auth_conf, app_conf=None, global_conf=None,
prefix='authkit.sso.cas'):
if 'authority' not in auth_conf:
raise AuthKitConfigError("No %sauthority key specified" % prefix)
kwargs = dict(authority=auth_conf['authority'])
if 'use_cas2' in auth_conf:
kwargs['use_cas2'] = True
kwargs['path'] = auth_conf.get('path', '')
if 'path' in auth_conf:
kwargs['path'] = auth_conf['path']
if 'protect' in auth_conf:
kwargs['protect'] = auth_conf['protect'].split(',')
app = AuthCASMiddleware(app, **kwargs)
multi_app, app = find_multi_app(app)
multi_app.add_method('cas', AuthCASHandler, **kwargs)
multi_app.add_checker('cas', status_checker)
return app
|