source: TI12-security/trunk/python/ndg_security_server/ndg/security/server/wsgi/openid/provider/__init__.py @ 5870

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