Monday, October 13, 2014

A Real World Example of GWT 2.x, MySQL, and MVP: Part 4

Part 1: Getting Started
Part 2: Putting the Framework Together
Part 3: Finally Making the Page Content
Part 4: User Authentication

User Authentication

Now that we have our website up and running let's protect certain pages and restrict functionality that we wouldn't want to give to all users who visit your site with user authentication by using a Role Based Access Control (RBAC) approach. Don't know what RBAC is? You're best bet is to Google some tutorials and purchase some books because it can get real complicated real fast. Because there are so many approaches I will take the most basic and general approach which you can modify for whatever variant you wish to use.  Now at this time let me say it is VERY IMPORTANT that access to your site should be done over SSL, that means your website should be "https" and not "http". Information like passwords will be sent over in plain text and using SSL will fix a huge security layer on top of your website.

There are 3 basic principles of Role Based Access Control that we will need to know first:
  • User - An person (or service account) who can perform functions on your webpage.
  • Role - A job function that defines authority level.
  • Permission - The authority to someone to perform an operation.
The relationship between these principles looks like this:
Image source: http://www.isaca.org/Journal/Past-Issues/2008/Volume-3/Pages/JOnline-Role-Engineering-The-Cornerstone-of-RBAC1.aspx
Basically:
  • A User has one or many Roles
  • A Role has one or many Permissions
Next we need to decide what roles we will need and what type of access we would like to give to each of our roles. Having a good solid strategy ahead of time will make your security stronger and save you time later on.  In a real website we could add roles for super users, power users, super power users, each with their own levels of access, but for this example I will keep things very simple and have only 2 types of roles:
  • Administrator
  • User
Finally we should decide what each of these roles can and can't do.  For example we don't want an average user able to change things we don't want them to.  Remember a user can have multiple roles, so there is no need to duplicate some permissions. 
  • Administrator
    • View admin page
    • Add search engine
    • Edit search engine
    • Remove search engine
    • Edit user settings
  • User
    • Log-in
    • Edit own user settings
The basic idea is to be able to check to see if a user has permission to perform an action, not what role they have. We will do this in 2 ways. On the database side we will create a system that will use triggers and functions to check if a user has authority to perform an action, and on the client side we will store the list of permissions with the user object and perform checks on that list for a permission.

Create database tables

