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

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

Global replace to fix copyright from STFC & NERC to STFC alone because it's not possible to have copyright held by two orgs.

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