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

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

OpenID Provider:

  • Added support for transfer of ESG attributes using OpenID Attribute Exchange extension
  • If content to be returned from OP makes the URL too long the OpenID API converts the response into form via POST. Updated code to wrap this in a Javascript OnLoad? call to automatically submit and return the redirect the user back to the Relying Party.
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__ = "P.J.Kershaw@rl.ac.uk"
14__revision__ = "$Id$"
15
16import logging
17log = logging.getLogger(__name__)
18_debugLevel = log.getEffectiveLevel() <= logging.DEBUG
19
20import paste.request
21from paste.util.import_string import eval_import
22
23from authkit.authenticate import AuthKitConfigError
24
25from openid.extensions import sreg, ax
26from openid.server import server
27from openid.store.filestore import FileOpenIDStore
28from openid.consumer import discover
29
30import httplib
31import sys
32import cgi
33quoteattr = lambda s: '"%s"' % cgi.escape(s, 1)
34getInvalidKw = lambda kw: [k for k in kw if k not in \
35                           OpenIDProviderMiddleware.defKw]
36
37class OpenIDProviderMiddlewareError(Exception):
38    """OpenID Provider WSGI Middleware Error"""
39
40class OpenIDProviderConfigError(Exception):
41    """OpenID Provider Configuration Error"""
42   
43class OpenIDProviderMiddleware(object):
44    """WSGI Middleware to implement an OpenID Provider"""
45
46    defKw = dict(path_openidserver='/openidserver',
47               path_login='/login',
48               path_loginsubmit='/loginsubmit',
49               path_id='/id',
50               path_yadis='/yadis',
51               path_serveryadis='/serveryadis',
52               path_allow='/allow',
53               path_decide='/decide',
54               path_mainpage='/',
55               session_middleware='beaker.session', 
56               base_url='',
57               consumer_store_dirpath='./',
58               charset=None,
59               trace=False,
60               renderingClass=None,
61               sregResponseHandler=None,
62               axResponseHandler=None)
63   
64    defPaths=dict([(k,v) for k,v in defKw.items() if k.startswith('path_')])
65     
66    def __init__(self, app, app_conf=None, prefix='openid_provider.', **kw):
67        '''       
68        @type app_conf: dict       
69        @param app_conf: PasteDeploy application configuration dictionary
70        '''
71
72        opt = OpenIDProviderMiddleware.defKw.copy()
73        kw2AppConfOpt = {}
74        if app_conf is not None:
75            # Update from application config dictionary - filter from using
76            # prefix
77            for k,v in app_conf.items():
78                if k.startswith(prefix):
79                    subK = k.replace(prefix, '')                   
80                    filtK = '_'.join(subK.split('.'))                   
81                    opt[filtK] = v
82                    kw2AppConfOpt[filtK] = k
83                   
84            invalidOpt = getInvalidKw(opt)
85            if len(invalidOpt) > 0:
86                raise TypeError("Unexpected app_conf option(s): %s" % \
87                        (", ".join([kw2AppConfOpt[i] for i in invalidOpt])))
88           
89            # Convert from string type where required   
90            opt['charset'] = eval(opt.get('charset', 'None'))     
91            opt['trace'] = bool(eval(opt.get('trace', 'False'))) 
92             
93            renderingClassVal = opt.get('renderingClass', None)     
94            if renderingClassVal:
95                opt['renderingClass'] = eval_import(renderingClassVal)
96           
97            sregResponseHandlerVal = opt.get('sregResponseHandler', None) 
98            if sregResponseHandlerVal:
99                opt['sregResponseHandler']=eval_import(sregResponseHandlerVal) 
100            else:
101                 opt['sregResponseHandler'] = None
102
103            axResponseHandlerVal = opt.get('axResponseHandler', None) 
104            if axResponseHandlerVal:
105                opt['axResponseHandler'] = eval_import(axResponseHandlerVal)
106            else:
107                opt['axResponseHandler'] = None
108                         
109        invalidKw = getInvalidKw(kw)
110        if len(invalidKw) > 0:
111            raise TypeError("Unexpected keyword(s): %s" % ", ".join(invalidKw))
112       
113        # Update options from keywords - matching app_conf ones will be
114        # overwritten
115        opt.update(kw)
116
117        # Paths relative to base URL - Nb. remove trailing '/'
118        self.paths = dict([(k, opt[k].rstrip('/')) \
119                           for k in OpenIDProviderMiddleware.defPaths])
120       
121        if not opt['base_url']:
122            raise TypeError("base_url is not set")
123       
124        self.base_url = opt['base_url']
125
126        # Full Paths
127        self.urls = dict([(k.replace('path_', 'url_'), self.base_url+v) \
128                          for k,v in self.paths.items()])
129
130        self.method = dict([(v, k.replace('path_', 'do_')) \
131                            for k,v in self.paths.items()])
132
133        self.session_middleware = opt['session_middleware']
134
135        if opt['charset'] is None:
136            self.charset = ''
137        else:
138            self.charset = '; charset='+charset
139       
140        # If True and debug log level is set display content of response
141        self._trace = opt['trace']
142
143        log.debug("opt=%r", opt)       
144       
145        # Pages can be customised by setting external rendering interface
146        # class
147        renderingClass = opt.get('renderingClass', None) or RenderingInterface         
148        if not issubclass(renderingClass, RenderingInterface):
149            raise OpenIDProviderMiddlewareError("Rendering interface "
150                                                "class %r is not a %r "
151                                                "derived type" % \
152                                                (renderingClass, 
153                                                 RenderingInterface))
154
155        try:
156            self._renderer = renderingClass(self.base_url, self.urls)
157        except Exception, e:
158            log.error("Error instantiating rendering interface...")
159            raise
160           
161        # Callable for setting of Simple Registration attributes in HTTP header
162        # of response to Relying Party
163        self.sregResponseHandler = opt.get('sregResponseHandler', None)
164        if self.sregResponseHandler and not callable(self.sregResponseHandler):
165            raise OpenIDProviderMiddlewareError("Expecting callable for "
166                                                "sregResponseHandler keyword, "
167                                                "got %r" % \
168                                                self.sregResponseHandler)
169           
170        # Callable to handle OpenID Attribute Exchange (AX) requests from
171        # the Relying Party
172        self.axResponseHandler = opt.get('axResponseHandler', None)
173        if self.axResponseHandler and not callable(self.axResponseHandler):
174            raise OpenIDProviderMiddlewareError("Expecting callable for "
175                                                "axResponseHandler keyword, "
176                                                "got %r" % \
177                                                self.axResponseHandler)
178       
179        self.app = app
180       
181        # Instantiate OpenID consumer store and OpenID consumer.  If you
182        # were connecting to a database, you would create the database
183        # connection and instantiate an appropriate store here.
184        store = FileOpenIDStore(opt['consumer_store_dirpath'])
185        self.oidserver = server.Server(store, self.urls['url_openidserver'])
186
187   
188    def __call__(self, environ, start_response):
189       
190        if not environ.has_key(self.session_middleware):
191            raise OpenIDProviderConfigError('The session middleware %r is not '
192                                            'present. Have you set up the '
193                                            'session middleware?' % \
194                                            self.session_middleware)
195
196        self.path = environ.get('PATH_INFO').rstrip('/')
197        self.environ = environ
198        self.start_response = start_response
199        self.session = environ[self.session_middleware]
200        self._renderer.session = self.session
201       
202        if self.path in (self.paths['path_id'], self.paths['path_yadis']):
203            log.debug("No user id given in URL %s" % self.path)
204           
205            # Disallow identifier and yadis URIs where no ID was specified
206            return self.app(environ, start_response)
207           
208        elif self.path.startswith(self.paths['path_id']) or \
209           self.path.startswith(self.paths['path_yadis']):
210           
211            # Match against path minus ID as this is not known in advance           
212            pathMatch = self.path[:self.path.rfind('/')]
213        else:
214            pathMatch = self.path
215           
216        if pathMatch in self.method:
217            self.query = dict(paste.request.parse_formvars(environ)) 
218            log.debug("Calling method %s ..." % self.method[pathMatch]) 
219           
220            action = getattr(self, self.method[pathMatch])
221            response = action(environ, start_response) 
222            if self._trace and _debugLevel:
223                if isinstance(response, list):
224                    log.debug('Output for %s:\n%s', self.method[pathMatch],
225                                                    ''.join(response))
226                else:
227                    log.debug('Output for %s:\n%s', self.method[pathMatch],
228                                                    response)
229                   
230            return response
231        else:
232            log.debug("No match for path %s" % self.path)
233            return self.app(environ, start_response)
234
235
236    def do_id(self, environ, start_response):
237        '''Handle ID request'''
238        response = self._renderer.renderIdentityPage(environ)
239
240        start_response("200 OK", 
241                       [('Content-type', 'text/html'+self.charset),
242                        ('Content-length', str(len(response)))])
243        return response
244
245
246    def do_yadis(self, environ, start_response):
247        """Generate Yadis document"""
248        response = self._renderer.renderYadis(environ)
249     
250        start_response('200 OK',
251                       [('Content-type', 'application/xrds+xml'+self.charset),
252                        ('Content-length', str(len(response)))])
253        return response
254
255
256    def do_openidserver(self, environ, start_response):
257        """Handle OpenID Server Request"""
258
259        try:
260            oidRequest = self.oidserver.decodeRequest(self.query)
261           
262        except server.ProtocolError, why:
263            response = self._displayResponse(why)
264           
265        else:
266            if oidRequest is None:
267                # Display text indicating that this is an endpoint.
268                response = self.do_mainpage(environ, start_response)
269               
270            # Check mode is one of "checkid_immediate", "checkid_setup"
271            if oidRequest.mode in server.BROWSER_REQUEST_MODES:
272                response = self._handleCheckIDRequest(oidRequest)
273            else:
274                oidResponse = self.oidserver.handleRequest(oidRequest)
275                response = self._displayResponse(oidResponse)
276           
277        return response
278           
279
280    def do_allow(self, environ, start_response):
281        """Handle allow request - user allow credentials to be passed back to
282        the Relying Party?"""
283       
284        oidRequest = self.session.get('lastCheckIDRequest')
285
286        if 'Yes' in self.query:
287            if oidRequest.idSelect():
288                identity = self.urls['url_id']+'/'+self.session['username']
289            else:
290                identity = oidRequest.identity
291
292            trust_root = oidRequest.trust_root
293            if self.query.get('remember', 'no') == 'yes':
294                self.session['approved'] = {trust_root: 'always'}
295                self.session.save()
296             
297            oidResponse = self._identityApproved(oidRequest, identity)
298            response = self._displayResponse(oidResponse)
299            log.debug("do_allow response = \n%s" % response)
300            return response
301       
302        elif 'No' in self.query:
303            # TODO: Check 'no' response is OK - no causes AuthKit's Relying
304            # Party implementation to crash with 'openid.return_to' KeyError
305            # in Authkit.authenticate.open_id.process
306            oidResponse = oidRequest.answer(False)
307            return self._displayResponse(oidResponse)
308            #response = self._renderer.renderMainPage(environ)
309
310        else:
311            raise OpenIDProviderMiddlewareError('Expecting yes/no in allow '
312                                                'post.  %r' % self.query)
313
314
315    def do_serveryadis(self, environ, start_response):
316        """Handle Server Yadis call"""
317        response = self._renderer.renderServerYadis(environ)
318        start_response("200 OK", 
319                       [('Content-type', 'application/xrds+xml'),
320                        ('Content-length', str(len(response)))])
321        return response
322
323
324    def do_login(self, environ, start_response, **kw):
325        """Display Login form"""
326       
327        response = self._renderer.renderLogin(environ, **kw)
328        start_response('200 OK', 
329                       [('Content-type', 'text/html'+self.charset),
330                        ('Content-length', str(len(response)))])
331        return response
332
333
334    def do_loginsubmit(self, environ, start_response):
335        """Handle user submission from login and logout"""
336       
337        if 'submit' in self.query:
338            if 'username' in self.query:
339                # login
340                if 'username' in self.session:
341                    log.error("Attempting login for user %s: user %s is "
342                              "already logged in", self.session['username'],
343                              self.session['username'])
344                    return self._redirect(start_response,self.query['fail_to'])
345                   
346                self.session['username'] = self.query['username']
347                self.session['approved'] = {}
348                self.session.save()
349            else:
350                # logout
351                if 'username' not in self.session:
352                    log.error("No user is logged in")
353                    return self._redirect(start_response,self.query['fail_to'])
354               
355                del self.session['username']
356                self.session.pop('approved', None)
357                self.session.save()
358               
359            return self._redirect(start_response, self.query['success_to'])
360       
361        elif 'cancel' in self.query:
362            return self._redirect(start_response, self.query['fail_to'])
363        else:
364            log.error('Login input not recognised %r' % self.query)
365            return self._redirect(start_response, self.query['fail_to'])
366           
367
368    def do_mainpage(self, environ, start_response):
369
370        response = self._renderer.renderMainPage(environ)
371        start_response('200 OK', 
372                       [('Content-type', 'text/html'+self.charset),
373                        ('Content-length', str(len(response)))])
374        return response
375
376
377    def do_decide(self, environ, start_response):
378        """Display page prompting the user to decide whether to trust the site
379        requesting their credentials"""
380
381        oidRequest = self.session.get('lastCheckIDRequest')
382        if oidRequest is None:
383            log.error("No OpenID request set in session")
384            return self.do_mainpage(environ, start_response)
385       
386        approvedRoots = self.session.get('approved', {})
387       
388        if oidRequest.trust_root in approvedRoots and \
389           not oidRequest.idSelect():
390            response = self._identityApproved(oidRequest, oidRequest.identity)
391            return self._displayResponse(response)
392        else:
393            response = self._renderer.renderDecidePage(environ, oidRequest)
394           
395            start_response('200 OK', 
396                           [('Content-type', 'text/html'+self.charset),
397                            ('Content-length', str(len(response)))])
398            return response
399       
400       
401    def _identityIsAuthorized(self, oidRequest):
402        '''The given identity URL matches with a logged in user'''
403
404        username = self.session.get('username')
405        if username is None:
406            return False
407
408        if oidRequest.idSelect():
409            log.debug("OpenIDProviderMiddleware._identityIsAuthorized - "
410                      "ID Select mode set but user is already logged in")
411            return True
412       
413        identityURL = self.urls['url_id']+'/'+username
414        if oidRequest.identity != identityURL:
415            log.debug("OpenIDProviderMiddleware._identityIsAuthorized - "
416                      "user is already logged in with a different ID=%s" % \
417                      username)
418            return False
419       
420        log.debug("OpenIDProviderMiddleware._identityIsAuthorized - "
421                  "user is logged in with ID matching ID URL")
422        return True
423   
424   
425    def _trustRootIsAuthorized(self, trust_root):
426        '''The given trust root (Relying Party) has previously been approved
427        by the user'''
428        approvedRoots = self.session.get('approved', {})
429        return approvedRoots.get(trust_root) is not None
430
431
432    def _addSRegResponse(self, request, response):
433        '''Add Simple Registration attributes to response to Relying Party'''
434        if self.sregResponseHandler is None:
435            return
436       
437        sreg_req = sreg.SRegRequest.fromOpenIDRequest(request)
438
439        # Callout to external callable sets additional user attributes to be
440        # returned in response to Relying Party       
441        sreg_data = self.sregResponseHandler(self.session.get('username'))
442        sreg_resp = sreg.SRegResponse.extractResponse(sreg_req, sreg_data)
443        response.addExtension(sreg_resp)
444
445
446    def _addAXResponse(self, request, response):
447        '''Add attributes to response based on the OpenID Attribute Exchange
448        interface'''
449
450        ax_req = ax.FetchRequest.fromOpenIDRequest(request)
451        if ax_req is None:
452            log.debug("No Attribute Exchange extension set in request")
453            return
454       
455        ax_resp = ax.FetchResponse(request=ax_req)
456       
457        if self.axResponseHandler is None:
458            requiredAttr = ax_req.getRequiredAttrs()
459            if len(requiredAttr) > 0:
460                log.error("Relying party requires these attributes: %s; but no"
461                          "Attribute exchange handler 'axResponseHandler' has "
462                          "been set" % requiredAttr)
463            return
464       
465        # Set requested values - need user intervention here to confirm
466        # release of attributes + assignment based on required attributes -
467        # possibly via FetchRequest.getRequiredAttrs()
468#        for typeURI, attrInfo in ax_req.requested_attributes.items():
469#            # Value input must be list type
470#            ax_resp.setValues(typeURI, [attrInfo.alias+"Value"])
471        self.axResponseHandler(ax_req, ax_resp, self.session.get('username'))
472       
473        response.addExtension(ax_resp)
474       
475       
476    def _identityApproved(self, request, identifier=None):
477        '''Action following approval of a Relying Party by the user'''
478        response = request.answer(True, identity=identifier)
479        self._addSRegResponse(request, response)
480        self._addAXResponse(request, response)
481        return response
482
483
484    def _handleCheckIDRequest(self, oidRequest):
485       
486        log.debug("OpenIDProviderMiddleware._handleCheckIDRequest ...")
487
488        if self._identityIsAuthorized(oidRequest):
489           
490            # User is logged in - check for ID Select type request i.e. the
491            # user entered their IdP address at the Relying Party and not their
492            # OpenID Identifier.  In this case, the identity they wish to use
493            # must be confirmed.
494            if oidRequest.idSelect():
495                # OpenID identifier must be confirmed
496                return self.do_decide(self.environ, self.start_response)
497           
498            elif self._trustRootIsAuthorized(oidRequest.trust_root):
499                # User has approved this Relying Party
500                oidResponse = self._identityApproved(oidRequest)
501                return self._displayResponse(oidResponse)
502            else:
503                return self.do_decide(self.environ, self.start_response)
504               
505        elif oidRequest.immediate:
506            oidResponse = oidRequest.answer(False)
507            return self._displayResponse(oidResponse)
508       
509        else:
510            # User is not logged in - save request
511            self.session['lastCheckIDRequest'] = oidRequest
512            self.session.save()
513           
514            # Call login and if successful then call decide page to confirm
515            # user wishes to trust the Relying Party.
516            response = self.do_login(self.environ,
517                                     self.start_response,
518                                     success_to=self.urls['url_decide'])
519            return response
520
521    # If the response to the Relying Party is too long it's rendered as form
522    # with the POST method instead of query arguments in a GET 302 redirect.
523    # Wrap the form in this document to make the form submit automatically
524    # without user intervention.  See _displayResponse method below...
525    formRespWrapperTmpl = """<html>
526    <head>
527        <script type="text/javascript">
528            function doRedirect()
529            {
530                document.forms[0].submit();
531            }
532        </script>
533    </head>
534    <body onLoad="doRedirect()">
535        %s
536    </body>
537</html>"""
538
539    def _displayResponse(self, oidResponse):
540        try:
541            webresponse = self.oidserver.encodeResponse(oidResponse)
542        except server.EncodingError, why:
543            text = why.response.encodeToKVForm()
544            return self.showErrorPage(text)
545       
546        hdr = webresponse.headers.items()
547       
548        lenWebResponseBody = len(webresponse.body)
549        if lenWebResponseBody:
550            # Wrap in HTML with JAvascript OnLoad to submit the form
551            # automatically without user intervention
552            response = OpenIDProviderMiddleware.formRespWrapperTmpl % \
553                                                        webresponse.body
554        else:
555            response = ''
556           
557        hdr += [('Content-type', 'text/html'+self.charset),
558                ('Content-length', str(lenWebResponseBody))]
559           
560        log.debug("webresponse.code = %d" % webresponse.code)
561        self.start_response('%d %s' % (webresponse.code, 
562                                       httplib.responses[webresponse.code]), 
563                            hdr)
564        return response
565
566
567    def _redirect(self, start_response, url):
568        start_response('302 %s' % httplib.responses[302], 
569                       [('Content-type', 'text/html'+self.charset),
570                        ('Location', url)])
571        return []
572
573
574    def _showErrorPage(self, msg, code=500):
575        response = self._renderer.renderErrorPage(self.environ,cgi.escape(msg))
576        self.start_response('%d %s' % (code, httplib.responses[code]), 
577                            [('Content-type', 'text/html'+self.charset),
578                             ('Content-length', str(len(msg)))])
579        return response
580   
581   
582class RenderingInterface(object):
583    """Interface class for rendering of OpenID Provider pages.  Create a
584    derivative from this class to override the default look and feel and
585    behaviour of these pages.  Pass the new class name via the renderClass
586    keyword to OpenIDProviderMiddleware.__init__"""
587   
588    def __init__(self, base_url, urls):
589        self.base_url = base_url
590        self.urls = urls
591
592    def renderIdentityPage(self, environ):
593        """Render the identity page."""
594        path = environ.get('PATH_INFO').rstrip('/')
595        username = path[len(self.paths['path_id'])+1:]
596       
597        link_tag = '<link rel="openid.server" href="%s">' % \
598              self.urls['url_openidserver']
599             
600        yadis_loc_tag = '<meta http-equiv="x-xrds-location" content="%s">'%\
601            (self.urls['url_yadis']+'/'+path[4:])
602           
603        disco_tags = link_tag + yadis_loc_tag
604        ident = self.base_url + path
605
606        msg = ''
607        return self._showPage(environ, 
608                              'Identity Page', 
609                              head_extras=disco_tags, 
610                              msg='''<p>This is the identity page for %s.</p>
611                                %s
612                                ''' % (ident, msg))
613   
614    tmplServerYadis = """\
615<?xml version="1.0" encoding="UTF-8"?>
616<xrds:XRDS
617    xmlns:xrds="xri://$xrds"
618    xmlns="xri://$xrd*($v*2.0)">
619  <XRD>
620
621    <Service priority="0">
622      <Type>%(openid20type)s</Type>
623      <URI>%(endpoint_url)s</URI>
624    </Service>
625
626  </XRD>
627</xrds:XRDS>
628"""
629
630    def renderServerYadis(self, environ):
631        '''Render Yadis info'''
632        endpoint_url = self.urls['url_openidserver']
633        return RenderingInterface.tmplServerYadis % \
634            {'openid20type': discover.OPENID_IDP_2_0_TYPE, 
635             'endpoint_url': endpoint_url}
636       
637    tmplYadis = """\
638<?xml version="1.0" encoding="UTF-8"?>
639<xrds:XRDS
640    xmlns:xrds="xri://$xrds"
641    xmlns="xri://$xrd*($v*2.0)">
642  <XRD>
643
644    <Service priority="0">
645      <Type>%(openid20type)s</Type>
646      <Type>%(openid10type)s</Type>
647      <URI>%(endpoint_url)s</URI>
648      <LocalID>%(user_url)s</LocalID>
649    </Service>
650
651  </XRD>
652</xrds:XRDS>"""   
653   
654   
655    def renderYadis(self, environ):
656        """Render Yadis document"""
657        username = environ['PATH_INFO'].rstrip('/').split('/')[-1]
658       
659        endpoint_url = self.urls['url_openidserver']
660        user_url = self.urls['url_id'] + '/' + username
661       
662        yadisDict = dict(openid20type=discover.OPENID_2_0_TYPE, 
663                         openid10type=discover.OPENID_1_0_TYPE,
664                         endpoint_url=endpoint_url, 
665                         user_url=user_url)
666       
667        return RenderingInterface.tmplYadis % yadisDict
668
669       
670    def renderLogin(self, environ, success_to=None, fail_to=None):
671        """Render the login form."""
672       
673        if success_to is None:
674            success_to = self.urls['url_mainpage']
675           
676        if fail_to is None:
677            fail_to = self.urls['url_mainpage']
678       
679        return self._showPage(environ,
680                              'Login Page', form='''\
681            <h2>Login</h2>
682            <form method="GET" action="%s">
683              <input type="hidden" name="success_to" value="%s" />
684              <input type="hidden" name="fail_to" value="%s" />
685              <input type="text" name="username" value="" />
686              <input type="submit" name="submit" value="Log In" />
687              <input type="submit" name="cancel" value="Cancel" />
688            </form>
689            ''' % (self.urls['url_loginsubmit'], success_to, fail_to))
690
691
692    def renderMainPage(self, environ):
693        """Rendering the main page."""
694       
695        yadis_tag = '<meta http-equiv="x-xrds-location" content="%s">' % \
696                    self.urls['url_serveryadis']
697        username = environ['beaker.session']['username']   
698        if username:
699            openid_url = self.urls['url_id'] + '/' + username
700            user_message = """\
701            <p>You are logged in as %s. Your OpenID identity URL is
702            <tt><a href=%s>%s</a></tt>. Enter that URL at an OpenID
703            consumer to test this server.</p>
704            """ % (username, quoteattr(openid_url), openid_url)
705        else:
706            user_message = "<p>You are not <a href='%s'>logged in</a>.</p>" % \
707                            self.urls['url_login']
708
709        return self._showPage(environ,
710                              'Main Page', head_extras=yadis_tag, msg='''\
711            <p>OpenID server</p>
712   
713            %s
714   
715            <p>The URL for this server is <a href=%s><tt>%s</tt></a>.</p>
716        ''' % (user_message, quoteattr(self.base_url), self.base_url))
717   
718    def renderDecidePage(self, environ, oidRequest):
719        id_url_base = self.urls['url_id'] + '/'
720       
721        # XXX: This may break if there are any synonyms for id_url_base,
722        # such as referring to it by IP address or a CNAME.
723       
724        # TODO: OpenID 2.0 Allows oidRequest.identity to be set to
725        # http://specs.openid.net/auth/2.0/identifier_select.  See,
726        # http://openid.net/specs/openid-authentication-2_0.html.  This code
727        # implements this overriding the behaviour of the example code on
728        # which this is based.  - Check is the example code based on OpenID 1.0
729        # and therefore wrong for this behaviour?
730#        assert oidRequest.identity.startswith(id_url_base), \
731#               repr((oidRequest.identity, id_url_base))
732        expected_user = oidRequest.identity[len(id_url_base):]
733        username = environ['beaker.session']['username']
734       
735        if oidRequest.idSelect(): # We are being asked to select an ID
736            msg = '''\
737            <p>A site has asked for your identity.  You may select an
738            identifier by which you would like this site to know you.
739            On a production site this would likely be a drop down list
740            of pre-created accounts or have the facility to generate
741            a random anonymous identifier.
742            </p>
743            '''
744            fdata = {
745                'path_allow': self.urls['url_allow'],
746                'id_url_base': id_url_base,
747                'trust_root': oidRequest.trust_root,
748                }
749            form = '''\
750            <form method="POST" action="%(path_allow)s">
751            <table>
752              <tr><td>Identity:</td>
753                 <td>%(id_url_base)s<input type='text' name='identifier'></td></tr>
754              <tr><td>Trust Root:</td><td>%(trust_root)s</td></tr>
755            </table>
756            <p>Allow this authentication to proceed?</p>
757            <input type="checkbox" id="remember" name="remember" value="yes"
758                /><label for="remember">Remember this
759                decision</label><br />
760            <input type="submit" name="yes" value="yes" />
761            <input type="submit" name="no" value="no" />
762            </form>
763            '''%fdata
764           
765        elif expected_user == username:
766            msg = '''\
767            <p>A new site has asked to confirm your identity.  If you
768            approve, the site represented by the trust root below will
769            be told that you control identity URL listed below. (If
770            you are using a delegated identity, the site will take
771            care of reversing the delegation on its own.)</p>'''
772
773            fdata = {
774                'path_allow': self.urls['url_allow'],
775                'identity': oidRequest.identity,
776                'trust_root': oidRequest.trust_root,
777                }
778            form = '''\
779            <table>
780              <tr><td>Identity:</td><td>%(identity)s</td></tr>
781              <tr><td>Trust Root:</td><td>%(trust_root)s</td></tr>
782            </table>
783            <p>Allow this authentication to proceed?</p>
784            <form method="POST" action="%(path_allow)s">
785              <input type="checkbox" id="remember" name="remember" value="yes"
786                  /><label for="remember">Remember this
787                  decision</label><br />
788              <input type="submit" name="yes" value="yes" />
789              <input type="submit" name="no" value="no" />
790            </form>''' % fdata
791        else:
792            mdata = {
793                'expected_user': expected_user,
794                'username': username,
795                }
796            msg = '''\
797            <p>A site has asked for an identity belonging to
798            %(expected_user)s, but you are logged in as %(username)s.  To
799            log in as %(expected_user)s and approve the login oidRequest,
800            hit OK below.  The "Remember this decision" checkbox
801            applies only to the trust root decision.</p>''' % mdata
802
803            fdata = {
804                'path_allow': self.urls['url_allow'],
805                'identity': oidRequest.identity,
806                'trust_root': oidRequest.trust_root,
807                'expected_user': expected_user,
808                }
809            form = '''\
810            <table>
811              <tr><td>Identity:</td><td>%(identity)s</td></tr>
812              <tr><td>Trust Root:</td><td>%(trust_root)s</td></tr>
813            </table>
814            <p>Allow this authentication to proceed?</p>
815            <form method="POST" action="%(path_allow)s">
816              <input type="checkbox" id="remember" name="remember" value="yes"
817                  /><label for="remember">Remember this
818                  decision</label><br />
819              <input type="hidden" name="login_as" value="%(expected_user)s"/>
820              <input type="submit" name="yes" value="yes" />
821              <input type="submit" name="no" value="no" />
822            </form>''' % fdata
823
824        return self._showPage(environ,
825                              'Approve OpenID request?', 
826                              msg=msg, form=form)
827       
828
829    def _showPage(self, environ, 
830                  title, head_extras='', msg=None, err=None, form=None):
831
832        username = environ['beaker.session'].get('username')
833        if username is None:
834            user_link = '<a href="/login">not logged in</a>.'
835        else:
836            user_link = 'logged in as <a href="%s/%s">%s</a>.<br />'\
837                        '<a href="%s?submit=true&'\
838                        'success_to=%s">Log out</a>' % \
839                        (self.urls['url_id'], username, username, 
840                         self.urls['url_loginsubmit'],
841                         self.urls['url_login'])
842
843        body = ''
844
845        if err is not None:
846            body +=  '''\
847            <div class="error">
848              %s
849            </div>
850            ''' % err
851
852        if msg is not None:
853            body += '''\
854            <div class="message">
855              %s
856            </div>
857            ''' % msg
858
859        if form is not None:
860            body += '''\
861            <div class="form">
862              %s
863            </div>
864            ''' % form
865
866        contents = {
867            'title': 'Python OpenID Server - ' + title,
868            'head_extras': head_extras,
869            'body': body,
870            'user_link': user_link,
871            }
872
873        response = '''<html>
874  <head>
875    <title>%(title)s</title>
876    %(head_extras)s
877  </head>
878  <style type="text/css">
879      h1 a:link {
880          color: black;
881          text-decoration: none;
882      }
883      h1 a:visited {
884          color: black;
885          text-decoration: none;
886      }
887      h1 a:hover {
888          text-decoration: underline;
889      }
890      body {
891        font-family: verdana,sans-serif;
892        width: 50em;
893        margin: 1em;
894      }
895      div {
896        padding: .5em;
897      }
898      table {
899        margin: none;
900        padding: none;
901      }
902      .banner {
903        padding: none 1em 1em 1em;
904        width: 100%%;
905      }
906      .leftbanner {
907        text-align: left;
908      }
909      .rightbanner {
910        text-align: right;
911        font-size: smaller;
912      }
913      .error {
914        border: 1px solid #ff0000;
915        background: #ffaaaa;
916        margin: .5em;
917      }
918      .message {
919        border: 1px solid #2233ff;
920        background: #eeeeff;
921        margin: .5em;
922      }
923      .form {
924        border: 1px solid #777777;
925        background: #ddddcc;
926        margin: .5em;
927        margin-top: 1em;
928        padding-bottom: 0em;
929      }
930      dd {
931        margin-bottom: 0.5em;
932      }
933  </style>
934  <body>
935    <table class="banner">
936      <tr>
937        <td class="leftbanner">
938          <h1><a href="/">Python OpenID Server</a></h1>
939        </td>
940        <td class="rightbanner">
941          You are %(user_link)s
942        </td>
943      </tr>
944    </table>
945%(body)s
946  </body>
947</html>
948''' % contents
949
950        return response
951
952    def renderErrorPage(self, environ, msg):
953        response = self._showPage(environ, 'Error Processing Request', err='''\
954        <p>%s</p>
955        <!--
956
957        This is a large comment.  It exists to make this page larger.
958        That is unfortunately necessary because of the "smart"
959        handling of pages returned with an error code in IE.
960
961        *************************************************************
962        *************************************************************
963        *************************************************************
964        *************************************************************
965        *************************************************************
966        *************************************************************
967        *************************************************************
968        *************************************************************
969        *************************************************************
970        *************************************************************
971        *************************************************************
972        *************************************************************
973        *************************************************************
974        *************************************************************
975        *************************************************************
976        *************************************************************
977        *************************************************************
978        *************************************************************
979        *************************************************************
980        *************************************************************
981        *************************************************************
982        *************************************************************
983        *************************************************************
984
985        -->
986        ''' % msg)
987       
988        return response
Note: See TracBrowser for help on using the repository browser.