I love it when a plan comes together! Now let's get moving. To set this up in a database we will need 6 tables, 5 of them will store everything we will need for RBAC authentication, and 1 table to store user sessions. A session is how we will actually be authenticating a user after he or she logs into the system. A session should be unique per user and very difficult to predict and have an end date requiring the user to re-login at some point.  You can create each table manually or also download a SQL script here.
    Table 1: users
    Stores user information and any metadata you want to keep. Things like, address, city, state, gender, etc... Heck be as creative as you want because if you're lucky you can eventually sell this information to advertisers for a profit! Evil laugh! MWA-HA-HA-HA! (Seriously though don't be a jerk and do that.)
    CREATE TABLE `users` (
      `id` int(11) NOT NULL AUTO_INCREMENT,
      `username` varchar(99) NOT NULL COMMENT 'SYMC username',
      `password` varchar(99) NOT NULL COMMENT 'User password',
      `first_name` varchar(99) DEFAULT NULL COMMENT 'User first name',
      `last_name` varchar(99) DEFAULT NULL COMMENT 'User last name',
      `email` varchar(99) DEFAULT NULL COMMENT 'User email address',
      `created_by` int(11) NOT NULL COMMENT 'Created by',
      `created_on` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'Date created on',
      `updated_by` int(11) DEFAULT NULL COMMENT 'Updated by',
      `updated_on` timestamp NULL DEFAULT NULL COMMENT 'Date updated on',
      `active` tinyint(1) DEFAULT '1' COMMENT 'Is the user active',
      PRIMARY KEY (`id`),
      UNIQUE KEY `id_UNIQUE` (`id`)
    ) ENGINE=InnoDB COMMENT='User of the system';
    
    

    Table 2: roles
    Stores the roles that can be assigned to a user.
    CREATE TABLE `roles` (
      `id` int(11) NOT NULL AUTO_INCREMENT,
      `name` varchar(45) DEFAULT NULL,
      `description` varchar(45) DEFAULT NULL,
      PRIMARY KEY (`id`)
    ) ENGINE=InnoDB COMMENT='User roles';
    

    Table 3: permissions
    Permissions hold the whole security together.  They tell us that user X can do Y with object Z. The name will be a short name to description what the permission allows and will be our alternate key, action will be something like "add", "remove", "modify", and presenter will be the webpage (object) we want to access.
    CREATE TABLE `permissions` (
      `id` int(11) NOT NULL AUTO_INCREMENT,
      `name` varchar(99) DEFAULT NULL,
      `action` varchar(45) DEFAULT NULL,
      `presenter` varchar(45) DEFAULT NULL,
      PRIMARY KEY (`id`)
    ) ENGINE=InnoDB COMMENT='User permissions';
    
    

    Table4: user_roles
    This will contain many-to-many references for users and their assigned roles. We'll also add some constraints to delete rows if a user or a role is ever deleted from their respective tables. That should keep this table nice and tidy.
    CREATE TABLE `user_roles` (
      `id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'Unique identifier',
      `user_id` int(11) DEFAULT NULL COMMENT 'User identifier',
      `role_id` int(11) DEFAULT NULL COMMENT 'Role identifier',
      PRIMARY KEY (`id`),
      UNIQUE KEY `id_UNIQUE` (`id`),
      KEY `FK_user_roles_users_idx` (`user_id`),
      KEY `FK_user_roles_roles_idx` (`role_id`),
      CONSTRAINT `FK_user_roles_roles` FOREIGN KEY (`role_id`) REFERENCES `roles` (`id`) ON DELETE CASCADE ON UPDATE NO ACTION,
      CONSTRAINT `FK_user_roles_users` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE NO ACTION
    ) ENGINE=InnoDB COMMENT='User roles';
    
    

    Table 5: role_permissions
    Ah, ah, ah, ah, table 5! Table 5! Not a Simpsons fan, eh? Never-mind then. This will contain many-to-many references for roles and their permissions. Again we'll add constraints to keep the table neat.
    CREATE TABLE `role_permissions` (
      `id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'Unique identifier',
      `role_id` int(11) DEFAULT '0' COMMENT 'Role identifier',
      `permission_id` int(11) DEFAULT '0' COMMENT 'Permission identifier',
      PRIMARY KEY (`id`),
      UNIQUE KEY `id_UNIQUE` (`id`),
      KEY `FK_role_permissions_roles_idx` (`role_id`),
      KEY `FK_role_permissions_permissions_idx` (`permission_id`),
      CONSTRAINT `FK_role_permissions_permissions` FOREIGN KEY (`permission_id`) REFERENCES `permissions` (`id`) ON DELETE CASCADE ON UPDATE NO ACTION,
      CONSTRAINT `FK_role_permissions_roles` FOREIGN KEY (`role_id`) REFERENCES `roles` (`id`) ON DELETE CASCADE ON UPDATE NO ACTION
    ) ENGINE=InnoDB COMMENT='Role permissions';
    

    Table 6: user_sessions
    This table will contain user sessions. It will help us in our system security to track when a user has logged in, if the session is still active, and also provide some mechanism for metrics at a later date if we so wish.
    CREATE TABLE `user_sessions` (
      `id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'Unique identifier',
      `user_id` int(11) NOT NULL DEFAULT '0' COMMENT 'User identifier',
      `session_id` varchar(45) DEFAULT NULL COMMENT 'Unique client session identifier',
      `ip_address` varchar(45) NOT NULL COMMENT 'User IP address',
      `created_on` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'Date created on',
      `ended_on` timestamp NULL DEFAULT NULL COMMENT 'Date session will end on',
      PRIMARY KEY (`id`)
    ) ENGINE=InnoDB;
    
    

    Create Some Triggers

    If you are not familiar with databases this might start getting a bit complicated, but don't worry it really isn't. We are going to create a trigger on the "users" table to automatically assign any new user the role "user" which will give them the ability to log into the system, edit their own user settings, and any other permissions we give the user role. This will save us the time from having to add this role manually every time a new user signs up or is registered in the system.
    DELIMITER ;;
    CREATE TRIGGER `users_AINS` AFTER INSERT ON `users` FOR EACH ROW
    BEGIN
        -- Get the user role id
        DECLARE role_id INT;
        SET role_id = (SELECT `id` FROM `roles` WHERE name = 'user'); 
    
        -- Insert the new user with a user role
        INSERT INTO `user_roles` (`user_id`, `role_id`) VALUES (NEW.id, role_id);
    END ;;
    DELIMITER ;
    

    Insert Test Data

    For the sake of getting something out of this tutorial what I will do is create a test user and set up our roles and permissions directly within the database. What should really happen is we create some administrator pages in GWT to manage users, roles, and permissions... and we will get to that (I think.)  In the mean-time let's set up some test data:

    Insert 1: roles
    INSERT INTO `roles` VALUES 
    (1,'administrator','Administrator of the system'),
    (2,'user','User of the system');
    

    Insert 2: permissions
    INSERT INTO `permissions` VALUES 
    (1,'allow_login','login','login'),
    (2,'allow_show_admin','show','admin'),
    (3,'allow_modify_engine','modify','engine'),
    (4,'allow_add_engine','add','engine'),
    (5,'allow_remove_engine','remove','engine'),
    (6,'allow_modify_user','modify','user'),
    (7,'allow_modify_self','modify','self');
    

    Insert 3: users
    INSERT INTO `users` VALUES 
    (1,'admin','test','Admin','Test','admin@mom.com',1,NULL,NULL,NULL,1),
    (2,'user','test','User','Test','user@mom.com',1,NULL,NULL,NULL,1);
    

    STOP RIGHT THERE! I wanna know right now. Before we go any further do you now have rows in the "user_roles" table? If so it should look like this:
    iduser_idrole_id
    112
    222
    This means that when we created the users above in "Insert 3" they were automatically assigned the "user" role by the trigger 'users_AINS'. Now we don't have to worry about making sure every user can log in when they register at our site. Cool!

    Insert 4: user_roles
    We already have some user roles thanks to the trigger, but now we just have to give the 'admin' user the 'administrator' role.
    INSERT INTO `user_roles` (`user_id`, `role_id`) VALUES 
    (1,1);
    

    Insert 5: role_permissions
    Do the following insert and it will assign the permissions to the roles that we decided above. Again normally we would have pages in an Administrator section on our website to manage what permissions belong to what roles, and I highly recommend building those, but once again let's just get some test data so we can play with this tutorial.
    • Administrator (role_id: 1)
      • View admin page (permission_id: 2)
      • Add search engine (permission_id: 4)
      • Edit search engine (permission_id: 3)
      • Remove search engine (permission_id: 5)
      • Edit user settings (permission_id: 6)
    • User (role_id: 2)
      • Log-in (permission_id: 1)
      • Edit own user settings (permission_id: 7)
    INSERT INTO `role_permissions` VALUES 
    (1,1,2),
    (2,1,3),
    (3,1,4),
    (4,1,5),
    (4,1,6),
    (5,2,1),
    (6,2,7);
    

    I think that gets us to a place where we have a normal user and an administrator. Sweet. Now let's start adding to our website.

    Refactoring the Server

    I'm glad you stepped through my tutorial on MySQL and GWT (if you haven't then right now might be a good time right now since that is where all the code I will be referencing is) but now that we are working on a much larger and scalable application we need to make some tiny adjustments to my previous code. Here is the dilemma, the authentication stuff and the database stuff are going to be used together a lot. What I want to avoid is having multiple RPC guys on my client, because that's not where I want to do all of the business logic for authentication. I want to check to see if a user has permission to insert a record or get certain records before sending that data back to the client. So what I want to avoid is this:


    Instead I want to add another layer of abstraction by creating a class to group all off the back-end services on the server-side in order to deal with all of the business logic, such as the authentication crap we're working on now and the data layer. This will have the benefit of separation of concerns and saving time with RPC calls.

     To start we need a few things. I will first create a custom Exception called "NotAuthenticatedException" which will help communicate specific failures from my server to my client. Of course you can skip this and send everything as a plain-old Exception, but what if you want to branch your logic depending on exactly what the problem was on the server-side? Maybe I want to limit the number of times a user has to authenticate? It will be easier to add this up by catching NotAuthenticatedExceptions than looking at something like the error message. So here we go:

    NotAuthenticatedException.java
    package com.example.searchengines.shared;
    
    public class NotAuthenticatedException extends Exception {
    
        private static final long serialVersionUID = 1L;
    
        public NotAuthenticatedException() {
            this("Login failed; Invalid username or password");
        }
    
        public NotAuthenticatedException(String arg0) {
            super(arg0);
        }
    
        public NotAuthenticatedException(Throwable arg0) {
            super(arg0);
        }
    
        public NotAuthenticatedException(String arg0, Throwable arg1) {
            super(arg0, arg1);
        }
    }
    

    Now let's add modify and add new methods to our DBConnection and DBConnectionAsync services that we created in my previous blog to check user authentication and log a user out of the system:

    DBConnection.java
    package com.rappa.lapizza.client.service;
    
    public interface DBConnection extends RemoteService
    {
        /**
         * Authenticates the User with the specified username and password
         * @param username User's name
         * @param password User's password
         * @return User
         * @throws Exception
         */
        public User authenticateUser(String username, String password) throws NotAuthenticatedException, Exception;
    
        /**
         * Logs out a user session
         * @param jSessionId User session to log out
         * @throws Exception 
         */
        public void logout(String jSessionId) throws Exception;
    

    DBConnectionAsync.java
    package com.example.searchengines.client.service;
    
    public interface DBConnectionAsync
    {
        /**
         * Authenticates the User with the specified username and password
         * @param username User's name
         * @param password User's password
         * @param callback Async return of the User
         * @return User
         * @throws Exception
         */
        public void authenticateUser(String username, String password, AsyncCallback<User> callback) throws NotAuthenticatedException, Exception;
    
        /**
         * Logs out a user session
         * @param jSessionId User session to log out
         * @param callback Nothing
         */
        public void logout(String jSessionId, AsyncCallback<Void> callback) throws Exception;
    

    Sorry MySQLConnection but we are stripping you of your rank of RemoteServiceServlet. Hand over your badge and your gun. Let's remove the "extends RemoteServiceServlet" but keep "implements DBConnection" in order to no longer use this class as our remote service.

    MySQLConnection.java
    package com.example.searchengines.server;
    
    public class MySQLConnection extends RemoteServiceServlet implements DBConnection {
    

    Now let's add a new class to act as a repository for the multiple back-end server processes. If I thought ahead with the previous tutorials I would maybe have named this better, but whatever fuck-it, I'll change it all later. This class will basically wrap the MySQLConnection class and pass most of the results on.

    DBConnectionImpl.java
    package com.example.searchengines.server;
    
    /**
     * @author Austin_Rappa
     *
     */
    public class DBConnectionImpl extends RemoteServiceServlet implements DBConnection {
    
        private MySQLConnection mysql = new MySQLConnection();
        
        /**
         * Authenticates the User with the specified username and password
         * @param username User's name
         * @param password User's password
         * @return User
         * @throws Exception
         */
        public User authenticateUser(String username, String password) throws Exception, NotAuthenticatedException {
            User user = null;
            
            user = mysql.authenticateUser(username, password);
            if (user != null) {
                //Store session id
                HttpServletRequest httpServletRequest = this.getThreadLocalRequest();
                HttpSession session = httpServletRequest.getSession(true);
                String jSessionId = session.getId();
                session.setAttribute("JSESSIONID", jSessionId);
                
                //Insert database session
                mysql.insertUserSession(user, jSessionId, getThreadLocalRequest().getRemoteAddr());
            }
            
            return user;
        }
    
        /**
         * Logs out a user session
         * @param jSessionId User session to log out
         * @throws Exception 
         */
        public void logout(String jSessionId) throws Exception {
            //Remove session id from http header
            HttpServletRequest httpServletRequest = this.getThreadLocalRequest();
            HttpSession session = httpServletRequest.getSession();
            session.removeAttribute("JSESSIONID");
            
            //Logout from database session
            mysql.logout(jSessionId);
        }
    
        /**
         * Gets the SearchEngines
         * @return SearchEngine
         * @throws Exception
         */
        public List<SearchEngine> getSearchEngines() throws Exception {
            return mysql.getSearchEngines();
        }
    }
    

    Finally we have to update the web.xml file to use the new DBConnectionImpl.java class instead of MySQLConnection.java class.

    web.xml
        <servlet>
            <servlet-name>mySQLConnection</servlet-name>
            <servlet-class>com.example.searchengines.server.DBConnectionImpl</servlet-class>
        </servlet>
    

    Permissions in the Website

    First thing I am going to do is create an enum with my list of permissions. You don't have to do this and in-fact can be a pain as your website grows and strings work just fine as permission names shouldn't change. For me I don't mind. Add an enum to your 'shared' package called 'Permission.java'.

    Permissions.java
    package com.example.searchengines.shared;
    
    public enum Permission {
        ALLOW_LOGIN,
        ALLOW_SHOW_ADMIN,
        ALLOW_MODIFY_ENGINE,
        ALLOW_ADD_ENGINE,
        ALLOW_REMOVE_ENGINE,
        ALLOW_MODIFY_USER,
        ALLOW_MODIFY_SELF
    }
    

    Next edit your user class in the "shared" package to ad a list of permissions. If you chose to not use an enum for your permissions just change the List type to String and be on your way. What we will do is store a list of the permissions assigned to the user. This will allow our presenter classes to check to see if this user has permission to perform an action or show a control.

    User.java
    package com.example.searchengines.shared;
    
    public class User implements IsSerializable {
    
        private List<Permission> permissions;
    
        ...
    
        /**
         * Gets the user permissions
         * @return the permissions
         */
        public List<Permission> getPermissions() {
            return permissions;
        }
    
        /**
         * Sets the users permissions
         * @param permissions the permissions to set
         */
        public void setPermissions(List<Permission> permissions) {
            this.permissions = permissions;
        }
    

    Let's standardize on the length of time we would like the session to be active. 24 hours seems good so let's go with that. If you want to change to a week just change that "1" at the end of the formula to a "7".

    Property.java
    package com.example.searchengines.client;
    
    public class Property {
        
        public static final long SessionDuration = (1000 * 60 * 60 * 24) * 1; //duration remembering login.     
    


    The last step for the server-side code is adding methods to the MySQLConnection.java class to connect to the database, look up the username and password (which is right now stored as plain-text but we will change later) and return a User object with the user's information, otherwise raise an NotAuthenticatedException if we could not authenticate.

    MySQLConnection.java
    package com.example.searchengines.server;
    
    public class MySQLConnection implements DBConnection {
    
        /**
         * Authenticates the User with the specified username and password
         * @param username User's name
         * @param password User's password
         * @return User
         * @throws Exception
         */
        public User authenticateUser(String username, String password) throws Exception, NotAuthenticatedException {        
            Connection conn = null;
            PreparedStatement pstmt = null;
            ResultSet result = null;
            User user = null;
    
            try {
                conn = getConnection();
                
                boolean authenticated = true;
                //TODO: Authenticate encrypted password.
                
                if (authenticated) {
                    pstmt = conn.prepareStatement("SELECT * FROM users WHERE username = ? AND password = ?");
                    pstmt.setString(1, username);
                    pstmt.setString(2, password);
                    result = pstmt.executeQuery();
                    
                    //Check for null results
                    if (result.isBeforeFirst() ) {
                        //Loop through results
                        while (result.next()) {
                            //Create return object
                            user = new User();
                            user.setId(result.getDouble("id"));
                            user.setUserName(result.getString("username"));
                            user.setFirstName(result.getString("first_name"));
                            user.setLastName(result.getString("last_name"));
                            user.setEmail(result.getString("email"));
                            user.setPermissions(this.getUserPermissions(result.getDouble("id")));
                        }
                    }
                    else {
                        throw new NotAuthenticatedException();
                    }
                }
                else {
                    throw new NotAuthenticatedException();
                }
            } catch (SQLException sqle) {
                logger.error("SQL error in authenticateUser(" + username + "): " + sqle.getMessage() + "\n" + sqle.getStackTrace().toString());
                throw new Exception(sqle);
            } finally {
                result.close();
                pstmt.close();
                conn.close();
            }
    
            return user;
        }
        
        /**
         * Logs a user login session
         * @param user User who logged into session
         * @param session_id Session identifier
         * @param ip_address User's IP address
         * @throws Exception     
         */
        void insertUserSession(User user, String session_id, String ip_address) throws Exception {
            Connection conn = null;
            PreparedStatement pstmt = null;
    
            try {
                // Get database connection
                conn = getConnection();            
    
                // Insert new user
                pstmt = conn.prepareStatement("INSERT INTO user_sessions (user_id, session_id, ip_address, created_on, ended_on) VALUES (?, ?, ?, ?, ?)", Statement.RETURN_GENERATED_KEYS);
                pstmt.setDouble(1, user.getId());
                pstmt.setString(2, session_id);
                pstmt.setString(3, ip_address);
                pstmt.setTimestamp(4, new Timestamp(new java.util.Date().getTime()));
                pstmt.setTimestamp(5, new Timestamp(new java.util.Date().getTime() + Property.SessionDuration));
                 
                //Execute sql statement
                if (pstmt.executeUpdate() == 0) {            
                    logger.error("Error in insertUserSession(): Bad log of session.");
                }
            } catch (SQLException sqle) {
                logger.error("Error in insertUserSession(): " + sqle.getMessage() + "\n" + sqle.getStackTrace().toString());
                throw new Exception(sqle);
            } catch (Exception e) {
                throw new Exception(e);
            } finally {
                // Cleanup
                pstmt.close();
                conn.close();
            }
        }
        
        /**
         * Gets the permission list of a user
         * @param user_id User identifier
         * @throws Exception     
         */
        private List<Permission> getUserPermissions(Double user_id) throws Exception {
            Connection conn = null;
            PreparedStatement pstmt = null;
            ResultSet result = null;
            List<Permission> permissions = new ArrayList<Permission>();
    
            try {
                // Get database connection
                conn = getConnection();            
    
                //Get user permissions
                pstmt = conn.prepareStatement("SELECT permissions.name FROM user_roles LEFT JOIN role_permissions ON role_permissions.role_id = user_roles.role_id LEFT JOIN permissions ON permissions.id = role_permissions.permission_id WHERE user_roles.user_id = ?");
                pstmt.setDouble(1, user_id);
                 
                //Execute sql statement
                result = pstmt.executeQuery();
                while (result.next()) {
                    Permission permission = Permission.valueOf(result.getString("name").toUpperCase());
                    permissions.add(permission);
                }
            } catch (SQLException sqle) {
                logger.error("Error in getUserPermissions(): " + sqle.getMessage() + "\n" + sqle.getStackTrace().toString());
                throw new Exception(sqle);
            } catch (Exception e) {
                throw new Exception(e);
            } finally {
                // Cleanup
                result.close();
                pstmt.close();
                conn.close();
            }
            
            return permissions;
        }
        
         /**
          * Logs out a user session
          * @param jSessionId User session to log out
          * @throws Exception 
          */
        public void logout(String jSessionId) throws Exception {
            Connection conn = null;
            PreparedStatement pstmt = null;
    
            try {            
                // Get database connection
                conn = getConnection();            
    
                // Insert new user
                pstmt = conn.prepareStatement("UPDATE user_sessions SET ended_on = NOW() WHERE session_id = ?");
                pstmt.setString(1, jSessionId);
                 
                //Execute sql statement
                if (pstmt.executeUpdate() == 0) {
                    throw new Exception("Bad log of session.");
                }
            } catch (SQLException sqle) {
                logger.error("Error in logout(): " + sqle.getMessage() + "\n" + sqle.getStackTrace().toString());
                throw new Exception(sqle);
            } catch (Exception e) {            
                logger.error("Error in logout(): " + e.getMessage() + "\n" + e.getStackTrace().toString());
                throw new Exception(e);
            } finally {
                // Cleanup
                pstmt.close();
                conn.close();
            }   
        }
    


    Modifying the User Interface

    Let's create some events! Events in GWT are used to broadcast a notification to the rest of the code in your project and any code that is listening for that notification will be called. What we want to do is to notify our header page (and any other page that is listening) that the user has successfully logged-in and take actions to show a different collection of buttons, like a log out button.  In your "client" package created a sub-package called "event" and create the following classes:

    LogInEvent.java
    package com.example.searchengines.client.event;
    
    import com.google.gwt.event.shared.GwtEvent;
    
    public class LogInEvent extends GwtEvent<LogInEventHandler>{
        
          public static Type<LogInEventHandler> TYPE = new Type<LogInEventHandler>();
          
          public LogInEvent() {
          }
    
          @Override
          public Type<LogInEventHandler> getAssociatedType() {
            return TYPE;
          }
    
          @Override
          protected void dispatch(LogInEventHandler handler) {
            handler.onLogIn(this);
          }
    }
    

    LogInEventHandler.java
    package com.example.searchengines.client.event;
    
    import com.google.gwt.event.shared.EventHandler;
    
    public interface LogInEventHandler extends EventHandler {
          void onLogIn(LogInEvent event);
    }
    

    LogOutEvent.java
    package com.example.searchengines.client.event;
    
    import com.google.gwt.event.shared.GwtEvent;
    
    public class LogOutEvent extends GwtEvent<LogOutEventHandler>{
        
          public static Type<LogOutEventHandler> TYPE = new Type<LogOutEventHandler>();
          
          public LogOutEvent() {
          }
    
          @Override
          public Type<LogOutEventHandler> getAssociatedType() {
            return TYPE;
          }
    
          @Override
          protected void dispatch(LogOutEventHandler handler) {
            handler.onLogOut(this);
          }
    }
    

    LogOutEventHandler.java
    package com.example.searchengines.client.event;
    
    import com.google.gwt.event.shared.EventHandler;
    
    public interface LogOutEventHandler extends EventHandler {
          void onLogOut(LogOutEvent event);
    }
    
    

    This is where we add the most important piece, the guy that ties this all together on the client-side, the "REMEMBER_ME" cookie. Create a presenter for the log-in page in your "presenter" package. This will hold the log-in page logic. For our view we'll need a text box for the username and password, a button to accept the form, and a button to take the user to a password recovery page. If the user authentication comes back from the server okay then we will create a cookie called "REMEMBER_ME" that has an expiration date, this way if a user returns to your site we can check if that session is still active and retrieve the session.

    LogInPresenter.java
    package com.example.searchengines.client.presenter;
    
    public class LoginPresenter implements Presenter {
    
        public interface Display {
            HasClickHandlers getLogin();
            HasClickHandlers getForgotPassword();
            HasKeyPressHandlers getKeyPressUsername();
            HasKeyPressHandlers getKeyPressPassword();
            HasText getUsername();
            HasText getPassword();
            Widget asWidget();
            void showError(String errormessage);
            void hideError();
        }
    
        private final DataFactory factory;
        private final Display display;
    
        /**
         * 
         * @param factory
         * @param view
         */
        public LoginPresenter(DataFactory factory, Display view) {
            this.factory = factory;
            this.display = view;
        }
        
        /**
         * 
         */
        public void bind() {
    
            display.getLogin().addClickHandler(new ClickHandler() {
                public void onClick(ClickEvent event) {
                    try {
                        if (!display.getUsername().getText().isEmpty() && !display.getPassword().getText().isEmpty()) {
                            factory.getRpcService().authenticateUser(display.getUsername().getText(), display.getPassword().getText(), new AsyncCallback<User>() {
                                public void onFailure(Throwable caught) {
                                    display.showError("Error authenticating user. " + caught.getMessage());
                                }
        
                                public void onSuccess(final User user) {
                                    if (user != null) {
                                        //Set session cookie for 1 day expire. Set secure to false while in development but true in production.
                                        String sessionID = Cookies.getCookie("JSESSIONID");
                                        Date expires = new Date(System.currentTimeMillis() + Property.SessionDuration);
                                        Cookies.setCookie("REMEMBER_ME", sessionID, expires, null, "/", false);
                                        
                                        //Set user in the data factory
                                        factory.setUser(user);
    
                                        //Broadcast log-in notification. Presenters like the HeaderPresenter are listening for this.                            
                                        factory.getEventBus().fireEvent(new LogInEvent());              
                                        
                                        //Go to home page
                                        History.newItem("home");
                                    }
                                    else {
                                        display.showError("Username or password is invalid.");
                                    }
                                        
                                }                        
                            });
                        }
                        else {
                            display.showError("Please enter your username and password.");
                        }
                    } 
                    catch (Exception caught) {
                        display.showError("Error authenticating user. " + caught.getMessage());
                    }
                }            
            });
            
            display.getKeyPressUsername().addKeyPressHandler(new KeyPressHandler() {
                public void onKeyPress(KeyPressEvent event) {
                    if (event.getNativeEvent().getKeyCode() == KeyCodes.KEY_ENTER) {
                        display.getLogin().fireEvent(new ClickEvent(){});
                    }
                }            
            });
            
            display.getKeyPressPassword().addKeyPressHandler(new KeyPressHandler() {
                public void onKeyPress(KeyPressEvent event) {
                    if (event.getNativeEvent().getKeyCode() == KeyCodes.KEY_ENTER) {
                        display.getLogin().fireEvent(new ClickEvent(){});
                    }
                }            
            });        
        }
        
        /**
         * 
         */
        public void go(final HasWidgets container) {
            bind();
            container.clear();
            container.add(display.asWidget());
        }
    }
    
    

    Now let's create the view that will contain our display widgets.

    LoginView.java
    package com.example.searchengines.client.view;
    
    public class LoginView extends View implements LoginPresenter.Display {
        
        private Button forgotpasswordButton = new Button(constants.forgotpassword());
        private Button loginButton = new Button(constants.login());
        private TextBox usernameTextBox = new TextBox();
        private PasswordTextBox passwordTextBox = new PasswordTextBox();
        private Label errorLabel = new Label();
    
        public LoginView() {    
        
            //Main panel
            VerticalDivPanel mainPanel = new VerticalDivPanel();
            mainPanel.addStyleName("login");
            initWidget(mainPanel);    
            
            //Define controls
            Label usernameLabel = new Label(constants.loginUsername());
            usernameLabel.setStyleName("bodytext-bold");
            Label passwordLabel = new Label(constants.loginPassword());
            passwordLabel.setStyleName("bodytext-bold");
                    
            loginButton.setStyleName("loginbutton");
            loginButton.setTitle("Log in");
            
            forgotpasswordButton.setStyleName("hyperlink");
            
            //Form table
            FlexTable loginTable = new FlexTable();
            loginTable.setStyleName("loginform");
            
            //Add controls to form
            loginTable.setWidget(0, 0, usernameLabel);
            loginTable.setWidget(0, 1, usernameTextBox);
            loginTable.setWidget(1, 0, passwordLabel);                
            loginTable.setWidget(1, 1, passwordTextBox);
            loginTable.setWidget(2, 1, loginButton);
            loginTable.setWidget(3, 1, forgotpasswordButton);
            mainPanel.add(loginTable); 
            
            Scheduler.get().scheduleDeferred(new Command() {
                public void execute() {
                    usernameTextBox.setFocus(true);
                }
              });
            
            //Error message
            this.hideError();
            errorLabel.setStyleName("errortext");
            mainPanel.add(errorLabel);        
        }
        
        public Widget asWidget() {
            return this;
        }        
    
        public HasClickHandlers getLogin() {
            return this.loginButton;
        }
    
        public HasText getUsername() {
            return this.usernameTextBox;
        }
    
        public HasText getPassword() {
            return this.passwordTextBox;
        }
    
        public void showError(String errormessage) {
            errorLabel.setText(errormessage);
            errorLabel.setVisible(true);
        }
    
        public void hideError() {
            errorLabel.setText("");
            errorLabel.setVisible(false);
        }
    
        public HasKeyPressHandlers getKeyPressUsername() {
            return this.usernameTextBox;
        }    
        
        public HasKeyPressHandlers getKeyPressPassword() {
            return this.passwordTextBox;
        }
    
        public HasClickHandlers getForgotPassword() {
            return this.forgotpasswordButton;
        }
    }
    

    Now that we have our Log-in header and presenter let's modify our project so our users can browse to those pages when they click the LogIn button on the header. Open the HeaderPresenter class and change "TODO 10" with the highlighted code below that inserts a new browser history and highlights the LogIn header button.

    HeaderPresenter.java
    package com.example.searchengines.client.presenter;
    
    public class HeaderPresenter implements Presenter {
    
        ...
        
        /**
         * Bind events and actions
         */
        public void bind() {
            
            ...
            
            display.getLogIn().addClickHandler(new ClickHandler() {
                public void onClick(ClickEvent event) {
                    History.newItem("login");
                    display.setSelected();
                }
            });
    

    For the last part of the log-in page we need to tell the AppController class to call the LoginPresenter when the Log-in header button is clicked and your site's url changes with the "login" token added to the history.

    AppController.java
    package com.example.searchengines.client;
    
    public class AppController implements Presenter, ValueChangeHandler<String> {
    
        ...
    
        public void onValueChange(ValueChangeEvent<String> event) {
            ...        
                
                // For every page you create set the presenter here
                if (token.getPage().equals("home"))    {
                    presenter = new HomePresenter(factory, new HomeView());
                }        
                ...
                }
                else if (token.getPage().equals("login")) {
                    presenter = new LoginPresenter(factory, new LoginView());
                }            
    

    If you debug the page, now you should be able to see the log-in page when you click the button on the header. Also notice the "login" token in your url. That is a result of adding the "History.newItem()" in the HeaderPresenter and the AppController class picking up that change in the history, branching on that history token value, and displaying the login page.

    Admin only pages

    Let's say we want to give users who have elevated privileges access to areas of your website where changes to the site's data can be made. Modify HeaderPresenter.java and add functionality for a new button for a future administrator page and to listen for log-in and log-out events. If you followed the previous tutorials (which you should have) then this will replace the TODO #4 for the log-out button and TODO #5 for listening for log-in events. We'll also add a check if the data factory's user object is not null.

    HeaderPresenter.java
    public class HeaderPresenter implements Presenter {
    
        public interface Display {
            ...
            HasClickHandlers getAdmin();
            ...
        }
    
        public void bind() {
            ...
            display.getAdmin().addClickHandler(new ClickHandler() {
                public void onClick(ClickEvent event) {
                    History.newItem("admin");
                    display.setSelected();
                }
            });
    
            display.getLogOut().addClickHandler(new ClickHandler() {
                public void onClick(ClickEvent event) { 
                    doLogout();
                }
            });
            
            factory.getEventBus().addHandler(LogInEvent.TYPE, new LogInEventHandler() {
                public void onLogIn(LogInEvent event) {
                    doLogIn(factory.getUser());
                } 
            });
        }
    
        public void go(final HasWidgets container) {
            bind();
            container.clear();
            container.add(display.asWidget());
            if (factory.getUser() != null) {
                this.doLogIn(factory.getUser());
            }
            display.setSelected();
        }
    
        private void doLogIn(User user) {
            display.setData(factory.getUser());
        }
    
        private void doLogout() {
            try {
                String sessionId = Cookies.getCookie("REMEMBER_ME");
                factory.getRpcService().logout(sessionId, new AsyncCallback() {
                    public void onFailure(Throwable caught) {
                }
    
                public void onSuccess(Void result) {
                    Cookies.removeCookie("REMEMBER_ME");
                    factory.setUser(null);
                    display.setData();
                    factory.getEventBus().fireEvent(new LogOutEvent());
                    History.fireCurrentHistoryState();  
                }});
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    

     Nearing the home stretch. Let's just modify the HeaderViewto show our new "Admin" button and to display it only if the user has the "allow_show_admin" permission. Oh wow see how that all comes together now? You can have 50 roles that have the "allow_show_admin" permission and instead of listing them all we just need to know if that user can get to the admin page and that is all. Sweet!

    HeaderView.java
    package com.example.searchengines.client.view;
    
    public class HeaderView extends View implements HeaderPresenter.Display {
                
        private final Button adminButton = new Button(constants.admin());
        
        public void setData(User user) {
            //Clear right panel
            rightPanel.clear();
            
            //Hello user
            Label userLabel = new Label(constants.welcome() + " " + user.getFirstName());
            userLabel.setStyleName("headerpaneltext");
            rightPanel.add(userLabel);
            
            Label seperatorLabel = new Label("|");
            seperatorLabel.setStyleName("headerpaneltext");
            rightPanel.add(seperatorLabel);
            
            if (user.getPermissions().contains(Permission.ALLOW_SHOW_ADMIN)) {
                adminButton.setStyleName("headerbutton");
                rightPanel.add(adminButton);
                
                Label seperatorLabelAdmin = new Label("|");
                seperatorLabelAdmin.setStyleName("headerpaneltext");
                rightPanel.add(seperatorLabelAdmin);
            }
                
            logoutButton.setStyleName("headerbutton");
            rightPanel.add(logoutButton);
        }
    
    
        public HasClickHandlers getAdmin() {
            return this.adminButton;
        }
    }
    

    Almost forgot. Add a new constant to SearchEnginesConstants.java for the display text of our administrator button.

    SearchEnginesConstants.java
    public interface SearchEnginesConstants extends Constants {
            
        ...
        @DefaultStringValue("Admin")
        String admin();
        ...
    

    Let's take a quick look what our website can do so far with authentication:
    • Display log-in page
    • Authenticate a user
    • Retrieve information about that user
    • Assign permissions to a user based on their role
    • Display a new admin button on the header when user has permission
    • When admin header button is clicked, the "admin" token is added to the history.
    Now let's take the last step and display the admin page, only if the user has permission to do so. In the AppControler, the class that controls what page to display in our webpage, we will add some logic to show an Administrator page, only if the user who has logged in, has the "ALLOW_SHOW_ADMIN" permission, otherwise send them to the home page.

    package com.example.searchengines.client;
    
    public class AppController implements Presenter, ValueChangeHandler<String> {
    
        ...
    
        public void onValueChange(ValueChangeEvent<String> event) {
            ...        
                
                // For every page you create set the presenter here
                if (token.getPage().equals("home"))    {
                    presenter = new HomePresenter(factory, new HomeView());
                }        
                ...
                }
                else if (token.getPage().equals("admin")) {
                    if (factory.getUser() != null && factory.getUser().getPermissions().contains(Permission.ALLOW_SHOW_ADMIN)) {
                        //TODO 9: Create administrator page
                    }
                    else {
                        presenter = new HomePresenter(factory, new HomeView());
                    }
                }               
    

    Now when you debug the page as a Web Application try authenticating with your test user and you should not see an "Admin" button on the header but have a "Log Out" button available. However if you authenticate with your test admin user then not only should you see the "Admin" button on the header but you can click that button and your site's url will change with an "admin" token now on the history like so:

    http://127.0.0.1:8888/SearchEngines.html?gwt.codesvr=127.0.0.1:9997#admin

    If you see this then that will indicate that your test admin user does indeed have permission to view the administrator page, we just haven't built it yet.  So in the next tutorial let's build the administrator page, a user registration page, add some more roles and permissions, and learn how to encrypt user passwords in the database.

    References


    http://csrc.nist.gov/rbac/rbac-std-ncits.pdf
    http://code.google.com/p/google-web-toolkit-incubator/wiki/LoginSecurityFAQ
    https://www.owasp.org/index.php/Authentication_Cheat_Sheet
    https://www.owasp.org/index.php/Session_Management_Cheat_Sheet
    https://code.google.com/p/yfs-health/source/browse/trunk/yfs/src/com/varun/yfs/server/login/



    Friday, October 10, 2014

    SAXON: Fatal Error! Cannot find a matching 1-argument function named {http://exslt.org/common}node-set(). There is no Saxon extension function with the local name node-set

    I was working on a DITA-OT plugin when I encountered this guy:

    Fatal Error! Cannot find a matching 1-argument function named {http://exslt.org/common}node-set(). There is no Saxon extension function with the local name node-set

    I am using the latest version of SAXON HE which apparently does not support EXSLT.
    http://www.saxonica.com/feature-matrix.html
    However you can pay $50 bucks or more for the PE and EE versions. Or you can save yourself some green and download SAXON 9.1.0.8 which was the last version to support EXSLT. You can download the JAR file here and put it on your classpath. Hint: The download link is next to the floppy drive icon.
    Download saxon-9.1.0.8.jar

    For a DITA project you'll also need the following jar:
    Download saxon-dom-9.1.0.8.jar

    And if you are using Eclipse you can set the classpath by doing the following:

    1. On the file menu click: Window > Preferences > Ant > Runtime > Classpath tab > Global entries
    2. Remove the existing versions of SAXON from the classpath
    3. Click the "Add External Jars" button
    4. Add "saxon-9.1.0.8.jar" and "saxon-dom-9.1.0.8.jar"