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

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