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

lucuma.ui.sso.SSOClient.scala Maven / Gradle / Ivy

There is a newer version: 0.123.0
Show newest version
// Copyright (c) 2016-2023 Association of Universities for Research in Astronomy, Inc. (AURA)
// For license information see LICENSE or https://opensource.org/licenses/BSD-3-Clause

package lucuma.ui.sso

import cats.Applicative
import cats.effect.*
import cats.implicits.*
import eu.timepit.refined.*
import eu.timepit.refined.collection.NonEmpty
import io.circe.Decoder
import io.circe.parser.*
import lucuma.core.model.StandardRole
import lucuma.core.model.User
import lucuma.sso.client.codec.user.*
import lucuma.ui.utils.RetryHelpers.*
import org.http4s.*
import org.http4s.dom.FetchClientBuilder
import org.scalajs.dom.RequestCredentials
import org.scalajs.dom.window
import org.typelevel.log4cats.Logger
import retry.*

import java.time.Instant
import java.util as ju

case class JwtOrcidProfile(exp: Long, `lucuma-user`: User) derives Decoder

case class SSOClient[F[_]: Async: Logger](config: SSOConfig):
  private val client = FetchClientBuilder[F]
    .withRequestTimeout(config.readTimeout)
    .withCredentials(RequestCredentials.include)
    .resource

  // Does a client side redirect to the sso site
  val redirectToLogin: F[Unit] =
    Sync[F].delay {
      val returnUrl = window.location
      window.location.href =
        (config.uri / "auth" / "v1" / "stage1").withQueryParam("state", returnUrl.toString).toString
    }

  val guest: F[UserVault] =
    retryingOnAllErrors(retryPolicy[F], logError[F]("Switching to guest")) {
      client.use(
        _.expect[String](Request[F](Method.POST, config.uri / "api" / "v1" / "auth-as-guest"))
          .map(body =>
            (for {
              k <- Either.catchNonFatal(
                     ju.Base64.getDecoder.decode(body.split('.')(1).replace("-", "+"))
                   )
              j  = new String(k)
              p <- parse(j)
              u <- p.as[JwtOrcidProfile]
              t <- refineV[NonEmpty](body)
            } yield UserVault(u.`lucuma-user`, Instant.ofEpochSecond(u.exp), t))
              .getOrElse(throw new RuntimeException("Error decoding the token"))
          )
      )
    }

  val whoami: F[Option[UserVault]] =
    retryingOnAllErrors(retryPolicy[F], logError[F]("Calling whoami")) {
      client
        .flatMap(_.run(Request[F](Method.POST, config.uri / "api" / "v1" / "refresh-token")))
        .use {
          case Status.Successful(r) =>
            r.attemptAs[String]
              .leftMap(_.message)
              .value
              .map(
                _.flatMap(body =>
                  for {
                    k <- Either
                           .catchNonFatal(
                             ju.Base64.getDecoder.decode(body.split('.')(1).replace("-", "+"))
                           )
                           .leftMap(_.getMessage)
                    j  = new String(k)
                    p <- parse(j).leftMap(_.message)
                    u <- p.as[JwtOrcidProfile].leftMap(_.message)
                    t <- refineV[NonEmpty](body)
                  } yield UserVault(u.`lucuma-user`, Instant.ofEpochSecond(u.exp), t)
                ).fold(msg => throw new RuntimeException(s"Error decoding the token: $msg"), _.some)
              )
          case _                    =>
            Applicative[F].pure(none[UserVault])
        }
    }
      .adaptError { case t =>
        new Exception("Error connecting to authentication server.", t)
      }

  def switchRole(roleId: StandardRole.Id): F[Option[UserVault]] =
    retryingOnAllErrors(retryPolicy[F], logError[F]("Switching role")) {
      client
        .flatMap(
          _.run(
            Request(Method.GET,
                    (config.uri / "auth" / "v1" / "set-role").withQueryParam("role", roleId.show)
            )
          )
        )
        .use_ *> whoami

    }

  val logout: F[Unit] =
    retryingOnAllErrors(retryPolicy[F], logError[F]("Calling logout")) {
      client.flatMap(_.run(Request(Method.POST, config.uri / "api" / "v1" / "logout"))).use_
    }

  val switchToORCID: F[Unit] =
    logout.attempt >> redirectToLogin




© 2015 - 2024 Weber Informatics LLC | Privacy Policy