Changeset 4537


Ignore:
Timestamp:
04/12/08 17:09:59 (11 years ago)
Author:
pjkersha
Message:

OpenID Provider Authentication interface:

  • added capability for defining OpenID identifier from authentication interface. Still needs to be tested in ID Select mode.
Location:
TI12-security/trunk/python
Files:
2 edited

Legend:

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

    r4528 r4537  
    3939    A standard message is raised set by the msg class variable but the actual 
    4040    exception details are logged to the error log.  The use of a standard  
    41     message enbales callers to use its content for user error messages. 
     41    message enables callers to use its content for user error messages. 
    4242     
    4343    @type msg: basestring 
    4444    @cvar msg: standard message to be raised for this exception""" 
    45     msg = "An error occured with login" 
     45    userMsg = ("An internal error occurred during login,  Please contact your " 
     46               "system administrator") 
     47    errorMsg = "AuthNInterface error" 
     48     
    4649    def __init__(self, *arg, **kw): 
    47         Exception.__init__(self, AuthNInterfaceError.msg, *arg, **kw) 
    4850        if len(arg) > 0: 
    4951            msg = arg[0] 
    5052        else: 
    51             msg = AuthNInterfaceError.msg 
     53            msg = self.__class__.errorMsg 
    5254             
    5355        log.error(msg) 
     56        Exception.__init__(self, msg, **kw) 
    5457         
    5558class AuthNInterfaceInvalidCredentials(AuthNInterfaceError): 
    5659    """User has provided incorrect username/password.  Raise from logon""" 
    57     msg = "Invalid username/password provided" 
     60    userMsg = ("Invalid username / password provided.  Please try again.  If " 
     61               "the problem persists please contact your system administrator") 
     62    errorMsg = "Invalid username/password provided" 
     63 
     64class AuthNInterfaceUsername2IdentifierMismatch(AuthNInterfaceError):  
     65    """User has provided a username which doesn't match the identifier from 
     66    the OpenID URL that they provided.  DOESN'T apply to ID Select mode where 
     67    the user has given a generic URL for their OpenID Provider.""" 
     68    userMsg = ("Invalid username for the OpenID entered.  Please ensure you " 
     69               "have the correct OpenID and username and try again.  If the " 
     70               "problem persists contact your system administrator") 
     71    errorMsg = "invalid username / OpenID identifier combination" 
    5872     
    5973class AuthNInterfaceRetrieveError(AuthNInterfaceError): 
    6074    """Error with retrieval of information to authenticate user e.g. error with 
    6175    database look-up.  Raise from logon""" 
    62     msg = \ 
    63     "An error occured retrieving information to check the login credentials" 
     76    errorMsg = ("An error occurred retrieving information to check the login " 
     77                "credentials") 
    6478 
    6579class AuthNInterfaceInitError(AuthNInterfaceError): 
    6680    """Error with initialisation of AuthNInterface.  Raise from __init__""" 
    67     msg = "An error occured with the initialisation of the OpenID " + \ 
    68         "Provider's Authentication Service" 
     81    errorMsg = "AuthNInterface initialisation error" 
    6982     
    7083class AuthNInterfaceConfigError(AuthNInterfaceError): 
    7184    """Error with Authentication configuration.  Raise from __init__""" 
    72     msg = "An error occured with the OpenID Provider's Authentication " + \ 
    73         "Service configuration" 
     85    errorMsg = "AuthNInterface configuration error" 
    7486     
    7587class AbstractAuthNInterface(object): 
     
    92104        other specific exception types. 
    93105        """ 
    94  
    95      
    96     def logon(self, username, password): 
     106     
     107    def logon(self, userIdentifier, username, password): 
    97108        """Interface login method 
    98109         
     110        @type userIdentifier: basestring or None 
     111        @param userIdentifier: OpenID user identifier - this implementation of 
     112        an OpenID Provider uses the suffix of the user's OpenID URL to specify 
     113        a unique user identifier.  It ID Select mode was chosen, the identifier 
     114        will be None and can be ignored.  In this case, the implementation of 
     115        the decide method in the rendering interface must match up the username 
     116        to a corresponding identifier in order to construct a complete OpenID 
     117        user URL. 
     118         
    99119        @type username: basestring 
    100         @param username: user identifier 
     120        @param username: user identifier for authentication 
    101121         
    102122        @type password: basestring 
     
    104124         
    105125        @raise AuthNInterfaceInvalidCredentials: invalid username/password 
    106         @raise AuthNInterfaceError: error  
     126        @raise AuthNInterfaceUsername2IdentifierMismatch: username doesn't  
     127        match the OpenID URL provided by the user.  (Doesn't apply to ID Select 
     128        type requests). 
    107129        @raise AuthNInterfaceRetrieveError: error with retrieval of information 
    108130        to authenticate user e.g. error with database look-up. 
     
    112134        raise NotImplementedError(self.logon.__doc__.replace('\n       ','')) 
    113135     
     136    def username2UserIdentifiers(self, username): 
     137        """Map the login username to an identifier which will become the 
     138        unique path suffix to the user's OpenID identifier.  The  
     139        OpenIDProviderMiddleware takes self.urls['id_url'] and adds it to this 
     140        identifier: 
     141         
     142            identifier = self._authN.username2UserIdentifiers(username) 
     143            identityURL = self.urls['url_id'] + '/' + identifier 
     144         
     145        @type username: basestring 
     146        @param username: user identifier 
     147         
     148        @rtype: tuple 
     149        @return: one or more identifiers to be used to make OpenID user  
     150        identity URL(s). 
     151         
     152        @raise AuthNInterfaceConfigError: problem with the configuration  
     153        @raise AuthNInterfaceRetrieveError: error with retrieval of information 
     154        to identifier e.g. error with database look-up. 
     155        @raise AuthNInterfaceError: generic exception not described by the  
     156        other specific exception types. 
     157        """ 
     158        raise NotImplementedError( 
     159                    self.username2UserIdentifiers.__doc__.replace('\n       ','')) 
     160     
    114161     
    115162class BasicAuthNInterface(AbstractAuthNInterface): 
     
    131178        """ 
    132179        # Test/Admin username/password set from ini/kw args 
    133         userCreds = prop.get('usercreds') 
     180        userCreds = prop.get('userCreds') 
    134181        if userCreds: 
    135             self._userCreds = dict([i.strip().split(':') \ 
     182            self._userCreds = dict([i.strip().split(':') 
    136183                                    for i in userCreds.split(',')]) 
    137184        else: 
    138185            raise AuthNInterfaceConfigError('No "userCreds" config option ' 
    139186                                            "found") 
    140      
    141     def logon(self, username, password): 
     187             
     188        user2Identifier = prop.get('username2UserIdentifiers') 
     189        if user2Identifier: 
     190            self._username2Identifier = {} 
     191            for i in user2Identifier.split(): 
     192                username, identifierStr = i.strip().split(':') 
     193                identifiers = tuple(identifierStr.split(',')) 
     194                self._username2Identifier[username] = identifiers 
     195        else: 
     196            raise AuthNInterfaceConfigError('No "user2Identifier" config ' 
     197                                            'option found') 
     198         
     199        userCredNames = self._userCreds.keys() 
     200        userCredNames.sort() 
     201        username2IdentifierNames = self._username2Identifier.keys() 
     202        username2IdentifierNames.sort() 
     203        if userCredNames != username2IdentifierNames: 
     204            raise AuthNInterfaceConfigError('Mismatch between usernames in ' 
     205                                            '"userCreds" and ' 
     206                                            '"username2UserIdentifiers" options')    
     207     
     208    def logon(self, userIdentifier, username, password): 
    142209        """Interface login method 
    143210         
     
    150217        @raise AuthNInterfaceInvalidCredentials: invalid username/password 
    151218        """ 
    152         if self._userCreds.get(username, '') != password: 
     219        if self._userCreds.get(username) != password: 
    153220            raise AuthNInterfaceInvalidCredentials() 
     221         
     222        if userIdentifier is not None and \ 
     223           userIdentifier not in self._username2Identifier.get(username): 
     224            raise AuthNInterfaceUsername2IdentifierMismatch() 
     225     
     226    def username2UserIdentifiers(self, username): 
     227        """Map the login username to an identifier which will become the 
     228        unique path suffix to the user's OpenID identifier.  The  
     229        OpenIDProviderMiddleware takes self.urls['id_url'] and adds it to this 
     230        identifier: 
     231         
     232            identifier = self._authN.username2UserIdentifiers(username) 
     233            identityURL = self.urls['url_id'] + '/' + identifier 
     234         
     235        @type username: basestring 
     236        @param username: user identifier 
     237         
     238        @rtype: tuple 
     239        @return: identifiers to be used to make OpenID user identity URLs. This 
     240        implementation only returns one 
     241         
     242        @raise AuthNInterfaceRetrieveError: error with retrieval of information 
     243        to identifier e.g. error with database look-up. 
     244        """ 
     245        try: 
     246            return (self._username2Identifier[username],) 
     247        except KeyError: 
     248            raise AuthNInterfaceRetrieveError('No entries for "%s" user' %  
     249                                              username) 
    154250         
    155251         
     
    286382 
    287383        # Paths relative to base URL - Nb. remove trailing '/' 
    288         self.paths = dict([(k, opt[k].rstrip('/')) \ 
     384        self.paths = dict([(k, opt[k].rstrip('/')) 
    289385                           for k in OpenIDProviderMiddleware.defPaths]) 
    290386         
     
    295391 
    296392        # Full Paths 
    297         self.urls = dict([(k.replace('path_', 'url_'), self.base_url+v) \ 
     393        self.urls = dict([(k.replace('path_', 'url_'), self.base_url+v) 
    298394                          for k,v in self.paths.items()]) 
    299395 
    300         self.method = dict([(v, k.replace('path_', 'do_')) \ 
     396        self.method = dict([(v, k.replace('path_', 'do_')) 
    301397                            for k,v in self.paths.items()]) 
    302398 
     
    324420 
    325421        try: 
    326             self._render = renderingClass(self.base_url, self.urls) 
     422            self._render = renderingClass(self._authN,  
     423                                          self.base_url,  
     424                                          self.urls) 
    327425        except Exception, e: 
    328426            log.error("Error instantiating rendering interface...") 
     
    421519             
    422520        elif self.path.startswith(self.paths['path_id']) or \ 
    423            self.path.startswith(self.paths['path_yadis']): 
     521             self.path.startswith(self.paths['path_yadis']): 
    424522             
    425523            # Match against path minus ID as this is not known in advance             
     
    478576 
    479577 
     578    def do_serveryadis(self, environ, start_response): 
     579        """Yadis based discovery for ID Select mode i.e. no user ID given for  
     580        OpenID identifier at Relying Party 
     581         
     582        @type environ: dict 
     583        @param environ: dictionary of environment variables 
     584        @type start_response: callable 
     585        @param start_response: standard WSGI callable to set HTTP headers 
     586        @rtype: basestring 
     587        @return: WSGI response 
     588 
     589        """ 
     590        response = self._render.serverYadis(environ, start_response) 
     591        return response 
     592 
     593 
    480594    def do_openidserver(self, environ, start_response): 
    481         """Handle OpenID Server Request for ID Select mode i.e. no user id  
    482         given OpenID URL at Relying Party 
     595        """OpenID Server endpoint - handles OpenID Request following discovery 
    483596         
    484597        @type environ: dict 
     
    563676        else: 
    564677            response = self._render.errorPage(environ, start_response, 
    565                                               'Expecting yes/no in allow ' 
     678                                              'Expecting Yes/No in allow ' 
    566679                                              'post. %r' % self.query, 
    567680                                              code=400) 
    568681         
    569         return response 
    570  
    571     def do_serveryadis(self, environ, start_response): 
    572         """Yadis based discovery for ID Select mode i.e. no user ID given for  
    573         OpenID identifier at Relying Party 
    574          
    575         @type environ: dict 
    576         @param environ: dictionary of environment variables 
    577         @type start_response: callable 
    578         @param start_response: standard WSGI callable to set HTTP headers 
    579         @rtype: basestring 
    580         @return: WSGI response 
    581  
    582         """ 
    583         response = self._render.serverYadis(environ, start_response) 
    584682        return response 
    585683 
     
    625723                    return self._redirect(start_response,self.query['fail_to']) 
    626724                 
     725                oidRequest = self.session.get('lastCheckIDRequest') 
     726                if oidRequest is None: 
     727                    log.error("Getting OpenID request for login - no request " 
     728                              "found in session") 
     729                    return self._render.errorPage(environ, start_response, 
     730                                                  "An internal error occured " 
     731                                                  "during login.  Please " 
     732                                                  "report the problem to your " 
     733                                                  "site administrator.") 
     734                     
     735                # Get user identifier to check against credentials provided 
     736                if oidRequest.idSelect(): 
     737                    # ID select mode enables the user to request specifying 
     738                    # their OpenID Provider without giving a personal user URL  
     739                    userIdentifier = None 
     740                else: 
     741                    # Get the unique user identifier from the user's OpenID URL 
     742                    userIdentifier = oidRequest.identity.split('/')[-1] 
     743                     
    627744                # Invoke custom authentication interface plugin 
    628745                try: 
    629                     self._authN.logon(self.query['username'], 
     746                    self._authN.logon(userIdentifier, 
     747                                      self.query['username'], 
    630748                                      self.query.get('password', '')) 
    631749                     
    632750                except AuthNInterfaceError, e: 
    633                     # A known exception was raised 
    634                     log.error("Authenticating: %s" % e) 
    635                     msg = "<p>%s.  " + \ 
    636                         "Please try again or if the problems persists " + \ 
    637                         "contact your system administrator.</p>" % e 
    638                          
    639                     response = self._render.login(environ, start_response, 
    640                                       msg=msg, 
    641                                       success_to=self.urls['url_decide']) 
    642                     return response 
    643                          
     751                    return self._render.login(environ, start_response, 
     752                                          msg=e.userMsg, 
     753                                          success_to=self.urls['url_decide'])                    
    644754                except Exception, e: 
    645755                    log.error("Unexpected exception raised during " 
    646756                              "authentication: %s" % e) 
    647                     msg = "<p>An internal error occured.  " + \ 
    648                         "Please try again or if the problems persists " + \ 
    649                         "contact your system administrator.</p>" % e 
     757                    msg = ("An internal error occured.  " 
     758                           "Please try again or if the problems persists " 
     759                           "contact your system administrator.") 
    650760 
    651761                    response = self._render.login(environ, start_response, 
     
    705815        if oidRequest is None: 
    706816            log.error("No OpenID request set in session") 
    707             return self.do_mainpage(environ, start_response) 
     817            return self._render.errorPage(environ, start_response, 
     818                                          "Invalid request.  Please report " 
     819                                          "the error to your site " 
     820                                          "administrator.", 
     821                                          code=400) 
    708822         
    709823        approvedRoots = self.session.get('approved', {}) 
     
    717831                log.error("Setting response following ID Approval: %s" % e) 
    718832                response = self._render.errorPage(environ, start_response, 
    719                     'Error setting response.  Please report the error to your ' 
    720                     'site administrator.') 
    721  
     833                                                  'Error setting response. ' 
     834                                                  'Please report the error to ' 
     835                                                  'your site administrator.') 
    722836                return response 
    723837 
    724             return self._displayResponse(response) 
     838            return self.oidResponse(response) 
    725839        else: 
    726840            return self._render.decidePage(environ, start_response, oidRequest) 
     
    747861            return True 
    748862         
    749         identityURL = self.urls['url_id']+'/'+username 
     863        identifier = self._authN.username2UserIdentifiers(username) 
     864        identityURL = self.urls['url_id']+'/'+identifier 
    750865        if oidRequest.identity != identityURL: 
    751866            log.debug("OpenIDProviderMiddleware._identityIsAuthorized - " 
     
    815930            if len(requiredAttr) > 0: 
    816931                msg = ("Relying party requires these attributes: %s; but no" 
    817                         "Attribute exchange handler 'axResponseHandler' has " 
    818                         "been set" % requiredAttr) 
     932                       "Attribute exchange handler 'axResponseHandler' has " 
     933                       "been set" % requiredAttr) 
    819934                log.error(msg) 
    820935                raise OpenIDProviderConfigError(msg) 
     
    9691084                        ('Location', url)]) 
    9701085        return [] 
    971  
    972  
    973     def _showErrorPage(self, msg, code=500): 
    974         """Display error information to the user 
    975          
    976         @type msg: basestring 
    977         @param msg: error message 
    978         @type code: int 
    979         @param code: HTTP error code 
    980         """ 
    981          
    982         response = self._render.errorPage(self.environ, start_response, 
    983                                           cgi.escape(msg), code=code) 
    984         return response 
    9851086     
    9861087     
     
    10401141</xrds:XRDS>"""     
    10411142    
    1042     def __init__(self, base_url, urls, **opt): 
    1043         """ 
     1143    def __init__(self, authN, base_url, urls, **opt): 
     1144        """ 
     1145        @type authN: AuthNInterface 
     1146        @param param: reference to authentication interface to enable OpenID 
     1147        user URL construction from username 
    10441148        @type base_url: basestring 
    10451149        @param base_url: base URL for OpenID Provider to which individual paths 
     
    10521156        OpenIDProviderMiddleware config 
    10531157        """ 
     1158        self._authN = authN 
    10541159        self.base_url = base_url 
    10551160        self.urls = urls 
     
    10581163     
    10591164    def serverYadis(self, environ, start_response): 
    1060         '''Render Yadis info 
     1165        '''Render Yadis info for ID Select mode request 
    10611166         
    10621167        @type environ: dict 
     
    10921197        # Override this method to implement an alternate means to derive the  
    10931198        # username identifier 
    1094         username = environ['PATH_INFO'].rstrip('/').split('/')[-1] 
     1199        userIdentifier = environ['PATH_INFO'].rstrip('/').split('/')[-1] 
    10951200         
    10961201        endpoint_url = self.urls['url_openidserver'] 
    1097         user_url = self.urls['url_id'] + '/' + username 
     1202        user_url = self.urls['url_id'] + '/' + userIdentifier 
    10981203         
    10991204        yadisDict = dict(openid20type=discover.OPENID_2_0_TYPE,  
     
    12261331    """Example rendering interface class for demonstration purposes""" 
    12271332    
    1228     def __init__(self, base_url, urls): 
    1229         """ 
    1230         @type base_url: basestring 
    1231         @param base_url: base URL for OpenID Provider to which individual paths 
    1232         are appended 
    1233         @type urls: dict 
    1234         @param urls: full urls for all the paths used by all the exposed  
    1235         methods - keyed by method name - see OpenIDProviderMiddleware.paths 
    1236         """ 
    1237         self.base_url = base_url 
    1238         self.urls = urls 
    1239         self.charset = '' 
    1240  
    1241  
    12421333    def identityPage(self, environ, start_response): 
    12431334        """Render the identity page. 
     
    12521343        """ 
    12531344        path = environ.get('PATH_INFO').rstrip('/') 
    1254         username = path.split('/')[-1] 
     1345        userIdentifier = path.split('/')[-1] 
    12551346         
    12561347        link_tag = '<link rel="openid.server" href="%s">' % \ 
     
    12581349               
    12591350        yadis_loc_tag = '<meta http-equiv="x-xrds-location" content="%s">' % \ 
    1260             (self.urls['url_yadis']+'/'+username) 
     1351            (self.urls['url_yadis']+'/'+userIdentifier) 
    12611352             
    12621353        disco_tags = link_tag + yadis_loc_tag 
     
    12641355 
    12651356        response = self._showPage(environ,  
    1266                               'Identity Page',  
    1267                               head_extras=disco_tags,  
    1268                               msg='<p>This is the identity page for %s.</p>' %  
    1269                                   ident) 
     1357                                  'Identity Page',  
     1358                                  head_extras=disco_tags,  
     1359                                  msg='<p>This is the identity page for %s.' 
     1360                                      '</p>' % ident) 
    12701361         
    12711362        start_response("200 OK",  
     
    14061497#        assert oidRequest.identity.startswith(id_url_base), \ 
    14071498#               repr((oidRequest.identity, id_url_base)) 
    1408         expected_user = oidRequest.identity[len(id_url_base):] 
     1499        userIdentifier = oidRequest.identity[len(id_url_base):] 
    14091500        username = environ['beaker.session']['username'] 
    14101501         
    14111502        if oidRequest.idSelect(): # We are being asked to select an ID 
     1503            userIdentifier = self._authN.username2UserIdentifiers(username)[0] 
     1504            userOpenIDURL = id_url_base + userIdentifier 
     1505             
    14121506            msg = '''\ 
    14131507            <p>A site has asked for your identity.  You may select an 
     
    14391533''' % fdata 
    14401534             
    1441         elif expected_user == username: 
     1535        elif userIdentifier == username: 
    14421536            msg = '''\ 
    14431537            <p>A new site has asked to confirm your identity.  If you 
     
    14621556      /><label for="remember">Remember this 
    14631557      decision</label><br /> 
    1464   <input type="submit" name="yes" value="yes" /> 
    1465   <input type="submit" name="no" value="no" /> 
     1558  <input type="submit" name="Yes" value="Yes" /> 
     1559  <input type="submit" name="No" value="No" /> 
    14661560</form>''' % fdata 
    14671561        else: 
    14681562            mdata = { 
    1469                 'expected_user': expected_user, 
     1563                'userIdentifier': userIdentifier, 
    14701564                'username': username, 
    14711565                } 
    14721566            msg = '''\ 
    14731567            <p>A site has asked for an identity belonging to 
    1474             %(expected_user)s, but you are logged in as %(username)s.  To 
    1475             log in as %(expected_user)s and approve the login oidRequest, 
     1568            %(userIdentifier)s, but you are logged in as %(username)s.  To 
     1569            log in as %(userIdentifier)s and approve the login oidRequest, 
    14761570            hit OK below.  The "Remember this decision" checkbox 
    14771571            applies only to the trust root decision.</p>''' % mdata 
     
    14811575                'identity': oidRequest.identity, 
    14821576                'trust_root': oidRequest.trust_root, 
    1483                 'expected_user': expected_user, 
     1577                'username': username, 
    14841578                } 
    14851579            form = '''\ 
     
    14931587      /><label for="remember">Remember this 
    14941588      decision</label><br /> 
    1495   <input type="hidden" name="login_as" value="%(expected_user)s"/> 
    1496   <input type="submit" name="yes" value="yes" /> 
    1497   <input type="submit" name="no" value="no" /> 
     1589  <input type="hidden" name="login_as" value="%(username)s"/> 
     1590  <input type="submit" name="Yes" value="Yes" /> 
     1591  <input type="submit" name="No" value="No" /> 
    14981592</form>''' % fdata 
    14991593 
  • TI12-security/trunk/python/ndg.security.test/ndg/security/test/combinedservices/services.ini

    r4528 r4537  
    308308[filter:OpenIDProviderFilter] 
    309309paste.filter_app_factory=ndg.security.server.wsgi.openid_provider:OpenIDProviderMiddleware 
    310 openid.provider.path.openidserver=/openid/openidserver 
     310openid.provider.path.openidserver=/openid/endpoint 
    311311openid.provider.path.login=/openid/login 
    312312openid.provider.path.loginsubmit=/openid/loginsubmit 
     313 
     314# Comment out next two lines and uncomment the third to disable URL based  
     315# discovery and allow only Yadis based instead 
    313316openid.provider.path.id=/openid/id 
    314317openid.provider.path.yadis=/openid/yadis 
     318#openid.provider.path.yadis=/id/ 
     319 
    315320openid.provider.path.serveryadis=/openid/serveryadis 
    316321openid.provider.path.allow=/openid/allow 
    317322openid.provider.path.decide=/openid/decide 
    318 openid.provider.path.mainpage=/openid 
     323openid.provider.path.mainpage=/openid/ 
    319324openid.provider.session_middleware=beaker.session  
    320325openid.provider.base_url=http://localhost:8000 
     
    326331openid.provider.axResponseHandler=ndg.security.server.pylons.container.lib.openid_provider_util:esgAXResponseHandler 
    327332openid.provider.authNInterface=ndg.security.server.wsgi.openid_provider.BasicAuthNInterface 
    328 openid.provider.authN.usercreds=pjk:test 
     333openid.provider.authN.userCreds=pjk:test 
     334openid.provider.authN.username2UserIdentifiers=pjk:PhilipKershaw,P.J.Kershaw 
    329335 
    330336# Basic authentication for testing/admin - comma delimited list of  
Note: See TracChangeset for help on using the changeset viewer.