loklak_depot – The Beginning: Accounts (Part 2)

So here I’m back with another blog post on loklak_depot, this time on the Login page. If you have not read my previous blog post, I suggest you do so here.

For the login page, the idea is simple: there’s a field for Email ID and Password, and a checkbox to “Remember me” (in the case when users don’t want to be logged out). The Email and Password are then verified from the database via a servlet, and the user logs in. We are implementing login by two ways: by POSTing directly to servlet (i.e on clicking on the Login button), and via parameters (using things like curl). On the face of it, this is the functionality. Behind the scenes, it’s a bit more complicated.

For the posting (JavaScript) of the Login and Password to servlet part, that is trivial. It goes something like this:


$('#login').click(function(){
        checkEmpty();
        var total = passerr || emailerr;
        if(total){
            alert("Please fill empty fields");
        } else{
            var mail = encodeURIComponent($('#email').val());
            var pwd = encodeURIComponent($('#pass').val());
            var posting = $.post( "/api/login.json", { login: mail, password: pwd, request_cookie: checked, request_session: session }, function(data) {
                console.log(data.status);
                console.log(data.reason);
                alert(data.status + ", " + data.reason);
                if(data.status=="ok" && data.reason=="ok"){
                    window.location = '/apps/applist/index.html';
                }
            }, "json" );
        }
    });

Here, the $('#login') is the login button. The email and password is POSTed as a URI to prevent any errors on non-UTF8 characters. In addition, there are two more fields: request_session and request_cookie. The former by default is true while the latter is true or false depending upon whether the “Remember Me” checkbox is checked. If the servlet returns an ok status and reason, then you are redirected to the applist page (for now, although we will get onto implementation of the dashboard soon).

Now for the backend code. It has two parts: a servlet and an added logic in AbstractAPIHandler (again, if you don’t know what the latter is, go read my previous blogpost). Here it is:

Servlet: (LoginServlet.java)


public class LoginServlet extends AbstractAPIHandler implements APIHandler {
   
    private static final long serialVersionUID = 8578478303032749879L;

    @Override
    public APIServiceLevel getDefaultServiceLevel() {
        return APIServiceLevel.PUBLIC;
    }

    @Override
    public APIServiceLevel getCustomServiceLevel(Authorization rights) {
        return APIServiceLevel.ADMIN;
    }

    public String getAPIPath() {
        return "/api/login.json";
    }
    
    @Override
    public JSONObject serviceImpl(Query post, Authorization rights) throws APIException {

    	JSONObject result = new JSONObject();
    	
    	if(rights.getIdentity().getType() == ClientIdentity.Type.email){
    		result.put("status", "ok");
    		result.put("reason", "ok");
    	}
    	else{
    		result.put("status", "error");
    		result.put("reason", "Wrong login credentials");
    	}
    	
		return result;
    }
    
}

The servlet extends AbstractAPIHandler. Type is an enum which contains a variable email.This code calls the getIdentity() method from AbstractAPIHandler, which contains the main backend code as follows:



