Play! Framework Security Example
Today, security has become a priority in most web applications. Even giant enterprises have security issues that are found from time to time. The first barrier to avoid this issues is a login page to avoid people from seeing information they are not meant to see. Also, there are headers that mitigate security issues and provide an extra level of defence for new applications.
In this example, we’ll write a login page, and forbid access to secured pages to users which are not logged in. Then we’ll configure security headers, such as: X-Frame-Options, X-XSS-Protection, X-Content-Type-Options, X-Permitted-Cross-Domain-Policies and Content-Security-Policy. And finally, we’ll configure CORS. I assume the reader has knowledge about hashing, CORS and security headers (or at least the capabilities to research), I will give a shallow explain to these concepts, but further research will be needed if it’s not clear after that.
All this will be done using typesafe activator 1.2.12, sbt 0.13.5, java 1.8.0_66, scala 2.11.6 and Play 2.4.3. Have a look at this example of how to write a Play application, and come back to make it secure!
1. Authentication
Writing authentication for a Play! application is not that hard. The process is pretty simple, the first thing we need is to write a page to create a user, and store the password hashed.
For the hashing process we’ll use BCrypt, so add this line "org.mindrot" % "jbcrypt" % "0.3m"
to your library dependencies in your build.sbt
.
scala’s build.sbt
name := """play-security-scala""" version := "1.0-SNAPSHOT" lazy val root = (project in file(".")).enablePlugins(PlayScala) scalaVersion := "2.11.6" libraryDependencies ++= Seq( jdbc, cache, ws, "org.mindrot" % "jbcrypt" % "0.3m", specs2 % Test ) resolvers += "scalaz-bintray" at "http://dl.bintray.com/scalaz/releases" // Play provides two styles of routers, one expects its actions to be injected, the // other, legacy style, accesses its actions statically. routesGenerator := InjectedRoutesGenerator
java’s build.sbt
name := """play-security-java""" version := "1.0-SNAPSHOT" lazy val root = (project in file(".")).enablePlugins(PlayJava) scalaVersion := "2.11.6" libraryDependencies ++= Seq( javaJdbc, cache, "org.mindrot" % "jbcrypt" % "0.3m", javaWs ) // Play provides two styles of routers, one expects its actions to be injected, the // other, legacy style, accesses its actions statically. routesGenerator := InjectedRoutesGenerator
Then we make our model, which will be a User
and a Session
, in a package named model
.
User.scala
package model case class User(name: String, password: String)
User.java
package model; public class User { private final String name; private final String password; public User(String name, String password) { this.name = name; this.password = password; } public String getName() { return name; } public String getPassword() { return password; } }
As you see, user’s model is pretty simple. It has a name and a password.
Session.scala
package model import java.util.Date case class Session(user: User, loggedIn: Date, lastSeen: Date)
Session.java
package model; import java.util.Date; public class Session { private final User user; private final Date loggedIn; private final Date lastSeen; public Session(User user, Date loggedIn, Date lastSeen) { this.user = user; this.loggedIn = loggedIn; this.lastSeen = lastSeen; } public User getUser() { return user; } public Date getLoggedIn() { return loggedIn; } public Date getLastSeen() { return lastSeen; } }
Sessions are composed by a user, a logged in date and a last seen date.
We’ll use the lastSeen
date, to make max inactivity time restriction, so now we configure it in application.conf
as user.inactivity.max=7200000 #Two Hours
.
Now we need a storage for this model. For this example, we’ll just store them on a map on memory.
SessionRepository.scala
package repository import java.util.Date import model.{User, Session} import play.api.Play import play.api.Play.current import scala.collection.concurrent.TrieMap /** * Created by svinci on 06/12/15. */ trait SessionRepository { def isLoggedIn(name: String): Boolean def login(user: User): Unit def logout(name: String): Unit } object SessionRepository extends SessionRepository{ private val repo: TrieMap[String, Session] = TrieMap() private val ttl: Long = Play.application.configuration.getLong("user.inactivity.max").get def isLoggedIn(name: String): Boolean = { val maybeSession: Option[Session] = repo.get(name) val result = maybeSession.exists(s => { val now: Long = new Date().getTime val lastSeen: Long = s.lastSeen.getTime now - lastSeen < ttl }) if (result) { repo.put(name, maybeSession.get.copy(lastSeen = new Date())) } result } def login(user: User): Unit = { isLoggedIn(user.name) match { case true => throw new IllegalArgumentException("user is already logged in") case false => val now: Date = new Date() repo.put(user.name, Session(user, now, now)) } } def logout(name: String): Unit = repo.remove(name) }
SessionRepository.java
package repository; import com.google.common.base.Preconditions; import model.Session; import model.User; import org.apache.commons.lang3.StringUtils; import play.Play; import java.util.Date; import java.util.HashMap; import java.util.Map; import java.util.Optional; /** * Created by svinci on 06/12/15. */ class SessionRepository { private final Map<String, Session> repo; private final long ttl; public static final SessionRepository instance = new SessionRepository(); private SessionRepository() { this.repo = new HashMap<>(); this.ttl = Play.application().configuration().getLong("user.inactivity.max"); } public boolean isLoggedIn(String name) { final Session session = repo.get(name); final boolean loggedIn = Optional.ofNullable(session).map(s -> { Date now = new Date(); final long inactivityPeriod = now.getTime() - s.getLastSeen().getTime(); return inactivityPeriod < ttl; }).orElse(false); if (!loggedIn) repo.remove(name); else repo.put(name, new Session(session.getUser(), session.getLoggedIn(), new Date())); return loggedIn; } public void login(User user) { Preconditions.checkArgument(!isLoggedIn(user.getName()), "user is already logged in"); final Date now = new Date(); repo.put(user.getName(), new Session(user, now, now)); } public void logout(String name) { repo.remove(name); } }
Session reporitory defines three methods: isLoggedIn
, login
and logout
.
The method isLoggedIn
receives a user name, and retrieves the session assigned to it in our map. If there is any, it checks it’s not expired. If everything is correct it returns true, else false.
The method login
makes a simple check to make sure the user is not already logged in, and, if it passes, it just creates a new session with the current date as logged in and last seen and inserts it in our map.
The method logout
just removes the session assigned to that user (if any).
UserRepository.scala
package repository import model.User import org.mindrot.jbcrypt.BCrypt import scala.collection.concurrent.TrieMap trait UserRepository { def create(name: String, password: String): User def login(name: String, password: String): Boolean def logout(name: String): Unit def isLoggedIn(name: String): Boolean } object UserRepository extends UserRepository { private val sessionRepository: SessionRepository = SessionRepository private val repo: TrieMap[String, User] = TrieMap() private val salt: String = BCrypt.gensalt() private def hash(v: String): String = BCrypt.hashpw(v, salt) private def compare(v: String, hashed: String): Boolean = hash(v) == hashed override def create(name: String, password: String): User = { repo.get(name) match { case Some(_) => throw new IllegalArgumentException("duplicated user name") case None => val user = User(name, hash(password)) repo.put(name, user) user } } override def isLoggedIn(name: String): Boolean = this.sessionRepository.isLoggedIn(name) override def logout(name: String): Unit = this.sessionRepository.logout(name) override def login(name: String, password: String): Boolean = repo.get(name) match { case None => false case Some(u) => val pwdMatch = compare(password, u.password) if (pwdMatch) sessionRepository.login(u) pwdMatch } }
UserRepository.java
package repository; import com.google.common.base.Preconditions; import model.User; import org.mindrot.jbcrypt.BCrypt; import java.util.HashMap; import java.util.Map; import java.util.Optional; public class UserRepository { private final Map<String, User> repo; private final SessionRepository sessionRepository; private final String salt; public static final UserRepository instance = new UserRepository(); private UserRepository() { this.repo = new HashMap<>(); this.salt = BCrypt.gensalt(); this.sessionRepository = SessionRepository.instance; } public User create(String name, String password) { final Optional maybeUser = Optional.ofNullable(repo.get(name)); Preconditions.checkArgument(!maybeUser.isPresent(), "duplicated username"); final User newUser = new User(name, hash(password)); repo.put(name, newUser); return newUser; } public boolean login(String name, String password) { return Optional.ofNullable(repo.get(name)) .map(u -> { boolean validPassword = compare(password, u.getPassword()); if (validPassword) sessionRepository.login(u); return validPassword; }).orElse(false); } public void logout(String name) { this.sessionRepository.logout(name); } public boolean isLoggedIn(String name) { return this.sessionRepository.isLoggedIn(name); } private String hash(String value) { return BCrypt.hashpw(value, salt); } private boolean compare(String password, String hashed) { return hash(password).equals(hashed); } }
User repository behaves as a repository of users and as a wrapper of SessionRepository
.
It defines four methods: create
,login
, logout
and isLoggedIn
The method create
receives a name and a password, and creates a user out of it and puts it in our map, but not without checking if the name is duplicated and hashing the password first.
The method login
receives a name and a password, checks if a user with such name actually exists and, if it does, it compares the passwords. If they match, a session is created in SessionRepository
.
The methods logout
and isLoggedIn
are just wrappers of the ones in SessionRepository
.
Now let’s write our authentication controller.
AuthController.scala
package controllers import play.api.data._ import play.api.data.Forms._ import play.api.mvc.{Action, Controller} import repository.UserRepository case class LoginForm(name: String, password: String) case class SignUpForm(name: String, password: String, passwordConfirmation: String) /** * Created by svinci on 06/12/15. */ class AuthController extends Controller { private val loginForm: Form[LoginForm] = Form( mapping("name" -> text, "password" -> text)(LoginForm.apply)(LoginForm.unapply) ) private val signUpForm: Form[SignUpForm] = Form( mapping("name" -> text, "password" -> text, "passwordConfirmation" -> text)(SignUpForm.apply)(SignUpForm.unapply) ) private val repository: UserRepository = UserRepository def login = Action { Ok(views.html.login(form = loginForm)) } def logout = Action { implicit request => repository.logout(request.session.get("user").get) Redirect("/login") withNewSession } def authenticate = Action { implicit request => Option(loginForm.bindFromRequest().get).map(f => { repository.login(f.name, f.password) match { case true => Redirect(routes.Application.index()) withSession("user" -> f.name) case false => Redirect("/login") } }).getOrElse(Redirect("/login")) } def signup = Action { Ok(views.html.signup(signUpForm)) } def newUser = Action { implicit request => Option(signUpForm.bindFromRequest().get).map(f => { f.password == f.passwordConfirmation match { case true => repository.create(f.name, f.password) Redirect("/login") case false => Redirect("/signup") } }).getOrElse(Redirect("/signup")) } }
AuthController.java
package controllers; import play.data.Form; import play.mvc.Controller; import play.mvc.Result; import repository.UserRepository; import views.html.login; import views.html.signup; import java.util.Objects; import java.util.Optional; public class AuthController extends Controller { private final UserRepository repository; public AuthController() { repository = UserRepository.instance; } public Result login() { return ok(login.render(Form.form(LoginForm.class))); } public Result logout() { repository.logout(session("user")); session().clear(); return redirect(routes.AuthController.login()); } public Result authenticate() { final Form loginForm = Form.form(LoginForm.class).bindFromRequest(); return Optional.ofNullable(loginForm.get()) .map(f -> { if (repository.login(f.name, f.password)) { session().put("user", f.name); return redirect(routes.Application.index()); } else return redirect(routes.AuthController.login()); }).orElse(redirect(routes.AuthController.login())); } public Result signup() { return ok(signup.render(Form.form(SignUpForm.class))); } public Result newUser() { return Optional.ofNullable(Form.form(SignUpForm.class).bindFromRequest().get()).map(f -> { if (Objects.equals(f.password, f.passwordConfirmation)) { repository.create(f.name, f.password); return redirect(routes.AuthController.login()); } else { return redirect(routes.AuthController.signup()); } }).orElse(redirect(routes.AuthController.signup())); } }
This controller defines 5 actions: login
, logout
, signup
, newUser
and authenticate
:
- login: This action renders the log in page.
- logout: This action cleans up user’s session, deletes it from our
SessionRepository
and then redirects to login page. - signup: This action renders the create user page.
- newUser: This action receives a
SignUpForm
, creates a user with its data and redirects to login page. - authenticate: This action receives a
LoginForm
and creates a session with its data.
These LoginForm
and SignUpForm
in scala were written in the same file, in java they must be written in separated files:
LoginForm.java
package controllers; public class LoginForm { public String name; public String password; }
SignUpForm.java
package controllers; public class SignUpForm { public String name; public String password; public String passwordConfirmation; }
Now, let’s move on to the templates:
index.scala.html
@(user: String) @main(s"Welcome $user") { <h1>You are logged in!</h1> <p><a href="@routes.AuthController.logout()">Log out!</a></p> }
The index is a simple title that says “You are logged in!, with a link to log out.
login.scala.html
@(form: Form[LoginForm]) @main("Login") { @helper.form(routes.AuthController.authenticate()) { <h1>Login</h1> <p> <input type="text" name="name" placeholder="Name" value="@form("name").value"> </p> <p> <input type="password" name="password" placeholder="Password"> </p> <p> <button type="submit">Login</button> </p> <p> Don't have a user yet? <a href="@routes.AuthController.signup()">Sign up now!</a> </p> } }
The login page is a form that posts the LoginForm
to the authenticate
action and a link to the signup page.
signup.scala.html
@(form: Form[SignUpForm]) @main("Sign up!") { @helper.form(routes.AuthController.newUser()) { <h1>Sign up</h1> <p> <input type="text" name="name" placeholder="Name"> </p> <p> <input type="password" name="password" placeholder="Password"> </p> <p> <input type="password" name="passwordConfirmation" placeholder="Confirm Password"> </p> <p> <button type="submit">Sign up</button> </p> <p> Already have a user? <a href="@routes.AuthController.login()">Log in now!</a> </p> } }
The signup page is a form that submits de SignUpForm
to the newUser
action and a link to the login page.
Now, with all this done there is only one thing missing: we need to check if the user is logged in to prevent unauthenticated users to access our index page. So, in a package name controllers.action
we are going to use action composition to make that check.
Secured.scala
package controllers.action import play.api.mvc._ import repository.UserRepository import scala.concurrent.Future import scala.concurrent.ExecutionContext.Implicits.global case class Secured[A](action: Action[A]) extends Action[A] { private val repository: UserRepository = UserRepository override def apply(request: Request[A]): Future[Result] = request.session.get("user").map(repository.isLoggedIn) match { case None => Future(Results.Redirect("/login")) case Some(r) if r => action(request) case Some(r) if !r => Future(Results.Redirect("/login")) } override def parser: BodyParser[A] = action.parser }
The Scala way of doing this (one of many actually) is writing an Action
wrapper that, on its apply method, makes the check and if everything is correct continues with its underlying action.
Secured.java
package controllers.action; import org.apache.commons.lang3.StringUtils; import play.mvc.Http; import play.mvc.Result; import play.mvc.Security; import repository.UserRepository; public class Secured extends Security.Authenticator { private final UserRepository repository; public Secured() { repository = UserRepository.instance; } @Override public String getUsername(Http.Context context) { final String userName = context.session().get("user"); if (StringUtils.isNotBlank(userName) && repository.isLoggedIn(userName)) { return userName; } else { return null; } } @Override public Result onUnauthorized(Http.Context context) { return redirect("/login"); } }
In the java way, we extend from Security.Athenticator
and make the check in the getUsername
method, which is expected to return the user name if the user is logged in and null if not, and then in the onUnauthorized
method we write the redirect.
Now we modify Application
to use this Action
to make it unavailable to any user that is not authenticated.
Application.scala
package controllers import controllers.action.Secured import play.api.mvc._ class Application extends Controller { def index = Secured { Action { request => Ok(views.html.index(request.session.get("user").get)) } } }
Application.java
package controllers; import controllers.action.Secured; import play.*; import play.mvc.*; import views.html.*; public class Application extends Controller { @Security.Authenticated(Secured.class) public Result index() { return ok(index.render(session("user"))); } }
And now we connect everything in our routes file:
routes
# Routes # This file defines all application routes (Higher priority routes first) # ~~~~ # Home page GET / controllers.Application.index() GET /signup controllers.AuthController.signup() POST /signup controllers.AuthController.newUser() GET /login controllers.AuthController.login() POST /login controllers.AuthController.authenticate() GET /logout controllers.AuthController.logout() # Map static resources from the /public folder to the /assets URL path GET /assets/*file controllers.Assets.versioned(path="/public", file: Asset)
Now we run the application with activator run
and visit http://localhost:9000/
in our favourite browser. We’ll be redirected to http://localhost:9000/login
and we’ll see something like:
Then we click on the sign up link to create a user and we’ll be redirected to http://localhost:9000/signup
. We’ll see:
We create a user and after we are redirected again to the login page, we login and then we’ll finally see our index:
So, now we have an application with login! Let’s move on.
2. Security Headers
Play provides a lot of useful filters, between them there is SecurityHeadersFilter
which can be used to configure some headers in the HTTP response to mitigate security issues and provide an extra level of defense for new applications.
To make this filter available we need to add filters
to our dependency list, just as we added BCrypt. Now we need to add this new filter to our application, which is typically done by creating a Filters
class in the root of the project like this:
Filters.scala
import javax.inject.Inject import play.api.http.HttpFilters import play.filters.headers.SecurityHeadersFilter class Filters @Inject() (securityHeadersFilter: SecurityHeadersFilter) extends HttpFilters { def filters = Seq(securityHeadersFilter) }
Filters.java
import play.api.mvc.EssentialFilter; import play.filters.headers.SecurityHeadersFilter; import play.http.HttpFilters; import javax.inject.Inject; public class Filters implements HttpFilters { @Inject SecurityHeadersFilter securityHeadersFilter; public EssentialFilter[] filters() { return new EssentialFilter[] { securityHeadersFilter }; } }
If you don’t want to name it Filters
, or you want to locate it in another package, you just need to configure it in your application.conf
like: play.http.filters = "mypackage.MyFilters"
.
Now, these headers’ values are configured via our application.conf
, let’s see:
application.conf
user.inactivity.max=7200000 #Two Hours play.filters.headers.frameOptions="DENY" # SETS X-Frame-Options play.filters.headers.xssProtection="1; mode=block" # SETS X-XSS-Protection play.filters.headers.contentTypeOptions="nosniff" # SETS X-Content-Type-Options play.filters.headers.permittedCrossDomainPolicies="master-only" # SETS X-Permitted-Cross-Domain-Policies play.filters.headers.contentSecurityPolicy="default-src ‘self’" # SETS Content-Security-Policy
All these values are the ones that play sets by default. Every one of these headers can be disabled by configuring it as null
just as: play.filters.headers.frameOptions = null
.
3. CORS
CORS (Cross-Origin Resource Sharing) is a protocol that allows web applications to make requests across different domains. Play provides a filter that configures this protocol.
From the filters
added to your dependency when we added the security headers, we’ll add the new CORSFilter
to it:
Filters.scala
import javax.inject.Inject import play.api.http.HttpFilters import play.filters.headers.SecurityHeadersFilter class Filters @Inject() (securityHeadersFilter: SecurityHeadersFilter, corsFilter: CORSFilter) extends HttpFilters { def filters = Seq(securityHeadersFilter, corsFilter) }
Filters.java
import play.api.mvc.EssentialFilter; import play.filters.headers.SecurityHeadersFilter; import play.http.HttpFilters; import javax.inject.Inject; public class Filters implements HttpFilters { @Inject SecurityHeadersFilter securityHeadersFilter; @Inject CORSFilter corsFilter; public EssentialFilter[] filters() { return new EssentialFilter[] { securityHeadersFilter, corsFilter }; } }
And now we’ll configure it in our application.conf
:
... play.filters.cors { pathPrefixes = ["/some/path", ...] allowedOrigins = ["http://www.example.com", ...] allowedHttpMethods = ["GET", "POST"] allowedHttpHeaders = ["Accept"] preflightMaxAge = 3 days }
And just like that, CORS is configured in our application!
4. Download the Code Project
This was an example of security in a play application.
You can download the full source code of this example here: play-scala-example, play-java-example
Hello,
Thank you so much for this exemple project, as a beginner i really wished that your project can turn minimally on a database with ( insertion of name + password )