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

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 _filterOpts(cls, opt, newOpt, prefix=''):
388        '''Convenience utility to filter input options set in __init__ via
389        app_conf or keywords
390       
391        Nb. exclusions for authN and rendering interface properties.
392       
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        '''
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
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)
428            else:
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
436               
437        if len(badOptNames) > 0:
438            raise TypeError("Invalid input option(s) set: %s" % 
439                            (", ".join(badOptNames)))
440           
441
442    def __call__(self, environ, start_response):
443        """Standard WSGI interface.  Intercepts the path if it matches any of
444        the paths set in the path_* keyword settings to the config
445       
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        """
453        if not environ.has_key(self.session_middleware):
454            raise OpenIDProviderConfigError('The session middleware %r is not '
455                                            'present. Have you set up the '
456                                            'session middleware?' % \
457                                            self.session_middleware)
458
459        self.path = environ.get('PATH_INFO').rstrip('/')
460        self.environ = environ
461        self.start_response = start_response
462        self.session = environ[self.session_middleware]
463        self._render.session = self.session
464       
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)
467           
468            # Disallow identifier and yadis URIs where no ID was specified
469            return self.app(environ, start_response)
470           
471        elif self.path.startswith(self.paths['path_id']) or \
472             self.path.startswith(self.paths['path_yadis']):
473           
474            # Match against path minus ID as this is not known in advance           
475            pathMatch = self.path[:self.path.rfind('/')]
476        else:
477            pathMatch = self.path
478           
479        if pathMatch in self.method:
480            self.query = dict(paste.request.parse_formvars(environ)) 
481            log.debug("Calling method %s ..." % self.method[pathMatch]) 
482           
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
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):
500        '''URL based discovery with an ID provided
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        '''
510        response = self._render.identityPage(environ, start_response)
511        return response
512
513
514    def do_yadis(self, environ, start_response):
515        """Handle Yadis based discovery with an ID provided
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        """
525        response = self._render.yadis(environ, start_response)
526        return response
527
528
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
545    def do_openidserver(self, environ, start_response):
546        """OpenID Server endpoint - handles OpenID Request following discovery
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
556        try:
557            oidRequest = self.oidserver.decodeRequest(self.query)
558           
559        except server.ProtocolError, why:
560            response = self._displayResponse(why)
561           
562        else:
563            if oidRequest is None:
564                # Display text indicating that this is an endpoint.
565                response = self.do_mainpage(environ, start_response)
566           
567            # Check mode is one of "checkid_immediate", "checkid_setup"
568            elif oidRequest.mode in server.BROWSER_REQUEST_MODES:
569                response = self._handleCheckIDRequest(oidRequest)
570            else:
571                oidResponse = self.oidserver.handleRequest(oidRequest)
572                response = self._displayResponse(oidResponse)
573           
574        return response
575           
576
577    def do_allow(self, environ, start_response):
578        """Handle allow request processing the result of do_decide: does user
579        allow credentials to be passed back to the Relying Party?
580       
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       
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       
605        oidRequest = self.session.get('lastCheckIDRequest')
606        if oidRequest is None:
607            log.error("Suspected do_allow called from stale request")
608            return self._render.errorPage(environ, start_response,
609                                          "Invalid request",
610                                          code=400)
611       
612        if 'Yes' in self.query:
613            if oidRequest.idSelect():
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")
622            else:
623                identity = oidRequest.identity
624
625            trust_root = oidRequest.trust_root
626            if self.query.get('remember', 'No') == 'Yes':
627                self.session['approved'] = {trust_root: 'always'}
628                self.session.save()
629             
630            try:
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                   
643            except Exception, e:
644                log.error("Setting response following ID Approval: %s" % e)
645                return self._render.errorPage(environ, start_response,
646                        'An error occurred setting additional parameters '
647                        'required by the site requesting your ID.  Please '
648                        'report this fault to your site administrator.')
649            else:
650                return self._displayResponse(oidResponse)
651       
652        elif 'No' in self.query:
653            # TODO: Check 'No' response is OK - No causes AuthKit's Relying
654            # Party implementation to crash with 'openid.return_to' KeyError
655            # in Authkit.authenticate.open_id.process
656            oidResponse = oidRequest.answer(False)
657            #return self._displayResponse(oidResponse)
658            return self._render.mainPage(environ, start_response)           
659        else:
660            return self._render.errorPage(environ, start_response,
661                                          'Expecting Yes/No in allow '
662                                          'post. %r' % self.query,
663                                          code=400)
664
665
666    def do_login(self, environ, start_response, **kw):
667        """Display Login form
668       
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       
679        if 'fail_to' not in kw:
680            kw['fail_to'] = self.urls['url_login']
681           
682        response = self._render.login(environ, start_response, **kw)
683        return response
684
685
686    def do_loginsubmit(self, environ, start_response):
687        """Handle user submission from login and logout
688       
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       
697        if 'submit' in self.query:
698            if 'username' in self.query:
699                # login
700                if 'username' in self.session:
701                    log.error("Attempting login for user %s: user %s is "
702                              "already logged in", self.session['username'],
703                              self.session['username'])
704                    return self._redirect(start_response,self.query['fail_to'])
705               
706                oidRequest = self.session.get('lastCheckIDRequest')
707                if oidRequest is None:
708                    log.error("Getting OpenID request for login - No request "
709                              "found in session")
710                    return self._render.errorPage(environ, start_response,
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.")
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                   
725                # Invoke custom authentication interface plugin
726                try:
727                    self._authN.logon(environ,
728                                      userIdentifier,
729                                      self.query['username'],
730                                      self.query.get('password', ''))
731                   
732                except AuthNInterfaceError, e:
733                    return self._render.login(environ, start_response,
734                                          msg=e.userMsg,
735                                          success_to=self.urls['url_decide'])                   
736                except Exception, e:
737                    log.error("Unexpected exception raised during "
738                              "authentication: %s" % e)
739                    msg = ("An internal error occured.  "
740                           "Please try again or if the problems persists "
741                           "contact your system administrator.")
742
743                    response = self._render.login(environ, start_response,
744                                      msg=msg,
745                                      success_to=self.urls['url_decide'])
746                    return response
747                       
748                self.session['username'] = self.query['username']
749                self.session['approved'] = {}
750                self.session.save()
751            else:
752                # logout
753                if 'username' not in self.session:
754                    log.error("No user is logged in")
755                    return self._redirect(start_response,self.query['fail_to'])
756               
757                del self.session['username']
758                self.session.pop('approved', None)
759                self.session.save()
760               
761            return self._redirect(start_response, self.query['success_to'])
762       
763        elif 'cancel' in self.query:
764            return self._redirect(start_response, self.query['fail_to'])
765        else:
766            log.error('Login input not recognised %r' % self.query)
767            return self._redirect(start_response, self.query['fail_to'])
768           
769
770    def do_mainpage(self, environ, start_response):
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
779        '''   
780        response = self._render.mainPage(environ, start_response)
781        return response
782
783
784    def do_decide(self, environ, start_response):
785        """Display page prompting the user to decide whether to trust the site
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        """
795
796        oidRequest = self.session.get('lastCheckIDRequest')
797        if oidRequest is None:
798            log.error("No OpenID request set in session")
799            return self._render.errorPage(environ, start_response,
800                                          "Invalid request.  Please report "
801                                          "the error to your site "
802                                          "administrator.",
803                                          code=400)
804       
805        approvedRoots = self.session.get('approved', {})
806       
807        if oidRequest.trust_root in approvedRoots and \
808           not oidRequest.idSelect():
809            try:
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                   
821            except Exception, e:
822                log.error("Setting response following ID Approval: %s" % e)
823                response = self._render.errorPage(environ, start_response,
824                        'An error occurred setting additional parameters '
825                        'required by the site requesting your ID.  Please '
826                        'report this fault to your site administrator.')
827                return response
828
829            return self.oidResponse(response)
830        else:
831            return self._render.decidePage(environ, start_response, oidRequest)
832       
833       
834    def _identityIsAuthorized(self, oidRequest):
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.
837        This last check doesn't apply for ID Select mode where No ID was input
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        '''
845        username = self.session.get('username')
846        if username is None:
847            return False
848
849        if oidRequest.idSelect():
850            log.debug("OpenIDProviderMiddleware._identityIsAuthorized - "
851                      "ID Select mode set but user is already logged in")
852            return True
853       
854        identifiers = self._authN.username2UserIdentifiers(self.environ,
855                                                           username)
856        idURLBase = self.urls['url_id']+'/'
857        identityURLs = [idURLBase+i for i in identifiers]
858        if oidRequest.identity not in identityURLs:
859            log.debug("OpenIDProviderMiddleware._identityIsAuthorized - "
860                      "user is already logged in with a different ID=%s" % \
861                      username)
862            return False
863       
864        log.debug("OpenIDProviderMiddleware._identityIsAuthorized - "
865                  "user is logged in with ID matching ID URL")
866        return True
867   
868   
869    def _trustRootIsAuthorized(self, trust_root):
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'''
879        approvedRoots = self.session.get('approved', {})
880        return approvedRoots.get(trust_root) is not None
881
882
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       
891        if self.sregResponseHandler is None:
892            # No Simple Registration response object was set
893            return
894       
895        sreg_req = sreg.SRegRequest.fromOpenIDRequest(oidRequest)
896
897        # Callout to external callable sets additional user attributes to be
898        # returned in response to Relying Party       
899        sreg_data = self.sregResponseHandler(self.session.get('username'))
900        sreg_resp = sreg.SRegResponse.extractResponse(sreg_req, sreg_data)
901        oidResponse.addExtension(sreg_resp)
902
903
904    def _addAXResponse(self, oidRequest, oidResponse):
905        '''Add attributes to response based on the OpenID Attribute Exchange
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'''
912
913
914        ax_req = ax.FetchRequest.fromOpenIDRequest(oidRequest)
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:
924                msg = ("Relying party requires these attributes: %s; but No"
925                       "Attribute exchange handler 'axResponseHandler' has "
926                       "been set" % requiredAttr)
927                log.error(msg)
928                raise OpenIDProviderMissingAXResponseHandler(msg)
929           
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()
935        try:
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       
943        except Exception, e:
944            log.error("%s exception raised setting requested Attribute "
945                      "Exchange values: %s" % (e.__class__, e))
946            raise
947       
948        oidResponse.addExtension(ax_resp)
949       
950       
951    def _identityApprovedPostProcessing(self, oidRequest, identifier=None):
952        '''Action following approval of a Relying Party by the user.  Add
953        Simple Registration and/or Attribute Exchange parameters if handlers
954        were specified - See _addSRegResponse and _addAXResponse methods - and
955        only if the Relying Party has requested them
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
961        @rtype: openid.server.server.OpenIDResponse
962        @return: OpenID response object'''
963
964        oidResponse = oidRequest.answer(True, identity=identifier)
965        self._addSRegResponse(oidRequest, oidResponse)
966        self._addAXResponse(oidRequest, oidResponse)
967       
968        return oidResponse
969
970
971    def _handleCheckIDRequest(self, oidRequest):
972        """Handle "checkid_immediate" and "checkid_setup" type requests from
973        Relying Party
974       
975        @type oidRequest: openid.server.server.CheckIDRequest
976        @param oidRequest: OpenID Check ID request
977        @rtype: basestring
978        @return: WSGI response
979        """
980        log.debug("OpenIDProviderMiddleware._handleCheckIDRequest ...")
981       
982        # Save request
983        self.session['lastCheckIDRequest'] = oidRequest
984        self.session.save()
985       
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):
997                # User has approved this Relying Party
998                try:
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                   
1010                except Exception, e:
1011                    log.error("Setting response following ID Approval: %s" % e)
1012                    response = self._render.errorPage(environ, start_response,
1013                        'An error occurred setting additional parameters '
1014                        'required by the site requesting your ID.  Please '
1015                        'report this fault to your site administrator.')
1016                    return response
1017               
1018                return self._displayResponse(oidResponse)
1019            else:
1020                return self.do_decide(self.environ, self.start_response)
1021               
1022        elif oidRequest.immediate:
1023            oidResponse = oidRequest.answer(False)
1024            return self._displayResponse(oidResponse)
1025       
1026        else:
1027            # User is not logged in
1028           
1029            # Call login and if successful then call decide page to confirm
1030            # user wishes to trust the Relying Party.
1031            response = self.do_login(self.environ,
1032                                     self.start_response,
1033                                     success_to=self.urls['url_decide'])
1034            return response
1035
1036
1037    def _displayResponse(self, oidResponse):
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       
1051        try:
1052            webresponse = self.oidserver.encodeResponse(oidResponse)
1053        except server.EncodingError, why:
1054            text = why.response.encodeToKVForm()
1055            return self.showErrorPage(text)
1056       
1057        hdr = webresponse.headers.items()
1058       
1059        # If the content length exceeds the maximum to represent on a URL, it's
1060        # rendered as a form instead
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():
1066            # Wrap in HTML with Javascript OnLoad to submit the form
1067            # automatically without user intervention
1068            response = OpenIDProviderMiddleware.formRespWrapperTmpl % \
1069                                                        webresponse.body
1070        else:
1071            response = webresponse.body
1072           
1073        hdr += [('Content-type', 'text/html'+self.charset),
1074                ('Content-length', str(len(response)))]
1075           
1076        self.start_response('%d %s' % (webresponse.code, 
1077                                       httplib.responses[webresponse.code]), 
1078                            hdr)
1079        return response
1080
1081
1082    def _redirect(self, start_response, url):
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        """
1092        start_response('302 %s' % httplib.responses[302], 
1093                       [('Content-type', 'text/html'+self.charset),
1094                        ('Location', url)])
1095        return []
1096   
1097   
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   
1128class RenderingInterface(object):
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.
1137   
1138    @cvar tmplServerYadis: template for returning Yadis document to Relying
1139    Party.  Derived classes can reset this or completely override the
1140    serverYadis method.
1141   
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
1146    override the yadis method.
1147   
1148    @type tmplYadis: basestring"""
1149   
1150    tmplServerYadis = """\
1151<?xml version="1.0" encoding="UTF-8"?>
1152<xrds:XRDS
1153    xmlns:xrds="xri://$xrds"
1154    xmlns="xri://$xrd*($OptNameSfx*2.0)">
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
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>"""   
1182   
1183    def __init__(self, authN, base_url, urls, **opt):
1184        """
1185        @type authN: AuthNInterface
1186        @param param: reference to authentication interface to enable OpenID
1187        user URL construction from username
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
1194        @type opt: dict
1195        @param opt: additional custom options passed from the
1196        OpenIDProviderMiddleware config
1197        """
1198        self._authN = authN
1199        self.base_url = base_url
1200        self.urls = urls
1201        self.charset = ''
1202   
1203   
1204    def serverYadis(self, environ, start_response):
1205        '''Render Yadis info for ID Select mode request
1206       
1207        @type environ: dict
1208        @param environ: dictionary of environment variables
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
1212        @rtype: basestring
1213        @return: WSGI response
1214        '''
1215        endpoint_url = self.urls['url_openidserver']
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
1224
1225
1226    def yadis(self, environ, start_response):
1227        """Render Yadis document containing user URL
1228       
1229        @type environ: dict
1230        @param environ: dictionary of environment variables
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
1234        @rtype: basestring
1235        @return: WSGI response
1236        """
1237        # Override this method to implement an alternate means to derive the
1238        # username identifier
1239        userIdentifier = environ['PATH_INFO'].rstrip('/').split('/')[-1]
1240       
1241        endpoint_url = self.urls['url_openidserver']
1242        user_url = self.urls['url_id'] + '/' + userIdentifier
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       
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   
1256
1257    def identityPage(self, environ, start_response):
1258        """Render the identity page.
1259       
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=''):
1277        """Render the login form.
1278       
1279        @type environ: dict
1280        @param environ: dictionary of environment variables
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
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       
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
1330
1331        These fields should be posted by this page ready for
1332        OpenIdProviderMiddleware.do_allow to process:
1333       
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       
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('/')
1399        userIdentifier = path.split('/')[-1]
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">' % \
1405            (self.urls['url_yadis']+'/'+userIdentifier)
1406           
1407        disco_tags = link_tag + yadis_loc_tag
1408        ident = self.base_url + path
1409
1410        response = self._showPage(environ, 
1411                                  'Identity Page', 
1412                                  head_extras=disco_tags, 
1413                                  msg='<p>This is the identity page for %s.'
1414                                      '</p>' % ident)
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       
1443        if success_to is None:
1444            success_to = self.urls['url_mainpage']
1445           
1446        if fail_to is None:
1447            fail_to = self.urls['url_mainpage']
1448       
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)
1471
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
1477
1478
1479    def mainPage(self, environ, start_response):
1480        """Rendering the main page.
1481       
1482        @type environ: dict
1483        @param environ: dictionary of environment variables
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
1487        @rtype: basestring
1488        @return: WSGI response
1489        """
1490       
1491        yadis_tag = '<meta http-equiv="x-xrds-location" content="%s">' % \
1492                    self.urls['url_serveryadis']
1493        username = environ['beaker.session'].get('username')   
1494        if username:
1495            openid_url = self.urls['url_id'] + '/' + username
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>
1500            """ % (username, quoteattr(openid_url), openid_url)
1501        else:
1502            user_message = "<p>You are not <a href='%s'>logged in</a>.</p>" % \
1503                            self.urls['url_login']
1504
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)
1516   
1517        start_response('200 OK', 
1518                       [('Content-type', 'text/html'+self.charset),
1519                        ('Content-length', str(len(response)))])
1520        return response
1521   
1522
1523    def decidePage(self, environ, start_response, oidRequest):
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
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
1535        @type oidRequest: openid.server.server.CheckIDRequest
1536        @param oidRequest: OpenID Check ID Request object
1537        @rtype: basestring
1538        @return: WSGI response
1539        """
1540        idURLBase = self.urls['url_id'] + '/'
1541       
1542        # XXX: This may break if there are any synonyms for idURLBase,
1543        # such as referring to it by IP address or a CNAME.
1544       
1545        # TODO: OpenID 2.0 Allows oidRequest.identity to be set to
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?
1551#        assert oidRequest.identity.startswith(idURLBase), \
1552#               repr((oidRequest.identity, idURLBase))
1553        userIdentifier = oidRequest.identity[len(idURLBase):]
1554        username = environ['beaker.session']['username']
1555       
1556        if oidRequest.idSelect(): # We are being asked to select an ID
1557            userIdentifier = self._authN.username2UserIdentifiers(environ,
1558                                                                  username)[0]
1559            identity = idURLBase + userIdentifier
1560           
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 = {
1570                'pathAllow': self.urls['url_allow'],
1571                'identity': identity,
1572                'trust_root': oidRequest.trust_root,
1573                }
1574            form = '''\
1575<form method="POST" action="%(pathAllow)s">
1576<table>
1577  <tr><td>Identity:</td>
1578     <td>%(identity)s</td></tr>
1579  <tr><td>Trust Root:</td><td>%(trust_root)s</td></tr>
1580</table>
1581<p>Allow this authentication to proceed?</p>
1582<input type="checkbox" id="remember" name="remember" value="Yes"
1583    /><label for="remember">Remember this
1584    decision</label><br />
1585<input type="hidden" name="identity" value="%(identity)s" />
1586<input type="submit" name="Yes" value="Yes" />
1587<input type="submit" name="No" value="No" />
1588</form>
1589''' % fdata
1590           
1591        elif userIdentifier in self._authN.username2UserIdentifiers(environ,
1592                                                                    username):
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 = {
1601                'pathAllow': self.urls['url_allow'],
1602                'identity': oidRequest.identity,
1603                'trust_root': oidRequest.trust_root,
1604                }
1605            form = '''\
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>
1611<form method="POST" action="%(pathAllow)s">
1612  <input type="checkbox" id="remember" name="remember" value="Yes"
1613      /><label for="remember">Remember this
1614      decision</label><br />
1615  <input type="submit" name="Yes" value="Yes" />
1616  <input type="submit" name="No" value="No" />
1617</form>''' % fdata
1618        else:
1619            mdata = {
1620                'userIdentifier': userIdentifier,
1621                'username': username,
1622                }
1623            msg = '''\
1624            <p>A site has asked for an identity belonging to
1625            %(userIdentifier)s, but you are logged in as %(username)s.  To
1626            log in as %(userIdentifier)s and approve the login oidRequest,
1627            hit OK below.  The "Remember this decision" checkbox
1628            applies only to the trust root decision.</p>''' % mdata
1629
1630            fdata = {
1631                'pathAllow': self.urls['url_allow'],
1632                'identity': oidRequest.identity,
1633                'trust_root': oidRequest.trust_root,
1634                'username': username,
1635                }
1636            form = '''\
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>
1642<form method="POST" action="%(pathAllow)s">
1643  <input type="checkbox" id="remember" name="remember" value="Yes"
1644      /><label for="remember">Remember this
1645      decision</label><br />
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" />
1649</form>''' % fdata
1650
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   
1658
1659    def _showPage(self, 
1660                  environ, 
1661                  title, 
1662                  head_extras='', 
1663                  msg=None, 
1664                  err=None, 
1665                  form=None):
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       
1684        username = environ['beaker.session'].get('username')
1685        if username is None:
1686            user_link = '<a href="/login">not logged in</a>.'
1687        else:
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>' % \
1691                        (self.urls['url_id'], username, username, 
1692                         self.urls['url_loginsubmit'],
1693                         self.urls['url_login'])
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 = {
1719            'title': 'Python OpenID Provider - ' + title,
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">
1790          <h1><a href="/">Python OpenID Provider</a></h1>
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
1802        return response
1803
1804    def errorPage(self, environ, start_response, msg, code=500):
1805        """Display error page
1806       
1807        @type environ: dict
1808        @param environ: dictionary of environment variables
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
1812        @type msg: basestring
1813        @param msg: optional message for page body
1814        @rtype: basestring
1815        @return: WSGI response
1816        """
1817       
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       
1853        start_response('%d %s' % (code, httplib.responses[code]), 
1854                       [('Content-type', 'text/html'+self.charset),
1855                        ('Content-length', str(len(response)))])
1856        return response
Note: See TracBrowser for help on using the repository browser.