Announcement Announcement Module
Collapse
No announcement yet.
How to limit logons? Page Title Module
Move Remove Collapse
This topic is closed
X
X
Conversation Detail Module
Collapse
  • Filter
  • Time
  • Show
Clear All
new posts

  • How to limit logons?

    How to limit logons from user to one session?
    What i mean:
    For example user enters site (authenticated) with Mozilla. I n a few minutes the same user (same login and password) enters this site with another browser (IE or something else but not Mozilla) and tryes to authenticate. During this second authentication i need to ban it or perform logout for first session. Is there any way to do this?

  • #2
    Not out of the box.

    The first step would be updating your Authentication request object to contain the HttpSession ID. To do that you'd edit net.sf.acegisecurity.ui.webapp.AuthenticationProce ssingFilter as follows:

    Code:
        public Authentication attemptAuthentication(HttpServletRequest request)
            throws AuthenticationException {
            String username = request.getParameter(ACEGI_SECURITY_FORM_USERNAME_KEY);
            String password = request.getParameter(ACEGI_SECURITY_FORM_PASSWORD_KEY);
    
            if (username == null) {
                username = "";
            }
    
            if (password == null) {
                password = "";
            }
    
            UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
            authRequest.setDetails(request.getSession().getId());
            return this.getAuthenticationManager().authenticate(authRequest);
        }
    Next you'd write an ApplicationListener. It would listen for net.sf.acegisecurity.providers.dao.event.Authentic ationSuccessEvent and update your authentication repository with the session ID contained in the event for the user subject of the authentication.

    Next you'd ensure your AuthenticationDao returns the session ID from the database in response to any loadUserByUsername(String) request. Finally, subclass DaoAuthenticationProvider and override isPasswordCorrect(Authentication, UserDetails). It will compare the Authentication.getDetails() with the UserDetails your AuthenticationDao returned. This is where you'd put logic to do with timeouts since the last login etc. If there is a problem, just return false (or throw a new exception such as AlreadyLoggedOnException).

    Instead of setting the session ID into the Authentication.details property you might prefer to make a holder object that contains the IP address as well. Thus you can more easily detect if they're logging on from a different browser etc. Although be mindful of proxy servers.

    Comment


    • #3
      Ben,

      I followed the above steps word for word:
      1) I created a custom AuthenticationProcessingFilter which captures the sessionId and requestor IP address.
      2) I created a AppListener to trap AuthenticationSuccessEvent events. When the AppListener traps the AuthenticationSuccessEvent it updates the authentication repository with the session ID and requestor IP address.
      3) My AuthenticationDao returns the session ID from the database in response to any loadUserByUsername(String) request.
      4) I created a custom DaoAuthenticationProvider and override isPasswordCorrect(Authentication, UserDetails) to verify the Authentication details with the details from the authentication repository.

      The problem I'm having is the sequence of events:

      The first event that happens is the AuthenticationProcessingFilter.attemptAuthenticati on, which captures the authentication details, no problem.

      The next event is the AuthenticationDao.loadUserByUsername, which restores the UserDetails from the authentication repository, including the session Id and requestor Ip, no problem.

      The third event is the DaoAuthenticationProvider.isPasswordCorrect, which is were I'm having the problem. This event is comparing the authentication details from the attemptAuthentication event to the user details that has just been restored from the authentication repository. This event never succeeds because the authentication details has not been stored to the authentication repository.

      The fourth event, which is the AuthenticationSuccessEvent in the appListener, never happens.

      I'm I missing something. It looks like I need to some how put logic in the isPasswordCorrect method so that it does not verify the authentication details with the user details until after the first successful login. Is there an easy way to see if the user has already successfully logged in? Or is there some way of setting a session parameter or a application parameter on the AuthenticationSuccessEvent that can be checked by the isPasswordValid method? I rather not put an indicator in the authentication repository because this information can get out of sync with application. Meaning, I have no controlled way of unsetting this information if the user does not log out or lets there session expire.

      Thanks

      Guy

      Comment


      • #4
        Hi Guy

        The basic problem is one of business logic deciding when the logical session (as distinct from the Java HttpSession) ends.

        The whole idea of this thread, AFAICS, is to ensure a given principal can only have one LogicalSession with the application at a time. Is that your goal?

        In this case you need to define what a LogicalSession is, compared with a HttpSession. Generally you'll define a LogicalSession as being up to (say) 30 minutes in length for a given principal, irrespective of the HttpSession ID or IP address. Let's consider this and how we'd implement it.

        Your AppListeners when consuming the AuthenticationSuccessEvents will update the UserDetails table with a sessionDate. The DaoAuthenticationProvider isPasswordCorrect method will compare the sessionDate in the UserDetails and if it is LESS than 30 minutes old, expect the same HttpSession ID and IP address to be used. If it is MORE than 30 minutes old, it can safely skip the check of the HttpSession ID and IP address, knowing they're stale from the last LogicalSession.

        Does the above help at all? If not, would you mind posting a more detailed description of your use case as well as your current isPasswordCorrect method so I can offer some more specific advice.

        Comment


        • #5
          Ben,

          The company where I work wants to charge a fee for every userId created for the application, then we don't want to people to share the same userId, that is why we don't want two HttpSessions running at the same time with the same UserId.

          I was planning to follow your recommendation using the field LastSessionId as an indicator of the user being already login somewhere else (no matter if it is seconds of minutes ago).

          This is how I see it would work:

          1) User has never login, database field LastSessionId= null
          2) User attempts to login
          3) AuthenticationDao.loadUserByUsername read database
          4) CustomDaoAuthenticationProvider.isPasswordCorrect
          is executed and checks the UserDetails LastSessionId. If it nulls it is okay, if it already has a value then sends an error message. In this case sends a true result (LastSessionId = null, user is not login anywhere)
          5) User is login, successfuly event is fired, then the listener updates the database with session id
          6) User attempts to login a second time
          7) AuthenticationDao.loadUserByUsername read database, LastSessionId is not null
          8) CustomDaoAuthenticationProvider.isPasswordCorrect
          is executed and sends error messabe because LastSessionId is not null
          9) User logout from the session where it was successfully login.. program sets the db LastSessionId = null
          (same should happen with timeout)

          I was planning to use the HttpSessionListener to catch the timeout event and set the LastSessionId = null in the database

          I would really appreciate any feedback in this subject.

          Thank you
          Zenobia

          Comment


          • #6
            Sounds like a good approach.

            Comment


            • #7
              Ben,

              Thank you for your response.

              Originally posted by Ben Alex
              Does the above help at all? If not, would you mind posting a more detailed description of your use case as well as your current isPasswordCorrect method so I can offer some more specific advice.
              Yes, the above comments do help. It gives me a better understanding of the internals of Acegi's login and authentications process. The security requirements of the application are: to not allow a user to start a new HttpSession if one has already started for that user (this may not be the same as a Logical session); and to lock the account if the user has 5 or more failed login attempts. The second requirement I was able to implement by trapping failed login attempts in the AppListener (See, AppListener code below). The authenticationDao.updateUserFailedLoginAttempts method increments a count that is stored into the authentication repository. And in the CustomDaoAuthenticationProvider.isPasswordCorrect method I check the failedLoginAttempts count, if it exceeds 5 then set the return results to false (See, CustomDaoAuthenticationProvider code below).

              Code:
              public class AppListener implements  ApplicationListener, InitializingBean,
                      ApplicationContextAware {
                  //~ Static fields/initializers =============================================
              
                  private static final Log logger = LogFactory.getLog(AppListener.class);
              
                  private ApplicationContext context;
              
                  private CustomAuthenticationDao authenticationDao;
                 
                  //~ Methods ================================================================
              
                  public void setApplicationContext(ApplicationContext applicationContext)
                          throws BeansException {
                      this.context = applicationContext;
                  }
                                  
                  public void setAuthenticationDao(CustomAuthenticationDao authenticationDao) {        
                      this.authenticationDao = authenticationDao;
              
                  }
              
                  public CustomAuthenticationDao getAuthenticationDao() {
                      return authenticationDao;
                  }
              
                  public ApplicationContext getContext() {
                      return context;
                  }
              
                  public void afterPropertiesSet() throws Exception {
                      if (this.authenticationDao == null) {
                          throw new IllegalArgumentException("An Authentication DAO must be set");
                      }
                  }
              
                  public void onApplicationEvent(ApplicationEvent event) {
                      if (event instanceof AuthenticationFailurePasswordEvent) {
                          AuthenticationFailurePasswordEvent authEvent = (AuthenticationFailurePasswordEvent) event;
                          
                          authenticationDao.updateUserFailedLoginAttempts(authEvent.getUser());
              
                          if (logger.isWarnEnabled()) {
                              logger.warn(
                                  "Authentication failed due to incorrect password for user: "
                                  + authEvent.getUser().getUsername() + "; details: "
                                  + authEvent.getAuthentication().getDetails());
                          }
                      }
              
                      if (event instanceof AuthenticationFailureUsernameOrPasswordEvent) {
                          AuthenticationFailureUsernameOrPasswordEvent authEvent = (AuthenticationFailureUsernameOrPasswordEvent) event;
              
                          authenticationDao.updateUserFailedLoginAttempts(authEvent.getUser());
                          
                          if (logger.isWarnEnabled()) {
                              logger.warn(
                                  "Authentication failed due to invalid username or password: "
                                  + authEvent.getUser().getUsername() + "; details: "
                                  + authEvent.getAuthentication().getDetails());
                          }
                      }
              
                      if (event instanceof AuthenticationSuccessEvent) {
                          AuthenticationSuccessEvent authEvent = (AuthenticationSuccessEvent) event;
              
                          authenticationDao.saveUserAuthDetails(authEvent.getUser(),
                              authEvent.getAuthentication().getDetails());
              
                          if (logger.isInfoEnabled()) {
                              logger.info("Authentication success for user: "
                                      + authEvent.getUser().getUsername() + "; details: "
                                      + authEvent.getAuthentication().getDetails());
                          }
                      }
                  }
                 
              }
              Code:
              public class CustomDaoAuthenticationProvider extends DaoAuthenticationProvider {
              
                  protected boolean isPasswordCorrect(Authentication authentication,
                      UserDetails user) {
                      boolean result = super.isPasswordCorrect(authentication, user);
                      Object details = authentication.getDetails();
                      if (result) {
                          if ((user instanceof MpUser) && (details instanceof AuthDetails)) {
                              AuthDetails authDetails = (AuthDetails) details;
                              MpUser mpUser = (MpUser) user;
              
                              String authSessionId = authDetails.getSessionId();
                              String userSessionId = mpUser.getLastSessionId();
              
                              if ((authSessionId != null) && (userSessionId != null)) {
                                  if (!authSessionId.equals(userSessionId)) {
                                      /*
                                       * TODO need to put exception logic here maybe
                                       */
                                      /*
                                       * TODO Commeted out result because of issues.
                                       * result = false;
                                       */
                                  }
                              }
              
                              Long failedLoginAttempts = mpUser.getFailedAttemptsCount();
              
                              /*
                               * Limit users to only 5 failed login attempts
                               */
                              if (failedLoginAttempts != null) {
                                  if (failedLoginAttempts.longValue() >= 5) {
                                      result = false;
                                  }
                              }
                          }
                      }
                      return result;
                  }
              }



              Thank you again for any comments that you may provide.

              Guy

              Comment


              • #8
                Where are you actuallly storing the login attempts? Does the provider hold it? You are not storing it in the database, are you?

                Comment


                • #9
                  The login attempts are being stored in the database.

                  Comment


                  • #10
                    Originally posted by Zenobia
                    I was planning to use the HttpSessionListener to catch the timeout event and set the LastSessionId = null in the database
                    The problem I'm having with the HttpSessionListener is that when the session times out the Authentication object in the Secure Context is null. So, I can not set the LastSessionId to null because I do not know which user in the database to update.

                    Comment


                    • #11
                      ContextHolder should be null unless the web container is actually processing a request for the principal. Can you get the authentication from the HttpSession?

                      Comment


                      • #12
                        gtuberson,

                        Thanks. I ended up storing it in there as well.

                        Dan

                        Comment


                        • #13
                          What is the easiest way to lock users after a predefined number of attempts regardless of the session aspects mentioned in this thread ?

                          I need to implement this for a new application and would appreciate any help.

                          Comment


                          • #14
                            closing the browser

                            Limiting the logins is working using the approach described above, except when the user close the browser without loging out.

                            If the user close the browser, the corresponding session will still be valid in the server, since there is no logout or time out yet.

                            If the user close the browser and then opens it again and tries to login, it will get the message that its user id is already login somewhere. The user would have to wait for that session to timeout.. this is not good.

                            I am trying to find out a way to capture the window onclosed event.. but there is no such event in javascript.. for IE there is a way to capture it with the window.onunload envent + some javascript logic to differentiate a page unload from a real window closed event; however I haven't found anything for Netscape or Mozilla..

                            any ideas? it will be appreciatted.

                            If I can not capture the window onclose event, I was thinking about
                            using two different daoAuthenticationProvider, one for the Login, and the other for the periodic authentication that ACEGI does behind the scenes. The login authentication would stored the sessionId in the database. The periodic authentication will compare the sessionId of the HTTPserveletrquest with the database, if different it will invalidate the http session, sending a message like "another user has login with this userId and password".. But I haven't tried at all this approach yet.. I am working in the window onclose event[/quote]

                            Comment


                            • #15
                              I am pretty sure you can't capture a window on close event unless the application created the window. Thus many webapps have a "start using the app" link which opens a popup, as the popup can have its close event captured and if desired redirect to a logout page instead (it can also take away the URL bar and back button, which is often useful).

                              I cannot see how at a web browser level you can differentiate between:

                              1. A closed browser, where the corresponding HttpSession has not timed out, so you want to allow another login; and

                              2. An still active browser, and the user loading another active browser, and login being allowed (just in case #1 applied).

                              Perhaps the easiest approach is to end sessions using HttpSessionListener and relying on users logging out (perhaps with the help of window close event capturing), which will resolve 80% of your issues. For the remaining 20%, maybe display a "this user is already logged in" message and ask for confirmation the user is no longer active. If they confirm, set a database column, "session_terminated", to true. Then if a subsequent request comes in for the original session, your authentication provider could throw a TerminatedSessionReusedException.

                              Comment

                              Working...
                              X