/**
     * Checks a request for valid login data, send via cookie or parameters
     * @return user identity if some login is active, anonymous identity otherwise
     */
    private ClientIdentity getIdentity(HttpServletRequest request, HttpServletResponse response){
    	
    	// check for login information
		if("true".equals(request.getParameter("logout"))){	// logout if requested
			
			// invalidate session
			request.getSession().invalidate();
			
			// delete cookie if set
			deleteLoginCookie(response);
		}
		else if(getLoginCookie(request) != null){
			
			Cookie loginCookie = getLoginCookie(request);
			
			ClientCredential credential = new ClientCredential(ClientCredential.Type.cookie, loginCookie.getValue());
			Authentication authentication = new Authentication(credential, DAO.authentication);
			
			if(authentication.getIdentity() != null){
				
				if(authentication.checkExpireTime()){
					
					//reset cookie validity time
					authentication.setExpireTime(defaultCookieTime);
					loginCookie.setMaxAge(defaultCookieTime.intValue());
					loginCookie.setPath("/"); // bug. The path gets reset
					response.addCookie(loginCookie);
						
					return authentication.getIdentity();
				}
				else{
					authentication.delete();
					
					// delete cookie if set
					deleteLoginCookie(response);
					
					Log.getLog().info("Invalid login try via cookie from host: " + request.getRemoteHost());
				}
			}
			else{
				authentication.delete();
				
				// delete cookie if set
				deleteLoginCookie(response);
				
				Log.getLog().info("Invalid login try via cookie from host: " + request.getRemoteHost());
			}
		}
		else if(request.getSession().getAttribute("identity") != null){ // if identity is registered for session			
			return (ClientIdentity) request.getSession().getAttribute("identity");
		}
		else if (request.getParameter("login") != null && request.getParameter("password") != null ){ // check if login parameters are set
    		
    		
    		String login = null;
    		String password = null;
			try {
				login = URLDecoder.decode(request.getParameter("login"),"UTF-8");
				password = URLDecoder.decode(request.getParameter("password"),"UTF-8");
			} catch (UnsupportedEncodingException e) {}

    		ClientCredential credential = new ClientCredential(ClientCredential.Type.passwd_login, login);
    		Authentication authentication = new Authentication(credential, DAO.authentication);
    		
    		// check if password is valid
    		if(authentication.getIdentity() != null){
    			
    			if(authentication.has("activated") && authentication.getBoolean("activated")){
    			
	    			if(authentication.has("passwordHash") && authentication.has("salt")){
	    				
						String passwordHash = authentication.getString("passwordHash");
						String salt = authentication.getString("salt");
						
	    				ClientIdentity identity = authentication.getIdentity();
						
		    			if(getHash(password, salt).equals(passwordHash)){
		    				
		    				// only create a cookie or session if requested (by login page)
		    				if("true".equals(request.getParameter("request_cookie"))){
	            				
	            				// create random string as token
	            				String loginToken = createRandomString(30);
	            				
	            				// create cookie
	            				Cookie loginCookie = new Cookie("login", loginToken);
	            				loginCookie.setPath("/");
	            				loginCookie.setMaxAge(defaultCookieTime.intValue());
	            				
	            				// write cookie to database
	            				ClientCredential cookieCredential = new ClientCredential(ClientCredential.Type.cookie, loginToken);
	            				JSONObject user_obj = new JSONObject();
	            				user_obj.put("id",identity.toString());
	            				user_obj.put("expires_on", Instant.now().getEpochSecond() + defaultCookieTime);
	            				DAO.authentication.put(cookieCredential.toString(), user_obj, cookieCredential.isPersistent());
	        	    			
	            				response.addCookie(loginCookie);
	        	    		}
		    				else if("true".equals(request.getParameter("request_session"))){
		            			request.getSession().setAttribute("identity",identity);
		            		}
		    				
		    				Log.getLog().info("login for user: " + identity.getName() + " via passwd from host: " + request.getRemoteHost());
		            		
		            		return identity;
		    			}
		    			Log.getLog().info("Invalid login try for user: " + identity.getName() + " via passwd from host: " + request.getRemoteHost());
	    			}
	    			Log.getLog().info("Invalid login try for user: " + credential.getName() + " from host: " + request.getRemoteHost() + " : password or salt missing in database");
    			}
    			Log.getLog().info("Invalid login try for user: " + credential.getName() + " from host: " + request.getRemoteHost() + " : user not activated yet");
    		}
    		else{
    			authentication.delete();
    			Log.getLog().info("Invalid login try for unknown user: " + credential.getName() + " via passwd from host: " + request.getRemoteHost());
    		}
    	}
    	else if (request.getParameter("login_token") != null){
    		ClientCredential credential = new ClientCredential(ClientCredential.Type.login_token, request.getParameter("login_token"));
    		Authentication authentication = new Authentication(credential, DAO.authentication);
			
    		
    		// check if login_token is valid
    		if(authentication.getIdentity() != null){
    			ClientIdentity identity = authentication.getIdentity();
    			
    			if(authentication.checkExpireTime()){
    				Log.getLog().info("login for user: " + identity.getName() + " via token from host: " + request.getRemoteHost());
    				
    				if("true".equals(request.getParameter("request_session"))){
            			request.getSession().setAttribute("identity",identity);
            		}
    				if(authentication.has("one_time") && authentication.getBoolean("one_time")){
    					authentication.delete();
    				}
    				return identity;
    			}
    			Log.getLog().info("Invalid login try for user: " + identity.getName() + " via token from host: " + request.getRemoteHost());
    		}
    		Log.getLog().info("Invalid login token from host: " + request.getRemoteHost());
    		authentication.delete();
    	}
    	
        return getAnonymousIdentity(request);
    }

Long, right? The logic behind it though is pretty simple. As shown in this code, if request_session is true, it verifies the email and password from authentication.json. If request_cookie is set to true, the email ID and cookie is written to ClientCredential along with an expiry time, and once that times goes up, the user is logged out. And as in the signup, the passwords all have a hash and a salt. If the password encoded in the salt equals the hash, the login is verified. And the login system thus works fine.

The AbstractAPIHandler is a very useful class for most loklak operations, because it encapsulates most of the important backend operations under one class, and here it has come in handy again.

My next post will be on the Password Recovery system for the accounts, and the LoklakEmailHandler class. Feedback and suggestions for improvement are welcome. 🙂

loklak_depot – The Beginning: Accounts (Part 2)