summaryrefslogtreecommitdiff
path: root/paste/login.py
blob: 49d9e43a334f9fd4e40e2f4762badf860501b41c (plain)
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
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
# (c) 2005 Ian Bicking and contributors; written for Paste (http://pythonpaste.org)
# Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php

"""
Login/authentication middleware

NOT YET FINISHED
"""

import wsgilib
import sha
from paste.deploy import converters
from paste.util import import_string

def middleware(
    application,
    global_conf=None,
    http_login=False,
    http_realm='Secure Website',
    http_overwrite_realm=True,
    http_and_cookie=True,
    cookie_prefix='',
    login_page='_login/login_form',
    logout_page='_login/logout_form',
    secret=None,
    authenticator=None,
    ):
    """
    Configuration:
    
    http_login:
        If true, then we'll prefer HTTP Basic logins, passing a 401 to
        the user.  If false, we'll use form logins with Cookie
        authentication.
    http_realm:
        The realm to use.  If http_overwrite_realm is true then we will
        force this to be the realm (even if the application supplies
        its own realm).
    http_and_cookie:
        If true, we'll give the user a login cookie even if they use
        HTTP.  Then we don't have to throw a 401 on every page to get
        them to re-login.
    cookie_prefix:
        Used before all cookie names; like a domain.
    login_page:
        If using cookie login and we get a 401, we'll turn it into a
        200 and do an internal redirect to this page (using recursive).
    logout_page:
        Ditto the logout (logout will at some point be triggered with
        another key we add to the environment).
    secret:
        We use this for signing cookies.  We'll generate it automatically
        if it's not provided explicitly (set it explicitly to be sure
        it is stable).
    authenticator:
        When we do HTTP logins we need to tell if they are using the
        correct login immediately.  See the Authenticator object for
        the framework of an implementation.

    When you require a login, return a 401 error.  When a login has
    occurred, the logged-in username will be in REMOTE_USER.  When the
    user is logged in, but denied access, use a 403 error (not a 401).
    It might be useful to have another middleware that wraps an application
    and returns a 401 error, based on parsing the URL.

    Currently, the login form, if used, is rendered at the URL requested
    by the user, instead of issuing an HTTP redirect.  This will require
    some attention to caching issues, but allows forms to be POSTed without
    losing data after the login (as long as the login page contains the
    appropriate hidden fields.)

    Also, the cookie is not deleted on an unsuccessful login attempt.

    The cookie is issued with path '/' and no expiration date.  This
    should probably be overridable.

    Environment variables used:
      paste.login.signer:
          signer, created from UsernameSigner class
      paste.login._dologin:
          user name to be logged in, either from HTTP auth
          or from form submission (XXX form not implement)
      paste.login._doredirect:
          login page to which to redirect
      paste.login._loginredirect:
          set to True iff _doredirect set and login_page is
          relative, else undefined.  Used where?
    """
    
    global_conf = global_conf or {}
    http_login = converters.asbool(http_login)
    http_overwrite_realm = converters.asbool(http_overwrite_realm)
    http_and_cookie = converters.asbool(http_and_cookie)
    if authenticator and isinstance(authenticator, (str, unicode)):
        authenticator = import_string.eval_import(authenticator)
        
    if http_login:
        assert authenticator, (
            "You must provide an authenticator argument if you "
            "are using http_login")
    if secret is None:
        secret = global_conf.get('secret')
    if secret is None:
        secret = create_secret()
    cookie_name = cookie_prefix + '_login_auth'

    signer = UsernameSigner(secret)

    def login_application(environ, start_response):
        orig_script_name = environ['SCRIPT_NAME']
        orig_path_info = environ['PATH_INFO']
        cookies = wsgilib.get_cookies(environ)
        cookie = cookies.get(cookie_name)
        username = None
        environ['paste.login.signer'] = signer
        if cookie and cookie.value:
            username = signer.check_signature(
                cookie.value, environ['wsgi.errors'])
        authenticatee = (
            environ.get('HTTP_AUTHORIZATION') or
            environ.get('HTTP_CGI_AUTHORIZATION'))
        if (not username
            and authenticator
            and authenticatee):
            username = authenticator().check_basic_auth(authenticatee)
            if http_and_cookie:
                environ['paste.login._dologin'] = username
        if username:
            environ['REMOTE_USER'] = username

        def login_start_response(status, headers, exc_info=None):
            if environ.get('paste.login._dologin'):
                cookie = SimpleCookie(cookie_name,
                                      signer.make_signature(username),
                                      '/')
                headers.append(('Set-Cookie', str(cookie)))
                del environ['paste.login._dologin']
            status_int = int(status.split(None, 1)[0].strip())
            if status_int == 401 and http_login:
                if (http_overwrite_realm
                    or not wsgilib.has_header(headers, 'www-authenticate')):
                    headers.append(('WWW-Authenticate', 'Basic realm="%s"' % http_realm))
            elif status_int == 401:
                status = '200 OK'
                if login_page.startswith('/'):
                    assert environ.has_key('paste.recursive.include'), (
                        "You must use the recursive middleware to "
                        "use a non-relative page for the login_page")
                environ['paste.login._doredirect'] = login_page
                return garbage_writer
            return start_response(status, headers, exc_info)

        app_iter = application(environ, login_start_response)
        
        if environ.get('paste.login._doredirect'):
            page_name = environ['paste.login._doredirect']
            del environ['paste.login._doredirect']
            eat_app_iter(app_iter)
            if login_page.startswith('/'):
                app_iter = environ['paste.recursive.forward'](
                    login_page[1:])
            else:
                # Don't use recursive, since login page is
                # internal to 
                new_environ = environ.copy()
                new_environ['SCRIPT_NAME'] = orig_script_name
                new_environ['PATH_INFO'] = '/' + login_page
                new_environ['paste.login._loginredirect'] = True
                app_iter = login_application(new_environ, start_response)
        return app_iter

    return login_application

    
