source: TI12-security/trunk/python/ndg.security.server/ndg/security/server/wsgi/openid_provider.py @ 4528

Subversion URL: http://proj.badc.rl.ac.uk/svn/ndg/TI12-security/trunk/python/ndg.security.server/ndg/security/server/wsgi/openid_provider.py@4528
Revision 4528, 65.2 KB checked in by pjkersha, 12 years ago (diff)

Added an Authentication interface for the OpenID Provider to enable authentication method to be customised. TODO:

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