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

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@5757
Revision 5757, 16.3 KB checked in by pjkersha, 10 years ago (diff)

Testing SSL Client Authentication middleware with session and redirect middleware to enable wget support for NDG Security.

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