source: TI12-security/branches/ndg-security-1.5.x/ndg_security_server/ndg/security/server/wsgi/openid/provider/__init__.py @ 7122

Subversion URL: http://proj.badc.rl.ac.uk/svn/ndg/TI12-security/branches/ndg-security-1.5.x/ndg_security_server/ndg/security/server/wsgi/openid/provider/__init__.py@7122
Revision 7122, 77.3 KB checked in by pjkersha, 10 years ago (diff)

task 10: OpenID Provider HTML/Javascript response incompatible with OpenID4Java

  • 1.5.6 release fixes this issue by checking the response at the OpenID Provider before it's submitted and adding the Javascript only if a HTML form is contained.
  • Property svn:keywords set to Id
Line 
1"""NDG Security OpenID Provider Middleware
2
3Compliments AuthKit OpenID Middleware used for OpenID *Relying Party*
4
5NERC DataGrid Project
6"""
7__author__ = "P J Kershaw"
8__date__ = "01/08/08"
9__copyright__ = "(C) 2009 Science and Technology Facilities Council"
10__license__ = "BSD - see top-level directory for LICENSE file"
11__contact__ = "Philip.Kershaw@stfc.ac.uk"
12__revision__ = "$Id$"
13import httplib
14import sys
15import os
16import traceback
17import logging
18log = logging.getLogger(__name__)
19_debugLevel = log.getEffectiveLevel() <= logging.DEBUG
20
21import re
22from string import Template
23
24import paste.request
25from paste.util.import_string import eval_import
26import beaker.session
27from openid.extensions import sreg, ax
28from openid.server import server
29from openid.store.filestore import FileOpenIDStore
30from openid.consumer import discover
31
32from ndg.security.common.utils.classfactory import instantiateClass
33from ndg.security.server.wsgi import NDGSecurityMiddlewareBase 
34
35
36class IdentityMapping(object):
37    """Utility class to map between user identifiers and OpenID URIs
38    The user identifier is the user unique component of an OpenID URI
39    """
40    USER_IDENTIFIER_NAME = 'userIdentifier'
41   
42    # Template substitution may or may not contain braces
43    USER_IDENTIFIER_RE = re.compile('\$\{?%s\}?' % USER_IDENTIFIER_NAME)
44   
45    @classmethod
46    def userIdentifier2IdentityURI(cls, identityUriTmpl, userIdentifier):
47        """Convert an OpenID user identifier into an identity URI given a
48        template e.g.
49       
50        https://${userIdentifier}.openid.ac.uk + pjk => https://pjk.openid.ac.uk
51        """
52        mapping = {cls.USER_IDENTIFIER_NAME: userIdentifier}
53        tmpl = Template(identityUriTmpl)
54        return tmpl.substitute(mapping)
55       
56    @classmethod
57    def identityUri2UserIdentifier(cls, identityUriTmpl, identityUri):
58        """Parse an OpenID user identifier from an identity URI given a
59        template e.g.
60       
61        https://pjk.openid.ac.uk + https://${userIdentifier}.openid.ac.uk => pjk
62        """
63        # Subtract start and end URI snippets from the template
64        try:
65            uriPrefix, uriSuffix = cls.USER_IDENTIFIER_RE.split(identityUriTmpl)
66        except ValueError:
67            raise OpenIDProviderConfigError('Error parsing identity URI %r '
68                                            'using template %r' %
69                                            (identityUri, identityUriTmpl))
70           
71        if not identityUri.startswith(uriPrefix): 
72            raise OpenIDProviderConfigError('Identity URI %r doesn\'t match '
73                                            'the template prefix %r' %
74                                            (identityUri, uriPrefix))
75           
76        suffixIndex = identityUri.rfind(uriSuffix)   
77        if suffixIndex == -1: 
78            raise OpenIDProviderConfigError('Identity URI %r doesn\'t match '
79                                            'the template suffix %r' %
80                                            (identityUri, uriSuffix))
81       
82        userIdentifier = identityUri[:suffixIndex].replace(uriPrefix, '', 1)
83        return userIdentifier
84 
85# Place here to avoid circular import error with IdentityMapping class     
86from ndg.security.server.wsgi.openid.provider.authninterface import (
87    AbstractAuthNInterface, AuthNInterfaceError, 
88    AuthNInterfaceInvalidCredentials)
89from ndg.security.server.wsgi.openid.provider.axinterface import (AXInterface,
90    MissingRequiredAttrs, AXInterfaceReloginRequired)
91
92
93# Aliases to AXInterface exception types   
94class OpenIDProviderMissingRequiredAXAttrs(MissingRequiredAttrs): 
95    """Raise if a Relying Party *requires* one or more attributes via
96    the AX interface but this OpenID Provider cannot return them.  This
97    doesn't apply to attributes that are optional"""
98
99
100class OpenIDProviderReloginRequired(AXInterfaceReloginRequired):
101    pass
102
103# end aliases to AXInterface exception types
104
105
106class OpenIDProviderMiddlewareError(Exception):
107    """OpenID Provider WSGI Middleware Error"""
108
109
110class OpenIDProviderConfigError(OpenIDProviderMiddlewareError):
111    """OpenID Provider Configuration Error"""
112
113
114class OpenIDProviderMissingAXResponseHandler(OpenIDProviderMiddlewareError): 
115    """Raise if a Relying Party *requires* one or more attributes via
116    the AX interface but no AX Response handler has been set"""
117 
118 
119class OpenIDProviderMiddleware(NDGSecurityMiddlewareBase):
120    """WSGI Middleware to implement an OpenID Provider
121   
122    @cvar defOpt: app_conf options / keywords to __init__ and their default
123    values.  Input keywords must match these
124    @type defOpt: dict
125   
126    @cvar defPaths: subset of defOpt.  These are keyword items corresponding
127    to the URL paths to be set for the individual OpenID Provider functions
128    @type: defPaths: dict
129   
130    @cvar FORM_MATCH_TEXT: if the response from the server starts with this
131    text (i.e. it's a HTML form), return the response wrapped in a Javascript/
132    HTML wrapper which selects from submit onload.  This avoids the button
133    appearing on screen and the user having to click on it.
134    @type FORM_MATCH_TEXT: string
135   
136    @cvar FORM_RESP_WRAPPER_TMPL: If the response to the Relying Party is too
137    long it's rendered as form with the POST method instead of query arguments
138    in a GET 302 redirect.  Wrap the form in this document to make the form
139    submit automatically without user intervention.  See _displayResponse method
140    below...
141    @type FORM_RESP_WRAPPER_TMPL: basestring"""
142   
143    FORM_MATCH_TEXT = '<form'
144    FORM_RESP_WRAPPER_TMPL = """<html>
145    <head>
146        <script type="text/javascript">
147            function doRedirect()
148            {
149                document.forms[0].style.visibility = "hidden";
150                document.forms[0].submit();
151            }
152        </script>
153    </head>
154    <body onLoad="doRedirect()">
155        %s
156    </body>
157</html>"""
158
159    defOpt = dict(
160        path_openidserver='/openidserver',
161        path_login='/login',
162        path_loginsubmit='/loginsubmit',
163        path_id='/id/${userIdentifier}',
164        path_yadis='/yadis/${userIdentifier}',
165        path_serveryadis='/serveryadis',
166        path_allow='/allow',
167        path_decide='/decide',
168        path_mainpage='/',
169        session_middleware='beaker.session',
170        base_url='',
171        consumer_store_dirpath='./',
172        charset=None,
173        trace=False,
174        renderingClass=None,
175        sregResponse=None,
176        axResponse=None,
177        authNInterface=AbstractAuthNInterface,
178        trustedRelyingParties=())
179   
180    defPaths = dict([(k, v) for k, v in defOpt.items() 
181                     if k.startswith('path_')])
182   
183    userIdentifierPat = '([^/]*)'
184   
185    USERNAME_SESSION_KEYNAME = 'username'
186    IDENTITY_URI_SESSION_KEYNAME = 'identityURI'
187    APPROVED_FLAG_SESSION_KEYNAME = 'approved'
188    LAST_CHECKID_REQUEST_SESSION_KEYNAME = 'lastCheckIDRequest'
189    OID_RESPONSE_SESSION_KEYNAME = 'oidResponse'
190   
191    PARAM_PREFIX = 'openid.provider.'
192   
193    IDENTITY_URI_TMPL_PARAMNAME = 'identityUriTemplate'
194   
195    # Approve and reject submit HTML input types for the Relying Party Approval
196    # page
197    APPROVE_RP_SUBMIT = "ApproveRelyingParty"
198    REJECT_RP_SUBMIT = "RejectRelyingParty"
199   
200    TRUSTED_RELYINGPARTIES_SEP_PAT = re.compile(',\s*')
201   
202    def __init__(self, app, app_conf=None, prefix=PARAM_PREFIX, **kw):
203        '''
204        @type app: callable following WSGI interface
205        @param app: next middleware application in the chain     
206        @type app_conf: dict       
207        @param app_conf: PasteDeploy application configuration dictionary
208        @type prefix: basestring
209        @param prefix: prefix for OpenID Provider configuration items
210        @type kw: dict
211        @param kw: keyword dictionary - must follow format of defOpt
212        class variable   
213        '''
214        super(OpenIDProviderMiddleware, self).__init__(app, {})
215       
216        self.__charset = None
217        self.__paths = None
218        self.__base_url = None
219        self.__urls = None
220        self.__method = None
221        self.__sessionMiddlewareEnvironKeyName = None
222        self.__session = None 
223        self.__oidserver = None
224        self.__query = {}
225        self.__sregResponse = None
226        self.__trustedRelyingParties = ()
227        self.__axResponse = None
228
229        opt = OpenIDProviderMiddleware.defOpt.copy()
230        if app_conf is not None:
231            # Update from application config dictionary - filter from using
232            # prefix
233            OpenIDProviderMiddleware._filterOpts(opt, app_conf, prefix=prefix)
234                       
235        # Similarly, filter keyword input                 
236        OpenIDProviderMiddleware._filterOpts(opt, kw, prefix=prefix)
237       
238        # Update options from keywords - matching app_conf ones will be
239        # overwritten
240        opt.update(kw)
241       
242        self.charset = opt['charset']
243     
244        # If True and debug log level is set display content of response
245        self._trace = opt.get('trace', 'false').lower() == 'true'
246         
247        renderingClassVal = opt.get('renderingClass', None)     
248        if renderingClassVal:
249            opt['renderingClass'] = eval_import(renderingClassVal)
250       
251        sregResponseVal = opt.get('sregResponse', None) 
252        if sregResponseVal:
253            opt['sregResponse'] = eval_import(sregResponseVal) 
254        else:
255            opt['sregResponse'] = None
256
257        # Authentication interface to OpenID Provider - interface to for
258        # example a user database or other means of authentication
259        authNInterfaceName = opt.get('authNInterface')
260        if authNInterfaceName:
261            authNInterfaceClass = eval_import(authNInterfaceName)
262            if not issubclass(authNInterfaceClass, AbstractAuthNInterface):
263                raise OpenIDProviderMiddlewareError("Authentication interface "
264                                                    "class %r is not a %r "
265                                                    "derived type" % 
266                                                    (authNInterfaceClass,
267                                                     AbstractAuthNInterface))
268        else:
269            authNInterfaceClass = AbstractAuthNInterface
270       
271        # Extract Authentication interface specific properties
272        authNInterfaceProperties = dict([(k.replace('authN_', ''), v) 
273                                         for k, v in opt.items() 
274                                         if k.startswith('authN_')]) 
275         
276        try:
277            self._authN = authNInterfaceClass(**authNInterfaceProperties)
278        except Exception, e:
279            log.error("Error instantiating authentication interface: %s" % e)
280            raise
281
282        # Paths relative to base URL - Nb. remove trailing '/'
283        self.paths = dict([(k, opt[k].rstrip('/'))
284                           for k in OpenIDProviderMiddleware.defPaths])
285                       
286        if not opt['base_url']:
287            raise TypeError("base_url is not set")
288       
289        self.base_url = opt['base_url']
290
291
292        self.identityUriTmpl = opt.get(
293                        OpenIDProviderMiddleware.IDENTITY_URI_TMPL_PARAMNAME)
294        if not self.identityUriTmpl:
295            raise TypeError("%s is not set" % 
296                        OpenIDProviderMiddleware.IDENTITY_URI_TMPL_PARAMNAME)
297       
298        # Full Paths
299        self.urls = dict([(k.replace('path_', 'url_'), self.base_url + v)
300                          for k, v in self.paths.items()])
301
302        self.method = dict([(v, k.replace('path_', 'do_'))
303                            for k, v in self.paths.items()])
304
305        self.sessionMiddlewareEnvironKeyName = opt['session_middleware']
306
307        log.debug("opt=%r", opt)       
308       
309        # Pages can be customised by setting external rendering interface
310        # class
311        renderingClass = opt.get('renderingClass', None) or RenderingInterface         
312        if not issubclass(renderingClass, RenderingInterface):
313            raise OpenIDProviderMiddlewareError("Rendering interface "
314                                                "class %r is not a %r "
315                                                "derived type" % \
316                                                (renderingClass,
317                                                 RenderingInterface))
318       
319        # Extract rendering interface specific properties
320        renderingProperties = dict([(k.replace('rendering_', ''), v) 
321                                         for k, v in opt.items() 
322                                         if k.startswith('rendering_')])   
323
324        try:
325            self._render = renderingClass(self._authN,
326                                          self.base_url,
327                                          self.urls,
328                                          **renderingProperties)
329        except Exception, e:
330            log.error("Error instantiating rendering interface: %s" % e)
331            raise
332                   
333        # Callable for setting of Simple Registration attributes in HTTP header
334        # of response to Relying Party
335        self.sregResponse = opt.get('sregResponse', None)
336        if self.sregResponse and not callable(self.sregResponse):
337            raise OpenIDProviderMiddlewareError("Expecting callable for "
338                                                "sregResponse keyword, "
339                                                "got %r" % 
340                                                self.sregResponse)
341           
342        # Class to handle OpenID Attribute Exchange (AX) requests from
343        # the Relying Party
344        axResponseClassName = opt.pop('axResponse_class', None)
345        if axResponseClassName is not None:
346            axResponseModuleFilePath = opt.pop('axResponse_moduleFilePath',
347                                               None)
348            axResponseProperties = dict(
349                [(k.replace('axResponse_', ''), v) 
350                 for k,v in opt.items() if k.startswith('axResponse_')])
351               
352            try:
353                self.__axResponse = instantiateClass(
354                                        axResponseClassName, 
355                                        None, 
356                                        moduleFilePath=axResponseModuleFilePath,
357                                        objectType=AXInterface, 
358                                        classProperties=axResponseProperties)
359            except Exception, e:
360                log.error("Error instantiating AX interface: %s" % e)
361                raise
362   
363        self.trustedRelyingParties = opt['trustedRelyingParties']
364           
365        # Instantiate OpenID consumer store and OpenID consumer.  If you
366        # were connecting to a database, you would create the database
367        # connection and instantiate an appropriate store here.
368        store = FileOpenIDStore(
369                            os.path.expandvars(opt['consumer_store_dirpath']))
370        self.oidserver = server.Server(store, self.urls['url_openidserver'])
371       
372    @classmethod
373    def _filterOpts(cls, opt, newOpt, prefix=''):
374        '''Convenience utility to filter input options set in __init__ via
375        app_conf or keywords
376       
377        Nb. exclusions for authN and rendering interface properties.
378       
379        @type opt: dict
380        @param opt: existing options set.  These will be updated by this
381        method based on the content of newOpt
382        @type newOpt: dict
383        @param newOpt: new options to update opt with
384        @type prefix: basestring
385        @param prefix: if set, remove the given prefix from the input options
386        @raise KeyError: if an option is set that is not in the classes
387        defOpt class variable
388        '''
389        def _isBadOptName(optName):
390            # Allow for authN.* and rendering.* properties used by the
391            # Authentication and Rendering interfaces respectively
392            return optName not in cls.defOpt and \
393               not optName.startswith('authN_') and \
394               not optName.startswith('rendering_') and \
395               not optName.startswith('axResponse_')
396               
397        badOptNames = [] 
398        for optName, optVal in newOpt.items():
399            if prefix:
400                if optName.startswith(prefix):
401                    optName = optName.replace(prefix, '')               
402                    filtOptName = '_'.join(optName.split('.'))
403                                           
404                    # Skip assignment for bad option names and record them in
405                    # an error list instead
406                    if _isBadOptName(filtOptName):
407                        badOptNames += [optName]                   
408                    else:
409                        opt[filtOptName] = optVal
410            else:
411                filtOptName = '_'.join(optName.split('.'))
412
413                # Record any bad option names
414                if _isBadOptName(filtOptName):
415                    badOptNames += [optName]                   
416                else:
417                    opt[filtOptName] = optVal
418               
419        if len(badOptNames) > 0:
420            raise TypeError("Invalid input option(s) set: %s" % 
421                            (", ".join(badOptNames)))
422                       
423    def _matchIdentityURI(self):
424        idPaths = (self.paths['path_id'], self.paths['path_yadis'])
425        idPathMatches = [Template(path).substitute(
426                    userIdentifier=OpenIDProviderMiddleware.userIdentifierPat)
427                    for path in idPaths]
428       
429        for idPathMatch, idPath in zip(idPathMatches, idPaths):
430            if re.search(idPathMatch, self.path):
431                return idPath
432           
433        return None
434
435    def _isAnIdentityURI(self):
436        """Check input URI is an identity URI.  Use to determine whether a
437        RP discovery request has been made based on a provided user OpenID.
438        i.e. do_id / do_yadis should be invoked - see __call__ method for
439        details.  It takes the identity portion of the URI from the config
440        path_id / path_yadis settings - which ever matches e.g.
441       
442        <http/https>://<domainname>/<path>/${userIdentifier}
443       
444        e.g.
445       
446        https://badc.rl.ac.uk/openid/johnsmith
447       
448        This method should be overridden in a derived class if some
449        other means of representing identity URIs is required. e.g.
450       
451        https://johnsmith.badc.rl.ac.uk
452       
453        but note also see _matchIdentityURI method
454       
455        @rtype: bool
456        @return: return True if the given URI is an identity URI, otherwise
457        False
458        """       
459        return self._matchIdentityURI() is not None
460
461    def _parseIdentityURI(self):
462        '''Split path into identity and path fragment components
463       
464        @rtype: list
465        @return: 2 element list containing the Identity URI path fragment and
466        user identifier respectively.
467        '''
468        return OpenIDProviderMiddleware.parseIdentityURI(self.path)   
469
470    @classmethod
471    def parseIdentityURI(cls, uri):
472        '''Split uri into identity and uri fragment components
473       
474        FIXME: this method assumes a fixed identity URI scheme and should be
475        refactored to use a template based parsing
476       
477        @type uri: basestring
478        @param uri: identity URI to be parsed
479        @rtype: list
480        @return: 2 element list containing the Identity URI fragment and
481        user identifier respectively.
482        '''
483        return uri.rsplit('/', 1)
484   
485    @classmethod
486    def createIdentityURI(cls, uri, userIdentifier):
487        '''This method is the compliment to parseIdentityURI.  Make an OpenID
488        URI from a user identifier and URI fragment
489       
490        @type uri: basestring
491        @param uri: identity URI containing $userIdentifier where user id is
492        to be substituted in
493        @type userIdentifier: basestring
494        @param userIdentifier: identity URI to be parsed
495        @rtype: basestring
496        @return: user OpenID URI
497        '''
498        return Template(uri).substitute(userIdentifier=userIdentifier)
499       
500    @NDGSecurityMiddlewareBase.initCall
501    def __call__(self, environ, start_response):
502        """Standard WSGI interface.  Intercepts the path if it matches any of
503        the paths set in the path_* keyword settings to the config
504       
505        @type environ: dict
506        @param environ: dictionary of environment variables
507        @type start_response: callable
508        @param start_response: standard WSGI callable to set HTTP headers
509        @rtype: basestring
510        @return: WSGI response
511        """
512        if not environ.has_key(self.sessionMiddlewareEnvironKeyName):
513            raise OpenIDProviderConfigError('The session middleware %r is not '
514                                            'present. Have you set up the '
515                                            'session middleware?' %
516                                        self.sessionMiddlewareEnvironKeyName)
517
518        # Beware path is a property and invokes the _setPath method
519        self.session = environ[self.sessionMiddlewareEnvironKeyName]
520        self._render.session = self.session
521       
522        pathMatch = self._matchIdentityURI()
523        if not pathMatch:
524            pathMatch = self.path
525
526        if pathMatch in self.method:
527            # Calls to parse_formvars seem to gobble up the POST content such
528            # that a 2nd call yields nothing! (with Paste 1.7.1)
529            self.query = dict(paste.request.parse_formvars(environ)) 
530            log.debug("Calling method %s ..." % self.method[pathMatch]) 
531           
532            action = getattr(self, self.method[pathMatch])
533            response = action(environ, start_response) 
534            if self._trace and _debugLevel:
535                if isinstance(response, list):
536                    log.debug('Output for %s:\n%s', self.method[pathMatch],
537                                                    ''.join(response))
538                else:
539                    log.debug('Output for %s:\n%s', self.method[pathMatch],
540                                                    response)
541                   
542            return response
543        else:
544            log.debug("No match for path %s" % self.path)
545            return self._setResponse(environ, start_response)
546
547    def do_id(self, environ, start_response):
548        '''URL based discovery with an ID provided
549       
550        @type environ: dict
551        @param environ: dictionary of environment variables
552        @type start_response: callable
553        @param start_response: standard WSGI callable to set HTTP headers
554        @rtype: basestring
555        @return: WSGI response
556       
557        '''
558        response = self._render.identityPage(environ, start_response)
559        return response
560
561    def do_yadis(self, environ, start_response):
562        """Handle Yadis based discovery with an ID provided
563       
564        @type environ: dict
565        @param environ: dictionary of environment variables
566        @type start_response: callable
567        @param start_response: standard WSGI callable to set HTTP headers
568        @rtype: basestring
569        @return: WSGI response
570
571        """
572        response = self._render.yadis(environ, start_response)
573        return response
574
575    def do_serveryadis(self, environ, start_response):
576        """Yadis based discovery for ID Select mode i.e. no user ID given for
577        OpenID identifier at Relying Party
578       
579        @type environ: dict
580        @param environ: dictionary of environment variables
581        @type start_response: callable
582        @param start_response: standard WSGI callable to set HTTP headers
583        @rtype: basestring
584        @return: WSGI response
585
586        """
587        response = self._render.serverYadis(environ, start_response)
588        return response
589
590    def do_openidserver(self, environ, start_response):
591        """OpenID Server endpoint - handles OpenID Request following discovery
592       
593        @type environ: dict
594        @param environ: dictionary of environment variables
595        @type start_response: callable
596        @param start_response: standard WSGI callable to set HTTP headers
597        @rtype: basestring
598        @return: WSGI response
599        """
600        try:
601            oidRequest = self.oidserver.decodeRequest(self.query)
602           
603        except server.ProtocolError, why:
604            response = self._displayResponse(why)
605           
606        else:
607            if oidRequest is None:
608                # Display text indicating that this is an endpoint.
609                response = self.do_mainpage(environ, start_response)
610           
611            # Check mode is one of "checkid_immediate", "checkid_setup"
612            elif oidRequest.mode in server.BROWSER_REQUEST_MODES:
613                response = self._handleCheckIDRequest(oidRequest)
614            else:
615                oidResponse = self.oidserver.handleRequest(oidRequest)
616                response = self._displayResponse(oidResponse)
617           
618        return response
619
620    def do_allow(self, environ, start_response):
621        """Handle allow request processing the result of do_decide: does user
622        allow credentials to be passed back to the Relying Party?
623       
624        This method expects the follow fields to have been set in the posted
625        form created by the RenderingInterface.decidePage method called by
626        do_decide:
627       
628        'Yes'/'No': for return authentication details back to the RP or
629        abort return to RP respectively
630        'remember': remember the decision corresponding to the above 'Yes'
631        /'No'.
632        This may be set to 'Yes' or 'No'
633        'identity': set to the user's identity URL.  This usually is not
634        required since it can be obtained from oidRequest.identity attribute
635        but in ID Select mode, the identity URL will have been selected or set
636        in the decide page interface.
637       
638        @type environ: dict
639        @param environ: dictionary of environment variables
640        @type start_response: callable
641        @param start_response: standard WSGI callable to set HTTP headers
642        @rtype: basestring
643        @return: WSGI response
644        """ 
645        oidRequest = self.session.get(
646                OpenIDProviderMiddleware.LAST_CHECKID_REQUEST_SESSION_KEYNAME)
647        if oidRequest is None:
648            log.error("Suspected do_allow called from stale request")
649            return self._render.errorPage(environ, start_response,
650                                          "Invalid request.  Please report "
651                                          "this fault to your site "
652                                          "administrator.",
653                                          code=400)
654       
655        if OpenIDProviderMiddleware.APPROVE_RP_SUBMIT in self.query:
656            if oidRequest.idSelect():
657                identity = self.query.get('identity')
658                if identity is None:
659                    log.error("No identity field set from decide page for "
660                              "processing in ID Select mode")
661                    return self._render.errorPage(environ, start_response,
662                                                  "An internal error has "
663                                                  "occurred setting the "
664                                                  "OpenID user identity.  "
665                                                  "Please report this fault "
666                                                  "to your site "
667                                                  "administrator.")
668            else:
669                identity = oidRequest.identity
670
671            # Check for user selected to trust this RP for future logins.
672            self._applyTrustRoot(oidRequest)
673
674            # Check for POST'ed user explicit setting of AX parameters
675            self._applyUserAXSelections()
676                   
677            return self._displayResponse(self.oidResponse)
678       
679        elif OpenIDProviderMiddleware.REJECT_RP_SUBMIT in self.query:
680            # TODO: Check 'No' response is OK - No causes AuthKit's Relying
681            # Party implementation to crash with 'openid.return_to' KeyError
682            # in Authkit.authenticate.open_id.process
683            self.oidResponse = oidRequest.answer(False)
684            return self._render.mainPage(environ, start_response)           
685        else:
686            log.error('Setting response following ID Approval: expecting '
687                      'Yes/No in allow post. %r' % self.query)
688            return self._render.errorPage(environ, start_response,
689                                          'Error setting Yes/No response to '
690                                          'return your credentials back to '
691                                          'the requesting site.  Please '
692                                          'report this fault to your site '
693                                          'administrator.',
694                                          code=400)
695           
696    def _applyTrustRoot(self, oidRequest):
697        """Check to see if user wants to trust the current Relying Party and if
698        so set record this decision in the session
699        """
700        if self.query.get('remember', 'no').lower() == 'yes':
701            self.session[
702                OpenIDProviderMiddleware.APPROVED_FLAG_SESSION_KEYNAME] = {
703                    oidRequest.trust_root: 'always'
704                }
705            self.session.save()
706                       
707    def _applyUserAXSelections(self):
708        """Helper for do_allow method - process the query response checking
709        user Attribute Exchange settings deleting optional attributes if the
710        user has deselected them from the decide interface.  All mandatory
711        attributes will be returned regardless if the user submitted and called
712        do_allow()
713        """
714       
715        # Process decide page AX POST'ed items
716        userAXSettings = {}
717        for fieldName in self.query:
718            if fieldName.startswith('ax.'):
719                alias = fieldName.rsplit('ax.', 1)[-1]
720                userAXSettings[alias] = self.query[fieldName]
721       
722        # Apply user selections to the response
723        if userAXSettings:
724            # Get all the content namespaced as AX type
725            axArgs = self.oidResponse.fields.getArgs(ax.AXMessage.ns_uri)
726            for i in axArgs.keys():
727                # Parse alias name but note not all keys are alias and so will
728                # not use the '.' delimiter
729                keyParts = i.split('.')
730                if len(keyParts) > 1 and not keyParts[1] in userAXSettings:
731                    self.oidResponse.fields.delArg(ax.AXMessage.ns_uri, i)
732                   
733    def do_login(self, environ, start_response, **kw):
734        """Display Login form
735       
736        @type environ: dict
737        @param environ: dictionary of environment variables
738        @type start_response: callable
739        @param start_response: standard WSGI callable to set HTTP headers
740        @type kw: dict
741        @param kw: keywords to login renderer - see RenderingInterface class
742        @rtype: basestring
743        @return: WSGI response
744        """
745       
746        if 'fail_to' not in kw:
747            kw['fail_to'] = self.urls['url_login']
748           
749        response = self._render.login(environ, start_response, **kw)
750        return response
751
752    def do_loginsubmit(self, environ, start_response):
753        """Handle user submission from login and logout
754       
755        @type environ: dict
756        @param environ: dictionary of environment variables
757        @type start_response: callable
758        @param start_response: standard WSGI callable to set HTTP headers
759        @rtype: basestring
760        @return: WSGI response
761        """
762        fail2URI = self.query.get('fail_to')
763       
764        if 'submit' in self.query:
765            if 'username' in self.query:
766                return self.__loginSubmit(environ, start_response)
767            else:
768                return self.__logoutSubmit(environ, start_response)
769       
770        else:
771            if 'cancel' not in self.query:
772                log.error('Login input not recognised: %r' % self.query)
773               
774            if fail2URI is None:
775                log.error("No 'fail_to' field set in Login POST: %r" % 
776                          self.query)
777                return self._render.errorPage(environ, start_response,
778                    "An internal error occurred possibly due to a request "
779                    "that's expired.  Please retry from the site where "
780                    "you entered your OpenID.  If the problem persists "
781                    "report it to your site administrator.")               
782            else:
783                return self._redirect(start_response, fail2URI) 
784       
785    def __loginSubmit(self, environ, start_response):
786        """Helper method to do_loginsubmit - handles 'username' field in login
787        form POST
788       
789        @type environ: dict
790        @param environ: dictionary of environment variables
791        @type start_response: callable
792        @param start_response: standard WSGI callable to set HTTP headers
793        @rtype: basestring
794        @return: WSGI response
795        """
796        if (OpenIDProviderMiddleware.USERNAME_SESSION_KEYNAME in 
797            self.session):
798            log.error("Attempting login for user %s: user is already "
799                      "logged in", self.session[
800                    OpenIDProviderMiddleware.USERNAME_SESSION_KEYNAME])
801           
802            return self._redirect(start_response,
803                                  self.query['fail_to'])
804       
805        oidRequest = self.session.get(
806                OpenIDProviderMiddleware.LAST_CHECKID_REQUEST_SESSION_KEYNAME)
807       
808        if oidRequest is None:
809            log.error("Getting OpenID request for login - No request "
810                      "found in session")
811            return self._render.errorPage(environ, start_response,
812                "An internal error occurred possibly due to a request "
813                "that's expired.  Please retry from the site where "
814                "you entered your OpenID.  If the problem persists "
815                "report it to your site administrator.")
816           
817        # Get user identifier to check against credentials provided
818        if oidRequest.idSelect():
819            # ID select mode enables the user to request specifying
820            # their OpenID Provider without giving a personal user URL
821            try:
822                userIdentifiers = self._authN.username2UserIdentifiers(
823                                                environ,
824                                                self.query['username'])
825               
826            except AuthNInterfaceInvalidCredentials:
827                log.error("No username %r matching an OpenID URL: %s",
828                          self.query.get('username'),
829                          traceback.format_exc())
830                msg = ("No match was found for your account name.  Please "
831                       "check that your details are correct or contact the "
832                       "site administrator.")
833
834                response = self._render.login(environ, start_response,
835                                          msg=msg,
836                                          success_to=self.urls['url_decide'])
837                return response
838           
839            except Exception:
840                log.error("Associating username %r with OpenID URL: %s", 
841                          self.query.get('username'),
842                          traceback.format_exc())
843                msg = ("An error occured matching an OpenID to your account.  "
844                       "If the problem persists contact the site "
845                       "administrator.")
846
847                response = self._render.login(environ, start_response,
848                                          msg=msg,
849                                          success_to=self.urls['url_decide'])
850                return response
851
852            if not isinstance(userIdentifiers, (list, tuple)):
853                log.error("Unexpected type %r returned from %r for "
854                          "user identifiers; expecting list or tuple"
855                          % (type(userIdentifiers), type(self._authN)))
856                         
857                return self._render.errorPage(environ, start_response,
858                    "An internal error occurred setting your default "
859                    "OpenID URL.  Please retry from the site where "
860                    "you entered your OpenID providing your full "
861                    "identity URL.  If the problem persists report it "
862                    "to your site administrator.")
863               
864            # FIXME: Assume the *first* user identifier entry is the
865            # one to use.  The user could have multiple identifiers
866            # but in practice it's more manageable to have a single one
867            identityURI = self.createIdentityURI(self.identityUriTmpl,
868                                                 userIdentifiers[0])
869        else:
870            # Get the unique user identifier from the user's OpenID URL
871            identityURI = oidRequest.identity
872           
873        # Invoke custom authentication interface plugin
874        try:
875            self._authN.logon(environ,
876                              identityURI,
877                              self.query['username'],
878                              self.query.get('password', ''))
879           
880        except AuthNInterfaceError, e:
881            return self._render.login(environ, 
882                                      start_response,
883                                      msg=e.userMsg,
884                                      success_to=self.urls['url_decide'])                   
885        except Exception, e:
886            log.error("Unexpected %s type exception raised during "
887                      "authentication: %s", type(e),
888                      traceback.format_exc())
889            msg = ("An internal error occurred.  "
890                   "Please try again or if the problems persists "
891                   "contact your system administrator.")
892
893            response = self._render.login(environ, start_response,
894                                  msg=msg,
895                                  success_to=self.urls['url_decide'])
896            return response
897       
898        # Update session details     
899        self.session[
900            OpenIDProviderMiddleware.USERNAME_SESSION_KEYNAME
901        ] = self.query['username']
902       
903        self.session[
904            OpenIDProviderMiddleware.IDENTITY_URI_SESSION_KEYNAME] = identityURI
905               
906        self.session[
907            OpenIDProviderMiddleware.APPROVED_FLAG_SESSION_KEYNAME] = {}
908           
909        self.session.save()
910       
911        log.info("user [%s] logged in", self.session[
912                 OpenIDProviderMiddleware.USERNAME_SESSION_KEYNAME])
913       
914        return self._redirect(start_response, self.query['success_to'])
915       
916    def __logoutSubmit(self, environ, start_response):
917        """Logout helper method to do_loginsubmit - triggered when 'submit'
918        field set in login POST action but no 'username' field set
919       
920        @type environ: dict
921        @param environ: dictionary of environment variables
922        @type start_response: callable
923        @param start_response: standard WSGI callable to set HTTP headers
924        @rtype: basestring
925        @return: WSGI response
926        """
927        if (OpenIDProviderMiddleware.USERNAME_SESSION_KEYNAME not in 
928            self.session):
929            log.error("No user is logged in")
930            return self._redirect(start_response, self.query['fail_to'])
931       
932        log.info("user [%s] logging out ...", self.session[
933                 OpenIDProviderMiddleware.USERNAME_SESSION_KEYNAME])
934
935        del self.session[OpenIDProviderMiddleware.USERNAME_SESSION_KEYNAME]
936        self.session.pop(OpenIDProviderMiddleware.APPROVED_FLAG_SESSION_KEYNAME, 
937                         None)
938        self.session.save()
939       
940        try:
941            self._authN.logout()
942           
943        except Exception:
944            log.error("Unexpected exception raised during logout: %s" % 
945                      traceback.format_exc())
946            msg = ("An internal error occured during logout.  If the problem "
947                   "persists contact your system administrator.")
948
949            response = self._render.errorPage(environ, start_response, msg) 
950            return response
951           
952        return self._redirect(start_response, self.query['success_to'])
953   
954    def do_mainpage(self, environ, start_response):
955        '''Show an information page about the OpenID Provider
956       
957        @type environ: dict
958        @param environ: dictionary of environment variables
959        @type start_response: callable
960        @param start_response: standard WSGI callable to set HTTP headers
961        @rtype: basestring
962        @return: WSGI response
963        '''   
964        response = self._render.mainPage(environ, start_response)
965        return response
966
967    def _getRender(self):
968        """Get method for rendering interface object
969        @rtype: RenderingInterface
970        @return: rendering interface object
971        """
972        return self._render
973   
974    render = property(fget=_getRender, doc="Rendering interface instance")
975   
976    def do_decide(self, environ, start_response):
977        """Display page prompting the user to decide whether to trust the site
978        requesting their credentials
979       
980        @type environ: dict
981        @param environ: dictionary of environment variables
982        @type start_response: callable
983        @param start_response: standard WSGI callable to set HTTP headers
984        @rtype: basestring
985        @return: WSGI response
986        """
987        oidRequest = self.session.get(
988                OpenIDProviderMiddleware.LAST_CHECKID_REQUEST_SESSION_KEYNAME)
989        if oidRequest is None:
990            log.error("No OpenID request set in session")
991            return self._render.errorPage(environ, start_response,
992                                          "Invalid request.  Please report "
993                                          "the error to your site "
994                                          "administrator.",
995                                          code=400)
996           
997        # Prepare a response with any additional AX or SReg parameters appended
998        # so that the decide page can if required show these to the user for
999        # review before return to the RP.
1000        identityURI = self.session[
1001            OpenIDProviderMiddleware.IDENTITY_URI_SESSION_KEYNAME]
1002       
1003        # Set the Response HERE to enable the inclusion of a user confirmation
1004        # interface in the decide page so that the user can review the content
1005        # of the response to be returned to the RP.
1006        try:
1007            self._createResponse(oidRequest, identifier=identityURI)
1008                   
1009        except (OpenIDProviderMissingRequiredAXAttrs,
1010                OpenIDProviderMissingAXResponseHandler):
1011            log.error("%s type exception raised setting response following ID "
1012                      "Approval: %s", e.__class__.__name__, 
1013                      traceback.format_exc())
1014            response = self._render.errorPage(self.environ, 
1015                                              self.start_response,
1016                'The site where you wish to signin requires '
1017                'additional information which this site isn\'t '
1018                'configured to provide.  Please report this fault to '
1019                'your site administrator.')
1020            return response
1021           
1022        except OpenIDProviderReloginRequired, e:
1023            log.error("%s type exception raised setting response following ID "
1024                      "Approval: %s", e.__class__.__name__, 
1025                      traceback.format_exc())
1026            response = self._render.errorPage(self.environ, 
1027                                              self.start_response,
1028                'An error occurred setting additional parameters required by '
1029                'the site requesting your ID.  Please try logging in again.')
1030            return response
1031           
1032        except Exception, e:
1033            log.error("%s type exception raised setting response following ID "
1034                      "Approval: %s", e.__class__.__name__, 
1035                      traceback.format_exc())
1036            response = self._render.errorPage(self.environ,
1037                                              self.start_response,
1038                'An error occurred setting additional parameters required by '
1039                'the site requesting your ID.  Please report this fault to '
1040                'your site administrator.')
1041            return response
1042       
1043        try:
1044            return self._render.decidePage(environ, 
1045                                           start_response, 
1046                                           oidRequest,
1047                                           self.oidResponse)
1048        except AuthNInterfaceError, e:
1049            log.error("%s type exception raised calling decide page "
1050                      "rendering - an OpenID identifier look-up error? "
1051                      "message is: %s", e.__class__.__name__,
1052                      traceback.format_exc())
1053            response = self._render.errorPage(environ, start_response,
1054                    'An error has occurred displaying an options page '
1055                    'which checks whether you want to return to the site '
1056                    'requesting your ID.  Please report this fault to '
1057                    'your site administrator.')
1058            return response
1059
1060    def _identityIsAuthenticated(self, oidRequest):
1061        '''Check that a user is authorized i.e. does a session exist for their
1062        username and if so does it correspond to the identity URL provided.
1063        This last check doesn't apply for ID Select mode where No ID was input
1064        at the Relying Party.
1065       
1066        @type oidRequest: openid.server.server.CheckIDRequest
1067        @param oidRequest: OpenID Request object
1068        @rtype: bool
1069        @return: True/False is user authorized
1070        '''
1071        username = self.session.get(
1072                            OpenIDProviderMiddleware.USERNAME_SESSION_KEYNAME)
1073        if username is None:
1074            return False
1075
1076        if oidRequest.idSelect():
1077            log.debug("OpenIDProviderMiddleware._identityIsAuthenticated - "
1078                      "ID Select mode set but user is already logged in")
1079            return True
1080       
1081        identityURI = self.session.get(
1082                        OpenIDProviderMiddleware.IDENTITY_URI_SESSION_KEYNAME)
1083        if identityURI is None:
1084            return False
1085       
1086        if oidRequest.identity != identityURI:
1087            log.debug("OpenIDProviderMiddleware._identityIsAuthenticated - user "
1088                      "is already logged in with a different ID=%s and "
1089                      "identityURI=%s" %
1090                      (username, identityURI))
1091            return False
1092       
1093        log.debug("OpenIDProviderMiddleware._identityIsAuthenticated - "
1094                  "user is logged in with ID matching ID URI")
1095        return True
1096   
1097
1098    def _trustRootIsAuthorized(self, trust_root):
1099        '''Return True/False for the given trust root (Relying Party)
1100        previously been approved by the user
1101       
1102        @type trust_root: dict
1103        @param trust_root: keyed by trusted root (Relying Party) URL and
1104        containing string item 'always' if approved
1105        @rtype: bool
1106        @return: True - trust has already been approved, False - trust root is
1107        not approved'''
1108        approvedRoots = self.session.get(
1109                        OpenIDProviderMiddleware.APPROVED_FLAG_SESSION_KEYNAME, 
1110                        {})
1111        return approvedRoots.get(trust_root) is not None
1112
1113    def _isConfiguredTrustedRoot(self, trust_root):
1114        """the Relying Party is one of a
1115        list of RPs set in the start up configuration as not needing
1116        approval.  This could be for example sites within the same
1117        organisation as this Provider
1118
1119        @type trust_root: dict
1120        @param trust_root: keyed by trusted root (Relying Party) URL and
1121        containing string item 'always' if approved
1122        @rtype: bool
1123        @return: True - trust has already been approved, False - trust root is
1124        not approved
1125        """
1126        return trust_root in self.trustedRelyingParties
1127   
1128    def _addSRegResponse(self, oidRequest):
1129        '''Add Simple Registration attributes to response to Relying Party
1130       
1131        @type oidRequest: openid.server.server.CheckIDRequest
1132        @param oidRequest: OpenID Check ID Request object'''
1133       
1134        if self.sregResponse is None:
1135            # No Simple Registration response object was set
1136            return
1137       
1138        sreg_req = sreg.SRegRequest.fromOpenIDRequest(oidRequest)
1139
1140        # Callout to external callable sets additional user attributes to be
1141        # returned in response to Relying Party       
1142        sreg_data = self.sregResponse(self.session.get(
1143                            OpenIDProviderMiddleware.USERNAME_SESSION_KEYNAME))
1144        sreg_resp = sreg.SRegResponse.extractResponse(sreg_req, sreg_data)
1145        self.oidResponse.addExtension(sreg_resp)
1146
1147    def _addAXResponse(self, oidRequest):
1148        '''Add attributes to response based on the OpenID Attribute Exchange
1149        interface
1150       
1151        @type oidRequest: openid.server.server.CheckIDRequest
1152        @param oidRequest: OpenID Check ID Request object
1153        '''
1154
1155        ax_req = ax.FetchRequest.fromOpenIDRequest(oidRequest)
1156        if ax_req is None:
1157            log.debug("No Attribute Exchange extension set in request")
1158            return
1159       
1160        ax_resp = ax.FetchResponse(request=ax_req)
1161       
1162        if self.axResponse is None:
1163            requiredAttr = ax_req.getRequiredAttrs()
1164            if len(requiredAttr) > 0:
1165                msg = ("Relying party requires these attributes: %s; but No"
1166                       "Attribute exchange handler 'axResponse' has "
1167                       "been set" % requiredAttr)
1168                log.error(msg)
1169                raise OpenIDProviderMissingAXResponseHandler(msg)
1170           
1171            return
1172       
1173        log.debug("Calling AX plugin: %s ...",
1174                  self.axResponse.__class__.__name__)
1175       
1176        # Set requested values - need user intervention here to confirm
1177        # release of attributes + assignment based on required attributes -
1178        # possibly via FetchRequest.getRequiredAttrs()
1179        try:
1180            self.axResponse(ax_req, ax_resp, self._authN, self.session)
1181       
1182        except OpenIDProviderMissingRequiredAXAttrs, e:
1183            log.error("OpenID Provider is unable to set the AX attributes "
1184                      "required by the Relying Party's request: %s" % e)
1185            raise
1186       
1187        except OpenIDProviderReloginRequired, e:
1188            log.exception(e)
1189            raise
1190       
1191        except Exception, e:
1192            log.error("%s exception raised setting requested Attribute "
1193                      "Exchange values: %s", 
1194                      e.__class__.__name__, 
1195                      traceback.format_exc())
1196            raise
1197       
1198        log.debug("Adding AX parameters to response: %s ...", ax_resp)
1199        self.oidResponse.addExtension(ax_resp)
1200        log.debug("Added AX parameters to response")
1201       
1202    def _createResponse(self, oidRequest, identifier=None):
1203        '''Create a response object from the input request and add
1204        Simple Registration and/or Attribute Exchange parameters if handlers
1205        were specified - See _addSRegResponse and _addAXResponse methods - and
1206        only if the Relying Party has requested them
1207       
1208        @type oidRequest: openid.server.server.CheckIDRequest
1209        @param oidRequest: OpenID Check ID Request object
1210        @type identifier: basestring
1211        @param identifier: OpenID selected by user - for ID Select mode only
1212        '''
1213        self.oidResponse = oidRequest.answer(True, identity=identifier)
1214        self._addSRegResponse(oidRequest)
1215        self._addAXResponse(oidRequest)
1216
1217    def _handleCheckIDRequest(self, oidRequest):
1218        """Handle "checkid_immediate" and "checkid_setup" type requests from
1219        Relying Party
1220       
1221        @type oidRequest: openid.server.server.CheckIDRequest
1222        @param oidRequest: OpenID Check ID request
1223        @rtype: basestring
1224        @return: WSGI response
1225        """
1226        log.debug("OpenIDProviderMiddleware._handleCheckIDRequest ...")
1227       
1228        # Save request
1229        self.session[
1230            OpenIDProviderMiddleware.LAST_CHECKID_REQUEST_SESSION_KEYNAME
1231        ] = oidRequest
1232       
1233        self.session.save()
1234       
1235        if self._identityIsAuthenticated(oidRequest):
1236           
1237            # User is logged in - check for ID Select type request i.e. the
1238            # user entered their IdP address at the Relying Party and not their
1239            # full OpenID URL.  In this case, the identity they wish to use must
1240            # be confirmed.
1241            isConfiguredTrustedRoot = self._isConfiguredTrustedRoot(
1242                                                        oidRequest.trust_root)
1243            if oidRequest.idSelect() and not isConfiguredTrustedRoot:
1244                # OpenID identifier must be confirmed
1245                return self.do_decide(self.environ, self.start_response)
1246           
1247            elif (self._trustRootIsAuthorized(oidRequest.trust_root) or
1248                  isConfiguredTrustedRoot):
1249               
1250                # User entered their full OpenID URL and they have previously
1251                # approved this Relying Party OR the Relying Party is one of a
1252                # list of RPs set in the start up configuration as not needing
1253                # approval.  This could be for example sites within the same
1254                # organisation as this Provider
1255                try:
1256                    identityURI = self.session[
1257                        OpenIDProviderMiddleware.IDENTITY_URI_SESSION_KEYNAME
1258                    ]
1259                    self._createResponse(oidRequest, identifier=identityURI)
1260                   
1261                except (OpenIDProviderMissingRequiredAXAttrs,
1262                        OpenIDProviderMissingAXResponseHandler):
1263                    response = self._render.errorPage(self.environ, 
1264                                                      self.start_response,
1265                        'The site where you wish to signin requires '
1266                        'additional information which this site isn\'t '
1267                        'configured to provide.  Please report this fault to '
1268                        'your site administrator.')
1269                    return response
1270                   
1271                except OpenIDProviderReloginRequired, e:
1272                    response = self._render.errorPage(self.environ, 
1273                                                      self.start_response,
1274                        'An error occurred setting additional parameters '
1275                        'required by the site requesting your ID.  Please '
1276                        'try logging in again.')
1277                    return response
1278                   
1279                except Exception, e:
1280                    log.error("%s type exception raised setting response "
1281                              "following ID Approval: %s", 
1282                              e.__class__.__name__, 
1283                              traceback.format_exc())
1284                    response = self._render.errorPage(self.environ,
1285                                                      self.start_response,
1286                        'An error occurred setting additional parameters '
1287                        'required by the site requesting your ID.  Please '
1288                        'report this fault to your site administrator.')
1289                    return response
1290               
1291                return self._displayResponse(self.oidResponse)
1292            else:
1293                # This OpenID is being used for a login for the first time. 
1294                # Check with the user whether they want to approval the Relying
1295                # Party's request
1296                return self.do_decide(self.environ, self.start_response)
1297               
1298        elif oidRequest.immediate:
1299            oidResponse = oidRequest.answer(False)
1300            return self._displayResponse(oidResponse)       
1301        else:
1302            # User is not logged in
1303           
1304            # Call login and if successful then call decide page to confirm
1305            # user wishes to trust the Relying Party.
1306            response = self.do_login(self.environ,
1307                                     self.start_response,
1308                                     success_to=self.urls['url_decide'])
1309            return response
1310
1311    def _displayResponse(self, oidResponse):
1312        """Serialize an OpenID Response object, set headers and return WSGI
1313        response.
1314       
1315        If the URL length for a GET request exceeds a maximum, then convert the
1316        response into a HTML form and use POST method.
1317       
1318        @type oidResponse: openid.server.server.OpenIDResponse
1319        @param oidResponse: OpenID response object
1320       
1321        @rtype: basestring
1322        @return: WSGI response'''
1323        """     
1324        if not isinstance(oidResponse, server.OpenIDResponse):
1325            log.error("OpenID Response is %r type, expecting %r",
1326                      type(oidResponse), server.OpenIDResponse)
1327            return self._render.errorPage(self.environ, self.start_response,
1328                                          "Error setting a response.  Please "
1329                                          "report this fault to your site "
1330                                          "administrator.",
1331                                          code=500)
1332
1333        try:
1334            webresponse = self.oidserver.encodeResponse(oidResponse)
1335        except server.EncodingError, why:
1336            text = why.response.encodeToKVForm()
1337            response = self._render.errorPage(self.environ, 
1338                                              self.start_response, 
1339                                              text)
1340            return response
1341       
1342        hdr = webresponse.headers.items()
1343       
1344        # If the content length exceeds the maximum to represent on a URL, it's
1345        # rendered as a form instead
1346        # FIXME: Commented out oidResponse.renderAsForm() test as it doesn't
1347        # give consistent answers.  Testing based on body content should work
1348        # OK
1349        if webresponse.body.startswith(
1350                                    OpenIDProviderMiddleware.FORM_MATCH_TEXT):
1351#        if oidResponse.renderAsForm():
1352            # Wrap in HTML with Javascript OnLoad to submit the form
1353            # automatically without user intervention
1354            response = OpenIDProviderMiddleware.FORM_RESP_WRAPPER_TMPL % \
1355                                                        webresponse.body
1356        else:
1357            response = webresponse.body
1358           
1359        hdr += [('Content-type', 'text/html' + self.charset),
1360                ('Content-length', str(len(response)))]
1361       
1362        log.debug("Sending response to Relying Party:\n\nheader=%r\nbody=%r",
1363                  hdr, response)
1364           
1365        self.start_response('%d %s' % (webresponse.code,
1366                                       httplib.responses[webresponse.code]),
1367                            hdr)
1368        return response
1369
1370    def _redirect(self, start_response, url):
1371        """Do a HTTP 302 redirect
1372       
1373        @type start_response: function
1374        @param start_response: WSGI start response callable
1375        @type url: basestring
1376        @param url: URL to redirect to
1377        @rtype: list
1378        @return: empty HTML body
1379        """
1380        start_response('302 %s' % httplib.responses[302],
1381                       [('Content-type', 'text/html' + self.charset),
1382                        ('Location', url)])
1383        return []
1384
1385    def getCharset(self):
1386        return self.__charset
1387
1388    def getPaths(self):
1389        return self.__paths
1390
1391    def getBase_url(self):
1392        return self.__base_url
1393
1394    def getUrls(self):
1395        return self.__urls
1396
1397    def getMethod(self):
1398        return self.__method
1399
1400    def getSessionMiddlewareEnvironKeyName(self):
1401        return self.__sessionMiddlewareEnvironKeyName
1402
1403    def getSession(self):
1404        return self.__session
1405
1406    def setCharset(self, value):       
1407        # Convert from string type where required   
1408        if not value:
1409            self.__charset = ''
1410        elif isinstance(value, basestring):
1411            self.__charset = '; charset=' + value
1412        else:
1413            raise TypeError('Expecting string type for "charset" attribute; '
1414                            'got %r' % type(value))
1415
1416    def setPaths(self, value):
1417        if not isinstance(value, dict):
1418            raise TypeError('Expecting dict type for '
1419                            '"paths" attribute; got %r' %
1420                            type(value))
1421        self.__paths = value
1422
1423    def setBase_url(self, value):
1424        if not isinstance(value, basestring):
1425            raise TypeError('Expecting string type for '
1426                            '"base_url" attribute; got %r' %
1427                            type(value))
1428        self.__base_url = value
1429
1430    def setUrls(self, value):
1431        if not isinstance(value, dict):
1432            raise TypeError('Expecting dict type for '
1433                            '"urls" attribute; got %r' %
1434                            type(value))
1435        self.__urls = value
1436
1437    def setMethod(self, value):
1438        if not isinstance(value, dict):
1439            raise TypeError('Expecting dict type for '
1440                            '"method" attribute; got %r' %
1441                            type(value))
1442        self.__method = value
1443
1444    def setSessionMiddlewareEnvironKeyName(self, value):
1445        if not isinstance(value, basestring):
1446            raise TypeError('Expecting string type for '
1447                            '"sessionMiddlewareEnvironKeyName" attribute; '
1448                            'got %r' %
1449                            type(value))
1450        self.__sessionMiddlewareEnvironKeyName = value
1451
1452    def setSession(self, value):
1453        if not isinstance(value, beaker.session.SessionObject):
1454            raise TypeError('Expecting beaker.session.SessionObject type for '
1455                            '"session" attribute; got %r' %
1456                            type(value))
1457
1458        self.__session = value
1459
1460    def getOidResponse(self):
1461        '''Cached OpenID Response object'''
1462        cls = OpenIDProviderMiddleware
1463        return self.session.get(cls.OID_RESPONSE_SESSION_KEYNAME)
1464
1465    def getSregResponse(self):
1466        return self.__sregResponse
1467
1468    def getAxResponse(self):
1469        return self.__axResponse
1470
1471    def getOidserver(self):
1472        return self.__oidserver
1473
1474    def getQuery(self):
1475        return self.__query
1476
1477    def getTrustedRelyingParties(self):
1478        return self.__trustedRelyingParties
1479
1480    def setOidResponse(self, value):
1481        if not isinstance(value, server.OpenIDResponse):
1482            raise TypeError('Expecting OpenIDResponse type for '
1483                            '"oidResponse" attribute; got %r' %
1484                            type(value))
1485           
1486        cls = OpenIDProviderMiddleware
1487        self.session[cls.OID_RESPONSE_SESSION_KEYNAME] = value
1488        self.session.save()
1489
1490    def setSregResponse(self, value):
1491        self.__sregResponse = value
1492
1493    def setAxResponse(self, value):
1494        if not isinstance(value, AXInterface):
1495            raise TypeError('Expecting AXInterface type for '
1496                            '"axResponse" attribute; got %r' %
1497                            type(value))
1498        self.__axResponse = value
1499
1500    def setOidserver(self, value):
1501        if not isinstance(value, server.Server):
1502            raise TypeError('Expecting openid.server.server.Server type for '
1503                            '"oidserver" attribute; got %r' %
1504                            type(value))
1505        self.__oidserver = value
1506
1507    def setQuery(self, value):
1508        if not isinstance(value, dict):
1509            raise TypeError('Expecting dict type for '
1510                            '"query" attribute; got %r' %
1511                            type(value))
1512        self.__query = value
1513
1514    def setTrustedRelyingParties(self, value):
1515        if isinstance(value, basestring):
1516            pat = OpenIDProviderMiddleware.TRUSTED_RELYINGPARTIES_SEP_PAT
1517            self.__trustedRelyingParties = tuple([i for i in pat.split(value)])
1518           
1519        elif isinstance(value, (list, tuple)):
1520            self.__trustedRelyingParties = tuple(value)
1521        else:
1522            raise TypeError('Expecting list or tuple type for '
1523                            '"trustedRelyingParties" attribute; got %r' %
1524                            type(value))
1525
1526    charset = property(getCharset, setCharset, None, "Charset's Docstring")
1527
1528    paths = property(getPaths, setPaths, None, "Dictionary of Paths for the app")
1529
1530    base_url = property(getBase_url, setBase_url, None, "Base URL for the app")
1531
1532    urls = property(getUrls, setUrls, None, "dictionary of URLs for app")
1533
1534    method = property(getMethod, setMethod, None, 
1535                      "Method name keyed from requested URL")
1536
1537    sessionMiddlewareEnvironKeyName = property(
1538                                  getSessionMiddlewareEnvironKeyName, 
1539                                  setSessionMiddlewareEnvironKeyName, 
1540                                  None, 
1541                                  "environ key name for Beaker Session "
1542                                  "middleware")
1543
1544    session = property(getSession, setSession, None, "Session's Docstring")
1545
1546    oidResponse = property(getOidResponse, 
1547                           setOidResponse, 
1548                           None, 
1549                           "OpenID response object")
1550
1551    sregResponse = property(getSregResponse, 
1552                            setSregResponse, 
1553                            None, 
1554                            "SReg response handler class")
1555
1556    axResponse = property(getAxResponse, setAxResponse, None, 
1557                          "Attribute Exchange response object")
1558
1559    oidserver = property(getOidserver, setOidserver, None, 
1560                         "OpenID server instance")
1561
1562    query = property(getQuery, setQuery, None, 
1563                     "dictionary of HTML query parameters")
1564
1565    trustedRelyingParties = property(getTrustedRelyingParties, 
1566                                     setTrustedRelyingParties, 
1567                                     None, 
1568                                     "Relying Parties trusted by this Provider")
1569
1570   
1571class RenderingInterfaceError(Exception):
1572    """Base class for RenderingInterface exceptions
1573   
1574    A standard message is raised set by the msg class variable but the actual
1575    exception details are logged to the error log.  The use of a standard
1576    message enables callers to use its content for user error messages.
1577   
1578    @type msg: basestring
1579    @cvar msg: standard message to be raised for this exception"""
1580    userMsg = ("An internal error occurred with the page layout,  Please "
1581               "contact your system administrator")
1582    errorMsg = "RenderingInterface error"
1583   
1584    def __init__(self, *arg, **kw):
1585        if len(arg) > 0:
1586            msg = arg[0]
1587        else:
1588            msg = self.__class__.errorMsg
1589           
1590        log.error(msg)
1591        Exception.__init__(self, msg, **kw)
1592       
1593       
1594class RenderingInterfaceInitError(RenderingInterfaceError):
1595    """Error with initialisation of RenderingInterface.  Raise from __init__"""
1596    errorMsg = "RenderingInterface initialisation error"
1597   
1598   
1599class RenderingInterfaceConfigError(RenderingInterfaceError):
1600    """Error with configuration settings.  Raise from __init__"""
1601    errorMsg = "RenderingInterface configuration error"   
1602
1603 
1604class RenderingInterface(object):
1605    """Interface class for rendering of OpenID Provider pages.  It implements
1606    methods for handling Yadis requests only.  All other interface methods
1607    return a 404 error response.  Create a derivative from this class to
1608    implement the other rendering methods as required. 
1609    ndg.security.server.wsgi.openid.provider.renderingInterface.demo.DemoRenderingInterface
1610    provides an example of how to do this.  To apply a custom
1611    RenderingInterface class pass it's name in the OpenIDProviderMiddleware
1612    app_conf dict or as a keyword argument using the option name
1613    renderingClass.
1614   
1615    @cvar tmplServerYadis: template for returning Yadis document to Relying
1616    Party.  Derived classes can reset this or completely override the
1617    serverYadis method.
1618   
1619    @type tmplServerYadis: basestring
1620   
1621    @cvar tmplYadis: template for returning Yadis document containing user
1622    URL to Relying Party.  Derived classes can reset this or completely
1623    override the yadis method.
1624   
1625    @type tmplYadis: basestring"""
1626   
1627    # Enable slot support for derived classes if they require it
1628    __slots__ = ('_authN', 'base_url', 'urls', 'charset')
1629   
1630    tmplServerYadis = """\
1631<?xml version="1.0" encoding="UTF-8"?>
1632<xrds:XRDS
1633    xmlns:xrds="xri://$xrds"
1634    xmlns="xri://$xrd*($v*2.0)">
1635  <XRD>
1636
1637    <Service priority="0">
1638      <Type>%(openid20type)s</Type>
1639      <URI>%(endpoint_url)s</URI>
1640    </Service>
1641
1642  </XRD>
1643</xrds:XRDS>
1644"""
1645
1646    tmplYadis = """\
1647<?xml version="1.0" encoding="UTF-8"?>
1648<xrds:XRDS
1649    xmlns:xrds="xri://$xrds"
1650    xmlns="xri://$xrd*($v*2.0)">
1651  <XRD>
1652
1653    <Service priority="0">
1654      <Type>%(openid20type)s</Type>
1655      <Type>%(openid10type)s</Type>
1656      <URI>%(endpoint_url)s</URI>
1657      <LocalID>%(user_url)s</LocalID>
1658    </Service>
1659
1660  </XRD>
1661</xrds:XRDS>"""   
1662   
1663    def __init__(self, authN, base_url, urls, **opt):
1664        """
1665        @type authN: AuthNInterface
1666        @param param: reference to authentication interface to enable OpenID
1667        user URL construction from username
1668        @type base_url: basestring
1669        @param base_url: base URL for OpenID Provider to which individual paths
1670        are appended
1671        @type urls: dict
1672        @param urls: full urls for all the paths used by all the exposed
1673        methods - keyed by method name - see OpenIDProviderMiddleware.paths
1674        @type opt: dict
1675        @param opt: additional custom options passed from the
1676        OpenIDProviderMiddleware config
1677        """
1678        self._authN = authN
1679        self.base_url = base_url
1680        self.urls = urls
1681        self.charset = ''
1682   
1683    def serverYadis(self, environ, start_response):
1684        '''Render Yadis info for ID Select mode request
1685       
1686        @type environ: dict
1687        @param environ: dictionary of environment variables
1688        @type start_response: callable
1689        @param start_response: WSGI start response function.  Should be called
1690        from this method to set the response code and HTTP header content
1691        @rtype: basestring
1692        @return: WSGI response
1693        '''
1694        endpoint_url = self.urls['url_openidserver']
1695        response = RenderingInterface.tmplServerYadis % \
1696                                {'openid20type': discover.OPENID_IDP_2_0_TYPE,
1697                                 'endpoint_url': endpoint_url}
1698             
1699        start_response("200 OK",
1700                       [('Content-type', 'application/xrds+xml'),
1701                        ('Content-length', str(len(response)))])
1702        return response
1703
1704    def yadis(self, environ, start_response):
1705        """Render Yadis document containing user URL
1706       
1707        @type environ: dict
1708        @param environ: dictionary of environment variables
1709        @type start_response: callable
1710        @param start_response: WSGI start response function.  Should be called
1711        from this method to set the response code and HTTP header content
1712        @rtype: basestring
1713        @return: WSGI response
1714        """
1715        # Override this method to implement an alternate means to derive the
1716        # username identifier
1717        userIdentifier = OpenIDProviderMiddleware.parseIdentityURI(
1718                                                    environ['PATH_INFO'])[-1]
1719       
1720        endpoint_url = self.urls['url_openidserver']
1721        user_url = self.urls['url_id'] + '/' + userIdentifier
1722       
1723        yadisDict = dict(openid20type=discover.OPENID_2_0_TYPE,
1724                         openid10type=discover.OPENID_1_0_TYPE,
1725                         endpoint_url=endpoint_url,
1726                         user_url=user_url)
1727       
1728        response = RenderingInterface.tmplYadis % yadisDict
1729     
1730        start_response('200 OK',
1731                       [('Content-type', 'application/xrds+xml' + self.charset),
1732                        ('Content-length', str(len(response)))])
1733        return response
1734   
1735    def identityPage(self, environ, start_response):
1736        """Render the identity page.
1737       
1738        @type environ: dict
1739        @param environ: dictionary of environment variables
1740        @type start_response: callable
1741        @param start_response: WSGI start response function.  Should be called
1742        from this method to set the response code and HTTP header content
1743        @rtype: basestring
1744        @return: WSGI response
1745        """
1746        response = "Page is not implemented"
1747        start_response('%d %s' % (404, httplib.responses[404]),
1748                       [('Content-type', 'text/html' + self.charset),
1749                        ('Content-length', str(len(response)))])
1750        return response
1751         
1752    def login(self, environ, start_response,
1753              success_to=None, fail_to=None, msg=''):
1754        """Render the login form.
1755       
1756        @type environ: dict
1757        @param environ: dictionary of environment variables
1758        @type start_response: callable
1759        @param start_response: WSGI start response function.  Should be called
1760        from this method to set the response code and HTTP header content
1761        @type success_to: basestring
1762        @param success_to: URL put into hidden field telling 
1763        OpenIDProviderMiddleware.do_loginsubmit() where to forward to on
1764        successful login
1765        @type fail_to: basestring
1766        @param fail_to: URL put into hidden field telling 
1767        OpenIDProviderMiddleware.do_loginsubmit() where to forward to on
1768        login error
1769        @type msg: basestring
1770        @param msg: display (error) message below login form e.g. following
1771        previous failed login attempt.
1772        @rtype: basestring
1773        @return: WSGI response
1774        """
1775       
1776        response = "Page is not implemented"
1777        start_response('%d %s' % (404, httplib.responses[404]),
1778                       [('Content-type', 'text/html' + self.charset),
1779                        ('Content-length', str(len(response)))])
1780        return response
1781
1782    def mainPage(self, environ, start_response):
1783        """Rendering the main page.
1784       
1785        @type environ: dict
1786        @param environ: dictionary of environment variables
1787        @type start_response: callable
1788        @param start_response: WSGI start response function.  Should be called
1789        from this method to set the response code and HTTP header content
1790        @rtype: basestring
1791        @return: WSGI response
1792        """   
1793        response = "Page is not implemented"
1794        start_response('%d %s' % (404, httplib.responses[404]),
1795                       [('Content-type', 'text/html' + self.charset),
1796                        ('Content-length', str(len(response)))])
1797        return response
1798
1799    def decidePage(self, environ, start_response, oidRequest, oidResponse):
1800        """Show page giving the user the option to approve the return of their
1801        credentials to the Relying Party.  This page is also displayed for
1802        ID select mode if the user is already logged in at the OpenID Provider.
1803        This enables them to confirm the OpenID to be sent back to the
1804        Relying Party
1805
1806        These fields should be posted by this page ready for
1807        OpenIdProviderMiddleware.do_allow to process:
1808       
1809        'Yes'/'No': for return authentication details back to the RP or
1810        abort return to RP respectively
1811        'remember': remember the decision corresponding to the above 'Yes'
1812        /'No'.
1813        This may be set to 'Yes' or 'No'
1814        'identity': set to the user's identity URL.  This usually is not
1815        required since it can be obtained from oidRequest.identity attribute
1816        but in ID Select mode, the identity URL will have been selected or set
1817        here.
1818       
1819       
1820        @type environ: dict
1821        @param environ: dictionary of environment variables
1822        @type start_response: callable
1823        @param start_response: WSGI start response function.  Should be called
1824        from this method to set the response code and HTTP header content
1825        @type oidRequest: openid.server.server.CheckIDRequest
1826        @param oidRequest: OpenID Check ID Request object
1827        @type oidResponse: openid.server.server.OpenIDResponse
1828        @param oidResponse: OpenID response object
1829        @rtype: basestring
1830        @return: WSGI response
1831        """
1832        response = "Page is not implemented"
1833        start_response('%d %s' % (404, httplib.responses[404]),
1834                       [('Content-type', 'text/html' + self.charset),
1835                        ('Content-length', str(len(response)))])
1836        return response
1837
1838    def errorPage(self, environ, start_response, msg, code=500):
1839        """Display error page
1840       
1841        @type environ: dict
1842        @param environ: dictionary of environment variables
1843        @type start_response: callable
1844        @param start_response: WSGI start response function.  Should be called
1845        from this method to set the response code and HTTP header content
1846        @type msg: basestring
1847        @param msg: optional message for page body
1848        @type code: int
1849        @param code: HTTP Error code to return
1850        @rtype: basestring
1851        @return: WSGI response
1852        """     
1853        response = "Page is not implemented"
1854        start_response('%d %s' % (404, httplib.responses[code]),
1855                       [('Content-type', 'text/html' + self.charset),
1856                        ('Content-length', str(len(response)))])
1857        return response
1858       
Note: See TracBrowser for help on using the repository browser.