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

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@5084
Revision 5084, 78.1 KB checked in by pjkersha, 12 years ago (diff)

ndg.security.test.integration.openid - combined OpenID Provider and Relying Party in same WSGI stack: improvements to interface

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