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

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

Adding SSL Client authentication step into authz_lite integration test. Broken redirecting back from authn step to requested resource.

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