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

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

OpenID Provider Authentication interface:

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