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

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@5549
Revision 5549, 18.2 KB checked in by pjkersha, 11 years ago (diff)

Fixes for testing OpenID Relying Party running in the application code stack instead of the separate services stack:

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