File: open_id.py

package info (click to toggle)
python-authkit 0.4.1~r143-1
  • links: PTS, VCS
  • area: main
  • in suites: lenny
  • size: 740 kB
  • ctags: 703
  • sloc: python: 4,643; makefile: 39; sh: 33
file content (548 lines) | stat: -rw-r--r-- 19,180 bytes parent folder | download
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
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
"""Highly flexible OpenID based authentication middleware

.. Note::

    If you want to test this module feel free to setup an account at
    passurl.com. The site is in alpha and any accounts will be deleted before
    launch but it might help with testing.

Full documentation on the use of this OpenID module is in the AuthKit manual.

.. warning ::

    Any data returned from an OpenID identity provider is stored in the cookie
    user data. This is not encryted when sent to the browser (unless you are 
    using a secure connection). Whilst this is unlikely to be a major problem 
    in your application it is something to be aware of.

This middleware actually sets two cookies, one for AuthKit and one for a session
store to store the ID which OpenID information is keyed against.

The OpenID sreg variables you can use include:

``nickname``
    Any UTF-8 string that the End User wants to use as a nickname. 

``email``
    The email address of the End User as specified in section 3.4.1 of
    [RFC2822] (Resnick, P., "Internet Message Format," .). 

``fullname``
    UTF-8 string free text representation of the End User's full name. 

``dob``
    The End User's date of birth as YYYY-MM-DD. Any values whose representation
    uses fewer than the specified number of digits should be zero-padded. The
    length of this value MUST always be 10. If the End User user does not want
    to reveal any particular component of this value, it MUST be set to zero.
    For instance, if a End User wants to specify that his date of birth is in
    1980, but not the month or day, the value returned SHALL be "1980-00-00".

``gender``
    The End User's gender, "M" for male, "F" for female. 

``postcode``
    UTF-8 string free text that SHOULD conform to the End User's country's
    postal system. 

``country``
    The End User's country of residence as specified by ISO3166. 

``language``
    End User's preferred language as specified by ISO639. 

``timezone``
    ASCII string from TimeZone database
    For example, "Europe/Paris" or "America/Los_Angeles".
"""

import cgi
import paste.request
import string
import sys
from authkit.authenticate import AuthKitConfigError
from paste.request import construct_url
from openid.consumer import consumer
from openid import sreg
from openid.cryptutil import randomString
from authkit.authenticate import get_template, valid_password, \
   get_authenticate_function, strip_base, RequireEnvironKey, \
   AuthKitUserSetter, AuthKitAuthHandler
from authkit.authenticate.multi import MultiHandler, status_checker

def template():
    return """\
<html>
  <head><title>Please Sign In</title></head>
  <body>
    <h1>Please Sign In</h1>
    <div class="$css_class">$message</div>
    <form action="$action" method="post">
      <dl>
        <dt>OpenID Passurl:</dt>
        <dd><input type="text" name="openid" value="$value"></dd>
      </dl>
      <input type="submit" name="authform" />
      <hr />
    </form>
  </body>
</html>
"""

def passurl_urltouser(environ, url):
    return url.strip('/').split('/')[-1]

def render(template, **p):
    if sys.version_info >= (2,4):
        return string.Template(template()).substitute(
            **p
        )
    else:
        for k, v in p.items():
            template = template().replace('$'+k, v)
        return template

class OpenIDAuthHandler(object):
    """
    This middleware is triggered when the authenticate middleware catches 
    a 401 response. The form is submitted to the verify URL which the other
    middleware handles
    """
    def __init__(self, app, template, path_verify, baseurl='', charset=None):
        self.app = app
        self.template = template
        self.baseurl = baseurl
        self.path_verify = path_verify
        if charset is None:
            self.charset = ''
        else:
            self.charset = '; charset='+charset

    def __call__(self, environ, start_response):
        baseurl = self.baseurl or construct_url(
            environ, 
            with_query_string=False, 
            with_path_info=False
        )
        content = render(
            self.template,
            message='',
            value='',
            css_class='',
            action=baseurl + self.path_verify
        )
        start_response(
            "200 OK",
            [
                ('Content-Type', 'text/html'+self.charset),
                ('Content-Length', str(len(content)))
            ]
        )
        return [content]

