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

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@5080
Revision 5080, 77.7 KB checked in by pjkersha, 13 years ago (diff)

ndg.security.test.integration.openid: working combined OpenID Provider and Relying Party in same WSGI stack

  • Removed call to paste.request.parse_formvars in ndg.security.server.wsgi.openid.relyingparty.OpenIDRelyingPartyMiddleware.__call__: for Paste 1.7.1 this seems to gobbled up environwsgi.input? such that a 2nd call to it yields nothing!
  • TODO: add login to OpenID Relying Party template so that for a given site, home users can skip OpenID URI entry and login directly.
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 RedneringInterface.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",
688                                          code=400)
689       
690        if 'Yes' in self.query:
691            if oidRequest.idSelect():
692                identity = self.query.get('identity')
693                if identity is None:
694                    log.error("No identity field set from decide page for "
695                              "processing in ID Select mode")
696                    return self._render.errorPage(environ, start_response,
697                                                  "An internal error has "
698                                                  "occurred setting the "
699                                                  "OpenID user identity")
700            else:
701                identity = oidRequest.identity
702
703            trust_root = oidRequest.trust_root
704            if self.query.get('remember', 'No') == 'Yes':
705                self.session['approved'] = {trust_root: 'always'}
706                self.session.save()
707             
708            try:
709                oidResponse = self._identityApprovedPostProcessing(oidRequest,
710                                                                   identity)
711
712            except (OpenIDProviderMissingRequiredAXAttrs,
713                    OpenIDProviderMissingAXResponseHandler):
714                response = self._render.errorPage(environ, start_response,
715                    'The site where you wish to signin requires '
716                    'additional information which this site isn\'t '
717                    'configured to provide.  Please report this fault to '
718                    'your site administrator.')
719                return response
720                   
721            except Exception, e:
722                log.error("Setting response following ID Approval: %s" % e)
723                return self._render.errorPage(environ, start_response,
724                        'An error occurred setting additional parameters '
725                        'required by the site requesting your ID.  Please '
726                        'report this fault to your site administrator.')
727            else:
728                return self._displayResponse(oidResponse)
729       
730        elif 'No' in self.query:
731            # TODO: Check 'No' response is OK - No causes AuthKit's Relying
732            # Party implementation to crash with 'openid.return_to' KeyError
733            # in Authkit.authenticate.open_id.process
734            oidResponse = oidRequest.answer(False)
735            #return self._displayResponse(oidResponse)
736            return self._render.mainPage(environ, start_response)           
737        else:
738            return self._render.errorPage(environ, start_response,
739                                          'Expecting Yes/No in allow '
740                                          'post. %r' % self.query,
741                                          code=400)
742
743
744    def do_login(self, environ, start_response, **kw):
745        """Display Login form
746       
747        @type environ: dict
748        @param environ: dictionary of environment variables
749        @type start_response: callable
750        @param start_response: standard WSGI callable to set HTTP headers
751        @type kw: dict
752        @param kw: keywords to login renderer - see RenderingInterface class
753        @rtype: basestring
754        @return: WSGI response
755        """
756       
757        if 'fail_to' not in kw:
758            kw['fail_to'] = self.urls['url_login']
759           
760        response = self._render.login(environ, start_response, **kw)
761        return response
762
763
764    def do_loginsubmit(self, environ, start_response):
765        """Handle user submission from login and logout
766       
767        @type environ: dict
768        @param environ: dictionary of environment variables
769        @type start_response: callable
770        @param start_response: standard WSGI callable to set HTTP headers
771        @rtype: basestring
772        @return: WSGI response
773        """
774       
775        if 'submit' in self.query:
776            if 'username' in self.query:
777                # login
778                if 'username' in self.session:
779                    log.error("Attempting login for user %s: user %s is "
780                              "already logged in", self.session['username'],
781                              self.session['username'])
782                    return self._redirect(start_response, self.query['fail_to'])
783               
784                oidRequest = self.session.get('lastCheckIDRequest')
785                if oidRequest is None:
786                    log.error("Getting OpenID request for login - No request "
787                              "found in session")
788                    return self._render.errorPage(environ, start_response,
789                        "An internal error occurred possibly due to a request "
790                        "that's expired.  Please retry from the site where "
791                        "you entered your OpenID.  If the problem persists "
792                        "report it to your site administrator.")
793                   
794                # Get user identifier to check against credentials provided
795                if oidRequest.idSelect():
796                    # ID select mode enables the user to request specifying
797                    # their OpenID Provider without giving a personal user URL
798                    userIdentifier = None
799                else:
800                    # Get the unique user identifier from the user's OpenID URL
801                    userIdentifier = OpenIDProviderMiddleware.parseIdentityURI(
802                                                    oidRequest.identity)[-1]
803                   
804                # Invoke custom authentication interface plugin
805                try:
806                    self._authN.logon(environ,
807                                      userIdentifier,
808                                      self.query['username'],
809                                      self.query.get('password', ''))
810                   
811                except AuthNInterfaceError, e:
812                    return self._render.login(environ, start_response,
813                                          msg=e.userMsg,
814                                          success_to=self.urls['url_decide'])                   
815                except Exception, e:
816                    log.error("Unexpected exception raised during "
817                              "authentication: %s" % e)
818                    msg = ("An internal error occurred.  "
819                           "Please try again or if the problems persists "
820                           "contact your system administrator.")
821
822                    response = self._render.login(environ, start_response,
823                                      msg=msg,
824                                      success_to=self.urls['url_decide'])
825                    return response
826                       
827                self.session['username'] = self.query['username']
828                self.session['approved'] = {}
829                self.session.save()
830            else:
831                # logout
832                if 'username' not in self.session:
833                    log.error("No user is logged in")
834                    return self._redirect(start_response,self.query['fail_to'])
835               
836                del self.session['username']
837                self.session.pop('approved', None)
838                self.session.save()
839               
840            return self._redirect(start_response, self.query['success_to'])
841       
842        elif 'cancel' in self.query:
843            return self._redirect(start_response, self.query['fail_to'])
844        else:
845            log.error('Login input not recognised %r' % self.query)
846            return self._redirect(start_response, self.query['fail_to'])
847           
848
849    def do_mainpage(self, environ, start_response):
850        '''Show an information page about the OpenID Provider
851       
852        @type environ: dict
853        @param environ: dictionary of environment variables
854        @type start_response: callable
855        @param start_response: standard WSGI callable to set HTTP headers
856        @rtype: basestring
857        @return: WSGI response
858        '''   
859        response = self._render.mainPage(environ, start_response)
860        return response
861
862    def _getRender(self):
863        """Get method for rendering interface object
864        @rtype: RenderingInterface
865        @return: rendering interface object
866        """
867        return self._render
868   
869    render = property(fget=_getRender, doc="Rendering interface instance")
870   
871   
872    def do_decide(self, environ, start_response):
873        """Display page prompting the user to decide whether to trust the site
874        requesting their credentials
875       
876        @type environ: dict
877        @param environ: dictionary of environment variables
878        @type start_response: callable
879        @param start_response: standard WSGI callable to set HTTP headers
880        @rtype: basestring
881        @return: WSGI response
882        """
883
884        oidRequest = self.session.get('lastCheckIDRequest')
885        if oidRequest is None:
886            log.error("No OpenID request set in session")
887            return self._render.errorPage(environ, start_response,
888                                          "Invalid request.  Please report "
889                                          "the error to your site "
890                                          "administrator.",
891                                          code=400)
892       
893        approvedRoots = self.session.get('approved', {})
894       
895        if oidRequest.trust_root in approvedRoots and \
896           not oidRequest.idSelect():
897            try:
898                response = self._identityApprovedPostProcessing(oidRequest,
899                                                        oidRequest.identity)
900            except (OpenIDProviderMissingRequiredAXAttrs,
901                    OpenIDProviderMissingAXResponseHandler):
902                response = self._render.errorPage(environ, start_response,
903                    'The site where you wish to signin requires '
904                    'additional information which this site isn\'t '
905                    'configured to provide.  Please report this fault to '
906                    'your site administrator.')
907                return response
908                   
909            except Exception, e:
910                log.error("Setting response following ID Approval: %s" % e)
911                response = self._render.errorPage(environ, start_response,
912                        'An error occurred setting additional parameters '
913                        'required by the site requesting your ID.  Please '
914                        'report this fault to your site administrator.')
915                return response
916
917            return self.oidResponse(response)
918        else:
919            return self._render.decidePage(environ, start_response, oidRequest)
920       
921       
922    def _identityIsAuthorized(self, oidRequest):
923        '''Check that a user is authorized i.e. does a session exist for their
924        username and if so does it correspond to the identity URL provided.
925        This last check doesn't apply for ID Select mode where No ID was input
926        at the Relying Party.
927       
928        @type oidRequest: openid.server.server.CheckIDRequest
929        @param oidRequest: OpenID Request object
930        @rtype: bool
931        @return: True/False is user authorized
932        '''
933        username = self.session.get('username')
934        if username is None:
935            return False
936
937        if oidRequest.idSelect():
938            log.debug("OpenIDProviderMiddleware._identityIsAuthorized - "
939                      "ID Select mode set but user is already logged in")
940            return True
941       
942        identifiers = self._authN.username2UserIdentifiers(self.environ,
943                                                           username)
944
945        # Take two passes to allow for yadis and non-based discovery
946        identityURIs = [self.createIdentityURI(self.urls['url_id'], i)
947                        for i in identifiers]
948       
949        identityURIs += [self.createIdentityURI(self.urls['url_yadis'], i)
950                         for i in identifiers]
951       
952        if oidRequest.identity not in identityURIs:
953            log.debug("OpenIDProviderMiddleware._identityIsAuthorized - "
954                      "user is already logged in with a different ID=%s" % \
955                      username)
956            return False
957       
958        log.debug("OpenIDProviderMiddleware._identityIsAuthorized - "
959                  "user is logged in with ID matching ID URI")
960        return True
961   
962   
963    def _trustRootIsAuthorized(self, trust_root):
964        '''Return True/False for the given trust root (Relying Party)
965        previously been approved by the user
966       
967        @type trust_root: dict
968        @param trust_root: keyed by trusted root (Relying Party) URL and
969        containing string item 'always' if approved
970        @rtype: bool
971        @return: True - trust has already been approved, False - trust root is
972        not approved'''
973        approvedRoots = self.session.get('approved', {})
974        return approvedRoots.get(trust_root) is not None
975
976
977    def _addSRegResponse(self, oidRequest, oidResponse):
978        '''Add Simple Registration attributes to response to Relying Party
979       
980        @type oidRequest: openid.server.server.CheckIDRequest
981        @param oidRequest: OpenID Check ID Request object
982        @type oidResponse: openid.server.server.OpenIDResponse
983        @param oidResponse: OpenID response object'''
984       
985        if self.sregResponseHandler is None:
986            # No Simple Registration response object was set
987            return
988       
989        sreg_req = sreg.SRegRequest.fromOpenIDRequest(oidRequest)
990
991        # Callout to external callable sets additional user attributes to be
992        # returned in response to Relying Party       
993        sreg_data = self.sregResponseHandler(self.session.get('username'))
994        sreg_resp = sreg.SRegResponse.extractResponse(sreg_req, sreg_data)
995        oidResponse.addExtension(sreg_resp)
996
997
998    def _addAXResponse(self, oidRequest, oidResponse):
999        '''Add attributes to response based on the OpenID Attribute Exchange
1000        interface
1001       
1002        @type oidRequest: openid.server.server.CheckIDRequest
1003        @param oidRequest: OpenID Check ID Request object
1004        @type oidResponse: openid.server.server.OpenIDResponse
1005        @param oidResponse: OpenID response object'''
1006
1007
1008        ax_req = ax.FetchRequest.fromOpenIDRequest(oidRequest)
1009        if ax_req is None:
1010            log.debug("No Attribute Exchange extension set in request")
1011            return
1012       
1013        ax_resp = ax.FetchResponse(request=ax_req)
1014       
1015        if self.axResponseHandler is None:
1016            requiredAttr = ax_req.getRequiredAttrs()
1017            if len(requiredAttr) > 0:
1018                msg = ("Relying party requires these attributes: %s; but No"
1019                       "Attribute exchange handler 'axResponseHandler' has "
1020                       "been set" % requiredAttr)
1021                log.error(msg)
1022                raise OpenIDProviderMissingAXResponseHandler(msg)
1023           
1024            return
1025       
1026        # Set requested values - need user intervention here to confirm
1027        # release of attributes + assignment based on required attributes -
1028        # possibly via FetchRequest.getRequiredAttrs()
1029        try:
1030            self.axResponseHandler(ax_req, ax_resp, self.session.get('username'))
1031           
1032        except OpenIDProviderMissingRequiredAXAttrs, e:
1033            log.error("OpenID Provider is unable to set the AX attributes "
1034                      "required by the Relying Party's request: %s" % e)
1035            raise
1036       
1037        except Exception, e:
1038            log.error("%s exception raised setting requested Attribute "
1039                      "Exchange values: %s" % (e.__class__, e))
1040            raise
1041       
1042        oidResponse.addExtension(ax_resp)
1043       
1044       
1045    def _identityApprovedPostProcessing(self, oidRequest, identifier=None):
1046        '''Action following approval of a Relying Party by the user.  Add
1047        Simple Registration and/or Attribute Exchange parameters if handlers
1048        were specified - See _addSRegResponse and _addAXResponse methods - and
1049        only if the Relying Party has requested them
1050       
1051        @type oidRequest: openid.server.server.CheckIDRequest
1052        @param oidRequest: OpenID Check ID Request object
1053        @type identifier: basestring
1054        @param identifier: OpenID selected by user - for ID Select mode only
1055        @rtype: openid.server.server.OpenIDResponse
1056        @return: OpenID response object'''
1057
1058        oidResponse = oidRequest.answer(True, identity=identifier)
1059        self._addSRegResponse(oidRequest, oidResponse)
1060        self._addAXResponse(oidRequest, oidResponse)
1061       
1062        return oidResponse
1063
1064
1065    def _handleCheckIDRequest(self, oidRequest):
1066        """Handle "checkid_immediate" and "checkid_setup" type requests from
1067        Relying Party
1068       
1069        @type oidRequest: openid.server.server.CheckIDRequest
1070        @param oidRequest: OpenID Check ID request
1071        @rtype: basestring
1072        @return: WSGI response
1073        """
1074        log.debug("OpenIDProviderMiddleware._handleCheckIDRequest ...")
1075       
1076        # Save request
1077        self.session['lastCheckIDRequest'] = oidRequest
1078        self.session.save()
1079       
1080        if self._identityIsAuthorized(oidRequest):
1081           
1082            # User is logged in - check for ID Select type request i.e. the
1083            # user entered their IdP address at the Relying Party and not their
1084            # OpenID Identifier.  In this case, the identity they wish to use
1085            # must be confirmed.
1086            if oidRequest.idSelect():
1087                # OpenID identifier must be confirmed
1088                return self.do_decide(self.environ, self.start_response)
1089           
1090            elif self._trustRootIsAuthorized(oidRequest.trust_root):
1091                # User has approved this Relying Party
1092                try:
1093                    oidResponse = self._identityApprovedPostProcessing(
1094                                                                    oidRequest)
1095                except (OpenIDProviderMissingRequiredAXAttrs,
1096                        OpenIDProviderMissingAXResponseHandler):
1097                    response = self._render.errorPage(environ, start_response,
1098                        'The site where you wish to signin requires '
1099                        'additional information which this site isn\'t '
1100                        'configured to provide.  Please report this fault to '
1101                        'your site administrator.')
1102                    return response
1103                   
1104                except Exception, e:
1105                    log.error("Setting response following ID Approval: %s" % e)
1106                    response = self._render.errorPage(environ, start_response,
1107                        'An error occurred setting additional parameters '
1108                        'required by the site requesting your ID.  Please '
1109                        'report this fault to your site administrator.')
1110                    return response
1111               
1112                return self._displayResponse(oidResponse)
1113            else:
1114                return self.do_decide(self.environ, self.start_response)
1115               
1116        elif oidRequest.immediate:
1117            oidResponse = oidRequest.answer(False)
1118            return self._displayResponse(oidResponse)
1119       
1120        else:
1121            # User is not logged in
1122           
1123            # Call login and if successful then call decide page to confirm
1124            # user wishes to trust the Relying Party.
1125            response = self.do_login(self.environ,
1126                                     self.start_response,
1127                                     success_to=self.urls['url_decide'])
1128            return response
1129
1130
1131    def _displayResponse(self, oidResponse):
1132        """Serialize an OpenID Response object, set headers and return WSGI
1133        response.
1134       
1135        If the URL length for a GET request exceeds a maximum, then convert the
1136        response into a HTML form and use POST method.
1137       
1138        @type oidResponse: openid.server.server.OpenIDResponse
1139        @param oidResponse: OpenID response object
1140       
1141        @rtype: basestring
1142        @return: WSGI response'''
1143        """
1144       
1145        try:
1146            webresponse = self.oidserver.encodeResponse(oidResponse)
1147        except server.EncodingError, why:
1148            text = why.response.encodeToKVForm()
1149            return self.showErrorPage(text)
1150       
1151        hdr = webresponse.headers.items()
1152       
1153        # If the content length exceeds the maximum to represent on a URL, it's
1154        # rendered as a form instead
1155        # FIXME: Commented out oidResponse.renderAsForm() test as it doesn't
1156        # give consistent answers.  Testing based on body content should work
1157        # OK
1158        if webresponse.body:
1159        #if oidResponse.renderAsForm():
1160            # Wrap in HTML with Javascript OnLoad to submit the form
1161            # automatically without user intervention
1162            response = OpenIDProviderMiddleware.formRespWrapperTmpl % \
1163                                                        webresponse.body
1164        else:
1165            response = webresponse.body
1166           
1167        hdr += [('Content-type', 'text/html' + self.charset),
1168                ('Content-length', str(len(response)))]
1169           
1170        self.start_response('%d %s' % (webresponse.code,
1171                                       httplib.responses[webresponse.code]),
1172                            hdr)
1173        return response
1174
1175
1176    def _redirect(self, start_response, url):
1177        """Do a HTTP 302 redirect
1178       
1179        @type start_response: builtin_function_or_method
1180        @param start_response: WSGI start response callable
1181        @type url: basestring
1182        @param url: URL to redirect to
1183        @rtype: list
1184        @return: empty HTML body
1185        """
1186        start_response('302 %s' % httplib.responses[302],
1187                       [('Content-type', 'text/html' + self.charset),
1188                        ('Location', url)])
1189        return []
1190   
1191   
1192class RenderingInterfaceError(Exception):
1193    """Base class for RenderingInterface exceptions
1194   
1195    A standard message is raised set by the msg class variable but the actual
1196    exception details are logged to the error log.  The use of a standard
1197    message enables callers to use its content for user error messages.
1198   
1199    @type msg: basestring
1200    @cvar msg: standard message to be raised for this exception"""
1201    userMsg = ("An internal error occurred with the page layout,  Please "
1202               "contact your system administrator")
1203    errorMsg = "RenderingInterface error"
1204   
1205    def __init__(self, *arg, **kw):
1206        if len(arg) > 0:
1207            msg = arg[0]
1208        else:
1209            msg = self.__class__.errorMsg
1210           
1211        log.error(msg)
1212        Exception.__init__(self, msg, **kw)
1213       
1214class RenderingInterfaceInitError(RenderingInterfaceError):
1215    """Error with initialisation of RenderingInterface.  Raise from __init__"""
1216    errorMsg = "RenderingInterface initialisation error"
1217   
1218class RenderingInterfaceConfigError(RenderingInterfaceError):
1219    """Error with configuration settings.  Raise from __init__"""
1220    errorMsg = "RenderingInterface configuration error"   
1221   
1222class RenderingInterface(object):
1223    """Interface class for rendering of OpenID Provider pages.  It implements
1224    methods for handling Yadis requests only.  All other interface methods
1225    return a 404 error response.  Create a derivative from this class to
1226    implement the other rendering methods as required.  DemoRenderingInterface
1227    provides an example of how to do this.  To apply a custom
1228    RenderingInterface class pass it's name in the OpenIDProviderMiddleware
1229    app_conf dict or as a keyword argument using the option name
1230    renderingClass.
1231   
1232    @cvar tmplServerYadis: template for returning Yadis document to Relying
1233    Party.  Derived classes can reset this or completely override the
1234    serverYadis method.
1235   
1236    @type tmplServerYadis: basestring
1237   
1238    @cvar tmplYadis: template for returning Yadis document containing user
1239    URL to Relying Party.  Derived classes can reset this or completely
1240    override the yadis method.
1241   
1242    @type tmplYadis: basestring"""
1243   
1244    tmplServerYadis = """\
1245<?xml version="1.0" encoding="UTF-8"?>
1246<xrds:XRDS
1247    xmlns:xrds="xri://$xrds"
1248    xmlns="xri://$xrd*($v*2.0)">
1249  <XRD>
1250
1251    <Service priority="0">
1252      <Type>%(openid20type)s</Type>
1253      <URI>%(endpoint_url)s</URI>
1254    </Service>
1255
1256  </XRD>
1257</xrds:XRDS>
1258"""
1259
1260    tmplYadis = """\
1261<?xml version="1.0" encoding="UTF-8"?>
1262<xrds:XRDS
1263    xmlns:xrds="xri://$xrds"
1264    xmlns="xri://$xrd*($v*2.0)">
1265  <XRD>
1266
1267    <Service priority="0">
1268      <Type>%(openid20type)s</Type>
1269      <Type>%(openid10type)s</Type>
1270      <URI>%(endpoint_url)s</URI>
1271      <LocalID>%(user_url)s</LocalID>
1272    </Service>
1273
1274  </XRD>
1275</xrds:XRDS>"""   
1276   
1277    def __init__(self, authN, base_url, urls, **opt):
1278        """
1279        @type authN: AuthNInterface
1280        @param param: reference to authentication interface to enable OpenID
1281        user URL construction from username
1282        @type base_url: basestring
1283        @param base_url: base URL for OpenID Provider to which individual paths
1284        are appended
1285        @type urls: dict
1286        @param urls: full urls for all the paths used by all the exposed
1287        methods - keyed by method name - see OpenIDProviderMiddleware.paths
1288        @type opt: dict
1289        @param opt: additional custom options passed from the
1290        OpenIDProviderMiddleware config
1291        """
1292        self._authN = authN
1293        self.base_url = base_url
1294        self.urls = urls
1295        self.charset = ''
1296   
1297   
1298    def serverYadis(self, environ, start_response):
1299        '''Render Yadis info for ID Select mode request
1300       
1301        @type environ: dict
1302        @param environ: dictionary of environment variables
1303        @type start_response: callable
1304        @param start_response: WSGI start response function.  Should be called
1305        from this method to set the response code and HTTP header content
1306        @rtype: basestring
1307        @return: WSGI response
1308        '''
1309        endpoint_url = self.urls['url_openidserver']
1310        response = RenderingInterface.tmplServerYadis % \
1311                                {'openid20type': discover.OPENID_IDP_2_0_TYPE,
1312                                 'endpoint_url': endpoint_url}
1313             
1314        start_response("200 OK",
1315                       [('Content-type', 'application/xrds+xml'),
1316                        ('Content-length', str(len(response)))])
1317        return response
1318
1319
1320    def yadis(self, environ, start_response):
1321        """Render Yadis document containing user URL
1322       
1323        @type environ: dict
1324        @param environ: dictionary of environment variables
1325        @type start_response: callable
1326        @param start_response: WSGI start response function.  Should be called
1327        from this method to set the response code and HTTP header content
1328        @rtype: basestring
1329        @return: WSGI response
1330        """
1331        # Override this method to implement an alternate means to derive the
1332        # username identifier
1333        userIdentifier = OpenIDProviderMiddleware.parseIdentityURI(
1334                                                    environ['PATH_INFO'])[ - 1]
1335       
1336        endpoint_url = self.urls['url_openidserver']
1337        user_url = self.urls['url_id'] + '/' + userIdentifier
1338       
1339        yadisDict = dict(openid20type=discover.OPENID_2_0_TYPE,
1340                         openid10type=discover.OPENID_1_0_TYPE,
1341                         endpoint_url=endpoint_url,
1342                         user_url=user_url)
1343       
1344        response = RenderingInterface.tmplYadis % yadisDict
1345     
1346        start_response('200 OK',
1347                       [('Content-type', 'application/xrds+xml' + self.charset),
1348                        ('Content-length', str(len(response)))])
1349        return response
1350   
1351
1352    def identityPage(self, environ, start_response):
1353        """Render the identity page.
1354       
1355        @type environ: dict
1356        @param environ: dictionary of environment variables
1357        @type start_response: callable
1358        @param start_response: WSGI start response function.  Should be called
1359        from this method to set the response code and HTTP header content
1360        @rtype: basestring
1361        @return: WSGI response
1362        """
1363        response = "Page is not implemented"
1364        start_response('%d %s' % (404, httplib.responses[code]),
1365                       [('Content-type', 'text/html' + self.charset),
1366                        ('Content-length', str(len(response)))])
1367        return response
1368   
1369       
1370    def login(self, environ, start_response,
1371              success_to=None, fail_to=None, msg=''):
1372        """Render the login form.
1373       
1374        @type environ: dict
1375        @param environ: dictionary of environment variables
1376        @type start_response: callable
1377        @param start_response: WSGI start response function.  Should be called
1378        from this method to set the response code and HTTP header content
1379        @type success_to: basestring
1380        @param success_to: URL put into hidden field telling 
1381        OpenIDProviderMiddleware.do_loginsubmit() where to forward to on
1382        successful login
1383        @type fail_to: basestring
1384        @param fail_to: URL put into hidden field telling 
1385        OpenIDProviderMiddleware.do_loginsubmit() where to forward to on
1386        login error
1387        @type msg: basestring
1388        @param msg: display (error) message below login form e.g. following
1389        previous failed login attempt.
1390        @rtype: basestring
1391        @return: WSGI response
1392        """
1393       
1394        response = "Page is not implemented"
1395        start_response('%d %s' % (404, httplib.responses[code]),
1396                       [('Content-type', 'text/html' + self.charset),
1397                        ('Content-length', str(len(response)))])
1398        return response
1399
1400
1401    def mainPage(self, environ, start_response):
1402        """Rendering the main page.
1403       
1404        @type environ: dict
1405        @param environ: dictionary of environment variables
1406        @type start_response: callable
1407        @param start_response: WSGI start response function.  Should be called
1408        from this method to set the response code and HTTP header content
1409        @rtype: basestring
1410        @return: WSGI response
1411        """   
1412        response = "Page is not implemented"
1413        start_response('%d %s' % (404, httplib.responses[code]),
1414                       [('Content-type', 'text/html' + self.charset),
1415                        ('Content-length', str(len(response)))])
1416        return response
1417   
1418
1419    def decidePage(self, environ, start_response, oidRequest):
1420        """Show page giving the user the option to approve the return of their
1421        credentials to the Relying Party.  This page is also displayed for
1422        ID select mode if the user is already logged in at the OpenID Provider.
1423        This enables them to confirm the OpenID to be sent back to the
1424        Relying Party
1425
1426        These fields should be posted by this page ready for
1427        OpenIdProviderMiddleware.do_allow to process:
1428       
1429        'Yes'/'No': for return authentication details back to the RP or
1430        abort return to RP respectively
1431        'remember': remember the decision corresponding to the above 'Yes'
1432        /'No'.
1433        This may be set to 'Yes' or 'No'
1434        'identity': set to the user's identity URL.  This usually is not
1435        required since it can be obtained from oidRequest.identity attribute
1436        but in ID Select mode, the identity URL will have been selected or set
1437        here.
1438       
1439       
1440        @type environ: dict
1441        @param environ: dictionary of environment variables
1442        @type start_response: callable
1443        @param start_response: WSGI start response function.  Should be called
1444        from this method to set the response code and HTTP header content
1445        @type oidRequest: openid.server.server.CheckIDRequest
1446        @param oidRequest: OpenID Check ID Request object
1447        @rtype: basestring
1448        @return: WSGI response
1449        """
1450        response = "Page is not implemented"
1451        start_response('%d %s' % (404, httplib.responses[code]),
1452                       [('Content-type', 'text/html' + self.charset),
1453                        ('Content-length', str(len(response)))])
1454        return response
1455
1456
1457    def errorPage(self, environ, start_response, msg, code=500):
1458        """Display error 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        @type msg: basestring
1466        @param msg: optional message for page body
1467        @type code: int
1468        @param code: HTTP Error code to return
1469        @rtype: basestring
1470        @return: WSGI response
1471        """     
1472        response = "Page is not implemented"
1473        start_response('%d %s' % (404, httplib.responses[code]),
1474                       [('Content-type', 'text/html' + self.charset),
1475                        ('Content-length', str(len(response)))])
1476        return response
1477       
1478   
1479class DemoRenderingInterface(RenderingInterface):
1480    """Example rendering interface class for demonstration purposes"""
1481   
1482    def identityPage(self, environ, start_response):
1483        """Render the identity page.
1484       
1485        @type environ: dict
1486        @param environ: dictionary of environment variables
1487        @type start_response: callable
1488        @param start_response: WSGI start response function.  Should be called
1489        from this method to set the response code and HTTP header content
1490        @rtype: basestring
1491        @return: WSGI response
1492        """
1493        path = environ.get('PATH_INFO').rstrip('/')
1494        userIdentifier = path.split('/')[ - 1]
1495       
1496        link_tag = '<link rel="openid.server" href="%s">' % \
1497              self.urls['url_openidserver']
1498             
1499        yadis_loc_tag = '<meta http-equiv="x-xrds-location" content="%s">' % \
1500            (self.urls['url_yadis'] + '/' + userIdentifier)
1501           
1502        disco_tags = link_tag + yadis_loc_tag
1503        ident = self.base_url + path
1504
1505        response = self._showPage(environ,
1506                                  'Identity Page',
1507                                  head_extras=disco_tags,
1508                                  msg='<p>This is the identity page for %s.'
1509                                      '</p>' % ident)
1510       
1511        start_response("200 OK",
1512                       [('Content-type', 'text/html' + self.charset),
1513                        ('Content-length', str(len(response)))])
1514        return response
1515   
1516       
1517    def login(self, environ, start_response,
1518              success_to=None, fail_to=None, msg=''):
1519        """Render the login form.
1520       
1521        @type environ: dict
1522        @param environ: dictionary of environment variables
1523        @type success_to: basestring
1524        @param success_to: URL put into hidden field telling 
1525        OpenIDProviderMiddleware.do_loginsubmit() where to forward to on
1526        successful login
1527        @type fail_to: basestring
1528        @param fail_to: URL put into hidden field telling 
1529        OpenIDProviderMiddleware.do_loginsubmit() where to forward to on
1530        login error
1531        @type msg: basestring
1532        @param msg: display (error) message below login form e.g. following
1533        previous failed login attempt.
1534        @rtype: basestring
1535        @return: WSGI response
1536        """
1537       
1538        if success_to is None:
1539            success_to = self.urls['url_mainpage']
1540           
1541        if fail_to is None:
1542            fail_to = self.urls['url_mainpage']
1543       
1544        form = '''\
1545<h2>Login</h2>
1546<form method="GET" action="%s">
1547  <input type="hidden" name="success_to" value="%s" />
1548  <input type="hidden" name="fail_to" value="%s" />
1549  <table cellspacing="0" border="0" cellpadding="5">
1550    <tr>
1551        <td>Username:</td>
1552        <td><input type="text" name="username" value=""/></td>
1553    </tr><tr>
1554        <td>Password:</td>
1555        <td><input type="password" name="password"/></td>
1556    </tr><tr>
1557        <td colspan="2" align="right">
1558            <input type="submit" name="submit" value="Login"/>
1559            <input type="submit" name="cancel" value="Cancel"/>
1560        </td>
1561    </tr>
1562  </table>
1563</form>
1564%s
1565''' % (self.urls['url_loginsubmit'], success_to, fail_to, msg)
1566
1567        response = self._showPage(environ, 'Login Page', form=form)
1568        start_response('200 OK',
1569                       [('Content-type', 'text/html' + self.charset),
1570                        ('Content-length', str(len(response)))])
1571        return response
1572
1573
1574    def mainPage(self, environ, start_response):
1575        """Rendering the main page.
1576       
1577        @type environ: dict
1578        @param environ: dictionary of environment variables
1579        @type start_response: callable
1580        @param start_response: WSGI start response function.  Should be called
1581        from this method to set the response code and HTTP header content
1582        @rtype: basestring
1583        @return: WSGI response
1584        """
1585       
1586        yadis_tag = '<meta http-equiv="x-xrds-location" content="%s">' % \
1587                    self.urls['url_serveryadis']
1588        username = environ['beaker.session'].get('username')   
1589        if username:
1590            openid_url = self.urls['url_id'] + '/' + username
1591            user_message = """\
1592            <p>You are logged in as %s. Your OpenID identity URL is
1593            <tt><a href=%s>%s</a></tt>. Enter that URL at an OpenID
1594            consumer to test this server.</p>
1595            """ % (username, quoteattr(openid_url), openid_url)
1596        else:
1597            user_message = "<p>You are not <a href='%s'>logged in</a>.</p>" % \
1598                            self.urls['url_login']
1599
1600        msg = '''\
1601<p>OpenID server</p>
1602
1603%s
1604
1605<p>The URL for this server is <a href=%s><tt>%s</tt></a>.</p>
1606''' % (user_message, quoteattr(self.base_url), self.base_url)
1607        response = self._showPage(environ,
1608                                  'Main Page',
1609                                  head_extras=yadis_tag,
1610                                  msg=msg)
1611   
1612        start_response('200 OK',
1613                       [('Content-type', 'text/html' + self.charset),
1614                        ('Content-length', str(len(response)))])
1615        return response
1616   
1617
1618    def decidePage(self, environ, start_response, oidRequest):
1619        """Show page giving the user the option to approve the return of their
1620        credentials to the Relying Party.  This page is also displayed for
1621        ID select mode if the user is already logged in at the OpenID Provider.
1622        This enables them to confirm the OpenID to be sent back to the
1623        Relying Party
1624       
1625        @type environ: dict
1626        @param environ: dictionary of environment variables
1627        @type start_response: callable
1628        @param start_response: WSGI start response function.  Should be called
1629        from this method to set the response code and HTTP header content
1630        @type oidRequest: openid.server.server.CheckIDRequest
1631        @param oidRequest: OpenID Check ID Request object
1632        @rtype: basestring
1633        @return: WSGI response
1634        """
1635        idURLBase = self.urls['url_id'] + '/'
1636       
1637        # XXX: This may break if there are any synonyms for idURLBase,
1638        # such as referring to it by IP address or a CNAME.
1639       
1640        # TODO: OpenID 2.0 Allows oidRequest.identity to be set to
1641        # http://specs.openid.net/auth/2.0/identifier_select.  See,
1642        # http://openid.net/specs/openid-authentication-2_0.html.  This code
1643        # implements this overriding the behaviour of the example code on
1644        # which this is based.  - Check is the example code based on OpenID 1.0
1645        # and therefore wrong for this behaviour?
1646#        assert oidRequest.identity.startswith(idURLBase), \
1647#               repr((oidRequest.identity, idURLBase))
1648        userIdentifier = oidRequest.identity[len(idURLBase):]
1649        username = environ['beaker.session']['username']
1650       
1651        if oidRequest.idSelect(): # We are being asked to select an ID
1652            userIdentifier = self._authN.username2UserIdentifiers(environ,
1653                                                                  username)[0]
1654            identity = idURLBase + userIdentifier
1655           
1656            msg = '''\
1657            <p>A site has asked for your identity.  You may select an
1658            identifier by which you would like this site to know you.
1659            On a production site this would likely be a drop down list
1660            of pre-created accounts or have the facility to generate
1661            a random anonymous identifier.
1662            </p>
1663            '''
1664            fdata = {
1665                'pathAllow': self.urls['url_allow'],
1666                'identity': identity,
1667                'trust_root': oidRequest.trust_root,
1668                }
1669            form = '''\
1670<form method="POST" action="%(pathAllow)s">
1671<table>
1672  <tr><td>Identity:</td>
1673     <td>%(identity)s</td></tr>
1674  <tr><td>Trust Root:</td><td>%(trust_root)s</td></tr>
1675</table>
1676<p>Allow this authentication to proceed?</p>
1677<input type="checkbox" id="remember" name="remember" value="Yes"
1678    /><label for="remember">Remember this
1679    decision</label><br />
1680<input type="hidden" name="identity" value="%(identity)s" />
1681<input type="submit" name="Yes" value="Yes" />
1682<input type="submit" name="No" value="No" />
1683</form>
1684''' % fdata
1685           
1686        elif userIdentifier in self._authN.username2UserIdentifiers(environ,
1687                                                                    username):
1688            msg = '''\
1689            <p>A new site has asked to confirm your identity.  If you
1690            approve, the site represented by the trust root below will
1691            be told that you control identity URL listed below. (If
1692            you are using a delegated identity, the site will take
1693            care of reversing the delegation on its own.)</p>'''
1694
1695            fdata = {
1696                'pathAllow': self.urls['url_allow'],
1697                'identity': oidRequest.identity,
1698                'trust_root': oidRequest.trust_root,
1699                }
1700            form = '''\
1701<table>
1702  <tr><td>Identity:</td><td>%(identity)s</td></tr>
1703  <tr><td>Trust Root:</td><td>%(trust_root)s</td></tr>
1704</table>
1705<p>Allow this authentication to proceed?</p>
1706<form method="POST" action="%(pathAllow)s">
1707  <input type="checkbox" id="remember" name="remember" value="Yes"
1708      /><label for="remember">Remember this
1709      decision</label><br />
1710  <input type="submit" name="Yes" value="Yes" />
1711  <input type="submit" name="No" value="No" />
1712</form>''' % fdata
1713        else:
1714            mdata = {
1715                'userIdentifier': userIdentifier,
1716                'username': username,
1717                }
1718            msg = '''\
1719            <p>A site has asked for an identity belonging to
1720            %(userIdentifier)s, but you are logged in as %(username)s.  To
1721            log in as %(userIdentifier)s and approve the login oidRequest,
1722            hit OK below.  The "Remember this decision" checkbox
1723            applies only to the trust root decision.</p>''' % mdata
1724
1725            fdata = {
1726                'pathAllow': self.urls['url_allow'],
1727                'identity': oidRequest.identity,
1728                'trust_root': oidRequest.trust_root,
1729                'username': username,
1730                }
1731            form = '''\
1732<table>
1733  <tr><td>Identity:</td><td>%(identity)s</td></tr>
1734  <tr><td>Trust Root:</td><td>%(trust_root)s</td></tr>
1735</table>
1736<p>Allow this authentication to proceed?</p>
1737<form method="POST" action="%(pathAllow)s">
1738  <input type="checkbox" id="remember" name="remember" value="Yes"
1739      /><label for="remember">Remember this
1740      decision</label><br />
1741  <input type="hidden" name="login_as" value="%(username)s"/>
1742  <input type="submit" name="Yes" value="Yes" />
1743  <input type="submit" name="No" value="No" />
1744</form>''' % fdata
1745
1746        response = self._showPage(environ, 'Approve OpenID request?',
1747                                  msg=msg, form=form)           
1748        start_response('200 OK',
1749                       [('Content-type', 'text/html' + self.charset),
1750                        ('Content-length', str(len(response)))])
1751        return response
1752   
1753
1754    def _showPage(self,
1755                  environ,
1756                  title,
1757                  head_extras='',
1758                  msg=None,
1759                  err=None,
1760                  form=None):
1761        """Generic page rendering method.  Derived classes may ignore this.
1762       
1763        @type environ: dict
1764        @param environ: dictionary of environment variables
1765        @type title: basestring
1766        @param title: page title
1767        @type head_extras: basestring
1768        @param head_extras: add extra HTML header elements
1769        @type msg: basestring
1770        @param msg: optional message for page body
1771        @type err: basestring
1772        @param err: optional error message for page body
1773        @type form: basestring
1774        @param form: optional form for page body       
1775        @rtype: basestring
1776        @return: WSGI response
1777        """
1778       
1779        username = environ['beaker.session'].get('username')
1780        if username is None:
1781            user_link = '<a href="/login">not logged in</a>.'
1782        else:
1783            user_link = 'logged in as <a href="%s/%s">%s</a>.<br />'\
1784                        '<a href="%s?submit=true&'\
1785                        'success_to=%s">Log out</a>' % \
1786                        (self.urls['url_id'], username, username,
1787                         self.urls['url_loginsubmit'],
1788                         self.urls['url_login'])
1789
1790        body = ''
1791
1792        if err is not None:
1793            body += '''\
1794            <div class="error">
1795              %s
1796            </div>
1797            ''' % err
1798
1799        if msg is not None:
1800            body += '''\
1801            <div class="message">
1802              %s
1803            </div>
1804            ''' % msg
1805
1806        if form is not None:
1807            body += '''\
1808            <div class="form">
1809              %s
1810            </div>
1811            ''' % form
1812
1813        contents = {
1814            'title': 'Python OpenID Provider - ' + title,
1815            'head_extras': head_extras,
1816            'body': body,
1817            'user_link': user_link,
1818            }
1819
1820        response = '''<html>
1821  <head>
1822    <title>%(title)s</title>
1823    %(head_extras)s
1824  </head>
1825  <style type="text/css">
1826      h1 a:link {
1827          color: black;
1828          text-decoration: none;
1829      }
1830      h1 a:visited {
1831          color: black;
1832          text-decoration: none;
1833      }
1834      h1 a:hover {
1835          text-decoration: underline;
1836      }
1837      body {
1838        font-family: verdana,sans-serif;
1839        width: 50em;
1840        margin: 1em;
1841      }
1842      div {
1843        padding: .5em;
1844      }
1845      table {
1846        margin: none;
1847        padding: none;
1848      }
1849      .banner {
1850        padding: none 1em 1em 1em;
1851        width: 100%%;
1852      }
1853      .leftbanner {
1854        text-align: left;
1855      }
1856      .rightbanner {
1857        text-align: right;
1858        font-size: smaller;
1859      }
1860      .error {
1861        border: 1px solid #ff0000;
1862        background: #ffaaaa;
1863        margin: .5em;
1864      }
1865      .message {
1866        border: 1px solid #2233ff;
1867        background: #eeeeff;
1868        margin: .5em;
1869      }
1870      .form {
1871        border: 1px solid #777777;
1872        background: #ddddcc;
1873        margin: .5em;
1874        margin-top: 1em;
1875        padding-bottom: 0em;
1876      }
1877      dd {
1878        margin-bottom: 0.5em;
1879      }
1880  </style>
1881  <body>
1882    <table class="banner">
1883      <tr>
1884        <td class="leftbanner">
1885          <h1><a href="/">Python OpenID Provider</a></h1>
1886        </td>
1887        <td class="rightbanner">
1888          You are %(user_link)s
1889        </td>
1890      </tr>
1891    </table>
1892%(body)s
1893  </body>
1894</html>
1895''' % contents
1896
1897        return response
1898
1899    def errorPage(self, environ, start_response, msg, code=500):
1900        """Display error page
1901       
1902        @type environ: dict
1903        @param environ: dictionary of environment variables
1904        @type start_response: callable
1905        @param start_response: WSGI start response function.  Should be called
1906        from this method to set the response code and HTTP header content
1907        @type msg: basestring
1908        @param msg: optional message for page body
1909        @rtype: basestring
1910        @return: WSGI response
1911        """
1912       
1913        response = self._showPage(environ, 'Error Processing Request', err='''\
1914        <p>%s</p>
1915        <!--
1916
1917        This is a large comment.  It exists to make this page larger.
1918        That is unfortunately necessary because of the "smart"
1919        handling of pages returned with an error code in IE.
1920
1921        *************************************************************
1922        *************************************************************
1923        *************************************************************
1924        *************************************************************
1925        *************************************************************
1926        *************************************************************
1927        *************************************************************
1928        *************************************************************
1929        *************************************************************
1930        *************************************************************
1931        *************************************************************
1932        *************************************************************
1933        *************************************************************
1934        *************************************************************
1935        *************************************************************
1936        *************************************************************
1937        *************************************************************
1938        *************************************************************
1939        *************************************************************
1940        *************************************************************
1941        *************************************************************
1942        *************************************************************
1943        *************************************************************
1944
1945        -->
1946        ''' % msg)
1947       
1948        start_response('%d %s' % (code, httplib.responses[code]),
1949                       [('Content-type', 'text/html' + self.charset),
1950                        ('Content-length', str(len(response)))])
1951        return response
Note: See TracBrowser for help on using the repository browser.