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

Subversion URL: http://proj.badc.rl.ac.uk/svn/ndg-security/TI12-security/trunk/NDGSecurity/python/ndg_security_server/ndg/security/server/wsgi/openid/relyingparty/__init__.py@6276
Revision 6276, 17.2 KB checked in by pjkersha, 11 years ago (diff)
  • ndg.security.server.wsgi.NDGSecurityMiddlewareBase: change code to leave trailing space in path info attribute
  • ndg.security.server.wsgi.openid.provider.OpenIDProviderMiddleware:
    • added properties with type checking
    • added trustedRelyingParties attribute - a list of sites trusted by the Provider for which no decide page interface is invoked.
  • ndg.security.server.wsgi.openid.provider.axinterface.csv: fixed adding of AX attributes for CSV class. Required attributes were being set twice.
  • ndg.security.server.wsgi.openid.relyingparty.OpenIDRelyingParty:
    • changed whitelisting to use ndg.security.server.wsgi.openid.relyingparty.validation.SSLIdPValidationDriver class. FIXME: Fix required in SSL verify callback needed to check only the server certificate in the verification chain and ignore CA certificates.
  • ndg.security.server.wsgi.openid.relyingparty.validation.SSLIdPValidationDriver: changed to init to include keyword option for installing default urllib2 opener and defaulting to OMIT it.
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    sslPropertyDefaults = {
57        'idpWhitelistConfigFilePath': None
58    }
59    propertyDefaults = {
60        'signinInterfaceMiddlewareClass': None,
61        'baseURL': ''
62    }
63    propertyDefaults.update(sslPropertyDefaults)
64    propertyDefaults.update(NDGSecurityMiddlewareBase.propertyDefaults)
65   
66    def __init__(self, app, global_conf, prefix='openid.relyingparty.', 
67                 **app_conf):
68        """Add AuthKit and Beaker middleware dependencies to WSGI stack and
69        set-up SSL Peer Certificate Authentication of OpenID Provider set by
70        the user
71       
72        @type app: callable following WSGI interface signature
73        @param app: next middleware application in the chain     
74        @type global_conf: dict       
75        @param global_conf: PasteDeploy application global configuration -
76        must follow format of propertyDefaults class variable
77        @type prefix: basestring
78        @param prefix: prefix for OpenID Relying Party configuration items
79        @type app_conf: dict
80        @param app_conf: application specific configuration - must follow
81        format of propertyDefaults class variable"""   
82
83        # Whitelisting of IDPs.  If no config file is set, no validation is
84        # executed
85        idpWhitelistConfigFilePath = app_conf.get(
86                                        prefix + 'idpWhitelistConfigFilePath')
87        if idpWhitelistConfigFilePath is not None:
88            self._initIdPValidation(idpWhitelistConfigFilePath)
89       
90        # Check for sign in template settings
91        if prefix+'signinInterfaceMiddlewareClass' in app_conf:
92            if 'authkit.openid.template.obj' in app_conf or \
93               'authkit.openid.template.string' in app_conf or \
94               'authkit.openid.template.file' in app_conf:
95                log.warning("OpenID Relying Party "
96                            "'signinInterfaceMiddlewareClass' "
97                            "setting overrides 'authkit.openid.template.*' "
98                            "AuthKit settings")
99               
100            signinInterfacePrefix = prefix+'signinInterface.'
101            classProperties = {'prefix': signinInterfacePrefix}
102            classProperties.update(app_conf)
103            app = instantiateClass(
104                           app_conf[prefix+'signinInterfaceMiddlewareClass'], 
105                           None, 
106                           objectType=SigninInterface, 
107                           classArgs=(app, global_conf),
108                           classProperties=classProperties)           
109           
110            # Delete sign in interface middleware settings
111            for conf in app_conf, global_conf or {}:
112                for k in conf.keys():
113                    if k.startswith(signinInterfacePrefix):
114                        del conf[k]
115       
116            app_conf['authkit.openid.template.string'] = app.makeTemplate()
117               
118        self.signoutPath = app_conf.get('authkit.cookie.signoutpath')
119
120        app = authkit.authenticate.middleware(app, app_conf)
121        _app = app
122        while True:
123            if isinstance(_app, AuthOpenIDHandler):
124                authOpenIDHandler = _app
125                self._authKitVerifyPath = authOpenIDHandler.path_verify
126                self._authKitProcessPath = authOpenIDHandler.path_process
127                break
128           
129            elif hasattr(_app, 'app'):
130                _app = _app.app
131            else:
132                break
133         
134        if not hasattr(self, '_authKitVerifyPath'):
135            raise OpenIDRelyingPartyConfigError("Error locating the AuthKit "
136                                                "AuthOpenIDHandler in the "
137                                                "WSGI stack")
138       
139        # Put this check in here after sessionKey has been set by the
140        # super class __init__ above
141        self.sessionKey = authOpenIDHandler.session_middleware
142           
143       
144        # Check for return to argument in query key value pairs
145        self._return2URIKey = AuthnRedirectMiddleware.RETURN2URI_ARGNAME + '='
146   
147        super(OpenIDRelyingPartyMiddleware, self).__init__(app, 
148                                                           global_conf, 
149                                                           prefix=prefix, 
150                                                           **app_conf)
151   
152    @NDGSecurityMiddlewareBase.initCall     
153    def __call__(self, environ, start_response):
154        '''
155        - Alter start_response to override the status code and force to 401.
156        This will enable non-browser based client code to bypass the OpenID
157        interface
158        - Manage AuthKit verify and process actions setting the referrer URI
159        to manage redirects
160       
161        @type environ: dict
162        @param environ: WSGI environment variables dictionary
163        @type start_response: function
164        @param start_response: standard WSGI start response function
165        @rtype: iterable
166        @return: response
167        '''
168        # Skip Relying Party interface set-up if user has been authenticated
169        # by other middleware
170        if 'REMOTE_USER' in environ:
171            log.debug("Found REMOTE_USER=%s in environ, AuthKit "
172                      "based authentication has taken place in other "
173                      "middleware, skipping OpenID Relying Party interface" %
174                      environ['REMOTE_USER'])
175            return self._app(environ, start_response)
176
177        session = environ.get(self.sessionKey)
178        if session is None:
179            raise OpenIDRelyingPartyConfigError('No beaker session key "%s" '
180                                                'found in environ' % 
181                                                self.sessionKey)
182       
183        # Check for return to address in URI query args set by
184        # AuthnRedirectMiddleware in application code stack
185        params = dict(parse_querystring(environ))
186        quotedReferrer = params.get(AuthnRedirectMiddleware.RETURN2URI_ARGNAME,
187                                    '')
188       
189        referrer = urllib.unquote(quotedReferrer)
190        referrerPathInfo = urlsplit(referrer)[2]
191
192        if (referrer and 
193            not referrerPathInfo.endswith(self._authKitVerifyPath) and 
194            not referrerPathInfo.endswith(self._authKitProcessPath)):
195            # Subvert authkit.authenticate.open_id.AuthOpenIDHandler.process
196            # reassigning it's session 'referer' key to the URI specified in
197            # the referrer query argument set in the request URI
198            session['referer'] = referrer
199            session.save()
200           
201        if self._return2URIKey in environ.get('HTTP_REFERER', ''):
202            # Remove return to arg to avoid interfering with AuthKit OpenID
203            # processing
204            splitURI = urlsplit(environ['HTTP_REFERER'])
205            query = splitURI[3]
206           
207            filteredQuery = '&'.join([arg for arg in query.split('&')
208                                if not arg.startswith(self._return2URIKey)])
209           
210            environ['HTTP_REFERER'] = urlunsplit(splitURI[:3] + \
211                                                 (filteredQuery,) + \
212                                                 splitURI[4:])
213                           
214        # See _start_response doc for an explanation...
215        if environ['PATH_INFO'] == self._authKitVerifyPath: 
216            def _start_response(status, header, exc_info=None):
217                '''Make OpenID Relying Party OpenID prompt page return a 401
218                status to signal to non-browser based clients that
219                authentication is required.  Requests are filtered on content
220                type so that static content such as graphics and style sheets
221                associated with the page are let through unaltered
222               
223                @type status: str
224                @param status: HTTP status code and status message
225                @type header: list
226                @param header: list of field, value tuple HTTP header content
227                @type exc_info: Exception
228                @param exc_info: exception info
229                '''
230                _status = status
231                for name, val in header:
232                    if name.lower() == 'content-type' and \
233                       val.startswith('text/html'):
234                        _status = self.getStatusMessage(401)
235                        break
236                   
237                return start_response(_status, header, exc_info)
238        else:
239            _start_response = start_response
240
241        return self._app(environ, _start_response)
242
243    def _initIdPValidation(self, idpWhitelistConfigFilePath):
244        """Initialise M2Crypto based urllib2 HTTPS handler to enable SSL
245        authentication of OpenID Providers"""
246        log.info("Setting parameters for SSL Authentication of OpenID "
247                 "Provider ...")
248       
249        idPValidationDriver = SSLIdPValidationDriver(
250                                idpConfigFilePath=idpWhitelistConfigFilePath)
251       
252#        def verifySSLPeerCertCallback(preVerifyOK, x509StoreCtx):
253#            '''SSL verify callback function used to control the behaviour when
254#            the SSL_VERIFY_PEER flag is set
255#           
256#            http://www.openssl.org/docs/ssl/SSL_CTX_set_verify.html
257#           
258#            @type preVerifyOK: int
259#            @param preVerifyOK: If a verification error is found, this
260#            parameter will be set to 0
261#            @type x509StoreCtx: M2Crypto.X509_Store_Context
262#            @param x509StoreCtx: locate the certificate to be verified and
263#            perform additional verification steps as needed
264#            @rtype: int
265#            @return: controls the strategy of the further verification process.
266#            - If verify_callback returns 0, the verification process is
267#            immediately stopped with "verification failed" state. If
268#            SSL_VERIFY_PEER is set, a verification failure alert is sent to the
269#            peer and the TLS/SSL handshake is terminated.
270#            - If verify_callback returns 1, the verification process is
271#            continued.
272#            If verify_callback always returns 1, the TLS/SSL handshake will not
273#            be terminated with respect to verification failures and the
274#            connection
275#            will be established. The calling process can however retrieve the
276#            error code of the last verification error using
277#            SSL_get_verify_result or by maintaining its own error storage
278#            managed by verify_callback.
279#            '''
280#            if preVerifyOK == 0:
281#                # Something is wrong with the certificate don't bother
282#                # proceeding any further
283#                log.error("verifyCallback: pre-verify OK flagged an error "
284#                          "with the peer certificate, returning error state "
285#                          "to caller ...")
286#                return preVerifyOK
287#           
288#            x509Cert = x509StoreCtx.get_current_cert()
289#            x509Cert.get_subject()
290#            x509CertChain = x509StoreCtx.get1_chain()
291#            for cert in x509CertChain:
292#                subject = cert.get_subject()
293#                dn = subject.as_text()
294#                log.debug("verifyCallback: dn = %r", dn)
295#               
296#            # If all is OK preVerifyOK will be 1.  Return this to the caller to
297#            # that it's OK to proceed
298#            return preVerifyOK
299#           
300#       
301#        # Create a context specifying verification of the peer but with an
302#        # additional callback function
303#        ctx = SSL.Context()
304#        ctx.set_verify(SSL.verify_peer|SSL.verify_fail_if_no_peer_cert,
305#                       9,
306#                       callback=verifySSLPeerCertCallback)
307#
308#        # Point to a directory containing CA certificates.  These must be named
309#        # in their hashed form as expected by the OpenSSL API.  Use c_rehash
310#        # utility to generate names or in the CA directory:
311#        #
312#        # $ for i in *.crt *.pem; do ln -s $i $(openssl x509 -hash -noout -in $i).0; done
313#        ctx.load_verify_locations(capath=self.caCertDirPath)
314#       
315#        # Load this client's certificate and private key to enable the peer
316#        # OpenID Provider to authenticate it
317#        ctx.load_cert(self.certFilePath,
318#                      keyfile=self.priKeyFilePath,
319#                      callback=lambda *arg, **kw: self.priKeyPwd)
320   
321        # Force Python OpenID library to use Urllib2 fetcher instead of the
322        # Curl based one otherwise the M2Crypto SSL handler will be ignored.
323        setDefaultFetcher(Urllib2Fetcher())
324       
325#        log.debug("Adding the M2Crypto SSL handler to urllib2's list of "
326#                  "handlers...")
327#        urllib2.install_opener(build_opener(ssl_context=ctx))
328        log.debug("Setting the M2Crypto SSL handler ...")
329       
330        opener = urllib2.OpenerDirector()           
331        opener.add_handler(FlagHttpsOnlyHandler())
332        opener.add_handler(HTTPSHandler(idPValidationDriver.ctx))
333       
334        urllib2.install_opener(opener)
335
336   
337class FlagHttpsOnlyHandler(urllib2.AbstractHTTPHandler):
338    '''Raise an exception for any other protocol than https'''
339    def unknown_open(self, req):
340        """Signal to caller that default handler is not supported"""
341        raise urllib2.URLError("Only HTTPS based OpenID Providers "
342                               "are supported")
343
344
345class SigninInterfaceError(Exception):
346    """Base class for SigninInterface exceptions
347   
348    A standard message is raised set by the msg class variable but the actual
349    exception details are logged to the error log.  The use of a standard
350    message enables callers to use its content for user error messages.
351   
352    @type msg: basestring
353    @cvar msg: standard message to be raised for this exception"""
354    userMsg = ("An internal error occurred with the page layout,  Please "
355               "contact your system administrator")
356    errorMsg = "SigninInterface error"
357   
358    def __init__(self, *arg, **kw):
359        if len(arg) > 0:
360            msg = arg[0]
361        else:
362            msg = self.__class__.errorMsg
363           
364        log.error(msg)
365        Exception.__init__(self, msg, **kw)
366       
367class SigninInterfaceInitError(SigninInterfaceError):
368    """Error with initialisation of SigninInterface.  Raise from __init__"""
369    errorMsg = "SigninInterface initialisation error"
370   
371class SigninInterfaceConfigError(SigninInterfaceError):
372    """Error with configuration settings.  Raise from __init__"""
373    errorMsg = "SigninInterface configuration error"   
374
375class SigninInterface(NDGSecurityMiddlewareBase):
376    """Base class for sign in rendering.  This is implemented as WSGI
377    middleware to enable additional middleware to be added into the call
378    stack e.g. StaticFileParser to enable rendering of graphics and other
379    static content in the Sign In page"""
380   
381    def getTemplateFunc(self):
382        """Return template function for AuthKit to render OpenID Relying
383        Party Sign in page"""
384        raise NotImplementedError()
385   
386    def __call__(self, environ, start_response):
387        return self._app(self, environ, start_response)
388
Note: See TracBrowser for help on using the repository browser.