Skip to content

Commit

Permalink
Merge pull request #107 from t2v/feature/cookie_max_age
Browse files Browse the repository at this point in the history
Supported cookie max age
  • Loading branch information
gakuzzzz committed Nov 12, 2014
2 parents 996911a + 138f61a commit ee6dcf0
Show file tree
Hide file tree
Showing 42 changed files with 864 additions and 338 deletions.
22 changes: 11 additions & 11 deletions module/src/main/scala/jp/t2v/lab/play2/auth/AsyncAuth.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,36 +4,36 @@ import play.api.mvc._
import play.api.libs.iteratee.{Iteratee, Done}
import scala.concurrent.{ExecutionContext, Future}

trait AsyncAuth {
trait AsyncAuth extends CookieSupport {
self: AuthConfig with Controller =>

def authorized(authority: Authority)(implicit request: RequestHeader, context: ExecutionContext): Future[Either[Result, User]] = {
def authorized(authority: Authority)(implicit request: RequestHeader, context: ExecutionContext): Future[Either[Result, (User, CookieUpdater)]] = {
restoreUser collect {
case Some(user) => Right(user)
case (Some(user), cookieUpdater) => Right(user -> cookieUpdater)
} recoverWith {
case _ => authenticationFailed(request).map(Left.apply)
} flatMap {
case Right(user) => authorize(user, authority) collect {
case true => Right(user)
case Right((user, cookieUpdater)) => authorize(user, authority) collect {
case true => Right(user -> cookieUpdater)
} recoverWith {
case _ => authorizationFailed(request).map(Left.apply)
}
case Left(result) => Future.successful(Left(result))
}
}

private[auth] def restoreUser(implicit request: RequestHeader, context: ExecutionContext): Future[Option[User]] = {
private[auth] def restoreUser(implicit request: RequestHeader, context: ExecutionContext): Future[(Option[User], CookieUpdater)] = {
(for {
cookie <- request.cookies.get(cookieName)
token <- CookieUtil.verifyHmac(cookie)
token <- verifyHmac(cookie)
} yield for {
Some(userId) <- idContainer.get(token)
Some(user) <- resolveUser(userId)
_ <- idContainer.prolongTimeout(token, sessionTimeoutInSeconds)
} yield {
Option(user)
Option(user) -> bakeCookie(token) _
}) getOrElse {
Future.successful(Option.empty)
Future.successful(Option.empty -> identity)
}
}

Expand All @@ -45,7 +45,7 @@ trait AsyncAuth {
def async[A](p: BodyParser[A], authority: Authority)(f: User => Request[A] => Future[Result])(implicit context: ExecutionContext): Action[(A, User)] = {
val parser = BodyParser {
req => Iteratee.flatten(authorized(authority)(req, context).map {
case Right(user) => p.map((_, user)).apply(req)
case Right((user, _)) => p.map((_, user)).apply(req)
case Left(result) => Done[Array[Byte], Either[Result, (A, User)]](Left(result))
})
}
Expand All @@ -65,7 +65,7 @@ trait AsyncAuth {
async(BodyParsers.parse.anyContent)(f)

def async[A](p: BodyParser[A])(f: Option[User] => Request[A] => Future[Result])(implicit context: ExecutionContext): Action[A] =
Action.async(p)(req => restoreUser(req, context).flatMap(user => f(user)(req)) )
Action.async(p)(req => restoreUser(req, context).flatMap { case (user, _) => f(user)(req)})

def apply(f: Option[User] => (Request[AnyContent] => Result))(implicit context: ExecutionContext): Action[AnyContent] =
async(f.andThen(_.andThen(t=>Future.successful(t))))
Expand Down
55 changes: 0 additions & 55 deletions module/src/main/scala/jp/t2v/lab/play2/auth/Auth.scala

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,13 @@ trait AuthActionBuilders extends AsyncAuth { self: AuthConfig with Controller =>
final case class GenericOptionalAuthRequest[A, R[_] <: Request[_]](user: Option[User], underlying: R[A]) extends WrappedRequest[A](underlying.asInstanceOf[Request[A]])
final case class GenericAuthRequest[A, R[_] <: Request[_]](user: User, underlying: R[A]) extends WrappedRequest[A](underlying.asInstanceOf[Request[A]])

final case class GenericOptionalAuthRefiner[R[_] <: Request[_]]() extends ActionRefiner[R, ({type L[A] = GenericOptionalAuthRequest[A, R]})#L] {
protected def refine[A](request: R[A]): Future[Either[Result, GenericOptionalAuthRequest[A, R]]] = {
final case class GenericOptionalAuthFunction[R[_] <: Request[_]]() extends ActionFunction[R, ({type L[A] = GenericOptionalAuthRequest[A, R]})#L] {
def invokeBlock[A](request: R[A], block: GenericOptionalAuthRequest[A, R] => Future[Result]) = {
implicit val ctx = executionContext
restoreUser(request, executionContext) recover {
case _ => None
} map { user =>
Right(GenericOptionalAuthRequest[A, R](user, request))
case _ => None -> identity[Result] _
} flatMap { case (user, cookieUpdater) =>
block(GenericOptionalAuthRequest[A, R](user, request)).map(cookieUpdater)
}
}
}
Expand Down Expand Up @@ -47,7 +47,7 @@ trait AuthActionBuilders extends AsyncAuth { self: AuthConfig with Controller =>
}

final def composeOptionalAuthAction[R[_] <: Request[_]](builder: ActionBuilder[R]): ActionBuilder[({type L[A] = GenericOptionalAuthRequest[A, R]})#L] = {
builder.andThen[({type L[A] = GenericOptionalAuthRequest[A, R]})#L](GenericOptionalAuthRefiner[R]())
builder.andThen[({type L[A] = GenericOptionalAuthRequest[A, R]})#L](GenericOptionalAuthFunction[R]())
}

final def composeAuthenticationAction[R[_] <: Request[_]](builder: ActionBuilder[R]): ActionBuilder[({type L[A] = GenericAuthRequest[A, R]})#L] = {
Expand All @@ -60,9 +60,9 @@ trait AuthActionBuilders extends AsyncAuth { self: AuthConfig with Controller =>

final type OptionalAuthRequest[A] = GenericOptionalAuthRequest[A, Request]
final type AuthRequest[A] = GenericAuthRequest[A, Request]
final type OptionalAuthRefiner = GenericOptionalAuthRefiner[Request]
final type AuthenticationRefiner = GenericAuthenticationRefiner[Request]
final type AuthorizationFilter = GenericAuthorizationFilter[Request]
final val OptionalAuthFunction: ActionFunction[Request, OptionalAuthRequest] = GenericOptionalAuthFunction[Request]()
final val AuthenticationRefiner: ActionRefiner[OptionalAuthRequest, AuthRequest] = GenericAuthenticationRefiner[Request]()
final def AuthorizationFilter(authority: Authority): ActionFilter[AuthRequest] = GenericAuthorizationFilter[Request](authority)

final val OptionalAuthAction: ActionBuilder[OptionalAuthRequest] = composeOptionalAuthAction(Action)
final val AuthenticationAction: ActionBuilder[AuthRequest] = composeAuthenticationAction(Action)
Expand Down
2 changes: 2 additions & 0 deletions module/src/main/scala/jp/t2v/lab/play2/auth/AuthConfig.scala
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,6 @@ trait AuthConfig {

lazy val cookiePathOption: String = "/"

lazy val isTransientCookie: Boolean = false

}
16 changes: 9 additions & 7 deletions module/src/main/scala/jp/t2v/lab/play2/auth/AuthElement.scala
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ trait AuthElement extends StackableController with AsyncAuth {
implicit val (r, ctx) = (req, StackActionExecutionContext(req))
req.get(AuthorityKey) map { authority =>
authorized(authority) flatMap {
case Right(user) => super.proceed(req.set(AuthKey, user))(f)
case Left(result) => Future.successful(result)
case Right((user, cookieUpdater)) => super.proceed(req.set(AuthKey, user))(f).map(cookieUpdater)
case Left(result) => Future.successful(result)
}
} getOrElse {
authorizationFailed(req)
Expand All @@ -33,8 +33,10 @@ trait OptionalAuthElement extends StackableController with AsyncAuth {

override def proceed[A](req: RequestWithAttributes[A])(f: RequestWithAttributes[A] => Future[Result]): Future[Result] = {
implicit val (r, ctx) = (req, StackActionExecutionContext(req))
val maybeUserFuture = restoreUser.recover { case _ => Option.empty }
maybeUserFuture.flatMap(maybeUser => super.proceed(maybeUser.map(u => req.set(AuthKey, u)).getOrElse(req))(f))
val maybeUserFuture = restoreUser.recover { case _ => None -> identity[Result] _ }
maybeUserFuture.flatMap { case (maybeUser, cookieUpdater) =>
super.proceed(maybeUser.map(u => req.set(AuthKey, u)).getOrElse(req))(f).map(cookieUpdater)
}
}

implicit def loggedIn[A](implicit req: RequestWithAttributes[A]): Option[User] = req.get(AuthKey)
Expand All @@ -48,10 +50,10 @@ trait AuthenticationElement extends StackableController with AsyncAuth {
override def proceed[A](req: RequestWithAttributes[A])(f: RequestWithAttributes[A] => Future[Result]): Future[Result] = {
implicit val (r, ctx) = (req, StackActionExecutionContext(req))
restoreUser recover {
case _ => Option.empty
case _ => None -> identity[Result] _
} flatMap {
case Some(u) => super.proceed(req.set(AuthKey, u))(f)
case None => authenticationFailed(req)
case (Some(u), cookieUpdater) => super.proceed(req.set(AuthKey, u))(f).map(cookieUpdater)
case (None, _) => authenticationFailed(req)
}
}

Expand Down
20 changes: 20 additions & 0 deletions module/src/main/scala/jp/t2v/lab/play2/auth/CookieSupport.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package jp.t2v.lab.play2.auth

import play.api.libs.Crypto
import play.api.mvc.{Result, Cookie}

trait CookieSupport { self: AuthConfig =>

def verifyHmac(cookie: Cookie): Option[String] = {
val (hmac, value) = cookie.value.splitAt(40)
if (Crypto.sign(value) == hmac) Some(value) else None
}

def bakeCookie(token: String)(result: Result): Result = {
val value = Crypto.sign(token) + token
val maxAge = if (isTransientCookie) None else Some(sessionTimeoutInSeconds)
result.withCookies(Cookie(cookieName, value, maxAge, cookiePathOption, cookieDomainOption, cookieSecureOption, cookieHttpOnlyOption))
}

}

16 changes: 0 additions & 16 deletions module/src/main/scala/jp/t2v/lab/play2/auth/CookieUtil.scala

This file was deleted.

9 changes: 3 additions & 6 deletions module/src/main/scala/jp/t2v/lab/play2/auth/LoginLogout.scala
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import play.api.mvc.Cookie
import play.api.libs.Crypto
import scala.concurrent.{Future, ExecutionContext}

trait LoginLogout {
trait LoginLogout extends CookieSupport {
self: Controller with AuthConfig =>

def gotoLoginSucceeded(userId: Id)(implicit request: RequestHeader, ctx: ExecutionContext): Future[Result] = {
Expand All @@ -14,18 +14,15 @@ trait LoginLogout {

def gotoLoginSucceeded(userId: Id, result: => Future[Result])(implicit request: RequestHeader, ctx: ExecutionContext): Future[Result] = for {
token <- idContainer.startNewSession(userId, sessionTimeoutInSeconds)
value = Crypto.sign(token) + token
r <- result
} yield {
r.withCookies(Cookie(cookieName, value, None, cookiePathOption, cookieDomainOption, cookieSecureOption, cookieHttpOnlyOption))
}
} yield bakeCookie(token)(r)

def gotoLogoutSucceeded(implicit request: RequestHeader, ctx: ExecutionContext): Future[Result] = {
gotoLogoutSucceeded(logoutSucceeded(request))
}

def gotoLogoutSucceeded(result: => Future[Result])(implicit request: RequestHeader, ctx: ExecutionContext): Future[Result] = {
request.cookies.get(cookieName) flatMap CookieUtil.verifyHmac foreach idContainer.remove
request.cookies.get(cookieName) flatMap verifyHmac foreach idContainer.remove
result.map(_.discardingCookies(DiscardingCookie(cookieName)))
}
}
4 changes: 4 additions & 0 deletions module/src/main/scala/jp/t2v/lab/play2/auth/package.scala
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
package jp.t2v.lab.play2

import play.api.mvc.Result

package object auth {

type AuthenticityToken = String

type CookieUpdater = Result => Result

}
3 changes: 2 additions & 1 deletion project/Build.scala
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,8 @@ object ApplicationBuild extends Build {
.settings(
libraryDependencies += play.Play.autoImport.jdbc,
libraryDependencies += "org.mindrot" % "jbcrypt" % "0.3m",
libraryDependencies += "org.scalikejdbc" %% "scalikejdbc-test" % "2.0.0" % "test",
libraryDependencies += "org.scalikejdbc" %% "scalikejdbc" % "2.2.0",
libraryDependencies += "org.scalikejdbc" %% "scalikejdbc-test" % "2.2.0" % "test",
libraryDependencies += "org.scalikejdbc" %% "scalikejdbc-play-plugin" % "2.3.0",
libraryDependencies += "com.github.tototoshi" %% "play-flyway" % "1.1.0",
TwirlKeys.templateImports in Compile += "jp.t2v.lab.play2.auth.sample._",
Expand Down
4 changes: 1 addition & 3 deletions sample/app/Global.scala
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
import play.api._

import jp.t2v.lab.play2.auth.sample._
import jp.t2v.lab.play2.auth.sample.Permission._
import scalikejdbc._

object Global extends GlobalSettings {

override def onStart(app: Application) {
// Class.forName("org.h2.Driver")
// ConnectionPool.singleton("jdbc:h2:mem:play", "sa", "")

if (Account.findAll.isEmpty) {
Seq(
Account(1, "alice@example.com", "secret", "Alice", Administrator),
Expand Down
Loading

0 comments on commit ee6dcf0

Please sign in to comment.