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

Subversion URL: http://proj.badc.rl.ac.uk/svn/ndg/TI12-security/trunk/NDGSecurity/python/ndg_security_server/ndg/security/server/wsgi/openid/relyingparty/__init__.py@7077
Revision 7077, 14.7 KB checked in by pjkersha, 9 years ago (diff)
  • Property svn:keywords set to Id
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
18import urllib2 # SSL based whitelisting
19from urlparse import urlsplit, urlunsplit
20
21from paste.request import parse_querystring, parse_formvars
22import authkit.authenticate
23from authkit.authenticate.open_id import AuthOpenIDHandler
24from beaker.middleware import SessionMiddleware
25
26# SSL based whitelisting
27from M2Crypto import SSL
28from M2Crypto.m2urllib2 import build_opener, HTTPSHandler
29from openid.fetchers import setDefaultFetcher, Urllib2Fetcher
30
31from ndg.security.common.utils.classfactory import instantiateClass
32from ndg.security.server.wsgi import NDGSecurityMiddlewareBase
33from ndg.security.server.wsgi.authn import AuthnRedirectMiddleware
34from ndg.security.server.wsgi.openid.relyingparty.validation import (
35                                                        SSLIdPValidationDriver)
36
37
38class OpenIDRelyingPartyMiddlewareError(Exception):
39    """OpenID Relying Party WSGI Middleware Error"""
40
41
42class OpenIDRelyingPartyConfigError(OpenIDRelyingPartyMiddlewareError):
43    """OpenID Relying Party Configuration Error"""
44 
45
46class OpenIDRelyingPartyMiddleware(NDGSecurityMiddlewareBase):
47    '''OpenID Relying Party middleware which wraps the AuthKit implementation.
48    This middleware is to be hosted in it's own security middleware stack.
49    WSGI middleware applications to be protected can be hosted in a separate
50    stack.  The AuthnRedirectMiddleware filter can respond to a HTTP
51    401 response from this stack and redirect to this middleware to initiate
52    OpenID based sign in.  AuthnRedirectMiddleware passes a query
53    argument in its request containing the URI return address for this
54    middleware to return to following OpenID sign in.
55    '''
56    OPENID_RP_PREFIX = 'openid.relyingparty.'
57    IDP_WHITELIST_CONFIG_FILEPATH_OPTNAME = 'idpWhitelistConfigFilePath'
58    SIGNIN_INTERFACE_MIDDLEWARE_CLASS_OPTNAME = 'signinInterfaceMiddlewareClass'
59    SIGNIN_INTERFACE_PREFIX = 'signinInterface.'
60   
61    AUTHKIT_COOKIE_SIGNOUTPATH_OPTNAME = 'authkit.cookie.signoutpath'
62    AUTHKIT_OPENID_TMPL_OPTNAME_PREFIX = 'authkit.openid.template.'
63    AUTHKIT_OPENID_TMPL_OBJ_OPTNAME = AUTHKIT_OPENID_TMPL_OPTNAME_PREFIX + 'obj'
64    AUTHKIT_OPENID_TMPL_STRING_OPTNAME = AUTHKIT_OPENID_TMPL_OPTNAME_PREFIX + \
65        'string'
66    AUTHKIT_OPENID_TMPL_FILE_OPTNAME = AUTHKIT_OPENID_TMPL_OPTNAME_PREFIX + \
67        'file'
68   
69    sslPropertyDefaults = {
70        IDP_WHITELIST_CONFIG_FILEPATH_OPTNAME: None
71    }
72    propertyDefaults = {
73        SIGNIN_INTERFACE_MIDDLEWARE_CLASS_OPTNAME: None,
74        'baseURL': ''
75    }
76    propertyDefaults.update(sslPropertyDefaults)
77    propertyDefaults.update(NDGSecurityMiddlewareBase.propertyDefaults)
78   
79    def __init__(self, app, global_conf, prefix=OPENID_RP_PREFIX, 
80                 **app_conf):
81        """Add AuthKit and Beaker middleware dependencies to WSGI stack and
82        set-up SSL Peer Certificate Authentication of OpenID Provider set by
83        the user
84       
85        @type app: callable following WSGI interface signature
86        @param app: next middleware application in the chain     
87        @type global_conf: dict       
88        @param global_conf: PasteDeploy application global configuration -
89        must follow format of propertyDefaults class variable
90        @type prefix: basestring
91        @param prefix: prefix for OpenID Relying Party configuration items
92        @type app_conf: dict
93        @param app_conf: application specific configuration - must follow
94        format of propertyDefaults class variable"""   
95
96        # Whitelisting of IDPs.  If no config file is set, no validation is
97        # executed
98        cls = OpenIDRelyingPartyMiddleware
99       
100        idpWhitelistConfigFilePath = app_conf.get(
101                            prefix + cls.IDP_WHITELIST_CONFIG_FILEPATH_OPTNAME)
102        if idpWhitelistConfigFilePath is not None:
103            self._initIdPValidation(idpWhitelistConfigFilePath)
104       
105        # Check for sign in template settings
106        if prefix+cls.SIGNIN_INTERFACE_MIDDLEWARE_CLASS_OPTNAME in app_conf:
107            if (cls.AUTHKIT_OPENID_TMPL_OBJ_OPTNAME in app_conf or 
108                cls.AUTHKIT_OPENID_TMPL_STRING_OPTNAME in app_conf or 
109                cls.AUTHKIT_OPENID_TMPL_FILE_OPTNAME in app_conf):
110               
111                log.warning("OpenID Relying Party %r setting overrides "
112                            "'%s*' AuthKit settings",
113                            cls.AUTHKIT_OPENID_TMPL_OPTNAME_PREFIX,
114                            cls.SIGNIN_INTERFACE_MIDDLEWARE_CLASS_OPTNAME)
115               
116            signinInterfacePrefix = prefix+cls.SIGNIN_INTERFACE_PREFIX
117           
118            className = app_conf[
119                        prefix + cls.SIGNIN_INTERFACE_MIDDLEWARE_CLASS_OPTNAME]
120            classProperties = {'prefix': signinInterfacePrefix}
121            classProperties.update(app_conf)
122           
123            app = instantiateClass(className, 
124                                   None, 
125                                   objectType=SigninInterface, 
126                                   classArgs=(app, global_conf),
127                                   classProperties=classProperties)           
128           
129            # Delete sign in interface middleware settings
130            for conf in app_conf, global_conf or {}:
131                for k in conf.keys():
132                    if k.startswith(signinInterfacePrefix):
133                        del conf[k]
134       
135            app_conf[
136                    cls.AUTHKIT_OPENID_TMPL_STRING_OPTNAME] = app.makeTemplate()
137               
138        self.signoutPath = app_conf.get(cls.AUTHKIT_COOKIE_SIGNOUTPATH_OPTNAME)
139
140        app = authkit.authenticate.middleware(app, app_conf)
141        _app = app
142        while True:
143            if isinstance(_app, AuthOpenIDHandler):
144                authOpenIDHandler = _app
145                self._authKitVerifyPath = authOpenIDHandler.path_verify
146                self._authKitProcessPath = authOpenIDHandler.path_process
147                break
148           
149            elif hasattr(_app, 'app'):
150                _app = _app.app
151            else:
152                break
153         
154        if not hasattr(self, '_authKitVerifyPath'):
155            raise OpenIDRelyingPartyConfigError("Error locating the AuthKit "
156                                                "AuthOpenIDHandler in the "
157                                                "WSGI stack")
158       
159        # Put this check in here after sessionKey has been set by the
160        # super class __init__ above
161        self.sessionKey = authOpenIDHandler.session_middleware
162           
163       
164        # Check for return to argument in query key value pairs
165        self._return2URIKey = AuthnRedirectMiddleware.RETURN2URI_ARGNAME + '='
166   
167        super(OpenIDRelyingPartyMiddleware, self).__init__(app, 
168                                                           global_conf, 
169                                                           prefix=prefix, 
170                                                           **app_conf)
171   
172    @NDGSecurityMiddlewareBase.initCall     
173    def __call__(self, environ, start_response):
174        '''
175        - Alter start_response to override the status code and force to 401.
176        This will enable non-browser based client code to bypass the OpenID
177        interface
178        - Manage AuthKit verify and process actions setting the referrer URI
179        to manage redirects correctly
180       
181        @type environ: dict
182        @param environ: WSGI environment variables dictionary
183        @type start_response: function
184        @param start_response: standard WSGI start response function
185        @rtype: iterable
186        @return: response
187        '''
188        # Skip Relying Party interface set-up if user has been authenticated
189        # by other middleware
190        if 'REMOTE_USER' in environ:
191            log.debug("Found REMOTE_USER=%s in environ, AuthKit "
192                      "based authentication has taken place in other "
193                      "middleware, skipping OpenID Relying Party interface" %
194                      environ['REMOTE_USER'])
195            return self._app(environ, start_response)
196
197        session = environ.get(self.sessionKey)
198        if session is None:
199            raise OpenIDRelyingPartyConfigError('No beaker session key "%s" '
200                                                'found in environ' % 
201                                                self.sessionKey)
202       
203        # Check for return to address in URI query args set by
204        # AuthnRedirectMiddleware in application code stack
205        params = dict(parse_querystring(environ))
206        quotedReferrer = params.get(AuthnRedirectMiddleware.RETURN2URI_ARGNAME,
207                                    '')
208       
209        referrer = urllib.unquote(quotedReferrer)
210        referrerPathInfo = urlsplit(referrer)[2]
211
212        if (referrer and 
213            not referrerPathInfo.endswith(self._authKitVerifyPath) and 
214            not referrerPathInfo.endswith(self._authKitProcessPath)):
215           
216            # An app has redirected to the Relying Party interface setting the
217            # special ndg.security.r query argument.  Subvert
218            # authkit.authenticate.open_id.AuthOpenIDHandler.process
219            # reassigning it's session 'referer' key to the URI specified in
220            # ndg.security.r in the request URI
221            session['referer'] = referrer
222            session.save()
223           
224        if self._return2URIKey in environ.get('HTTP_REFERER', ''):
225            # Remove return to arg to avoid interfering with AuthKit OpenID
226            # processing
227            splitURI = urlsplit(environ['HTTP_REFERER'])
228            query = splitURI[3]
229           
230            filteredQuery = '&'.join([arg for arg in query.split('&')
231                                if not arg.startswith(self._return2URIKey)])
232           
233            environ['HTTP_REFERER'] = urlunsplit(splitURI[:3] + \
234                                                 (filteredQuery,) + \
235                                                 splitURI[4:])
236                           
237        # See _start_response doc for an explanation...
238        if environ['PATH_INFO'] == self._authKitVerifyPath: 
239            def _start_response(status, header, exc_info=None):
240                '''Make OpenID Relying Party OpenID prompt page return a 401
241                status to signal to non-browser based clients that
242                authentication is required.  Requests are filtered on content
243                type so that static content such as graphics and style sheets
244                associated with the page are let through unaltered
245               
246                @type status: str
247                @param status: HTTP status code and status message
248                @type header: list
249                @param header: list of field, value tuple HTTP header content
250                @type exc_info: Exception
251                @param exc_info: exception info
252                '''
253                _status = status
254                for name, val in header:
255                    if (name.lower() == 'content-type' and 
256                        val.startswith('text/html')):
257                        _status = self.getStatusMessage(401)
258                        break
259                   
260                return start_response(_status, header, exc_info)
261        else:
262            _start_response = start_response
263
264        return self._app(environ, _start_response)
265
266    def _initIdPValidation(self, idpWhitelistConfigFilePath):
267        """Initialise M2Crypto based urllib2 HTTPS handler to enable SSL
268        authentication of OpenID Providers"""
269        log.info("Setting parameters for SSL Authentication of OpenID "
270                 "Provider ...")
271       
272        idPValidationDriver = SSLIdPValidationDriver(
273                                idpConfigFilePath=idpWhitelistConfigFilePath)
274           
275        # Force Python OpenID library to use Urllib2 fetcher instead of the
276        # Curl based one otherwise the M2Crypto SSL handler will be ignored.
277        setDefaultFetcher(Urllib2Fetcher())
278       
279        log.debug("Setting the M2Crypto SSL handler ...")
280       
281        opener = urllib2.OpenerDirector()           
282        opener.add_handler(FlagHttpsOnlyHandler())
283        opener.add_handler(HTTPSHandler(idPValidationDriver.ctx))
284       
285        urllib2.install_opener(opener)
286
287   
288class FlagHttpsOnlyHandler(urllib2.AbstractHTTPHandler):
289    '''Raise an exception for any other protocol than https'''
290    def unknown_open(self, req):
291        """Signal to caller that default handler is not supported"""
292        raise urllib2.URLError("Only HTTPS based OpenID Providers "
293                               "are supported")
294
295
296class SigninInterfaceError(Exception):
297    """Base class for SigninInterface exceptions
298   
299    A standard message is raised set by the msg class variable but the actual
300    exception details are logged to the error log.  The use of a standard
301    message enables callers to use its content for user error messages.
302   
303    @type msg: basestring
304    @cvar msg: standard message to be raised for this exception"""
305    userMsg = ("An internal error occurred with the page layout,  Please "
306               "contact your system administrator")
307    errorMsg = "SigninInterface error"
308   
309    def __init__(self, *arg, **kw):
310        if len(arg) > 0:
311            msg = arg[0]
312        else:
313            msg = self.__class__.errorMsg
314           
315        log.error(msg)
316        Exception.__init__(self, msg, **kw)
317       
318       
319class SigninInterfaceInitError(SigninInterfaceError):
320    """Error with initialisation of SigninInterface.  Raise from __init__"""
321    errorMsg = "SigninInterface initialisation error"
322   
323   
324class SigninInterfaceConfigError(SigninInterfaceError):
325    """Error with configuration settings.  Raise from __init__"""
326    errorMsg = "SigninInterface configuration error"   
327
328
329class SigninInterface(NDGSecurityMiddlewareBase):
330    """Base class for sign in rendering.  This is implemented as WSGI
331    middleware to enable additional middleware to be added into the call
332    stack e.g. StaticFileParser to enable rendering of graphics and other
333    static content in the Sign In page"""
334   
335    def getTemplateFunc(self):
336        """Return template function for AuthKit to render OpenID Relying
337        Party Sign in page"""
338        raise NotImplementedError()
339   
340    def __call__(self, environ, start_response):
341        return self._app(self, environ, start_response)
342
Note: See TracBrowser for help on using the repository browser.