Changeset 5333


Ignore:
Timestamp:
22/05/09 16:41:27 (10 years ago)
Author:
pjkersha
Message:

Working on SSL Client AuthN WSGI middleware

Location:
TI12-security/trunk/python
Files:
4 added
3 edited

Legend:

Unmodified
Added
Removed
  • TI12-security/trunk/python/ndg.security.server/ndg/security/server/wsgi/authn.py

    r5330 r5333  
    7373 
    7474import urllib 
    75 from urlparse import urlparse 
     75from urlparse import urlsplit 
    7676from paste.request import construct_url 
    7777from beaker.middleware import SessionMiddleware 
     
    7979 
    8080from ndg.security.server.wsgi import NDGSecurityMiddlewareBase, \ 
    81     NDGSecurityMiddlewareConfigError 
    82  
    83 class AuthenticationRedirectMiddleware(NDGSecurityMiddlewareBase): 
    84     '''Middleware to redirect to another URI if no user is set in the  
    85     REMOTE_USER key of environ 
    86      
    87     AuthKit.authenticate.middleware must be in place upstream of this  
    88     middleware.  AuthenticationMiddleware wrapper handles this.''' 
     81    NDGSecurityMiddlewareConfigError         
     82 
     83         
     84class AuthNRedirectMiddleware(NDGSecurityMiddlewareBase): 
     85    """Base class for Authentication HTTP redirect initiator and consumer WSGI  
     86    middleware 
     87 
     88    @type propertyDefaults: dict 
     89    @cvar propertyDefaults: valid configuration property keywords     
     90    @type return2URIArgName: basestring 
     91    @cvar return2URIArgName: name of URI query argument used to pass the  
     92    return to URI between initiator and consumer classes""" 
    8993    propertyDefaults = { 
    90         'redirectURI': None, 
    91         'sessionKey': 'beaker.session' 
     94        'sessionKey': 'beaker.session.ndg.security' 
    9295    } 
    93     propertyDefaults.update(NDGSecurityMiddlewareBase.propertyDefaults) 
    94  
     96    propertyDefaults.update(AuthNRedirectMiddleware.propertyDefaults) 
    9597    return2URIArgName = 'ndg.security.r' 
    96      
     98 
    9799    _isAuthenticated = lambda self: 'REMOTE_USER' in self.environ 
    98100    isAuthenticated = property(fget=_isAuthenticated, 
    99101                               doc='boolean for is user logged in') 
    100102 
     103class AuthNRedirectInitiatorMiddleware(AuthNRedirectMiddleware): 
     104    '''Middleware to initiate a redirect to another URI if a user is not  
     105    authenticated i.e. in AuthKit terms, no user is set in the REMOTE_USER key  
     106    of environ 
     107     
     108    AuthKit.authenticate.middleware must be in place upstream of this  
     109    middleware.  AuthenticationMiddleware wrapper handles this. 
     110     
     111    @type propertyDefaults: dict 
     112    @cvar propertyDefaults: valid configuration property keywords     
     113    ''' 
     114    propertyDefaults = { 
     115        'redirectURI': None, 
     116    } 
     117    propertyDefaults.update(AuthNRedirectMiddleware.propertyDefaults) 
     118     
     119 
    101120    triggerStatus = '401' 
    102     id = 'authNMiddleware' 
     121    id = 'authNRedirectInitiatorMiddleware' 
    103122 
    104123    def __init__(self, app, global_conf, **app_conf): 
    105124        self._redirectURI = None 
    106         super(AuthenticationRedirectMiddleware, self).__init__(app,  
     125        super(AuthNRedirectInitiatorMiddleware, self).__init__(app,  
    107126                                                               global_conf,  
    108127                                                               **app_conf) 
     
    112131        '''Invoke redirect if user is not authenticated''' 
    113132         
    114         log.debug("AuthenticationRedirectMiddleware.__call__ ...") 
     133        log.debug("AuthNRedirectInitiatorMiddleware.__call__ ...") 
    115134         
    116135        if self.isAuthenticated: 
     
    146165        quotedReturn2URI = urllib.quote(return2URI, safe='') 
    147166        return2URIQueryArg = urllib.urlencode( 
    148                     {AuthenticationRedirectMiddleware.return2URIArgName:  
     167                    {AuthNRedirectInitiatorMiddleware.return2URIArgName:  
    149168                     quotedReturn2URI}) 
    150169 
     
    161180            else: 
    162181                redirectURI += '?' + return2URIQueryArg 
    163                  
     182           
     183        # Call NDGSecurityMiddlewareBase.redirect utility method       
    164184        return self.redirect(redirectURI) 
    165185         
    166186    @classmethod 
    167187    def checker(cls, environ, status, headers): 
    168         """Set the trigger for calling this middleware.  In this case, it's a 
    169         HTTP 401 Unauthorized response detected in the middleware chain 
     188        """Set the MultiHandler checker function for triggering this  
     189        middleware.  In this case, it's a HTTP 401 Unauthorized response  
     190        detected in the middleware chain 
    170191        """ 
    171192        if status.startswith(cls.triggerStatus): 
    172             log.debug("%s.checker caught status %s: invoking authentication " 
    173                       "handler", cls.__name__, cls.triggerStatus) 
     193            log.debug("%s.checker caught status [%s]: invoking authentication" 
     194                      " handler", cls.__name__, cls.triggerStatus) 
    174195            return True 
    175196        else: 
     
    178199 
    179200 
     201class AuthNRedirectResponseMiddleware(AuthNRedirectMiddleware): 
     202    """Compliment to AuthNRedirectInitiatorMiddleware  
     203    functioning as the opposite end of the HTTP redirect interface.  It  
     204    performs the following tasks: 
     205    - Detect a redirect URI set in a URI query argument and copy it into 
     206    a user session object.  
     207    - Redirect back to the redirect URI once a user is authenticated 
     208     
     209    Also see, 
     210    ndg.security.server.wsgi.openid.relyingparty.OpenIDRelyingPartyMiddleware  
     211    which performs a similar function. 
     212    """ 
     213    @NDGSecurityMiddlewareBase.initCall 
     214    def __call__(self, environ, start_response): 
     215        session = environ[self.sessionKey] 
     216         
     217        # Check for return to address in URI query args set by  
     218        # AuthNRedirectInitiatorMiddleware in application code stack 
     219        if environ['REQUEST_METHOD'] == "GET": 
     220            params = dict(parse_querystring(environ)) 
     221        else: 
     222            params = {} 
     223         
     224        quotedReferrer = params.get(self.__class__.return2URIArgName, '') 
     225        referrerURI = urllib.unquote(quotedReferrer) 
     226        if referrerURI: 
     227            session[self.__class__.return2URIArgName] = referrerURI 
     228            session.save() 
     229             
     230        if self.isAuthenticated: 
     231            return2URI = session.get(self.__class__.return2URIArgName) 
     232            if return2URI is None: 
     233                log.warning("user is authenticated but no return2URI has been " 
     234                            "set") 
     235            else: 
     236                return self.redirect(return2URI) 
     237 
     238        return self._app(environ, start_response) 
     239 
     240  
     241     
    180242class SessionHandlerMiddleware(NDGSecurityMiddlewareBase): 
    181243    '''Middleware to handle: 
     
    336398        MultiHandler.__init__(self, app) 
    337399 
    338         self.add_method(AuthenticationRedirectMiddleware.id,  
    339                         AuthenticationRedirectMiddleware.filter_app_factory,  
     400        self.add_method(AuthNRedirectInitiatorMiddleware.id,  
     401                        AuthNRedirectInitiatorMiddleware.filter_app_factory,  
    340402                        global_conf, 
    341403                        prefix=prefix, 
    342404                        **app_conf) 
    343405         
    344         self.add_checker(AuthenticationRedirectMiddleware.id,  
    345                          AuthenticationRedirectMiddleware.checker) 
    346  
     406        self.add_checker(AuthNRedirectInitiatorMiddleware.id,  
     407                         AuthNRedirectInitiatorMiddleware.checker) 
     408 
  • TI12-security/trunk/python/ndg.security.server/ndg/security/server/wsgi/openid/relyingparty/__init__.py

    r5280 r5333  
    2323 
    2424from ndg.security.server.wsgi import NDGSecurityMiddlewareBase 
    25 from ndg.security.server.wsgi.authn import AuthenticationRedirectMiddleware 
     25from ndg.security.server.wsgi.authn import AuthNRedirectMiddleware 
    2626from ndg.security.common.utils.classfactory import instantiateClass 
    2727 
     
    3333   
    3434class OpenIDRelyingPartyMiddleware(NDGSecurityMiddlewareBase): 
    35     '''Implementation of OpenID Relying Party based on AuthKit''' 
     35    '''OpenID Relying Party middleware which wraps the AuthKit implementation. 
     36    This middleware is to be hosted in it's own security middleware stack. 
     37    WSGI middleware applications to be protected can be hosted in a separate 
     38    stack.  The AuthNRedirectMiddleware filter can respond to a HTTP  
     39    401 response from this stack and redirect to this middleware to initiate 
     40    OpenID based sign in.  AuthNRedirectMiddleware passes a query 
     41    argument in its request containing the URI return address for this  
     42    middleware to return to following OpenID sign in. 
     43    ''' 
    3644    propertyDefaults = { 
    3745        'signinInterfaceMiddlewareClass': None, 
    3846        'baseURL': '', 
    39         'sessionKey': 'beaker.session' 
     47        'sessionKey': 'beaker.session.ndg.security' 
    4048    } 
    4149    propertyDefaults.update(NDGSecurityMiddlewareBase.propertyDefaults) 
     
    108116         
    109117        # Check for return to argument in query key value pairs 
    110         self._return2URIKey = AuthenticationRedirectMiddleware.return2URIArgName + '=' 
     118        self._return2URIKey = AuthNRedirectMiddleware.return2URIArgName + '=' 
    111119     
    112120        super(OpenIDRelyingPartyMiddleware, self).__init__(authKitApp,  
     
    118126    @NDGSecurityMiddlewareBase.initCall      
    119127    def __call__(self, environ, start_response): 
    120         '''Alter start_response to override the status code and force to 401. 
     128        ''' 
     129        - Alter start_response to override the status code and force to 401. 
    121130        This will enable non-browser based client code to bypass the OpenID  
    122131        interface 
     132        - Manage AuthKit verify and process actions setting the referrer URI 
     133        to manage redirects 
    123134         
    124135        @type environ: dict 
     
    129140        session = environ[self.sessionKey] 
    130141         
    131         # Check for return to address in URI query args 
     142        # Check for return to address in URI query args set by  
     143        # AuthNRedirectMiddleware in application code stack 
    132144        if environ['REQUEST_METHOD'] == "GET": 
    133145            params = dict(parse_querystring(environ)) 
     
    135147            params = {} 
    136148         
    137         quotedReferrer = params.get( 
    138                         AuthenticationRedirectMiddleware.return2URIArgName, '') 
     149        quotedReferrer=params.get(AuthNRedirectMiddleware.return2URIArgName,'') 
    139150        referrer = urllib.unquote(quotedReferrer) 
    140151        referrerPathInfo = urlsplit(referrer)[2] 
     
    143154           not referrerPathInfo.endswith(self._authKitVerifyPath) and \ 
    144155           not referrerPathInfo.endswith(self._authKitProcessPath): 
    145             # Set-up for authkit.authenticate.open_id.AuthOpenIDHandler.process 
     156            # Subvert authkit.authenticate.open_id.AuthOpenIDHandler.process 
     157            # reassigning it's session 'referer' key to the URI specified in 
     158            # the referrer query argument set in the request URI 
    146159            session['referer'] = referrer 
    147160            session.save() 
     
    161174 
    162175        if self.signoutPath is not None and self.pathInfo == self.signoutPath: 
    163             # TODO: Redirect to referrer ... 
     176            # Redirect to referrer ... 
    164177            referrer = session.get( 
    165                         'ndg.security.server.wsgi.openid.relyingparty.referer') 
     178                    'ndg.security.server.wsgi.openid.relyingparty.referer') 
    166179            if referrer is not None: 
    167180                def setRedirectResponse(status, header, exc_info=None): 
  • TI12-security/trunk/python/ndg.security.server/ndg/security/server/wsgi/ssl.py

    r5280 r5333  
    1 """SSL Client Authentication Middleware Module 
     1"""SSL Peer Authentication Middleware Module 
    22 
    33Apply to SSL client authentication to configured URL paths. 
     
    66key as set by standard Apache SSL. 
    77 
    8 NERC Data Grid Project 
    9  
     8NERC DataGrid Project 
    109""" 
    1110__author__ = "P J Kershaw" 
     
    2120 
    2221from ndg.security.server.wsgi import NDGSecurityPathFilter 
    23 from ndg.security.common.X509 import X509Stack, X509Cert, X509CertError 
    24  
    25 class SSLClientAuthNMiddleware(NDGSecurityPathFilter): 
    26     '''Apply to SSL client authentication to configured URL paths. 
    27      
    28     B{This class must be run under Apache mod_wsgi} 
    29  
     22from ndg.security.common.X509 import X509Stack, X509Cert, X509CertError, X500DN 
     23 
     24class ApacheSSLAuthNMiddleware(NDGSecurityMiddlewareBase): 
     25    """Perform SSL peer certificate authentication making use of Apache 
     26    SSL environment settings 
     27     
     28    B{This class relies on SSL environment settings being present as available 
     29    when run embedded within Apache using for example mod_wsgi} 
     30     
    3031    - SSL Client certificate is expected to be present in environ as  
    3132    SSL_CLIENT_CERT key as set by Apache SSL with ExportCertData option to 
    32     SSLOptions directive enabled.''' 
    33  
     33    SSLOptions directive enabled. 
     34    """ 
     35    sslKeyName = 'HTTPS' 
     36 
     37    _isSSLRequest = lambda self: self.environ.get( 
     38                                ApacheSSLAuthNMiddleware.sslKeyName) == '1' 
     39    isSSLRequest = property(fget=_isSSLRequest, 
     40                            doc="Is an SSL request boolean - depends on " 
     41                                "'HTTPS' Apache environment variable setting") 
     42     
    3443    sslClientCertKeyName = 'SSL_CLIENT_CERT' 
    3544     
     
    3746        'caCertFilePathList': [] 
    3847    } 
    39     propertyDefaults.update(NDGSecurityPathFilter.propertyDefaults) 
     48    propertyDefaults.update(NDGSecurityMiddlewareBase.propertyDefaults) 
    4049         
    4150    _isSSLClientCertSet = lambda self: bool(self.environ.get( 
    42                                 SSLClientAuthNMiddleware.sslClientCertKeyName))  
     51                                ApacheSSLAuthNMiddleware.sslClientCertKeyName))  
    4352    isSSLClientCertSet = property(fget=_isSSLClientCertSet, 
    44                                   doc="Check for client cert. set in environ") 
     53                                  doc="Check for client X.509 certificate " 
     54                                      "'SSL_CLIENT_CERT' setting in environ") 
     55     
     56    def __init__(self, app, global_conf, prefix='', **app_conf): 
     57        super(ApacheSSLAuthNMiddleware, self).__init__(app,  
     58                                                       global_conf,  
     59                                                       **app_conf) 
     60         
    4561         
    4662    def _setCACertsFromFileList(self, caCertFilePathList): 
     
    6076                            '"caCertFilePathList"') 
    6177 
     78        self._caCertFilePathList = caCertFilePathList 
    6279        self._caCertStack = X509Stack() 
    6380 
     
    6582            x509Cert = X509Cert.Read(os.path.expandvars(caCertFilePath)) 
    6683            self._caCertStack.push(x509Cert) 
    67          
     84     
     85    def _getCACertFilePathList(self): 
     86        return self._caCertFilePathList 
     87     
    6888    caCertFilePathList = property(fset=_setCACertsFromFileList, 
     89                                  fget=_getCACertFilePathList, 
    6990                                  doc="list of CA certificate file paths - " 
    7091                                      "peer certificate must validate against " 
     
    7394    @NDGSecurityMiddlewareBase.initCall          
    7495    def __call__(self, environ, start_response): 
    75          
    76         log.debug("Calling SSLClientAuthNMiddleware.__call__ ...") 
     96        '''Check for peer certificate in environment and if present carry out 
     97        authentication''' 
     98        log.debug("ApacheSSLAuthNMiddleware.__call__ ...") 
    7799         
    78100        if not self.isSSLRequest: 
    79             log.debug("ignoring request - assuming non-SSL") 
    80             return self._setResponse(environ, start_response) 
    81              
    82         if not self.pathMatch: 
    83             log.debug("ignoring path [%s]" % self.path) 
     101            log.warning("ApacheSSLAuthNMiddleware: 'HTTPS' environment " 
     102                        "variable not found in environment; ignoring request") 
     103            return self._app(environ, start_response) 
     104             
     105        elif not self.pathMatch: 
     106            log.debug("ApacheSSLAuthNMiddleware: ignoring path [%s]",  
     107                      self.pathInfo) 
    84108            return self._setResponse(environ, start_response) 
    85109                     
    86110        elif not self.isSSLClientCertSet: 
    87             log.error("No SSL Client path set for request to [%s]" % self.path) 
    88             return self._setErrorResponse(start_response=start_response, 
     111            log.error("ApacheSSLAuthNMiddleware: No SSL Client certificate " 
     112                      "for request to [%s]; ignoring ",  
     113                      self.pathInfo) 
     114            return self._setErrorResponse(code=401, 
    89115                                          msg='No client SSL Certificate set') 
    90116             
    91         if self.isValidClientCert(environ):             
     117        if self.isValidClientCert():             
    92118            return self._setResponse(environ, start_response) 
    93119        else: 
    94             return self._setErrorResponse(start_response=start_response) 
     120            return self._setErrorResponse(code=401) 
     121 
    95122             
    96123    def _setResponse(self,  
     
    98125                     start_response, 
    99126                     notFoundMsg='No application set for ' 
    100                                  'SSLClientAuthNMiddleware'): 
    101         return super(SSLClientAuthNMiddleware, self)._setResponse(environ,  
     127                                 'ApacheSSLAuthNMiddleware'): 
     128        return super(ApacheSSLAuthNMiddleware, self)._setResponse(environ,  
    102129                                                    start_response, 
    103130                                                    notFoundMsg=notFoundMsg) 
     
    106133                          start_response, 
    107134                          msg='Invalid SSL client certificate'): 
    108         return super(SSLClientAuthNMiddleware, self)._setResponse( 
     135        return super(ApacheSSLAuthNMiddleware, self)._setResponse( 
    109136                                                start_response=start_response, 
    110137                                                msg=msg, 
    111138                                                code=self.errorResponseCode) 
    112139 
    113     def isValidClientCert(self, environ): 
    114         sslClientCert = environ[SSLClientAuthNMiddleware.sslClientCertKeyName] 
     140    def isValidClientCert(self): 
     141        sslClientCert = self.environ[ 
     142                                ApacheSSLAuthNMiddleware.sslClientCertKeyName] 
    115143        x509Cert = X509Cert.Parse(sslClientCert) 
    116144         
     
    123151 
    124152            except X509CertError, e: 
    125                 log.info("Client certificate verification failed: %s" % e) 
     153                log.info("Client certificate verification failed with %s " 
     154                         "exception: %s" % (e.__class__, e)) 
    126155                return False 
    127156             
    128157            except Exception, e: 
    129158                log.error("Client certificate verification failed with " 
    130                           "unexpected error: %s" % e) 
     159                          "unexpected exception type %s: %s" % (e.__class__,e)) 
    131160                return False 
    132              
     161         
     162        # Check certificate Distinguished Name via  
     163        # ClientCertDNVerificationInterface object 
     164        return self._clientCertDNVerify(x509Cert) 
     165 
     166 
     167class ClientCertDNVerificationInterface(object): 
     168    """Interface to enable customised verification of the client certificate  
     169    Distinguished Name""" 
     170    def __init__(self, **cfg): 
     171        """@type cfg: dict 
     172        @param cfg: configuration parameters, derived class may customise 
     173        """ 
     174        raise NotImplementedError() 
     175     
     176    def __call__(self, x509Cert): 
     177        """Derived class implementation should return True if the certificate 
     178        DN is valid, False otherwise 
     179        @type x509Cert: ndg.security.common.X509.X509Cert 
     180        @param x509Cert: client X.509 certificate received from Apache 
     181        environment""" 
     182        raise NotImplementedError() 
     183 
     184 
     185class NoClientCertDNVerification(ClientCertDNVerificationInterface): 
     186    """Implementation of ClientCertDNVerificationInterface ignoring the  
     187    client certificate DN set""" 
     188    def __init__(self, **cfg): 
     189        pass 
     190     
     191    def __call__(self, x509Cert): 
    133192        return True 
     193     
     194class ClientCertDNVerificationList(ClientCertDNVerificationInterface): 
     195    """Implementation of ClientCertDNVerificationInterface matching the input 
     196    client certificate DN against a configurable list""" 
     197     
     198    def __init__(self, validDNList=[]): 
     199        self.validDNList = validDNList 
     200     
     201    def __call__(self, x509Cert): 
     202        inputDN = x509Cert.dn 
     203        return inputDN in self._validDNList 
     204     
     205    def _setValidDNList(self, dnList): 
     206        '''Read CA certificates from file and add them to an X.509 Cert. 
     207        stack 
     208         
     209        @type dnList: list or tuple 
     210        @param dnList: list of DNs to match against the input certificate DN 
     211        ''' 
     212         
     213        if isinstance(dnList, basestring): 
     214            # Try parsing a space separated list of file paths 
     215            dnList = dnList.split() 
     216             
     217        elif not isinstance(dnList, (list, tuple)): 
     218            raise TypeError('Expecting a list or tuple for "dnList"') 
     219 
     220        self._validDNList = [X500DN(dn) for dn in dnList] 
     221 
     222     
     223    def _getValidDNList(self): 
     224        return self._validDNList 
     225     
     226    validDNList = property(fset=_setValidDNList, 
     227                           fget=_getValidDNList, 
     228                           doc="list of permissible certificate Distinguished " 
     229                               "Names permissible") 
Note: See TracChangeset for help on using the changeset viewer.