def encodestrip(s):
    return s.encode('base64').strip('\n')

class UsernameSigner(object):

    def __init__(self, secret):
        self.secret = secret

    def digest(self, username):
        return sha.new(self.secret+username).digest()        

    def __call__(self, username):
        return encodestrip(self.digest(username))

    def check_signature(self, b64value, errors):
        value = b64value.decode('base64')
        if ' ' not in value:
            errors.write('Badly formatted cookie: %r\n' % value)
            return None
        signature, username = value.split(' ', 1)
        sig_hash = self.digest(username)
        if sig_hash == signature:
            return username
        errors.write('Bad signature: %r\n' % value)
        return None
    
    def make_signature(self, username):
        return encodestrip(self.digest(username) + " " + username)

    def login_user(self, username, environ):
        """
        Adds a username so that the login middleware will later set
        the user to be logged in (with a cookie).
        """
        environ['paste.login._dologin'] = username

class SimpleCookie(object):
    def __init__(self, cookie_name, signed_val, path):
        self.cookie_name = cookie_name
        self.signed_val = signed_val
        self.path = '/'

    def __str__(self):
        return "%s=%s; Path=%s" % (self.cookie_name,
                                   self.signed_val, self.path)
    
class Authenticator(object):

    """
    This is the basic framework for an authenticating object.
    """

    def check_basic_auth(self, auth):
        """Returns either the authenticated username or, if unauthorized,
        None."""
        assert auth.lower().startswith('basic ')
        type, auth = auth.split()
        auth = auth.strip().decode('base64')
        username, password = auth.split(':')
        if self.check_auth(username, password):
            return username
        return None

    def check_auth(self, username, password):
        raise NotImplementedError


########################################
## Utility functions
########################################

def create_secret():
    # @@: obviously not a good secret generator: should be randomized
    # somehow, and maybe store the secret somewhere for later use.
    return 'secret'

def garbage_writer(s):
    """
    When we don't care about the written output.
    """
    pass

def eat_app_iter(app_iter):
    """
    When we don't care about the iterated output.
    """
    try:
        for s in app_iter:
            pass
    finally:
        if hasattr(app_iter, 'close'):
            app_iter.close()