source: TI12-security/trunk/python/ndg.security.server/ndg/security/server/wsgi/utils/openid/provider/openid_provider.py @ 4540

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

moved openid_provider into new openid.provider package structure.

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