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

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

Fixes to SOAP and WS-Security middleware, added SOAP fault handling for exceptions

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