source: main/waeup.cas/trunk/waeup/cas/server.py @ 10398

Last change on this file since 10398 was 10398, checked in by uli, 12 years ago

Enable session cookies for single sign on.

File size: 7.4 KB
Line 
1"""A WSGI app for serving CAS.
2"""
3import os
4import random
5import time
6from webob import exc, Response
7from webob.dec import wsgify
8from waeup.cas.authenticators import get_authenticator
9from waeup.cas.db import (
10    DB, DBSessionContext, LoginTicket, ServiceTicket, TicketGrantingCookie)
11
12template_dir = os.path.join(os.path.dirname(__file__), 'templates')
13
14RANDOM = random.SystemRandom(os.urandom(1024))
15
16#: The chars allowed by protocol specification for tickets and cookie
17#: values.
18ALPHABET = ('abcdefghijklmnopqrstuvwxyz'
19            'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
20            '01234567789-')
21
22
23def get_random_string(length):
24    """Get a random string of length `length`.
25
26    The returned string should be hard to guess but is not
27    neccessarily unique.
28    """
29    return ''.join([RANDOM.choice(ALPHABET) for x in range(length)])
30
31
32def get_unique_string():
33    """Get a unique string based on current time.
34
35    The returned string contains only chars from `ALPHABET`.
36
37    We try to be unique by using a timestamp in high resolution, so
38    that even tickets created shortly after another should differ. On
39    very fast machines, however, this might be not enough (currently
40    we use 16 decimal places).
41
42    This is fast because we don't have to fetch foreign data sources
43    nor have to do database lookups.
44
45    The returned string will be unique but it won't be hard to guess
46    for people able to read a clock.
47    """
48    return ('%.16f' % time.time()).replace('.', '-')
49
50
51def create_service_ticket(user, service=None):
52    """Get a service ticket.
53
54    Ticket length will be 32 chars, randomly picked from `ALPHABET`.
55    """
56    t_id = 'ST-' + get_random_string(29)
57    return ServiceTicket(t_id, user, service)
58
59
60def create_login_ticket():
61    """Create a unique login ticket.
62
63    Login tickets are required to be unique (but not neccessarily hard
64    to guess), according to protocol specification.
65    """
66    t_id = 'LT-%s' % get_unique_string()
67    return LoginTicket(t_id)
68
69
70def check_login_ticket(db, lt_string):
71    """Check whether `lt_string` represents a valid login ticket in `db`.
72    """
73    if lt_string is None:
74        return False
75    q = db.query(LoginTicket).filter(LoginTicket.ticket == lt_string)
76    result = [x for x in q]
77    if result:
78        db.delete(result[0])
79    return len(result) > 0
80
81
82def create_tgc_value():
83    """Get a ticket granting cookie value.
84    """
85    value = 'TGC-' + get_random_string(128)
86    return TicketGrantingCookie(value)
87
88
89def set_session_cookie(response, db):
90    """Create a session cookie (ticket granting cookie) on `response`.
91
92    The `db` database is used to make the created cookie value
93    persistent.
94    """
95    tgc = create_tgc_value()
96    db.add(tgc)
97    response.set_cookie(
98        'cas-tgc', tgc.value, path='/', secure=True, httponly=True)
99    return response
100
101
102def check_session_cookie(db, cookie_value):
103    """Check whether `cookie_value` represents a valid ticket granting
104    ticket in `db`.
105    """
106    if cookie_value is None:
107        return False
108    print("VAL1: ", cookie_value)
109    try:
110        # turn value into unicode (py2.x) / str (py3.x)
111        cookie_value = cookie_value.decode('utf-8')
112    except AttributeError:                         # pragma: no cover
113        pass
114    print("VAL2: ", cookie_value)
115    q = db.query(TicketGrantingCookie).filter(
116        TicketGrantingCookie.value == cookie_value)
117    result = [x for x in q]
118    if len(result):
119        return result[0]
120    return None
121
122
123class CASServer(object):
124    """A WSGI CAS server.
125
126    This CAS server stores credential data (tickets, etc.) in a local
127    sqlite3 database file.
128
129    `db_path` -
130       The filesystem path to the database to use. If none is given
131       CAS server will create a new one in some new, temporary
132       directory. Please note that credentials will be lost after a
133       CAS server restart.
134
135       If the path is given and the file exists already, it will be
136       used.
137
138       If the database file does not exist, it will be created.
139    """
140    def __init__(self, db='sqlite:///:memory:', auth=None):
141        self.db_connection_string = db
142        self.db = DB(self.db_connection_string)
143        self.auth = auth
144
145    @wsgify
146    def __call__(self, req):
147        with DBSessionContext():
148            if req.path in ['/login', '/validate', '/logout']:
149                return getattr(self, req.path[1:])(req)
150        return exc.HTTPNotFound()
151
152    def _get_template(self, name):
153        path = os.path.join(template_dir, name)
154        if os.path.isfile(path):
155            return open(path, 'r').read()
156        return None
157
158    def login(self, req):
159        service = req.POST.get('service', req.GET.get('service', None))
160        service_field = ''
161        msg = ''
162        username = req.POST.get('username', None)
163        password = req.POST.get('password', None)
164        valid_lt = check_login_ticket(self.db, req.POST.get('lt'))
165        tgc = req.cookies.get('cas-tgc', None)
166        print('TGC: ', tgc)
167        tgc = check_session_cookie(self.db, tgc)
168        print("TGC2: ", tgc)
169        print([(x, y) for x, y, in req.headers.items()])
170        if username and password and valid_lt or tgc:
171            # act as credentials acceptor
172            if tgc:
173                cred_ok, reason = True, ''
174                if not service:
175                    msg = 'You logged in already.'
176            else:
177                cred_ok, reason = self.auth.check_credentials(
178                    username, password)
179            if cred_ok:
180                if service is None:
181                    # show logged-in screen
182                    html = self._get_template('login_successful.html')
183                    html = html.replace('MSG_TEXT', msg)
184                    resp = Response(html)
185                    if not tgc:
186                        resp = set_session_cookie(resp, self.db)
187                    return resp
188                else:
189                    # safely redirect to service given
190                    st = create_service_ticket(service)
191                    self.db.add(st)
192                    service = '%s?ticket=%s' % (service, st.ticket)
193                    html = self._get_template('login_service_redirect.html')
194                    html = html.replace('SERVICE_URL', service)
195                    resp = exc.HTTPSeeOther(location=service)
196                    resp.cache_control = 'no-store'
197                    resp.pragma = 'no-cache'
198                    # some arbitrary date in the past
199                    resp.expires = 'Thu, 01 Dec 1994 16:00:00 GMT'
200                    resp.text = html
201                    return resp
202            else:
203                # login failed
204                msg = '<i>Login failed</i><br />Reason: %s' % reason
205        if service is not None:
206            service_field = (
207                '<input type="hidden" name="service" value="%s" />' % (
208                    service)
209                )
210        lt = create_login_ticket()
211        self.db.add(lt)
212        html = self._get_template('login.html')
213        html = html.replace('LT_VALUE', lt.ticket)
214        html = html.replace('SERVICE_FIELD_VALUE', service_field)
215        html = html.replace('MSG_TEXT', msg)
216        return Response(html)
217
218    def validate(self, req):
219        return exc.HTTPNotImplemented()
220
221    def logout(self, req):
222        return exc.HTTPNotImplemented()
223
224cas_server = CASServer
225
226
227def make_cas_server(global_conf, **local_conf):
228    local_conf = get_authenticator(local_conf)
229    return CASServer(**local_conf)
Note: See TracBrowser for help on using the repository browser.