def make_store(store, config):
    conn = None
    if store == 'file':
        from openid.store import filestore
        cstore = filestore.FileOpenIDStore(config)
    elif store == 'mysql':
        import MySQLdb
        from DBUtils.PersistentDB import PersistentDB
        from openid.store.sqlstore import MySQLStore
        from sqlalchemy.engine.url import make_url

        def create_conn(dburi):
            url = make_url(dburi)
            p={'db':url.database}
            if url.username:
                p['user'] = url.username
            if url.password:
                p['passwd'] = url.password
            if url.host:
                p['host'] = url.host
            if url.port:
                p['port'] = url.port
            return PersistentDB(MySQLdb, 1, **p).connection()
        conn = create_conn(config)
        cstore = MySQLStore(conn)
    else:
        raise Exception("Invalid store type %r"%store)
    return conn, cstore

class AuthOpenIDHandler:
    """
    The template should be setup from authkit.open_id.template.file or 
    authkit.open_id.template.obj before we get here!
    """
    def __init__(
        self, 
        app, 
        store_type, 
        store_config, 
        baseurl, 
        path_signedin, 
        template=None,
        session_middleware='beaker.session',
        path_verify='/verify', 
        path_process='/process',
        urltouser=None,
        charset=None,
        sreg_required=None,
        sreg_optional=None,
        sreg_policyurl=None
    ):
        self.conn, self.store = make_store(store_type, store_config)
        self.baseurl = baseurl
        self.template = template
        self.path_signedin = path_signedin
        self.path_verify = path_verify
        self.path_process = path_process
        self.session_middleware = session_middleware
        self.app = app
        self.urltouser = urltouser
        if charset is None:
            self.charset = ''
        else:
            self.charset = '; charset='+charset
        self.sreg_required = sreg_required
        self.sreg_optional = sreg_optional
        self.sreg_policyurl = sreg_policyurl

    def __call__(self, environ, start_response):
        # If we are called it is because we want to sign in, so show the 
        if not environ.has_key(self.session_middleware):
            raise AuthKitConfigError(
                'The session middleware %r is not present. '
                'Have you set up the session middleware?'%(
                    self.session_middleware
                )
            )
        if environ.get('PATH_INFO') == self.path_verify:
            response = self.verify(environ, start_response)
            environ[self.session_middleware].save()
            return response
        elif environ.get('PATH_INFO') == self.path_process:
            response = self.process(environ, start_response)
            environ[self.session_middleware].save()
            return response
        else:
            return self.app(environ, start_response)

    def verify(self, environ, start_response):
        baseurl = self.baseurl or construct_url(
            environ, 
            with_query_string=False, 
            with_path_info=False
        )
        params = dict(paste.request.parse_formvars(environ))
        openid_url = params.get('openid')
        if not openid_url:
            response = render(
                self.template,
                message='Enter an identity URL to verify.',
                value='',
                css_class='',
                action=baseurl + self.path_verify
            )
            start_response(
                '200 OK', 
                [
                    ('Content-type', 'text/html'+self.charset),
                    ('Content-length', str(len(response)))
                ]
            )
            return response
        oidconsumer = self._get_consumer(environ)
        try:
            request_ = oidconsumer.begin(openid_url)
        except consumer.DiscoveryFailure, exc:
            response = render(
                self.template,
                message='Error retrieving identity URL: %s' % (
                    cgi.escape(str(exc[0]))
                ),
                value=self._quoteattr(openid_url),
                css_class='error',
                action=baseurl + self.path_verify
            )
            start_response(
                '200 OK', 
                [
                    ('Content-type', 'text/html'+self.charset),
                    ('Content-length', str(len(response)))
                ]
            )
            return response
        else:
            if request_ is None:
                response = render(
                    self.template,
                    message='No OpenID services found for <code>%s</code>' % (
                        cgi.escape(openid_url),
                    ),
                    value=self._quoteattr(openid_url),
                    css_class='error',
                    action=baseurl + self.path_verify
                )
                start_response(
                    '200 OK', 
                    [
                        ('Content-type', 'text/html'+self.charset),
                        ('Content-length', str(len(response)))
                    ]
                )
                return response
            else:
                session = environ[self.session_middleware]
                if 'HTTP_REFERER' in environ and \
                        not environ['HTTP_REFERER'].endswith(self.path_verify) and \
                        not environ['HTTP_REFERER'].endswith(self.path_process):
                    session['referer'] = environ['HTTP_REFERER']

                if self.sreg_required or self.sreg_optional or self.sreg_policyurl:
                    required_list = []
                    if self.sreg_required:
                        required_list = [opt.strip() for opt in self.sreg_required.split(',')]
                    optional_list = []
                    if self.sreg_optional:
                        optional_list = [opt.strip() for opt in self.sreg_optional.split(',')]
                    sreg_request = sreg.SRegRequest(
                        required=required_list,
                        optional=optional_list,
                        policy_url=self.sreg_policyurl)
                    request_.addExtension(sreg_request)

                trust_root = baseurl
                return_to = baseurl + self.path_process

                if request_.shouldSendRedirect():
                    redirect_url = request_.redirectURL(
                        trust_root, return_to)

                    start_response(
                        '301 Redirect', 
                        [
                            ('Content-type', 'text/html'+self.charset),
                            ('Location', redirect_url)
                        ]
                    )
                    return []
                else:
                    # This gets called with sites such as myopenid.com
                    form_html = request_.formMarkup(
                        trust_root, return_to,
                        form_tag_attrs={'id':'openid_message'})
                    content = """\
                        <html><head><title>OpenID transaction in progress</title></head>
                        <body onload='document.getElementById("%s").submit()'>
                        %s
                        </body></html>
                    """%('openid_message', form_html)
                    start_response(
                        "200 OK",
                        [
                            ('Content-Type', 'text/html'+self.charset),
                            ('Content-Length', str(len(content)))
                        ]
                    )
                    return [content]

    def process(self, environ, start_response):
        baseurl = self.baseurl or construct_url(
            environ, 
            with_query_string=False, 
            with_path_info=False
        )
        value = ''
        css_class = 'error'
        message = ''

        params = dict(paste.request.parse_querystring(environ))
        oidconsumer = self._get_consumer(environ)
        info = oidconsumer.complete(dict(params))

        if info.status == consumer.FAILURE and info.identity_url:
            fmt = "Verification of %s failed."
            message = fmt % (cgi.escape(info.identity_url),)
            environ['wsgi.errors'].write(
                "Passurl Message: %s %s"%(message,info.message)
            )
        elif info.status == consumer.SUCCESS:
            username = info.identity_url
            if info.endpoint.canonicalID:
                username = cgi.escape(info.endpoint.canonicalID)
            user_data = str(sreg.SRegResponse.fromSuccessResponse(info).getExtensionArgs())
            # Set the cookie
            if self.urltouser:
                username = self.urltouser(environ, info.identity_url)
            environ['paste.auth_tkt.set_user'](username, user_data=user_data)
            # Return a page that does a meta refresh
            session = environ[self.session_middleware]
            if 'referer' in session:
                redirect_url = session.pop('referer')
            else:
                redirect_url = self.baseurl + self.path_signedin

            response = """
<HTML>
<HEAD>
<META HTTP-EQUIV="refresh" content="0;URL=%s">
<TITLE>Signed in</TITLE>
</HEAD>
<BODY>
<!-- You are sucessfully signed in. Redirecting... -->
</BODY>
</HTML>
            """ % (redirect_url)
            start_response(
                '200 OK', 
                [
                    ('Content-type', 'text/html'+self.charset),
                    ('Content-length', str(len(response)))
                ]
            )
            return response
        elif info.status == consumer.CANCEL:
            message = 'Verification cancelled'
        elif info.status == consumer.SETUP_NEEDED:
            if info.setup_url:
                message = '<a href=%s>Setup needed</a>' % (
                    self._quoteattr(info.setup_url),)
            else:
                message = 'Setup needed'
        else:
            environ['wsgi.errors'].write("Passurl Message: %s"%info.message)
            message = 'Verification failed.'
        value = self._quoteattr(info.identity_url)
        response = render(
            self.template,
            message=message,
            value=value,
            css_class=css_class,
            action=baseurl + self.path_verify
        )
        start_response(
            '200 OK', 
            [
                ('Content-type', 'text/html'+self.charset),
                ('Content-length', str(len(response)))
            ]
        )
        return response

    #
    # Helper methods
    #

    def _get_consumer(self, environ):
        session = environ[self.session_middleware]
        session['id'] = session.id
        oidconsumer = consumer.Consumer(session, self.store)
        oidconsumer.consumer.openid1_nonce_query_arg_name = 'passurl_nonce'
        session.save()
        return oidconsumer

    def _quoteattr(self, s):
        if s == None:
            s = ''
        qs = cgi.escape(s, 1)
        return '"%s"' % (qs,)

