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

Subversion URL: http://proj.badc.rl.ac.uk/svn/ndg/TI12-security/trunk/NDGSecurity/python/ndg_security_server/ndg/security/server/wsgi/openid/relyingparty/__init__.py@6354
Revision 6354, 13.6 KB checked in by pjkersha, 10 years ago (diff)

Old Pylons SSO code moved to separate branch in trunk

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
18import urllib2 # SSL based whitelisting
19from urlparse import urlsplit, urlunsplit
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
26# SSL based whitelisting
27from M2Crypto import SSL
28from M2Crypto.m2urllib2 import build_opener, HTTPSHandler
29from openid.fetchers import setDefaultFetcher, Urllib2Fetcher
30
31from ndg.security.common.utils.classfactory import instantiateClass
32from ndg.security.server.wsgi import NDGSecurityMiddlewareBase
33from ndg.security.server.wsgi.authn import AuthnRedirectMiddleware
34from ndg.security.server.wsgi.openid.relyingparty.validation import (
35                                                        SSLIdPValidationDriver)
36
37
38class OpenIDRelyingPartyMiddlewareError(Exception):
39    """OpenID Relying Party WSGI Middleware Error"""
40
41
42class OpenIDRelyingPartyConfigError(OpenIDRelyingPartyMiddlewareError):
43    """OpenID Relying Party Configuration Error"""
44 
45
46class OpenIDRelyingPartyMiddleware(NDGSecurityMiddlewareBase):
47    '''OpenID Relying Party middleware which wraps the AuthKit implementation.
48    This middleware is to be hosted in it's own security middleware stack.
49    WSGI middleware applications to be protected can be hosted in a separate
50    stack.  The AuthnRedirectMiddleware filter can respond to a HTTP
51    401 response from this stack and redirect to this middleware to initiate
52    OpenID based sign in.  AuthnRedirectMiddleware passes a query
53    argument in its request containing the URI return address for this
54    middleware to return to following OpenID sign in.
55    '''
56    sslPropertyDefaults = {
57        'idpWhitelistConfigFilePath': None
58    }
59    propertyDefaults = {
60        'signinInterfaceMiddlewareClass': None,
61        'baseURL': ''
62    }
63    propertyDefaults.update(sslPropertyDefaults)
64    propertyDefaults.update(NDGSecurityMiddlewareBase.propertyDefaults)
65   
66    def __init__(self, app, global_conf, prefix='openid.relyingparty.', 
67                 **app_conf):
68        """Add AuthKit and Beaker middleware dependencies to WSGI stack and
69        set-up SSL Peer Certificate Authentication of OpenID Provider set by
70        the user
71       
72        @type app: callable following WSGI interface signature
73        @param app: next middleware application in the chain     
74        @type global_conf: dict       
75        @param global_conf: PasteDeploy application global configuration -
76        must follow format of propertyDefaults class variable
77        @type prefix: basestring
78        @param prefix: prefix for OpenID Relying Party configuration items
79        @type app_conf: dict
80        @param app_conf: application specific configuration - must follow
81        format of propertyDefaults class variable"""   
82
83        # Whitelisting of IDPs.  If no config file is set, no validation is
84        # executed
85        idpWhitelistConfigFilePath = app_conf.get(
86                                        prefix + 'idpWhitelistConfigFilePath')
87        if idpWhitelistConfigFilePath is not None:
88            self._initIdPValidation(idpWhitelistConfigFilePath)
89       
90        # Check for sign in template settings
91        if prefix+'signinInterfaceMiddlewareClass' in app_conf:
92            if 'authkit.openid.template.obj' in app_conf or \
93               'authkit.openid.template.string' in app_conf or \
94               'authkit.openid.template.file' in app_conf:
95                log.warning("OpenID Relying Party "
96                            "'signinInterfaceMiddlewareClass' "
97                            "setting overrides 'authkit.openid.template.*' "
98                            "AuthKit settings")
99               
100            signinInterfacePrefix = prefix+'signinInterface.'
101            classProperties = {'prefix': signinInterfacePrefix}
102            classProperties.update(app_conf)
103            app = instantiateClass(
104                           app_conf[prefix+'signinInterfaceMiddlewareClass'], 
105                           None, 
106                           objectType=SigninInterface, 
107                           classArgs=(app, global_conf),
108                           classProperties=classProperties)           
109           
110            # Delete sign in interface middleware settings
111            for conf in app_conf, global_conf or {}:
112                for k in conf.keys():
113                    if k.startswith(signinInterfacePrefix):
114                        del conf[k]
115       
116            app_conf['authkit.openid.template.string'] = app.makeTemplate()
117               
118        self.signoutPath = app_conf.get('authkit.cookie.signoutpath')
119
120        app = authkit.authenticate.middleware(app, app_conf)
121        _app = app
122        while True:
123            if isinstance(_app, AuthOpenIDHandler):
124                authOpenIDHandler = _app
125                self._authKitVerifyPath = authOpenIDHandler.path_verify
126                self._authKitProcessPath = authOpenIDHandler.path_process
127                break
128           
129            elif hasattr(_app, 'app'):
130                _app = _app.app
131            else:
132                break
133         
134        if not hasattr(self, '_authKitVerifyPath'):
135            raise OpenIDRelyingPartyConfigError("Error locating the AuthKit "
136                                                "AuthOpenIDHandler in the "
137                                                "WSGI stack")
138       
139        # Put this check in here after sessionKey has been set by the
140        # super class __init__ above
141        self.sessionKey = authOpenIDHandler.session_middleware
142           
143       
144        # Check for return to argument in query key value pairs
145        self._return2URIKey = AuthnRedirectMiddleware.RETURN2URI_ARGNAME + '='
146   
147        super(OpenIDRelyingPartyMiddleware, self).__init__(app, 
148                                                           global_conf, 
149                                                           prefix=prefix, 
150                                                           **app_conf)
151   
152    @NDGSecurityMiddlewareBase.initCall     
153    def __call__(self, environ, start_response):
154        '''
155        - Alter start_response to override the status code and force to 401.
156        This will enable non-browser based client code to bypass the OpenID
157        interface
158        - Manage AuthKit verify and process actions setting the referrer URI
159        to manage redirects
160       
161        @type environ: dict
162        @param environ: WSGI environment variables dictionary
163        @type start_response: function
164        @param start_response: standard WSGI start response function
165        @rtype: iterable
166        @return: response
167        '''
168        # Skip Relying Party interface set-up if user has been authenticated
169        # by other middleware
170        if 'REMOTE_USER' in environ:
171            log.debug("Found REMOTE_USER=%s in environ, AuthKit "
172                      "based authentication has taken place in other "
173                      "middleware, skipping OpenID Relying Party interface" %
174                      environ['REMOTE_USER'])
175            return self._app(environ, start_response)
176
177        session = environ.get(self.sessionKey)
178        if session is None:
179            raise OpenIDRelyingPartyConfigError('No beaker session key "%s" '
180                                                'found in environ' % 
181                                                self.sessionKey)
182       
183        # Check for return to address in URI query args set by
184        # AuthnRedirectMiddleware in application code stack
185        params = dict(parse_querystring(environ))
186        quotedReferrer = params.get(AuthnRedirectMiddleware.RETURN2URI_ARGNAME,
187                                    '')
188       
189        referrer = urllib.unquote(quotedReferrer)
190        referrerPathInfo = urlsplit(referrer)[2]
191
192        if (referrer and 
193            not referrerPathInfo.endswith(self._authKitVerifyPath) and 
194            not referrerPathInfo.endswith(self._authKitProcessPath)):
195            # Subvert authkit.authenticate.open_id.AuthOpenIDHandler.process
196            # reassigning it's session 'referer' key to the URI specified in
197            # the referrer query argument set in the request URI
198            session['referer'] = referrer
199            session.save()
200           
201        if self._return2URIKey in environ.get('HTTP_REFERER', ''):
202            # Remove return to arg to avoid interfering with AuthKit OpenID
203            # processing
204            splitURI = urlsplit(environ['HTTP_REFERER'])
205            query = splitURI[3]
206           
207            filteredQuery = '&'.join([arg for arg in query.split('&')
208                                if not arg.startswith(self._return2URIKey)])
209           
210            environ['HTTP_REFERER'] = urlunsplit(splitURI[:3] + \
211                                                 (filteredQuery,) + \
212                                                 splitURI[4:])
213                           
214        # See _start_response doc for an explanation...
215        if environ['PATH_INFO'] == self._authKitVerifyPath: 
216            def _start_response(status, header, exc_info=None):
217                '''Make OpenID Relying Party OpenID prompt page return a 401
218                status to signal to non-browser based clients that
219                authentication is required.  Requests are filtered on content
220                type so that static content such as graphics and style sheets
221                associated with the page are let through unaltered
222               
223                @type status: str
224                @param status: HTTP status code and status message
225                @type header: list
226                @param header: list of field, value tuple HTTP header content
227                @type exc_info: Exception
228                @param exc_info: exception info
229                '''
230                _status = status
231                for name, val in header:
232                    if name.lower() == 'content-type' and \
233                       val.startswith('text/html'):
234                        _status = self.getStatusMessage(401)
235                        break
236                   
237                return start_response(_status, header, exc_info)
238        else:
239            _start_response = start_response
240
241        return self._app(environ, _start_response)
242
243    def _initIdPValidation(self, idpWhitelistConfigFilePath):
244        """Initialise M2Crypto based urllib2 HTTPS handler to enable SSL
245        authentication of OpenID Providers"""
246        log.info("Setting parameters for SSL Authentication of OpenID "
247                 "Provider ...")
248       
249        idPValidationDriver = SSLIdPValidationDriver(
250                                idpConfigFilePath=idpWhitelistConfigFilePath)
251           
252        # Force Python OpenID library to use Urllib2 fetcher instead of the
253        # Curl based one otherwise the M2Crypto SSL handler will be ignored.
254        setDefaultFetcher(Urllib2Fetcher())
255       
256        log.debug("Setting the M2Crypto SSL handler ...")
257       
258        opener = urllib2.OpenerDirector()           
259        opener.add_handler(FlagHttpsOnlyHandler())
260        opener.add_handler(HTTPSHandler(idPValidationDriver.ctx))
261       
262        urllib2.install_opener(opener)
263
264   
265class FlagHttpsOnlyHandler(urllib2.AbstractHTTPHandler):
266    '''Raise an exception for any other protocol than https'''
267    def unknown_open(self, req):
268        """Signal to caller that default handler is not supported"""
269        raise urllib2.URLError("Only HTTPS based OpenID Providers "
270                               "are supported")
271
272
273class SigninInterfaceError(Exception):
274    """Base class for SigninInterface exceptions
275   
276    A standard message is raised set by the msg class variable but the actual
277    exception details are logged to the error log.  The use of a standard
278    message enables callers to use its content for user error messages.
279   
280    @type msg: basestring
281    @cvar msg: standard message to be raised for this exception"""
282    userMsg = ("An internal error occurred with the page layout,  Please "
283               "contact your system administrator")
284    errorMsg = "SigninInterface error"
285   
286    def __init__(self, *arg, **kw):
287        if len(arg) > 0:
288            msg = arg[0]
289        else:
290            msg = self.__class__.errorMsg
291           
292        log.error(msg)
293        Exception.__init__(self, msg, **kw)
294       
295class SigninInterfaceInitError(SigninInterfaceError):
296    """Error with initialisation of SigninInterface.  Raise from __init__"""
297    errorMsg = "SigninInterface initialisation error"
298   
299class SigninInterfaceConfigError(SigninInterfaceError):
300    """Error with configuration settings.  Raise from __init__"""
301    errorMsg = "SigninInterface configuration error"   
302
303class SigninInterface(NDGSecurityMiddlewareBase):
304    """Base class for sign in rendering.  This is implemented as WSGI
305    middleware to enable additional middleware to be added into the call
306    stack e.g. StaticFileParser to enable rendering of graphics and other
307    static content in the Sign In page"""
308   
309    def getTemplateFunc(self):
310        """Return template function for AuthKit to render OpenID Relying
311        Party Sign in page"""
312        raise NotImplementedError()
313   
314    def __call__(self, environ, start_response):
315        return self._app(self, environ, start_response)
316
Note: See TracBrowser for help on using the repository browser.