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

Subversion URL: http://proj.badc.rl.ac.uk/svn/ndg/TI12-security/trunk/python/ndg.security.server/ndg/security/server/wsgi/openid/provider/__init__.py@4550
Revision 4549, 74.4 KB checked in by pjkersha, 12 years ago (diff)

OpenID Provider Authentication interface:

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