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))) |
---|