source: TI12-security/trunk/python/ndg.security.server/ndg/security/server/wsgi/openid_provider.py @ 4526

Subversion URL: http://proj.badc.rl.ac.uk/svn/ndg/TI12-security/trunk/python/ndg.security.server/ndg/security/server/wsgi/openid_provider.py@4526
Revision 4526, 59.0 KB checked in by pjkersha, 12 years ago (diff)

Integrated OpenID provider into WSGI stack together with Session Manager and Attribute Authority WSGI filters for Combined Services tests.

  • refactored ndg.security.server.wsgi.openid_provider.RenderingInterface? separating out e.g. code into separate DemoRenderingInterface? code
  • Updated ndg.security.server.pylons.cpntainer.lib.openid_provider_util Buffet rendering class so that it is independent of Pylons project structure. More work to be done to set Kid rendering input vars.

TODO: Add Authentication interface to OpenID Provider to enable integration with Session Manager based authentication.

Line 
1"""NDG Security OpenID Provider Middleware
2
3Compliments AuthKit OpenID Middleware used for OpenID *Relying Party*
4
5NERC Data Grid Project
6
7This software may be distributed under the terms of the Q Public License,
8version 1.0 or later.
9"""
10__author__ = "P J Kershaw"
11__date__ = "01/08/08"
12__copyright__ = "(C) 2008 STFC & NERC"
13__contact__ = "Philip.Kershaw@stfc.ac.uk"
14__revision__ = "$Id$"
15import httplib
16import sys
17import cgi
18import os
19import logging
20log = logging.getLogger(__name__)
21_debugLevel = log.getEffectiveLevel() <= logging.DEBUG
22
23import paste.request
24from paste.util.import_string import eval_import
25
26from authkit.authenticate import AuthKitConfigError
27
28from openid.extensions import sreg, ax
29from openid.server import server
30from openid.store.filestore import FileOpenIDStore
31from openid.consumer import discover
32
33quoteattr = lambda s: '"%s"' % cgi.escape(s, 1)
34
35class OpenIDProviderMiddlewareError(Exception):
36    """OpenID Provider WSGI Middleware Error"""
37
38class OpenIDProviderConfigError(Exception):
39    """OpenID Provider Configuration Error"""
40   
41class OpenIDProviderMiddleware(object):
42    """WSGI Middleware to implement an OpenID Provider
43   
44    @cvar defOpt: app_conf options / keywords to __init__ and their default
45    values.  Input keywords must match these
46    @type defOpt: dict
47   
48    @cvar defPaths: subset of defOpt.  These are keyword items corresponding
49    to the URL paths to be set for the individual OpenID Provider functions
50    @type: defPaths: dict
51   
52    @cvar formRespWrapperTmpl: If the response to the Relying Party is too long
53    it's rendered as form with the POST method instead of query arguments in a
54    GET 302 redirect.  Wrap the form in this document to make the form submit
55    automatically without user intervention.  See _displayResponse method
56    below...
57    @type formRespWrapperTmpl: basestring"""
58   
59    formRespWrapperTmpl = """<html>
60    <head>
61        <script type="text/javascript">
62            function doRedirect()
63            {
64                document.forms[0].submit();
65            }
66        </script>
67    </head>
68    <body onLoad="doRedirect()">
69        %s
70    </body>
71</html>"""
72
73    defOpt = dict(
74        path_openidserver='/openidserver',
75        path_login='/login',
76        path_loginsubmit='/loginsubmit',
77        path_id='/id',
78        path_yadis='/yadis',
79        path_serveryadis='/serveryadis',
80        path_allow='/allow',
81        path_decide='/decide',
82        path_mainpage='/',
83        session_middleware='beaker.session', 
84        base_url='',
85        consumer_store_dirpath='./',
86        charset=None,
87        trace=False,
88        renderingClass=None,
89        sregResponseHandler=None,
90        axResponseHandler=None,
91        usercreds=None)
92   
93    defPaths=dict([(k,v) for k,v in defOpt.items() if k.startswith('path_')])
94     
95    def __init__(self, app, app_conf=None, prefix='openid.provider.', **kw):
96        '''
97        @type app: callable following WSGI interface
98        @param app: next middleware application in the chain     
99        @type app_conf: dict       
100        @param app_conf: PasteDeploy application configuration dictionary
101        @type prefix: basestring
102        @param prefix: prefix for OpenID Provider configuration items
103        @type kw: dict
104        @param kw: keyword dictionary - must follow format of defOpt
105        class variable   
106        '''
107
108        opt = OpenIDProviderMiddleware.defOpt.copy()
109        if app_conf is not None:
110            # Update from application config dictionary - filter from using
111            # prefix
112            OpenIDProviderMiddleware._filterOpts(opt, app_conf, prefix=prefix)
113                       
114        # Similarly, filter keyword input                 
115        OpenIDProviderMiddleware._filterOpts(opt, kw, prefix=prefix)
116       
117        # Update options from keywords - matching app_conf ones will be
118        # overwritten
119        opt.update(kw)
120       
121        # Convert from string type where required   
122        opt['charset'] = opt.get('charset', '')
123        opt['trace'] = bool(opt.get('trace', 'False')) 
124         
125        renderingClassVal = opt.get('renderingClass', None)     
126        if renderingClassVal:
127            opt['renderingClass'] = eval_import(renderingClassVal)
128       
129        sregResponseHandlerVal = opt.get('sregResponseHandler', None) 
130        if sregResponseHandlerVal:
131            opt['sregResponseHandler'] = eval_import(sregResponseHandlerVal) 
132        else:
133            opt['sregResponseHandler'] = None
134
135        axResponseHandlerVal = opt.get('axResponseHandler', None) 
136        if axResponseHandlerVal:
137            opt['axResponseHandler'] = eval_import(axResponseHandlerVal)
138        else:
139            opt['axResponseHandler'] = None
140
141        # Paths relative to base URL - Nb. remove trailing '/'
142        self.paths = dict([(k, opt[k].rstrip('/')) \
143                           for k in OpenIDProviderMiddleware.defPaths])
144       
145        if not opt['base_url']:
146            raise TypeError("base_url is not set")
147       
148        self.base_url = opt['base_url']
149
150        # Full Paths
151        self.urls = dict([(k.replace('path_', 'url_'), self.base_url+v) \
152                          for k,v in self.paths.items()])
153
154        self.method = dict([(v, k.replace('path_', 'do_')) \
155                            for k,v in self.paths.items()])
156
157        self.session_middleware = opt['session_middleware']
158
159        if not opt['charset']:
160            self.charset = ''
161        else:
162            self.charset = '; charset='+charset
163       
164        # If True and debug log level is set display content of response
165        self._trace = opt['trace']
166
167        # Test/Admin username/password set from ini/kw args
168        userCreds = opt.get('usercreds')
169        if userCreds:
170            self._userCreds = dict([i.strip().split(':') \
171                                    for i in userCreds.split(',')])
172        else:
173            self._userCreds = {}
174
175        # TODO: revise this once a link an authN mechanism has been included.
176        if not self._userCreds:
177            raise OpenIDProviderConfigError("No username/password config "
178                                            "set-up")
179
180        log.debug("opt=%r", opt)       
181       
182        # Pages can be customised by setting external rendering interface
183        # class
184        renderingClass = opt.get('renderingClass', None) or RenderingInterface         
185        if not issubclass(renderingClass, RenderingInterface):
186            raise OpenIDProviderMiddlewareError("Rendering interface "
187                                                "class %r is not a %r "
188                                                "derived type" % \
189                                                (renderingClass, 
190                                                 RenderingInterface))
191
192        try:
193            self._render = renderingClass(self.base_url, self.urls)
194        except Exception, e:
195            log.error("Error instantiating rendering interface...")
196            raise
197                   
198        # Callable for setting of Simple Registration attributes in HTTP header
199        # of response to Relying Party
200        self.sregResponseHandler = opt.get('sregResponseHandler', None)
201        if self.sregResponseHandler and not callable(self.sregResponseHandler):
202            raise OpenIDProviderMiddlewareError("Expecting callable for "
203                                                "sregResponseHandler keyword, "
204                                                "got %r" %
205                                                self.sregResponseHandler)
206           
207        # Callable to handle OpenID Attribute Exchange (AX) requests from
208        # the Relying Party
209        self.axResponseHandler = opt.get('axResponseHandler', None)
210        if self.axResponseHandler and not callable(self.axResponseHandler):
211            raise OpenIDProviderMiddlewareError("Expecting callable for "
212                                                "axResponseHandler keyword, "
213                                                "got %r" %
214                                                self.axResponseHandler)
215       
216        self.app = app
217       
218        # Instantiate OpenID consumer store and OpenID consumer.  If you
219        # were connecting to a database, you would create the database
220        # connection and instantiate an appropriate store here.
221        store = FileOpenIDStore(
222                            os.path.expandvars(opt['consumer_store_dirpath']))
223        self.oidserver = server.Server(store, self.urls['url_openidserver'])
224
225    @classmethod
226    def _filterOpts(cls, opt, newOpt, prefix=None):
227        '''Convenience utility to filter input options set in __init__ via
228        app_conf or keywords
229       
230        @type opt: dict
231        @param opt: existing options set.  These will be updated by this
232        method based on the content of newOpt
233        @type newOpt: dict
234        @param newOpt: new options to update opt with
235        @type prefix: basestring
236        @param prefix: if set, remove the given prefix from the input options
237        @raise KeyError: if an option is set that is not in the classes
238        defOpt class variable
239        '''
240       
241        badOpt = []
242        for k,v in newOpt.items():
243            if k.startswith(prefix):
244                subK = k.replace(prefix, '')                   
245                filtK = '_'.join(subK.split('.'))   
246                if filtK not in cls.defOpt:
247                    badOpt += [k]               
248                else:
249                    opt[filtK] = v
250               
251        if len(badOpt) > 0:
252            raise TypeError("Invalid input option(s) set: %s" % 
253                            (", ".join(badOpt)))
254           
255
256   
257    def __call__(self, environ, start_response):
258        """Standard WSGI interface.  Intercepts the path if it matches any of
259        the paths set in the path_* keyword settings to the config
260       
261        @type environ: dict
262        @param environ: dictionary of environment variables
263        @type start_response: callable
264        @param start_response: standard WSGI callable to set HTTP headers
265        @rtype: basestring
266        @return: WSGI response
267        """
268        if not environ.has_key(self.session_middleware):
269            raise OpenIDProviderConfigError('The session middleware %r is not '
270                                            'present. Have you set up the '
271                                            'session middleware?' % \
272                                            self.session_middleware)
273
274        self.path = environ.get('PATH_INFO').rstrip('/')
275        self.environ = environ
276        self.start_response = start_response
277        self.session = environ[self.session_middleware]
278        self._render.session = self.session
279       
280        if self.path in (self.paths['path_id'], self.paths['path_yadis']):
281            log.debug("No user id given in URL %s" % self.path)
282           
283            # Disallow identifier and yadis URIs where no ID was specified
284            return self.app(environ, start_response)
285           
286        elif self.path.startswith(self.paths['path_id']) or \
287           self.path.startswith(self.paths['path_yadis']):
288           
289            # Match against path minus ID as this is not known in advance           
290            pathMatch = self.path[:self.path.rfind('/')]
291        else:
292            pathMatch = self.path
293           
294        if pathMatch in self.method:
295            self.query = dict(paste.request.parse_formvars(environ)) 
296            log.debug("Calling method %s ..." % self.method[pathMatch]) 
297           
298            action = getattr(self, self.method[pathMatch])
299            response = action(environ, start_response) 
300            if self._trace and _debugLevel:
301                if isinstance(response, list):
302                    log.debug('Output for %s:\n%s', self.method[pathMatch],
303                                                    ''.join(response))
304                else:
305                    log.debug('Output for %s:\n%s', self.method[pathMatch],
306                                                    response)
307                   
308            return response
309        else:
310            log.debug("No match for path %s" % self.path)
311            return self.app(environ, start_response)
312
313
314    def do_id(self, environ, start_response):
315        '''URL based discovery with an ID provided
316       
317        @type environ: dict
318        @param environ: dictionary of environment variables
319        @type start_response: callable
320        @param start_response: standard WSGI callable to set HTTP headers
321        @rtype: basestring
322        @return: WSGI response
323       
324        '''
325        response = self._render.identityPage(environ, start_response)
326        return response
327
328
329    def do_yadis(self, environ, start_response):
330        """Handle Yadis based discovery with an ID provided
331       
332        @type environ: dict
333        @param environ: dictionary of environment variables
334        @type start_response: callable
335        @param start_response: standard WSGI callable to set HTTP headers
336        @rtype: basestring
337        @return: WSGI response
338
339        """
340        response = self._render.yadis(environ, start_response)
341        return response
342
343
344    def do_openidserver(self, environ, start_response):
345        """Handle OpenID Server Request for ID Select mode i.e. no user id
346        given OpenID URL at Relying Party
347       
348        @type environ: dict
349        @param environ: dictionary of environment variables
350        @type start_response: callable
351        @param start_response: standard WSGI callable to set HTTP headers
352        @rtype: basestring
353        @return: WSGI response
354        """
355
356        try:
357            oidRequest = self.oidserver.decodeRequest(self.query)
358           
359        except server.ProtocolError, why:
360            response = self._displayResponse(why)
361           
362        else:
363            if oidRequest is None:
364                # Display text indicating that this is an endpoint.
365                response = self.do_mainpage(environ, start_response)
366           
367            # Check mode is one of "checkid_immediate", "checkid_setup"
368            elif oidRequest.mode in server.BROWSER_REQUEST_MODES:
369                response = self._handleCheckIDRequest(oidRequest)
370            else:
371                oidResponse = self.oidserver.handleRequest(oidRequest)
372                response = self._displayResponse(oidResponse)
373           
374        return response
375           
376
377    def do_allow(self, environ, start_response):
378        """Handle allow request - user allow credentials to be passed back to
379        the Relying Party?
380       
381        @type environ: dict
382        @param environ: dictionary of environment variables
383        @type start_response: callable
384        @param start_response: standard WSGI callable to set HTTP headers
385        @rtype: basestring
386        @return: WSGI response
387
388        """
389       
390        oidRequest = self.session.get('lastCheckIDRequest')
391        if oidRequest is None:
392            log.error("Suspected do_allow called from stale request")
393            #return self.app(environ, start_response)
394            response = self._render.errorPage(environ, start_response,
395                                              "Invalid request",
396                                              code=400)
397       
398        if 'Yes' in self.query:
399            if oidRequest.idSelect():
400                identity = self.urls['url_id']+'/'+self.session['username']
401            else:
402                identity = oidRequest.identity
403
404            trust_root = oidRequest.trust_root
405            if self.query.get('remember', 'no') == 'yes':
406                self.session['approved'] = {trust_root: 'always'}
407                self.session.save()
408             
409            try:
410                oidResponse = self._identityApproved(oidRequest, identity)
411            except Exception, e:
412                log.error("Setting response following ID Approval: %s" % e)
413                response = self._render.errorPage(environ, start_response,
414                    'Error setting response.  Please report the error to your '
415                    'site administrator.')
416                return response
417                         
418            response = self._displayResponse(oidResponse)
419       
420        elif 'No' in self.query:
421            # TODO: Check 'no' response is OK - no causes AuthKit's Relying
422            # Party implementation to crash with 'openid.return_to' KeyError
423            # in Authkit.authenticate.open_id.process
424            oidResponse = oidRequest.answer(False)
425            #response = self._displayResponse(oidResponse)
426            response = self._render.mainPage(environ, start_response)           
427        else:
428            response = self._render.errorPage(environ, start_response,
429                                              'Expecting yes/no in allow '
430                                              'post. %r' % self.query,
431                                              code=400)
432       
433        return response
434
435    def do_serveryadis(self, environ, start_response):
436        """Yadis based discovery for ID Select mode i.e. no user ID given for
437        OpenID identifier at Relying Party
438       
439        @type environ: dict
440        @param environ: dictionary of environment variables
441        @type start_response: callable
442        @param start_response: standard WSGI callable to set HTTP headers
443        @rtype: basestring
444        @return: WSGI response
445
446        """
447        response = self._render.serverYadis(environ, start_response)
448        return response
449
450
451    def do_login(self, environ, start_response, **kw):
452        """Display Login form
453       
454        @type environ: dict
455        @param environ: dictionary of environment variables
456        @type start_response: callable
457        @param start_response: standard WSGI callable to set HTTP headers
458        @type kw: dict
459        @param kw: keywords to login renderer - see RenderingInterface class
460        @rtype: basestring
461        @return: WSGI response
462        """
463       
464        if 'fail_to' not in kw:
465            kw['fail_to'] = self.urls['url_login']
466           
467        response = self._render.login(environ, start_response, **kw)
468        return response
469
470
471    def do_loginsubmit(self, environ, start_response):
472        """Handle user submission from login and logout
473       
474        @type environ: dict
475        @param environ: dictionary of environment variables
476        @type start_response: callable
477        @param start_response: standard WSGI callable to set HTTP headers
478        @rtype: basestring
479        @return: WSGI response
480        """
481       
482        if 'submit' in self.query:
483            if 'username' in self.query:
484                # login
485                if 'username' in self.session:
486                    log.error("Attempting login for user %s: user %s is "
487                              "already logged in", self.session['username'],
488                              self.session['username'])
489                    return self._redirect(start_response,self.query['fail_to'])
490               
491                # TODO: revise this once a link an authN mechanism has been
492                # included.
493                if self._userCreds:
494                    username = self.query['username']
495                    password = self.query.get('password')
496                    if username not in self._userCreds or \
497                       password != self._userCreds[username]:
498                        log.error("Invalid username/password entered")
499                        msg = "<p>Invalid username/password entered.  " + \
500                            "Please try again or if the problems persists " + \
501                            "contact your system administrator.</p>"
502                        response = self._render.login(environ, start_response,
503                                          msg=msg,
504                                          success_to=self.urls['url_decide'])
505                        return response
506                       
507                self.session['username'] = self.query['username']
508                self.session['approved'] = {}
509                self.session.save()
510            else:
511                # logout
512                if 'username' not in self.session:
513                    log.error("No user is logged in")
514                    return self._redirect(start_response,self.query['fail_to'])
515               
516                del self.session['username']
517                self.session.pop('approved', None)
518                self.session.save()
519               
520            return self._redirect(start_response, self.query['success_to'])
521       
522        elif 'cancel' in self.query:
523            return self._redirect(start_response, self.query['fail_to'])
524        else:
525            log.error('Login input not recognised %r' % self.query)
526            return self._redirect(start_response, self.query['fail_to'])
527           
528
529    def do_mainpage(self, environ, start_response):
530        '''Show an information page about the OpenID Provider
531       
532        @type environ: dict
533        @param environ: dictionary of environment variables
534        @type start_response: callable
535        @param start_response: standard WSGI callable to set HTTP headers
536        @rtype: basestring
537        @return: WSGI response
538        '''   
539        response = self._render.mainPage(environ, start_response)
540        return response
541
542
543    def do_decide(self, environ, start_response):
544        """Display page prompting the user to decide whether to trust the site
545        requesting their credentials
546       
547        @type environ: dict
548        @param environ: dictionary of environment variables
549        @type start_response: callable
550        @param start_response: standard WSGI callable to set HTTP headers
551        @rtype: basestring
552        @return: WSGI response
553        """
554
555        oidRequest = self.session.get('lastCheckIDRequest')
556        if oidRequest is None:
557            log.error("No OpenID request set in session")
558            return self.do_mainpage(environ, start_response)
559       
560        approvedRoots = self.session.get('approved', {})
561       
562        if oidRequest.trust_root in approvedRoots and \
563           not oidRequest.idSelect():
564            try:
565                response = self._identityApproved(oidRequest, 
566                                                  oidRequest.identity)
567            except Exception, e:
568                log.error("Setting response following ID Approval: %s" % e)
569                response = self._render.errorPage(environ, start_response,
570                    'Error setting response.  Please report the error to your '
571                    'site administrator.')
572
573                return response
574
575            return self._displayResponse(response)
576        else:
577            return self._render.decidePage(environ, start_response, oidRequest)
578       
579       
580    def _identityIsAuthorized(self, oidRequest):
581        '''Check that a user is authorized i.e. does a session exist for their
582        username and if so does it correspond to the identity URL provided.
583        This last check doesn't apply for ID Select mode where no ID was input
584        at the Relying Party.
585       
586        @type oidRequest: openid.server.server.CheckIDRequest
587        @param oidRequest: OpenID Request object
588        @rtype: bool
589        @return: True/False is user authorized
590        '''
591        username = self.session.get('username')
592        if username is None:
593            return False
594
595        if oidRequest.idSelect():
596            log.debug("OpenIDProviderMiddleware._identityIsAuthorized - "
597                      "ID Select mode set but user is already logged in")
598            return True
599       
600        identityURL = self.urls['url_id']+'/'+username
601        if oidRequest.identity != identityURL:
602            log.debug("OpenIDProviderMiddleware._identityIsAuthorized - "
603                      "user is already logged in with a different ID=%s" % \
604                      username)
605            return False
606       
607        log.debug("OpenIDProviderMiddleware._identityIsAuthorized - "
608                  "user is logged in with ID matching ID URL")
609        return True
610   
611   
612    def _trustRootIsAuthorized(self, trust_root):
613        '''Return True/False for the given trust root (Relying Party)
614        previously been approved by the user
615       
616        @type trust_root: dict
617        @param trust_root: keyed by trusted root (Relying Party) URL and
618        containing string item 'always' if approved
619        @rtype: bool
620        @return: True - trust has already been approved, False - trust root is
621        not approved'''
622        approvedRoots = self.session.get('approved', {})
623        return approvedRoots.get(trust_root) is not None
624
625
626    def _addSRegResponse(self, oidRequest, oidResponse):
627        '''Add Simple Registration attributes to response to Relying Party
628       
629        @type oidRequest: openid.server.server.CheckIDRequest
630        @param oidRequest: OpenID Check ID Request object
631        @type oidResponse: openid.server.server.OpenIDResponse
632        @param oidResponse: OpenID response object'''
633       
634        if self.sregResponseHandler is None:
635            # No Simple Registration response object was set
636            return
637       
638        sreg_req = sreg.SRegRequest.fromOpenIDRequest(oidRequest)
639
640        # Callout to external callable sets additional user attributes to be
641        # returned in response to Relying Party       
642        sreg_data = self.sregResponseHandler(self.session.get('username'))
643        sreg_resp = sreg.SRegResponse.extractResponse(sreg_req, sreg_data)
644        oidResponse.addExtension(sreg_resp)
645
646
647    def _addAXResponse(self, oidRequest, oidResponse):
648        '''Add attributes to response based on the OpenID Attribute Exchange
649        interface
650       
651        @type oidRequest: openid.server.server.CheckIDRequest
652        @param oidRequest: OpenID Check ID Request object
653        @type oidResponse: openid.server.server.OpenIDResponse
654        @param oidResponse: OpenID response object'''
655
656
657        ax_req = ax.FetchRequest.fromOpenIDRequest(oidRequest)
658        if ax_req is None:
659            log.debug("No Attribute Exchange extension set in request")
660            return
661       
662        ax_resp = ax.FetchResponse(request=ax_req)
663       
664        if self.axResponseHandler is None:
665            requiredAttr = ax_req.getRequiredAttrs()
666            if len(requiredAttr) > 0:
667                msg = ("Relying party requires these attributes: %s; but no"
668                        "Attribute exchange handler 'axResponseHandler' has "
669                        "been set" % requiredAttr)
670                log.error(msg)
671                raise OpenIDProviderConfigError(msg)
672           
673            return
674       
675        # Set requested values - need user intervention here to confirm
676        # release of attributes + assignment based on required attributes -
677        # possibly via FetchRequest.getRequiredAttrs()
678        try:
679            self.axResponseHandler(ax_req, ax_resp, 
680                                   self.session.get('username'))
681        except Exception, e:
682            log.error("%s exception raised setting requested Attribute "
683                      "Exchange values: %s" % (e.__class__, e))
684            raise
685       
686        oidResponse.addExtension(ax_resp)
687       
688       
689    def _identityApproved(self, oidRequest, identifier=None):
690        '''Action following approval of a Relying Party by the user.  Add
691        Simple Registration and/or Attribute Exchange parameters if handlers
692        were specified - See _addSRegResponse and _addAXResponse methods
693       
694        @type oidRequest: openid.server.server.CheckIDRequest
695        @param oidRequest: OpenID Check ID Request object
696        @type identifier: basestring
697        @param identifier: OpenID selected by user - for ID Select mode only
698        @rtype: openid.server.server.OpenIDResponse
699        @return: OpenID response object'''
700
701        oidResponse = oidRequest.answer(True, identity=identifier)
702        self._addSRegResponse(oidRequest, oidResponse)
703        self._addAXResponse(oidRequest, oidResponse)
704       
705        return oidResponse
706
707
708    def _handleCheckIDRequest(self, oidRequest):
709        """Handle "checkid_immediate" and "checkid_setup" type requests from
710        Relying Party
711       
712        @type oidRequest: openid.server.server.CheckIDRequest
713        @param oidRequest: OpenID Check ID request
714        @rtype: basestring
715        @return: WSGI response
716        """
717        log.debug("OpenIDProviderMiddleware._handleCheckIDRequest ...")
718       
719        # Save request
720        self.session['lastCheckIDRequest'] = oidRequest
721        self.session.save()
722       
723        if self._identityIsAuthorized(oidRequest):
724           
725            # User is logged in - check for ID Select type request i.e. the
726            # user entered their IdP address at the Relying Party and not their
727            # OpenID Identifier.  In this case, the identity they wish to use
728            # must be confirmed.
729            if oidRequest.idSelect():
730                # OpenID identifier must be confirmed
731                return self.do_decide(self.environ, self.start_response)
732           
733            elif self._trustRootIsAuthorized(oidRequest.trust_root):
734                # User has approved this Relying Party
735                try:
736                    oidResponse = self._identityApproved(oidRequest)
737                except Exception, e:
738                    log.error("Setting response following ID Approval: %s" % e)
739                    response = self._render.errorPage(environ, start_response,
740                        'Error setting response.  Please report the error to '
741                        'your site administrator.')
742                    return response
743               
744                return self._displayResponse(oidResponse)
745            else:
746                return self.do_decide(self.environ, self.start_response)
747               
748        elif oidRequest.immediate:
749            oidResponse = oidRequest.answer(False)
750            return self._displayResponse(oidResponse)
751       
752        else:
753            # User is not logged in
754           
755            # Call login and if successful then call decide page to confirm
756            # user wishes to trust the Relying Party.
757            response = self.do_login(self.environ,
758                                     self.start_response,
759                                     success_to=self.urls['url_decide'])
760            return response
761
762
763    def _displayResponse(self, oidResponse):
764        """Serialize an OpenID Response object, set headers and return WSGI
765        response.
766       
767        If the URL length for a GET request exceeds a maximum, then convert the
768        response into a HTML form and use POST method.
769       
770        @type oidResponse: openid.server.server.OpenIDResponse
771        @param oidResponse: OpenID response object
772       
773        @rtype: basestring
774        @return: WSGI response'''
775        """
776       
777        try:
778            webresponse = self.oidserver.encodeResponse(oidResponse)
779        except server.EncodingError, why:
780            text = why.response.encodeToKVForm()
781            return self.showErrorPage(text)
782       
783        hdr = webresponse.headers.items()
784       
785        # If the content length exceeds the maximum to represent on a URL, it's
786        # rendered as a form instead
787        # FIXME: Commented out oidResponse.renderAsForm() test as it doesn't
788        # give consistent answers.  Testing based on body content should work
789        # OK
790        if webresponse.body:
791        #if oidResponse.renderAsForm():
792            # Wrap in HTML with Javascript OnLoad to submit the form
793            # automatically without user intervention
794            response = OpenIDProviderMiddleware.formRespWrapperTmpl % \
795                                                        webresponse.body
796        else:
797            response = webresponse.body
798           
799        hdr += [('Content-type', 'text/html'+self.charset),
800                ('Content-length', str(len(response)))]
801           
802        self.start_response('%d %s' % (webresponse.code, 
803                                       httplib.responses[webresponse.code]), 
804                            hdr)
805        return response
806
807
808    def _redirect(self, start_response, url):
809        """Do a HTTP 302 redirect
810       
811        @type start_response: callable following WSGI start_response convention
812        @param start_response: WSGI start response callable
813        @type url: basestring
814        @param url: URL to redirect to
815        @rtype: list
816        @return: empty HTML body
817        """
818        start_response('302 %s' % httplib.responses[302], 
819                       [('Content-type', 'text/html'+self.charset),
820                        ('Location', url)])
821        return []
822
823
824    def _showErrorPage(self, msg, code=500):
825        """Display error information to the user
826       
827        @type msg: basestring
828        @param msg: error message
829        @type code: int
830        @param code: HTTP error code
831        """
832       
833        response = self._render.errorPage(self.environ, start_response,
834                                          cgi.escape(msg), code=code)
835        return response
836   
837   
838class RenderingInterface(object):
839    """Interface class for rendering of OpenID Provider pages.  It implements
840    methods for handling Yadis requests only.  All other interface methods
841    return a 404 error response.  Create a derivative from this class to
842    implement the other rendering methods as required.  DemoRenderingInterface
843    provides an example of how to do this.  To apply a custom
844    RenderingInterface class pass it's name in the OpenIDProviderMiddleware
845    app_conf dict or as a keyword argument using the option name
846    renderingClass.
847   
848    @cvar tmplServerYadis: template for returning Yadis document to Relying
849    Party.  Derived classes can reset this or completely override the
850    serverYadis method.
851   
852    @type tmplServerYadis: basestring
853   
854    @cvar tmplYadis: template for returning Yadis document containing user
855    URL to Relying Party.  Derived classes can reset this or completely
856    override the yadis method.
857   
858    @type tmplYadis: basestring"""
859   
860    tmplServerYadis = """\
861<?xml version="1.0" encoding="UTF-8"?>
862<xrds:XRDS
863    xmlns:xrds="xri://$xrds"
864    xmlns="xri://$xrd*($v*2.0)">
865  <XRD>
866
867    <Service priority="0">
868      <Type>%(openid20type)s</Type>
869      <URI>%(endpoint_url)s</URI>
870    </Service>
871
872  </XRD>
873</xrds:XRDS>
874"""
875
876    tmplYadis = """\
877<?xml version="1.0" encoding="UTF-8"?>
878<xrds:XRDS
879    xmlns:xrds="xri://$xrds"
880    xmlns="xri://$xrd*($v*2.0)">
881  <XRD>
882
883    <Service priority="0">
884      <Type>%(openid20type)s</Type>
885      <Type>%(openid10type)s</Type>
886      <URI>%(endpoint_url)s</URI>
887      <LocalID>%(user_url)s</LocalID>
888    </Service>
889
890  </XRD>
891</xrds:XRDS>"""   
892   
893    def __init__(self, base_url, urls, **opt):
894        """
895        @type base_url: basestring
896        @param base_url: base URL for OpenID Provider to which individual paths
897        are appended
898        @type urls: dict
899        @param urls: full urls for all the paths used by all the exposed
900        methods - keyed by method name - see OpenIDProviderMiddleware.paths
901        @type opt: dict
902        @param opt: additional custom options passed from the
903        OpenIDProviderMiddleware config
904        """
905        self.base_url = base_url
906        self.urls = urls
907        self.charset = ''
908   
909   
910    def serverYadis(self, environ, start_response):
911        '''Render Yadis info
912       
913        @type environ: dict
914        @param environ: dictionary of environment variables
915        @type start_response: callable
916        @param start_response: WSGI start response function.  Should be called
917        from this method to set the response code and HTTP header content
918        @rtype: basestring
919        @return: WSGI response
920        '''
921        endpoint_url = self.urls['url_openidserver']
922        response = RenderingInterface.tmplServerYadis % \
923                                {'openid20type': discover.OPENID_IDP_2_0_TYPE, 
924                                 'endpoint_url': endpoint_url}
925             
926        start_response("200 OK", 
927                       [('Content-type', 'application/xrds+xml'),
928                        ('Content-length', str(len(response)))])
929        return response
930
931
932    def yadis(self, environ, start_response):
933        """Render Yadis document containing user URL
934       
935        @type environ: dict
936        @param environ: dictionary of environment variables
937        @type start_response: callable
938        @param start_response: WSGI start response function.  Should be called
939        from this method to set the response code and HTTP header content
940        @rtype: basestring
941        @return: WSGI response
942        """
943        # Override this method to implement an alternate means to derive the
944        # username identifier
945        username = environ['PATH_INFO'].rstrip('/').split('/')[-1]
946       
947        endpoint_url = self.urls['url_openidserver']
948        user_url = self.urls['url_id'] + '/' + username
949       
950        yadisDict = dict(openid20type=discover.OPENID_2_0_TYPE, 
951                         openid10type=discover.OPENID_1_0_TYPE,
952                         endpoint_url=endpoint_url, 
953                         user_url=user_url)
954       
955        response = RenderingInterface.tmplYadis % yadisDict
956     
957        start_response('200 OK',
958                       [('Content-type', 'application/xrds+xml'+self.charset),
959                        ('Content-length', str(len(response)))])
960        return response
961   
962
963    def identityPage(self, environ, start_response):
964        """Render the identity page.
965       
966        @type environ: dict
967        @param environ: dictionary of environment variables
968        @type start_response: callable
969        @param start_response: WSGI start response function.  Should be called
970        from this method to set the response code and HTTP header content
971        @rtype: basestring
972        @return: WSGI response
973        """
974        response = "Page is not implemented"
975        start_response('%d %s' % (404, httplib.responses[code]), 
976                       [('Content-type', 'text/html'+self.charset),
977                        ('Content-length', str(len(response)))])
978        return response
979   
980       
981    def login(self, environ, start_response, 
982              success_to=None, fail_to=None, msg=''):
983        """Render the login form.
984       
985        @type environ: dict
986        @param environ: dictionary of environment variables
987        @type start_response: callable
988        @param start_response: WSGI start response function.  Should be called
989        from this method to set the response code and HTTP header content
990        @type success_to: basestring
991        @param success_to: URL put into hidden field telling 
992        OpenIDProviderMiddleware.do_loginsubmit() where to forward to on
993        successful login
994        @type fail_to: basestring
995        @param fail_to: URL put into hidden field telling 
996        OpenIDProviderMiddleware.do_loginsubmit() where to forward to on
997        login error
998        @type msg: basestring
999        @param msg: display (error) message below login form e.g. following
1000        previous failed login attempt.
1001        @rtype: basestring
1002        @return: WSGI response
1003        """
1004       
1005        response = "Page is not implemented"
1006        start_response('%d %s' % (404, httplib.responses[code]), 
1007                       [('Content-type', 'text/html'+self.charset),
1008                        ('Content-length', str(len(response)))])
1009        return response
1010
1011
1012    def mainPage(self, environ, start_response):
1013        """Rendering the main page.
1014       
1015        @type environ: dict
1016        @param environ: dictionary of environment variables
1017        @type start_response: callable
1018        @param start_response: WSGI start response function.  Should be called
1019        from this method to set the response code and HTTP header content
1020        @rtype: basestring
1021        @return: WSGI response
1022        """   
1023        response = "Page is not implemented"
1024        start_response('%d %s' % (404, httplib.responses[code]), 
1025                       [('Content-type', 'text/html'+self.charset),
1026                        ('Content-length', str(len(response)))])
1027        return response
1028   
1029
1030    def decidePage(self, environ, start_response, oidRequest):
1031        """Show page giving the user the option to approve the return of their
1032        credentials to the Relying Party.  This page is also displayed for
1033        ID select mode if the user is already logged in at the OpenID Provider.
1034        This enables them to confirm the OpenID to be sent back to the
1035        Relying Party
1036       
1037        @type environ: dict
1038        @param environ: dictionary of environment variables
1039        @type start_response: callable
1040        @param start_response: WSGI start response function.  Should be called
1041        from this method to set the response code and HTTP header content
1042        @type oidRequest: openid.server.server.CheckIDRequest
1043        @param oidRequest: OpenID Check ID Request object
1044        @rtype: basestring
1045        @return: WSGI response
1046        """
1047        response = "Page is not implemented"
1048        start_response('%d %s' % (404, httplib.responses[code]), 
1049                       [('Content-type', 'text/html'+self.charset),
1050                        ('Content-length', str(len(response)))])
1051        return response
1052
1053
1054    def errorPage(self, environ, start_response, msg, code=500):
1055        """Display error page
1056       
1057        @type environ: dict
1058        @param environ: dictionary of environment variables
1059        @type start_response: callable
1060        @param start_response: WSGI start response function.  Should be called
1061        from this method to set the response code and HTTP header content
1062        @type msg: basestring
1063        @param msg: optional message for page body
1064        @type code: int
1065        @param code: HTTP Error code to return
1066        @rtype: basestring
1067        @return: WSGI response
1068        """     
1069        response = "Page is not implemented"
1070        start_response('%d %s' % (404, httplib.responses[code]), 
1071                       [('Content-type', 'text/html'+self.charset),
1072                        ('Content-length', str(len(response)))])
1073        return response
1074       
1075   
1076class DemoRenderingInterface(RenderingInterface):
1077    """Example rendering interface class for demonstration purposes"""
1078   
1079    def __init__(self, base_url, urls):
1080        """
1081        @type base_url: basestring
1082        @param base_url: base URL for OpenID Provider to which individual paths
1083        are appended
1084        @type urls: dict
1085        @param urls: full urls for all the paths used by all the exposed
1086        methods - keyed by method name - see OpenIDProviderMiddleware.paths
1087        """
1088        self.base_url = base_url
1089        self.urls = urls
1090        self.charset = ''
1091
1092
1093    def identityPage(self, environ, start_response):
1094        """Render the identity page.
1095       
1096        @type environ: dict
1097        @param environ: dictionary of environment variables
1098        @type start_response: callable
1099        @param start_response: WSGI start response function.  Should be called
1100        from this method to set the response code and HTTP header content
1101        @rtype: basestring
1102        @return: WSGI response
1103        """
1104        path = environ.get('PATH_INFO').rstrip('/')
1105        username = path.split('/')[-1]
1106       
1107        link_tag = '<link rel="openid.server" href="%s">' % \
1108              self.urls['url_openidserver']
1109             
1110        yadis_loc_tag = '<meta http-equiv="x-xrds-location" content="%s">' % \
1111            (self.urls['url_yadis']+'/'+username)
1112           
1113        disco_tags = link_tag + yadis_loc_tag
1114        ident = self.base_url + path
1115
1116        response = self._showPage(environ, 
1117                              'Identity Page', 
1118                              head_extras=disco_tags, 
1119                              msg='<p>This is the identity page for %s.</p>' % 
1120                                  ident)
1121       
1122        start_response("200 OK", 
1123                       [('Content-type', 'text/html'+self.charset),
1124                        ('Content-length', str(len(response)))])
1125        return response
1126   
1127       
1128    def login(self, environ, start_response, 
1129              success_to=None, fail_to=None, msg=''):
1130        """Render the login form.
1131       
1132        @type environ: dict
1133        @param environ: dictionary of environment variables
1134        @type success_to: basestring
1135        @param success_to: URL put into hidden field telling 
1136        OpenIDProviderMiddleware.do_loginsubmit() where to forward to on
1137        successful login
1138        @type fail_to: basestring
1139        @param fail_to: URL put into hidden field telling 
1140        OpenIDProviderMiddleware.do_loginsubmit() where to forward to on
1141        login error
1142        @type msg: basestring
1143        @param msg: display (error) message below login form e.g. following
1144        previous failed login attempt.
1145        @rtype: basestring
1146        @return: WSGI response
1147        """
1148       
1149        if success_to is None:
1150            success_to = self.urls['url_mainpage']
1151           
1152        if fail_to is None:
1153            fail_to = self.urls['url_mainpage']
1154       
1155        form = '''\
1156<h2>Login</h2>
1157<form method="GET" action="%s">
1158  <input type="hidden" name="success_to" value="%s" />
1159  <input type="hidden" name="fail_to" value="%s" />
1160  <table cellspacing="0" border="0" cellpadding="5">
1161    <tr>
1162        <td>Username:</td>
1163        <td><input type="text" name="username" value=""/></td>
1164    </tr><tr>
1165        <td>Password:</td>
1166        <td><input type="password" name="password"/></td>
1167    </tr><tr>
1168        <td colspan="2" align="right">
1169            <input type="submit" name="submit" value="Login"/>
1170            <input type="submit" name="cancel" value="Cancel"/>
1171        </td>
1172    </tr>
1173  </table>
1174</form>
1175%s
1176''' % (self.urls['url_loginsubmit'], success_to, fail_to, msg)
1177
1178        response = self._showPage(environ, 'Login Page', form=form)
1179        start_response('200 OK', 
1180                       [('Content-type', 'text/html'+self.charset),
1181                        ('Content-length', str(len(response)))])
1182        return response
1183
1184
1185    def mainPage(self, environ, start_response):
1186        """Rendering the main page.
1187       
1188        @type environ: dict
1189        @param environ: dictionary of environment variables
1190        @type start_response: callable
1191        @param start_response: WSGI start response function.  Should be called
1192        from this method to set the response code and HTTP header content
1193        @rtype: basestring
1194        @return: WSGI response
1195        """
1196       
1197        yadis_tag = '<meta http-equiv="x-xrds-location" content="%s">' % \
1198                    self.urls['url_serveryadis']
1199        username = environ['beaker.session'].get('username')   
1200        if username:
1201            openid_url = self.urls['url_id'] + '/' + username
1202            user_message = """\
1203            <p>You are logged in as %s. Your OpenID identity URL is
1204            <tt><a href=%s>%s</a></tt>. Enter that URL at an OpenID
1205            consumer to test this server.</p>
1206            """ % (username, quoteattr(openid_url), openid_url)
1207        else:
1208            user_message = "<p>You are not <a href='%s'>logged in</a>.</p>" % \
1209                            self.urls['url_login']
1210
1211        msg = '''\
1212<p>OpenID server</p>
1213
1214%s
1215
1216<p>The URL for this server is <a href=%s><tt>%s</tt></a>.</p>
1217''' % (user_message, quoteattr(self.base_url), self.base_url)
1218        response = self._showPage(environ,
1219                                  'Main Page', 
1220                                  head_extras=yadis_tag, 
1221                                  msg=msg)
1222   
1223        start_response('200 OK', 
1224                       [('Content-type', 'text/html'+self.charset),
1225                        ('Content-length', str(len(response)))])
1226        return response
1227   
1228
1229    def decidePage(self, environ, start_response, oidRequest):
1230        """Show page giving the user the option to approve the return of their
1231        credentials to the Relying Party.  This page is also displayed for
1232        ID select mode if the user is already logged in at the OpenID Provider.
1233        This enables them to confirm the OpenID to be sent back to the
1234        Relying Party
1235       
1236        @type environ: dict
1237        @param environ: dictionary of environment variables
1238        @type start_response: callable
1239        @param start_response: WSGI start response function.  Should be called
1240        from this method to set the response code and HTTP header content
1241        @type oidRequest: openid.server.server.CheckIDRequest
1242        @param oidRequest: OpenID Check ID Request object
1243        @rtype: basestring
1244        @return: WSGI response
1245        """
1246        id_url_base = self.urls['url_id'] + '/'
1247       
1248        # XXX: This may break if there are any synonyms for id_url_base,
1249        # such as referring to it by IP address or a CNAME.
1250       
1251        # TODO: OpenID 2.0 Allows oidRequest.identity to be set to
1252        # http://specs.openid.net/auth/2.0/identifier_select.  See,
1253        # http://openid.net/specs/openid-authentication-2_0.html.  This code
1254        # implements this overriding the behaviour of the example code on
1255        # which this is based.  - Check is the example code based on OpenID 1.0
1256        # and therefore wrong for this behaviour?
1257#        assert oidRequest.identity.startswith(id_url_base), \
1258#               repr((oidRequest.identity, id_url_base))
1259        expected_user = oidRequest.identity[len(id_url_base):]
1260        username = environ['beaker.session']['username']
1261       
1262        if oidRequest.idSelect(): # We are being asked to select an ID
1263            msg = '''\
1264            <p>A site has asked for your identity.  You may select an
1265            identifier by which you would like this site to know you.
1266            On a production site this would likely be a drop down list
1267            of pre-created accounts or have the facility to generate
1268            a random anonymous identifier.
1269            </p>
1270            '''
1271            fdata = {
1272                'path_allow': self.urls['url_allow'],
1273                'id_url_base': id_url_base,
1274                'trust_root': oidRequest.trust_root,
1275                }
1276            form = '''\
1277<form method="POST" action="%(path_allow)s">
1278<table>
1279  <tr><td>Identity:</td>
1280     <td>%(id_url_base)s<input type='text' name='identifier'></td></tr>
1281  <tr><td>Trust Root:</td><td>%(trust_root)s</td></tr>
1282</table>
1283<p>Allow this authentication to proceed?</p>
1284<input type="checkbox" id="remember" name="remember" value="yes"
1285    /><label for="remember">Remember this
1286    decision</label><br />
1287<input type="submit" name="Yes" value="Yes" />
1288<input type="submit" name="No" value="No" />
1289</form>
1290''' % fdata
1291           
1292        elif expected_user == username:
1293            msg = '''\
1294            <p>A new site has asked to confirm your identity.  If you
1295            approve, the site represented by the trust root below will
1296            be told that you control identity URL listed below. (If
1297            you are using a delegated identity, the site will take
1298            care of reversing the delegation on its own.)</p>'''
1299
1300            fdata = {
1301                'path_allow': self.urls['url_allow'],
1302                'identity': oidRequest.identity,
1303                'trust_root': oidRequest.trust_root,
1304                }
1305            form = '''\
1306<table>
1307  <tr><td>Identity:</td><td>%(identity)s</td></tr>
1308  <tr><td>Trust Root:</td><td>%(trust_root)s</td></tr>
1309</table>
1310<p>Allow this authentication to proceed?</p>
1311<form method="POST" action="%(path_allow)s">
1312  <input type="checkbox" id="remember" name="remember" value="yes"
1313      /><label for="remember">Remember this
1314      decision</label><br />
1315  <input type="submit" name="yes" value="yes" />
1316  <input type="submit" name="no" value="no" />
1317</form>''' % fdata
1318        else:
1319            mdata = {
1320                'expected_user': expected_user,
1321                'username': username,
1322                }
1323            msg = '''\
1324            <p>A site has asked for an identity belonging to
1325            %(expected_user)s, but you are logged in as %(username)s.  To
1326            log in as %(expected_user)s and approve the login oidRequest,
1327            hit OK below.  The "Remember this decision" checkbox
1328            applies only to the trust root decision.</p>''' % mdata
1329
1330            fdata = {
1331                'path_allow': self.urls['url_allow'],
1332                'identity': oidRequest.identity,
1333                'trust_root': oidRequest.trust_root,
1334                'expected_user': expected_user,
1335                }
1336            form = '''\
1337<table>
1338  <tr><td>Identity:</td><td>%(identity)s</td></tr>
1339  <tr><td>Trust Root:</td><td>%(trust_root)s</td></tr>
1340</table>
1341<p>Allow this authentication to proceed?</p>
1342<form method="POST" action="%(path_allow)s">
1343  <input type="checkbox" id="remember" name="remember" value="yes"
1344      /><label for="remember">Remember this
1345      decision</label><br />
1346  <input type="hidden" name="login_as" value="%(expected_user)s"/>
1347  <input type="submit" name="yes" value="yes" />
1348  <input type="submit" name="no" value="no" />
1349</form>''' % fdata
1350
1351        response = self._showPage(environ, 'Approve OpenID request?', 
1352                                  msg=msg, form=form)           
1353        start_response('200 OK', 
1354                       [('Content-type', 'text/html'+self.charset),
1355                        ('Content-length', str(len(response)))])
1356        return response
1357   
1358
1359    def _showPage(self, 
1360                  environ, 
1361                  title, 
1362                  head_extras='', 
1363                  msg=None, 
1364                  err=None, 
1365                  form=None):
1366        """Generic page rendering method.  Derived classes may ignore this.
1367       
1368        @type environ: dict
1369        @param environ: dictionary of environment variables
1370        @type title: basestring
1371        @param title: page title
1372        @type head_extras: basestring
1373        @param head_extras: add extra HTML header elements
1374        @type msg: basestring
1375        @param msg: optional message for page body
1376        @type err: basestring
1377        @param err: optional error message for page body
1378        @type form: basestring
1379        @param form: optional form for page body       
1380        @rtype: basestring
1381        @return: WSGI response
1382        """
1383       
1384        username = environ['beaker.session'].get('username')
1385        if username is None:
1386            user_link = '<a href="/login">not logged in</a>.'
1387        else:
1388            user_link = 'logged in as <a href="%s/%s">%s</a>.<br />'\
1389                        '<a href="%s?submit=true&'\
1390                        'success_to=%s">Log out</a>' % \
1391                        (self.urls['url_id'], username, username, 
1392                         self.urls['url_loginsubmit'],
1393                         self.urls['url_login'])
1394
1395        body = ''
1396
1397        if err is not None:
1398            body +=  '''\
1399            <div class="error">
1400              %s
1401            </div>
1402            ''' % err
1403
1404        if msg is not None:
1405            body += '''\
1406            <div class="message">
1407              %s
1408            </div>
1409            ''' % msg
1410
1411        if form is not None:
1412            body += '''\
1413            <div class="form">
1414              %s
1415            </div>
1416            ''' % form
1417
1418        contents = {
1419            'title': 'Python OpenID Provider - ' + title,
1420            'head_extras': head_extras,
1421            'body': body,
1422            'user_link': user_link,
1423            }
1424
1425        response = '''<html>
1426  <head>
1427    <title>%(title)s</title>
1428    %(head_extras)s
1429  </head>
1430  <style type="text/css">
1431      h1 a:link {
1432          color: black;
1433          text-decoration: none;
1434      }
1435      h1 a:visited {
1436          color: black;
1437          text-decoration: none;
1438      }
1439      h1 a:hover {
1440          text-decoration: underline;
1441      }
1442      body {
1443        font-family: verdana,sans-serif;
1444        width: 50em;
1445        margin: 1em;
1446      }
1447      div {
1448        padding: .5em;
1449      }
1450      table {
1451        margin: none;
1452        padding: none;
1453      }
1454      .banner {
1455        padding: none 1em 1em 1em;
1456        width: 100%%;
1457      }
1458      .leftbanner {
1459        text-align: left;
1460      }
1461      .rightbanner {
1462        text-align: right;
1463        font-size: smaller;
1464      }
1465      .error {
1466        border: 1px solid #ff0000;
1467        background: #ffaaaa;
1468        margin: .5em;
1469      }
1470      .message {
1471        border: 1px solid #2233ff;
1472        background: #eeeeff;
1473        margin: .5em;
1474      }
1475      .form {
1476        border: 1px solid #777777;
1477        background: #ddddcc;
1478        margin: .5em;
1479        margin-top: 1em;
1480        padding-bottom: 0em;
1481      }
1482      dd {
1483        margin-bottom: 0.5em;
1484      }
1485  </style>
1486  <body>
1487    <table class="banner">
1488      <tr>
1489        <td class="leftbanner">
1490          <h1><a href="/">Python OpenID Provider</a></h1>
1491        </td>
1492        <td class="rightbanner">
1493          You are %(user_link)s
1494        </td>
1495      </tr>
1496    </table>
1497%(body)s
1498  </body>
1499</html>
1500''' % contents
1501
1502        return response
1503
1504    def errorPage(self, environ, start_response, msg, code=500):
1505        """Display error page
1506       
1507        @type environ: dict
1508        @param environ: dictionary of environment variables
1509        @type start_response: callable
1510        @param start_response: WSGI start response function.  Should be called
1511        from this method to set the response code and HTTP header content
1512        @type msg: basestring
1513        @param msg: optional message for page body
1514        @rtype: basestring
1515        @return: WSGI response
1516        """
1517       
1518        response = self._showPage(environ, 'Error Processing Request', err='''\
1519        <p>%s</p>
1520        <!--
1521
1522        This is a large comment.  It exists to make this page larger.
1523        That is unfortunately necessary because of the "smart"
1524        handling of pages returned with an error code in IE.
1525
1526        *************************************************************
1527        *************************************************************
1528        *************************************************************
1529        *************************************************************
1530        *************************************************************
1531        *************************************************************
1532        *************************************************************
1533        *************************************************************
1534        *************************************************************
1535        *************************************************************
1536        *************************************************************
1537        *************************************************************
1538        *************************************************************
1539        *************************************************************
1540        *************************************************************
1541        *************************************************************
1542        *************************************************************
1543        *************************************************************
1544        *************************************************************
1545        *************************************************************
1546        *************************************************************
1547        *************************************************************
1548        *************************************************************
1549
1550        -->
1551        ''' % msg)
1552       
1553        start_response('%d %s' % (code, httplib.responses[code]), 
1554                       [('Content-type', 'text/html'+self.charset),
1555                        ('Content-length', str(len(response)))])
1556        return response
Note: See TracBrowser for help on using the repository browser.