Authentication and authorization in Play Framework

on waitingforcode.com

Authentication and authorization in Play Framework

You're doing Java/C#/JavaScript and doing it great? But you're tired because of always facing the same problems. I was like that 4 years ago. I changed then to the data engineering field and it solved my existential problems :) If you want to follow my path, I prepared a course that will help you with that! Join the class!
If you remember well our previous article, we introduced the concept of forms and validation. To illustrate it, we used the register form case. Now, when user can register, he also should be able to login.

This article will cover the authentication and authorization part in Play Framework. In the first part we'll present the authentication. After that, we'll introduce the authorization part. At the end, we'll show how to improve security aspects in Play.

Authentication in Play Framework

Before we start to talking about security aspects in Play Framework, we need to understand the difference between authentication and authorization. The authentication consists on checking if given user is known by the system. In the other hand, the authorization consists on verifying if user identified by system (= authenticated) has sufficient rights to access protected resource, as for example web page.

Play provides very simple authentication mechanism. We can even tell that authentication mechanism isn't provided with the framework and we need to implement it in our own. In the authentication mechanism of our store, we'll write a simple form page:

@(userForm: play.data.Form[models.forms.LoginForm], flash: play.mvc.Http.Flash)
@import helper._

@implicitField = @{FieldConstructor(helpers.form.inputFieldHelper.render)}

