Server-side validation of SUSI.AI is required for increasing the security of user against malicious users who can bypass client-side validations easily and submit dangerous input to SUSI.AI server. SUSI.AI uses both client-side and server-side validation for various authentication purposes.
Client-side validates user input on the browser and provides a better user interaction. If the form is disabled on incorrect fields, it would save a round trip to the server, so the network traffic which will help your server perform better. Using client-side validation, an error message can be immediately shown before the user moves to the next field.
Adding Recaptcha while on new registration, change password, and more than one login attempts help to reduce abusive traffic, spam, brute force attack. For making user accounts safe from getting hacked on SUSI.AI, a strong password is supposed to be at least 8 characters, at least one special character, one number and text characters are must. The username must have 5-51 characters. So as to prevent SUSI.AI admin panel from crashing.
Code Integration
Client side reCaptcha validation:
onCaptchaSuccess = captchaResponse => { if (captchaResponse) { this.setState({ showCaptchaErrorMessage: false, captchaResponse, }); } };
Once the user verifies captcha on client-side, a callback onCaptchaSuccess fires which receives a parameter captchaResponse. This captchaResponse is sent over to server-side with key g-recaptcha-response
Validation for User Login, ReCaptcha needs to be shown if the user has tried to log in for more than 1 time. For this, sessionStorage needs to be maintained. sessionStorage has an expiration time, which expires once the user closes the tab.
In the constructor of Login, the attempt is initialized with sessionStorage using key loginAttempts. Now, we have the correct value of loginAttempts and can update it further if the user tries to make more attempt.
constructor(props) { super(props); this.state = { ... showCaptchaErrorMessage: false, attempts: sessionStorage.getItem('loginAttempts') || 0, captchaResponse: '', }; }
If the user closes the login dialog, we need to store the current attempts state on componentWillUnmount
componentWillUnmount() { sessionStorage.setItem('loginAttempts', this.state.attempts); }
If attempts are less than 1, render the reCaptcha component and send g-recaptcha-response parameter to api.susi.ai/aaa/login.json API endpoint.
Server-side reCaptcha Validation for user login
The checkOneOrMoreInvalidLogins function in LoginService checks if the user has attempted to login in more than once. Access the accounting object and get identity and store it in accounting.
private boolean checkOneOrMoreInvalidLogins(Query post, Authorization authorization, JsonObjectWithDefault permissions) throws APIException { Accounting accounting = DAO.getAccounting(authorization.getIdentity()); JSONObject invalidLogins = accouting.getRequests().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 > 0){ return true; } return false; }
If user has attempted to login in more than once, get from g-recaptcha-response API parameter. Check if reCaptcha is verified using VerifyRecaptcha utility class.
if(checkOneOrMoreInvalidLogins(post, authorization, permissions) ){ String gRecaptchaResponse = post.get("g-recaptcha-response", null); boolean isRecaptchaVerified = VerifyRecaptcha.verify(gRecaptchaResponse); if(!isRecaptchaVerified){ result.put("message", "Please verify recaptcha"); result.put("accepted", false); return new ServiceResponse(result); } }
The VerifyRecaptcha class provides with a verify method, which can be called to check if user response if correct or not. The secretKey is key given for communication between server-side and google.
URL is created and using InputStream, content(res) is directly read from a URL using openStream() function provided. The bufferedReader reads the contents line by line(using cp), and rd.read() returns a string. The result is appended to stringBuilder sb until -1 is read.
res.close() closes the stream and releases the resources for URL reading. JSONObject is created using the string created from URL, and value mapped to success is used to check if ReCaptcha is verified. If the response is correct, it returns true.
public class VerifyRecaptcha { public static boolean verify(String response) { try { String url = "https://www.google.com/recaptcha/api/siteverify?" + "secret=6LfPZGAUAAAAAAULjaq7Rt9-7IGJoLYoz2Di6yVV&response=" + response; InputStream res = new URL(url).openStream(); BufferedReader rd = new BufferedReader(new InputStreamReader(res, Charset.forName("UTF-8"))); StringBuilder sb = new StringBuilder(); int cp; while ((cp = rd.read()) != -1) { sb.append((char) cp); } String jsonText = sb.toString(); res.close(); JSONObject json = new JSONObject(jsonText); return json.getBoolean("success"); } catch (Exception e) { e.printStackTrace(); } return false; } }
User name validation on server side
Changing regex in config: Regular express for username to have 5-51 characters. Setting regex and tooltip in configs:
users.username.regex=^(.{5,51})$
users.username.regex.tooltip=Enter atleast 5 character, upto 51 character
users.username.regex=^(.{5,51})$ users.username.regex.tooltip=Enter atleast 5 character, upto 51 character
If a user tries to add userName more than 51 characters or less than 5 characters,
The regex is compiled using Pattern.compile and then matched against the value provided by the user in API parameter. If the pattern matches, it means the user provided the correct userName.
if(possibleKeys[i].equals("userName") && value != null){ String usernamePattern = DAO.getConfig("users.username.regex", "^(.{5,51})"); String usernamePatternTooltip = DAO.getConfig("users.username.regex.tooltip", "Enter atleast 5 character, upto 51 character"); Pattern pattern = Pattern.compile(usernamePattern); if(!pattern.matcher(value).matches()) { throw new APIException(400, usernamePatternTooltip); } }
With server-side validation incorporated, the SUSI.AI users accounts are much safer than only with client-side validation. Validation over both client-side and server-side complement each, server-side validation being much more robust.
Resources
- Verifying the user’s response: https://developers.google.com/recaptcha/docs/verify
- Introduction to google recaptcha: https://developers.google.com/recaptcha/intro
- React-recaptcha documentation: https://github.com/appleboy/react-recaptcha
- Reading Directly from a URL: https://docs.oracle.com/javase/tutorial/networking/urls/readingURL.html
- BufferedReader in java: https://www.guru99.com/buffered-reader-in-java.html
- Link to PR on server-side: https://github.com/fossasia/susi_server/pull/1312
- Link to PR on client-side: https://github.com/fossasia/susi.ai/pull/2661, https://github.com/fossasia/susi.ai/pull/2664
Tags
SUSI.AI, FOSSASIA, GSoC19, SUSI.AI Server