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

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@4559
Revision 4559, 70.3 KB checked in by pjkersha, 12 years ago (diff)

Moved BasicAuthNInterface from OpenID Provider __init.py module to it's own module in the authninterface package.

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