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

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@5057
Revision 5057, 76.0 KB checked in by pjkersha, 11 years ago (diff)

Tested WS-Security SignatureHandler? based on the 4Suite-XML Canonicalizer. - Tested a client connecting to a server using the old dom based implementation of the SignatureHandler?.

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