@main("Welcome to login page") { 
	@if(flash.get("logout") == "1"){
		

You're correctly logged out

} @helper.form(action = routes.UserController.loginSubmit()) { @helper.inputText(userForm("login"), '_label -> "Login", 'size -> 10) @helper.inputPassword(userForm("password"), '_label -> "Password") <input type="submit" name="register" value="login" /> } }

As you can see, there are a new model, placed in models.forms package, LoginForm. It looks like User register form, but only login and password fields are needed. The reason why we don't use User object is that it already contains a global validate() method. To keep this method as simple as possible, it's better to create new simple POJO object with only login and password fields. Take a look on it:

public class LoginForm {

	private String login;
	private String password;
	
	public List validate() {
		UserService userService = (UserService) ServicesInstances.USER_SERVICE.getService();
		User user = userService.getByLoginAndPassword(getLogin(), getPassword());
		if (user == null) {
			List errors = new ArrayList();
			errors.add(new ValidationError("login", "Login or password is incorrect"));
			return errors;
		}
		return null;
	}
	
	public String getLogin() {
		return this.login;
	}
	public String getPassword() {
		return this.password;
	}
	
	public void setLogin(String login) {
		this.login = login;
	}
	public void setPassword(String password) {
		this.password = password;
	}
	
	@Override
	public String toString() {
		return "LoginForm {login: "+this.login+"}";
	}
	
}

If you examine the validate() method, you'll see that it contains an call to getByLoginAndPassword method of UserService. This method makes some basic checks to be sure that the password set in the login form corresponds to the password from the database:

public User getByLoginAndPassword(String login, String password) {
	try {
		User user = getByLogin(login);
		if (user == null) throw new UserNotFoundException("User with login '"+login+"' was not found");
		password = PasswordCreator.sha1Password(password, user.getSalt());
		if (!password.equals(user.getPassword())) throw new UserAuthenticationException("Password for user '"+login+"' doesn't match");
		return user;
	} catch (Exception e) {
		Logger.error("An error occurred on getting user by login and password. Login used: "+login, e);
	}
	return null;
}

Now, all this authentication job is handled in two methods from UserController:

private static Form loginForm = Form.form(LoginForm.class);

@Transactional
public static Result login() {
	return ok(login.render(loginForm, flash()));
}

@Transactional
public static Result loginSubmit() {
	Form submittedForm = loginForm.bindFromRequest("login", "password");
	if (!submittedForm.hasErrors()) {
		LoginForm formObj = submittedForm.get();
		Logger.debug("User exists, no errors. Found user is: "+formObj);
		session().put("u", formObj.getLogin());
		return redirect(routes.UserController.dashboard());
	}
	return badRequest(login.render(submittedForm, flash()));
}

These two methods don't do nothing complicated. The first one only displays the login form while the loginSubmit() handles the data sent by the first method. If LoginForm's validate() method returns null, so there're no validation errors, we consider that user introduced correct connection information. In this case, we put new session data called "u" which contains the login of connected user. Does it secured ? As you could see in the article about sessions in Play Framework, it's very hard to compromise data stored in Play's session cookie. But the cookie can be stolen and used in other computer. This danger is also associated with another session mechanisms in web world (PHP, Java Servlet API) and is called session hijacking. To improve security level of our application, we could for example add an unique user proof and check it every time when authorization request is invoked. We'll cover this more in the third part of the article.

Authorization in Play Framework

The authorization level is partially provided with Play Framework. All necessary methods are contained by play.mvc.Security class. The first one is @Authorized annotation which handles only one attribute - a class which must be a child of Authenticator. Authenticator is inner Security class that defines two methods:
- getUsername(Context ctx): returns the name of connected user as String, or null if user isn't connected.
- onUnauthorized(Context ctx): returns a Result instance which represents the page shown to non connected user.

@Authenticated is evaluated by Security.AuthenticatedAction class which call() method determines if given user can access to protected resource:

public F.Promise call(Context ctx) {
    try {
        Authenticator authenticator = configuration.value().newInstance();
        String username = authenticator.getUsername(ctx);
        if(username == null) {
            Result unauthorized = authenticator.onUnauthorized(ctx);
            return F.Promise.pure(unauthorized);
        } else {
            try {
                ctx.request().setUsername(username);
                return delegate.call(ctx);
            } finally {
                ctx.request().setUsername(null);
            }
        }
    } catch(RuntimeException e) {
        throw e;
    } catch(Throwable t) {
        throw new RuntimeException(t);
    }
}

As you can imagine, we should decorate all protected methods with @Authorized annotation. This annotation should be accompanied with our implementation of Authenticator. We need to override the default one because the informatios about user are stored in session entry called "u" and not in default "username" attribute. The version of overriden Authenticator is:

public class StoreSecured extends Security.Authenticator {

    @Override
    public String getUsername(Context ctx) {
        Logger.debug("Getting user from StoreSecured getUsername() method");
        return ctx.session().get("u");
    }

    @Override
    public Result onUnauthorized(Context ctx) {
    	Logger.debug("User is unathorized to access to the protected ressource. We redirect him to login page");
        return redirect(routes.UserController.login());
    }
}

Now, we can simply decorate methods with @Authorized annotation, as below:

@Transactional
@Security.Authenticated(StoreSecured.class)
public static Result logout() {
	session().clear();
	flash("logout", "1");
	return redirect(routes.UserController.login());
}

@Transactional
@Security.Authenticated(StoreSecured.class)
public static Result dashboard() {
	return ok(dashboard.render());
}

Two URLs by now, /user/dashboard and /user/logout, are protected by the authorization mechanism. Let's test it by observing log entries, beginning with login page and ending with logout page:

[debug] application - User exists, no errors. Found user is: LoginForm {login: bartosz2}
[debug] application - Getting user from StoreSecured getUsername() method
[debug] application - User exists, no errors. Found user is: LoginForm {login: bartosz2}
[debug] application - Getting user from StoreSecured getUsername() method
[debug] application - Getting user from StoreSecured getUsername() method

If we logout and try to access /user/dashboard page again, we'll find this entry in the logs and we'll redirected to login page:

[debug] application - User is unathorized to access to the protected ressource. We redirect him to login page

Improve authorization mechanism in Play Framework

We mentioned in the first part that cookie with connected user information can be stolen. To prove that, we'll try to do it by opening two browsers. Now, we login in the first one and copy the content of PLAY_SESSION cookie ("f3a448cfb143765838998b8dbcc3c147900d6073-u=bartosz2") to the other browser. To do it, you can open Firefox, login there and copy generated PLAY_SESSION cookie content. After you can open Chromium with EditThisCookie extension, create new PLAY_SESSION cookie with Firefox content. Now, you can freely access to /user/dashboard page.

To protect our system against this kind of attacks, we'll opt for a technique called fingerprinting. This technique is based on physical proof of user identity. This proof is usually composed by data unique to given user session, as browser, IP address and another personal information got from the request. We'll implement the fingerprinting with IP address and browser:

public class FingerprintMaker {

	public static String makeFromRequest(Request request, String secret) throws Exception {
		String token = request.remoteAddress()+secret+request.getHeader("User-Agent")+secret;
		return PasswordCreator.sha1Password(token, "");
	}
	
}

As you can see, the class isn't difficult to understand. It concatenates secret key (added in conf/application.conf under fingerpring.secret key) with user's personal informations. After that it returns a SHA1 with them. This value needs to be stored in the session at the moment of login:

@Transactional
public static Result loginSubmit() {
	Form submittedForm = loginForm.bindFromRequest("login", "password");
	if (!submittedForm.hasErrors()) {
		String fingerprint = null;
		try {
			fingerprint = FingerprintMaker.makeFromRequest(request(), Play.application().configuration().getString("fingerprint.secret"));
		} catch (Exception e) {
			// TODO : redirect to 500 error page
		}
		LoginForm formObj = submittedForm.get();
		Logger.debug("User exists, no errors. Found user is: "+formObj);
		session().put(StoreSecured.COOKIE_KEY_USER, formObj.getLogin());
		session().put(StoreSecured.COOKIE_KEY_FINGERPRINT, fingerprint);
		return redirect(routes.UserController.dashboard());
	}
	return badRequest(login.render(submittedForm, flash()));
}

StoreSecured also changed. Its new implementation of getUsername method looks like:

   public static final String COOKIE_KEY_USER = "u";
   public static final String COOKIE_KEY_FINGERPRINT = "fg";
	
    @Override
    public String getUsername(Context ctx) {
		Logger.debug("Getting user from StoreSecured getUsername() method");
		String login = ctx.session().get(COOKIE_KEY_USER);
		String sessionFg = ctx.session().get(COOKIE_KEY_FINGERPRINT);
		try {
			String fingerprint = FingerprintMaker.makeFromRequest(ctx.request(), Play.application().configuration().getString("fingerprint.secret"));
			if (fingerprint != null && fingerprint.equals(sessionFg)) {
				return login;
			}
			Logger.debug("Unknown user for the login '"+login+"'. It mays be session hijacking try because fingerprings aren't the same "+
					" (in the session: "+sessionFg+", generated: "+fingerprint+")");
		} catch (Exception e) {
			Logger.error("An error occurred on getting username from session. Concerned user login is '"+login+"'", e);
		}
		return null;
    }

The supplementary protection is in place. Normally, if we try to reproduce hack from the begin of this part, we won't be able to login with the same cookie on two different computers. To understand why, we can simply compare two cookies generated by the browser when user logs manually and consult log message when user tried to access protected page with copied PLAY_SESSION cookie :
- first browser: 67eb506266bbf595c81da737006e8088faa80b03-u=bartosz2&fg=db9e3fc52a7001b98a1a42ff2c336c0bbe396147
- second browser: be5c293eee2f541b3c10c623a67f356719a9d0a8-u=bartosz2&fg=66f1da55bb7191cabacc4ee06abb0ccbf46bd81f

And log:

[debug] application - Unknown user for the login 'bartosz2'. It mays be session hijacking try because fingerprings aren't the same  (in the session: db9e3fc52a7001b98a1a42ff2c336c0bbe396147, generated: 66f1da55bb7191cabacc4ee06abb0ccbf46bd81f)

At the begin, this article introduced us to authentication methods in Play Framework. After that we discovered the authorization with @Authorized annotation and Security.Authenticator classes responsible to decide if given user is authorized to access protected resource. The last part of the article presented a supplementary protection against session hijacking risk by introducing physical fingerprint associated with the user who made successful connection attempt.

Share on: