Under the hood: Accounting example

The login-api is now the first service in loklak that utilizes the accounting feature. It does that to protect user accounts against brute-force login attempts.

How does it work?

Quite simple. First, we define some permissions:

public JSONObject getDefaultPermissions(BaseUserRole baseUserRole) {
   JSONObject result = new JSONObject();
   result.put("maxInvalidLogins", 10);
   result.put("blockTimeSeconds", 120);
   result.put("periodSeconds", 60);
   result.put("blockedUntil", 0);
   return result;
}

Each user is only allowed to make 10 invalid login attempts over a period of 60 seconds and will otherwise get blocked for 120 seconds. Why do we save that in the permissions? Because we could change them on user basis. If one user get’s blocked for the 3rd time, we could set his blocked time up to 24h for example. That’s not implemented yet though.

Now, whenever we have a bad login attempt we save it in the accounting system:

authorization.getAccounting().addRequest(this.getClass().getCanonicalName(), "invalid login");

throw new APIException(422, "Invalid credentials");

Note that we have to specify some path or name in the accounting object. We use the full name of the login service, so other service will mess with that.

Now this is how we check:

private void checkInvalidLogins(Query post, Authorization authorization, JSONObjectWithDefault permissions) throws APIException {

   // is already blocked?
   long blockedUntil = permissions.getLong("blockedUntil");
   if(blockedUntil != 0) {
      if (blockedUntil > Instant.now().getEpochSecond()) {
         Log.getLog().info("Blocked ip " + post.getClientHost() + " because of too many invalid login attempts.");
         throw new APIException(403, "Too many invalid login attempts. Try again in "
               + (blockedUntil - Instant.now().getEpochSecond()) + " seconds");
      }
      else{
         authorization.setPermission(this, "blockedUntil", 0);
      }
   }

   // check if too many invalid login attempts were made already
   JSONObject invalidLogins = authorization.getAccounting().getRequests(this.getClass().getCanonicalName());
   long period = permissions.getLong("periodSeconds", 600) * 1000; // get time period in which wrong logins are counted (e.g. the last 10 minutes)
   int counter = 0;
   for(String key : invalidLogins.keySet()){
      if(Long.parseLong(key, 10) > System.currentTimeMillis() - period) counter++;
   }
   if(counter > permissions.getInt("maxInvalidLogins", 10)){
      authorization.setPermission(this, "blockedUntil", Instant.now().getEpochSecond() + permissions.getInt("blockTimeSeconds", 120));
      throw new APIException(403, "Too many invalid login attempts. Try again in "
            + permissions.getInt("blockTimeSeconds", 120) + " seconds");
   }
}

First we check if there’s an client specific override of our permissions: blockedUntil

Normally that is 0, but if it’s set to some second in the future, we respond with an error message, saying how long the client has to wait.

Otherwise, we check how many entries are in the accounting object for service. As the login service only saves bad requests, we only need to know the number and when they were made.

Each request in the accounting object is stored with the current timestamp as key. So we check which key were made in the last 60 seconds (as defined in the permissions). If it’s more than 10, set the permission blockedUntil to the current second + 120.

This is a short example of a mighty tool to archive user specific reactions in our services and could be adopted for many different scenarios. Feel free to try 🙂

Under the hood: Accounting example