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

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

Tested WS-Security SignatureHandler? based on the 4Suite-XML Canonicalizer. - Tested a client connecting to a server using the old dom based implementation of the SignatureHandler?.

Line 
1"""NDG Security OpenID Relying Party Middleware
2
3Wrapper to AuthKit OpenID Middleware
4
5NERC DataGrid Project
6"""
7__author__ = "P J Kershaw"
8__date__ = "20/01/2009"
9__copyright__ = "(C) 2009 Science and Technology Facilities Council"
10__license__ = "BSD - see top-level directory for LICENSE file"
11__contact__ = "Philip.Kershaw@stfc.ac.uk"
12__revision__ = "$Id$"
13import logging
14log = logging.getLogger(__name__)
15
16import httplib # to get official status code messages
17import urllib # decode quoted URI in query arg
18from urlparse import urlsplit, urlunsplit
19
20from paste.request import parse_querystring, parse_formvars
21import authkit.authenticate
22import beaker.middleware
23
24from ndg.security.server.wsgi import NDGSecurityMiddlewareBase
25from ndg.security.server.wsgi.authn import AuthNRedirectMiddleware
26from ndg.security.common.utils.classfactory import instantiateClass
27
28class OpenIDRelyingPartyMiddlewareError(Exception):
29    """OpenID Relying Party WSGI Middleware Error"""
30
31class OpenIDRelyingPartyConfigError(OpenIDRelyingPartyMiddlewareError):
32    """OpenID Relying Party Configuration Error"""
33 
34class OpenIDRelyingPartyMiddleware(NDGSecurityMiddlewareBase):
35    '''Implementation of OpenID Relying Party based on AuthKit'''
36    propertyDefaults = {
37        'signinInterfaceMiddlewareClass': None,
38        'baseURL': '',
39        'sessionKey': 'beaker.session'
40    }
41    propertyDefaults.update(NDGSecurityMiddlewareBase.propertyDefaults)
42   
43    def __init__(self, app, global_conf, prefix='openid.relyingparty.', 
44                 **app_conf):
45        """Add AuthKit and Beaker middleware dependencies to WSGI stack
46       
47        @type app: callable following WSGI interface signature
48        @param app: next middleware application in the chain     
49        @type global_conf: dict       
50        @param global_conf: PasteDeploy application global configuration -
51        must follow format of propertyDefaults class variable
52        @type prefix: basestring
53        @param prefix: prefix for OpenID Relying Party configuration items
54        @type app_conf: dict
55        @param app_conf: application specific configuration - must follow
56        format of propertyDefaults class variable"""   
57
58           
59        # Check for sign in template settings
60        if prefix+'signinInterfaceMiddlewareClass' in app_conf:
61            if 'authkit.openid.template.obj' in app_conf or \
62               'authkit.openid.template.string' in app_conf or \
63               'authkit.openid.template.file' in app_conf:
64                log.warning("OpenID Relying Party "
65                            "'signinInterfaceMiddlewareClass' "
66                            "setting overrides 'authkit.openid.template.*' "
67                            "AuthKit settings")
68               
69            moduleName, className = \
70                app_conf[prefix+'signinInterfaceMiddlewareClass'].rsplit('.',1)
71           
72            signinInterfacePrefix = prefix+'signinInterface.'
73            classProperties = {'prefix': signinInterfacePrefix}
74            classProperties.update(app_conf)
75            app = instantiateClass(moduleName, 
76                                   className, 
77                                   objectType=SigninInterface, 
78                                   classArgs=(app, global_conf),
79                                   classProperties=classProperties)           
80           
81            # Delete sign in interface middleware settings
82            for conf in app_conf, global_conf or {}:
83                for k in conf.keys():
84                    if k.startswith(signinInterfacePrefix):
85                        del conf[k]
86       
87            app_conf['authkit.openid.template.string'] = app.makeTemplate()
88               
89        self.signoutPath = app_conf.get('authkit.cookie.signoutpath')
90
91        authKitApp = authkit.authenticate.middleware(app, app_conf)
92        _app = authKitApp
93        while True:
94            if isinstance(_app,authkit.authenticate.open_id.AuthOpenIDHandler):
95                self._authKitVerifyPath = _app.path_verify
96                self._authKitProcessPath = _app.path_process
97                break
98           
99            elif hasattr(_app, 'app'):
100                _app = _app.app
101            else:
102                break
103         
104        if not hasattr(self, '_authKitVerifyPath'):
105            raise OpenIDRelyingPartyConfigError("Error locating the AuthKit "
106                                                "AuthOpenIDHandler in the "
107                                                "WSGI stack")
108       
109        # Check for return to argument in query key value pairs
110        self._return2URIKey = AuthNRedirectMiddleware.return2URIArgName + '='
111   
112        super(OpenIDRelyingPartyMiddleware, self).__init__(authKitApp, 
113                                                           global_conf, 
114                                                           prefix=prefix, 
115                                                           **app_conf)
116
117       
118    @NDGSecurityMiddlewareBase.initCall     
119    def __call__(self, environ, start_response):
120        '''Alter start_response to override the status code and force to 401.
121        This will enable non-browser based client code to bypass the OpenID
122        interface
123       
124        @type environ: dict
125        @param environ: WSGI environment variables dictionary
126        @type start_response: builtin_function_or_method
127        @param start_response: standard WSGI start response function
128        '''
129        session = environ[self.sessionKey]
130       
131        # Check for return to address in URI query args
132        if environ['REQUEST_METHOD'] == "GET":
133            params = dict(parse_querystring(environ))
134        else:
135            params = dict(parse_formvars(environ))
136       
137        quotedReferrer=params.get(AuthNRedirectMiddleware.return2URIArgName,'')
138        referrer = urllib.unquote(quotedReferrer)
139        referrerPathInfo = urlsplit(referrer)[2]
140
141        if referrer and \
142           not referrerPathInfo.endswith(self._authKitVerifyPath) and \
143           not referrerPathInfo.endswith(self._authKitProcessPath):
144            # Set-up for authkit.authenticate.open_id.AuthOpenIDHandler.process
145            session['referer'] = referrer
146            session.save()
147           
148        if self._return2URIKey in environ.get('HTTP_REFERER', ''):
149            # Remove return to arg to avoid interfering with AuthKit OpenID
150            # processing
151            splitURI = urlsplit(environ['HTTP_REFERER'])
152            query = splitURI[3]
153           
154            filteredQuery = '&'.join([arg for arg in query.split('&')
155                                if not arg.startswith(self._return2URIKey)])
156           
157            environ['HTTP_REFERER'] = urlunsplit(splitURI[:3] + \
158                                                 (filteredQuery,) + \
159                                                 splitURI[4:])
160
161        if self.signoutPath is not None and self.pathInfo == self.signoutPath:
162            # TODO: Redirect to referrer ...
163            referrer = session.get(
164                        'ndg.security.server.wsgi.openid.relyingparty.referer')
165            if referrer is not None:
166                def setRedirectResponse(status, header, exc_info=None):
167                    header.extend([('Location', referrer)])
168                    return start_response(self.getStatusMessage(302), 
169                                          header,
170                                          exc_info)
171                   
172                return self._app(environ, setRedirectResponse)
173            else:
174                log.debug('No referrer set for redirect following logout')
175               
176        # Set a return to address following logout. 
177        # TODO: This code will need to be refactored if this middleware is
178        # deployed externally via a proxy - HTTP_REFERER will be the internal
179        # URI instead of the one exposed outside
180        if 'HTTP_REFERER' in environ:
181            session['ndg.security.server.wsgi.openid.relyingparty.referer'] = \
182                environ['HTTP_REFERER']
183            session.save()
184       
185        # See _start_response doc for an explanation...
186        if environ['PATH_INFO'] == self._authKitVerifyPath: 
187            def _start_response(status, header, exc_info=None):
188                '''Make OpenID Relying Party OpenID prompt page return a 401
189                status to signal to non-browser based clients that
190                authentication is required.  Requests are filtered on content
191                type so that static content such as graphics and style sheets
192                associated with the page are let through unaltered
193               
194                @type status: str
195                @param status: HTTP status code and status message
196                @type header: list
197                @param header: list of field, value tuple HTTP header content
198                @type exc_info: Exception
199                @param exc_info: exception info
200                '''
201                _status = status
202                for name, val in header:
203                    if name.lower() == 'content-type' and \
204                       val.startswith('text/html'):
205                        _status = self.getStatusMessage(401)
206                        break
207                   
208                return start_response(_status, header, exc_info)
209        else:
210            _start_response = start_response
211
212        return self._app(environ, _start_response)
213
214   
215class SigninInterfaceError(Exception):
216    """Base class for SigninInterface exceptions
217   
218    A standard message is raised set by the msg class variable but the actual
219    exception details are logged to the error log.  The use of a standard
220    message enables callers to use its content for user error messages.
221   
222    @type msg: basestring
223    @cvar msg: standard message to be raised for this exception"""
224    userMsg = ("An internal error occurred with the page layout,  Please "
225               "contact your system administrator")
226    errorMsg = "SigninInterface error"
227   
228    def __init__(self, *arg, **kw):
229        if len(arg) > 0:
230            msg = arg[0]
231        else:
232            msg = self.__class__.errorMsg
233           
234        log.error(msg)
235        Exception.__init__(self, msg, **kw)
236       
237class SigninInterfaceInitError(SigninInterfaceError):
238    """Error with initialisation of SigninInterface.  Raise from __init__"""
239    errorMsg = "SigninInterface initialisation error"
240   
241class SigninInterfaceConfigError(SigninInterfaceError):
242    """Error with configuration settings.  Raise from __init__"""
243    errorMsg = "SigninInterface configuration error"   
244
245class SigninInterface(NDGSecurityMiddlewareBase):
246    """Base class for sign in rendering.  This is implemented as WSGI
247    middleware to enable additional middleware to be added into the call
248    stack e.g. StaticFileParser to enable rendering of graphics and other
249    static content in the Sign In page"""
250   
251    def getTemplateFunc(self):
252        """Return template function for AuthKit to render OpenID Relying
253        Party Sign in page"""
254        raise NotImplementedError()
255   
256    def __call__(self, environ, start_response):
257        return self._app(self, environ, start_response)
Note: See TracBrowser for help on using the repository browser.