Skip to content
This repository has been archived by the owner on Sep 12, 2021. It is now read-only.

Commit

Permalink
Merge pull request #371 from akkie/master
Browse files Browse the repository at this point in the history
Stateless and non-stateless CookieAuthenticator
  • Loading branch information
akkie committed Jun 9, 2015
2 parents ae82b22 + 9eb6776 commit 188f18d
Show file tree
Hide file tree
Showing 9 changed files with 389 additions and 112 deletions.
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
## 3.0 (2015-06-03)
## 3.0 (2015-06-08)

- Update to Play 2.4
- Stateless and non-stateless CookieAuthenticator
- A lot of API enhancements

## 2.0 (2015-03-28)
Expand Down
2 changes: 1 addition & 1 deletion project/BuildSettings.scala
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ object BasicSettings extends AutoPlugin {

override def projectSettings = Seq(
organization := "com.mohiva",
version := "3.0.0-RC1",
version := "3.0.0-RC2",
resolvers ++= Dependencies.resolvers,
scalaVersion := Dependencies.Versions.scalaVersion,
crossScalaVersions := Dependencies.Versions.crossScala,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ case class FakeSessionAuthenticatorService() extends SessionAuthenticatorService
*/
case class FakeCookieAuthenticatorService() extends CookieAuthenticatorService(
new CookieAuthenticatorSettings(),
new FakeAuthenticatorDAO[CookieAuthenticator],
None,
new DefaultFingerprintGenerator(),
new SecureRandomIDGenerator(),
Clock())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,23 +23,27 @@ import com.mohiva.play.silhouette._
import com.mohiva.play.silhouette.api.exceptions._
import com.mohiva.play.silhouette.api.services.{ AuthenticatorResult, AuthenticatorService }
import com.mohiva.play.silhouette.api.services.AuthenticatorService._
import com.mohiva.play.silhouette.api.util.{ Clock, FingerprintGenerator, IDGenerator }
import com.mohiva.play.silhouette.api.util.{ Base64, Clock, FingerprintGenerator, IDGenerator }
import com.mohiva.play.silhouette.api.{ Logger, LoginInfo, StorableAuthenticator }
import com.mohiva.play.silhouette.impl.authenticators.CookieAuthenticatorService._
import com.mohiva.play.silhouette.impl.daos.AuthenticatorDAO
import org.joda.time.DateTime
import play.api.Play
import play.api.Play.current
import play.api.http.HeaderNames
import play.api.libs.Crypto
import play.api.libs.json.Json
import play.api.mvc._

import scala.concurrent.{ ExecutionContext, Future }
import scala.util.Try
import scala.util.{ Failure, Success, Try }

/**
* An authenticator that uses a cookie based approach. It works by storing an ID in a cookie
* to track the authenticated user and a server side backing store that maps the ID to an
* authenticator instance.
* An authenticator that uses a cookie based approach.
*
* It works either by storing an ID in a cookie to track the authenticated user and a server side backing
* store that maps the ID to an authenticator instance or by a stateless approach that stores the authenticator
* in a serialized form directly into the cookie.
*
* The authenticator can use sliding window expiration. This means that the authenticator times
* out after a certain time if it wasn't used. This can be controlled with the [[idleTimeout]]
Expand Down Expand Up @@ -92,25 +96,81 @@ case class CookieAuthenticator(
private def isTimedOut = idleTimeout.isDefined && lastUsedDate.plusSeconds(idleTimeout.get).isBeforeNow
}

/**
* The companion object of the authenticator.
*/
object CookieAuthenticator extends Logger {

/**
* Converts the CookieAuthenticator to Json and vice versa.
*/
implicit val jsonFormat = Json.format[CookieAuthenticator]

/**
* Serializes the authenticator.
*
* @param authenticator The authenticator to serialize.
* @param settings The authenticator settings.
* @return The serialized authenticator.
*/
def serialize(authenticator: CookieAuthenticator)(settings: CookieAuthenticatorSettings) = {
if (settings.encryptAuthenticator) {
Crypto.encryptAES(Json.toJson(authenticator).toString())
} else {
Base64.encode(Json.toJson(authenticator))
}
}

/**
* Unserializes the authenticator.
*
* @param str The string representation of the authenticator.
* @param settings The authenticator settings.
* @return Some authenticator on success, otherwise None.
*/
def unserialize(str: String)(settings: CookieAuthenticatorSettings): Try[CookieAuthenticator] = {
if (settings.encryptAuthenticator) buildAuthenticator(Crypto.decryptAES(str))
else buildAuthenticator(Base64.decode(str))
}

/**
* Builds the authenticator from Json.
*
* @param str The string representation of the authenticator.
* @return Some authenticator on success, otherwise None.
*/
private def buildAuthenticator(str: String): Try[CookieAuthenticator] = {
Try(Json.parse(str)) match {
case Success(json) => json.validate[CookieAuthenticator].asEither match {
case Left(error) => Failure(new AuthenticatorException(InvalidJsonFormat.format(ID, error)))
case Right(authenticator) => Success(authenticator)
}
case Failure(error) => Failure(new AuthenticatorException(JsonParseError.format(ID, str), error))
}
}
}

/**
* The service that handles the cookie authenticator.
*
* @param settings The cookie settings.
* @param dao The DAO to store the authenticator.
* @param dao The DAO to store the authenticator. Set it to None to use a stateless approach.
* @param fingerprintGenerator The fingerprint generator implementation.
* @param idGenerator The ID generator used to create the authenticator ID.
* @param clock The clock implementation.
* @param executionContext The execution context to handle the asynchronous operations.
*/
class CookieAuthenticatorService(
val settings: CookieAuthenticatorSettings,
dao: AuthenticatorDAO[CookieAuthenticator],
dao: Option[AuthenticatorDAO[CookieAuthenticator]],
fingerprintGenerator: FingerprintGenerator,
idGenerator: IDGenerator,
clock: Clock)(implicit val executionContext: ExecutionContext)
extends AuthenticatorService[CookieAuthenticator]
with Logger {

import CookieAuthenticator._

/**
* The type of this class.
*/
Expand Down Expand Up @@ -165,13 +225,21 @@ class CookieAuthenticatorService(
if (settings.useFingerprinting) Some(fingerprintGenerator.generate) else None
}).flatMap { fingerprint =>
request.cookies.get(settings.cookieName) match {
case Some(cookie) => dao.find(cookie.value).map {
case Some(a) if fingerprint.isDefined && a.fingerprint != fingerprint =>
logger.info(InvalidFingerprint.format(ID, fingerprint, a))
None
case Some(a) => Some(a)
case None => None
}
case Some(cookie) =>
(dao match {
case Some(d) => d.find(cookie.value)
case None => unserialize(cookie.value)(settings) match {
case Success(authenticator) => Future.successful(Some(authenticator))
case Failure(error) =>
logger.info(error.getMessage, error)
Future.successful(None)
}
}).map {
case Some(a) if fingerprint.isDefined && a.fingerprint != fingerprint =>
logger.info(InvalidFingerprint.format(ID, fingerprint, a))
None
case v => v
}
case None => Future.successful(None)
}
}.recover {
Expand All @@ -180,18 +248,23 @@ class CookieAuthenticatorService(
}

/**
* Creates a new cookie for the given authenticator and return it. The authenticator will also be
* Creates a new cookie for the given authenticator and return it.
*
* If the non-stateless approach will be used the the authenticator will also be
* stored in the backing store.
*
* @param authenticator The authenticator instance.
* @param request The request header.
* @return The serialized authenticator value.
*/
override def init(authenticator: CookieAuthenticator)(implicit request: RequestHeader): Future[Cookie] = {
dao.add(authenticator).map { a =>
(dao match {
case Some(d) => d.add(authenticator).map(_.id)
case None => Future.successful(serialize(authenticator)(settings))
}).map { value =>
Cookie(
name = settings.cookieName,
value = a.id,
value = value,
maxAge = settings.cookieMaxAge,
path = settings.cookiePath,
domain = settings.cookieDomain,
Expand Down Expand Up @@ -243,9 +316,10 @@ class CookieAuthenticatorService(
}

/**
* Updates the authenticator with the new last used date in the backing store.
* Updates the authenticator with the new last used date.
*
* We needn't embed the cookie in the response here because the cookie itself will not be changed.
* If the stateless approach will be used then we update the cookie on the client. With the non-stateless
* approach we needn't embed the cookie in the response here because the cookie itself will not be changed.
* Only the authenticator in the backing store will be changed.
*
* @param authenticator The authenticator to update.
Expand All @@ -256,9 +330,18 @@ class CookieAuthenticatorService(
override def update(authenticator: CookieAuthenticator, result: Result)(
implicit request: RequestHeader): Future[AuthenticatorResult] = {

dao.update(authenticator).map { a =>
AuthenticatorResult(result)
}.recover {
(dao match {
case Some(d) => d.update(authenticator).map(_ => AuthenticatorResult(result))
case None => Future.successful(AuthenticatorResult(result.withCookies(Cookie(
name = settings.cookieName,
value = serialize(authenticator)(settings),
maxAge = settings.cookieMaxAge,
path = settings.cookiePath,
domain = settings.cookieDomain,
secure = settings.secureCookie,
httpOnly = settings.httpOnlyCookie
))))
}).recover {
case e => throw new AuthenticatorUpdateException(UpdateError.format(ID, authenticator), e)
}
}
Expand All @@ -275,7 +358,10 @@ class CookieAuthenticatorService(
* @return The serialized expression of the authenticator.
*/
override def renew(authenticator: CookieAuthenticator)(implicit request: RequestHeader): Future[Cookie] = {
dao.remove(authenticator.id).flatMap { _ =>
(dao match {
case Some(d) => d.remove(authenticator.id)
case None => Future.successful(())
}).flatMap { _ =>
create(authenticator.loginInfo).flatMap(init)
}.recover {
case e => throw new AuthenticatorRenewalException(RenewError.format(ID, authenticator), e)
Expand All @@ -285,8 +371,8 @@ class CookieAuthenticatorService(
/**
* Renews an authenticator and replaces the authenticator cookie with a new one.
*
* The old authenticator will be revoked in the backing store. After that it isn't possible to use
* a cookie which was bound to this authenticator.
* If the non-stateless approach will be used then the old authenticator will be revoked in the backing
* store. After that it isn't possible to use a cookie which was bound to this authenticator.
*
* @param authenticator The authenticator to update.
* @param result The result to manipulate.
Expand All @@ -302,7 +388,9 @@ class CookieAuthenticatorService(
}

/**
* Discards the cookie and remove the authenticator from backing store.
* Discards the cookie.
*
* If the non-stateless approach will be used then the authenticator will also be removed from backing store.
*
* @param result The result to manipulate.
* @param request The request header.
Expand All @@ -311,7 +399,10 @@ class CookieAuthenticatorService(
override def discard(authenticator: CookieAuthenticator, result: Result)(
implicit request: RequestHeader): Future[AuthenticatorResult] = {

dao.remove(authenticator.id).map { _ =>
(dao match {
case Some(d) => d.remove(authenticator.id)
case None => Future.successful(())
}).map { _ =>
AuthenticatorResult(result.discardingCookies(DiscardingCookie(
name = settings.cookieName,
path = settings.cookiePath,
Expand All @@ -336,6 +427,8 @@ object CookieAuthenticatorService {
/**
* The error messages.
*/
val JsonParseError = "[Silhouette][%s] Cannot parse Json: %s"
val InvalidJsonFormat = "[Silhouette][%s] Invalid Json format: %s"
val InvalidFingerprint = "[Silhouette][%s] Fingerprint %s doesn't match authenticator: %s"
}

Expand All @@ -358,6 +451,7 @@ case class CookieAuthenticatorSettings(
cookieDomain: Option[String] = None,
secureCookie: Boolean = Play.isProd, // Default to sending only for HTTPS in production, but not for development and test.
httpOnlyCookie: Boolean = true,
encryptAuthenticator: Boolean = true,
useFingerprinting: Boolean = true,
cookieMaxAge: Option[Int] = Some(12 * 60 * 60),
authenticatorIdleTimeout: Option[Int] = Some(30 * 60),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -457,7 +457,6 @@ object JWTAuthenticatorService {
*/
val InvalidJWTToken = "[Silhouette][%s] Error on parsing JWT token: %s"
val JsonParseError = "[Silhouette][%s] Cannot parse Json: %s"
val InvalidJsonFormat = "[Silhouette][%s] Invalid Json format: %s"
val UnexpectedJsonValue = "[Silhouette][%s] Unexpected Json value: %s"
val OverrideReservedClaim = "[Silhouette][%s] Try to overriding a reserved claim `%s`; list of reserved claims: %s"

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ object SessionAuthenticator extends Logger {
* @param settings The authenticator settings.
* @return Some authenticator on success, otherwise None.
*/
def unserialize(str: String)(settings: SessionAuthenticatorSettings): Option[SessionAuthenticator] = {
def unserialize(str: String)(settings: SessionAuthenticatorSettings): Try[SessionAuthenticator] = {
if (settings.encryptAuthenticator) buildAuthenticator(Crypto.decryptAES(str))
else buildAuthenticator(Base64.decode(str))
}
Expand All @@ -125,17 +125,13 @@ object SessionAuthenticator extends Logger {
* @param str The string representation of the authenticator.
* @return Some authenticator on success, otherwise None.
*/
private def buildAuthenticator(str: String): Option[SessionAuthenticator] = {
private def buildAuthenticator(str: String): Try[SessionAuthenticator] = {
Try(Json.parse(str)) match {
case Success(json) => json.validate[SessionAuthenticator].asEither match {
case Left(error) =>
logger.info(InvalidJsonFormat.format(ID, error))
None
case Right(authenticator) => Some(authenticator)
case Left(error) => Failure(new AuthenticatorException(InvalidJsonFormat.format(ID, error)))
case Right(authenticator) => Success(authenticator)
}
case Failure(error) =>
logger.info(JsonParseError.format(ID, str), error)
None
case Failure(error) => Failure(new AuthenticatorException(JsonParseError.format(ID, str), error))
}
}
}
Expand Down Expand Up @@ -209,12 +205,16 @@ class SessionAuthenticatorService(
Future.from(Try {
if (settings.useFingerprinting) Some(fingerprintGenerator.generate) else None
}).map { fingerprint =>
request.session.get(settings.sessionKey).flatMap(v => unserialize(v)(settings)) match {
case Some(a) if fingerprint.isDefined && a.fingerprint != fingerprint =>
logger.info(InvalidFingerprint.format(ID, fingerprint, a))
None
case Some(a) => Some(a)
case None => None
request.session.get(settings.sessionKey).flatMap { value =>
unserialize(value)(settings) match {
case Success(authenticator) if fingerprint.isDefined && authenticator.fingerprint != fingerprint =>
logger.info(InvalidFingerprint.format(ID, fingerprint, authenticator))
None
case Success(authenticator) => Some(authenticator)
case Failure(error) =>
logger.info(error.getMessage, error)
None
}
}
}.recover {
case e => throw new AuthenticatorRetrievalException(RetrieveError.format(ID), e)
Expand Down
Loading

0 comments on commit 188f18d

Please sign in to comment.