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

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

Updated OpenID AX (Attribute Exchange) interface. Attributes passed over this interface are now stored in the authentication session at the Relying Party.

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        params = dict(parse_querystring(environ))
191        quotedReferrer = params.get(AuthnRedirectMiddleware.RETURN2URI_ARGNAME,
192                                    '')
193       
194        referrer = urllib.unquote(quotedReferrer)
195        referrerPathInfo = urlsplit(referrer)[2]
196
197        if referrer and \
198           not referrerPathInfo.endswith(self._authKitVerifyPath) and \
199           not referrerPathInfo.endswith(self._authKitProcessPath):
200            # Subvert authkit.authenticate.open_id.AuthOpenIDHandler.process
201            # reassigning it's session 'referer' key to the URI specified in
202            # the referrer query argument set in the request URI
203            session['referer'] = referrer
204            session.save()
205           
206        if self._return2URIKey in environ.get('HTTP_REFERER', ''):
207            # Remove return to arg to avoid interfering with AuthKit OpenID
208            # processing
209            splitURI = urlsplit(environ['HTTP_REFERER'])
210            query = splitURI[3]
211           
212            filteredQuery = '&'.join([arg for arg in query.split('&')
213                                if not arg.startswith(self._return2URIKey)])
214           
215            environ['HTTP_REFERER'] = urlunsplit(splitURI[:3] + \
216                                                 (filteredQuery,) + \
217                                                 splitURI[4:])
218                           
219        # See _start_response doc for an explanation...
220        if environ['PATH_INFO'] == self._authKitVerifyPath: 
221            def _start_response(status, header, exc_info=None):
222                '''Make OpenID Relying Party OpenID prompt page return a 401
223                status to signal to non-browser based clients that
224                authentication is required.  Requests are filtered on content
225                type so that static content such as graphics and style sheets
226                associated with the page are let through unaltered
227               
228                @type status: str
229                @param status: HTTP status code and status message
230                @type header: list
231                @param header: list of field, value tuple HTTP header content
232                @type exc_info: Exception
233                @param exc_info: exception info
234                '''
235                _status = status
236                for name, val in header:
237                    if name.lower() == 'content-type' and \
238                       val.startswith('text/html'):
239                        _status = self.getStatusMessage(401)
240                        break
241                   
242                return start_response(_status, header, exc_info)
243        else:
244            _start_response = start_response
245
246        return self._app(environ, _start_response)
247
248    def _initSSLPeerAuthN(self):
249        """Initialise M2Crypto based urllib2 HTTPS handler to enable SSL
250        authentication of OpenID Providers"""
251        log.info("Setting parameters for SSL Authentication of OpenID "
252                 "Provider ...")
253       
254        def verifySSLPeerCertCallback(preVerifyOK, x509StoreCtx):
255            '''SSL verify callback function used to control the behaviour when
256            the SSL_VERIFY_PEER flag is set
257           
258            http://www.openssl.org/docs/ssl/SSL_CTX_set_verify.html
259           
260            @type preVerifyOK: int
261            @param preVerifyOK: If a verification error is found, this
262            parameter will be set to 0
263            @type x509StoreCtx: M2Crypto.X509_Store_Context
264            @param x509StoreCtx: locate the certificate to be verified and
265            perform additional verification steps as needed
266            @rtype: int
267            @return: controls the strategy of the further verification process.
268            - If verify_callback returns 0, the verification process is
269            immediately stopped with "verification failed" state. If
270            SSL_VERIFY_PEER is set, a verification failure alert is sent to the
271            peer and the TLS/SSL handshake is terminated.
272            - If verify_callback returns 1, the verification process is
273            continued.
274            If verify_callback always returns 1, the TLS/SSL handshake will not
275            be terminated with respect to verification failures and the
276            connection
277            will be established. The calling process can however retrieve the
278            error code of the last verification error using
279            SSL_get_verify_result or by maintaining its own error storage
280            managed by verify_callback.
281            '''
282            if preVerifyOK == 0:
283                # Something is wrong with the certificate don't bother
284                # proceeding any further
285                log.error("verifyCallback: pre-verify OK flagged an error "
286                          "with the peer certificate, returning error state "
287                          "to caller ...")
288                return preVerifyOK
289           
290            x509Cert = x509StoreCtx.get_current_cert()
291            x509Cert.get_subject()
292            x509CertChain = x509StoreCtx.get1_chain()
293            for cert in x509CertChain:
294                subject = cert.get_subject()
295                dn = subject.as_text()
296                log.debug("verifyCallback: dn = %r", dn)
297               
298            # If all is OK preVerifyOK will be 1.  Return this to the caller to
299            # that it's OK to proceed
300            return preVerifyOK
301           
302        # Imports here so that if SSL Auth is not set the app will not need
303        # these packages
304        import urllib2
305        from M2Crypto import SSL
306        from M2Crypto.m2urllib2 import build_opener
307        from openid.fetchers import setDefaultFetcher, Urllib2Fetcher
308       
309        # Create a context specifying verification of the peer but with an
310        # additional callback function
311        ctx = SSL.Context()
312        ctx.set_verify(SSL.verify_peer|SSL.verify_fail_if_no_peer_cert, 
313                       9, 
314                       callback=verifySSLPeerCertCallback)
315
316        # Point to a directory containing CA certificates.  These must be named
317        # in their hashed form as expected by the OpenSSL API.  Use c_rehash
318        # utility to generate names or in the CA directory:
319        #
320        # $ for i in *.crt *.pem; do ln -s $i $(openssl x509 -hash -noout -in $i).0; done
321        ctx.load_verify_locations(capath=self.caCertDirPath)
322       
323        # Load this client's certificate and private key to enable the peer
324        # OpenID Provider to authenticate it
325        ctx.load_cert(self.certFilePath, 
326                      keyfile=self.priKeyFilePath, 
327                      callback=lambda *arg, **kw: self.priKeyPwd)
328   
329        # Force Python OpenID library to use Urllib2 fetcher instead of the
330        # Curl based one otherwise the M2Crypto SSL handler will be ignored.
331        setDefaultFetcher(Urllib2Fetcher())
332       
333        log.debug("Adding the M2Crypto SSL handler to urllib2's list of "
334                  "handlers...")
335        urllib2.install_opener(build_opener(ssl_context=ctx))
336   
337class SigninInterfaceError(Exception):
338    """Base class for SigninInterface exceptions
339   
340    A standard message is raised set by the msg class variable but the actual
341    exception details are logged to the error log.  The use of a standard
342    message enables callers to use its content for user error messages.
343   
344    @type msg: basestring
345    @cvar msg: standard message to be raised for this exception"""
346    userMsg = ("An internal error occurred with the page layout,  Please "
347               "contact your system administrator")
348    errorMsg = "SigninInterface error"
349   
350    def __init__(self, *arg, **kw):
351        if len(arg) > 0:
352            msg = arg[0]
353        else:
354            msg = self.__class__.errorMsg
355           
356        log.error(msg)
357        Exception.__init__(self, msg, **kw)
358       
359class SigninInterfaceInitError(SigninInterfaceError):
360    """Error with initialisation of SigninInterface.  Raise from __init__"""
361    errorMsg = "SigninInterface initialisation error"
362   
363class SigninInterfaceConfigError(SigninInterfaceError):
364    """Error with configuration settings.  Raise from __init__"""
365    errorMsg = "SigninInterface configuration error"   
366
367class SigninInterface(NDGSecurityMiddlewareBase):
368    """Base class for sign in rendering.  This is implemented as WSGI
369    middleware to enable additional middleware to be added into the call
370    stack e.g. StaticFileParser to enable rendering of graphics and other
371    static content in the Sign In page"""
372   
373    def getTemplateFunc(self):
374        """Return template function for AuthKit to render OpenID Relying
375        Party Sign in page"""
376        raise NotImplementedError()
377   
378    def __call__(self, environ, start_response):
379        return self._app(self, environ, start_response)
380
Note: See TracBrowser for help on using the repository browser.