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

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

OpenID Relying Party flexible configuration

Fixed security WSGI configuration so that the OpenID Relying Party can run in the same middleware as the application it protects or independently in the security services middleware stack. There are two applications involved in applying security:

  1. the app to be secured
  2. app running security services


  1. is configured with middleware to intercept requests and apply the security policy. 2. runs services such as the Attribute Authority and OpenID Provider used by 1. The OpenID Relying Party can now be incorporated in either. For cases where an application runs in a different domain to the security services stack it's easier to deploy a Relying Party with the app in 1. as otherwise cookies set by the RP won't be in the scope of the secured app. 2. is useful for where the app is in the same domain as 2. and there's a need to run the RP over SSL.

Configurations can be set at deployment from Paste ini file pipeline settings.

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.return2URIArgName + '='
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.return2URIArgName,'')
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 parameter
251            will be set to 0
252            @type x509StoreCtx: M2Crypto.X509_Store_Context
253            @param x509StoreCtx: locate the certificate to be verified and perform
254            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 immediately
258            stopped with "verification failed" state. If SSL_VERIFY_PEER is set,
259            a verification failure alert is sent to the peer and the TLS/SSL
260            handshake is terminated.
261            - If verify_callback returns 1, the verification process is continued.
262            If verify_callback always returns 1, the TLS/SSL handshake will not be
263            terminated with respect to verification failures and the connection
264            will be established. The calling process can however retrieve the error
265            code of the last verification error using SSL_get_verify_result or
266            by maintaining its own error storage managed by verify_callback.
267            '''
268            if preVerifyOK == 0:
269                # Something is wrong with the certificate don't bother proceeding
270                # any further
271                log.error("verifyCallback: pre-verify OK flagged an error "
272                          "with the peer certificate, returning error state "
273                          "to caller ...")
274                return preVerifyOK
275           
276            x509Cert = x509StoreCtx.get_current_cert()
277            x509Cert.get_subject()
278            x509CertChain = x509StoreCtx.get1_chain()
279            for cert in x509CertChain:
280                subject = cert.get_subject()
281                dn = subject.as_text()
282                log.debug("verifyCallback: dn = %r", dn)
283               
284            # If all is OK preVerifyOK will be 1.  Return this to the caller to
285            # that it's OK to proceed
286            return preVerifyOK
287           
288        # Imports here so that if SSL Auth is not set the app will not need
289        # these packages
290        import urllib2
291        from M2Crypto import SSL
292        from M2Crypto.m2urllib2 import build_opener
293        from openid.fetchers import setDefaultFetcher, Urllib2Fetcher
294       
295        # Create a context specifying verification of the peer but with an
296        # additional callback function
297        ctx = SSL.Context()
298        ctx.set_verify(SSL.verify_peer|SSL.verify_fail_if_no_peer_cert, 
299                       9, 
300                       callback=verifySSLPeerCertCallback)
301
302        # Point to a directory containing CA certificates.  These must be named
303        # in their hashed form as expected by the OpenSSL API.  Use c_rehash
304        # utility to generate names or in the CA directory:
305        #
306        # $ for i in *.crt *.pem; do ln -s $i $(openssl x509 -hash -noout -in $i).0; done
307        ctx.load_verify_locations(capath=self.caCertDirPath)
308       
309        # Load this client's certificate and private key to enable the peer
310        # OpenID Provider to authenticate it
311        ctx.load_cert(self.certFilePath, 
312                      keyfile=self.priKeyFilePath, 
313                      callback=lambda *arg, **kw: self.priKeyPwd)
314   
315        # Force Python OpenID library to use Urllib2 fetcher instead of the
316        # Curl based one otherwise the M2Crypto SSL handler will be ignored.
317        setDefaultFetcher(Urllib2Fetcher())
318       
319        log.debug("Adding the M2Crypto SSL handler to urllib2's list of "
320                  "handlers...")
321        urllib2.install_opener(build_opener(ssl_context=ctx))
322   
323class SigninInterfaceError(Exception):
324    """Base class for SigninInterface exceptions
325   
326    A standard message is raised set by the msg class variable but the actual
327    exception details are logged to the error log.  The use of a standard
328    message enables callers to use its content for user error messages.
329   
330    @type msg: basestring
331    @cvar msg: standard message to be raised for this exception"""
332    userMsg = ("An internal error occurred with the page layout,  Please "
333               "contact your system administrator")
334    errorMsg = "SigninInterface error"
335   
336    def __init__(self, *arg, **kw):
337        if len(arg) > 0:
338            msg = arg[0]
339        else:
340            msg = self.__class__.errorMsg
341           
342        log.error(msg)
343        Exception.__init__(self, msg, **kw)
344       
345class SigninInterfaceInitError(SigninInterfaceError):
346    """Error with initialisation of SigninInterface.  Raise from __init__"""
347    errorMsg = "SigninInterface initialisation error"
348   
349class SigninInterfaceConfigError(SigninInterfaceError):
350    """Error with configuration settings.  Raise from __init__"""
351    errorMsg = "SigninInterface configuration error"   
352
353class SigninInterface(NDGSecurityMiddlewareBase):
354    """Base class for sign in rendering.  This is implemented as WSGI
355    middleware to enable additional middleware to be added into the call
356    stack e.g. StaticFileParser to enable rendering of graphics and other
357    static content in the Sign In page"""
358   
359    def getTemplateFunc(self):
360        """Return template function for AuthKit to render OpenID Relying
361        Party Sign in page"""
362        raise NotImplementedError()
363   
364    def __call__(self, environ, start_response):
365        return self._app(self, environ, start_response)
366
Note: See TracBrowser for help on using the repository browser.