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

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

Added code for validation of OpenID Provider by Relying Party using M2Crypto.m2urllib2 for SSL peer authN

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            moduleName, className = \
103                app_conf[prefix+'signinInterfaceMiddlewareClass'].rsplit('.',1)
104           
105            signinInterfacePrefix = prefix+'signinInterface.'
106            classProperties = {'prefix': signinInterfacePrefix}
107            classProperties.update(app_conf)
108            app = instantiateClass(moduleName, 
109                                   className, 
110                                   objectType=SigninInterface, 
111                                   classArgs=(app, global_conf),
112                                   classProperties=classProperties)           
113           
114            # Delete sign in interface middleware settings
115            for conf in app_conf, global_conf or {}:
116                for k in conf.keys():
117                    if k.startswith(signinInterfacePrefix):
118                        del conf[k]
119       
120            app_conf['authkit.openid.template.string'] = app.makeTemplate()
121               
122        self.signoutPath = app_conf.get('authkit.cookie.signoutpath')
123
124        authKitApp = authkit.authenticate.middleware(app, app_conf)
125        _app = authKitApp
126        while True:
127            if isinstance(_app,authkit.authenticate.open_id.AuthOpenIDHandler):
128                self._authKitVerifyPath = _app.path_verify
129                self._authKitProcessPath = _app.path_process
130                break
131           
132            elif hasattr(_app, 'app'):
133                _app = _app.app
134            else:
135                break
136         
137        if not hasattr(self, '_authKitVerifyPath'):
138            raise OpenIDRelyingPartyConfigError("Error locating the AuthKit "
139                                                "AuthOpenIDHandler in the "
140                                                "WSGI stack")
141       
142        # Check for return to argument in query key value pairs
143        self._return2URIKey = AuthNRedirectMiddleware.return2URIArgName + '='
144   
145        super(OpenIDRelyingPartyMiddleware, self).__init__(authKitApp, 
146                                                           global_conf, 
147                                                           prefix=prefix, 
148                                                           **app_conf)
149
150       
151    @NDGSecurityMiddlewareBase.initCall     
152    def __call__(self, environ, start_response):
153        '''
154        - Alter start_response to override the status code and force to 401.
155        This will enable non-browser based client code to bypass the OpenID
156        interface
157        - Manage AuthKit verify and process actions setting the referrer URI
158        to manage redirects
159       
160        @type environ: dict
161        @param environ: WSGI environment variables dictionary
162        @type start_response: function
163        @param start_response: standard WSGI start response function
164        @rtype: iterable
165        @return: response
166        '''
167        session = environ[self.sessionKey]
168       
169        # Check for return to address in URI query args set by
170        # AuthNRedirectMiddleware in application code stack
171        if environ['REQUEST_METHOD'] == "GET":
172            params = dict(parse_querystring(environ))
173        else:
174            params = {}
175       
176        quotedReferrer=params.get(AuthNRedirectMiddleware.return2URIArgName,'')
177        referrer = urllib.unquote(quotedReferrer)
178        referrerPathInfo = urlsplit(referrer)[2]
179
180        if referrer and \
181           not referrerPathInfo.endswith(self._authKitVerifyPath) and \
182           not referrerPathInfo.endswith(self._authKitProcessPath):
183            # Subvert authkit.authenticate.open_id.AuthOpenIDHandler.process
184            # reassigning it's session 'referer' key to the URI specified in
185            # the referrer query argument set in the request URI
186            session['referer'] = referrer
187            session.save()
188           
189        if self._return2URIKey in environ.get('HTTP_REFERER', ''):
190            # Remove return to arg to avoid interfering with AuthKit OpenID
191            # processing
192            splitURI = urlsplit(environ['HTTP_REFERER'])
193            query = splitURI[3]
194           
195            filteredQuery = '&'.join([arg for arg in query.split('&')
196                                if not arg.startswith(self._return2URIKey)])
197           
198            environ['HTTP_REFERER'] = urlunsplit(splitURI[:3] + \
199                                                 (filteredQuery,) + \
200                                                 splitURI[4:])
201
202        if self.signoutPath is not None and self.pathInfo == self.signoutPath:
203            # Redirect to referrer ...
204            referrer = session.get(
205                    'ndg.security.server.wsgi.openid.relyingparty.referer')
206            if referrer is not None:
207                def setRedirectResponse(status, header, exc_info=None):
208                    header.extend([('Location', referrer)])
209                    return start_response(self.getStatusMessage(302), 
210                                          header,
211                                          exc_info)
212                   
213                return self._app(environ, setRedirectResponse)
214            else:
215                log.debug('No referrer set for redirect following logout')
216               
217        # Set a return to address following logout. 
218        # TODO: This code will need to be refactored if this middleware is
219        # deployed externally via a proxy - HTTP_REFERER will be the internal
220        # URI instead of the one exposed outside
221        if 'HTTP_REFERER' in environ:
222            session['ndg.security.server.wsgi.openid.relyingparty.referer'] = \
223                environ['HTTP_REFERER']
224            session.save()
225       
226        # See _start_response doc for an explanation...
227        if environ['PATH_INFO'] == self._authKitVerifyPath: 
228            def _start_response(status, header, exc_info=None):
229                '''Make OpenID Relying Party OpenID prompt page return a 401
230                status to signal to non-browser based clients that
231                authentication is required.  Requests are filtered on content
232                type so that static content such as graphics and style sheets
233                associated with the page are let through unaltered
234               
235                @type status: str
236                @param status: HTTP status code and status message
237                @type header: list
238                @param header: list of field, value tuple HTTP header content
239                @type exc_info: Exception
240                @param exc_info: exception info
241                '''
242                _status = status
243                for name, val in header:
244                    if name.lower() == 'content-type' and \
245                       val.startswith('text/html'):
246                        _status = self.getStatusMessage(401)
247                        break
248                   
249                return start_response(_status, header, exc_info)
250        else:
251            _start_response = start_response
252
253        return self._app(environ, _start_response)
254
255    def _initSSLPeerAuthN(self):
256        """Initialise M2Crypto based urllib2 HTTPS handler to enable SSL
257        authentication of OpenID Providers"""
258        log.info("Setting parameters for SSL Authentication of OpenID "
259                 "Provider ...")
260       
261        def verifySSLPeerCertCallback(preVerifyOK, x509StoreCtx):
262            '''SSL verify callback function used to control the behaviour when
263            the SSL_VERIFY_PEER flag is set
264           
265            http://www.openssl.org/docs/ssl/SSL_CTX_set_verify.html
266           
267            @type preVerifyOK: int
268            @param preVerifyOK: If a verification error is found, this parameter
269            will be set to 0
270            @type x509StoreCtx: M2Crypto.X509_Store_Context
271            @param x509StoreCtx: locate the certificate to be verified and perform
272            additional verification steps as needed
273            @rtype: int
274            @return: controls the strategy of the further verification process.
275            - If verify_callback returns 0, the verification process is immediately
276            stopped with "verification failed" state. If SSL_VERIFY_PEER is set,
277            a verification failure alert is sent to the peer and the TLS/SSL
278            handshake is terminated.
279            - If verify_callback returns 1, the verification process is continued.
280            If verify_callback always returns 1, the TLS/SSL handshake will not be
281            terminated with respect to verification failures and the connection
282            will be established. The calling process can however retrieve the error
283            code of the last verification error using SSL_get_verify_result or
284            by maintaining its own error storage managed by verify_callback.
285            '''
286            if preVerifyOK == 0:
287                # Something is wrong with the certificate don't bother proceeding
288                # any further
289                log.error("verifyCallback: pre-verify OK flagged an error "
290                          "with the peer certificate, returning error state "
291                          "to caller ...")
292                return preVerifyOK
293           
294            x509Cert = x509StoreCtx.get_current_cert()
295            x509Cert.get_subject()
296            x509CertChain = x509StoreCtx.get1_chain()
297            for cert in x509CertChain:
298                subject = cert.get_subject()
299                dn = subject.as_text()
300                log.debug("verifyCallback: dn = %r", dn)
301               
302            # If all is OK preVerifyOK will be 1.  Return this to the caller to
303            # that it's OK to proceed
304            return preVerifyOK
305           
306        # Imports here so that if SSL Auth is not set the app will not need
307        # these packages
308        import urllib2
309        from M2Crypto import SSL
310        from M2Crypto.m2urllib2 import build_opener
311        from openid.fetchers import setDefaultFetcher, Urllib2Fetcher
312       
313        # Create a context specifying verification of the peer but with an
314        # additional callback function
315        ctx = SSL.Context()
316        ctx.set_verify(SSL.verify_peer|SSL.verify_fail_if_no_peer_cert, 
317                       9, 
318                       callback=verifySSLPeerCertCallback)
319
320        # Point to a directory containing CA certificates.  These must be named
321        # in their hashed form as expected by the OpenSSL API.  Use c_rehash
322        # utility to generate names or in the CA directory:
323        #
324        # $ for i in *.crt *.pem; do ln -s $i $(openssl x509 -hash -noout -in $i).0; done
325        ctx.load_verify_locations(capath=self.caCertDirPath)
326       
327        # Load this client's certificate and private key to enable the peer
328        # OpenID Provider to authenticate it
329        ctx.load_cert(self.certFilePath, 
330                      keyfile=self.priKeyFilePath, 
331                      callback=lambda *arg, **kw: self.priKeyPwd)
332   
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.