backend/app/utils/auth/providers/PanDomainUserProvider.scala (80 lines of code) (raw):

package utils.auth.providers import com.gu.pandomainauth.PanDomain import com.gu.pandomainauth.model._ import com.gu.pandomainauth.service.CryptoConf.Verification import model.frontend.TotpActivation import model.frontend.user.PartialUser import model.user.{DBUser, UserPermissions} import play.api.libs.json.{JsString, JsValue} import play.api.mvc.{AnyContent, Request} import services.users.UserManagement import services.{MetricsService, PandaAuthConfig} import utils.attempt.AttemptAwait._ import utils.attempt._ import utils.auth.totp.TfaToken import utils.{Epoch, Logging} import scala.concurrent.ExecutionContext import scala.concurrent.duration._ /** * A UserAuthenticator implementation that authenticates a valid user based on the presence of a pan-domain cookie */ class PanDomainUserProvider(val config: PandaAuthConfig, verificationProvider: () => Verification, users: UserManagement, metricsService: MetricsService)(implicit ec: ExecutionContext) extends UserProvider with Logging { /** The client needs to know where to redirect the user so they can pick up a pan domain cookie **/ override def clientConfig: Map[String, JsValue] = Map( "loginUrl" -> JsString(config.loginUrl) ) override def authenticate(request: Request[AnyContent], time: Epoch): Attempt[PartialUser] = { def validateUser(user: AuthenticatedUser): Boolean = { val passesMultifactor = if (config.require2FA) user.multiFactor else true val dbUser = users.getUser(user.user.email.toLowerCase()).awaitEither(10.seconds) dbUser.isRight && passesMultifactor } val maybeCookie = request.cookies.get(config.cookieName) maybeCookie match { case Some(cookieData) => val status = PanDomain.authStatus(cookieData.value, verificationProvider(), validateUser, 0L, "giant", false, false) status match { case Authenticated(authedUser) => val downcasedAuthedUser = authedUser.copy(user = authedUser.user.copy(email = authedUser.user.email.toLowerCase())) for { user <- users.getUser(downcasedAuthedUser.user.email) displayName = s"${downcasedAuthedUser.user.firstName} ${downcasedAuthedUser.user.lastName}" _ <- if (user.registered) Attempt.Right(user) else { users.registerUser(user.username, displayName, None, None) } } yield { metricsService.recordUsageEvent(user.username) PartialUser(user.username, user.displayName.getOrElse(displayName)) } case NotAuthorized(authedUser) => Attempt.Left(PanDomainCookieInvalid(s"User ${authedUser.user.email} is not authorised to use this system.", reportAsFailure = true)) case InvalidCookie(integrityFailure) => Attempt.Left(PanDomainCookieInvalid(s"Pan domain cookie invalid: $integrityFailure", reportAsFailure = true)) case Expired(authedUser) => Attempt.Left(PanDomainCookieInvalid(s"User ${authedUser.user.email} panda cookie has expired.", reportAsFailure = false)) case other => logger.warn(s"Pan domain auth failure: $other") Attempt.Left(AuthenticationFailure(s"Pan domain auth failed: $other", reportAsFailure = true)) } case None => Attempt.Left(PanDomainCookieInvalid(s"No pan domain cookie available in request with name ${config.cookieName}", reportAsFailure = false)) } } /** create an all powerful initial user **/ override def genesisUser(request: JsValue, time: Epoch): Attempt[PartialUser] = { for { email <- (request \ "username").validate[String].toAttempt user = DBUser(email, None, None, None, registered = false, None) createdUser <- users.createUser(user, UserPermissions.bigBoss) } yield createdUser.toPartial } /** create a new user account */ override def createUser(username: String, request: JsValue): Attempt[PartialUser] = { val user = DBUser(username, None, None, None, registered = false, None) for { // we mark this user as not registered so we can cache the display name when we see them createdUser <- users.createUser(user, UserPermissions.default) } yield createdUser.toPartial } /** delete and disable a user account **/ override def removeUser(username: String): Attempt[Unit] = { users.removeUser(username) } /** None of these make sense for a pan domain authed user so we return a failure **/ override def updatePassword(username: String, newPassword: String): Attempt[Unit] = unsupportedOperation override def generate2faToken(username: String, instance: String): Attempt[TfaToken] = unsupportedOperation override def registerUser(request: JsValue, time: Epoch): Attempt[Unit] = unsupportedOperation override def enrollUser2FA(username: String, totpActivation: TotpActivation, time: Epoch): Attempt[Unit] = unsupportedOperation override def removeUser2FA(username: String): Attempt[Unit] = unsupportedOperation def unsupportedOperation[T] = Attempt.Left[T](UnsupportedOperationFailure("This authentication provider is federated and doesn't support this operation.")) }