backend/app/utils/auth/AuthActionBuilder.scala (88 lines of code) (raw):

package utils.auth import java.time.{Clock, Instant, LocalDateTime, ZoneId, ZoneOffset} import pdi.jwt.JwtSession._ import play.api.Configuration import play.api.libs.json.{JsError, JsSuccess} import play.api.mvc.Security.AuthenticatedRequest import play.api.mvc._ import services.users.UserManagement import utils.Logging import utils.attempt.{Attempt, AuthenticationFailure, Failure} import utils.controller.FailureToResultMapper import scala.concurrent.duration.FiniteDuration import scala.concurrent.{ExecutionContext, Future} trait AuthActionBuilder extends ActionBuilder[UserIdentityRequest, AnyContent] { val controllerComponents: ControllerComponents final implicit val executionContext: ExecutionContext = controllerComponents.executionContext final val parser: BodyParser[AnyContent] = controllerComponents.parsers.default } class DefaultAuthActionBuilder(val controllerComponents: ControllerComponents, failureToResultMapper: FailureToResultMapper, maxLoginAge: FiniteDuration, maxVerificationAge: FiniteDuration, users: UserManagement)(implicit conf: Configuration, clock: Clock) extends AuthActionBuilder with Logging { final def invokeBlock[A](request: Request[A], block: UserIdentityRequest[A] => Future[Result]): Future[Result] = invokeBlockWithTime(request, block, System.currentTimeMillis()) map { case Right(result) => result case Left(err) => failureToResultMapper.failureToResult(err) } private[auth] def invokeBlockWithTime[A](request: Request[A], block: (UserIdentityRequest[A]) => Future[Result], now: Long): Future[Either[Failure, Result]] = { implicit val implicitReq = request val claimData = request.jwtSession.claimData val maybeToken = claimData.validate[Token] maybeToken match { case (JsSuccess(token, _)) if token.loginExpiry > now => val isVerificationExpired = token.verificationExpiry <= now for { maybeDbUser <- if (isVerificationExpired) getUser(token.user.username).asFuture else Future.successful(Left(AuthenticationFailure("Verification hasn't expired", reportAsFailure = true))) result <- block(new AuthenticatedRequest(token.user, request)) } yield { if (isVerificationExpired) { maybeDbUser match { case Right(user) if user.invalidationTime.exists(token.issuedAt < _) => // The user has logged out val msg = s"Authenticated failed because token was issued before database invalidation time" logger.warn(token.user.asLogMarker, msg) Left(AuthenticationFailure(msg, reportAsFailure = true)) case Left(failure) => logger.error(token.user.asLogMarker, "Authentication failed because user was not found in DB", failure.toThrowable) Left(failure) case Right(_) => val verificationExpiry = now + maxVerificationAge.toMillis val expiryDateTime = LocalDateTime.ofInstant( Instant.ofEpochMilli(verificationExpiry), ZoneId.systemDefault() ) logger.info(token.user.asLogMarker, s"Authentication succeeded, verification expired but token renewed. New verification expiry: ${expiryDateTime}") Right(result .refreshJwtSession .addingToJwtSession(Token.VERIFICATION_EXPIRY_KEY, verificationExpiry) .addingToJwtSession(Token.REFRESHED_AT_KEY, now) ) } } else { Right(result .refreshJwtSession .addingToJwtSession(Token.REFRESHED_AT_KEY, now) ) } } case JsSuccess(token, _) => { val msg = s"Token is older than $maxLoginAge" logger.info(token.user.asLogMarker, msg) Future.successful(Left(AuthenticationFailure(msg, reportAsFailure = false))) } case JsError(errors) => { val msg = s"Failed to parse token: $errors" logger.warn(msg) // This error happens whenever the token is missing, // as a result of vulnerability scanners and occasionally developer testing/debugging // (e.g. just opening https://giant.pfi.gutools.co.uk/api/search in your browser will fire this alarm). // For this reason we don't want to get an alarm. Future.successful(Left(AuthenticationFailure(msg, reportAsFailure = false))) } } } private def getUser(username: String): Attempt[model.user.DBUser] = users.getUser(username).flatMap { u => if(!u.registered) { Attempt.Left(AuthenticationFailure("User not registered", reportAsFailure = true)) } else { Attempt.Right(u) } } }