[3] | 1 | # (c) 2005 Clark C. Evans |
---|
| 2 | # This module is part of the Python Paste Project and is released under |
---|
| 3 | # the MIT License: http://www.opensource.org/licenses/mit-license.php |
---|
| 4 | # This code was written with funding by http://prometheusresearch.com |
---|
| 5 | """ |
---|
| 6 | CAS 1.0 Authentication |
---|
| 7 | |
---|
| 8 | The Central Authentication System is a straight-forward single sign-on |
---|
| 9 | mechanism developed by Yale University's ITS department. It has since |
---|
| 10 | enjoyed widespread success and is deployed at many major universities |
---|
| 11 | and some corporations. |
---|
| 12 | |
---|
| 13 | https://clearinghouse.ja-sig.org/wiki/display/CAS/Home |
---|
| 14 | http://www.yale.edu/tp/auth/usingcasatyale.html |
---|
| 15 | |
---|
| 16 | This implementation has the goal of maintaining current path arguments |
---|
| 17 | passed to the system so that it can be used as middleware at any stage |
---|
| 18 | of processing. It has the secondary goal of allowing for other |
---|
| 19 | authentication methods to be used concurrently. |
---|
| 20 | """ |
---|
| 21 | import urllib |
---|
| 22 | from paste.request import construct_url |
---|
| 23 | from paste.httpexceptions import HTTPSeeOther, HTTPForbidden |
---|
| 24 | |
---|
| 25 | class CASLoginFailure(HTTPForbidden): |
---|
| 26 | """ The exception raised if the authority returns 'no' """ |
---|
| 27 | |
---|
| 28 | class CASAuthenticate(HTTPSeeOther): |
---|
| 29 | """ The exception raised to authenticate the user """ |
---|
| 30 | |
---|
| 31 | def AuthCASHandler(application, authority): |
---|
| 32 | """ |
---|
| 33 | middleware to implement CAS 1.0 authentication |
---|
| 34 | |
---|
| 35 | There are several possible outcomes: |
---|
| 36 | |
---|
| 37 | 0. If the REMOTE_USER environment variable is already populated; |
---|
| 38 | then this middleware is a no-op, and the request is passed along |
---|
| 39 | to the application. |
---|
| 40 | |
---|
| 41 | 1. If a query argument 'ticket' is found, then an attempt to |
---|
| 42 | validate said ticket /w the authentication service done. If the |
---|
| 43 | ticket is not validated; an 403 'Forbidden' exception is raised. |
---|
| 44 | Otherwise, the REMOTE_USER variable is set with the NetID that |
---|
| 45 | was validated and AUTH_TYPE is set to "cas". |
---|
| 46 | |
---|
| 47 | 2. Otherwise, a 303 'See Other' is returned to the client directing |
---|
| 48 | them to login using the CAS service. After logon, the service |
---|
| 49 | will send them back to this same URL, only with a 'ticket' query |
---|
| 50 | argument. |
---|
| 51 | |
---|
| 52 | Parameters: |
---|
| 53 | |
---|
| 54 | ``authority`` |
---|
| 55 | |
---|
| 56 | This is a fully-qualified URL to a CAS 1.0 service. The URL |
---|
| 57 | should end with a '/' and have the 'login' and 'validate' |
---|
| 58 | sub-paths as described in the CAS 1.0 documentation. |
---|
| 59 | |
---|
| 60 | """ |
---|
| 61 | assert authority.endswith("/") and authority.startswith("http") |
---|
| 62 | def cas_application(environ, start_response): |
---|
| 63 | username = environ.get('REMOTE_USER','') |
---|
| 64 | if username: |
---|
| 65 | return application(environ, start_response) |
---|
| 66 | qs = environ.get('QUERY_STRING','').split("&") |
---|
| 67 | if qs and qs[-1].startswith("ticket="): |
---|
| 68 | # assume a response from the authority |
---|
| 69 | ticket = qs.pop().split("=", 1)[1] |
---|
| 70 | environ['QUERY_STRING'] = "&".join(qs) |
---|
| 71 | service = construct_url(environ) |
---|
| 72 | args = urllib.urlencode( |
---|
| 73 | {'service': service,'ticket': ticket}) |
---|
| 74 | requrl = authority + "validate?" + args |
---|
| 75 | result = urllib.urlopen(requrl).read().split("\n") |
---|
| 76 | if 'yes' == result[0]: |
---|
| 77 | environ['REMOTE_USER'] = result[1] |
---|
| 78 | environ['AUTH_TYPE'] = 'cas' |
---|
| 79 | return application(environ, start_response) |
---|
| 80 | exce = CASLoginFailure() |
---|
| 81 | else: |
---|
| 82 | service = construct_url(environ) |
---|
| 83 | args = urllib.urlencode({'service': service}) |
---|
| 84 | location = authority + "login?" + args |
---|
| 85 | exce = CASAuthenticate(location) |
---|
| 86 | return exce.wsgi_application(environ, start_response) |
---|
| 87 | return cas_application |
---|
| 88 | |
---|
| 89 | middleware = AuthCASHandler |
---|
| 90 | |
---|
| 91 | __all__ = ['CASLoginFailure', 'CASAuthenticate', 'AuthCASHandler' ] |
---|
| 92 | |
---|
| 93 | if '__main__' == __name__: |
---|
| 94 | authority = "https://secure.its.yale.edu/cas/servlet/" |
---|
| 95 | from paste.wsgilib import dump_environ |
---|
| 96 | from paste.httpserver import serve |
---|
| 97 | from paste.httpexceptions import * |
---|
| 98 | serve(HTTPExceptionHandler( |
---|
| 99 | AuthCASHandler(dump_environ, authority))) |
---|