This module offers Authentication and Authorization features to Play2.0 applications
This module is targets the Scala version of Play2.0.
For the Java version of Play2.0, there is an authorization module called Deadbolt 2.
This module has been tested on Play2.0final and Play2.0.1.
Security
trait in Play2.0 API does not define an identifier that identifies a user.
If you use an E-mail or a user ID as an identier, users can not invalidate the session when the cookie leaks.
This module creates a unique SessionID using a secure random number generator. Even if the cookie leaks, users can invalidate the session by logging in again and your application can set a time limit for sessions.
Since Security
trait in Play2.0 API returns Action
,
complicated action methods are nested too deep.
This module provides an interface that return Either[PlainResult, User]
.
so, writing complicated action methods is easy.
-
add a repository resolver into your
Build.scala
orbuild.sbt
file.resolvers += "t2v.jp repo" at "http://www.t2v.jp/maven-repo/"
-
add a dependency declaration into your
Build.scala
orbuild.sbt
file.-
stable release
"jp.t2v" %% "play20.auth" % "0.2"
-
current version
"jp.t2v" %% "play20.auth" % "0.3-SNAPSHOT"
-
For example: Build.scala
val appDependencies = Seq(
"jp.t2v" %% "play20.auth" % "0.1"
)
val main = PlayProject(appName, appVersion, appDependencies, mainLang = SCALA).settings(
resolvers += "t2v.jp repo" at "http://www.t2v.jp/maven-repo/"
)
-
First step, create a trait that is mixed-in
jp.t2v.lab.play20.auth.AuthConfig
inapp/controllers
.// Example trait AuthConfigImpl extends AuthConfig { /** * A type that is used to identify a user. * `String`, `Int`, `Long` and so on. */ type Id = String /** * A type that represents a user in your application. * `User`, `Account` and so on. */ type User = Account /** * A type that is defined by every action for authorization. * This sample uses the following trait. * * sealed trait Permission * case object Administrator extends Permission * case object NormalUser extends Permission */ type Authority = Permission /** * A `ClassManifest` is used to get an id from the Cache API. * Basically use the same setting as the following. */ val idManifest: ClassManifest[Id] = classManifest[Id] /** * A duration of the session timeout in seconds */ val sessionTimeoutInSeconds: Int = 3600 /** * A function that returns a `User` object from an `Id`. * Describe the procedure according to your application. */ def resolveUser(id: Id): Option[User] = Account.findById(id) /** * A redirect target after a successful user login. */ def loginSucceeded[A](request: Request[A]): PlainResult = Redirect(routes.Message.main) /** * A redirect target after a successful user logout. */ def logoutSucceeded[A](request: Request[A]): PlainResult = Redirect(routes.Application.login) /** * A redirect target after a failed authentication. */ def authenticationFailed[A](request: Request[A]): PlainResult = Redirect(routes.Application.login) /** * A redirect target after a failed authorization. */ def authorizationFailed[A](request: Request[A]): PlainResult = Forbidden("no permission") /** * A function that authorizes a user by `Authority`. * Describe the procedure according to your application. */ def authorize(user: User, authority: Authority): Boolean = (user.permission, authority) match { case (Administrator, _) => true case (NormalUser, NormalUser) => true case _ => false } }
-
Next step, create a
Controller
that defines login and logout actions. ThisController
is mixed withjp.t2v.lab.play20.auth.LoginLogout
trait and the trait that was created in first step.object Application extends Controller with LoginLogout with AuthConfigImpl { /** Describe the login form according to your application. */ val loginForm = Form { mapping("email" -> email, "password" -> text)(Account.authenticate)(_.map(u => (u.email, ""))) .verifying("Invalid email or password", result => result.isDefined) } /** Describe the login page action according to your application. */ def login = Action { implicit request => Ok(html.login(loginForm)) } /** * Return the `gotoLogoutSucceeded` method's result in the logout action. * * Since the `gotoLogoutSucceeded` returns `PlainResult`, * you can add a procedure like the following. * * gotoLogoutSucceeded.flashing( * "success" -> "You've been logged out" * ) */ def logout = Action { implicit request => // do something... gotoLogoutSucceeded } /** * Return the `gotoLoginSucceeded` method's result in the login action. * * Since the `gotoLoginSucceeded` returns `PlainResult`, * you can add a procedure like the `gotoLogoutSucceeded`. */ def authenticate = Action { implicit request => loginForm.bindFromRequest.fold( formWithErrors => BadRequest(html.login(formWithErrors)), user => gotoLoginSucceeded(user.get.id) ) } }
-
Last step, mix
jp.t2v.lab.play20.auth.Auth
trait and the trait that was created in first step into your Controllers.object Message extends Controller with Auth with AuthConfigImpl { // The `authorizedAction` method // takes an `Authority` as a first argument and // takes a function whose type is `User => Request[AnyContent] => Result` as a second argument and // returns an `Action` def main = authorizedAction(NormalUser) { user => implicit request => val title = "message main" Ok(html.message.main(title)) } def list = authorizedAction(NormalUser) { user => implicit request => val title = "all messages" Ok(html.message.list(title)) } def detail(id: Int) = authorizedAction(NormalUser) { user => implicit request => val title = "messages detail " Ok(html.message.detail(title + id)) } // Only Administrator can execute this action. def write = authorizedAction(Administrator) { user => implicit request => val title = "write message" Ok(html.message.write(title)) } }
For example, a SNS application has a function that edit messages.
Your application should make it possible for a user to edit their own messages and impossible to edit other people's messages.
In this case, it is easy if Authority
is a Function
as follows.
trait AuthConfigImpl extends AuthConfig {
// Other setup is omitted.
type Authority = User => Boolean
def authorize(user: User, authority: Authority): Boolean = authority(user)
}
object Application extends Controller with Auth with AuthConfigImpl {
private def sameAuthor(messageId: Int)(account: Account): Boolean =
Message.getAuther(messageId) == account
def edit(messageId: Int) = authorizedAction(sameAuthor(messageId)) { user => request =>
val target = Message.findById(messageId)
Ok(html.message.edit(messageForm.fill(target)))
}
}
For example, when an unauthenticated user requests access to non-login page, your application redirects the user to the login page. Then, when the user successfully logs in, your application redirects the user to the originally requested page.
In this case, you only have to change authenticationFailed
and loginSucceeded
as follows.
trait AuthConfigImpl extends AuthConfig {
// Other settings are omitted.
def authenticationFailed[A](request: Request[A]): PlainResult =
Redirect(routes.Application.login).withSession("access_uri" -> request.uri)
def loginSucceeded[A](request: Request[A]): PlainResult = {
val uri = request.session.get("access_uri").getOrElse(routes.Message.main.url)
request.session - "access_uri"
Redirect(uri)
}
}
For example, you want to validate token at every action to defeat a CSRF attack.
Since it is impractical to perform the validation in all actions, Usually a method is defined as follows.
object Application extends Controller {
// Other settings are omitted.
val tokenForm = Form("token" -> text)
private def validateToken(request: Request[AnyContent]): Boolean = (for {
tokenInForm <- tokenForm.bindFromRequest(request).value
tokenInSession <- request.session.get("token")
} yield tokenInForm == tokenInSession).getOrElse(false)
private def validAction(f: Request[AnyContent] => Result) = Action { request =>
if (validateToken(request)) f(request)
else BadRequest
}
def page1 = validAction { request =>
// do something
Ok(html.page1("result"))
}
def page2 = validAction { request =>
// do something
Ok(html.page2("result"))
}
}
How do you incorporate a function that authenticates and authorizes a user in validateToken
?
You need to use the authorized
method insted of authorizedAction
method.
object Application extends Controller with Auth with AuthConfigImpl {
// The token publication is omitted.
val tokenForm = Form("token" -> text)
private def validateToken(implicit request: Request[AnyContent]): Boolean = (for {
tokenInForm <- tokenForm.bindFromRequest(request).value
tokenInSession <- request.session.get("token")
} yield tokenInForm == tokenInSession).getOrElse(false)
private authAndValidAction(authority: Authority)(f: User => Request[AnyContent] => Result) =
Action { implicit request =>
(for {
user <- authorized(authority).right
_ <- Either.cond(validateToken, (), BadRequest).right
} yield f(user)(request)).merge
}
def page1 = authAndValidAction(NormalUser) { user => request =>
// do something
Ok(html.page1("result"))
}
def page2 = authAndValidAction(NormalUser) { user => request =>
// do something
Ok(html.page2("result"))
}
}
This example is complex.
Then, how do you incorporate function that changes templates dynamically by pjax ?
This is easy:
private type Template = String => Html
private def pjax(implicit request: Request[AnyContent]): Template = {
if (request.headers.keys("X-PJAX")) {
html.pjaxTemplate.apply
} else {
val displayValues = DomainLogic.getDisplayValues()
html.fullTemplate.apply(displayValues)
}
}
private complexAction(authority: Authority)(f: User => Template => Request[AnyContent] => Result) =
Action { implicit request =>
(for {
user <- authorized(authority).right
_ <- Either.cond(validateToken, (), BadRequest).right
template <- Right(pjax).right
} yield f(user)(template)(request)).merge
}
def page1 = complexAction(NormalUser) { user => template => request =>
// do something
Ok(template("result"))
}
def page2 = complexAction(NormalUser) { user => template => request =>
// do something
Ok(template("result"))
}
Thus, you can combine functions for action methods.
git clone https://github.com/t2v/play20-auth.git
cd play20-auth
play
run
- access to
http://localhost:9000/
on your browser.-
click
Apply this script now!
-
login
defined accounts
Email | Password | Permission alice@example.com | secret | Administrator bob@example.com | secret | NormalUser chris@example.com | secret | NormalUser
-
This module uses the Cache API of Play2.0.
Ehcache, the default implementation, can not treat authentication information appropriately when the application servers are distributed.
If you have distributed servers, you should rather use the Memcached Plugin or something similar.
This library is released under the Apache Software License, version 2,
which should be included with the source in a file named LICENSE
.