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

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

Updated copyright

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