class OpenIDUserSetter(AuthKitUserSetter):
    def __init__(self, app, **options):
        app = AuthOpenIDHandler(
            app,
            store_type=options['store_type'], 
            store_config = options['store_config'],
            baseurl=options.get('baseurl',''),
            path_signedin=options['path_signedin'],
            path_process=options.get('path_process','/process'),
            template = options['template'],
            urltouser = options['urltouser'],
            charset = options['charset'],
            sreg_required=options['sreg_required'],
            sreg_optional=options['sreg_optional'],
            sreg_policyurl=options['sreg_policyurl'],
        )
        self.app = app

    def __call__(self, environ, start_response):
        return self.app(environ, start_response)

def load_openid_config(
    app,
    auth_conf, 
    app_conf=None,
    global_conf=None,
    prefix='authkit.openid', 
):
    global template
    template_ = template
    template_conf = strip_base(auth_conf, 'template.')
    if template_conf:
        template_ = get_template(template_conf, prefix=prefix+'template.')
    urltouser = auth_conf.get('urltouser', None)
    if isinstance(urltouser, str):
        urltouser = eval_import(urltouser)
    for option in ['store.type', 'store.config', 'path.signedin']:
        if not auth_conf.has_key(option):
            raise AuthKitConfigError(
                'Missing the config key %s%s'%(prefix, option)
            )
    user_setter_params={
        'store_type': auth_conf['store.type'], 
        'store_config': auth_conf['store.config'],
        'baseurl': auth_conf.get('baseurl',''),
        'path_signedin': auth_conf['path.signedin'],
        'path_process': auth_conf.get('path.process','/process'),
        'template': template_,
        'urltouser': urltouser,
        'charset': auth_conf.get('charset'),
        'sreg_required': auth_conf.get('sreg.required'),
        'sreg_optional': auth_conf.get('sreg.optional'),
        'sreg_policyurl': auth_conf.get('sreg.policyurl'),
        'session_middleware': 'beaker.session',
    }
    auth_handler_params={
        'template':user_setter_params['template'],
        'path_verify':auth_conf.get('path.verify', '/verify'),
        'baseurl':user_setter_params['baseurl'],
        'charset':user_setter_params['charset'],
    }
    return app, auth_handler_params, user_setter_params
    
def make_passurl_handler(
    app,
    auth_conf, 
    app_conf=None,
    global_conf=None,
    prefix='authkit.openid', 
):
    app, auth_handler_params, user_setter_params = load_openid_config(
        app,
        auth_conf, 
        app_conf=None,
        global_conf=None,
        prefix='authkit.openid', 
    )
    # Note, the session middleware should already be setup by now
    # if we are not using beaker
    app = MultiHandler(app)
    app.add_method(
        'openid', 
        OpenIDAuthHandler,
        template=auth_handler_params['template'],
        path_verify=auth_handler_params['path_verify'],
        baseurl=auth_handler_params['baseurl'],
        charset = auth_handler_params['charset'],
    )
    app.add_checker('openid', status_checker)
    # XXX Some of this functionality should be moved into OpenIDAuthHandler
    app = OpenIDUserSetter(
        app,
        **user_setter_params
    )
    return app

# Backwards compatibility
PassURLSignIn = OpenIDAuthHandler