All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.mle.play.auth.RememberMe.scala Maven / Gradle / Ivy

The newest version!
package com.mle.play.auth

import com.mle.play.controllers.AuthResult
import com.mle.util.Log
import play.api.mvc.{Cookie, CookieBaker, DiscardingCookie, RequestHeader}

import scala.util.Random

/**
 * Adapted from https://github.com/wsargent/play20-rememberme
 *
 * @author Michael
 */
object RememberMe extends CookieBaker[UnAuthToken] with Log {
  val COOKIE_NAME = "REMEMBER_ME"
  val SERIES_NAME = "series"
  val USER_ID_NAME = "userId"
  val TOKEN_NAME = "token"
  val discardingCookie = DiscardingCookie(COOKIE_NAME)

  import scala.concurrent.duration.DurationInt

  override def maxAge: Option[Int] = Some(365.days.toSeconds.toInt)

  /**
   * @param req request
   * @return the browser's possibly stored token
   */
  def readToken(req: RequestHeader): Option[UnAuthToken] = {
    val cookie = req.cookies get COOKIE_NAME
    log debug s"Reading cookie: $cookie"
    val tokenMaybeEmpty = decodeFromCookie(cookie)
    if (!tokenMaybeEmpty.isEmpty) {
      log debug s"Read token: $tokenMaybeEmpty"
    }
    Option(tokenMaybeEmpty).filterNot(_.isEmpty)
  }

  override val emptyCookie: UnAuthToken = UnAuthToken.empty

  override protected def serialize(cookie: UnAuthToken): Map[String, String] = Map(
    USER_ID_NAME -> cookie.user,
    SERIES_NAME -> cookie.series.toString,
    TOKEN_NAME -> cookie.token.toString
  )

  /**
   * The API says we must return a token, even if deserialization fails, so we introduce the concept of an "empty" token
   * and filter it away in `readToken(RequestHeader)`.
   *
   * @param data token data
   * @return a token
   */
  override protected def deserialize(data: Map[String, String]): UnAuthToken = try {
    val maybeToken =
      for {
        u <- data get USER_ID_NAME
        s <- data get SERIES_NAME
        t <- data get TOKEN_NAME
      } yield UnAuthToken(u, s.toLong, t.toLong)
    maybeToken getOrElse UnAuthToken.empty
  } catch {
    case nfe: NumberFormatException => UnAuthToken.empty
  }
}

class RememberMe(store: TokenStore) extends Log {
  /**
   * @return the authenticated user, along with an optional cookie to include
   */
  def authenticateFromCookie(req: RequestHeader): Option[AuthResult] =
    authenticateToken(req) map (token => AuthResult(token.user, Some(cookify(token))))

  /**
   *
   * @return an authenticated token
   */
  def authenticateToken(req: RequestHeader): Option[Token] = authenticate(req).right.toOption

  def authenticate(req: RequestHeader): Either[AuthFailure, Token] =
    RememberMe.readToken(req).map(cookieAuth) getOrElse {
      log debug s"Found no token in request: ${req.cookies}"
      Left(CookieMissing)
    }

  def cookify(token: Token) = RememberMe.encodeAsCookie(token.asUnAuth)

  def persistNewCookie(loggedInUser: String): Cookie = cookify(createToken(loggedInUser))

  private def createToken(loggedInUser: String): Token = {
    val token = Token(loggedInUser, Random.nextLong(), Random.nextLong())
    store persist token
    token
  }

  private def cookieAuth(attempt: UnAuthToken): Either[AuthFailure, Token] = {
    log debug s"Authenticating: $attempt"
    val user = attempt.user
    store.findToken(user, attempt.series).map(savedToken => {
      if (savedToken.token == attempt.token) {
        /**
         * I believe the intention is to ensure that a browser cannot reuse another browser's token.
         *
         * The token is replaced with a new one at each successful token authentication, while the series remains the
         * same; this updated cookie is then sent to the browser. The series acts as a browser identifier. So, if
         * there's a token mismatch, it suggests some other actor has authenticated using this browser's token, which is
         * suspect.
         */
        log info s"Cookie authentication succeeded. Updating token."
        store remove savedToken
        val newToken = Token(user, attempt.series, Random.nextLong())
        store persist newToken
        Right(newToken)
      } else {
        log warn s"The saved token did not match the one from the request. Refusing access."
        store removeAll user
        Left(InvalidCookie)
      }
    }).getOrElse {
      log debug s"Unable to authenticate token: $attempt"
      Left(InvalidCredentials)
    }
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy